cfn-check 0.2.1__tar.gz → 0.8.1__tar.gz

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.

Files changed (57) hide show
  1. cfn_check-0.8.1/PKG-INFO +642 -0
  2. cfn_check-0.8.1/README.md +600 -0
  3. cfn_check-0.8.1/cfn_check/cli/config.py +10 -0
  4. cfn_check-0.8.1/cfn_check/cli/render.py +142 -0
  5. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/cli/root.py +3 -1
  6. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/cli/utils/attributes.py +1 -1
  7. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/cli/utils/files.py +47 -22
  8. cfn_check-0.8.1/cfn_check/cli/utils/stdout.py +18 -0
  9. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/cli/validate.py +38 -33
  10. cfn_check-0.8.1/cfn_check/collection/collection.py +59 -0
  11. cfn_check-0.8.1/cfn_check/evaluation/evaluator.py +113 -0
  12. cfn_check-0.8.1/cfn_check/evaluation/parsing/__init__.py +1 -0
  13. cfn_check-0.8.1/cfn_check/evaluation/parsing/query_parser.py +145 -0
  14. cfn_check-0.8.1/cfn_check/evaluation/parsing/token.py +287 -0
  15. cfn_check-0.8.1/cfn_check/evaluation/parsing/token_type.py +14 -0
  16. cfn_check-0.8.1/cfn_check/evaluation/validate.py +91 -0
  17. cfn_check-0.8.1/cfn_check/rendering/__init__.py +1 -0
  18. cfn_check-0.8.1/cfn_check/rendering/cidr_solver.py +66 -0
  19. cfn_check-0.8.1/cfn_check/rendering/renderer.py +1316 -0
  20. cfn_check-0.8.1/cfn_check/rendering/utils.py +13 -0
  21. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/rules/rule.py +3 -0
  22. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/validation/validator.py +11 -1
  23. cfn_check-0.8.1/cfn_check.egg-info/PKG-INFO +642 -0
  24. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check.egg-info/SOURCES.txt +15 -4
  25. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check.egg-info/requires.txt +1 -1
  26. cfn_check-0.8.1/example/multitag.py +21 -0
  27. cfn_check-0.8.1/example/pydantic_rules.py +114 -0
  28. cfn_check-0.8.1/example/renderer_test.py +42 -0
  29. cfn_check-0.8.1/example/rules.py +80 -0
  30. {cfn_check-0.2.1 → cfn_check-0.8.1}/pyproject.toml +4 -3
  31. cfn_check-0.2.1/PKG-INFO +0 -247
  32. cfn_check-0.2.1/README.md +0 -206
  33. cfn_check-0.2.1/cfn_check/collection/collection.py +0 -2
  34. cfn_check-0.2.1/cfn_check/evaluation/check.py +0 -20
  35. cfn_check-0.2.1/cfn_check/evaluation/search.py +0 -137
  36. cfn_check-0.2.1/cfn_check/evaluation/validate.py +0 -51
  37. cfn_check-0.2.1/cfn_check/loader/loader.py +0 -21
  38. cfn_check-0.2.1/cfn_check/validation/__init__.py +0 -0
  39. cfn_check-0.2.1/cfn_check.egg-info/PKG-INFO +0 -247
  40. cfn_check-0.2.1/example/rules.py +0 -12
  41. {cfn_check-0.2.1 → cfn_check-0.8.1}/LICENSE +0 -0
  42. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/__init__.py +0 -0
  43. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/cli/__init__.py +0 -0
  44. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/cli/utils/__init__.py +0 -0
  45. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/collection/__init__.py +0 -0
  46. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/evaluation/__init__.py +0 -0
  47. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/evaluation/errors.py +0 -0
  48. {cfn_check-0.2.1/cfn_check/loader → cfn_check-0.8.1/cfn_check/logging}/__init__.py +0 -0
  49. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/logging/models.py +0 -0
  50. {cfn_check-0.2.1/cfn_check/logging → cfn_check-0.8.1/cfn_check/rules}/__init__.py +0 -0
  51. {cfn_check-0.2.1/cfn_check/rules → cfn_check-0.8.1/cfn_check/shared}/__init__.py +0 -0
  52. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check/shared/types.py +0 -0
  53. {cfn_check-0.2.1/cfn_check/shared → cfn_check-0.8.1/cfn_check/validation}/__init__.py +0 -0
  54. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check.egg-info/dependency_links.txt +0 -0
  55. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check.egg-info/entry_points.txt +0 -0
  56. {cfn_check-0.2.1 → cfn_check-0.8.1}/cfn_check.egg-info/top_level.txt +0 -0
  57. {cfn_check-0.2.1 → cfn_check-0.8.1}/setup.cfg +0 -0
