AWS CloudFormation Guard
User Guide
AWS CloudFormation Guard: User Guide
Copyright © Amazon Web Services, Inc. and/or its affiliates. All rights reserved.
Amazon's trademarks and trade dress may not be used in connection with any product or service that is not Amazon's, in any manner that is likely to cause confusion among customers, or in any manner that disparages or discredits Amazon. All other trademarks not owned by Amazon are the property of their respective owners, who may or may not be affiliated with, connected to, or sponsored by Amazon.
Table of Contents
What is AWS CloudFormation Guard? ... 1
Are you a first-time Guard user? ... 1
Features of Guard ... 1
Accessing Guard ... 2
Best practices ... 2
Setting up Guard ... 3
Installing Guard ... 3
Installing Guard (Linux, macOS, or Unix) ... 3
Installing Guard (Windows) ... 4
Installing Guard as an AWS Lambda function ... 5
Install Guard as a Lambda ... 6
To install the Rust package manager ... 4
To install Guard as a Lambda function ... 6
To build and run ... 7
Calling the Lambda function ... 7
Getting started with Guard ... 8
Prerequisites ... 8
Overview of using Guard rules ... 8
Migrating Guard 1.0 rules to Guard 2.0 ... 8
Enhancements in Guard 2.0 ... 8
Migrate a Guard rule ... 9
Writing Guard rules ... 10
Clauses ... 10
Using queries in clauses ... 12
Using operators in clauses ... 12
Using custom messages in clauses ... 14
Combining clauses ... 14
Using blocks with Guard rules ... 15
Defining queries and filtering ... 18
Assigning and referencing variables in Guard rules ... 27
Composing named-rule blocks ... 32
Writing clauses to perform context-aware evaluations ... 36
Testing Guard rules ... 45
Prerequisites ... 45
Overview ... 45
Walkthrough ... 46
Validating input data against Guard rules ... 52
Prerequisites ... 53
Using the validate command ... 53
Validating multiple rules against multiple data files ... 53
Troubleshooting Guard ... 54
Clause fails when no resources of the selected type are present ... 54
Guard does not evaluate CloudFormation template ... 54
General troubleshooting topics ... 54
Guard CLI reference ... 56
Global parameters ... 56
migrate ... 56
Syntax ... 56
Parameters ... 56
Options ... 57
Examples ... 57
See also ... 57
parse-tree ... 57
Syntax ... 57
Parameters ... 57
Options ... 57
Examples ... 58
rulegen ... 58
Syntax ... 58
Parameters ... 58
Options ... 58
Examples ... 58
test ... 59
Syntax ... 59
Parameters ... 59
Options ... 59
args ... 59
Examples ... 59
Output ... 60
See also ... 60
validate ... 60
Syntax ... 60
Parameters ... 60
Options ... 61
Examples ... 62
Output ... 62
See also ... 62
Security ... 63
Document history ... 64
AWS glossary ... 65
Are you a first-time Guard user?
What is AWS CloudFormation Guard?
AWS CloudFormation Guard is a policy-as-code evaluation tool that is open source and useful for general purposes. The Guard command line interface (CLI) provides you with a declarative domain-specific language (DSL) that you can use to express policy as code. In addition, you can use CLI commands to validate JSON- or YAML-formatted structured data against those rules. Guard also provides a built-in unit testing framework to verify that your rules work as intended.
Guard doesn't validate CloudFormation templates for valid syntax or allowed property values. You can use the cfn-lint tool to perform a thorough inspection of template structure.
NoteGuard 2.0.3 is the latest version, released in June 2021. We recommend that you migrate your rules to this version because it has many significant enhancements. Guard 2.0 is backward incompatible with Guard 1.0 rules. Using Guard 2.0 together with Guard 1.0 rules and vice versa can result in breaking changes. For information about enhancements and migrating your Guard rules, see Migrating Guard 1.0 rules to Guard 2.0 (p. 8).
Topics
• Are you a first-time Guard user? (p. 1)
• Features of Guard (p. 1)
• Accessing Guard (p. 2)
• Best practices (p. 2)
Are you a first-time Guard user?
If you're a first-time user of Guard, we recommend that you begin by reading the following sections:
• Setting up Guard (p. 3) – This section describes how to install Guard. With Guard, you can write policy rules using the Guard DSL and validate your JSON- or YAML-formatted structured data against those rules.
• Writing Guard rules (p. 10) – This section provides detailed walkthroughs for writing policy rules.
• Testing Guard rules (p. 45) – This section provides a detailed walkthrough for testing your rules to verify that they work as intended, and validating your JSON- or YAML-formatted structured data against your rules.
• Validating input data against Guard rules (p. 52) – This section provides a detailed walkthrough for validating your JSON- or YAML-formatted structured data against your rules.
• Guard CLI reference (p. 56) – This section describes the commands that are available in the Guard CLI.
Features of Guard
Using Guard, you can write policy rules to validate any JSON- or YAML-formatted structured data against, including but not limited to AWS CloudFormation templates. Guard supports the entire
spectrum of end-to-end evaluation of policy checks. Rules are useful in the following business domains:
• Preventative governance and compliance (shift-left testing) – Validate infrastructure as code (IaC) or infrastructure and service compositions against policy rules that represent your organizational
Accessing Guard
best practices for security and compliance. For example, you can validate CloudFormation templates, CloudFormation change sets, JSON-based Terraform configuration files, or Kubernetes configurations.
• Detective governance and compliance – Validate conformity of Configuration Management Database (CMDB) resources such as AWS Config-based configuration items (CIs). For example, developers can use Guard policies against AWS Config CIs to continuously monitor the state of deployed AWS and non- AWS resources, detect violations from policies, and start remediation.
• Deployment safety – Ensure that changes are safe before deployment. For example, validate CloudFormation change sets against policy rules to prevent changes that result in resource replacement, such as renaming an Amazon DynamoDB table.
Accessing Guard
To access the Guard DSL and commands, you must install the Guard CLI. For information about installing the Guard CLI, see Setting up Guard (p. 3).
Best practices
When you use Guard 2.0, be aware of the following best practices:
• Migrate Guard 1.0 rules to Guard 2.0 (p. 8).
• Write simple rules, and use named rules to reference them in other rules. Complex rules can be difficult to maintain and test.
Installing Guard
Setting up AWS CloudFormation Guard
AWS CloudFormation Guard is an open-source command line interface. It provides you with a simple, domain-specific language to write policy rules and validate their structured hierarchical JSON and YAML data against those rules. The rules can represent company policy guidelines related to security, compliance, and more. The structured hierarchical data can represent cloud infrastructure described as code. For example, you can create rules to ensure that they always model encrypted Amazon Simple Storage Service (Amazon S3) buckets in their CloudFormation templates.
The following topics provides information about how to install Guard.
Topics
• Installing AWS CloudFormation Guard (p. 3)
• Installing Guard as an AWS Lambda function (p. 5)
Installing AWS CloudFormation Guard
AWS CloudFormation Guard is an open-source command line interface. It provides you with a domain- specific language that you can use to write policy rules and validate structured hierarchical JSON and YAML data against those rules.
To install Guard, see the following instructions for your host environment.
Topics
• Installing Guard (Linux, macOS, or Unix) (p. 3)
• Installing Guard (Windows) (p. 4)
Installing Guard (Linux, macOS, or Unix)
On a Linux, macOs, or Unix environment, you can install AWS CloudFormation Guard by using either the pre-built release binary or Cargo, which is the Rust package manager.
To install Guard from a pre-built release binary
Use the following procedure to install Guard from a pre-built binary.
1. Open a terminal, and run the following command.
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws- cloudformation/cloudformation-guard/main/install-guard.sh | sh
2. Run the following command to set your PATH variable.
PATH=~/.guard/bin/
Results: You have successfully installed Guard and set the PATH variable.
Installing Guard (Windows)
• (Optional) To confirm the installation of Guard, run the following command.
cfn-guard --version
The command returns the following output.
cfn-guard 2.0
To install the Rust package manager
Cargo is the Rust package manager. Complete the following steps to install Rust which includes Cargo.
1. Run the following command from a terminal, and follow the onscreen instructions to install Rust.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
• (Optional) For Ubuntu environments, run the following command.
sudo apt-get update; sudo apt install build-essential
2. Configure your PATH environment variable, and run the following command.
source $HOME/.cargo/env
To install Guard from Cargo
Open a terminal, and run the following command.
cargo install cfn-guard
Results: You have successfully installed Guard.
(Optional) To confirm the installation of Guard, run the following command.
cfn-guard --version
The command returns the following output.
cfn-guard 2.0
Installing Guard (Windows)
On a Windows host, you can install AWS CloudFormation Guard by using Cargo, the Rust package manager.
Prerequisites
Complete these prerequisites before you install Guard on your Windows host:
• ??? (p. 5)
Installing Guard as an AWS Lambda function
• ??? (p. 5)
• ??? (p. 5)
Install Microsoft Visual C++ Build Tools
To build Guard from the command line interface, you must install the Build Tools for Visual Studio 2019.
1. Download Microsoft Visual C++ build tools from the Build Tools for Visual Studio 2019 website.
2. Run the installer, and select the defaults.
To install Rust package manager
Download and install Rust, which contains the package manager Cargo.
1. Download Rust and then run rustup-init.exe.
2. From the command prompt, choose 1, which is the default option.
The command returns the following output.
Rust is installed now. Great!
To get started you may need to restart your current shell.
This would reload its PATH environment variable to include Cargo's bin directory (%USERPROFILE%\.cargo\bin).
Press the Enter key to continue.
3. To finish the installation, press the Enter key.
To install Guard from Cargo
Open a terminal, and then run the following command.
cargo install cfn-guard
Results: You have successfully installed Guard.
(Optional) To confirm the installation of Guard, run the following command.
cfn-guard --version
The command returns the following output.
cfn-guard 2.0
Installing Guard as an AWS Lambda function
You can install AWS CloudFormation Guard through Cargo, the Rust package manager. Guard as an AWS Lambda function (cfn-guard-lambda) is a lightweight wrapper around Guard () that can be used as a Lambda function.
Install Guard as a Lambda
Install Guard as a Lambda
Prerequisites
Before you can install Guard as a Lambda function, you must fulfill the following prerequisites:
• AWS Command Line Interface (AWS CLI) configured with permissions to deploy and invoke Lambda functions. For more information, see Configuring the AWS CLI.
• An AWS Lambda execution role in AWS Identity and Access Management (IAM). For more information, see AWS Lambda execution role.
• In CentOS/RHEL environments, add the musl-libc package repository to your yum config. For more information, see ngompa/musl-libc.
To install the Rust package manager
Cargo is the Rust package manager. Complete the following steps to install Rust which includes Cargo.
1. Run the following command from a terminal, and then follow the onscreen instructions to install Rust.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
• (Optional) For Ubuntu environments, run the following command.
sudo apt-get update; sudo apt install build-essential
2. Configure your PATH environment variable, and run the following command.
source $HOME/.cargo/env
To install Guard as a Lambda function (Linux, macOS, or Unix)
1. From your command terminal, run the following command.
cargo install cfn-guard-lambda
• (Optional) To confirm the installation of Guard as a Lambda function, run the following command.
cfn-guard-lambda --version
The command returns the following output.
cfn-guard-lambda 2.0
2. To install musl support, run the following command.
rustup target add x86_64-unknown-linux-musl
To build and run
3. Build with musl, and then run the following command in your terminal.
cargo build --release --target x86_64-unknown-linux-musl
For a custom runtime, AWS Lambda requires an executable with the name bootstrap in the deployment package .zip file. Rename the generated cfn-lambda executable to bootstrap and then add it to the .zip archive.
• For macOS environments, create your cargo configuration file in the root of the Rust project or in ~/.cargo/config.
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
4. Change to the cfn-guard-lambda root directory.
cd ~/.cargo/bin/cfn-guard-lambda
5. Run the following command in your terminal.
cp ./../target/x86_64-unknown-linux-musl/release/cfn-guard-lambda ./bootstrap && zip lambda.zip bootstrap && rm bootstrap
6. Run the following command to submit as a Lambda function to your account.
aws lambda create-function --function-name cfnGuard \ --handler guard.handler \
--zip-file fileb://./lambda.zip \ --runtime provided \
--role arn:aws:iam::444455556666:role/your_lambda_execution_role \ --environment Variables={RUST_BACKTRACE=1} \
--tracing-config Mode=Active
To build and run Guard as a Lambda function
To invoke the submitted cfn-guard-lambda as a Lambda function, run the following command.
aws lambda invoke --function-name rustTest \
--payload '{"data": "input data", "rules" : "input rules"}' \ output.json
To call the Lambda function request structure
Requests to cfn-guard-lambda require the following fields:
• data – The string version of the YAML or JSON template
• rules – The string version of the rule set file
Prerequisites
Getting started with AWS CloudFormation Guard
This section demonstrates how you can complete the core Guard tasks of writing, testing, and validating rules against JSON- or YAML-formatted structured data. In addition, it contains detailed walkthroughs that demonstrate writing rules that respond to specific use cases.
Topics
• Prerequisites (p. 8)
• Overview of using Guard rules (p. 8)
• Migrating Guard 1.0 rules to Guard 2.0 (p. 8)
• Writing AWS CloudFormation Guard rules (p. 10)
• Testing AWS CloudFormation Guard rules (p. 45)
• Validating input data against AWS CloudFormation Guard rules (p. 52)
Prerequisites
Before you can write policy rules using the Guard domain-specific language (DSL), you must install the Guard command line interface (CLI). For more information, see Setting up Guard (p. 3).
Overview of using Guard rules
When using Guard, you typically perform the following steps:
1. Write JSON- or YAML-formatted structured data to validate.
2. Write Guard policy rules. For more information, see Writing Guard rules (p. 10).
3. Verify that your rules work as intended by using the Guard test command. For more information about unit testing, see Testing Guard rules (p. 45).
4. Use the Guard validate command to validate your JSON- or YAML-formatted structured data against your rules. For more information, see Validating input data against Guard rules (p. 52).
Migrating Guard 1.0 rules to Guard 2.0
If you have written rules using Guard 1.0 syntax, we recommend that you migrate them to Guard 2.0 syntax. Guard 2.0 is backward incompatible with Guard 1.0 rules. Migrating involves updating the Guard CLI but doesn't require any changes to your rules files themselves.
Enhancements in Guard 2.0
The enhancements that are available in Guard 2.0 do the following:
Migrate a Guard rule
• Makes Guard general purpose by providing the ability to validate any JSON- and YAML-formatted structured data, against policy rules. You're no longer limited to using only AWS CloudFormation templates.
• Adds the Guard migrate (p. 56) CLI command so that you can migrate your Guard 1.0 rules to Guard 2.0 syntax.
• Adds the Guard parse-tree (p. 57) CLI command so that you can generate a parse tree for the rules defined in a Guard rules file.
• Adds the ability to validate additional CloudFormation template sections. Using Guard 1.0, you can write rules that validate the Resources section of your CloudFormation templates. Using Guard 2.0, you can write rules that validate all sections of your CloudFormation templates, including the Description and Parameters sections.
• Adds filtering capability so that you can select and further refine your selection of target resources or values against which to evaluate clauses.
• Introduces the query block feature that you can use to write rules that are more concise than those available with Guard 1.0. Using the query block feature, you can group relevant resource type properties together. For more information about query blocks, see Query blocks (p. 15).
• Introduces the named rule feature, which you can use to assign a name to a set of rules. Then, you can reference these modular validation blocks, called named-rule blocks, in other rules. Named-rule blocks promote reuse, improve composition, and remove verbosity and repetition. For more information about named-rule blocks, see Named-rule blocks (p. 17).
• Provides the ability to write unit tests and then verify that your rules work as expected using the Guard Testing Guard rules (p. 45) CLI command.
Migrate a Guard rule
To migrate a Guard 1.0 rule to Guard 2.0, do the following:
• Run the migrate command.
In the following example, for the --rules parameter, we specify the name of a Guard 1.0 rules file as rules.guard. For the --output parameter, we specify the file name migrated_rules.guard.
Guard creates migrated_rules.guard and then writes the migrated rules to this file.
cfn-guard migrate
--output migrated_rules.guard --rules rules.guard
The following are the contents of the rules.guard file, which is written in Guard 1.0 syntax.
let encryption_flag = true
AWS::EC2::Volume Encrypted == %encryption_flag AWS::EC2::Volume Size <= 100
After we run migrate on the rules.guard file, migrated_rules.guard contains the following rules, which are written in Guard 2.0 syntax.
rule migrated_rules {
let aws_ec2_volume = Resources.*[ Type == "AWS::EC2::Volume" ] let encryption_flag = true
%aws_ec2_volume.Properties.Encrypted == %encryption_flag %aws_ec2_volume.Properties.Size <= 100
Writing Guard rules
}
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 so that you can validate your JSON- or YAML-formatted data against multiple rule files at the same time.
Topics
• Clauses (p. 10)
• Using queries in clauses (p. 12)
• Using operators in clauses (p. 12)
• Using custom messages in clauses (p. 14)
• Combining clauses (p. 14)
• Using blocks with Guard rules (p. 15)
• Defining queries and filtering (p. 18)
• Assigning and referencing variables in AWS CloudFormation Guard rules (p. 27)
• Composing named-rule blocks in AWS CloudFormation Guard (p. 32)
• Writing clauses to perform context-aware evaluations (p. 36)
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/
Clauses
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 and so that you can access query results.
For more information about writing queries and filtering, see Defining queries and filtering (p. 18).
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 or integer(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), or char ranges expressed as:
• r[<lower_limit>, <upper_limit>], which translates to any value k that satisfies the following expression: lower_limit <= k <= upper_limit
• r[<lower_limit>, <upper_limit>), which translates to any value k that satisfies the following expression: lower_limit <= k < upper_limit
• r(<lower_limit>, <upper_limit>], which translates to any value k that satisfies the following expression: lower_limit < k <= upper_limit
• r(<lower_limit>, <upper_limit>), which translates to any value k 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 } ] } }
Using queries in clauses
• 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 and test 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 (p. 18) and Assigning and referencing variables in Guard rules (p. 27).
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
Using operators in clauses
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 (p. 18).
The following clause checks whether the template has one or more resources defined. It evaluates to PASS because a resource with the logical ID S3Bucket is defined in Template-1.
Resources !empty
The following clause checks whether one or more tags are defined for the S3Bucket resource. It evaluates to PASS because S3Bucket has two tags defined for the Tags property in Template-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 the S3Bucket.
It evaluates to PASS because BucketEncryption is defined for S3Bucket in Template-1.
Resources.S3Bucket.Properties.BucketEncryption exists
NoteThe 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 of string type.
The following clause checks whether a string value is specified for the BucketName property of the S3Bucket resource. It evaluates to PASS because the string value "MyServiceS3Bucket" is specified for BucketName in Template-1.
Resources.S3Bucket.Properties.BucketName is_string
• is_list – Checks whether each occurrence of the query is of list type.
The following clause checks whether a list is specified for the Tags property of the S3Bucket resource. It evaluates to PASS because two key-value pairs are specified for Tags in Template-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 the S3Bucket resource. It evaluates to PASS because BucketEncryption is specified using the ServerSideEncryptionConfiguration property type (object) in Template-1.
Using custom messages in clauses
NoteTo 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' ]
NoteThe 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 (p. 18) for details about usage and explore the examples directory in the cloudformation-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
Using blocks with Guard rules
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|ORs) 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
Using blocks with Guard rules
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 (p. 32).
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 ...
Using blocks with Guard rules
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 to uniquely identify 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
Defining queries and filtering
...
}
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 ...
}
Defining queries and filtering
This topic covers writing queries and using filtering when writing Guard rule clauses.
Prerequisites
Filtering is an advanced AWS CloudFormation Guard concept. We recommend that you review the following foundational topics before you learn about filtering:
• What is AWS CloudFormation Guard? (p. 1)
• Writing rules, clauses (p. 10)
Defining queries
Query expressions are simple dot (.) separated expressions written to traverse hierarchical data. Query expressions can include filter expressions to target a subset of values. When queries are evaluated, they result in a collection of values, similar to a result set returned from an SQL query.
The following example query searces a AWS CloudFormation template for AWS::IAM::Role resources.
Resources.*[ Type == 'AWS::IAM::Role' ]
Queries follow these basic principles:
• Each dot (.) part of the query traverses down the hierarchy when an explicit key term is used, such as Resources or Properties.Encrypted. If any part of the query doesn't match the incoming datum, Guard throws a retrieval error.
• A dot (.) part of the query that uses a wildcard * traverses all values for the structure at that level.
• A dot (.) part of the query that uses an array wildcard [*] traverses all indices for that array.
• All collections can be filtered by specifying filters inside square brackets []. Collections can be encountered in the following ways:
• Naturally occurring arrays in datum are collections. Following are examples:
Ports: [20, 21, 110, 190]
Tags: [{"Key": "Stage", "Value": "PROD"}, {"Key": "App", "Value":
"MyService"}]
• When traversing all values for a structure like Resources.*
Defining queries and filtering
• Any query result is itself a collection from which values can be further filtered. See the following example.
let all_resources = Resource.* # query let iam_resources = %resources[ Type == / IAM/ ] # filter from query results let managed_policies = %iam_resources[ Type == / ManagedPolicy/ ] # further refinements %managed_policies { # traversing each value # do something with each }
The following is an example CloudFormation template snippet.
Resources:
SampleRole:
Type: AWS::IAM::Role ...
SampleInstance:
Type: AWS::EC2::Instance ...
SampleVPC:
Type: AWS::EC2::VPC ...
SampleSubnet1:
Type: AWS::EC2::Subnet ...
SampleSubnet2:
Type: AWS::EC2::Subnet ...
Based on this template, the path traversed is SampleRole and the final value selected is Type:
AWS::IAM::Role.
Resources:
SampleRole:
Type: AWS::IAM::Role ...
The resulting value of the query Resources.*[ Type == 'AWS::IAM::Role' ] in YAML format is shown in the following example.
- Type: AWS::IAM::Role ...
Some of the ways that you can use queries are as follows:
• Assign a query to variables so that query results can be accessed by referencing those variables.
• Follow the query with a block that tests against each of the selected values.
• Compare a query directly against a basic clause.
Assigning queries to variables
Guard supports one-shot variable assignments within a given scope. For more information about variables in Guard rules, see Assigning and referencing variables in Guard rules (p. 27).
You can assign queries to variables so that you can write queries once and then reference them
elsewhere in your Guard rules. See the following example variable assignments that demonstrate query principles discussed later in this section.
Defining queries and filtering
## Simple query assignment
#let resources = Resources.* # All resources
## A more complex query here (this will be explained below)
#let iam_policies_allowing_log_creates = Resources.*[
Type in [/IAM::Policy/, /IAM::ManagedPolicy/]
some Properties.PolicyDocument.Statement[*] { some Action[*] == 'cloudwatch:CreateLogGroup' Effect == 'Allow'
} ]
Directly looping through values from a variable assigned to a query
Guard supports directly running against the results from a query. In the following example, the when block tests against the Encrypted, VolumeType, and AvailabilityZone property for each AWS::EC2::Volume resource found in a CloudFormation template.
let ec2_volumes = Resources.*[ Type == 'AWS::EC2::Volume' ] when %ec2_volumes !empty {
%ec2_volumes { Properties {
Encrypted == true
VolumeType in ['gp2', 'gp3']
AvailabilityZone in ['us-west-2b', 'us-west-2c']
} } }
Direct clause-level comparisons
Guard also supports queries as a part of direct comparisons. For example, see the following.
let resources = Resources.*
some %resources.Properties.Tags[*].Key == /PROD$/
some %resources.Properties.Tags[*].Value == /^App/
In the preceding example, the two clauses (starting with the some keyword) expressed in the form shown are considered independent clauses and are evaluated separately.
Single clause and block clause form
Taken together, the two example clauses shown in the preceding section aren't equivalent to the following block.
let resources = Resources.*
some %resources.Properties.Tags[*] { Key == /PROD$/
Value == /^App/
Defining queries and filtering
}
This block queries for each Tag value in the collection and compares its property values to the expected property values. The combined form of the clauses in the preceding section evaluates the two clauses independently. Consider the following input.
Resources:
...
MyResource:
...
Properties:
Tags:
- Key: EndPROD Value: NotAppStart - Key: NotPRODEnd Value: AppStart
Clauses in the first form evaluate to PASS. When validating the first clause in first form, the following path across Resources, Properties, Tags, and Key matches the value NotPRODEnd and does not match the expected value PROD.
Resources:
...
MyResource:
...
Properties:
Tags:
- Key: EndPROD Value: NotAppStart - Key: NotPRODEnd Value: AppStart
The same happens with the second clause of the first form. The path across Resources, Properties, Tags, and Value matches the value AppStart. As a result, the second clause independently.
The overall result is a PASS.
However, the block form evaluates as follows. For each Tags value, it compares if both the Key and Value does match; NotAppStart and NotPRODEnd values are not matched in the following example.
Resources:
...
MyResource:
...
Properties:
Tags:
- Key: EndPROD Value: NotAppStart - Key: NotPRODEnd Value: AppStart
Because evaluations check for both Key == /PROD$/, and Value == /^App/, the match is not complete. Therefore, the result is FAIL.
NoteWhen working with collections, we recommend that you use the block clause form when you want to compare multiple values for each element in the collection. Use the single clause form when the collection is a set of scalar values, or when you only intend to compare a single attribute.
Defining queries and filtering
Query outcomes and associated clauses
All queries return a list of values. Any part of a traversal, such as a missing key, empty values for an array (Tags: []) when accessing all indices, or missing values for a map when encountering an empty map (Resources: {}), can lead to retrieval errors.
All retrieval errors are considered failures when evaluating clauses against such queries. The only exception is when explicit filters are used in the query. When filters are used, associated clauses are skipped.
The following block failures are associated with running queries.
• If a template does not contain resources, then the query evaluates to FAIL, and the associated block level clauses also evaluate to FAIL.
• When a template contains an empty resources block like { "Resources": {} }, the query evaluates to FAIL, and the associated block level clauses also evaluate to FAIL.
• If a template contains resources but none match the query, then the query returns empty results, and the block level clauses are skipped.
Using filters in queries
Filters in queries are effectively Guard clauses that are used as selection criteria. Following is the structure of a clause.
<query> <operator> [query|value literal] [message] [or|OR]
Keep in mind the following key points from ??? (p. 10) when you work with filters:
• Combine clauses by using Conjunctive Normal Form (CNF).
• Specify each conjunction (and) clause on a new line.
• Specify disjunctions (or) by using the or keyword between two clauses.
The following example demonstrates conjunctive and disjunctive clauses.
resourceType == 'AWS::EC2::SecurityGroup' InputParameters.TcpBlockedPorts not empty InputParameters.TcpBlockedPorts[*] { this in r(100, 400] or
this in r(4000, 65535]
}
Using clauses for selection criteria
You can apply filtering to any collection. Filtering can be applied directly on attributes in the input that are already a collection like securityGroups: [....]. You can also apply filtering against a query, which is always a collection of values. You can use all features of clauses, including conjunctive normal form, for filtering.
The following common query is often used when selecting resources by type from a CloudFormation template.
Resources.*[ Type == 'AWS::IAM::Role' ]
Defining queries and filtering
The query Resources.* returns all values present in the Resources section of the input. For the example template input in Defining queries (p. 18), the query returns the following.
- Type: AWS::IAM::Role ...
- Type: AWS::EC2::Instance ...
- Type: AWS::EC2::VPC ...
- Type: AWS::EC2::Subnet ...
- Type: AWS::EC2::Subnet ...
Now, apply the filter against this collection. The criterion to match is Type == AWS::IAM::Role.
Following is the output of the query after the filter is applied.
- Type: AWS::IAM::Role ...
Next, check various clauses for AWS::IAM::Role resources.
let all_resources = Resources.*
let all_iam_roles = %all_resources[ Type == 'AWS::IAM::Role' ]
The following is an example filtering query that selects all AWS::IAM::Policy and AWS::IAM::ManagedPolicy resources.
Resources.*[
Type in [ /IAM::Policy/,
/IAM::ManagedPolicy/ ] ]
The following example checks if these policy resources have a PolicyDocument specified.
Resources.*[
Type in [ /IAM::Policy/,
/IAM::ManagedPolicy/ ] Properties.PolicyDocument exists ]
Building out more complex filtering needs
Consider the following example of an AWS Config configuration item for ingress and egress security groups information.
---
resourceType: 'AWS::EC2::SecurityGroup' configuration:
ipPermissions:
- fromPort: 172 ipProtocol: tcp toPort: 172 ipv4Ranges:
- cidrIp: 10.0.0.0/24 - cidrIp: 0.0.0.0/0 - fromPort: 89
Defining queries and filtering
ipProtocol: tcp ipv6Ranges:
- cidrIpv6: '::/0' toPort: 189
userIdGroupPairs: []
ipv4Ranges:
- cidrIp: 1.1.1.1/32 - fromPort: 89
ipProtocol: '-1' toPort: 189
userIdGroupPairs: []
ipv4Ranges:
- cidrIp: 1.1.1.1/32 ipPermissionsEgress:
- ipProtocol: '-1' ipv6Ranges: []
prefixListIds: []
userIdGroupPairs: []
ipv4Ranges:
- cidrIp: 0.0.0.0/0 ipRanges:
- 0.0.0.0/0 tags:
- key: Name
value: good-sg-delete-me vpcId: vpc-0123abcd
InputParameter:
TcpBlockedPorts:
- 3389 - 20 - 21 - 110 - 143
Note the following:
• ipPermissions (ingress rules) is a collection of rules inside a configuration block.
• Each rule structure contains attributes such as ipv4Ranges and ipv6Ranges to specify a collection of CIDR blocks.
Let’s write a rule that selects any ingress rules that allow connections from any IP address and verifies that the rules do not allow TCP blocked ports to be exposed.
Start with the query portion that covers IPv4, as shown in the following example.
configuration.ipPermissions[
#
# at least one ipv4Ranges equals ANY IPv4 #
some ipv4Ranges[*].cidrIp == '0.0.0.0/0' ]
The some keyword is useful in this context. All queries return a collection of values that match the query.
By default, Guard evaluates that all values returned as a result of the query are matched against checks.
However, this behavior might not always be what you need for checks. Consider the following part of the input from the configuration item.
ipv4Ranges:
- cidrIp: 10.0.0.0/24
- cidrIp: 0.0.0.0/0 # any IP allowed
Defining queries and filtering
There are two values present for ipv4Ranges. Not all ipv4Ranges values equal an IP address denoted by 0.0.0.0/0. You want to see if at least one value matches 0.0.0.0/0. You tell Guard that not all results returned from a query need to match, but at least one result must match. The some keyword tells Guard to ensure that one or more values from the resultant query match the check. If no query result values match, Guard throws an error.
Next, add IPv6, as shown in the following example.
configuration.ipPermissions[
#
# at-least-one ipv4Ranges equals ANY IPv4 #
some ipv4Ranges[*].cidrIp == '0.0.0.0/0' or #
# at-least-one ipv6Ranges contains ANY IPv6 #
some ipv6Ranges[*].cidrIpv6 == '::/0' ]
Finally, in the following example, validate that the protocol is not udp.
configuration.ipPermissions[
#
# at-least-one ipv4Ranges equals ANY IPv4 #
some ipv4Ranges[*].cidrIp == '0.0.0.0/0' or #
# at-least-one ipv6Ranges contains ANY IPv6 #
some ipv6Ranges[*].cidrIpv6 == '::/0' #
# and ipProtocol is not udp #
ipProtocol != 'udp' ] ]
The following is the complete rule.
rule any_ip_ingress_checks {
let ports = InputParameter.TcpBlockedPorts[*]
let targets = configuration.ipPermissions[
#
# if either ipv4 or ipv6 that allows access from any address #
some ipv4Ranges[*].cidrIp == '0.0.0.0/0' or some ipv6Ranges[*].cidrIpv6 == '::/0' #
# the ipProtocol is not UDP #
ipProtocol != 'udp' ]
when %targets !empty {
%targets {
ipProtocol != '-1' <<
result: NON_COMPLIANT
Defining queries and filtering
check_id: HUB_ID_2334
message: Any IP Protocol is allowed >>
when fromPort exists toPort exists {
let each_target = this %ports {
this < %each_target.fromPort or this > %each_target.toPort <<
result: NON_COMPLIANT check_id: HUB_ID_2340
message: Blocked TCP port was allowed in range >>
} } } }
}
Separating collections based on their contained types
When using infrastructure as code (IaC) configuration templates, you might encounter a collection that contains references to other entities within the configuration template. The following is an example CloudFormation template that describes Amazon Elastic Container Service (Amazon ECS) tasks with a local reference to TaskRoleArn, a reference to TaskArn, and a direct string reference.
Parameters:
TaskArn:
Type: String Resources:
ecsTask:
Type: 'AWS::ECS::TaskDefinition' Metadata:
SharedExectionRole: allowed Properties:
TaskRoleArn: 'arn:aws:....' ExecutionRoleArn: 'arn:aws:...' ecsTask2:
Type: 'AWS::ECS::TaskDefinition' Metadata:
SharedExectionRole: allowed Properties:
TaskRoleArn:
'Fn::GetAtt':
- iamRole - Arn
ExecutionRoleArn: 'arn:aws:...2' ecsTask3:
Type: 'AWS::ECS::TaskDefinition' Metadata:
SharedExectionRole: allowed Properties:
TaskRoleArn:
Ref: TaskArn
ExecutionRoleArn: 'arn:aws:...2' iamRole:
Type: 'AWS::IAM::Role' Properties:
PermissionsBoundary: 'arn:aws:...3'
Assigning and referencing variables in Guard rules
Consider the following query.
let ecs_tasks = Resources.*[ Type == 'AWS::ECS::TaskDefinition' ]
This query returns a collection of values that contains all three AWS::ECS::TaskDefinition resources shown in the example template. Separate ecs_tasks that contain TaskRoleArn local references from others, as shown in the following example.
let ecs_tasks = Resources.*[ Type == 'AWS::ECS::TaskDefinition' ] let ecs_tasks_role_direct_strings = %ecs_tasks[
Properties.TaskRoleArn is_string ] let ecs_tasks_param_reference = %ecs_tasks[
Properties.TaskRoleArn.'Ref' exists ] rule task_role_from_parameter_or_string { %ecs_tasks_role_direct_strings !empty or %ecs_tasks_param_reference !empty }
rule disallow_non_local_references {
# Known issue for rule access: Custom message must start on the same line not task_role_from_parameter_or_string
<<
result: NON_COMPLIANT
message: Task roles are not local to stack definition >>
}
Assigning and referencing variables in AWS CloudFormation Guard rules
You can assign variables in your AWS CloudFormation Guard rules files to store information that you want to reference in your Guard rules. Guard supports one-shot variable assignment. Variables are evaluated lazily, meaning that Guard only evaluates variables when rules are run.
Topics
• Assigning variables (p. 27)
• Referencing variables (p. 28)
• Variable scope (p. 28)
• Examples of variables in Guard rules files (p. 29)
Assigning variables
Use the let keyword to initialize and assign a variable. As a best practice, use snake case for variable names. Variables can store static literals or dynamic properties resulting from queries. In the following example, the variable ecs_task_definition_task_role_arn stores the static string value arn:aws:iam:123456789012:role/my-role-name.
let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-role-name'
In the following example, the variable ecs_tasks stores the results of a query that searches for all AWS::ECS::TaskDefinition resources in an AWS CloudFormation template. You could reference ecs_tasks to access information about those resources when you write rules.
Assigning and referencing variables in Guard rules
let ecs_tasks = Resources.*[
Type == 'AWS::ECS::TaskDefinition' ]
Referencing variables
Use the % prefix to reference a variable.
Based on the ecs_task_definition_task_role_arn variable example in Assigning
variables (p. 27), you can reference ecs_task_definition_task_role_arn in the query|value literal section of a Guard rule clause. Using that reference ensures that the value specified for the TaskDefinitionArn property of any AWS::ECS::TaskDefinition resources in a CloudFormation template is the static string value arn:aws:iam:123456789012:role/my-role-name.
Resources.*.Properties.TaskDefinitionArn == %ecs_task_definition_role_arn
Based on the ecs_tasks variable example in Assigning variables (p. 27), you can reference ecs_tasks in a query (for example, %ecs_tasks.Properties). First, Guard evaluates the variable ecs_tasks and then uses the returned values to traverse the hierarchy. If the variable ecs_tasks resolves to non-string values, then Guard throws an error.
NoteCurrently, Guard doesn't support referencing variables inside custom error messages.
Variable scope
Scope refers to the visibility of variables defined in a rules file. A variable name can only be used once within a scope. There are three levels where a variable can be declared, or three possible variable scopes:
• File-level – Usually declared at the top of the rules file, you can use file-level variables in all rules within the rules file. They are visible to the entire file.
In the following example rules file, the variables ecs_task_definition_task_role_arn and ecs_task_definition_execution_role_arn are initialized at the file-level.
let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-task-role- name'
let ecs_task_definition_execution_role_arn = 'arn:aws:iam::123456789012:role/my- execution-role-name'
rule check_ecs_task_definition_task_role_arn {
Resources.*.Properties.TaskRoleArn == %ecs_task_definition_task_role_arn }
rule check_ecs_task_definition_execution_role_arn {
Resources.*.Properties.ExecutionRoleArn == %ecs_task_definition_execution_role_arn }
• Rule-level – Declared within a rule, rule-level variables are only visible to that specific rule. Any references outside of the rule result in an error.
In the following example rules file, the variables ecs_task_definition_task_role_arn and ecs_task_definition_execution_role_arn are initialized at the rule-
level. The ecs_task_definition_task_role_arn can only be referenced within the check_ecs_task_definition_task_role_arn named rule. You can only
Assigning and referencing variables in Guard rules
reference the ecs_task_definition_execution_role_arn variable within the check_ecs_task_definition_execution_role_arn named rule.
rule check_ecs_task_definition_task_role_arn {
let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-task-role- name'
Resources.*.Properties.TaskRoleArn == %ecs_task_definition_task_role_arn }
rule check_ecs_task_definition_execution_role_arn {
let ecs_task_definition_execution_role_arn = 'arn:aws:iam::123456789012:role/my- execution-role-name'
Resources.*.Properties.ExecutionRoleArn == %ecs_task_definition_execution_role_arn }
• Block-level – Declared within a block, such as a when clause, block-level variables are only visible to that specific block. Any references outside of the block result in an error.
In the following example rules file, the variables ecs_task_definition_task_role_arn and ecs_task_definition_execution_role_arn are initialized at the block-
level within the AWS::ECS::TaskDefinition type block. You can only reference the
ecs_task_definition_task_role_arn and ecs_task_definition_execution_role_arn variables within the AWS::ECS::TaskDefinition type blocks for their respective rules.
rule check_ecs_task_definition_task_role_arn {
AWS::ECS::TaskDefinition {
let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-task- role-name'
Properties.TaskRoleArn == %ecs_task_definition_task_role_arn }
}
rule check_ecs_task_definition_execution_role_arn {
AWS::ECS::TaskDefinition {
let ecs_task_definition_execution_role_arn = 'arn:aws:iam::123456789012:role/my- execution-role-name'
Properties.ExecutionRoleArn == %ecs_task_definition_execution_role_arn }
}
Examples of variables in Guard rules files
The following sections provide examples of both static and dynamic assignment of variables.
Static assignment
The following is an example CloudFormation template.
Resources:
EcsTask:
Type: 'AWS::ECS::TaskDefinition' Properties:
TaskRoleArn: 'arn:aws:iam::123456789012:role/my-role-name'
Assigning and referencing variables in Guard rules
Based on this template, you can write a rule called check_ecs_task_definition_task_role_arn that ensures that the TaskRoleArn property of all AWS::ECS::TaskDefinition template resources is arn:aws:iam::123456789012:role/my-role-name.
rule check_ecs_task_definition_task_role_arn
{ let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-role-name' Resources.*.Properties.TaskRoleArn == %ecs_task_definition_task_role_arn
}
Within the scope of the rule, you can initialize a variable called
ecs_task_definition_task_role_arn and assign to it the static string value 'arn:aws:iam::123456789012:role/my-role-name'. The rule clause checks whether the value specified for the TaskRoleArn property of the EcsTask resource is arn:aws:iam::123456789012:role/my-role-name by referencing the
ecs_task_definition_task_role_arn variable in the query|value literal section.
Dynamic assignment
The following is an example CloudFormation template.
Resources:
EcsTask:
Type: 'AWS::ECS::TaskDefinition' Properties:
TaskRoleArn: 'arn:aws:iam::123456789012:role/my-role-name'
Based on this template, you can initialize a variable called ecs_tasks within the scope of the file and assign to it the query Resources.*[ Type == 'AWS::ECS::TaskDefinition'. Guard queries all resources in the input template and stores information about them in ecs_tasks.
You can also write a rule called check_ecs_task_definition_task_role_arn that ensures that the TaskRoleArn property of all AWS::ECS::TaskDefinition template resources is arn:aws:iam::123456789012:role/my-role-name
let ecs_tasks = Resources.*[
Type == 'AWS::ECS::TaskDefinition' ]
rule check_ecs_task_definition_task_role_arn {
%ecs_tasks.Properties.TaskRoleArn == 'arn:aws:iam::123456789012:role/my-role-name' }
The rule clause checks whether the value specified for the TaskRoleArn property of the EcsTask resource is arn:aws:iam::123456789012:role/my-role-name by referencing the ecs_task_definition_task_role_arn variable in the query section.
Enforcing AWS CloudFormation template configuration
Let’s walk through a more complex example of a production use case. In this example, we write Guard rules to ensure stricter controls on how Amazon ECS tasks are defined.
The following is an example CloudFormation template.
Resources:
EcsTask:
Type: 'AWS::ECS::TaskDefinition' Properties:
Assigning and referencing variables in Guard rules
TaskRoleArn:
'Fn::GetAtt': [TaskIamRole, Arn]
ExecutionRoleArn:
'Fn::GetAtt': [ExecutionIamRole, Arn]
TaskIamRole:
Type: 'AWS::IAM::Role' Properties:
PermissionsBoundary: 'arn:aws:iam::123456789012:policy/MyExamplePolicy' ExecutionIamRole:
Type: 'AWS::IAM::Role' Properties:
PermissionsBoundary: 'arn:aws:iam::123456789012:policy/MyExamplePolicy'
Based on this template, we write the following rules to ensure that these requirements are met:
• Each AWS::ECS::TaskDefinition resource in the template has both a task role and an execution role attached.
• The task roles and execution roles are AWS Identity and Access Management (IAM) roles.
• The roles are defined in the template.
• The PermissionsBoundary property is specified for each role.
# Select all Amazon ECS task definition resources from the template let ecs_tasks = Resources.*[
Type == 'AWS::ECS::TaskDefinition' ]
# Select a subset of task definitions whose specified value for the TaskRoleArn property is an Fn::Gett-retrievable attribute
let task_role_refs = some %ecs_tasks.Properties.TaskRoleArn.'Fn::GetAtt'[0]
# Select a subset of TaskDefinitions whose specified value for the ExecutionRoleArn property is an Fn::Gett-retrievable attribute
let execution_role_refs = some %ecs_tasks.Properties.ExecutionRoleArn.'Fn::GetAtt'[0]
# Verify requirement #1
rule all_ecs_tasks_must_have_task_end_execution_roles when %ecs_tasks !empty
{
%ecs_tasks.Properties { TaskRoleArn exists ExecutionRoleArn exists }
}
# Verify requirements #2 and #3 rule all_roles_are_local_and_type_IAM
when all_ecs_tasks_must_have_task_end_execution_roles {
let task_iam_references = Resources.%task_role_refs
let execution_iam_reference = Resources.%execution_role_refs when %task_iam_references !empty {
%task_iam_references.Type == 'AWS::IAM::Role' }
when %execution_iam_reference !empty {
%execution_iam_reference.Type == 'AWS::IAM::Role' }
}
Composing named-rule blocks
# Verify requirement #4
rule check_role_have_permissions_boundary
when all_ecs_tasks_must_have_task_end_execution_roles {
let task_iam_references = Resources.%task_role_refs
let execution_iam_reference = Resources.%execution_role_refs when %task_iam_references !empty {
%task_iam_references.Properties.PermissionsBoundary exists }
when %execution_iam_reference !empty {
%execution_iam_reference.Properties.PermissionsBoundary exists }
}
Composing named-rule blocks in AWS CloudFormation Guard
When writing named-rule blocks using AWS CloudFormation Guard, you can use the following two styles of composition:
• Conditional dependency
• Correlational dependency
Using either of these styles of dependency composition helps promote reusability and reduces verbosity and repetition in named-rule blocks.
Topics
• Prerequisites (p. 32)
• Conditional dependency composition (p. 32)
• Correlational dependency composition (p. 35)
Prerequisites
Learn about named-rule blocks in Writing rules (p. 17).
Conditional dependency composition
In this style of composition, the evaluation of a when block or a named-rule block has a conditional dependency on the evaluation result of one or more other named-rule blocks or clauses. The following example Guard rules file contains named-rule blocks that demonstrate conditional dependencies.
# Named-rule block, rule_name_A rule rule_name_A {
Guard_rule_1 Guard_rule_2 ...
}
# Example-1, Named-rule block, rule_name_B, takes a conditional dependency on rule_name_A rule rule_name_B when rule_name_A {
Guard_rule_3 Guard_rule_4 ...
}
Composing named-rule blocks
# Example-2, when block takes a conditional dependency on rule_name_A when rule_name_A {
Guard_rule_3 Guard_rule_4 ...
}
# Example-3, Named-rule block, rule_name_C, takes a conditional dependency on rule_name_A ^ rule_name_B
rule rule_name_C when rule_name_A rule_name_B { Guard_rule_3
Guard_rule_4 ...
}
# Example-4, Named-rule block, rule_name_D, takes a conditional dependency on (rule_name_A v clause_A) ^ clause_B ^ rule_name_B
rule rule_name_D when rule_name_A OR clause_A clause_B rule_name_B { Guard_rule_3
Guard_rule_4 ...
}
In the preceding example rules file, Example-1 has the following possible outcomes:
• If rule_name_A evaluates to PASS, the Guard rules encapsulated by rule_name_B are evaluated.
• If rule_name_A evaluates to FAIL, the Guard rules encapsulated by rule_name_B are not evaluated.
rule_name_B evaluates to SKIP.
• If rule_name_A evaluates to SKIP, the Guard rules encapsulated by rule_name_B are not evaluated.
rule_name_B evaluates to SKIP.
NoteThis case happens if rule_name_A conditionally depends on a rule that evaluates to FAIL and results in rule_name_A evaluating to SKIP.
Following is an example of a configuration management database (CMDB) configuration item from an AWS Config item for ingress and egress security groups information. This example demonstrates conditional dependency composition.
rule check_resource_type_and_parameter { resourceType == /AWS::EC2::SecurityGroup/
InputParameters.TcpBlockedPorts NOT EMPTY }
rule check_parameter_validity when check_resource_type_and_parameter { InputParameters.TcpBlockedPorts[*] {
this in r[0,65535]
} }
rule check_ip_procotol_and_port_range_validity when check_parameter_validity { let ports = InputParameters.TcpBlockedPorts[*]
#
# select all ipPermission instances that can be reached by ANY IP address # IPv4 or IPv6 and not UDP
#