cfn-check 0.7.1__tar.gz → 0.9.0__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.
Files changed (86) hide show
  1. {cfn_check-0.7.1 → cfn_check-0.9.0}/PKG-INFO +104 -28
  2. {cfn_check-0.7.1 → cfn_check-0.9.0}/README.md +101 -26
  3. cfn_check-0.9.0/cfn_check/cli/config.py +10 -0
  4. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/cli/render.py +65 -8
  5. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/cli/utils/files.py +1 -1
  6. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/cli/utils/stdout.py +2 -2
  7. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/cli/validate.py +17 -4
  8. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/collection/collection.py +7 -7
  9. cfn_check-0.9.0/cfn_check/evaluation/evaluator.py +128 -0
  10. cfn_check-0.9.0/cfn_check/evaluation/parsing/operators/__init__.py +1 -0
  11. cfn_check-0.9.0/cfn_check/evaluation/parsing/operators/value_operator.py +118 -0
  12. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/evaluation/parsing/query_parser.py +26 -2
  13. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/evaluation/parsing/token.py +124 -70
  14. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/evaluation/parsing/token_type.py +2 -0
  15. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/evaluation/validate.py +29 -3
  16. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/rendering/renderer.py +62 -11
  17. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/rendering/utils.py +1 -1
  18. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/rules/rule.py +3 -3
  19. cfn_check-0.9.0/cfn_check/shared/__init__.py +0 -0
  20. cfn_check-0.9.0/cfn_check/validation/__init__.py +0 -0
  21. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/validation/validator.py +5 -5
  22. cfn_check-0.9.0/cfn_check/yaml/__init__.py +5 -0
  23. cfn_check-0.9.0/cfn_check/yaml/anchor.py +18 -0
  24. cfn_check-0.9.0/cfn_check/yaml/comments.py +1201 -0
  25. cfn_check-0.9.0/cfn_check/yaml/compat.py +235 -0
  26. cfn_check-0.9.0/cfn_check/yaml/composer.py +229 -0
  27. cfn_check-0.9.0/cfn_check/yaml/configobjwalker.py +15 -0
  28. cfn_check-0.9.0/cfn_check/yaml/constructor.py +1722 -0
  29. cfn_check-0.9.0/cfn_check/yaml/cyaml.py +196 -0
  30. cfn_check-0.9.0/cfn_check/yaml/docinfo.py +115 -0
  31. cfn_check-0.9.0/cfn_check/yaml/dumper.py +216 -0
  32. cfn_check-0.9.0/cfn_check/yaml/emitter.py +1813 -0
  33. cfn_check-0.9.0/cfn_check/yaml/error.py +314 -0
  34. cfn_check-0.9.0/cfn_check/yaml/events.py +265 -0
  35. cfn_check-0.9.0/cfn_check/yaml/loader.py +91 -0
  36. cfn_check-0.9.0/cfn_check/yaml/main.py +1521 -0
  37. cfn_check-0.9.0/cfn_check/yaml/mergevalue.py +37 -0
  38. cfn_check-0.9.0/cfn_check/yaml/nodes.py +148 -0
  39. cfn_check-0.9.0/cfn_check/yaml/parser.py +909 -0
  40. cfn_check-0.9.0/cfn_check/yaml/reader.py +274 -0
  41. cfn_check-0.9.0/cfn_check/yaml/representer.py +1146 -0
  42. cfn_check-0.9.0/cfn_check/yaml/resolver.py +390 -0
  43. cfn_check-0.9.0/cfn_check/yaml/scalarbool.py +41 -0
  44. cfn_check-0.9.0/cfn_check/yaml/scalarfloat.py +103 -0
  45. cfn_check-0.9.0/cfn_check/yaml/scalarint.py +122 -0
  46. cfn_check-0.9.0/cfn_check/yaml/scalarstring.py +140 -0
  47. cfn_check-0.9.0/cfn_check/yaml/scanner.py +2390 -0
  48. cfn_check-0.9.0/cfn_check/yaml/serializer.py +231 -0
  49. cfn_check-0.9.0/cfn_check/yaml/tag.py +124 -0
  50. cfn_check-0.9.0/cfn_check/yaml/timestamp.py +61 -0
  51. cfn_check-0.9.0/cfn_check/yaml/tokens.py +382 -0
  52. cfn_check-0.9.0/cfn_check/yaml/util.py +262 -0
  53. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check.egg-info/PKG-INFO +104 -28
  54. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check.egg-info/SOURCES.txt +39 -2
  55. cfn_check-0.9.0/example/pydantic_rules.py +35 -0
  56. {cfn_check-0.7.1 → cfn_check-0.9.0}/example/rules.py +3 -3
  57. cfn_check-0.9.0/example/test_models/__init__.py +0 -0
  58. cfn_check-0.9.0/example/test_models/models.py +54 -0
  59. {cfn_check-0.7.1 → cfn_check-0.9.0}/pyproject.toml +4 -3
  60. cfn_check-0.7.1/cfn_check/evaluation/evaluator.py +0 -98
  61. cfn_check-0.7.1/example/multitag.py +0 -21
  62. cfn_check-0.7.1/example/pydantic_rules.py +0 -114
  63. {cfn_check-0.7.1 → cfn_check-0.9.0}/LICENSE +0 -0
  64. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/__init__.py +0 -0
  65. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/cli/__init__.py +0 -0
  66. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/cli/root.py +0 -0
  67. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/cli/utils/__init__.py +0 -0
  68. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/cli/utils/attributes.py +0 -0
  69. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/collection/__init__.py +0 -0
  70. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/evaluation/__init__.py +0 -0
  71. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/evaluation/errors.py +0 -0
  72. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/evaluation/parsing/__init__.py +0 -0
  73. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/logging/__init__.py +0 -0
  74. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/logging/models.py +0 -0
  75. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/rendering/__init__.py +0 -0
  76. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/rendering/cidr_solver.py +0 -0
  77. {cfn_check-0.7.1/cfn_check/rules → cfn_check-0.9.0/cfn_check/rendering/parsing}/__init__.py +0 -0
  78. /cfn_check-0.7.1/cfn_check/shared/__init__.py → /cfn_check-0.9.0/cfn_check/rendering/parsing/cloudformation_loader.py +0 -0
  79. {cfn_check-0.7.1/cfn_check/validation → cfn_check-0.9.0/cfn_check/rules}/__init__.py +0 -0
  80. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check/shared/types.py +0 -0
  81. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check.egg-info/dependency_links.txt +0 -0
  82. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check.egg-info/entry_points.txt +0 -0
  83. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check.egg-info/requires.txt +0 -0
  84. {cfn_check-0.7.1 → cfn_check-0.9.0}/cfn_check.egg-info/top_level.txt +0 -0
  85. {cfn_check-0.7.1 → cfn_check-0.9.0}/example/renderer_test.py +0 -0
  86. {cfn_check-0.7.1 → cfn_check-0.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfn-check
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Validate Cloud Formation
5
5
  Author-email: Ada Lundhe <adalundhe@lundhe.audio>
6
6
  License: MIT License
@@ -27,10 +27,11 @@ License: MIT License
27
27
 
28
28
  Project-URL: Homepage, https://github.com/adalundhe/cfn-check
29
29
  Keywords: cloud-formation,testing,aws,cli
30
+ Classifier: Programming Language :: Python :: 3.11
30
31
  Classifier: Programming Language :: Python :: 3.12
31
32
  Classifier: Programming Language :: Python :: 3.13
32
33
  Classifier: Operating System :: OS Independent
33
- Requires-Python: >=3.12
34
+ Requires-Python: >=3.11
34
35
  Description-Content-Type: text/markdown
35
36
  License-File: LICENSE
36
37
  Requires-Dist: pydantic
@@ -50,7 +51,7 @@ Dynamic: license-file
50
51
 
51
52
  | Package | cfn-check |
52
53
  | ----------- | ----------- |
53
- | Version | 0.7.1 |
54
+ | Version | 0.8.1 |
54
55
  | Download | https://pypi.org/project/cfn-check/ |
55
56
  | Source | https://github.com/adalundhe/cfn-check |
56
57
  | Keywords | cloud-formation, testing, aws, cli |
@@ -255,19 +256,20 @@ Congrats! You've just made the cloud a bit better place!
255
256
 
256
257
  # Queries, Tokens, and Syntax
257
258
 
258
- A `cfn-check` Query is a string made up of double-colon (`::`) delimited "Tokens" centered around three primary types:
259
+ A `cfn-check` Query is a string made up of period (`.`) delimited "Tokens" centered around four primary types:
259
260
 
260
261
  - <b>`Keys`</b> - `<KEY>`: String name key Tokens that perform exact matching on keys of key/value pairs in a CloudFormation document.
261
- - <b>`Patterns`</b> - `(\d+)`: Paren-enclosed regex pattern Tokens that perform pattern-based matching on keys of key/value pairs in a CloudFormation document.
262
+ - <b>`Values`</b> - `(<KEY> = <VALUE>)`: Parenthesis-enclosed `K==V` pairs that perform matching on values of key/value pairs in a CloudFormation document.
263
+ - <b>`Patterns`</b> - `<\d+>`: Arrow-enclosed regex pattern Tokens that perform pattern-based matching on keys of key/value pairs in a CloudFormation document.
262
264
  - <b>`Ranges`</b> - `[]`: Brackets enclosed Tokens that perform array selection and filtering in a CloudFormation document.
263
265
 
264
266
 
265
- In addition to `Key`, `Pattern`, and `Range` selection, you can also incorporate:
267
+ In addition to `Key`, `Value`, `Pattern`, and `Range` selection, you can also incorporate:
266
268
 
267
269
  - <b>`Bounded Ranges`</b> - `[<A>-<B>]`: Exact matches from the starting position (if specified) to the end position (if specified) of an array
268
270
  - <b>`Indicies`</b> - `[<A>]`: Exact matches the specified indicies of an array
269
271
  - <b>`Key Ranges`</b> - `[<KEY>]`: Exact matches keys of objects within an array
270
- - <b>`Pattern Ranges`</b> (`[(\d+)]`): Matches they keys of objects within an array based on the specified pattern
272
+ - <b>`Pattern Ranges`</b> (`[<\d+>]`): Matches they keys of objects within an array based on the specified pattern
271
273
  - <b>`Wildcards`</b> (`*`): Selects all values for a given object or array or returns the non-object/array value at the specified path
272
274
  - <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).
