Validate Cloud Formation
Project description
CFN-Check
A tool for checking CloudFormation
| Package | cfn-check |
|---|---|
| Version | 0.13.4 |
| Download | https://pypi.org/project/cfn-check/ |
| Source | https://github.com/adalundhe/cfn-check |
| Keywords | cloud-formation, testing, aws, cli |
CFN-Check is a small, fast, friendly tool for validating AWS CloudFormation YAML templates. It is code-driven, with
rules written as simple, Rule decorator wrapped python class methods for Collection-inheriting classes.
Why CFN-Check?
AWS has its own tools for validating Cloud Formation - cfn-lint and cfn-guard. cfn-check aims to solve
problems inherint to cfn-lint more than cfn-guard, primarily:
- Confusing, unclear syntax around rules configuration
- Inability to parse non-resource wildcards
- Inability to validate non-resource template data
- Inabillity to use structured models to validate input
- Poor ability to parse and render CloudFormation Refs/Functions
In comparison to cfn-guard, cfn-check is pure Python, thus
avoiding YADSL (Yet Another DSL) headaches. It also proves
significantly more configurable/modular/hackable as a result.
cfn-check can resolve some (not all) CloudFormation Intrinsic
Functions and Refs.
CFN-Check uses a combination of simple depth-first-search tree
parsing, friendly cfn-lint like query syntax, Pydantic models,
and pytest-like assert-driven checks to make validating your
Cloud Formation easy while offering both CLI and Python API interfaces.
CFN-Check also uses a lightning-fast AST-parser to render your templates,
allowing you to validate policy, not just a YAML document.
Getting Started
cfn-check requires:
Python 3.12- Any number of valid CloudFormation templates or a path to said templates.
- A
.pyfile containing at least oneCollectionclass with at least one valid@Rule()decorated method
To get started (we recommend using uv), run:
uv venv
source .venv/bin/activate
uv pip install cfn-check
touch rules.py
touch template.yaml
Next open the rules.py file and create a basic Python class
as below.
from cfn_check import Collection, Rule
class ValidateResourceType(Collection):
@Rule(
"Resources::*::Type",
"It checks Resource::Type is correctly definined",
)
def validate_test(self, value: str):
assert value is not None, '❌ Resource Type not defined'
assert isinstance(value, str), '❌ Resource Type not a string'
This provides us a basic rule set that validates that the Type field of our CloudFormation template(s) exists and is the correct data type.
[!NOTE] Don't worry about adding an
__init__()method to this class!
Next open the template.yaml file and paste the following CloudFormation:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
ExistingSecurityGroups:
Type: List<AWS::EC2::SecurityGroup::Id>
ExistingVPC:
Type: AWS::EC2::VPC::Id
Description: The VPC ID that includes the security groups in the ExistingSecurityGroups parameter.
InstanceType:
Type: String
Default: t2.micro
AllowedValues:
- t2.micro
- m1.small
Mappings:
AWSInstanceType2Arch:
t2.micro:
Arch: HVM64
m1.small:
Arch: HVM64
AWSRegionArch2AMI:
us-east-1:
HVM64: ami-0ff8a91507f77f867
HVMG2: ami-0a584ac55a7631c0c
Resources:
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP traffic to the host
VpcId: !Ref ExistingVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
AllSecurityGroups:
Type: Custom::Split
Properties:
ServiceToken: !GetAtt AppendItemToListFunction.Arn
List: !Ref ExistingSecurityGroups
AppendedItem: !Ref SecurityGroup
AppendItemToListFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: !Join
- ''
- - var response = require('cfn-response');
- exports.handler = function(event, context) {
- ' var responseData = {Value: event.ResourceProperties.List};'
- ' responseData.Value.push(event.ResourceProperties.AppendedItem);'
- ' response.send(event, context, response.SUCCESS, responseData);'
- '};'
Runtime: nodejs20.x
MyEC2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: !FindInMap
- AWSRegionArch2AMI
- !Ref AWS::Region
- !FindInMap
- AWSInstanceType2Arch
- !Ref InstanceType
- Arch
SecurityGroupIds: !GetAtt AllSecurityGroups.Value
InstanceType: !Ref InstanceType
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: /
Policies:
- PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:*
Resource: arn:aws:logs:*:*:*
Outputs:
AllSecurityGroups:
Description: Security Groups that are associated with the EC2 instance
Value: !Join
- ', '
- !GetAtt AllSecurityGroups.Value
This represents a basic configuration for an AWS Lambda function.
Finally, run:
cfn-check validate -r rules.py template.yaml
which outputs:
2025-09-17T01:46:41.542078+00:00 - INFO - 19783474 - /Users/adalundhe/Documents/adalundhe/cfn-check/cfn_check/cli/validate.py:validate.80 - ✅ 1 validations met for 1 templates
Congrats! You've just made the cloud a bit better place!
Queries, Tokens, and Syntax
A cfn-check Query is a string made up of period (.) delimited "Tokens" centered around four primary types:
Keys-<KEY>: String name key Tokens that perform exact matching on keys of key/value pairs in a CloudFormation document.Values-(<KEY> = <VALUE>): Parenthesis-enclosedK==Vpairs that perform matching on values of key/value pairs in a CloudFormation document.Patterns-<\d+>: Arrow-enclosed regex pattern Tokens that perform pattern-based matching on keys of key/value pairs in a CloudFormation document.Ranges-[]: Brackets enclosed Tokens that perform array selection and filtering in a CloudFormation document.
In addition to Key, Value, Pattern, and Range selection, you can also incorporate:
Bounded Ranges-[<A>-<B>]: Exact matches from the starting position (if specified) to the end position (if specified) of an arrayIndicies-[<A>]: Exact matches the specified indicies of an arrayKey Ranges-[<KEY>]: Exact matches keys of objects within an arrayPattern Ranges([<\d+>]): Matches they keys of objects within an array based on the specified patternWildcards(*): Selects all values for a given object or array or returns the non-object/array value at the specified pathWildcard Ranges([*]): Selects all values for a given array and ensures that only the values of a valid array type are returned (any other type will be treated as a mismatch).
Working with Keys
Keys likely the most commos Token type you'll use in your queries. In fact, if you ran the example above, you already have! For example, with:
Resources
as your query, you'll select all items within the CloudFormation document under the Resources key.
Working with Values
In addition to searching by keys, filtering by the values associated with those keys is the most common way you'll traverse and validate your CloudFormation template. To filter Resource key matches by the value of their Type value to only return EC2 Instances, we would specify a query like:
Resources.*.(Type == AWS::EC2::Instance)
You can also match multiple values by utilizing the in/IN operator:
Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)
and providing a comma-delimited list of values.
[!NOTE] Value queries do not support Nested Ranges or nested queries, but do support Wildcards and Patterns. See below for more info!
Working with Patterns
If an object within a CloudFormation document contains multiple similar keys you want to select, Pattern Tokens are your go-to solution. Consider this segment of CloudFormation:
Resources:
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP traffic to the host
VpcId: !Ref ExistingVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
We want to select both SecurityGroupIngress and SecurityGroupEgress to perform the same rule evaluations. Since the keys for both blocks start with SecurityGroup, we could write a Query using a Pattern Token like:
Resources.SecurityGroup.Properties.<SecurityGroup>
which would allow us to use a single rule to evaluate both:
class ValidateSecurityGroups(Collection):
@Rule(
"Resources.SecurityGroup.Properties.<SecurityGroup>",
"It checks Security Groups are correctly definined",
)
def validate_security_groups(self, value: list[dict]):
assert len(value) > 0
for item in value:
protocol = item.get("IpProtocol")
assert isinstance(protocol, str)
assert protocol == "tcp"
from_port = item.get("FromPort")
assert isinstance(from_port, int)
assert from_port == 80
to_port = item.get('ToPort')
assert isinstance(to_port, int)
assert to_port == 80
cidr_ip = item.get('CidrIp')
assert isinstance(cidr_ip, str)
assert cidr_ip == '0.0.0.0/0'
Working with Wildcards
Wildcard Tokens allow you to select all matching objects, array entries, or values (given preceding tokens) within a CloudFormation document. Wildcard Tokens are powerful, allowing you to effectively destructure objects into their respective keys and values or arrays into their entries for easier filtering and checking.
In fact, you've already used one! In the first example, we use a Wildcard Token in the below query:
Resources.*.Type
To select all Resource objects, then further extract the Type field from each object. This helps us avoid copy-paste rules at the potential cost of deferring more work to individual Rule methods if we aren't careful and select too much!
Working with Ranges
Ranges allow you to perform sophisticated selection of objects or data within a CloudFormation document.
[!IMPORTANT] Range Tokens only work on arrays. This means that any values or other objects/data in the selected section of the CloudFormation document will be ignored and filtered out.
Unbounded Ranges
Unbounded ranges allow you to select and return an array in its entirety. For example:
Resources.SecurityGroup.Properties.SecurityGroupIngress.[]
Would return all SecurityGroupIngress objects in the CloudFormation document as a list, allowing you to check that the array of ingresses has been both defined and populated.
Indexes
Indexes allow you to select specific positions within an array. For example:
Resources.SecurityGroup.Properties.SecurityGroupIngress.[0]
Would return the first SecurityGroupIngress objects in the document.
Bounded Ranges
Bounded Ranges allow you to select subsets of indicies within an array (much like Python slicing). Unlike Python slicing, Bounded Ranges do not allow you to select a "step", however like Python slicing, starting positions are inclusive and end positions are exclusive (i.e. 0-10 will select from indexes 0 to 9)
As an example:
Resources.SecurityGroup.Properties.SecurityGroupIngress.[1-3]
Would select the second and third SecurityGroupIngress objects in the document.
Start or end positions are optional for Bounded Ranges. If a starting position is not defined, cfn-check will default to 0. Likewise, if an end position is not defined, cfn-check will default to the end of given list. For example:
Resources.SecurityGroup.Properties.SecurityGroupIngress.[-3]
selects the first through third SecurityGroupIngress objects in the document while:
Resources.SecurityGroup.Properties.SecurityGroupIngress.[3-]
selects the remaining SecurityGroupIngress objects starting from the third.
Key Ranges
Often times it's easier to match based upon an array's contents than by exact index. Key Ranges allow you to do this by matching the contents of each item in an array by:
- Exact match value comparison if the array value is not an object or array
- Single exact match value comparison if the array value is an array (i.e. there is at least one value exactly matching the Token in the array)
- Single exact match key comparison if the array value is an object
For example:
Resources.MyEC2Instance.Properties.ImageId.[AWSRegionArch2AMI]
returns only the EC2 ImageIds where the ImageId exactly matches AWSRegionArch2AMI.
Pattern Ranges
Pattern Ranges function much like Key Ranges, but utilize regex-based pattern matching for comparison. Adapting the above example:
Resources.MyEC2Instance.Properties.ImageId.[<^AWSRegion>]
returns only the EC2 ImageIds where the ImageId begins with AWSRegion. This can be helpful in checking for and enforcing naming standards, etc.
Wildcard Ranges
Wildcard Ranges extend the powerful functionality of Wildcard Tokens with the added safety of ensuring only arrays selected for further filtering or checks.
For example we know:
Resources.*.Type
Selects all Resource objects. If we convert the Wildcard Token in the query to a Wildcard Range Token:
Resources.[*].Type
The Rule will fail as below:
error: ❌ No results matching results for query Resources::[*]::Type
as we're selecting objects, not an array! A valid use would be in validating the deeply nested zipfile code of a Lambda's AppendItemToListFunction:
AppendItemToListFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: !Join
- ''
- - var response = require('cfn-response');
- exports.handler = function(event, context) {
- ' var responseData = {Value: event.ResourceProperties.List};'
- ' responseData.Value.push(event.ResourceProperties.AppendedItem);'
- ' response.send(event, context, response.SUCCESS, responseData);'
- '};'
Runtime: nodejs20.x
Note that the array we want is nested within another array, and we need to make sure we don't select the empty string that is the first element of the outer array!
We can accomplish this by using a Wildcard Range Token in our Query as below:
Resources.AppendItemToListFunction.Properties.Code.ZipFile.[*].[]
Which allows us to then evaluate the Unbounded Range token against each array item, returning only the array we want.
Using Multiple Tokens in Ranges
You can use multiple Tokens within a Range Token by seperating each token with a comma.
[!NOTE] While YAML does allow commas in keys, CloudFormation does not. As such, the case where a Pattern or Pattern Range might contain a comma is non-existent.
For example:
Resources.SecurityGroup.Properties.SecurityGroupIngress.[0, -2]
Would select all except the last element of an array.
This also applies to Bounded Ranges, Key Ranges, Pattern Ranges, and Wildcard Ranges! For example:
Resources.MyEC2Instance.Properties.ImageId.[(^AWSRegion),(^),(^Custom)]
will select any EC2 ImageIds that start with either AWSRegion or Custom.
Nested Ranges
CloudFormation often involes nested arrays, and navigating these can make for long and difficult-to-read Queries. To help reduce Query length, cfn-check supports nesting Range Tokens. For example, when evaluating:
ZipFile: !Join
- ''
- - var response = require('cfn-response');
- exports.handler = function(event, context) {
- ' var responseData = {Value: event.ResourceProperties.List};'
- ' responseData.Value.push(event.ResourceProperties.AppendedItem);'
- ' response.send(event, context, response.SUCCESS, responseData);'
- '};'
from our previous examples, we used the below query to select the nested array:
Resources.AppendItemToListFunction.Properties.Code.ZipFile.[*].[]
With Nested Ranges, this can be shortened to:
Resources.AppendItemToListFunction.Properties.Code.ZipFile.[[]]
Which is both more concise and more representitave of our intention to select only the array.
Grouping Queries
CFN-Check grouping allows you significant freedom of expression in how you write queries while also allowing you to more easily restrict and filter results by multiple criterion. Queries support both logical "or" and "and" statements via the | and & operators respectively. For example, consider the previous values query where we used an in operator:
Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)
This could be rewritten as:
Resources.*(Type == AWS::Lambda::Function | Type == AWS::Serverless::Function)
A more likely scenario might be finding specifically NodeJS Lambda functions. For example:
Resources.*.(Type == AWS::Lambda::Function,AWS::Serverless::Function & Properties.Runtime == <nodejs20>)
Queries are "split" by | operator and then "grouped" by & operator. That means if we want a query to match one set of criterion or another we could write:
Resources.*.(Type == AWS::Lambda::Function & Properties.Runtime == <nodejs20> | Type == AWS::Serverless::Function & Properties.Runtime == <nodejs20>)
Using Pydantic Models
In addition to traditional pytest-like assert statements, cfn-lint can validate results returned by queries via Pydantic models.
For example, consider again the initial example where we validate the Type field of Resource objects.
from cfn_check import Collection, Rule
class ValidateResourceType(Collection):
@Rule(
"Resources.*.Type",
"It checks Resource.Type is correctly definined",
)
def validate_test(self, value: str):
assert value is not None, '❌ Resource Type not defined'
assert isinstance(value, str), '❌ Resource Type not a string'
Rather than explicitly querying for the type field and writing assertions, we can instead define a Pydantic schema, then pass all Resource objects to that schema by specifying it as a Python type hint in our Rule method's signature.
from cfn_check import Collection, Rule
from pydantic import BaseModel, StrictStr
class Resource(BaseModel):
Type: StrictStr
class ValidateResourceType(Collection):
@Rule(
"Resources.*",
"It checks Resource.Type is correctly definined",
)
def validate_test(self, value: Resource):
assert value is not None
By deferring type and existence assertions to Pydantic models, you can focus your actual assertion logic on business/security policy checks.
Using .query()
Some of the most challenging validations to write in CFN-Guard or CFN-Lint are those requring validation of other template information against an existing selection. For example, validating that a Lambda has a LoggingGroup attached and specified within the same template.
CFN-Check makes performing these complex assertions intuitive and painless by allowing you to execute additional querieis within a rule via the .query() method. For example, to perform the LoggingGroup validation above you might write:
@Rule("Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)", "It validates a lambda is configured correctly")
def validate_lambda(self, lambda_resource: Lambda):
assert isinstance(lambda_resource, Lambda), "Not a valid Lambda"
log_groups = self.query(
f"Resources.{lambda_resource.Properties.LoggingConfig.LogGroup}",
transforms=[
lambda data: LoggingGroup(**data)
]
)
assert log_groups is not None, f"No resources found for LoggingGroup {lambda_resource.Properties.LoggingConfig.LogGroup}"
assert len(log_groups) > 0, "No matching logging group found in Resources for Lambda"
The query() method accepts the following parameters:
query - (str, required): A string CFN-Check query as used when defining Rules.document - (dict/list/any, optional): The filepath to a CloudFormation template document. This document must have been loaded by and specified to CFN-Check either via the default CLI path param, the-i/--import-valuesoptional CLI arg, or under theinput_valuesconfig key. This will cause the query specified in the call to execute against the template specified.transforms - (list[Function], optional): A list of functions that can be used to modify and filter returned results. In the example above, we use a single transform function to convert matches returned to aLoggingGroupPydantic model instance.
The Rendering Engine
Overview
In Version 0.6.X, CFN-Check introduced a rendering engine, which allows it to parse and execute Refs and all CloudFormation intrinsic functions via either the CloudFormation document or user-supplied values. This additional also resulted in the:
cfn-check render <TEMPLATE_PATH >
command being added, allowing you to effectively "dry run" render your
CloudFormation templates akin to the helm template command for Helm.
By default, cfn-check render outputs to stdout, however you can easily
save rendered output to a file via the -o/--output-file flag. For example:
cfn-check render template.yml -o rendered.yml
The cfn-check render command also offers the following options:
-a/--attributes: A list of = input!GetAttattributes to use-m/--mappings: A list of = inputMappingsto use-p/--parameters: A list of = inputParametersto use-l/--log-level: The log level to use
The Rendering Engine during Checks
By default rendering is enabled when running cfn-check validation. You can
disable it by supplying no-render to the -F/--flags option as below:
cfn-check validate -F no-render -r rules.py template.yaml
Disabling rendering means CFN-Check will validate your template as-is, with no additional pre-processing and no application of user input values.
[!WARNING] CloudFormation documents are not "plain yaml" and disabling rendering means any dynamically determined values will likely fail to pass validation, resulting in false positives for failures!
FAQ
How Do I Check or Render Multiple Files?
Both the render and validate commands accept an arbitrary number of filepaths. So for example:
cfn-check validate -r rules.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cfn_check-0.13.4.tar.gz.
File metadata
- Download URL: cfn_check-0.13.4.tar.gz
- Upload date:
- Size: 134.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21cd3d0908b62d6b1afd31bbe03f19b2325c1069996ecb846a77fac272b10dce
|
|
| MD5 |
19417f52953371a1c93cd360af777e6c
|
|
| BLAKE2b-256 |
b190a8fc482d0a04882edae6c3aa2ef748860781ac054c6b50b46f8fb9534fcc
|
File details
Details for the file cfn_check-0.13.4-py3-none-any.whl.
File metadata
- Download URL: cfn_check-0.13.4-py3-none-any.whl
- Upload date:
- Size: 148.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67c68fbc47857be34235a29bf59451cdefe88e4cc0f45dea03df1d576b444b41
|
|
| MD5 |
0f04d362a749f7e2a717c48a4d9d7779
|
|
| BLAKE2b-256 |
385b4b4968ca526351e142b046e077dd3312146bc3a0b53879ebb8675a19ed59
|