Skip to content

Writing Rules⚓︎

Custom rule development generally has four parts:

Types of rules⚓︎

Tip

For a tutorial on writing a simple custom rule, see Example: Writing a Simple Rule.

Regula rules are written in Rego and use the same format as Fugue Custom Rules. This means there are (currently) two kinds of rules: simple rules and advanced rules.

Simple rules⚓︎

Simple rules are useful when the policy applies to a single resource type only, and you want to make simple yes/no decision.

# Rules must always be located right below the `rules` package.
package rules.my_simple_rule

# Simple rules must specify the resource type they will police.
resource_type = "aws_ebs_volume"

# Simple rules must specify `allow` or `deny`.  For this example, we use
# an `allow` rule to check that the EBS volume is encrypted.
default allow = false
allow {
  input.encrypted == true
}

Custom error messages and attributes (simple rules)⚓︎

If you want to return more information to the user, you can also define a custom error message in a simple rule. This is done by writing a deny[msg] style rule.

package rules.simple_rule_custom_message
resource_type = "aws_ebs_volume"

deny[msg] {
  not input.encrypted
  msg = "EBS volumes should be encrypted"
}

Advanced rules⚓︎

Advanced rules are harder to write, but more powerful. They allow you to observe different kinds of resource types and decide which specific resources are valid or invalid.

# Rules still must be located in the `rules` package.
package rules.user_attached_policy

# Advanced rules typically use functions from the `fugue` library.
import data.fugue

# We mark an advanced rule by setting `resource_type` to `MULTIPLE`.
resource_type = "MULTIPLE"

# `fugue.resources` is a function that allows querying for resources of a
# specific type.  In our case, we are just going to ask for the EBS volumes
# again.
ebs_volumes = fugue.resources("aws_ebs_volume")

# Auxiliary function.
is_encrypted(resource) {
  resource.encrypted == true
}

# Regula expects advanced rules to contain a `policy` rule that holds a set
# of _judgements_.
policy[p] {
  resource = ebs_volumes[_]
  is_encrypted(resource)
  p = fugue.allow_resource(resource)
} {
  resource = ebs_volumes[_]
  not is_encrypted(resource)
  p = fugue.deny_resource(resource)
}

The fugue API consists of these functions for advanced rules:

  • fugue.resources(resource_type) returns an object with all resources of the requested type. fugue.input_resource_types is a set with all resource types in the input document.
  • fugue.allow_resource(resource) marks a resource as valid.
  • fugue.deny_resource(resource) marks a resource as invalid.
  • fugue.deny_resource_with_message(resource, msg) marks a resource as invalid and displays a custom rule_message in the report.
  • fugue.missing_resource(resource_type) marks a resource as missing. This is useful if you for example require a log group to be present.
  • fugue.missing_resource_with_message(resource_type, msg) marks a resource as missing and displays a custom rule_message in the report.

Custom error messages (advanced rules)⚓︎

As stated above, the functions fugue.deny_resource_with_message(resource, msg) and fugue.missing_resource_with_message(resource_type, msg) allow Regula to display a custom rule_message in its report. This rule demonstrates both functions:

package rules.account_password_policy

import data.fugue

resource_type = "MULTIPLE"

password_policies = fugue.resources("aws_iam_account_password_policy")

policy[r] {
  password_policy = password_policies[_]
  password_policy.minimum_password_length >= 16
  r = fugue.allow_resource(password_policy)
} {
  password_policy = password_policies[_]
  not password_policy.minimum_password_length >= 16
  msg = "Password policy is too short. It must be at least 16 characters."
  r = fugue.deny_resource_with_message(password_policy, msg)
} {
  count(password_policies) == 0
  msg = "No password policy exists."
  r = fugue.missing_resource_with_message("aws_iam_account_password_policy", msg)
}