273
275
 
@@ -281,6 +283,27 @@ Resources
281
283
 
282
284
  as your query, you'll select all items within the CloudFormation document under the `Resources` key.
283
285
 
286
+
287
+ ### Working with Values
288
+
289
+ 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:
290
+
291
+ ```
292
+ Resources.*.(Type == AWS::EC2::Instance)
293
+ ```
294
+
295
+ You can also match multiple values by utilizing the `in`/`IN` operator:
296
+
297
+ ```
298
+ Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)
299
+ ```
300
+
301
+ and providing a comma-delimited list of values.
302
+
303
+ > [!NOTE]
304
+ > Value queries do not support Nested Ranges or nested queries, but *do* support
305
+ > Wildcards and Patterns. See below for more info!
306
+
284
307
  ### Working with Patterns
285
308
 
286
309
  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:
@@ -307,7 +330,7 @@ Resources:
307
330
  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:
308
331
 
309
332
  ```
310
- Resources::SecurityGroup::Properties::(SecurityGroup)
333
+ Resources.SecurityGroup.Properties.<SecurityGroup>
311
334
  ```
312
335
 
313
336
  which would allow us to use a single rule to evaluate both:
@@ -316,7 +339,7 @@ which would allow us to use a single rule to evaluate both:
316
339
  class ValidateSecurityGroups(Collection):