@@ -0,0 +1,642 @@
1
+ Metadata-Version: 2.4
2
+ Name: cfn-check
3
+ Version: 0.8.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.11
31
+ Classifier: Programming Language :: Python :: 3.12
32
+ Classifier: Programming Language :: Python :: 3.13
33
+ Classifier: Operating System :: OS Independent
34
+ Requires-Python: >=3.11
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: pydantic
38
+ Requires-Dist: ruamel.yaml
39
+ Requires-Dist: hyperlight-cocoa
40
+ Requires-Dist: async-logging
41
+ Dynamic: license-file
42
+
43
+ # <b>CFN-Check</b>
44
+ <b>A tool for checking CloudFormation</b>
45
+
46
+ [![PyPI version](https://img.shields.io/pypi/v/cfn-check?color=blue)](https://pypi.org/project/cfn-check/)
47
+ [![License](https://img.shields.io/github/license/adalundhe/cfn-check)](https://github.com/adalundhe/cfn-check/blob/main/LICENSE)
48
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/adalundhe/cfn-check/blob/main/CODE_OF_CONDUCT.md)
49
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cfn-check?color=red)](https://pypi.org/project/cfn-check/)
50
+
51
+
52
+ | Package | cfn-check |
53
+ | ----------- | ----------- |
54
+ | Version | 0.8.0 |
55
+ | Download | https://pypi.org/project/cfn-check/ |
56
+ | Source | https://github.com/adalundhe/cfn-check |
57
+ | Keywords | cloud-formation, testing, aws, cli |
58
+
59
+
60
+ CFN-Check is a small, fast, friendly tool for validating AWS CloudFormation YAML templates. It is code-driven, with
61
+ rules written as simple, `Rule` decorator wrapped python class methods for `Collection`-inheriting classes.
62
+
63
+ <br/>
64
+
65
+ # Why CFN-Check?
66
+
67
+ AWS has its own tools for validating Cloud Formation - `cfn-lint` and `cfn-guard`. `cfn-check` aims to solve
68
+ problems inherint to `cfn-lint` more than `cfn-guard`, primarily:
69
+
70
+ - Confusing, unclear syntax around rules configuration
71
+ - Inability to parse non-resource wildcards
72
+ - Inability to validate non-resource template data
73
+ - Inabillity to use structured models to validate input
74
+ - Poor ability to parse and render CloudFormation Refs/Functions
75
+
76
+ In comparison to `cfn-guard`, `cfn-check` is pure Python, thus
77
+ avoiding YADSL (Yet Another DSL) headaches. It also proves
78
+ significantly more configurable/modular/hackable as a result.
79
+ `cfn-check` can resolve _some_ (not all) CloudFormation Intrinsic
80
+ Functions and Refs.
81
+
82
+ CFN-Check uses a combination of simple depth-first-search tree
83
+ parsing, friendly `cfn-lint` like query syntax, `Pydantic` models,
84
+ and `pytest`-like assert-driven checks to make validating your
85
+ Cloud Formation easy while offering both CLI and Python API interfaces.
86
+ CFN-Check also uses a lightning-fast AST-parser to render your templates,
87
+ allowing you to validate policy, not just a YAML document.
88
+
89
+ <br/>
90
+
91
+ # Getting Started
92
+
93
+ `cfn-check` requires:
94
+
95
+ - `Python 3.12`
96
+ - Any number of valid CloudFormation templates or a path to said templates.
97
+ - A `.py` file containing at least one `Collection` class with at least one valid `@Rule()` decorated method
98
+
99
+ To get started (we recommend using `uv`), run:
100
+
101
+ ```bash
102
+ uv venv
103
+ source .venv/bin/activate
104
+
105
+ uv pip install cfn-check
106
+
107
+ touch rules.py
108
+ touch template.yaml
109
+ ```
110
+
111
+ Next open the `rules.py` file and create a basic Python class
112
+ as below.
113
+
114
+ ```python
115
+ from cfn_check import Collection, Rule
116
+
117
+
118
+ class ValidateResourceType(Collection):
119
+
120
+ @Rule(
121
+ "Resources::*::Type",
122
+ "It checks Resource::Type is correctly definined",
123
+ )
124
+ def validate_test(self, value: str):
125
+ assert value is not None, '❌ Resource Type not defined'
126
+ assert isinstance(value, str), '❌ Resource Type not a string'
127
+ ```
128
+
129
+ 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.
130
+
131
+ > [!NOTE]
132
+ > Don't worry about adding an `__init__()` method to this class!
133
+
134
+ Next open the `template.yaml` file and paste the following CloudFormation:
135
+
136
+ ```yaml
137
+ AWSTemplateFormatVersion: '2010-09-09'
138
+ Parameters:
139
+ ExistingSecurityGroups:
140
+ Type: List<AWS::EC2::SecurityGroup::Id>
141
+ ExistingVPC:
142
+ Type: AWS::EC2::VPC::Id
143
+ Description: The VPC ID that includes the security groups in the ExistingSecurityGroups parameter.
144
+ InstanceType:
145
+ Type: String
146
+ Default: t2.micro
147
+ AllowedValues:
148
+ - t2.micro
149
+ - m1.small
150
+ Mappings:
151
+ AWSInstanceType2Arch:
152
+ t2.micro:
153
+ Arch: HVM64
154
+ m1.small:
155
+ Arch: HVM64
156
+ AWSRegionArch2AMI:
157
+ us-east-1:
158
+ HVM64: ami-0ff8a91507f77f867
159
+ HVMG2: ami-0a584ac55a7631c0c
160
+ Resources:
161
+ SecurityGroup:
162
+ Type: AWS::EC2::SecurityGroup
163
+ Properties:
164
+ GroupDescription: Allow HTTP traffic to the host
165
+ VpcId: !Ref ExistingVPC
166
+ SecurityGroupIngress:
167
+ - IpProtocol: tcp
168
+ FromPort: 80
169
+ ToPort: 80
170
+ CidrIp: 0.0.0.0/0
171
+ SecurityGroupEgress:
172
+ - IpProtocol: tcp
173
+ FromPort: 80
174
+ ToPort: 80
175
+ CidrIp: 0.0.0.0/0
176
+ AllSecurityGroups:
177
+ Type: Custom::Split
178
+ Properties:
179
+ ServiceToken: !GetAtt AppendItemToListFunction.Arn
180
+ List: !Ref ExistingSecurityGroups
181
+ AppendedItem: !Ref SecurityGroup
182
+ AppendItemToListFunction:
183
+ Type: AWS::Lambda::Function
184
+ Properties:
185
+ Handler: index.handler
186
+ Role: !GetAtt LambdaExecutionRole.Arn
187
+ Code:
188
+ ZipFile: !Join
189
+ - ''
190
+ - - var response = require('cfn-response');
191
+ - exports.handler = function(event, context) {
192
+ - ' var responseData = {Value: event.ResourceProperties.List};'
193
+ - ' responseData.Value.push(event.ResourceProperties.AppendedItem);'
194
+ - ' response.send(event, context, response.SUCCESS, responseData);'
195
+ - '};'
196
+ Runtime: nodejs20.x
197
+ MyEC2Instance:
198
+ Type: AWS::EC2::Instance
199
+ Properties:
200
+ ImageId: !FindInMap
201
+ - AWSRegionArch2AMI
202
+ - !Ref AWS::Region
203
+ - !FindInMap
204
+ - AWSInstanceType2Arch
205
+ - !Ref InstanceType
206
+ - Arch
207
+ SecurityGroupIds: !GetAtt AllSecurityGroups.Value
208
+ InstanceType: !Ref InstanceType
209
+ LambdaExecutionRole:
210
+ Type: AWS::IAM::Role
211
+ Properties:
212
+ AssumeRolePolicyDocument:
213
+ Version: '2012-10-17'
214
+ Statement:
215
+ - Effect: Allow
216
+ Principal:
217
+ Service:
218
+ - lambda.amazonaws.com
219
+ Action:
220
+ - sts:AssumeRole
221
+ Path: /
222
+ Policies:
223
+ - PolicyName: root
224
+ PolicyDocument:
225
+ Version: '2012-10-17'
226
+ Statement:
227
+ - Effect: Allow
228
+ Action:
229
+ - logs:*
230
+ Resource: arn:aws:logs:*:*:*
231
+ Outputs:
232
+ AllSecurityGroups:
233
+ Description: Security Groups that are associated with the EC2 instance
234
+ Value: !Join
235
+ - ', '
236
+ - !GetAtt AllSecurityGroups.Value
237
+ ```
238
+
239
+ This represents a basic configuration for an AWS Lambda function.
240
+
241
+ Finally, run:
242
+
243
+ ```bash
244
+ cfn-check validate -r rules.py template.yaml
245
+ ```
246
+
247
+ which outputs:
248
+
249
+ ```
250
+ 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
251
+ ```
252
+
253
+ Congrats! You've just made the cloud a bit better place!
254
+
255
+ <br/>
256
+
257
+ # Queries, Tokens, and Syntax
258
+
259
+ A `cfn-check` Query is a string made up of double-colon (`::`) delimited "Tokens" centered around three primary types:
260
+
261
+ - <b>`Keys`</b> - `<KEY>`: String name key Tokens that perform exact matching on keys of key/value pairs in a CloudFormation document.
262
+ - <b>`Patterns`</b> - `(\d+)`: Paren-enclosed regex pattern Tokens that perform pattern-based matching on keys of key/value pairs in a CloudFormation document.
263
+ - <b>`Ranges`</b> - `[]`: Brackets enclosed Tokens that perform array selection and filtering in a CloudFormation document.
264
+
265
+
266
+ In addition to `Key`, `Pattern`, and `Range` selection, you can also incorporate:
267
+
268
+ - <b>`Bounded Ranges`</b> - `[<A>-<B>]`: Exact matches from the starting position (if specified) to the end position (if specified) of an array
269
+ - <b>`Indicies`</b> - `[<A>]`: Exact matches the specified indicies of an array
270
+ - <b>`Key Ranges`</b> - `[<KEY>]`: Exact matches keys of objects within an array
271
+ - <b>`Pattern Ranges`</b> (`[(\d+)]`): Matches they keys of objects within an array based on the specified pattern
272
+ - <b>`Wildcards`</b> (`*`): Selects all values for a given object or array or returns the non-object/array value at the specified path
273
+ - <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).
274
+
275
+ ### Working with Keys
276
+
277
+ 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:
278
+
279
+ ```
280
+ Resources
281
+ ```
282
+
283
+ as your query, you'll select all items within the CloudFormation document under the `Resources` key.
284
+
285
+ ### Working with Patterns
286
+
287
+ 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:
288
+
289
+ ```yaml
290
+ Resources:
291
+ SecurityGroup:
292
+ Type: AWS::EC2::SecurityGroup
293
+ Properties:
294
+ GroupDescription: Allow HTTP traffic to the host
295
+ VpcId: !Ref ExistingVPC
296
+ SecurityGroupIngress:
297
+ - IpProtocol: tcp
298
+ FromPort: 80
299
+ ToPort: 80
300
+ CidrIp: 0.0.0.0/0
301
+ SecurityGroupEgress:
302
+ - IpProtocol: tcp
303
+ FromPort: 80
304
+ ToPort: 80
305
+ CidrIp: 0.0.0.0/0
306
+ ```
307
+
308
+ 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:
309
+
310
+ ```
311
+ Resources::SecurityGroup::Properties::(SecurityGroup)
312
+ ```
313
+
314
+ which would allow us to use a single rule to evaluate both:
315
+
316
+ ```python
317
+ class ValidateSecurityGroups(Collection):
318
+
319
+ @Rule(
320
+ "Resources::SecurityGroup::Properties::(SecurityGroup)",
321
+ "It checks Security Groups are correctly definined",
322
+ )
323
+ def validate_security_groups(self, value: list[dict]):
324
+ assert len(value) > 0
325
+
326
+ for item in value:
327
+ protocol = item.get("IpProtocol")
328
+ assert isinstance(protocol, str)
329
+ assert protocol == "tcp"
330
+
331
+ from_port = item.get("FromPort")
332
+ assert isinstance(from_port, int)
333
+ assert from_port == 80
334
+
335
+ to_port = item.get('ToPort')
336
+ assert isinstance(to_port, int)
337
+ assert to_port == 80
338
+
339
+ cidr_ip = item.get('CidrIp')
340
+ assert isinstance(cidr_ip, str)
341
+ assert cidr_ip == '0.0.0.0/0'
342
+ ```
343
+
344
+ ### Working with Wildcards
345
+
346
+ 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.
347
+
348
+ In fact, you've already used one! In the first example, we use a Wildcard Token in the below query:
349
+
350
+ ```
351
+ Resources::*::Type
352
+ ```
353
+
354
+ 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!
355
+
356
+ ### Working with Ranges
357
+
358
+ Ranges allow you to perform sophisticated selection of objects or data within a CloudFormation document.
359
+
360
+ > [!IMPORTANT]
361
+ > Range Tokens *only* work on arrays. This means that any
362
+ > values or other objects/data in the selected section of the
363
+ > CloudFormation document will be *ignored* and filtered out.
364
+
365
+ #### Unbounded Ranges
366
+
367
+ Unbounded ranges allow you to select and return an array in its entirety. For example:
368
+
369
+ ```
370
+ Resources::SecurityGroup::Properties::SecurityGroupIngress::[]
371
+ ```
372
+
373
+ 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.
374
+
375
+
376
+ #### Indexes
377
+
378
+ Indexes allow you to select specific positions within an array. For example:
379
+
380
+ ```
381
+ Resources::SecurityGroup::Properties::SecurityGroupIngress::[0]
382
+ ```
383
+
384
+ Would return the first SecurityGroupIngress objects in the document.
385
+
386
+
387
+ #### Bounded Ranges
388
+
389
+ 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`)
390
+
391
+
392
+ As an example:
393
+
394
+ ```
395
+ Resources::SecurityGroup::Properties::SecurityGroupIngress::[1-3]
396
+ ```
397
+
398
+ Would select the second and third SecurityGroupIngress objects in the document.
399
+
400
+ 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:
401
+
402
+ ```
403
+ Resources::SecurityGroup::Properties::SecurityGroupIngress::[-3]
404
+ ```
405
+
406
+ selects the first through third SecurityGroupIngress objects in the document while:
407
+
408
+ ```
409
+ Resources::SecurityGroup::Properties::SecurityGroupIngress::[3-]
410
+ ```
411
+
412
+ selects the remaining SecurityGroupIngress objects starting from the third.
413
+
414
+
415
+ #### Key Ranges
416
+
417
+ 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:
418
+
419
+ - Exact match value comparison if the array value is not an object or array
420
+ - 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)
421
+ - Single exact match key comparison if the array value is an object
422
+
423
+ For example:
424
+
425
+ ```
426
+ Resources::MyEC2Instance::Properties::ImageId::[AWSRegionArch2AMI]
427
+ ```
428
+
429
+ returns only the EC2 ImageIds where the ImageId exactly matches `AWSRegionArch2AMI`.
430
+
431
+
432
+ #### Pattern Ranges
433
+
434
+ Pattern Ranges function much like Key Ranges, but utilize regex-based pattern matching for comparison. Adapting the above example:
435
+
436
+ ```
437
+ Resources::MyEC2Instance::Properties::ImageId::[(^AWSRegion)]
438
+ ```
439
+
440
+ returns only the EC2 ImageIds where the ImageId begins with `AWSRegion`. This can be helpful in checking for and enforcing naming standards, etc.
441
+
442
+
443
+ #### Wildcard Ranges
444
+
445
+ Wildcard Ranges extend the powerful functionality of Wildcard Tokens with the added safety of ensuring *only* arrays selected for further filtering or checks.
446
+
447
+ For example we know:
448
+
449
+ ```
450
+ Resources::*::Type
451
+ ```
452
+
453
+ Selects all `Resource` objects. If we convert the Wildcard Token in the query to a Wildcard Range Token:
454
+
455
+ ```
456
+ Resources::[*]::Type
457
+ ```
458
+
459
+ The Rule will fail as below:
460
+
461
+ ```
462
+ error: ❌ No results matching results for query Resources::[*]::Type
463
+ ```
464
+
465
+ 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`:
466
+
467
+ ```yaml
468
+ AppendItemToListFunction:
469
+ Type: AWS::Lambda::Function
470
+ Properties:
471
+ Handler: index.handler
472
+ Role: !GetAtt LambdaExecutionRole.Arn
473
+ Code:
474
+ ZipFile: !Join
475
+ - ''
476
+ - - var response = require('cfn-response');
477
+ - exports.handler = function(event, context) {
478
+ - ' var responseData = {Value: event.ResourceProperties.List};'
479
+ - ' responseData.Value.push(event.ResourceProperties.AppendedItem);'
480
+ - ' response.send(event, context, response.SUCCESS, responseData);'
481
+ - '};'
482
+ Runtime: nodejs20.x
483
+ ```
484
+
485
+ 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!
486
+
487
+ We can accomplish this by using a Wildcard Range Token in our Query as below:
488
+
489
+ ```
490
+ Resources::AppendItemToListFunction::Properties::Code::ZipFile::[*]::[]
491
+ ```
492
+
493
+ Which allows us to then evaluate the Unbounded Range token against each array item, returning only the array we want.
494
+
495
+ ### Using Multiple Tokens in Ranges
496
+
497
+ You can use multiple Tokens within a Range Token by seperating each token with a comma.
498
+
499
+ > [!NOTE]
500
+ > While YAML does allow commas in keys, CloudFormation does not.
501
+ > As such, the case where a Pattern or Pattern Range might
502
+ > contain a comma is non-existent.
503
+
504
+ For example:
505
+
506
+ ```
507
+ Resources::SecurityGroup::Properties::SecurityGroupIngress::[0, -2]
508
+ ```
509
+
510
+ Would select all except the last element of an array.
511
+
512
+ This also applies to Bounded Ranges, Key Ranges, Pattern Ranges, and Wildcard Ranges! For example:
513
+
514
+ ```
515
+ Resources::MyEC2Instance::Properties::ImageId::[(^AWSRegion),(^),(^Custom)]
516
+ ```
517
+
518
+ will select any EC2 ImageIds that start with either `AWSRegion` or `Custom`.
519
+
520
+
521
+ ### Nested Ranges
522
+
523
+ 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:
524
+
525
+ ```yaml
526
+ ZipFile: !Join
527
+ - ''
528
+ - - var response = require('cfn-response');
529
+ - exports.handler = function(event, context) {
530
+ - ' var responseData = {Value: event.ResourceProperties.List};'
531
+ - ' responseData.Value.push(event.ResourceProperties.AppendedItem);'
532
+ - ' response.send(event, context, response.SUCCESS, responseData);'
533
+ - '};'
534
+ ```
535
+
536
+ from our previous examples, we used the below query to select the nested array:
537
+
538
+ ```
539
+ Resources::AppendItemToListFunction::Properties::Code::ZipFile::[*]::[]
540
+ ```
541
+
542
+ With Nested Ranges, this can be shortened to:
543
+
544
+ ```
545
+ Resources::AppendItemToListFunction::Properties::Code::ZipFile::[[]]
546
+ ```
547
+
548
+ Which is both more concise *and* more representitave of our intention to select only the array.
549
+
550
+ <br/>
551
+
552
+ # Using Pydantic Models
553
+
554
+ In addition to traditional `pytest`-like assert statements, `cfn-lint` can validate results returned by queries via `Pydantic` models.
555
+
556
+ For example, consider again the initial example where we validate the `Type` field of `Resource` objects.
557
+
558
+ ```python
559
+ from cfn_check import Collection, Rule
560
+
561
+
562
+ class ValidateResourceType(Collection):
563
+
564
+ @Rule(
565
+ "Resources::*::Type",
566
+ "It checks Resource::Type is correctly definined",
567
+ )
568
+ def validate_test(self, value: str):
569
+ assert value is not None, '❌ Resource Type not defined'
570
+ assert isinstance(value, str), '❌ Resource Type not a string'
571
+ ```
572
+
573
+ 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.
574
+
575
+ ```python
576
+ from cfn_check import Collection, Rule
577
+ from pydantic import BaseModel, StrictStr
578
+
579
+ class Resource(BaseModel):
580
+ Type: StrictStr
581
+
582
+
583
+ class ValidateResourceType(Collection):
584
+
585
+ @Rule(
586
+ "Resources::*",
587
+ "It checks Resource::Type is correctly definined",
588
+ )
589
+ def validate_test(self, value: Resource):
590
+ assert value is not None
591
+ ```
592
+
593
+ By deferring type and existence assertions to `Pydantic` models, you can focus your actual assertion logic on business/security policy checks.
594
+
595
+ <br/>
596
+
597
+ # The Rendering Engine
598
+
599
+ ### Overview
600
+
601
+ In Version 0.6.X, CFN-Check introduced a rendering engine, which allows it
602
+ to parse and execute Refs and all CloudFormation intrinsic functions via
603
+ either the CloudFormation document or user-supplied values. This additional
604
+ also resulted in the:
605
+
606
+ ```bash
607
+ cfn-check render <TEMPLATE_PATH >
608
+ ```
609
+
610
+ command being added, allowing you to effectively "dry run" render your
611
+ CloudFormation templates akin to the `helm template` command for Helm.
612
+
613
+ By default, `cfn-check render` outputs to stdout, however you can easily
614
+ save rendered output to a file via the `-o/--output-file` flag. For example:
615
+
616
+ ```bash
617
+ cfn-check render template.yml -o rendered.yml
618
+ ```
619
+
620
+ The `cfn-check render` command also offers the following options:
621
+
622
+ - `-a/--attributes`: A list of <key>=<value> input `!GetAtt` attributes to use
623
+ - `-m/--mappings`: A list of <key>=<value> input `Mappings` to use
624
+ - `-p/--parameters`: A list of <key>=<value> input `Parameters` to use
625
+ - `-l/--log-level`: The log level to use
626
+
627
+ ### The Rendering Engine during Checks
628
+
629
+ By default rendering is enabled when running `cfn-check` validation. You can
630
+ disable it by supplying `no-render` to the `-F/--flags` option as below:
631
+
632
+ ```bash
633
+ cfn-check validate -F no-render -r rules.py template.yaml
634
+ ```
635
+
636
+ Disabling rendering means CFN-Check will validate your template as-is, with
637
+ no additional pre-processing and no application of user input values.
638
+
639
+ > [!WARNING]
640
+ > CloudFormation documents are <b>not</b> "plain yaml" and disabling
641
+ > rendering means any dynamically determined values will likely fail
642
+ > to pass validation, resulting in false positives for failures!