Writing a rule

Rules in Rego

Rules are written in Rego. When you are writing Rego, you do two things:
  1. 1.
    Write rules that make policy decisions. A rule is a conditional assignment.
  2. 2.
    organize rules into policies. A policy is a set of rules with a hierarchical name
To learn more about the Policy Language, visit the official OPA Policy Language Documentation Page.
You can also use the OPA Playground to try out Rego, or run examples of this guide.

How to generate a new rule

There are two options to get started:
  1. 1.
    Use the template command to generate the required files for writing a rule:
    1
    snyk-iac-rules template --rule <RULE-NAME> --format <hcl2|json|yaml|tf-plan>
    Copied!
    This generates the scaffolding for the rule, including fixture files based on the provided configuration format. For more details, read the documentation about the template command.
  2. 2.
    Create a Rego policy from scratch and match the expected file and folder structure on your own: rules └── my_rule ├── main.rego └── main_test.rego
You will have to write your own Rego testing framework if you don't use the templatecommand.

Structure of the rule

In Rego, you can write statements that allow or deny a request, such as: allow { input.name == "alice" } or deny { input.name == "alice" }
If the template command was used to generate the rules, then the default entry point is rules/deny. To override it and use a different name than deny, check the section Bundling Rules.
This is what a generated skeleton of a deny rule looks like when we run snyk-iac-rules template --rule new-rule --format hcl2:
rules/new-rule/main.rego
1
package rules
2
3
deny[msg] {
4
resource := input.resource.test[name]
5
resource.todo
6
msg := {
7
# Mandatory fields
8
"publicId": "new-rule",
9
"title": "Default title",
10
"severity": "low",
11
"msg": sprintf("input.resource.test[%s].todo", [name]),
12
# Optional fields
13
"issue": "",
14
"impact": "",
15
"remediation": "",
16
"references": [],
17
}
18
}
Copied!
You must follow this format of the msg property to ensure the output correctly displays on the Snyk IaC CLI.
The attributes are:
  • publicId: a naming convention unique to yourselves, such as COMPANY-001. This should not contain/start with “SNYK-” to differentiate from the internal Snyk rules.
  • title: a short title that should summarise the issue.
  • severity: this can be one of low/medium/high/critical.
  • msg: we recommend only changing the resource name and property e.g. input.aws_s3_bucket[%s].tags to match your example. The function sprintf is provided by Rego and enables us to provide a dynamic error message explaining exactly where the issue was found.
The following attributes are optional but can be used to enhance the scan results in the Snyk CLI:
  • issue: a more detailed string explanation of what the exact issue is.
  • impact: a more detailed string explanation of what the impact of not resolving this issue is.
  • remediation: a more detailed string explanation of how to resolve the issue. We recommend providing a code snippet here.
  • references: you can provide an array of strings with URLs to documentation
The generated test for the rule uses two generated Terraform files to verify if the correct msg field is returned by the rule for allowed and denied fixtures:
1
package rules
2
3
import data.lib
4
import data.lib.testing
5
6
test_new_ruleryle {
7
# array containing test cases where the rule is allowed
8
allowed_test_cases := [{
9
"want_msgs": [],
10
"fixture": "allowed.tf",
11
}]
12
13
# array containing cases where the rule is denied
14
denied_test_cases := [{
15
"want_msgs": ["input.resource.test[denied].todo"], # verifies that the correct msg is returned by the denied rule
16
"fixture": "denied.tf",
17
}]
18
19
test_cases := array.concat(allowed_test_cases, denied_test_cases)
20
testing.evaluate_test_cases("new-rule", "./rules/new-rule/fixtures", test_cases)
21
}
Copied!

Example of a rule

For more examples, see Custom Rules Examples.
For this example, we modified our templated rule to assign a msg when a resource does not have an owner tag:
rules/my_rule/main.rego
1
package rules
2
3
deny[msg] {
4
resource := input.resource.aws_redshift_cluster[name]
5
not resource.tags.owner
6
7
msg := {
8
"publicId": "my_rule",
9
"title": "Missing an owner from tag",
10
"severity": "medium",
11
"msg": sprintf("input.resource.aws_redshift_cluster[%s].tags", [name]),
12
"issue": "",
13
"impact": "",
14
"remediation": "",
15
"references": [],
16
}
17
}
Copied!

Limitations/Notes

  • As we compile Rego policies into Wasm modules, you can only use built-in functions that support Wasm. There is a table at the bottom of the Policy Reference Documentation that can help you identify those.
  • A rule may be defined multiple times with the same name, either in a file, or in separate files under the same package, e.g:
1
packages rules
2
3
deny[msg] {
4
resource.this
5
}
6
...
7
8
deny[msg] {
9
resource.that
10
}
11
...
Copied!
These rules are referred as incremental as each definition is additive. You can read more about Incremental Definitions here. Note that these same named rules have to return a different value, or OPA will return an error. You can read more about Complete Definitions here.
For more complex topics, check how OPA resolves Conflict Resolution.