317
340
 
318
341
  @Rule(
319
- "Resources::SecurityGroup::Properties::(SecurityGroup)",
342
+ "Resources.SecurityGroup.Properties.<SecurityGroup>",
320
343
  "It checks Security Groups are correctly definined",
321
344
  )
322
345
  def validate_security_groups(self, value: list[dict]):
@@ -347,7 +370,7 @@ Wildcard Tokens allow you to select all matching objects, array entries, or valu
347
370
  In fact, you've already used one! In the first example, we use a Wildcard Token in the below query:
348
371
 
349
372
  ```
350
- Resources::*::Type
373
+ Resources.*.Type
351
374
  ```
352
375
 
353
376
  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!
@@ -366,7 +389,7 @@ Ranges allow you to perform sophisticated selection of objects or data within a
366
389
  Unbounded ranges allow you to select and return an array in its entirety. For example:
367
390
 
368
391
  ```
369
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[]
392
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[]
370
393
  ```
371
394
 
372
395
  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.
@@ -377,7 +400,7 @@ Would return all SecurityGroupIngress objects in the CloudFormation document as
377
400
  Indexes allow you to select specific positions within an array. For example:
378
401
 
379
402
  ```
380
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[0]
403
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[0]
381
404
  ```
382
405
 
383
406
  Would return the first SecurityGroupIngress objects in the document.
