Name | cfn-check JSON |
Version |
0.3.3
JSON |
| download |
home_page | None |
Summary | Validate Cloud Formation |
upload_time | 2025-09-19 01:37:24 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.12 |
license | MIT License
Copyright (c) 2025 Ada Lündhé
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
|
keywords |
cloud-formation
testing
aws
cli
|
VCS |
 |
bugtrack_url |
|
requirements |
annotated-types
async-logging
hyperlight-cocoa
msgspec
pydantic
pydantic-core
pyyaml
typing-extensions
typing-inspection
zstandard
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# <b>CFN-Check</b>
<b>A tool for checking CloudFormation</b>
[](https://pypi.org/project/cfn-check/)
[](https://github.com/adalundhe/cfn-check/blob/main/LICENSE)
[](https://github.com/adalundhe/cfn-check/blob/main/CODE_OF_CONDUCT.md)
[](https://pypi.org/project/cfn-check/)
| Package | cfn-check |
| ----------- | ----------- |
| Version | 0.3.3 |
| 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.
<br/>
# 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
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 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.
<br/>
# Getting Started
`cfn-check` requires:
- `Python 3.12`
- Any number of valid CloudFormation templates or a path to said templates.
- A `.py` file containing at least one `Collection` class with at least one valid `@Rule()` decorated method
To get started (we recommend using `uv`), run:
```bash
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.
```python
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:
```yaml
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:
```bash
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!
<br/>
# Queries, Tokens, and Syntax
A `cfn-check` Query is a string made up of double-colon (`::`) delimited "Tokens" centered around three primary types:
- <b>`Keys`</b> - `<KEY>`: String name key Tokens that perform exact matching on keys of key/value pairs in a CloudFormation document.
- <b>`Patterns`</b> - `(\d+)`: Paren-enclosed regex pattern Tokens that perform pattern-based matching on keys of key/value pairs in a CloudFormation document.
- <b>`Ranges`</b> - `[]`: Brackets enclosed Tokens that perform array selection and filtering in a CloudFormation document.
In addition to `Key`, `Pattern`, and `Range` selection, you can also incorporate:
- <b>`Bounded Ranges`</b> - `[<A>-<B>]`: Exact matches from the starting position (if specified) to the end position (if specified) of an array
- <b>`Indicies`</b> - `[<A>]`: Exact matches the specified indicies of an array
- <b>`Key Ranges`</b> - `[<KEY>]`: Exact matches keys of objects within an array
- <b>`Pattern Ranges`</b> (`[(\d+)]`): Matches they keys of objects within an array based on the specified pattern
- <b>`Wildcards`</b> (`*`): Selects all values for a given object or array or returns the non-object/array value at the specified path
- <b>`Wildcard Ranges`</b> (`[*]`): 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 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:
```yaml
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 <i>both</i> `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:
```python
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`:
```yaml
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:
```yaml
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.
<br/>
# 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.
```python
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.
```python
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.
Raw data
{
"_id": null,
"home_page": null,
"name": "cfn-check",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.12",
"maintainer_email": null,
"keywords": "cloud-formation, testing, aws, cli",
"author": null,
"author_email": "Ada Lundhe <adalundhe@lundhe.audio>",
"download_url": "https://files.pythonhosted.org/packages/eb/ac/6947b7121eb597f0a300053cdfb7becb9fb88273a04e370c3acd9b72084c/cfn_check-0.3.3.tar.gz",
"platform": null,
"description": "# <b>CFN-Check</b>\n<b>A tool for checking CloudFormation</b>\n\n[](https://pypi.org/project/cfn-check/)\n[](https://github.com/adalundhe/cfn-check/blob/main/LICENSE)\n[](https://github.com/adalundhe/cfn-check/blob/main/CODE_OF_CONDUCT.md)\n[](https://pypi.org/project/cfn-check/)\n\n\n| Package | cfn-check |\n| ----------- | ----------- |\n| Version | 0.3.3 |\n| Download | https://pypi.org/project/cfn-check/ | \n| Source | https://github.com/adalundhe/cfn-check |\n| Keywords | cloud-formation, testing, aws, cli |\n\n\nCFN-Check is a small, fast, friendly tool for validating AWS CloudFormation YAML templates. It is code-driven, with \nrules written as simple, `Rule` decorator wrapped python class methods for `Collection`-inheriting classes.\n\n<br/>\n\n# Why CFN-Check?\n\nAWS has its own tools for validating Cloud Formation - `cfn-lint` and `cfn-guard`. `cfn-check` aims to solve\nproblems inherint to `cfn-lint` more than `cfn-guard`, primarily:\n\n- Confusing, unclear syntax around rules configuration\n- Inability to parse non-resource wildcards\n- Inability to validate non-resource template data\n- Inabillity to use structured models to validate input\n\nIn comparison to `cfn-guard`, `cfn-check` is pure Python, thus\navoiding YADSL (Yet Another DSL) headaches. It also proves\nsignificantly more configurable/modular/hackable as a result.\n\nCFN-Check uses a combination of simple depth-first-search tree\nparsing, friendly `cfn-lint` like query syntax, `Pydantic` models,\nand `pytest`-like assert-driven checks to make validating your\nCloud Formation easy while offering both CLI and Python API interfaces.\n\n<br/>\n\n# Getting Started\n\n`cfn-check` requires:\n\n- `Python 3.12`\n- Any number of valid CloudFormation templates or a path to said templates.\n- A `.py` file containing at least one `Collection` class with at least one valid `@Rule()` decorated method\n\nTo get started (we recommend using `uv`), run:\n\n```bash\nuv venv\nsource .venv/bin/activate\n\nuv pip install cfn-check\n\ntouch rules.py\ntouch template.yaml\n```\n\nNext open the `rules.py` file and create a basic Python class\nas below.\n\n```python\nfrom cfn_check import Collection, Rule\n\n\nclass ValidateResourceType(Collection):\n\n @Rule(\n \"Resources::*::Type\",\n \"It checks Resource::Type is correctly definined\",\n )\n def validate_test(self, value: str): \n assert value is not None, '\u274c Resource Type not defined'\n assert isinstance(value, str), '\u274c Resource Type not a string'\n```\n\nThis provides us a basic rule set that validates that the `Type` field of our CloudFormation template(s) exists and is the correct data type.\n\n> [!NOTE]\n> Don't worry about adding an `__init__()` method to this class!\n\nNext open the `template.yaml` file and paste the following CloudFormation:\n\n```yaml\nAWSTemplateFormatVersion: '2010-09-09'\nParameters:\n ExistingSecurityGroups:\n Type: List<AWS::EC2::SecurityGroup::Id>\n ExistingVPC:\n Type: AWS::EC2::VPC::Id\n Description: The VPC ID that includes the security groups in the ExistingSecurityGroups parameter.\n InstanceType:\n Type: String\n Default: t2.micro\n AllowedValues:\n - t2.micro\n - m1.small\nMappings:\n AWSInstanceType2Arch:\n t2.micro:\n Arch: HVM64\n m1.small:\n Arch: HVM64\n AWSRegionArch2AMI:\n us-east-1:\n HVM64: ami-0ff8a91507f77f867\n HVMG2: ami-0a584ac55a7631c0c\nResources:\n SecurityGroup:\n Type: AWS::EC2::SecurityGroup\n Properties:\n GroupDescription: Allow HTTP traffic to the host\n VpcId: !Ref ExistingVPC\n SecurityGroupIngress:\n - IpProtocol: tcp\n FromPort: 80\n ToPort: 80\n CidrIp: 0.0.0.0/0\n SecurityGroupEgress:\n - IpProtocol: tcp\n FromPort: 80\n ToPort: 80\n CidrIp: 0.0.0.0/0\n AllSecurityGroups:\n Type: Custom::Split\n Properties:\n ServiceToken: !GetAtt AppendItemToListFunction.Arn\n List: !Ref ExistingSecurityGroups\n AppendedItem: !Ref SecurityGroup\n AppendItemToListFunction:\n Type: AWS::Lambda::Function\n Properties:\n Handler: index.handler\n Role: !GetAtt LambdaExecutionRole.Arn\n Code:\n ZipFile: !Join\n - ''\n - - var response = require('cfn-response');\n - exports.handler = function(event, context) {\n - ' var responseData = {Value: event.ResourceProperties.List};'\n - ' responseData.Value.push(event.ResourceProperties.AppendedItem);'\n - ' response.send(event, context, response.SUCCESS, responseData);'\n - '};'\n Runtime: nodejs20.x\n MyEC2Instance:\n Type: AWS::EC2::Instance\n Properties:\n ImageId: !FindInMap\n - AWSRegionArch2AMI\n - !Ref AWS::Region\n - !FindInMap\n - AWSInstanceType2Arch\n - !Ref InstanceType\n - Arch\n SecurityGroupIds: !GetAtt AllSecurityGroups.Value\n InstanceType: !Ref InstanceType\n LambdaExecutionRole:\n Type: AWS::IAM::Role\n Properties:\n AssumeRolePolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n Service:\n - lambda.amazonaws.com\n Action:\n - sts:AssumeRole\n Path: /\n Policies:\n - PolicyName: root\n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Action:\n - logs:*\n Resource: arn:aws:logs:*:*:*\nOutputs:\n AllSecurityGroups:\n Description: Security Groups that are associated with the EC2 instance\n Value: !Join\n - ', '\n - !GetAtt AllSecurityGroups.Value\n```\n\nThis represents a basic configuration for an AWS Lambda function.\n\nFinally, run:\n\n```bash\ncfn-check validate -r rules.py template.yaml\n```\n\nwhich outputs:\n\n```\n2025-09-17T01:46:41.542078+00:00 - INFO - 19783474 - /Users/adalundhe/Documents/adalundhe/cfn-check/cfn_check/cli/validate.py:validate.80 - \u2705 1 validations met for 1 templates\n```\n\nCongrats! You've just made the cloud a bit better place!\n\n<br/>\n\n# Queries, Tokens, and Syntax\n\nA `cfn-check` Query is a string made up of double-colon (`::`) delimited \"Tokens\" centered around three primary types:\n\n- <b>`Keys`</b> - `<KEY>`: String name key Tokens that perform exact matching on keys of key/value pairs in a CloudFormation document.\n- <b>`Patterns`</b> - `(\\d+)`: Paren-enclosed regex pattern Tokens that perform pattern-based matching on keys of key/value pairs in a CloudFormation document.\n- <b>`Ranges`</b> - `[]`: Brackets enclosed Tokens that perform array selection and filtering in a CloudFormation document.\n\n\nIn addition to `Key`, `Pattern`, and `Range` selection, you can also incorporate:\n\n- <b>`Bounded Ranges`</b> - `[<A>-<B>]`: Exact matches from the starting position (if specified) to the end position (if specified) of an array\n- <b>`Indicies`</b> - `[<A>]`: Exact matches the specified indicies of an array\n- <b>`Key Ranges`</b> - `[<KEY>]`: Exact matches keys of objects within an array\n- <b>`Pattern Ranges`</b> (`[(\\d+)]`): Matches they keys of objects within an array based on the specified pattern\n- <b>`Wildcards`</b> (`*`): Selects all values for a given object or array or returns the non-object/array value at the specified path\n- <b>`Wildcard Ranges`</b> (`[*]`): 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).\n\n### Working with Keys\n\nKeys 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:\n\n```\nResources\n```\n\nas your query, you'll select all items within the CloudFormation document under the `Resources` key.\n\n### Working with Patterns\n\nIf 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:\n\n```yaml\nResources:\n SecurityGroup:\n Type: AWS::EC2::SecurityGroup\n Properties:\n GroupDescription: Allow HTTP traffic to the host\n VpcId: !Ref ExistingVPC\n SecurityGroupIngress:\n - IpProtocol: tcp\n FromPort: 80\n ToPort: 80\n CidrIp: 0.0.0.0/0\n SecurityGroupEgress:\n - IpProtocol: tcp\n FromPort: 80\n ToPort: 80\n CidrIp: 0.0.0.0/0\n```\n\nWe want to select <i>both</i> `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:\n\n```\nResources::SecurityGroup::Properties::(SecurityGroup)\n```\n\nwhich would allow us to use a single rule to evaluate both:\n\n```python\nclass ValidateSecurityGroups(Collection):\n\n @Rule(\n \"Resources::SecurityGroup::Properties::(SecurityGroup)\",\n \"It checks Security Groups are correctly definined\",\n )\n def validate_security_groups(self, value: list[dict]):\n assert len(value) > 0\n \n for item in value:\n protocol = item.get(\"IpProtocol\")\n assert isinstance(protocol, str)\n assert protocol == \"tcp\"\n \n from_port = item.get(\"FromPort\")\n assert isinstance(from_port, int)\n assert from_port == 80\n\n to_port = item.get('ToPort')\n assert isinstance(to_port, int)\n assert to_port == 80\n\n cidr_ip = item.get('CidrIp')\n assert isinstance(cidr_ip, str)\n assert cidr_ip == '0.0.0.0/0'\n```\n\n### Working with Wildcards\n\nWildcard 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.\n\nIn fact, you've already used one! In the first example, we use a Wildcard Token in the below query:\n\n```\nResources::*::Type\n```\n\nTo 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!\n\n### Working with Ranges\n\nRanges allow you to perform sophisticated selection of objects or data within a CloudFormation document.\n\n> [!IMPORTANT]\n> Range Tokens *only* work on arrays. This means that any\n> values or other objects/data in the selected section of the\n> CloudFormation document will be *ignored* and filtered out.\n\n#### Unbounded Ranges\n\nUnbounded ranges allow you to select and return an array in its entirety. For example:\n\n```\nResources::SecurityGroup::Properties::SecurityGroupIngress::[]\n```\n\nWould 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.\n\n\n#### Indexes\n\nIndexes allow you to select specific positions within an array. For example:\n\n```\nResources::SecurityGroup::Properties::SecurityGroupIngress::[0]\n```\n\nWould return the first SecurityGroupIngress objects in the document.\n\n\n#### Bounded Ranges\n\nBounded 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`)\n\n\nAs an example:\n\n```\nResources::SecurityGroup::Properties::SecurityGroupIngress::[1-3]\n```\n\nWould select the second and third SecurityGroupIngress objects in the document.\n\nStart 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:\n\n```\nResources::SecurityGroup::Properties::SecurityGroupIngress::[-3]\n```\n\nselects the first through third SecurityGroupIngress objects in the document while:\n\n```\nResources::SecurityGroup::Properties::SecurityGroupIngress::[3-]\n```\n\nselects the remaining SecurityGroupIngress objects starting from the third.\n\n\n#### Key Ranges\n\nOften 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:\n\n- Exact match value comparison if the array value is not an object or array\n- 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)\n- Single exact match key comparison if the array value is an object\n\nFor example:\n\n```\nResources::MyEC2Instance::Properties::ImageId::[AWSRegionArch2AMI]\n```\n\nreturns only the EC2 ImageIds where the ImageId exactly matches `AWSRegionArch2AMI`.\n\n\n#### Pattern Ranges\n\nPattern Ranges function much like Key Ranges, but utilize regex-based pattern matching for comparison. Adapting the above example:\n\n```\nResources::MyEC2Instance::Properties::ImageId::[(^AWSRegion)]\n```\n\nreturns only the EC2 ImageIds where the ImageId begins with `AWSRegion`. This can be helpful in checking for and enforcing naming standards, etc.\n\n\n#### Wildcard Ranges\n\nWildcard Ranges extend the powerful functionality of Wildcard Tokens with the added safety of ensuring *only* arrays selected for further filtering or checks. \n\nFor example we know:\n\n```\nResources::*::Type\n```\n\nSelects all `Resource` objects. If we convert the Wildcard Token in the query to a Wildcard Range Token:\n\n```\nResources::*::Type\n```\n\nThe Rule will fail as below:\n\n```\nerror: \u274c No results matching results for query Resources::[*]::Type\n```\n\nas we're selecting objects, not an array! A valid use would be in validating the deeply nested zipfile code of a Lambda's `AppendItemToListFunction`:\n\n```yaml\n AppendItemToListFunction:\n Type: AWS::Lambda::Function\n Properties:\n Handler: index.handler\n Role: !GetAtt LambdaExecutionRole.Arn\n Code:\n ZipFile: !Join\n - ''\n - - var response = require('cfn-response');\n - exports.handler = function(event, context) {\n - ' var responseData = {Value: event.ResourceProperties.List};'\n - ' responseData.Value.push(event.ResourceProperties.AppendedItem);'\n - ' response.send(event, context, response.SUCCESS, responseData);'\n - '};'\n Runtime: nodejs20.x\n```\n\nNote 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!\n\nWe can accomplish this by using a Wildcard Range Token in our Query as below:\n\n```\nResources::AppendItemToListFunction::Properties::Code::ZipFile::[*]::[]\n```\n\nWhich allows us to then evaluate the Unbounded Range token against each array item, returning only the array we want.\n\n### Using Multiple Tokens in Ranges\n\nYou can use multiple Tokens within a Range Token by seperating each token with a comma.\n\n> [!NOTE]\n> While YAML does allow commas in keys, CloudFormation does not.\n> As such, the case where a Pattern or Pattern Range might\n> contain a comma is non-existent.\n\nFor example:\n\n```\nResources::SecurityGroup::Properties::SecurityGroupIngress::[0, -2]\n```\n\nWould select all except the last element of an array.\n\nThis also applies to Bounded Ranges, Key Ranges, Pattern Ranges, and Wildcard Ranges! For example:\n\n```\nResources::MyEC2Instance::Properties::ImageId::[(^AWSRegion),(^),(^Custom)]\n```\n\nwill select any EC2 ImageIds that start with either `AWSRegion` or `Custom`.\n\n\n### Nested Ranges\n\nCloudFormation 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:\n\n```yaml\nZipFile: !Join\n - ''\n - - var response = require('cfn-response');\n - exports.handler = function(event, context) {\n - ' var responseData = {Value: event.ResourceProperties.List};'\n - ' responseData.Value.push(event.ResourceProperties.AppendedItem);'\n - ' response.send(event, context, response.SUCCESS, responseData);'\n - '};'\n```\n\nfrom our previous examples, we used the below query to select the nested array:\n\n```\nResources::AppendItemToListFunction::Properties::Code::ZipFile::[*]::[]\n```\n\nWith Nested Ranges, this can be shortened to:\n\n```\nResources::AppendItemToListFunction::Properties::Code::ZipFile::[[]]\n```\n\nWhich is both more concise *and* more representitave of our intention to select only the array.\n\n<br/>\n\n# Using Pydantic Models\n\nIn addition to traditional `pytest`-like assert statements, `cfn-lint` can validate results returned by queries via `Pydantic` models.\n\nFor example, consider again the initial example where we validate the `Type` field of `Resource` objects.\n\n```python\nfrom cfn_check import Collection, Rule\n\n\nclass ValidateResourceType(Collection):\n\n @Rule(\n \"Resources::*::Type\",\n \"It checks Resource::Type is correctly definined\",\n )\n def validate_test(self, value: str): \n assert value is not None, '\u274c Resource Type not defined'\n assert isinstance(value, str), '\u274c Resource Type not a string'\n```\n\nRather 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.\n\n```python\nfrom cfn_check import Collection, Rule\nfrom pydantic import BaseModel, StrictStr\n\nclass Resource(BaseModel):\n Type: StrictStr\n\n\nclass ValidateResourceType(Collection):\n\n @Rule(\n \"Resources::*\",\n \"It checks Resource::Type is correctly definined\",\n )\n def validate_test(self, value: Resource):\n assert value is not None\n```\n\nBy deferring type and existence assertions to `Pydantic` models, you can focus your actual assertion logic on business/security policy checks.\n",
"bugtrack_url": null,
"license": "MIT License\n \n Copyright (c) 2025 Ada L\u00fcndh\u00e9\n \n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n \n The above copyright notice and this permission notice shall be included in all\n copies or substantial portions of the Software.\n \n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n SOFTWARE.\n ",
"summary": "Validate Cloud Formation",
"version": "0.3.3",
"project_urls": {
"Homepage": "https://github.com/adalundhe/cfn-check"
},
"split_keywords": [
"cloud-formation",
" testing",
" aws",
" cli"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "2aff891659ea68bd4885a9cecbcb1bf1216aaa433ed9249f053a34cc47786ac1",
"md5": "ecb695508565bb87e09a4846893f1dcc",
"sha256": "568ca8178c628080ebdb1acab221a1a350df7838382d754a74f728558bdab040"
},
"downloads": -1,
"filename": "cfn_check-0.3.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "ecb695508565bb87e09a4846893f1dcc",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.12",
"size": 22181,
"upload_time": "2025-09-19T01:37:23",
"upload_time_iso_8601": "2025-09-19T01:37:23.081330Z",
"url": "https://files.pythonhosted.org/packages/2a/ff/891659ea68bd4885a9cecbcb1bf1216aaa433ed9249f053a34cc47786ac1/cfn_check-0.3.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "ebac6947b7121eb597f0a300053cdfb7becb9fb88273a04e370c3acd9b72084c",
"md5": "8e234ad6296c58e06b0226960be7b1b8",
"sha256": "1439b9ee461cbeed7f5fa059f0cab4437992601b3445c0ce74cba8090638a705"
},
"downloads": -1,
"filename": "cfn_check-0.3.3.tar.gz",
"has_sig": false,
"md5_digest": "8e234ad6296c58e06b0226960be7b1b8",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.12",
"size": 22884,
"upload_time": "2025-09-19T01:37:24",
"upload_time_iso_8601": "2025-09-19T01:37:24.326404Z",
"url": "https://files.pythonhosted.org/packages/eb/ac/6947b7121eb597f0a300053cdfb7becb9fb88273a04e370c3acd9b72084c/cfn_check-0.3.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-09-19 01:37:24",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "adalundhe",
"github_project": "cfn-check",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [
{
"name": "annotated-types",
"specs": [
[
"==",
"0.7.0"
]
]
},
{
"name": "async-logging",
"specs": [
[
"==",
"0.3.8"
]
]
},
{
"name": "hyperlight-cocoa",
"specs": [
[
"==",
"0.4.1"
]
]
},
{
"name": "msgspec",
"specs": [
[
"==",
"0.19.0"
]
]
},
{
"name": "pydantic",
"specs": [
[
"==",
"2.11.9"
]
]
},
{
"name": "pydantic-core",
"specs": [
[
"==",
"2.33.2"
]
]
},
{
"name": "pyyaml",
"specs": [
[
"==",
"6.0.2"
]
]
},
{
"name": "typing-extensions",
"specs": [
[
"==",
"4.15.0"
]
]
},
{
"name": "typing-inspection",
"specs": [
[
"==",
"0.4.1"
]
]
},
{
"name": "zstandard",
"specs": [
[
"==",
"0.25.0"
]
]
}
],
"lcname": "cfn-check"
}