cfn-check 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cfn-check might be problematic. Click here for more details.
- cfn_check/cli/utils/files.py +1 -1
- cfn_check/cli/validate.py +8 -10
- cfn_check/evaluation/evaluator.py +85 -0
- cfn_check/evaluation/parsing/__init__.py +1 -0
- cfn_check/evaluation/parsing/query_parser.py +145 -0
- cfn_check/evaluation/parsing/token.py +269 -0
- cfn_check/evaluation/parsing/token_type.py +14 -0
- cfn_check/evaluation/validate.py +56 -47
- cfn_check-0.3.0.dist-info/METADATA +541 -0
- {cfn_check-0.2.1.dist-info → cfn_check-0.3.0.dist-info}/RECORD +15 -12
- example/rules.py +65 -6
- cfn_check/evaluation/check.py +0 -20
- cfn_check/evaluation/search.py +0 -137
- cfn_check-0.2.1.dist-info/METADATA +0 -247
- {cfn_check-0.2.1.dist-info → cfn_check-0.3.0.dist-info}/WHEEL +0 -0
- {cfn_check-0.2.1.dist-info → cfn_check-0.3.0.dist-info}/entry_points.txt +0 -0
- {cfn_check-0.2.1.dist-info → cfn_check-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {cfn_check-0.2.1.dist-info → cfn_check-0.3.0.dist-info}/top_level.txt +0 -0
cfn_check/evaluation/search.py
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from collections import deque
|
|
3
|
-
from typing import Deque
|
|
4
|
-
from cfn_check.shared.types import (
|
|
5
|
-
Data,
|
|
6
|
-
Items,
|
|
7
|
-
YamlList,
|
|
8
|
-
YamlObject,
|
|
9
|
-
YamlValueBase,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
numbers_pattern = re.compile(r'\d+')
|
|
14
|
-
|
|
15
|
-
def search(
|
|
16
|
-
resources: YamlObject,
|
|
17
|
-
path: str,
|
|
18
|
-
):
|
|
19
|
-
items: Items = deque()
|
|
20
|
-
items.append(resources)
|
|
21
|
-
|
|
22
|
-
segments = path.split("::")[::-1]
|
|
23
|
-
# Queries can be multi-segment,
|
|
24
|
-
# so we effectively perform per-segment
|
|
25
|
-
# repeated DFS searches, returning the matches
|
|
26
|
-
# for each segment
|
|
27
|
-
|
|
28
|
-
composite_keys: list[str] = []
|
|
29
|
-
|
|
30
|
-
while len(segments):
|
|
31
|
-
query = segments.pop()
|
|
32
|
-
items, keys = search_with_query(items, query)
|
|
33
|
-
|
|
34
|
-
if len(composite_keys) == 0:
|
|
35
|
-
composite_keys.extend(keys)
|
|
36
|
-
|
|
37
|
-
else:
|
|
38
|
-
updated_keys: list[str] = []
|
|
39
|
-
for composite_key in composite_keys:
|
|
40
|
-
while len(keys):
|
|
41
|
-
key = keys.pop()
|
|
42
|
-
|
|
43
|
-
updated_keys.append(f'{composite_key}.{key}')
|
|
44
|
-
|
|
45
|
-
composite_keys = updated_keys
|
|
46
|
-
|
|
47
|
-
results: list[tuple[str, Data]] = []
|
|
48
|
-
for idx, item in enumerate(list(items)):
|
|
49
|
-
results.append((
|
|
50
|
-
composite_keys[idx],
|
|
51
|
-
item,
|
|
52
|
-
))
|
|
53
|
-
|
|
54
|
-
return results
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def parse_list_query(query: str):
|
|
58
|
-
|
|
59
|
-
queries = query.strip('[]').split(',')
|
|
60
|
-
|
|
61
|
-
if len(queries) < 1:
|
|
62
|
-
return None
|
|
63
|
-
|
|
64
|
-
indexes = []
|
|
65
|
-
for query in queries:
|
|
66
|
-
|
|
67
|
-
if match := numbers_pattern.match(query):
|
|
68
|
-
indexes.append(
|
|
69
|
-
int(match.group(0))
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
return indexes
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def parse_list_matches(
|
|
76
|
-
query: str,
|
|
77
|
-
node: YamlList,
|
|
78
|
-
):
|
|
79
|
-
if indexes := parse_list_query(query):
|
|
80
|
-
return [
|
|
81
|
-
item
|
|
82
|
-
for idx, item in enumerate(node)
|
|
83
|
-
if idx in indexes
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
return [
|
|
87
|
-
str(idx) for idx in indexes
|
|
88
|
-
], node
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def search_with_query(
|
|
92
|
-
items: Items,
|
|
93
|
-
query: str,
|
|
94
|
-
) -> tuple[Items, Deque[str]]:
|
|
95
|
-
|
|
96
|
-
found: Items = deque()
|
|
97
|
-
|
|
98
|
-
keys: Deque[str] = deque()
|
|
99
|
-
|
|
100
|
-
while len(items):
|
|
101
|
-
node = items.pop()
|
|
102
|
-
|
|
103
|
-
key: (
|
|
104
|
-
str | None
|
|
105
|
-
) = None
|
|
106
|
-
value: (
|
|
107
|
-
YamlValueBase | YamlList | YamlObject | None
|
|
108
|
-
) = None
|
|
109
|
-
|
|
110
|
-
if isinstance(node, dict):
|
|
111
|
-
items.extend(node.items())
|
|
112
|
-
|
|
113
|
-
elif query.startswith('[') and query.startswith(']'):
|
|
114
|
-
indexes, matched = parse_list_matches(query, node)
|
|
115
|
-
keys.extend(indexes)
|
|
116
|
-
found.extend(matched)
|
|
117
|
-
|
|
118
|
-
elif isinstance(node, list):
|
|
119
|
-
items.extend(node)
|
|
120
|
-
|
|
121
|
-
elif isinstance(node, tuple):
|
|
122
|
-
key, value = node
|
|
123
|
-
|
|
124
|
-
else:
|
|
125
|
-
# If we encounter just
|
|
126
|
-
# a raw YAML int/bool/etc
|
|
127
|
-
# then we should just
|
|
128
|
-
# skip to the next iteration
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
key == query or query == "*"
|
|
133
|
-
) and value:
|
|
134
|
-
keys.append(key)
|
|
135
|
-
found.append(value)
|
|
136
|
-
|
|
137
|
-
return found, keys
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: cfn-check
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: Validate Cloud Formation
|
|
5
|
-
Author-email: Ada Lundhe <adalundhe@lundhe.audio>
|
|
6
|
-
License: MIT License
|
|
7
|
-
|
|
8
|
-
Copyright (c) 2025 Ada Lündhé
|
|
9
|
-
|
|
10
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
-
in the Software without restriction, including without limitation the rights
|
|
13
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
-
furnished to do so, subject to the following conditions:
|
|
16
|
-
|
|
17
|
-
The above copyright notice and this permission notice shall be included in all
|
|
18
|
-
copies or substantial portions of the Software.
|
|
19
|
-
|
|
20
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
-
SOFTWARE.
|
|
27
|
-
|
|
28
|
-
Project-URL: Homepage, https://github.com/adalundhe/cfn-check
|
|
29
|
-
Keywords: cloud-formation,testing,aws,cli
|
|
30
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
31
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
32
|
-
Classifier: Operating System :: OS Independent
|
|
33
|
-
Requires-Python: >=3.12
|
|
34
|
-
Description-Content-Type: text/markdown
|
|
35
|
-
License-File: LICENSE
|
|
36
|
-
Requires-Dist: pydantic
|
|
37
|
-
Requires-Dist: pyyaml
|
|
38
|
-
Requires-Dist: hyperlight-cocoa
|
|
39
|
-
Requires-Dist: async-logging
|
|
40
|
-
Dynamic: license-file
|
|
41
|
-
|
|
42
|
-
# <b>CFN-Check</b>
|
|
43
|
-
<b>A tool for checking CloudFormation</b>
|
|
44
|
-
|
|
45
|
-
[](https://pypi.org/project/cfn-check/)
|
|
46
|
-
[](https://github.com/adalundhe/cfn-check/blob/main/LICENSE)
|
|
47
|
-
[](https://github.com/adalundhe/cfn-check/blob/main/CODE_OF_CONDUCT.md)
|
|
48
|
-
[](https://pypi.org/project/cfn-check/)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
| Package | cfn-check |
|
|
52
|
-
| ----------- | ----------- |
|
|
53
|
-
| Version | 0.2.0 |
|
|
54
|
-
| Download | https://pypi.org/project/cfn-check/ |
|
|
55
|
-
| Source | https://github.com/adalundhe/cfn-check |
|
|
56
|
-
| Keywords | cloud-formation, testing, aws, cli |
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
CFN-Check is a small, fast, friendly tool for validating AWS CloudFormation YAML templates. It is code-driven, with
|
|
60
|
-
rules written as simple, `Rule` decorator wrapped python class methods for `Collection`-inheriting classes.
|
|
61
|
-
|
|
62
|
-
<br/>
|
|
63
|
-
|
|
64
|
-
# Why CFN-Check?
|
|
65
|
-
|
|
66
|
-
AWS has its own tools for validating Cloud Formation - `cfn-lint` and `cfn-guard`. `cfn-check` aims to solve
|
|
67
|
-
problems inherint to `cfn-lint` more than `cfn-guard`, primarily:
|
|
68
|
-
|
|
69
|
-
- Confusing, unclear syntax around rules configuration
|
|
70
|
-
- Inability to parse non-resource wildcards
|
|
71
|
-
- Inability to validate non-resource template data
|
|
72
|
-
- Inabillity to use structured models to validate input
|
|
73
|
-
|
|
74
|
-
In comparison to `cfn-guard`, `cfn-check` is pure Python, thus
|
|
75
|
-
avoiding YADSL (Yet Another DSL) headaches. It also proves
|
|
76
|
-
significantly more configurable/modular/hackable as a result.
|
|
77
|
-
|
|
78
|
-
CFN-Check uses a combination of simple depth-first-search tree
|
|
79
|
-
parsing, friendly `cfn-lint` like query syntax, `Pydantic` models,
|
|
80
|
-
and `pytest`-like assert-driven checks to make validating your
|
|
81
|
-
Cloud Formation easy while offering both CLI and Python API interfaces.
|
|
82
|
-
|
|
83
|
-
<br/>
|
|
84
|
-
|
|
85
|
-
# Getting Started
|
|
86
|
-
|
|
87
|
-
`cfn-check` requires:
|
|
88
|
-
|
|
89
|
-
- `Python 3.12`
|
|
90
|
-
- Any number of valid CloudFormation templates or a path to said templates.
|
|
91
|
-
- A `.py` file containing at least one `Collection` class with at least one valid `@Rule()` decorated method
|
|
92
|
-
|
|
93
|
-
To get started (we recommend using `uv`), run:
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
uv venv
|
|
97
|
-
source .venv/bin/activate
|
|
98
|
-
|
|
99
|
-
uv pip install cfn-check
|
|
100
|
-
|
|
101
|
-
touch rules.py
|
|
102
|
-
touch template.yaml
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Next open the `rules.py` file and create a basic Python class
|
|
106
|
-
as below.
|
|
107
|
-
|
|
108
|
-
```python
|
|
109
|
-
from cfn_check import Collection, Rule
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class ValidateResourceType(Collection):
|
|
113
|
-
|
|
114
|
-
@Rule(
|
|
115
|
-
"Resources::*::Type",
|
|
116
|
-
"It checks Resource::Type is correctly definined",
|
|
117
|
-
)
|
|
118
|
-
def validate_test(self, value: str):
|
|
119
|
-
assert value is not None, '❌ Resource Type not defined'
|
|
120
|
-
assert isinstance(value, str), '❌ Resource Type not a string'
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
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.
|
|
124
|
-
|
|
125
|
-
> [!NOTE]
|
|
126
|
-
> Don't worry about adding an `__init__()` method to this class!
|
|
127
|
-
|
|
128
|
-
Next open the `template.yaml` file and paste the following CloudFormation:
|
|
129
|
-
|
|
130
|
-
```yaml
|
|
131
|
-
AWSTemplateFormatVersion: '2010-09-09'
|
|
132
|
-
Parameters:
|
|
133
|
-
ExistingSecurityGroups:
|
|
134
|
-
Type: List<AWS::EC2::SecurityGroup::Id>
|
|
135
|
-
ExistingVPC:
|
|
136
|
-
Type: AWS::EC2::VPC::Id
|
|
137
|
-
Description: The VPC ID that includes the security groups in the ExistingSecurityGroups parameter.
|
|
138
|
-
InstanceType:
|
|
139
|
-
Type: String
|
|
140
|
-
Default: t2.micro
|
|
141
|
-
AllowedValues:
|
|
142
|
-
- t2.micro
|
|
143
|
-
- m1.small
|
|
144
|
-
Mappings:
|
|
145
|
-
AWSInstanceType2Arch:
|
|
146
|
-
t2.micro:
|
|
147
|
-
Arch: HVM64
|
|
148
|
-
m1.small:
|
|
149
|
-
Arch: HVM64
|
|
150
|
-
AWSRegionArch2AMI:
|
|
151
|
-
us-east-1:
|
|
152
|
-
HVM64: ami-0ff8a91507f77f867
|
|
153
|
-
HVMG2: ami-0a584ac55a7631c0c
|
|
154
|
-
Resources:
|
|
155
|
-
SecurityGroup:
|
|
156
|
-
Type: AWS::EC2::SecurityGroup
|
|
157
|
-
Properties:
|
|
158
|
-
GroupDescription: Allow HTTP traffic to the host
|
|
159
|
-
VpcId: !Ref ExistingVPC
|
|
160
|
-
SecurityGroupIngress:
|
|
161
|
-
- IpProtocol: tcp
|
|
162
|
-
FromPort: 80
|
|
163
|
-
ToPort: 80
|
|
164
|
-
CidrIp: 0.0.0.0/0
|
|
165
|
-
SecurityGroupEgress:
|
|
166
|
-
- IpProtocol: tcp
|
|
167
|
-
FromPort: 80
|
|
168
|
-
ToPort: 80
|
|
169
|
-
CidrIp: 0.0.0.0/0
|
|
170
|
-
AllSecurityGroups:
|
|
171
|
-
Type: Custom::Split
|
|
172
|
-
Properties:
|
|
173
|
-
ServiceToken: !GetAtt AppendItemToListFunction.Arn
|
|
174
|
-
List: !Ref ExistingSecurityGroups
|
|
175
|
-
AppendedItem: !Ref SecurityGroup
|
|
176
|
-
AppendItemToListFunction:
|
|
177
|
-
Type: AWS::Lambda::Function
|
|
178
|
-
Properties:
|
|
179
|
-
Handler: index.handler
|
|
180
|
-
Role: !GetAtt LambdaExecutionRole.Arn
|
|
181
|
-
Code:
|
|
182
|
-
ZipFile: !Join
|
|
183
|
-
- ''
|
|
184
|
-
- - var response = require('cfn-response');
|
|
185
|
-
- exports.handler = function(event, context) {
|
|
186
|
-
- ' var responseData = {Value: event.ResourceProperties.List};'
|
|
187
|
-
- ' responseData.Value.push(event.ResourceProperties.AppendedItem);'
|
|
188
|
-
- ' response.send(event, context, response.SUCCESS, responseData);'
|
|
189
|
-
- '};'
|
|
190
|
-
Runtime: nodejs20.x
|
|
191
|
-
MyEC2Instance:
|
|
192
|
-
Type: AWS::EC2::Instance
|
|
193
|
-
Properties:
|
|
194
|
-
ImageId: !FindInMap
|
|
195
|
-
- AWSRegionArch2AMI
|
|
196
|
-
- !Ref AWS::Region
|
|
197
|
-
- !FindInMap
|
|
198
|
-
- AWSInstanceType2Arch
|
|
199
|
-
- !Ref InstanceType
|
|
200
|
-
- Arch
|
|
201
|
-
SecurityGroupIds: !GetAtt AllSecurityGroups.Value
|
|
202
|
-
InstanceType: !Ref InstanceType
|
|
203
|
-
LambdaExecutionRole:
|
|
204
|
-
Type: AWS::IAM::Role
|
|
205
|
-
Properties:
|
|
206
|
-
AssumeRolePolicyDocument:
|
|
207
|
-
Version: '2012-10-17'
|
|
208
|
-
Statement:
|
|
209
|
-
- Effect: Allow
|
|
210
|
-
Principal:
|
|
211
|
-
Service:
|
|
212
|
-
- lambda.amazonaws.com
|
|
213
|
-
Action:
|
|
214
|
-
- sts:AssumeRole
|
|
215
|
-
Path: /
|
|
216
|
-
Policies:
|
|
217
|
-
- PolicyName: root
|
|
218
|
-
PolicyDocument:
|
|
219
|
-
Version: '2012-10-17'
|
|
220
|
-
Statement:
|
|
221
|
-
- Effect: Allow
|
|
222
|
-
Action:
|
|
223
|
-
- logs:*
|
|
224
|
-
Resource: arn:aws:logs:*:*:*
|
|
225
|
-
Outputs:
|
|
226
|
-
AllSecurityGroups:
|
|
227
|
-
Description: Security Groups that are associated with the EC2 instance
|
|
228
|
-
Value: !Join
|
|
229
|
-
- ', '
|
|
230
|
-
- !GetAtt AllSecurityGroups.Value
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
This represents a basic configuration for an AWS Lambda function.
|
|
234
|
-
|
|
235
|
-
Finally, run:
|
|
236
|
-
|
|
237
|
-
```bash
|
|
238
|
-
cfn-lint validate -r rules.py template.yaml
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
which outputs:
|
|
242
|
-
|
|
243
|
-
```
|
|
244
|
-
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
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Congrats! You've just made the cloud a bit better place!
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|