@@ -391,7 +414,7 @@ Bounded Ranges allow you to select subsets of indicies within an array (much lik
391
414
  As an example:
392
415
 
393
416
  ```
394
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[1-3]
417
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[1-3]
395
418
  ```
396
419
 
397
420
  Would select the second and third SecurityGroupIngress objects in the document.
@@ -399,13 +422,13 @@ Would select the second and third SecurityGroupIngress objects in the document.
399
422
  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:
400
423
 
401
424
  ```
402
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[-3]
425
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[-3]
403
426
  ```
404
427
 
405
428
  selects the first through third SecurityGroupIngress objects in the document while:
406
429
 
407
430
  ```
408
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[3-]
431
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[3-]
409
432
  ```
410
433
 
411
434
  selects the remaining SecurityGroupIngress objects starting from the third.
@@ -422,7 +445,7 @@ Often times it's easier to match based upon an array's contents than by exact in
422
445
  For example:
423
446
 
424
447
  ```
425
- Resources::MyEC2Instance::Properties::ImageId::[AWSRegionArch2AMI]
448
+ Resources.MyEC2Instance.Properties.ImageId.[AWSRegionArch2AMI]
426
449
  ```
427
450
 
428
451
  returns only the EC2 ImageIds where the ImageId exactly matches `AWSRegionArch2AMI`.
@@ -433,7 +456,7 @@ returns only the EC2 ImageIds where the ImageId exactly matches `AWSRegionArch2A
433
456
  Pattern Ranges function much like Key Ranges, but utilize regex-based pattern matching for comparison. Adapting the above example:
434
457
 
435
458
  ```
436
- Resources::MyEC2Instance::Properties::ImageId::[(^AWSRegion)]
459
+ Resources.MyEC2Instance.Properties.ImageId.[<^AWSRegion>]
437
460
  ```
438
461
 
439
462
  returns only the EC2 ImageIds where the ImageId begins with `AWSRegion`. This can be helpful in checking for and enforcing naming standards, etc.
@@ -446,13 +469,13 @@ Wildcard Ranges extend the powerful functionality of Wildcard Tokens with the ad
446
469
  For example we know:
447
470
 
448
471
  ```
449
- Resources::*::Type
472
+ Resources.*.Type
450
473
  ```
451
474
 
452
475
  Selects all `Resource` objects. If we convert the Wildcard Token in the query to a Wildcard Range Token:
453
476
 
454
477
  ```
455
- Resources::[*]::Type
478
+ Resources.[*].Type
456
479
  ```
457
480
 
458
481
  The Rule will fail as below:
@@ -486,7 +509,7 @@ Note that the array we want is nested within another array, and we need to make
486
509
  We can accomplish this by using a Wildcard Range Token in our Query as below:
487
510
 
488
511
  ```
489
- Resources::AppendItemToListFunction::Properties::Code::ZipFile::[*]::[]
512
+ Resources.AppendItemToListFunction.Properties.Code.ZipFile.[*].[]
490
513
  ```
491
514
 
492
515
  Which allows us to then evaluate the Unbounded Range token against each array item, returning only the array we want.
@@ -503,7 +526,7 @@ You can use multiple Tokens within a Range Token by seperating each token with a
503
526
  For example:
504
527
 
505
528
  ```
506
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[0, -2]
529
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[0, -2]
507
530
  ```
508
531
 
509
532
  Would select all except the last element of an array.
@@ -511,7 +534,7 @@ Would select all except the last element of an array.
511
534
  This also applies to Bounded Ranges, Key Ranges, Pattern Ranges, and Wildcard Ranges! For example:
512
535
 
513
536
  ```
514
- Resources::MyEC2Instance::Properties::ImageId::[(^AWSRegion),(^),(^Custom)]
537
+ Resources.MyEC2Instance.Properties.ImageId.[(^AWSRegion),(^),(^Custom)]
515
538
  ```
516
539
 
517
540
  will select any EC2 ImageIds that start with either `AWSRegion` or `Custom`.
@@ -535,17 +558,42 @@ ZipFile: !Join
535
558
  from our previous examples, we used the below query to select the nested array:
536
559
 
537
560
  ```
538
- Resources::AppendItemToListFunction::Properties::Code::ZipFile::[*]::[]
561
+ Resources.AppendItemToListFunction.Properties.Code.ZipFile.[*].[]
539
562
  ```
540
563
 
541
564
  With Nested Ranges, this can be shortened to:
542
565
 
543
566
  ```
544
- Resources::AppendItemToListFunction::Properties::Code::ZipFile::[[]]
567
+ Resources.AppendItemToListFunction.Properties.Code.ZipFile.[[]]
545
568
  ```
546
569
 
547
570
  Which is both more concise *and* more representitave of our intention to select only the array.
548
571
 
572
+ # Grouping Queries
573
+
574
+ 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:
575
+
576
+ ```
577
+ Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)
578
+ ```
579
+
580
+ This could be rewritten as:
581
+ ```
582
+ Resources.*(Type == AWS::Lambda::Function | Type == AWS::Serverless::Function)
583
+ ```
584
+
585
+ A more likely scenario might be finding specifically NodeJS Lambda functions. For example:
586
+
587
+ ```
588
+ Resources.*.(Type == AWS::Lambda::Function,AWS::Serverless::Function & Properties.Runtime == <nodejs20>)
589
+ ```
590
+
591
+ Queries are "split" by `|` operator and then "grouped" by `&` operator. That means if we want a query to match one set of criterion <i>or</i> another we could write:
592
+
593
+ ```
594
+ Resources.*.(Type == AWS::Lambda::Function & Properties.Runtime == <nodejs20> | Type == AWS::Serverless::Function & Properties.Runtime == <nodejs20>)
595
+ ```
596
+
549
597
  <br/>
550
598
 
551
599
  # Using Pydantic Models
@@ -561,8 +609,8 @@ from cfn_check import Collection, Rule
561
609
  class ValidateResourceType(Collection):
562
610
 
563
611
  @Rule(
564
- "Resources::*::Type",
565
- "It checks Resource::Type is correctly definined",
612
+ "Resources.*.Type",
613
+ "It checks Resource.Type is correctly definined",
566
614
  )
567
615
  def validate_test(self, value: str):
568
616
  assert value is not None, '❌ Resource Type not defined'
@@ -582,8 +630,8 @@ class Resource(BaseModel):
582
630
  class ValidateResourceType(Collection):
583
631
 
584
632
  @Rule(
585
- "Resources::*",
586
- "It checks Resource::Type is correctly definined",
633
+ "Resources.*",
634
+ "It checks Resource.Type is correctly definined",
587
635
  )
588
636
  def validate_test(self, value: Resource):
589
637
  assert value is not None
@@ -593,6 +641,34 @@ By deferring type and existence assertions to `Pydantic` models, you can focus y
593
641
 
594
642
  <br/>
595
643
 
644
+ # Using .query()
645
+
646
+ 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.
647
+
648
+ 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:
649
+
650
+ ```python
651
+ @Rule("Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)", "It validates a lambda is configured correctly")
652
+ def validate_lambda(self, lambda_resource: Lambda):
653
+ assert isinstance(lambda_resource, Lambda), "Not a valid Lambda"
654
+
655
+ log_groups = self.query(
656
+ f"Resources.{lambda_resource.Properties.LoggingConfig.LogGroup}",
657
+ transforms=[
658
+ lambda data: LoggingGroup(**data)
659
+ ]
660
+ )
661
+
662
+ assert log_groups is not None, f"No resources found for LoggingGroup {lambda_resource.Properties.LoggingConfig.LogGroup}"
663
+ assert len(log_groups) > 0, "No matching logging group found in Resources for Lambda"
664
+ ```
665
+
666
+ The `query()` method accepts the following parameters:
667
+
668
+ - `query - (str, required)`: A string CFN-Check query as used when defining Rules.
669
+ - `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-values` optional CLI arg, or under the `input_values` config key. This will cause the query specified in the call to execute against the template specified.
670
+ - `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 a `LoggingGroup` Pydantic model instance.
671
+
596
672
  # The Rendering Engine
