Examples of custom rules

Example of a simple boolean rule

You can find a full example of this guide in this OPA Playground and the snyk/custom-rules-example repository.
Let’s assume we have generated a new rule CUSTOM-RULE-1 using the SDK (i.e. snyk-iac-rules template --rule CUSTOM-RULE-1) and have a very simple fixture file containing a Terraform resource:
rules/CUSTOM-RULE-1/fixtures/denied.tf
1
resource "aws_redshift_cluster" "denied" {
2
cluster_identifier = "tf-redshift-cluster"
3
node_type = "dc1.large"
4
tags = {
5
}
6
}
Copied!
Now, we want to modify the generated Rego to enforce resources tagged with an owner:
  1. 1.
    Create a variable [name] to to enumerate across all of the aws_redshift_cluster resources. This variable can be named anything you like (e.g. i, j, name, etc.).
  2. 2.
    Store this into the resource variable by assigning the value to it with a walrus operator :=; e.g. resource := input.resource.aws_redshift_cluster[name]
  3. 3.
    Check if the owner tag exists for each resource; to do that, check if the path resource.tags.owner is defined. If it is undefined, it will evaluate to undefined. So, use the NOT keyword in front of it, which will evaluate to TRUE; e.g.not resource.tags.owner
The modified Rego is:
rules/CUSTOM-RULE-1/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": "CUSTOM-RULE-1",
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!
To understand how the Rego code evaluates the Terraform file provided earlier, have a look at how the SDK is able to parse a fixture file into JSON.
We recommend always validating that your rule is correct by updating and running the unit tests.
The test for this rule verifies that the Rego rule is able to identify that the fixture at the beginning of this guide is invalid:
rules/CUSTOM-RULE-1/main_test.rego
1
package rules
2
3
import data.lib
4
import data.lib.testing
5
6
test_CUSTOM_RULE_1 {
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.aws_redshift_cluster[denied].tags"],
16
"fixture": "denied.tf",
17
}]
18
19
test_cases := array.concat(allowed_test_cases, denied_test_cases)
20
testing.evaluate_test_cases("CUSTOM-RULE-1", "./rules/CUSTOM-RULE-1/fixtures", test_cases)
21
}
Copied!

Example with logical AND

Let’s try and extend the example above and update the rule to allow all cases that suffice two conditions:
  1. 1.
    A resource has an “owner” tag AND
  2. 2.
    A resource has a “description” tag
To test this new condition, we generate a new rule CUSTOM-RULE-2 using the template command and write the following fixture file:
rules/CUSTOM-RULE-2/fixtures/denied.tf
1
resource "aws_redshift_cluster" "denied" {
2
cluster_identifier = "tf-redshift-cluster"
3
node_type = "dc1.large"
4
tags = {
5
owner = "team-123"
6
}
7
}
Copied!
Joining multiple expressions together expresses logical AND.
  • You can do this with the ; operator.
  • Or, you can omit the ; (AND) operator by splitting expressions across multiple lines.
The logical AND is covered also in the OPA documentation.
rules/CUSTOM-RULE-2/main.rego
1
package rules
2
3
aws_redshift_cluster_tags_present(resource) {
4
resource.tags.owner
5
resource.tags.description
6
}
7
8
deny[msg] {
9
resource := input.resource.aws_redshift_cluster[name]
10
not aws_redshift_cluster_tags_present(resource)
11
msg := {
12
"publicId": "CUSTOM-RULE-2",
13
"title": "Missing a description and an owner from the tag",
14
"severity": "medium",
15
"msg": sprintf("input.resource.aws_redshift_cluster[%s].tags", [name]),
16
"issue": "",
17
"impact": "",
18
"remediation": "",
19
"references": [],
20
}
21
}
Copied!
We recommend always validating that your rule is correct by updating and running the unit tests.
The test for this rule will look the same as the one for CUSTOM-RULE-1, but the name of the test and the first two arguments passed to the testing.evaluate_test_cases function will differ:
rules/CUSTOM-RULE-2/main_test.rego
1
package rules
2
3
import data.lib
4
import data.lib.testing
5
6
test_CUSTOM_RULE_2 {
7
# array containing test cases where the rule is allowed
8
allowed_test_cases := [{
9
"want_msgs": [],
10
"fixture": "allowed.tf",
11
}]
12
# array containing cases where the rule is denied
13
denied_test_cases := [{
14
"want_msgs": ["input.resource.aws_redshift_cluster[denied].tags"],
15
"fixture": "denied.tf",
16
}]
17
test_cases := array.concat(allowed_test_cases, denied_test_cases)
18
testing.evaluate_test_cases("CUSTOM-RULE-2", "./rules/CUSTOM-RULE-2/fixtures", test_cases)
19
}
Copied!

