Writing Custom Rules

Note

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

Our open source tool Fregot allows you to easily evaluate, debug, and test custom rules (and more).

Note

This page provides an overview of custom rules and instructions for writing them.

For instructions on managing custom rules, see:

Fugue’s custom rules feature allows you to add user-defined compliance rules to your environment through the Custom Rules page (see UI instructions) or API. Fugue evaluates your infrastructure against the rules and displays results for your custom rules as it would for out-of-the-box standards such as SOC 2 or HIPAA. The visualizer also denotes noncompliance with custom rules.

You can use custom rules to enforce enterprise policies.

As with any other compliance family, you will need to explicitly enable custom rules if those should be active on the environment. To change the selected compliance families, see the FAQ.

Tip

To waive a rule for a specific resource in a specific environment, see Waivers.

What are Custom Rules?

A rule checks cloud infrastructure configurations to determine whether a resource, region, or account complies with a specific policy. For example, the rule “VPC flow logging should be enabled” evaluates an Amazon VPC’s flow log configuration. To learn more about rules, see Compliance Concepts.

Users can write custom rules with Rego, an open source policy-as-code language for Open Policy Agent (OPA).

Rego is a query language. In Rego, a rule consists of one or more queries that return information from a Fugue scan. Fugue uses Rego rules to determine whether a resource is compliant with a given configuration.

There are two types of rules:

Here is an example of a simple rule, which we’ll explain in detail later in this guide:

allow {
  input.multi_az == true
}

Here’s an example of an advanced rule, which we’ll also discuss later:

# Return all security groups in an environment
security_groups = fugue.resources("AWS.EC2.SecurityGroup")

# Security groups that have port 9200 open to the internet are considered invalid
invalid(sg) {
  sg.ingress[i].from_port <= 9200
  sg.ingress[i].to_port >= 9200
  sg.ingress[i].cidr_blocks[_] == "0.0.0.0/0"
}

# Build policy document; invalid security groups fail, valid ones pass
policy[r] {
   security_group = security_groups[_]
   invalid(security_group)
   r = fugue.deny_resource(security_group)
} {
   security_group = security_groups[_]
   not invalid(security_group)
   r = fugue.allow_resource(security_group)
}

General Custom Rules Workflow

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

To write a custom rule, you must:

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

  2. Define pass or fail conditions for the resource

Once you’ve successfully written a rule, you can:

  • Validate and add the rule to Fugue from the Custom Rules page (aka UI) or API

  • Modify the rule’s name, description, or Rego definition (UI | API)

  • Delete the rule (UI | API)

  • View compliance results by control (UI | API), resource type (UI | API), or resource (UI only).

  • View compliance results in the visualizer

  • Waive the rule result for a resource in an environment

See the API Reference for Swagger documentation.

How to Write Custom Rules

As stated earlier, 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.

We’ve added some Fugue-specific functionality to Rego. For details, jump ahead to the Custom Rules Cheat Sheet. Otherwise, continue reading to learn how to write custom rules.

Simple Custom Rules

First, we’ll dive into how to write a simple custom rule, which tests a single resource type.

(Can’t wait to write rules for multiple resources? Feel free to skip ahead to Advanced Custom Rules.)

Step 1: Determine resource attributes (simple)

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 need to determine the resource type and attributes that are involved. There are a couple ways to do this:

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

However, it may be easier to write rules by looking at a concrete example. 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 API to retrieve the results of a recent scan (see Creating a Rule via API - Get Input for Test for details).

Once you have the JSON document containing the scan results, you’ll see the configuration for every resource in the scan. You can then find the attribute in question. See the API Reference for details.

Step 2: Define pass or fail conditions for the resource (simple)

Let’s say your organization requires Amazon RDS instances to be deployed in multiple availability zones. If you look at the Terraform RDS docs, you’ll see the aws_db_instance resource lists a multi_az attribute. So, the custom rule should query each AWS.RDS.Instance in an environment and verify that the multi_az property is enabled (i.e., set to true). If so, the resource passes the compliance check. If not, the resource fails.

allow rules

In Fugue, you define a “pass” state by using the allow function. You can define a “fail” state by using the deny function.

For our RDS example, it makes sense to use the allow function, because anything with multi_az set to true should pass the compliance check.

Let’s look at the example again:

allow {
  input.multi_az == true
}

This simple rule contains the following elements:

allow { ... }

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

input.multi_az

This element tells Fugue to look in the provided input for the multi_az attribute. When you specify the resource type to assess, Fugue automatically sets the input to that resource type.

== true

If the preceding attribute is true, execute the function. Here, that means Fugue should allow (“pass”) resources that have multi_az set to true.

deny rules

You can also write the rule with the deny function, which accomplishes the same thing by failing any resource where input.multi_az is set to false:

deny {
  input.multi_az == false
}

Another example of a deny function 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, the security group policy we mentioned in the previous section – where port 9200 should not be open to the world – can be written thusly:

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:

deny { ... }

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

input.ingress[i].from_port <= 9200

We’ll explain the syntax in more detail in a bit, but this means Fugue should look at the input (security group resources) for the 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 checks whether the 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 in order 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:

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

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

Here, there are two deny functions, which means the rule is denied if either function applies.

The first deny function 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 function 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 compliance state.

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

Advanced Custom Rules

While simple custom rules focus on a single resource type, advanced custom rules look at the entire environment and can therefore optionally involve multiple resource types (though advanced rules can also involve a single resource type, and we’ll show you how here and here).

This makes it possible to mix and match different resource types and join them in a variety of ways. Advanced rules are far more expressive than simple rules, but they are also more complex.