597
673
 
598
674
  ### Overview
@@ -9,7 +9,7 @@
9
9
 
10
10
  | Package | cfn-check |
11
11
  | ----------- | ----------- |
12
- | Version | 0.7.1 |
12
+ | Version | 0.8.1 |
13
13
  | Download | https://pypi.org/project/cfn-check/ |
14
14
  | Source | https://github.com/adalundhe/cfn-check |
15
15
  | Keywords | cloud-formation, testing, aws, cli |
@@ -214,19 +214,20 @@ Congrats! You've just made the cloud a bit better place!
214
214
 
215
215
  # Queries, Tokens, and Syntax
216
216
 
217
- A `cfn-check` Query is a string made up of double-colon (`::`) delimited "Tokens" centered around three primary types:
217
+ A `cfn-check` Query is a string made up of period (`.`) delimited "Tokens" centered around four primary types:
218
218
 
219
219
  - <b>`Keys`</b> - `<KEY>`: String name key Tokens that perform exact matching on keys of key/value pairs in a CloudFormation document.
220
- - <b>`Patterns`</b> - `(\d+)`: Paren-enclosed regex pattern Tokens that perform pattern-based matching on keys of key/value pairs in a CloudFormation document.
220
+ - <b>`Values`</b> - `(<KEY> = <VALUE>)`: Parenthesis-enclosed `K==V` pairs that perform matching on values of key/value pairs in a CloudFormation document.
221
+ - <b>`Patterns`</b> - `<\d+>`: Arrow-enclosed regex pattern Tokens that perform pattern-based matching on keys of key/value pairs in a CloudFormation document.
221
222
  - <b>`Ranges`</b> - `[]`: Brackets enclosed Tokens that perform array selection and filtering in a CloudFormation document.