Example with logical OR

We can also rewrite the rule above by combining the NOT operator with the OR functionality.
Let’s update the example in a new rule CUSTOM-RULE-3, to deny all cases that fail either of the two conditions; we want to deny all aws_redshift_cluster resources that are missing either:
  1. 1.
    an “owner” tag , OR
  2. 2.
    A “description” tag
For this, we will use two new fixture files, one for each case:
rules/CUSTOM-RULE-3/fixtures/denied1.tf
1
resource "aws_redshift_cluster" "denied1" {
2
cluster_identifier = "tf-redshift-cluster"
3
node_type = "dc1.large"
4
tags = {
5
6
}
7
}
Copied!
rules/CUSTOM-RULE-3/fixtures/denied2.tg
1
resource "aws_redshift_cluster" "denied2" {
2
cluster_identifier = "tf-redshift-cluster"
3
node_type = "dc1.large"
4
tags = {
5
description = "description",
6
}
7
}
Copied!
To express logical OR in Rego, we can define multiple rules or functions with the same name. This is also described in the OPA documentation for Logical OR.
First, we will add a function that will implement the NOT for each tag. Then, we will call this function with the resource:
rules/CUSTOM-RULE-3/main.rego
1
package rules
2
3
aws_redshift_cluster_tags_missing(resource) {
4
not resource.tags.owner
5
}
6
7
aws_redshift_cluster_tags_missing(resource) {
8
not resource.tags.description
9
}
10
11
deny[msg] {
12
resource := input.resource.aws_redshift_cluster[name]
13
aws_redshift_cluster_tags_missing(resource)
14
msg := {
15
"publicId": "CUSTOM-RULE-3",
16
"title": "Missing a description or an owner from the tag",
17
"severity": "medium",
18
"msg": sprintf("input.resource.aws_redshift_cluster[%s].tags", [name]),
19
"issue": "",
20
"impact": "",
21
"remediation": "",
22
"references": [],
23
}
24
}
Copied!
This will successfully return all the rules that deny.
We recommend always validating that your rule is correct by updating and running the unit tests.
The test for this rule will now contain multiple test cases, to show that the logical OR works as expected:
rules/CUSTOM-RULE-3/main_test.rego
1
package rules
2
3
import data.lib
4
import data.lib.testing
5
6
test_CUSTOM_RULE_3 {
7
# array containing test cases where the rule is allowed
8
allowed_test_cases := [{
9
"want_msgs": [],
10
"fixture": "allowed.tf",
11
}]
12
# array containing cases where the rule is denied
13
denied_test_cases := [{
14
"want_msgs": ["input.resource.aws_redshift_cluster[denied1].tags"],
15
"fixture": "denied1.tf",
16
},{
17
"want_msgs": ["input.resource.aws_redshift_cluster[denied2].tags"],
18
"fixture": "denied2.tf",
19
}]
20
test_cases := array.concat(allowed_test_cases, denied_test_cases)
21
testing.evaluate_test_cases("CUSTOM-RULE-3", "./rules/CUSTOM-RULE-3/fixtures", test_cases)
22
}
Copied!

Example with strings

Let’s extend this further and add a third condition. Deny all resources that are missing either:
  1. 1.
    An “owner” tag , OR
  2. 2.
    A “description” tag, OR
  3. 3.
    The email of the owner does not belong to the “@corp-domain.com” domain
rules/CUSTOM-RULE-4/main.rego
1
package rules
2
3
aws_redshift_cluster_tags_missing(resource) {
4
not resource.tags.owner
5
}
6
7
aws_redshift_cluster_tags_missing(resource) {
8
not resource.tags.description
9
}
10
11
aws_redshift_cluster_tags_missing(resource) {
12
not endswith(resource.tags.owner, "@corp-domain.com")
13
}
14
15
deny[msg] {
16
resource := input.resource.aws_redshift_cluster[name]
17
aws_redshift_cluster_tags_missing(resource)
18
msg := {
19
"publicId": "CUSTOM-RULE-4",
20
"title": "Missing a description and an owner from tag, or owner tag does not comply with email requirements",
21
"severity": "medium",
22
"msg": sprintf("input.resource.aws_redshift_cluster[%s].tags", [name]),
23
"issue": "",
24
"impact": "",
25
"remediation": "",
26
"references": [],
27
}
28
}
Copied!
We recommend always validating that your rule is correct by updating and running the unit tests.
The test for this rule will look very similar to the ones from previous example and will also require its own fixture file.