The high-level workflow for advanced rules is usually:

  1. Query for resources by using the fugue.resources(resource_type) function.

  2. Declare a policy set rule that iterates over queried resources.

  3. Extend the policy set using one of the following primitives (explained in more detail here):

  • fugue.allow_resource(resource)

  • fugue.deny_resource(resource)

  • fugue.missing_resource(resource_type)

Advanced Custom Rule with a Single Resource Type

If we consider the simple rule to check whether security group port 9200 is open to the world, this would be an advanced port of that rule. Even though it checks a single resource type, the use of the functions fugue.resources(resource_type), fugue.allow_resource(resource), and fugue.deny_resource(resource) make it an advanced rule:

# Return all security groups in an environment
security_groups = fugue.resources("AWS.EC2.SecurityGroup")

# Security groups that have port 9200 open to the internet are considered invalid
invalid(sg) {
  sg.ingress[i].from_port <= 9200
  sg.ingress[i].to_port >= 9200
  sg.ingress[i].cidr_blocks[_] == "0.0.0.0/0"
}

# Build policy document; invalid security groups fail, valid ones pass
policy[r] {
   security_group = security_groups[_]
   invalid(security_group)
   r = fugue.deny_resource(security_group)
} {
   security_group = security_groups[_]
   not invalid(security_group)
   r = fugue.allow_resource(security_group)
}

Let’s dig into what these new functions do.

Advanced Rule Functions

fugue.resources(resource_type)

Returns a collection of resources of the given resource_type string. Fugue also keeps track of which resource types are requested. We use this information to decide whether there are simply no resources of a type, or if we do not have permission to scan that resource type.

fugue.allow_resource(resource)

Makes a PASS judgement about a resource retrieved using fugue.resources.

fugue.deny_resource(resource)

Makes a DENY judgement about a resource retrieved using fugue.resources.

fugue.missing_resource(resource_type)

Makes a DENY judgement about a resource type string without actually having access to a resource. This can be used to write a rule that checks for the presence of a specific resource.

Why do we need more than allow and deny? For example, if we require an AWS CloudTrail alarm for failed logins, it does not mean that other CloudTrail alarms are invalid – they are simply irrelevant. Hence, we should not mark these other CloudTrail alarms as DENY using fugue.deny_resource, but rather use fugue.missing_resource to indicate that a resource is missing.

Expanding an Advanced Custom Rule

We can expand the example above to check only certain security groups based on tags. Here, we only look at security groups with the tag key Stage and value Prod to determine whether any have port 9200 open to the world:

# Return all security groups in an environment, then filter on the tag Stage:Prod
tagged_sgs[tags] = security_group {
   security_groups = fugue.resources("AWS.EC2.SecurityGroup")
   security_group = security_groups[tags]
   security_group.tags.Stage == "Prod"
}

# Security groups that have port 9200 open to the internet are considered invalid
invalid(sg) {
  sg.ingress[i].from_port <= 9200
  sg.ingress[i].to_port >= 9200
  sg.ingress[i].cidr_blocks[_] == "0.0.0.0/0"
}

# Build policy document; of the security groups tagged Stage:Prod, invalid SGs fail, valid ones pass
policy[r] {
   security_group = tagged_sgs[_]
   invalid(security_group)
   r = fugue.deny_resource(security_group)
} {
   security_group = tagged_sgs[_]
   not invalid(security_group)
   r = fugue.allow_resource(security_group)
}

Advanced Custom Rules with Multiple Resource Types

Here’s an example of an advanced custom rule with multiple resource types – in this case, AWS.EC2.Vpc and AWS.EC2.SecurityGroup. This rule checks whether all security groups attached to a production VPC have a specific tag. If the security groups have the correct tag, they pass the compliance check; if not, they fail:

# The following multi-resource type validation checks that all Security Groups
# attached to the production VPC have a Stage tag with the value Prod.
# The production VPC.
prod_vpc = vpc {
  vpcs = fugue.resources("AWS.EC2.Vpc")
  vpc = vpcs[_]
  vpc.tags.Name == "prod-vpc"
}
# Security groups attached to the prod VPC.
prod_security_groups[id] = security_group {
  security_groups = fugue.resources("AWS.EC2.SecurityGroup")
  security_group = security_groups[id]
  security_group.vpc_id == prod_vpc.id
}
# Check that the security group is tagged with {"Stage": "Prod"}.
tagged_security_group(security_group) {
  security_group.tags.Stage == "Prod"
}
# Build policy document.
policy[p] {
  security_group = prod_security_groups[_]
  tagged_security_group(security_group)
  p = fugue.allow_resource(security_group)
} {
  security_group = prod_security_groups[_]
  not tagged_security_group(security_group)
  p = fugue.deny_resource(security_group)
}

For further assistance with advanced custom rules, reach out to support@fugue.co.

Custom Rules Cheat Sheet

When writing a custom rule in Fugue, remember the following tips:

  • Custom rule policies do not require the package declaration at the start of a rule.

  • Simple rules must specify at least one allow or deny rule.

    • When more than one allow or deny rule are given, they are simply “OR”-ed together, following standard Rego semantics.

    • If both allow and deny are specified in the same policy, then deny rules will override allow rules.

  • We’ve provided Fugue-specific functions to make writing advanced rules easier.

Example Rules

For examples of simple and advanced custom rules, see our GitHub repo.

Managing Rules in the UI and API

Learning Rego

Below are some resources for learning Rego:

Additionally, consider using our open-source tool Fregot to enhance the process of writing Rego rules. Fregot allows you to easily evaluate, debug, and test custom rules (and more).