222
223
 
223
224
 
224
- In addition to `Key`, `Pattern`, and `Range` selection, you can also incorporate:
225
+ In addition to `Key`, `Value`, `Pattern`, and `Range` selection, you can also incorporate:
225
226
 
226
227
  - <b>`Bounded Ranges`</b> - `[<A>-<B>]`: Exact matches from the starting position (if specified) to the end position (if specified) of an array
227
228
  - <b>`Indicies`</b> - `[<A>]`: Exact matches the specified indicies of an array
228
229
  - <b>`Key Ranges`</b> - `[<KEY>]`: Exact matches keys of objects within an array
229
- - <b>`Pattern Ranges`</b> (`[(\d+)]`): Matches they keys of objects within an array based on the specified pattern
230
+ - <b>`Pattern Ranges`</b> (`[<\d+>]`): Matches they keys of objects within an array based on the specified pattern
230
231
  - <b>`Wildcards`</b> (`*`): Selects all values for a given object or array or returns the non-object/array value at the specified path
231
232
  - <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).
232
233
 
@@ -240,6 +241,27 @@ Resources
240
241
 
241
242
  as your query, you'll select all items within the CloudFormation document under the `Resources` key.
242
243
 
244
+
245
+ ### Working with Values
246
+
247
+ 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:
248
+
249
+ ```
250
+ Resources.*.(Type == AWS::EC2::Instance)
251
+ ```
252
+
253
+ You can also match multiple values by utilizing the `in`/`IN` operator:
254
+
255
+ ```
256
+ Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)
257
+ ```
258
+
259
+ and providing a comma-delimited list of values.
260
+
261
+ > [!NOTE]
262
+ > Value queries do not support Nested Ranges or nested queries, but *do* support
263
+ > Wildcards and Patterns. See below for more info!
264
+
243
265
  ### Working with Patterns
244
266
 
245
267
  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:
@@ -266,7 +288,7 @@ Resources:
266
288
  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:
267
289
 
268
290
  ```
269
- Resources::SecurityGroup::Properties::(SecurityGroup)
291
+ Resources.SecurityGroup.Properties.<SecurityGroup>
270
292
  ```
271
293
 
272
294
  which would allow us to use a single rule to evaluate both:
@@ -275,7 +297,7 @@ which would allow us to use a single rule to evaluate both:
275
297
  class ValidateSecurityGroups(Collection):
276
298
 
277
299
  @Rule(
278
- "Resources::SecurityGroup::Properties::(SecurityGroup)",
300
+ "Resources.SecurityGroup.Properties.<SecurityGroup>",
279
301
  "It checks Security Groups are correctly definined",
280
302
  )
281
303
  def validate_security_groups(self, value: list[dict]):
@@ -306,7 +328,7 @@ Wildcard Tokens allow you to select all matching objects, array entries, or valu
306
328
  In fact, you've already used one! In the first example, we use a Wildcard Token in the below query:
307
329
 
308
330
  ```
309
- Resources::*::Type
331
+ Resources.*.Type
310
332
  ```
311
333
 
312
334
  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!
@@ -325,7 +347,7 @@ Ranges allow you to perform sophisticated selection of objects or data within a
325
347
  Unbounded ranges allow you to select and return an array in its entirety. For example:
326
348
 
327
349
  ```
328
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[]
350
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[]
329
351
  ```
330
352
 
331
353
  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.
@@ -336,7 +358,7 @@ Would return all SecurityGroupIngress objects in the CloudFormation document as
336
358
  Indexes allow you to select specific positions within an array. For example:
337
359
 
338
360
  ```
339
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[0]
361
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[0]
340
362
  ```
341
363
 
342
364
  Would return the first SecurityGroupIngress objects in the document.
