Simple Custom Rules¶
Note
For an overview on custom rules, see Writing Rules.
For details on advanced custom rules, see Advanced Custom Rules.
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:
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.
Optional but recommended step¶
To ensure compatibility with Regula, start your simple rule with a package declaration, where the name starts with rules
:
package rules.buckettags
This is an optional step if you’re only using Fugue, but it’s a good practice. See Compatibility with Regula for details.
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 stringsAPI: 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)
Runtime-only rules use Fugue names for resources:
AWS and AWS GovCloud (e.g.,
AWS.S3.Bucket
)Azure and Azure Government (e.g.,
Azure.Storage.Account
)Google (e.g.,
Google.Storage.Bucket
)
Repository (or repository + runtime) rules use the resource name according to the input type:
Terraform HCL and JSON plans
AWS and AWS GovCloud (e.g.,
aws_s3_bucket
)Azure (e.g.,
azurerm_storage_account
)Google (e.g.,
google_storage_bucket
)
CloudFormation (e.g.,
AWS::S3::Bucket
)Kubernetes (see the
KIND
column) (e.g.,Pod
)
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
environmentsRules 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 justGOOGLE
) 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
flagAPI: 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 thetags
attribute, and then thestage
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 theallow
rule evaluates totrue
, which means that Fugue shouldallow
(“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 eachfrom_port
attribute and check whether the value is less than or equal to9200
.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:
Via the UI
Via the CLI fugue create rule command (to create a single rule)
Via the CLI fugue sync rules command (to upload a directory of one or more rules)
Via the API
Note
Looking for some examples of custom rules? Check out our GitHub repo!