Writing AWS CloudFormation Guard rules
In AWS CloudFormation Guard, rules are policy-as-code rules. You write rules in the Guard domain-specific language (DSL) that you can validate your JSON- or YAML-formatted data against. Rules are made up of clauses.
You can save rules written using the Guard DSL into plaintext files that use any file extension.
You can create multiple rule files and categorize them as a rule set. Rule sets allow you to validate your JSON- or YAML-formatted data against multiple rule files at the same time.
Topics
- Clauses
- Using queries in clauses
- Using operators in clauses
- Using custom messages in clauses
- Combining clauses
- Using blocks with Guard rules
- Defining Guard queries and filtering
- Assigning and referencing variables in Guard rules
- Composing named-rule blocks in AWS CloudFormation Guard
- Writing clauses to perform context-aware evaluations
Clauses
Clauses are Boolean expressions that evaluate to either true (PASS
) or false
(FAIL
). Clauses use either binary operators to compare two values or unary
operators that operate on a single value.
Examples of unary clauses
The following unary clause evaluates whether the collection TcpBlockedPorts
is empty.
InputParameters.TcpBlockedPorts not empty
The following unary clause evaluates whether the ExecutionRoleArn
property is a string.
Properties.ExecutionRoleArn is_string
Examples of binary clauses
The following binary clause evaluates whether the BucketName
property contains
the string encrypted
, regardless of casing.
Properties.BucketName != /(?i)encrypted/
The following binary clause evaluates whether the ReadCapacityUnits
property is less than or equal to 5,000.
Properties.ProvisionedThroughput.ReadCapacityUnits <= 5000
Syntax for writing Guard rule clauses
<query> <operator> [query|value literal] [custom message]
Properties of Guard rule clauses
query
-
A dot (
.
) separated expression written to traverse hierarchical data. Query expressions can include filter expressions to target a subset of values. Queries can be assigned to variables so that you can write them once and reference them elsewhere in a rule set, which will allow you to access query results.For more information about writing queries and filtering, see Defining queries and filtering.
Required: Yes
operator
-
A unary or binary operator that helps check the state of the query. The left-hand side (LHS) of a binary operator must be a query, and the right-hand side (RHS) must be either a query or a value literal.
Supported binary operators:
==
(Equal) |!=
(Not equal) |>
(Greater than) |>=
(Greater than or equal to) |<
(Less than) |<=
(Less than or equal to) |IN
(In a list of form [x, y, z]Supported unary operators:
exists
|empty
|is_string
|is_list
|is_struct
|not(!)
Required: Yes
query|value literal
-
A query or a supported value literal such as
string
orinteger(64)
.Supported value literals:
-
All primitive types:
string
,integer(64)
,float(64)
,bool
,char
,regex
-
All specialized range types for expressing
integer(64)
,float(64)
, orchar
ranges expressed as:-
r[<lower_limit>, <upper_limit>]
, which translates to any valuek
that satisfies the following expression:lower_limit <= k <= upper_limit
-
r[<lower_limit>, <upper_limit>
), which translates to any valuek
that satisfies the following expression:lower_limit <= k < upper_limit
-
r(<lower_limit>, <upper_limit>]
, which translates to any valuek
that satisfies the following expression:lower_limit < k <= upper_limit
-
r(<lower_limit>, <upper_limit>),
which translates to any valuek
that satisfies the following expression:lower_limit < k < upper_limit
-
-
Associative arrays (maps) for nested key-value structure data. For example:
{ "my-map": { "nested-maps": [ { "key": 10, "value": 20 } ] } }
-
Arrays of primitive types or associative array types
Required: Conditional; required when a binary operator is used.
-
custom message
-
A string that provides information about the clause. The message is displayed in the verbose outputs of the
validate
andtest
commands and can be useful for understanding or debugging rule evaluation on hierarchical data.Required: No
Using queries in clauses
For information about writing queries, see Defining queries and filtering and Assigning and referencing variables in Guard rules.
Using operators in clauses
The following are example CloudFormation templates, Template-1
and
Template-2
. To demonstrate the use of supported operators, the example queries and
clauses in this section refer to these example templates.
Template-1
Resources: S3Bucket: Type: "AWS::S3::Bucket" Properties: BucketName: "MyServiceS3Bucket" BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'aws:kms' KMSMasterKeyID: 'arn:aws:kms:us-east-1:123456789:key/056ea50b-1013-3907-8617-c93e474e400' Tags: - Key: "stage" Value: "prod" - Key: "service" Value: "myService"
Template-2
Resources: NewVolume: Type: AWS::EC2::Volume Properties: Size: 100 VolumeType: io1 Iops: 100 AvailabilityZone: Fn::Select: - 0 - Fn::GetAZs: us-east-1 Tags: - Key: environment Value: test DeletionPolicy: Snapshot
Examples of clauses that use unary operators
-
empty
– Checks if a collection is empty. You can also use it to check if a query has values in a hierarchical data because queries result in a collection. You can't use it to check whether string value queries have an empty string (""
) defined. For more information, see Defining queries and filtering.The following clause checks whether the template has one or more resources defined. It evaluates to
PASS
because a resource with the logical IDS3Bucket
is defined inTemplate-1
.Resources !empty
The following clause checks whether one or more tags are defined for the
S3Bucket
resource. It evaluates toPASS
becauseS3Bucket
has two tags defined for theTags
property inTemplate-1
.Resources.S3Bucket.Properties.Tags !empty
-
exists
– Checks whether each occurrence of the query has a value and can be used in place of!= null
.The following clause checks whether the
BucketEncryption
property is defined for theS3Bucket
. It evaluates toPASS
becauseBucketEncryption
is defined forS3Bucket
inTemplate-1
.Resources.S3Bucket.Properties.BucketEncryption exists
Note
The empty
and not exists
checks evaluate to true
for missing property keys when traversing the input data. For example, if the
Properties
section isn't defined in the template for the S3Bucket
,
the clause Resources.S3Bucket.Properties.Tag empty
evaluates to true
.
The exists
and empty
checks don't display the JSON pointer path
inside the document in the error messages. Both of these clauses often have retrieval errors
that don't maintain this traversal information.
-
is_string
– Checks whether each occurrence of the query is ofstring
type.The following clause checks whether a string value is specified for the
BucketName
property of theS3Bucket
resource. It evaluates toPASS
because the string value"MyServiceS3Bucket"
is specified forBucketName
inTemplate-1
.Resources.S3Bucket.Properties.BucketName is_string
-
is_list
– Checks whether each occurrence of the query is oflist
type.The following clause checks whether a list is specified for the
Tags
property of theS3Bucket
resource. It evaluates toPASS
because two key-value pairs are specified forTags
inTemplate-1
.Resources.S3Bucket.Properties.Tags is_list
-
is_struct
– Checks whether each occurrence of the query is structured data.The following clause checks whether structured data is specified for the
BucketEncryption
property of theS3Bucket
resource. It evaluates toPASS
becauseBucketEncryption
is specified using theServerSideEncryptionConfiguration
property type(object)
inTemplate-1
.
Note
To check the inverse state, you can use the ( not !
) operator with the
is_string
, is_list
, and is_struct
operators .
Examples of clauses that use binary operators
The following clause checks whether the value specified for the BucketName
property of the S3Bucket
resource in Template-1
contains the string
encrypt
, regardless of casing. This evaluates to PASS
because the
specified bucket name "MyServiceS3Bucket"
does not contain the string
encrypt
.
Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/
The following clause checks whether the value specified for the Size
property of the NewVolume
resource in Template-2
is within a specific range: 50 <= Size
<= 200. It evaluates to PASS
because 100
is specified for Size
.
Resources.NewVolume.Properties.Size IN r[50,200]
The following clause checks whether the value specified for the VolumeType
property of the NewVolume
resource in Template-2
is io1
, io2
, or gp3
. It evaluates to PASS
because io1
is specified for NewVolume
.
Resources.NewVolume.Properties.NewVolume.VolumeType IN [ 'io1','io2','gp3' ]
Note
The example queries in this section demonstrate the use of operators using the resources
with logical IDs S3Bucket
and NewVolume
. Resource names are often
user-defined and can be arbitrarily named in an infrastructure as code (IaC) template. To write
a rule that is generic and applies to all AWS::S3::Bucket
resources defined in the
template, the most common form of query used is Resources.*[ Type == ‘AWS::S3::Bucket’
]
. For more information, see Defining queries and
filtering for details about usage and explore the examplescloudformation-guard
GitHub repository.
Using custom messages in clauses
In the following example, clauses for Template-2
include a custom
message.
Resources.NewVolume.Properties.Size IN r[50,200] << EC2Volume size must be between 50 and 200, not including 50 and 200 >> Resources.NewVolume.Properties.VolumeType IN [ 'io1','io2','gp3' ] <<Allowed Volume Types are io1, io2, and gp3>>
Combining clauses
In Guard, each clause written on a new line is combined implicitly with the next
clause by using conjunction (Boolean and
logic). See the following example.
# clause_A ^ clause_B ^ clause_C clause_A clause_B clause_C
You can also use disjunction to combine a clause with the next clause by specifying
or|OR
at the end of the first clause.
<query> <operator> [query|value literal] [custom message] [or|OR]
In a Guard clause, disjunctions are evaluated first, followed by conjunctions.
Guard rules can be defined as a conjunction of disjunction of clauses (an
and|AND
of or|OR
s) that evaluate to either true
(PASS
) or false
(FAIL
). This is similar to Conjunctive normal form
The following examples demonstrate the order of evaluations of clauses.
# (clause_E v clause_F) ^ clause_G clause_E OR clause_F clause_G # (clause_H v clause_I) ^ (clause_J v clause_K) clause_H OR clause_I clause_J OR clause_K # (clause_L v clause_M v clause_N) ^ clause_O clause_L OR clause_M OR clause_N clause_O
All clauses that are based on the example Template-1
can be combined by using
conjunction. See the following example.
Resources.S3Bucket.Properties.BucketName is_string Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/ Resources.S3Bucket.Properties.BucketEncryption exists Resources.S3Bucket.Properties.BucketEncryption is_struct Resources.S3Bucket.Properties.Tags is_list Resources.S3Bucket.Properties.Tags !empty
Using blocks with Guard rules
Blocks are compositions that remove verbosity and repetition from a set of related clauses, conditions, or rules. There are three types of blocks:
-
Query blocks
-
when
blocks -
Named-rule blocks
Query blocks
Following are the clauses that are based on the example Template-1
.
Conjunction was used to combine the clauses.
Resources.S3Bucket.Properties.BucketName is_string Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/ Resources.S3Bucket.Properties.BucketEncryption exists Resources.S3Bucket.Properties.BucketEncryption is_struct Resources.S3Bucket.Properties.Tags is_list Resources.S3Bucket.Properties.Tags !empty
Parts of the query expression in each clause are repeated. You can improve composability and remove verbosity and repetition from a set of related clauses with the same initial query path by using a query block. The same set of clauses can be written as shown in the following example.
Resources.S3Bucket.Properties { BucketName is_string BucketName != /(?i)encrypt/ BucketEncryption exists BucketEncryption is_struct Tags is_list Tags !empty }
In a query block, the query preceding the block sets the context for the clauses inside the block.
For more information about using blocks, see Composing named-rule blocks.
when
blocks
You can evaluate blocks conditionally by using when
blocks, which take the
following form.
when <condition> { Guard_rule_1 Guard_rule_2 ... }
The when
keyword designates the start of the when
block. condition
is a Guard rule. The block is only evaluated if the evaluation of the condition results in true
(PASS
).
The following is an example when
block that is based on
Template-1
.
when Resources.S3Bucket.Properties.BucketName is_string { Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/ }
The clause within the when
block is only evaluated if the value specified for
BucketName
is a string. If the value specified for BucketName
is
referenced in the Parameters
section of the template as shown in the following
example, the clause within the when
block is not evaluated.
Parameters: S3BucketName: Type: String Resources: S3Bucket: Type: "AWS::S3::Bucket" Properties: BucketName: Ref: S3BucketName ...
Named-rule blocks
You can assign a name to a set of rules (rule set), and then reference these modular validation blocks, called named-rule blocks, in other rules. Named-rule blocks take the following form.
rule <rule name> [when <condition>] { Guard_rule_1 Guard_rule_2 ... }
The rule
keyword designates the start of the named-rule block.
rule name
is a human-readable string that uniquely identifies a named-rule block.
It's a label for the Guard rule set that it encapsulates. In this use, the term Guard rule includes clauses, query blocks, when
blocks, and named-rule blocks. The rule name can be used to refer to the evaluation result of
the rule set that it encapsulates, which makes named-rule blocks reusable. The rule name also
provides context about rule failures in the validate
and test
command
outputs. The rule name is displayed along with the block’s evaluation status (PASS
,
FAIL
, or SKIP
) in the evaluation output of the rules file. See the
following example.
# Sample output of an evaluation where check1, check2, and check3 are rule names. _Summary__ __Report_ Overall File Status = **FAIL** **PASS/****SKIP** **rules** check1 **SKIP** check2 **PASS** **FAILED rules** check3 **FAIL**
You can also evaluate named-rule blocks conditionally by specifying the when
keyword followed by a condition after the rule name.
Following is the example when
block that was discussed previously in this
topic.
rule checkBucketNameStringValue when Resources.S3Bucket.Properties.BucketName is_string { Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/ }
Using named-rule blocks, the preceding can also be written as follows.
rule checkBucketNameIsString { Resources.S3Bucket.Properties.BucketName is_string } rule checkBucketNameStringValue when checkBucketNameIsString { Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/ }
You can reuse and group named-rule blocks with other Guard rules. Following are a few examples.
rule rule_name_A { Guard_rule_1 OR Guard_rule_2 ... } rule rule_name_B { Guard_rule_3 Guard_rule_4 ... } rule rule_name_C { rule_name_A OR rule_name_B } rule rule_name_D { rule_name_A rule_name_B } rule rule_name_E when rule_name_D { Guard_rule_5 Guard_rule_6 ... }