AWS CloudFormation Guard 2.1.0 is a major release that includes new features, resolves bugs, and addresses feedback from the open source community.
New Features
- Parameterized Rules
- Directory bundle support for running tests and validations for templates
- Dynamic data lookup for inspection via multiple data files
- Support Code view and pinpoint location of errors
- Backwards compatible with
cfn-guard
2.x.x
Bug Fixes
- Fixed short-circuit on error condition with multiple resources of the same type. All errors are now displayed
- Filter by type attribute and logical name match
Issues Addressed
- #178 - [Enhancement] Add file name convention based test file selection for the test command
- #186 - Filter based on Resource Logical ID
- #202 - [BUG] Error message includes details of unrelated resource
- #203 -
cfn-guard validate —print-json
does not work - #204 - [Enhancement] Add CDKTemplate type to validation command
- #217 - [BUG]
IN
Statement prints out extraneous error info - #219 - [Enhancement] Add line number of analyzed file into the json report
Other Changes
- Create pull_request_template.md by @priyap286 in #181
- Retrieve version number dynamically from environment variable in code by @priyap286 in #179
- Small correction in cfn-guard lambda README.md by @priyap286 in #185
- Fix typos in QUERY_AND_FILTERING.md by @priyap286 in #188
- Created an issue template for a general issue by @priyap286 in #187
- feat: Parameterized rules by @dchakrav-github in #220
- feat: Parameterized rules by @dchakrav-github in #222
Full Changelog: 2.0.3...v2.1.0-pre-rc1
Details
Parameterized Rules
A user can leverage parameterized rules to write re-usable checks. User can use these checks to write Guard rules that works across several types of payloads such as AWS CloudFormation Templates, Terraform plans that use AWS CC, and AWS Config for asserting conditions.
Example of re-usable checks:
Please note that some properties are intentionally omitted for brevity.
Sample parameterized guard rule to check network config (click to expand)
#
# Top level doc type checks
#
let cfn_resources = Resources.*
let aws_config = configuration.*
rule is_cfn_doc_type when %cfn_resources !empty {
Resources exists
}
rule is_aws_config_doc_type when %aws_config !empty {
configuration exists
}
#
# ECS Service
#
rule deny_ecs_services_invalid_configuration when is_cfn_doc_type {
check_ecs_services_cfgs(Resources[ Type == 'AWS::ECS::Service' ].Properties)
}
rule deny_ecs_services_invalid_configuration when is_aws_config_doc_type
resourceType == 'AWS::ECS::Service'
{
check_ecs_services_cfgs(configuration)
}
#
# Example of network configuration checks across ECS TaskSets and ECS Service using a common rule:
#
# ECS TaskSet
#
rule deny_ecs_task_set_invalid_configuration when is_cfn_doc_type {
#
# For TaskSet, the property is NetworkConfiguration.Aws[V]pcConfiguration
#
check_ecs_network_config(
Resources[ Type == 'AWS::ECS::TaskSet' ]
.Properties
.NetworkConfiguration
.AwsVpcConfiguration)
}
#
# ECS Service check, common across AWS Config and CloudFormation
#
rule check_ecs_services_cfgs(ecs_service_cfgs) {
%ecs_service_cfgs {
EnableExecuteCommand not exists or
EnableExecuteCommand == false
<<Disallowed command executions for ECS services>>
#
# For ECS Service, the property is NetworkConfiguration.Aws[V]pcConfiguration
#
check_ecs_network_config(NetworkConfiguration.AwsVpcConfiguration)
}
}
#
# Check ECS network configuration common to TaskSet and Service
#
rule check_ecs_network_config(network_cfgs) {
%network_cfgs {
AssignPublicIp == 'DISABLED' or
AssignPublicIp == 'disabled'
<<Prevent assignment of public IP address to ECS services. AssignPublicIp must be DISABLE>>
}
}
Sample infrastructure template for an ECS cluster (click to expand)
Resources:
Cluster:
Type: AWS::ECS::Cluster
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: test
ContainerDefinitions:
- Name: test
Image: amazon/amazon-ecs-sample
Essential: true
Cpu: 256
Memory: 512
Service:
Type: AWS::ECS::Service
Properties:
Cluster:
Ref: Cluster
DeploymentController:
Type: EXTERNAL
DesiredCount: 0
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: DISABLED
TaskSet1:
Type: AWS::ECS::TaskSet
Properties:
Service:
Ref: Service
Cluster:
Ref: Cluster
TaskDefinition:
Ref: TaskDefinition
Scale:
Unit: PERCENT
Value: 100
LaunchType: EC2
ExternalId: task-set-001
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: DISABLED
Directory bundle support for running tests and validations for templates
Users can now evaluate all rules at once for both testing and validating. Users can run all tests for all rules by pointing to the top-level directory. Testing follows a simple naming convention to run the appropriate tests against the rules.
Testing Setup
- Let's begin with a sample directory, by name
guard-test-root
. Create a Guard rule and name itrule_01.guard
. - Create a sub-directory within
guard-test-root
calledtests
directory (name has to be verbatimtests
for this to work). Createrule_01_*.yaml
for success and failures. This names will very as per different rule names.
Directory structure
guard-test-root/
├── rule_01.guard
└── tests/
├── rule_01_cfn_fail.yaml
├── rule_01_cfn_success.yaml
├── rule_01_config_fail.yaml
└── rule_01_config_success.yaml
Command
Run the following command in the parent directory ofguard-test-root
.
cfn-guard test -d guard-test-root
Validate Setup
For validate, we have overloaded all 3 arguments to support directory as input --data
, --rules
as well as --input-parameters
. Let us demonstrate an example with using a directory to pass rules, but know that it works the same way with others.
- Starting with a root directory called
guard-validate-root
. Create a sub-directory calledrules
. - Add different Guard rule files in here.
rule_01.guard
,rule_02.guard
,rule_03.guard
. Please note we should use one of the supported extensions here. As of now,.guard
and.ruleset
are good for rules. - Go back to
guard-validate-root
. Create an infrastructure as code template to be validated astemplate.yaml
.
Directory structure
guard-validate-root/
├── template.yaml
└── rules/
├── rule_01.guard
├── rule_02.guard
└── rule_03.guard
Command
Navigate to guard-validate-root
and run the following command.
cfn-guard validate -r rules/ -d template.yaml
The validate
command will pick all rule file names with the following extensions and execute the checks:
*.guard
*.ruleset
More scenarios for validate
In a similar scenario, where we have multiple template data files in a directory named data
to be validated against all files in a rules
directory, we can run the following command.
cfn-guard validate -r rules/ -d data/
Where directory structure looks like the following:
guard-validate-root/
├── data/
| ├── template_01.yaml
| ├── template_02.yaml
| └── template_03.yaml
└── rules/
├── rule_01.guard
├── rule_02.guard
└── rule_03.guard
For a data directory passed as input, the validate
command will pick all rule file names with the following extensions and execute the checks:
*.yaml
*.yml
*.json
*.jsn
*.template
Thus, we have extended support for validating...
- a single data template against a single guard rule
- a single data template against multiple guard rules
- multiple data templates against a single guard rule
- multiple data templates against multiple guard rules
Apart from support for directory we also support multiple usages of the arguments with values of mixed nature (directory/file). For example, the following command is a valid command.
cfn-guard validate -r rules/ -r foo/rule_99.guard -d data/ -d bar/template_99.yaml
Dynamic data lookup for inspection via multiple data files
Users can now specify multiple data files for dynamic look ups using --input-parameters
argument, along with the independent context of a data file passed as --data
for actual validation inspection (e.g., the template that is the validation target).
All files passed as --input-parameters
are combined to form a common context. This common context is then combined with every file passed as --data
independently.
For example, network related data can be stored in a network.yaml
file. rule:
NETWORK:
allowed_security_groups: ["sg-282850", "sg-292040"]
allowed_prefix_lists: ["pl-63a5400a", "pl-02cd2c6b"]
This data will be used dynamically to look up as valid values in our Guard rule defined in a different security_groups.guard
file:
security_groups.guard (click to expand)
let groups = Resources.*[ Type == 'AWS::EC2::SecurityGroup' ]
let permitted_sgs = NETWORK.allowed_security_groups
let permitted_pls = NETWORK.allowed_prefix_lists
rule check_permitted_security_groups_or_prefix_lists(groups) {
%groups {
this in %permitted_sgs or
this in %permitted_pls
}
}
rule CHECK_PERMITTED_GROUPS when %groups !empty {
check_permitted_security_groups_or_prefix_lists(
%groups.Properties.GroupName
)
}
The above two will be used to validate a data template security_groups_fail.yaml
:
security_groups_fail.yaml (click to expand)
# ---
# AWSTemplateFormatVersion: 2010-09-09
# Description: CloudFormation - EC2 Security Group
Resources:
mySecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupName: "wrong"
cfn-guard validate -r security_groups.guard -i network.yaml -d security_groups_fail.yaml
The above command will validate the security_groups_fail.yaml
template against the rules in security-groups.guard
which use dynamic data from network.yaml