@@ -350,7 +372,7 @@ Bounded Ranges allow you to select subsets of indicies within an array (much lik
350
372
  As an example:
351
373
 
352
374
  ```
353
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[1-3]
375
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[1-3]
354
376
  ```
355
377
 
356
378
  Would select the second and third SecurityGroupIngress objects in the document.
@@ -358,13 +380,13 @@ Would select the second and third SecurityGroupIngress objects in the document.
358
380
  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:
359
381
 
360
382
  ```
361
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[-3]
383
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[-3]
362
384
  ```
363
385
 
364
386
  selects the first through third SecurityGroupIngress objects in the document while:
365
387
 
366
388
  ```
367
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[3-]
389
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[3-]
368
390
  ```
369
391
 
370
392
  selects the remaining SecurityGroupIngress objects starting from the third.
@@ -381,7 +403,7 @@ Often times it's easier to match based upon an array's contents than by exact in
381
403
  For example:
382
404
 
383
405
  ```
384
- Resources::MyEC2Instance::Properties::ImageId::[AWSRegionArch2AMI]
406
+ Resources.MyEC2Instance.Properties.ImageId.[AWSRegionArch2AMI]
385
407
  ```
386
408
 
387
409
  returns only the EC2 ImageIds where the ImageId exactly matches `AWSRegionArch2AMI`.
@@ -392,7 +414,7 @@ returns only the EC2 ImageIds where the ImageId exactly matches `AWSRegionArch2A
392
414
  Pattern Ranges function much like Key Ranges, but utilize regex-based pattern matching for comparison. Adapting the above example:
393
415
 
394
416
  ```
395
- Resources::MyEC2Instance::Properties::ImageId::[(^AWSRegion)]
417
+ Resources.MyEC2Instance.Properties.ImageId.[<^AWSRegion>]
396
418
  ```
397
419
 
398
420
  returns only the EC2 ImageIds where the ImageId begins with `AWSRegion`. This can be helpful in checking for and enforcing naming standards, etc.
@@ -405,13 +427,13 @@ Wildcard Ranges extend the powerful functionality of Wildcard Tokens with the ad
405
427
  For example we know:
406
428
 
407
429
  ```
408
- Resources::*::Type
430
+ Resources.*.Type
409
431
  ```
410
432
 
411
433
  Selects all `Resource` objects. If we convert the Wildcard Token in the query to a Wildcard Range Token:
412
434
 
413
435
  ```
414
- Resources::[*]::Type
436
+ Resources.[*].Type
415
437
  ```
416
438
 
417
439
  The Rule will fail as below:
@@ -445,7 +467,7 @@ Note that the array we want is nested within another array, and we need to make
445
467
  We can accomplish this by using a Wildcard Range Token in our Query as below:
446
468
 
447
469
  ```
448
- Resources::AppendItemToListFunction::Properties::Code::ZipFile::[*]::[]
470
+ Resources.AppendItemToListFunction.Properties.Code.ZipFile.[*].[]
449
471
  ```
450
472
 
451
473
  Which allows us to then evaluate the Unbounded Range token against each array item, returning only the array we want.
@@ -462,7 +484,7 @@ You can use multiple Tokens within a Range Token by seperating each token with a
462
484
  For example:
463
485
 
464
486
  ```
465
- Resources::SecurityGroup::Properties::SecurityGroupIngress::[0, -2]
487
+ Resources.SecurityGroup.Properties.SecurityGroupIngress.[0, -2]
466
488
  ```
467
489
 
468
490
  Would select all except the last element of an array.
@@ -470,7 +492,7 @@ Would select all except the last element of an array.
470
492
  This also applies to Bounded Ranges, Key Ranges, Pattern Ranges, and Wildcard Ranges! For example:
471
493
 
472
494
  ```
473
- Resources::MyEC2Instance::Properties::ImageId::[(^AWSRegion),(^),(^Custom)]
495
+ Resources.MyEC2Instance.Properties.ImageId.[(^AWSRegion),(^),(^Custom)]
474
496
  ```
475
497
 
476
498
  will select any EC2 ImageIds that start with either `AWSRegion` or `Custom`.
@@ -494,17 +516,42 @@ ZipFile: !Join
494
516
  from our previous examples, we used the below query to select the nested array:
495
517
 
496
518
  ```
497
- Resources::AppendItemToListFunction::Properties::Code::ZipFile::[*]::[]
519
+ Resources.AppendItemToListFunction.Properties.Code.ZipFile.[*].[]
498
520
  ```
499
521
 
500
522
  With Nested Ranges, this can be shortened to:
501
523
 
502
524
  ```
503
- Resources::AppendItemToListFunction::Properties::Code::ZipFile::[[]]
525
+ Resources.AppendItemToListFunction.Properties.Code.ZipFile.[[]]
504
526
  ```
505
527
 
506
528
  Which is both more concise *and* more representitave of our intention to select only the array.
507
529
 
530
+ # Grouping Queries
531
+
532
+ 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:
533
+
534
+ ```
535
+ Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)
536
+ ```
537
+
538
+ This could be rewritten as:
539
+ ```
540
+ Resources.*(Type == AWS::Lambda::Function | Type == AWS::Serverless::Function)
541
+ ```
542
+
543
+ A more likely scenario might be finding specifically NodeJS Lambda functions. For example:
544
+
545
+ ```
546
+ Resources.*.(Type == AWS::Lambda::Function,AWS::Serverless::Function & Properties.Runtime == <nodejs20>)
547
+ ```
548
+
549
+ Queries are "split" by `|` operator and then "grouped" by `&` operator. That means if we want a query to match one set of criterion <i>or</i> another we could write:
550
+
551
+ ```
552
+ Resources.*.(Type == AWS::Lambda::Function & Properties.Runtime == <nodejs20> | Type == AWS::Serverless::Function & Properties.Runtime == <nodejs20>)
553
+ ```
554
+
508
555
  <br/>
509
556
 
510
557
  # Using Pydantic Models
@@ -520,8 +567,8 @@ from cfn_check import Collection, Rule
520
567
  class ValidateResourceType(Collection):
521
568
 
522
569
  @Rule(
523
- "Resources::*::Type",
524
- "It checks Resource::Type is correctly definined",
570
+ "Resources.*.Type",
571
+ "It checks Resource.Type is correctly definined",
525
572
  )
526
573
  def validate_test(self, value: str):
527
574
  assert value is not None, '❌ Resource Type not defined'
@@ -541,8 +588,8 @@ class Resource(BaseModel):
541
588
  class ValidateResourceType(Collection):
542
589
 
543
590
  @Rule(
544
- "Resources::*",
545
- "It checks Resource::Type is correctly definined",
591
+ "Resources.*",
592
+ "It checks Resource.Type is correctly definined",
546
593
  )
547
594
  def validate_test(self, value: Resource):
548
595
  assert value is not None
@@ -552,6 +599,34 @@ By deferring type and existence assertions to `Pydantic` models, you can focus y
552
599
 
553
600
  <br/>
554
601
 
602
+ # Using .query()
603
+
604
+ 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.
605
+
606
+ 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:
607
+
608
+ ```python
609
+ @Rule("Resources.*.(Type in AWS::Lambda::Function,AWS::Serverless::Function)", "It validates a lambda is configured correctly")
610
+ def validate_lambda(self, lambda_resource: Lambda):
611
+ assert isinstance(lambda_resource, Lambda), "Not a valid Lambda"
612
+
613
+ log_groups = self.query(
614
+ f"Resources.{lambda_resource.Properties.LoggingConfig.LogGroup}",
615
+ transforms=[
616
+ lambda data: LoggingGroup(**data)
617
+ ]
618
+ )
619
+
620
+ assert log_groups is not None, f"No resources found for LoggingGroup {lambda_resource.Properties.LoggingConfig.LogGroup}"
621
+ assert len(log_groups) > 0, "No matching logging group found in Resources for Lambda"
622
+ ```
623
+
624
+ The `query()` method accepts the following parameters:
625
+
626
+ - `query - (str, required)`: A string CFN-Check query as used when defining Rules.
627
+ - `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-values` optional CLI arg, or under the `input_values` config key. This will cause the query specified in the call to execute against the template specified.
628
+ - `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 a `LoggingGroup` Pydantic model instance.
629
+
555
630
  # The Rendering Engine
556
631
 
557
632
  ### Overview
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel, StrictStr, Field
2
+
3
+
4
+ class Config(BaseModel):
5
+ attributes: dict[StrictStr, StrictStr] | None = None
6
+ availability_zones: list[StrictStr] | None = None
7
+ import_values: dict[StrictStr, StrictStr] | None = None
8
+ mappings: dict[StrictStr, StrictStr] | None = None
9
+ parameters: dict[StrictStr, StrictStr] | None = None
10
+ references: dict[StrictStr, StrictStr] | None = None