Example with XOR

Now let’s say that we want to add more complexity and check the following:
  • If the tag type is a “user”, then we want the tag “email” to exist as well.
  • If not (assume the other type is a “service”), we want it to have a serviceDescription.
  • These two will be mutually exclusive; if the first condition applies, the second one shouldn’t, and vice versa.
Type
Email
ServiceDescription
User
YES
NO
Service
NO
YES
To do this, we are going to refactor our code to use a checkTags helper function. This can check if there are any tags, but also check for the two conditions above with an OR.
rules/CUSTOM-RULE-5/main.rego
1
package rules
2
3
checkTags(resource){
4
resource.tags.type == "user"
5
not resource.tags.email
6
}
7
8
checkTags(resource){
9
resource.tags.type == "service"
10
not resource.tags.serviceDescription
11
}
12
13
checkTags(resource){
14
count(resource.tags) == 0
15
}
16
17
deny[msg] {
18
resource := input.resource.aws_redshift_cluster[name]
19
checkTags(resource)
20
21
msg := {
22
"publicId": "CUSTOM-RULE-5",
23
"title": "Complex rule",
24
"severity": "medium",
25
"msg": sprintf("input.resource.aws_redshift_cluster[%v].tags", [name]),
26
"issue": "",
27
"impact": "",
28
"remediation": "",
29
"references": [],
30
}
31
}
Copied!
To convert this to an XOR we can use an else rule:
rules/CUSTOM-RULE-5/main.rego
1
package rules
2
3
checkUserTag(resource){
4
not resource.tags.email
5
}
6
7
checkUserTag(resource){
8
resource.tags.serviceDescription
9
}
10
11
checkServiceTag(resource){
12
not resource.tags.serviceDescription
13
}
14
15
checkServiceTag(resource){
16
resource.tags.email
17
}
18
19
checkTags(resource){
20
count(resource.tags) == 0
21
}
22
23
checkTags(resource) {
24
resource.tags.type == "user"
25
checkUserTag(resource)
26
} else {
27
resource.tags.type == "service"
28
checkServiceTag(resource)
29
}
30
31
deny[msg] {
32
resource := input.resource.aws_redshift_cluster[name]
33
checkTags(resource)
34
msg := {
35
"publicId": "CUSTOM-RULE-5",
36
"title": "Missing the right tags from for a resource of type user or service",
37
"severity": "medium",
38
"msg": sprintf("input.resource.aws_redshift_cluster[%v].tags", [name]),
39
"issue": "",
40
"impact": "",
41
"remediation": "",
42
"references": [],
43
}
44
}
Copied!
If you want to try it out yourselves, we have provided the same example in an OPA Playground.
We recommend always validating that your rule is correct by updating and running the unit tests.
The test for this rule will look very similar to the ones from previous example and will also require its own fixture file.

Examples with grouped resources

We can also iterate over many resources by adding them to an array of resources.
1
"resources": [
2
"aws_iam_policy",
3
"aws_iam_group_policy",
4
"aws_iam_user_policy",
5
"aws_iam_role_policy",
6
"data.aws_iam_policy_document",
7
]
Copied!
One way to leverage this is to implement denylist rules.
For example, we may want to ensure that if someone defines a Kubernetes ConfigMap, then they cannot use it to store sensitive information such as passwords, secret keys, and access tokens.
We can do that and expand what we define as "sensitive information" over time by defining a group of sensitive tokens inside a denylist:
1
package rules
2
3
sensitive_denylist := [
4
"pass",
5
"secret",
6
"key",
7
"token",
8
]
9
10
check_sensitive(keys, denylist) {
11
_ = keys[key]
12
contains(key, denylist[_])
13
}
14
15
deny[msg] {
16
input.kind == "ConfigMap"
17
input.data = keys
18
check_sensitive(keys, sensitive_denylist)
19
msg := {
20
"publicId": "CUSTOM-RULE-7",
21
"title": "ConfigMap exposes sensitive data",
22
"severity": "high",
23
"msg": "input.data",
24
"issue": "",
25
"impact": "",
26
"remediation": "",
27
"references": [],
28
}
29
}
Copied!
Any key containing the substrings "pass", "secret", "key", and "token" can be considered sensitive and so should not be included in the ConfigMap.

Last modified 7d ago