Simple Custom Rules

Note

A simple custom rule tests a single resource type. It evaluates every resource of a single resource type, and only in the context of that single resource – not the context of an environment.

Here’s a simple rule written for AWS runtime and Terraform repository environments. It checks whether an S3 bucket has a stage:prod tag:

package rules.buckettags

__rego__metadoc__ := {
  "title": "AWS S3 buckets must be tagged",
  "description": "S3 buckets must have the tag key 'stage' and value 'prod'",
  "custom": {
    "providers": ["AWS", "Repository"],
    "severity": "Medium"
  }
}

input_type = "tf"

resource_type = "aws_s3_bucket"

default allow = false

allow {
  input.tags.stage == "prod"
}

To create a simple custom rule, you write Rego code defining a pass (allow) or fail (deny) condition. Then, you validate and add the rule to Fugue from the Custom Rules page (UI), CLI, or API.

To write a custom rule, you must:

  1. Determine the environment provider(s) the rule should apply to

  2. Determine the resource type(s)

  3. Determine the input type

  4. Determine the resource attribute(s) you want Fugue to check

  5. Determine whether to write a simple or advanced rule

  6. Define pass/fail conditions for the resource

  7. Write metadata

You can write custom rules using Rego, an open source query language for Open Policy Agent (OPA).

We won’t detail how to write Rego here, but we encourage you to consult the Rego documentation for the basics.

Step 1: Determine provider(s)

Once you have a policy in mind that your resources should comply with, you can start writing a custom rule.

To translate policy to code, you’ll first need to determine the provider(s) involved.

Which provider is your rule for? (Reference)

  • Runtime environments:

    • AWS

    • AWS_GOVCLOUD

    • AZURE (includes Azure Government)

    • GOOGLE

  • Infrastructure as code (IaC) environments:

    • REPOSITORY, containing one of the following IaC input types:

      • Terraform for AWS/AWS GovCloud, Azure, or Google

      • CloudFormation for AWS and AWS GovCloud

      • Kubernetes

You can also write a simple rule for multiple providers by combining a Terraform (HCL and/or JSON plan) Repository environment with a single corresponding runtime provider. For instance:

  • Terraform for AWS/AWS Govcloud (REPOSITORY) + AWS

  • Terraform for Google (REPOSITORY) + GOOGLE

  • Terraform for Azure/Azure Government (REPOSITORY) + AZURE

Note

CloudFormation cannot be combined with AWS/AWS GovCloud runtime.

In this example, our rule is for AWS runtime and Terraform (HCL and plan) repository environments.

Declaring the provider

There are different ways you can declare the provider(s) for a rule, depending on the interface chosen:

  • UI: In the “Providers” field

  • CLI fugue create rule command: In the --provider flag (single runtime providers only)

  • CLI fugue sync rules command: In the providers metadata property of the Rego code, which accepts a list of strings

  • API: In the providers request body parameter

Our example rule is meant for AWS and repository environments, and we intend to upload it using the fugue sync rules CLI command, so we’ll include this line in metadata when we get to step 7:

"providers": ["AWS", "Repository"]

Step 2: Determine the resource type

Which resource type is your rule for? Specify the resource type according to the provider. (Reference)

Declaring the resource type

There are different ways you can declare the resource type in a rule:

  • Define in code. Required for:

    • Rules for REPOSITORY environments

    • Rules involving multiple providers of any type

    • Rules uploaded to Fugue via fugue sync rules

  • Define externally. Required for:

    • Rules for single, runtime-only providers (for example, just AWS, or just GOOGLE) not uploaded with fugue sync rules

Rules that must define the resource type in code require the resource_type declaration. For example:

resource_type = "google_storage_bucket"

If your rule is for a single, runtime-only provider, and you’re not uploading it to Fugue with fugue sync rules, you can declare the resource type according to the interface you use:

  • UI: In the “Resource Type” field

  • CLI fugue create rule command: In the --resource-type flag

  • API: In the resource_type request body parameter

Our example rule is meant for AWS and AWS repository (Terraform) environments, so we’ll include the resource_type declaration in the Rego code, and we’ll use the Terraform name for the resource type:

resource_type = "aws_s3_bucket"

Step 3: Determine the input type

Repository (and repository + runtime) rules require an input_type declaration, unless you’re writing a rule for Terraform, which is the default. For example:

input_type = "k8s"

Runtime-only rules do not require an input_type declaration.

What input type does your rule involve?

  • Terraform HCL and JSON plans: tf (default value)

  • Terraform JSON plans only: tf_plan

  • CloudFormation: cfn

  • Kubernetes: k8s

Input types cannot be combined; you cannot write a rule for both CloudFormation and Terraform, for instance.

Our example rule is for Terraform HCL and plans as well as AWS runtime resources, so setting the input type to tf is optional since it’s the default value. For completeness, we’ve included the declaration anyway:

input_type = "tf"

Step 4: Determine resource attribute(s) to check

If you have an environment (or create a new one) that contains the resource type you’re evaluating, you can determine which attributes you need to specify in the rule by using the CLI or API to retrieve the results of a recent scan, known as the “test input”.

Once you have the JSON document containing the test input, you’ll see the configuration for every resource in the scan. You can then find the attribute in question.

For instance, in this example we want to check an S3 bucket for stage:prod tags, and the test input for a scan of an AWS runtime environment has this information:

{
    "resources": {
        "aws_s3_bucket.ZnVndWUtbGFy": {
            "_provider": "provider.aws.us-east-1",
            "_skeleton": {
                "depends_on": null,
                "deposed": null,
                "primary": {
                    "id": "example-bucket",
                    "meta": null,
                    "tainted": false
                },
                "provider": "provider.aws.us-east-1",
                "type": "aws_s3_bucket"
            },
            "_type": "aws_s3_bucket",
            "acl": "private",
            "arn": "arn:aws:s3:::example-bucket",
            "bucket": "example-bucket",
            "bucket_domain_name": "example-bucket.s3.amazonaws.com",
            "bucket_regional_domain_name": "example-bucket.s3.amazonaws.com",
            "cors_rule": [],
            "force_destroy": false,
            "grant": [],
            "hosted_zone_id": "Z3AQBSABCDEFGH",
            "id": "example-bucket",
            "lifecycle_rule": [],
            "logging": [],
            "object_lock_configuration": [],
            "region": "us-east-1",
            "replication_configuration": [],
            "request_payer": "BucketOwner",
            "server_side_encryption_configuration": [],
            "tags": {
                "stage": "prod"
            },
               },
            "versioning": [
                {
                    "enabled": false,
                    "mfa_delete": false
                }
            ],
            "website": []
        }
    }
}

Here, we’d look at the input to see that the stage property is nested underneath tags, a “top-level” property, so the attribute we need to check for is tags.stage.

(Note that scan data for repository environments will look a little different because it has some additional information apart from resource attributes, such as _source_location, which indicates the line, column, and path of the resource in an infrastructure as code file.)

Alternate method for finding resource attributes

Alternatively, you can reference the Terraform documentation to determine the resource types and syntax required for AWS and AWS GovCloud, Azure, and Google. You’ll find the information you need in the Resource page of each provider resource. For an example, see Resource: aws_s3_bucket.

You can reference the CloudFormation documentation the same way. For an example, see AWS::S3::Bucket.

Step 5: Determine whether to write a simple or advanced rule

Once you determine the provider(s), resource type, and attribute(s) you need to check, you decide whether to write a simple or advanced rule.

In this example, our rule can be written as a simple rule because it’s checking a single resource type, Amazon S3 buckets.

It does not need to be written as an advanced rule, because:

  • It doesn’t involve multiple types

  • It doesn’t filter out certain resources of the selected type; we want to check all S3 buckets

  • It doesn’t evaluate whether a required resource type is missing

Step 6: Define pass/fail conditions for the resource

After determining the provider(s), resource type, input type (if needed), and attribute(s), you can start writing the rule logic. In simple rules, that requires an allow or deny rule.

In this example, we’ve determined that our S3 buckets should have a tag key of stage with the tag value prod. If it doesn’t, the bucket should raise a compliance violation (i.e., produce a FAIL rule result).

So, the custom rule should query each aws_s3_bucket in an environment and verify that the tags.stage property is set to prod. If so, the resource passes the compliance check. If not, the resource fails.

allow rules

In Fugue simple rules, you define a “pass” state by using the allow rule, or you define a “fail” state by using the deny rule.

If a resource is not explicitly allowed, it is implicitly denied, and vice versa.

As a result, all resources of the specified type are assigned either a PASS or FAIL result. No resource is skipped or filtered out – that’s something you can only do in an advanced rule.

For our S3 example, it makes sense to use allow, because any bucket with tags.stage set to prod should pass the compliance check.

Let’s look at the example logic from the beginning of this document:

default allow = false

allow {
  input.tags.stage == "prod"
}

This simple rule contains the following elements:

default allow = false

This statement is required for simple rules for repository environments. It’s optional for runtime-only rules, but it’s a good practice to include it.

allow { ... }

This rule states that any resource matching the query in the brackets should be assigned a PASS compliance state.

input.tags.stage

This element tells Fugue to look in the provided input (i.e., a single resource in a Fugue scan) for the tags attribute, and then the stage attribute. When you specify the resource type to assess, Fugue automatically sets the input to that resource type.

== "prod"

If the preceding attribute is set to "prod", then the allow rule evaluates to true, which means that Fugue should allow (“pass”) the resource.

deny rules

You can also write the rule with deny, which accomplishes the same thing by failing any resource where input.tags.stage is not set to prod:

default deny = false

deny {
  not input.tags.stage == "prod"
}

Another example of a deny rule would be a policy that forbids security groups from having port 9200 (Elasticsearch) open to the internet. Any resources matching that query should fail the compliance check. We’ll discuss this use case in the section below.

Multiple queries

So far, all the example rules we’ve shown have single queries, but Rego rules can also consist of multiple queries. These queries can all be required, or some can be required – just like the logical operators AND (&&) and OR (||).

For example, a security group policy stating that port 9200 should not be open to the world can be written thusly:

default deny = false

deny {
  input.ingress[i].from_port <= 9200
  input.ingress[i].to_port >= 9200
  input.ingress[i].cidr_blocks[_] == "0.0.0.0/0"
}

Let’s dissect this rule:

default deny = false

This statement is required for simple rules for repository environments. It’s optional for runtime-only rules, but it’s a good practice to include it.

deny { ... }

This rule states that any resource matching the query in the brackets should be assigned a FAIL compliance state.

input.ingress[i].from_port <= 9200

This means Fugue should look at the input (an individual security group) and iterate through the ingress block for each from_port attribute and check whether the value is less than or equal to 9200.

input.ingress[i].to_port >= 9200

Likewise, this line means Fugue should check whether the to_port is greater than or equal to 9200.

input.ingress[i].cidr_blocks[_] == "0.0.0.0/0"

This line means Fugue checks whether any cidr_blocks attribute is set to “0.0.0.0/0” (meaning the port is open to the world).

Note that there are three queries. Because they’re all contained in the same deny {} function, all queries must apply for the resource to fail the compliance check. You can restate it in pseudocode like this:

IF the ingress port range starts at 9200 or below, AND the ingress port range ends at 9200 or above, AND the ingress CIDR block is “0.0.0.0/0” (open to the internet), THEN deny (fail) the resource.

To demonstrate how Rego rules handle OR logic, we can rewrite the same rule thusly:

default deny = false

deny {
  input.ingress[i].from_port == 9200
  input.ingress[i].to_port == 9200
  input.ingress[i].cidr_blocks[_] == "0.0.0.0/0"
}

deny {
   input.ingress[i].from_port < 9200
   input.ingress[i].to_port > 9200
   input.ingress[i].cidr_blocks[_] == "0.0.0.0/0"
}

Here, there are two deny rules, which means the resource is denied if either applies.

The first deny states that if the port range is set to 9200 and the CIDR block source is "0.0.0.0/0", the resource should fail.

The second deny states that if the port range includes 9200 and the CIDR block source is "0.0.0.0/0", the resource should fail.

Should either of these conditions be met, Fugue will assign the resource a FAIL result.

This is a less efficient way of writing our example rule, but it demonstrates how you can use OR logic in Rego rules.

Rego supports some syntactic sugar for OR logic, where you can omit the second deny keyword. The rule above can be restated more concisely like this:

default deny = false

deny {
  input.ingress[i].from_port == 9200
  input.ingress[i].to_port == 9200
  input.ingress[i].cidr_blocks[_] == "0.0.0.0/0"
} {
   input.ingress[i].from_port < 9200
   input.ingress[i].to_port > 9200
   input.ingress[i].cidr_blocks[_] == "0.0.0.0/0"
}

Step 7: Write metadata

If you’re going to upload the rule to Fugue via the fugue sync rules CLI command, you must add metadata to tell Fugue additional information about the rule:

  • Title

  • Description

  • Providers

  • Severity

It’s also a good idea to include it when writing a rule that will be used with Regula.

If you’re not syncing the rule to Fugue with fugue sync rules, you define these fields externally – via the UI, CLI fugue create rule command, or API at the time of creation/update. In these cases, any metadata provided is simply ignored.

Declaring the metadata

Because we plan to sync our example rule to Fugue via fugue sync rules, we must include metadata in the __rego__metadoc__ (see metadata reference):

__rego__metadoc__ := {
  "title": "AWS S3 buckets must be tagged",
  "description": "S3 buckets must have the tag key 'stage' and value 'prod'",
  "custom": {
    "providers": ["AWS", "REPOSITORY"],
    "severity": "Medium"
  }
}

Here’s the complete rule, with metadata:

package rules.buckettags

__rego__metadoc__ := {
  "title": "AWS S3 buckets must be tagged",
  "description": "S3 buckets must have the tag key 'stage' and value 'prod'",
  "custom": {
    "providers": ["AWS", "REPOSITORY"],
    "severity": "Medium"
  }
}

input_type = "tf"

resource_type = "aws_s3_bucket"

default allow = false

allow {
  input.tags.stage == "prod"
}

What’s Next?

Once you’ve written a rule, the next step is to create it:

Note

Looking for some examples of custom rules? Check out our GitHub repo!