Here's an example rule result demonstrating a missing resource message:

    {
      "controls": [
        "CORPORATE-POLICY_1.1"
      ],
      "filepath": "main.tf",
      "input_type": "tf",
      "provider": "",
      "resource_id": "",
      "resource_type": "aws_iam_account_password_policy",
      "rule_description": "Per company policy, an AWS account must have a password policy, and it must require a minimum of 16 characters",
      "rule_id": "CUSTOM_0001",
      "rule_message": "No password policy exists.",
      "rule_name": "account_password_policy",
      "rule_raw_result": false,
      "rule_result": "FAIL",
      "rule_severity": "Medium",
      "rule_summary": "An AWS account must have a password policy requiring a minimum of 16 characters"
    },

Adding rule metadata⚓︎

You can add metadata to a rule to enhance Regula's report:

__rego__metadoc__ := {
  "id": "CUSTOM_0001",
  "title": "IAM policies must have a description of at least 25 characters",
  "description": "Per company policy, it is required for all IAM policies to have a description of at least 25 characters.",
  "custom": {
    "controls": {
      "CORPORATE-POLICY": [
        "CORPORATE-POLICY_1.1"
      ]
    },
    "severity": "Low",
    "rule_remediation_doc": "https://example.com"
  }
}

Regula supports the following metadata properties:

  • id: Rule ID
  • title: Short summary of the rule
  • description: Longer description of the rule
  • controls: An object where the key is the compliance family name and the value is an array of controls
  • severity: One of Critical, High, Medium, Low, Informational
  • rule_remediation_doc: A URL with instructions for remediating the rule

Here's an example rule result to show how this metadata looks in the report:

    {
      "controls": [
        "CORPORATE-POLICY_1.1"
      ],
      "families": [
        "CORPORATE-POLICY"
      ],
      "filepath": "../regula-ci-example/infra_tf/main.tf",
      "input_type": "tf",
      "provider": "aws",
      "resource_id": "aws_iam_policy.basically_allow_all",
      "resource_type": "aws_iam_policy",
      "resource_tags": {},
      "rule_description": "Per company policy, it is required for all IAM policies to have a description of at least 25 characters.",
      "rule_id": "CUSTOM_0001",
      "rule_message": "",
      "rule_name": "long_description",
      "rule_raw_result": false,
      "rule_result": "FAIL",
      "rule_severity": "Low",
      "rule_summary": "IAM policies must have a description of at least 25 characters",
      "source_location": [
        {
          "path": "../regula-ci-example/infra_tf/main.tf",
          "line": 6,
          "column": 1
        }
      ]
    }

CloudFormation vs. Terraform vs. Kubernetes vs. ARM rules⚓︎

CloudFormation rules are written the same way Terraform rules are, but require the line input_type := "cfn", as shown in the simple rule below:

package rules.cfn_ebs_volume_encryption

input_type := "cfn"

resource_type := "AWS::EC2::Volume"

default allow = false

allow {
  input.Encrypted == true
}

Kubernetes rules require the line input_type := "k8s", as shown in the simple rule below:

package rules.k8s_job_check

__rego__metadoc__ := {
    "id": "K8S_TEST_0123",
    "custom": {"severity": "Low"},
    "title": "Job containers should not be named `test`",
}

input_type := "k8s"

resource_type := "Job"

default deny = false

deny {
    input.spec.template.spec.containers[_].name == "test"
}

ARM rules (in preview) require the line input_type := "arm", as shown in the simple rule below:

package rules.arm_postgresql_tags

__rego__metadoc__ := {
    "id": "ARM_POSTGRESQL_001",
    "custom": {"severity": "Low"},
    "title": "Azure PostgreSQL servers should be tagged 'application:db'",
}

input_type := "arm"

resource_type = "Microsoft.DBforPostgreSQL/servers"

default allow = false

allow {
    input.tags.application == "db"
}

Terraform rules do not require input_type to be explicitly set.

Additionally, the resource_type is specified differently depending on the input type: