cfn-check 0.3.3__py3-none-any.whl → 0.5.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.

@@ -0,0 +1,13 @@
1
+ from typing import Any
2
+ from ruamel.yaml.comments import CommentedMap, CommentedSeq
3
+
4
+
5
+ def assign(parent: CommentedMap | CommentedSeq | None, key_or_index: Any, value: Any):
6
+ if parent is None:
7
+ return # root already set
8
+ if isinstance(parent, CommentedMap):
9
+ parent[key_or_index] = value
10
+ else:
11
+ # key_or_index is an int for sequences
12
+ # Ensure sequence large enough (iterative approach assigns in order, so append is fine)
13
+ parent.append(value)
cfn_check/rules/rule.py CHANGED
@@ -13,13 +13,16 @@ class Rule:
13
13
  self,
14
14
  query: str,
15
15
  name: str,
16
+ filters: list[Callable[[JsonValue], JsonValue]] | None = None
16
17
  ):
17
18
  self.query = query
18
19
  self.name = name
20
+ self.filters = filters
19
21
 
20
22
  def __call__(self, func: Callable[[T], None]):
21
23
  return Validator[T](
22
24
  func,
23
25
  self.query,
24
26
  self.name,
27
+ filters=self.filters,
25
28
  )
@@ -14,10 +14,12 @@ class Validator(Generic[T]):
14
14
  func: Callable[[T], None],
15
15
  query: str,
16
16
  name: str,
17
+ filters: list[Callable[[Data], Data]] | None = None
17
18
  ):
18
19
  self.func = func
19
20
  self.query = query
20
21
  self.name = name
22
+ self.filters = filters
21
23
 
22
24
  self.model: BaseModel | None = None
23
25
 
@@ -31,8 +33,16 @@ class Validator(Generic[T]):
31
33
 
32
34
  try:
33
35
  path, item = arg
36
+
37
+ if self.filters:
38
+ for filter in self.filters:
39
+ item = filter(item)
40
+
41
+ if item is None:
42
+ return
43
+
34
44
  if self.model and isinstance(item, dict):
35
- arg = self.model(**item)
45
+ item = self.model(**item)
36
46
 
37
47
  return self.func(item)
38
48
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfn-check
3
- Version: 0.3.3
3
+ Version: 0.5.0
4
4
  Summary: Validate Cloud Formation
5
5
  Author-email: Ada Lundhe <adalundhe@lundhe.audio>
6
6
  License: MIT License
@@ -1,35 +1,39 @@
1
1
  cfn_check/__init__.py,sha256=ccUo2YxBmuEmak1M5o-8J0ECLXNkDDUsLJ4mkm31GvU,96
2
2
  cfn_check/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- cfn_check/cli/root.py,sha256=dHq9zzXyj-Wrj-fzirtFjptShzWbBGsO3n9tspm-pec,1688
4
- cfn_check/cli/validate.py,sha256=EY6-YgORApOxCgFAmPdwmfDg1UA80GLsRzQv44zSuY4,1954
3
+ cfn_check/cli/render.py,sha256=7CuXd9e3JdbLxFcd0Yn7Die8XiVAyOUprgi_CFJERak,2232
4
+ cfn_check/cli/root.py,sha256=Fi-G3nP-HQMY4iPenF2xnkQF798x5cNWDqJZs9TH66A,1727
5
+ cfn_check/cli/validate.py,sha256=aQF-hCC7vcOpu5VNSkoM8DmrB2hZCgciQvFBHIrpnPc,2178
5
6
  cfn_check/cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- cfn_check/cli/utils/attributes.py,sha256=iIUIgl6cT5XEUOW7D54-xxmMpTis84ySQY1b9osB47E,339
7
- cfn_check/cli/utils/files.py,sha256=QMYYR7C7mXdDx_6jj1Ye9w7ol1twAzUEZ9hxSa-O4-k,2563
7
+ cfn_check/cli/utils/attributes.py,sha256=hEMWJfNcTOKqWrleS8idWlZP81wAq2J06yV-JQm_WNw,340
8
+ cfn_check/cli/utils/files.py,sha256=OVG95vfAbpfg-WdqoHT8UBGoa7KQima21KZTVp2mm6g,3378
8
9
  cfn_check/collection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- cfn_check/collection/collection.py,sha256=wNxahoOqQge3C56blz5VtOq6lX5MZ9F2JjQIyZ3_SxU,27
10
+ cfn_check/collection/collection.py,sha256=Fl5ONtvosLrksJklRoxER9j-YN5RUdPN45yS02Yw5jU,1492
10
11
  cfn_check/evaluation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
12
  cfn_check/evaluation/errors.py,sha256=yPJdtRYo67le4yMC9sYqcboCnkqKsJ3KPbSPFY2-Pi8,773
12
- cfn_check/evaluation/evaluator.py,sha256=RomoSlgimTNTjaUE0BdSslrPvPbFj6X8ScFinatERjs,2248
13
+ cfn_check/evaluation/evaluator.py,sha256=VtYXydZFkp66VaADB9nDmJOBlPJ6lKASSM8AP1xHBZE,2377
13
14
  cfn_check/evaluation/validate.py,sha256=yy8byYAoHxFqkS2HfewHup22B3bYtrUH2PhPuNAc--A,1547
14
15
  cfn_check/evaluation/parsing/__init__.py,sha256=s5TxU4mzsbNIpbMynbwibGR8ac0dTcf_2qUfGkAEDvQ,52
15
16
  cfn_check/evaluation/parsing/query_parser.py,sha256=4J3CJQKAyb11gugfx6OZT-mfSdNDB5Al8Jiy9DbJZMw,3459
16
17
  cfn_check/evaluation/parsing/token.py,sha256=nrg7Tca182WY0VhRqfsZ1UgpxsUX73vdLToSeK50DZE,7055
17
18
  cfn_check/evaluation/parsing/token_type.py,sha256=E5AVBerinBszMLjjc7ejwSSWEc0p0Ju_CNFhpoZi63c,325
18
- cfn_check/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- cfn_check/loader/loader.py,sha256=7yiDLLW_vNp_8O47erLXjQQtAB47fU3nimb91N5N_R8,532
20
19
  cfn_check/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
20
  cfn_check/logging/models.py,sha256=-tBaK6p8mJ0cO8h2keEJ-VmtFX_VW4XzwAw2PtqbkF0,490
21
+ cfn_check/rendering/__init__.py,sha256=atcbddYun4YHyY7bVGA9CgEYzzXpYzvkx9_Kg-gnD5w,42
22
+ cfn_check/rendering/renderer.py,sha256=mPLyXyv0yUsCZnKuLZg8yDN1NQlFivX-P8_bwhQqzXI,25903
23
+ cfn_check/rendering/utils.py,sha256=MNaKePylbJ9Bs4kjuoV0PpCmPJYttPXXvKQILemCrUI,489
22
24
  cfn_check/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- cfn_check/rules/rule.py,sha256=r6-eKCUdPaNIena2NDv3J_QlcWes1KLObI9ukRZZz1o,500
25
+ cfn_check/rules/rule.py,sha256=_cKNQ5ciJgPj-exmtBUz31cU2lxWYxw2n2NWIlhYc3s,635
24
26
  cfn_check/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
27
  cfn_check/shared/types.py,sha256=-om3DyZsjK_tJd-I8SITkoE55W0nB2WA3LOc87Cs7xI,414
26
28
  cfn_check/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- cfn_check/validation/validator.py,sha256=FGPeb8Uc8lvX3Y5rs-fxeJKIOqzUXwXh_gCFcy6d3b0,1182
28
- cfn_check-0.3.3.dist-info/licenses/LICENSE,sha256=EbCpGNzOkyQ53ig7J2Iwgmy4Og0dgHe8COo3WylhIKk,1069
29
- example/pydantic_rules.py,sha256=oWBBGBauSwRbrvZSYBk4ej3dsCY55zm3cHwl8vH_2lU,350
29
+ cfn_check/validation/validator.py,sha256=Z6S6T_4yQW1IUa5Kv3ohR9U8NDrhTvBadW2FEM8TRL8,1478
30
+ cfn_check-0.5.0.dist-info/licenses/LICENSE,sha256=EbCpGNzOkyQ53ig7J2Iwgmy4Og0dgHe8COo3WylhIKk,1069
31
+ example/multitag.py,sha256=QQfcRERGEDgTUCGqWRqRbXHrLwSX4jEOFq8ED4NJnz8,636
32
+ example/pydantic_rules.py,sha256=6NFtDiaqmnYWt6oZIWB7AO_v5LJoZVOGXrmEe2_J_rI,4162
33
+ example/renderer_test.py,sha256=XG5PVTSHztYXHrBw4bpwVuuYt1JNZdtLGJ-DZ9wPjFM,741
30
34
  example/rules.py,sha256=mWHB0DK283lb0CeSHgnyO5qiVTJJpybuwWXb4Yoa3zQ,3148
31
- cfn_check-0.3.3.dist-info/METADATA,sha256=-ElH6YysxwAbW9jdR5tpNn-CAQa3Z8NUY7vE5GzB77g,20459
32
- cfn_check-0.3.3.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
33
- cfn_check-0.3.3.dist-info/entry_points.txt,sha256=B4lCHoDHmwisABxKgRLShwqqFv7QwwDAFXoAChOnkwg,53
34
- cfn_check-0.3.3.dist-info/top_level.txt,sha256=hUn9Ya50yY1fpgWxEhG5iMgfMDDVX7qWQnM1xrgZnhM,18
35
- cfn_check-0.3.3.dist-info/RECORD,,
35
+ cfn_check-0.5.0.dist-info/METADATA,sha256=KJXT9Yc29NfwClW04f0TcQe1gI7iu82mNV_MpLb2aRg,20459
36
+ cfn_check-0.5.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
37
+ cfn_check-0.5.0.dist-info/entry_points.txt,sha256=B4lCHoDHmwisABxKgRLShwqqFv7QwwDAFXoAChOnkwg,53
38
+ cfn_check-0.5.0.dist-info/top_level.txt,sha256=hUn9Ya50yY1fpgWxEhG5iMgfMDDVX7qWQnM1xrgZnhM,18
39
+ cfn_check-0.5.0.dist-info/RECORD,,
example/multitag.py ADDED
@@ -0,0 +1,21 @@
1
+ import ruamel.yaml
2
+ import sys
3
+
4
+ class MultiTaggedObject:
5
+ def __init__(self, value, tags):
6
+ self.value = value
7
+ self.tags = tags
8
+
9
+ def represent_multi_tagged_object(dumper, data):
10
+ return dumper.represent_mapping('!MultiTagged', {'value': data.value, 'tags': data.tags})
11
+
12
+ def construct_multi_tagged_object(constructor, node):
13
+ mapping = constructor.construct_mapping(node)
14
+ return MultiTaggedObject(mapping['value'], mapping['tags'])
15
+
16
+ yaml = ruamel.yaml.YAML()
17
+ yaml.register_class(MultiTaggedObject)
18
+
19
+ # Example usage:
20
+ data = MultiTaggedObject("some_value", ["tag1", "tag2"])
21
+ yaml.dump({'item': data}, sys.stdout)
example/pydantic_rules.py CHANGED
@@ -1,10 +1,59 @@
1
1
  from cfn_check import Collection, Rule
2
- from pydantic import BaseModel, StrictStr
2
+ from pydantic import BaseModel, StrictStr, StrictInt, Field
3
+ from typing import Literal
4
+
5
+ class RDSDBProperties(BaseModel):
6
+ AvailabilityZone: StrictStr
7
+ BackupRetentionPeriod: StrictInt
8
+ DBInstanceClass: StrictStr = Field(pattern=r'^((db)\.(c6g|c6gd|c6gn|c6i|c6id|c7g|g5g|im4gn|is4gen|m6g|m6gd|r6g|r6gd|t4g|x2gd)\.(10xlarge|112xlarge|12xlarge|16xlarge|18xlarge|24xlarge|2xlarge|32xlarge|3xlarge|48xlarge|4xlarge|56xlarge|6xlarge|8xlarge|9xlarge|large|medium|metal|micro|nano|small|xlarge))')
9
+ StorageType: Literal['sc1', 'st1', 'gp3']
10
+
11
+ class RDSDBInstance(BaseModel):
12
+ Type: Literal["AWS::RDS::DBInstance"]
13
+
14
+ class EC2EbsDevice(BaseModel):
15
+ VolumeType: StrictStr
16
+ DeleteOnTermination: Literal[True]
17
+
18
+ class EC2BlockDeviceMappings(BaseModel):
19
+ Ebs: EC2EbsDevice
20
+
21
+ class EC2VolumeProperties(BaseModel):
22
+ VolumeType: Literal["sc1", "st1", "gp3"]
23
+ BlockDeviceMappings: EC2BlockDeviceMappings
24
+
25
+ class EC2Volume(BaseModel):
26
+ Type: Literal["AWS::EC2::Volume"]
27
+ Properties: EC2VolumeProperties
28
+
29
+ class EC2InstanceProperties(BaseModel):
30
+ InstanceType: StrictStr = Field(pattern=r'^((c6g|c6gd|c6gn|c6i|c6id|c7g|g5g|im4gn|is4gen|m6g|m6gd|r6g|r6gd|t4g|x2gd)\.(10xlarge|112xlarge|12xlarge|16xlarge|18xlarge|24xlarge|2xlarge|32xlarge|3xlarge|48xlarge|4xlarge|56xlarge|6xlarge|8xlarge|9xlarge|large|medium|metal|micro|nano|small|xlarge))')
31
+
32
+ class EC2Instance(BaseModel):
33
+ Type: Literal["AWS::EC2::Instance"]
34
+ Properties: EC2InstanceProperties
35
+
36
+ class LoggingGroupProperties(BaseModel):
37
+ LogGroupClass: Literal["Infrequent Access"]
38
+ RetentionInDays: StrictInt
39
+
40
+ class LoggingGroup(BaseModel):
41
+ Type: Literal["AWS::Logs::LogGroup"]
42
+ Properties: LoggingGroupProperties
43
+
44
+ class LambdaLoggingConfig(BaseModel):
45
+ LogGroup: StrictStr
46
+
47
+ class LambdaProperties(BaseModel):
48
+ LoggingConfig: LambdaLoggingConfig
49
+
50
+ class Lambda(BaseModel):
51
+ Type: Literal["AWS::Serverless::Function", "AWS::Lambda::Function"]
52
+ Properties: LambdaProperties
3
53
 
4
54
  class Resource(BaseModel):
5
55
  Type: StrictStr
6
56
 
7
-
8
57
  class ValidateResourceType(Collection):
9
58
 
10
59
  @Rule(
@@ -12,4 +61,54 @@ class ValidateResourceType(Collection):
12
61
  "It checks Resource::Type is correctly definined",
13
62
  )
14
63
  def validate_test(self, value: Resource):
15
- assert value is not None
64
+ assert isinstance(value, Resource), "Not a valid CloudFormation Resource"
65
+
66
+ @Rule(
67
+ "Resources::*",
68
+ "It validates a lambda is configured correctly",
69
+ filters=[
70
+ lambda data: data if data.get("Type") in ["AWS::Serverless::Function", "AWS::Lambda::Function"] else None,
71
+ ]
72
+ )
73
+ def validate_lambda(self, lambda_resource: Lambda):
74
+ assert isinstance(lambda_resource, Lambda), "Not a valid Lambda"
75
+
76
+ resources = self.query("Resources")
77
+ document = {}
78
+
79
+ for resource in resources:
80
+ document.update(resource)
81
+
82
+ lambda_logging_group = document.get(lambda_resource.Properties.LoggingConfig.LogGroup)
83
+ assert lambda_logging_group is not None, "No matching logging group found in Resources for Lambda"
84
+ LoggingGroup(**lambda_logging_group)
85
+
86
+ @Rule(
87
+ "Resources::*",
88
+ "It validates a logging group is configured correctly",
89
+ filters=[
90
+ lambda data: data if data.get("Type") == 'AWS::Logs::LogGroup' else None,
91
+ ]
92
+ )
93
+ def validate_logging_group(self, logging_group: LoggingGroup):
94
+ assert isinstance(logging_group, LoggingGroup), "Not a valid logging group"
95
+
96
+ @Rule(
97
+ "Resources::*",
98
+ "It validates an EC2 instance is configured correctly",
99
+ filters=[
100
+ lambda data: data if data.get("Type") == 'AWS::EC2::Instance' else None,
101
+ ]
102
+ )
103
+ def validate_ec2_instances(self, ec2_instance: EC2Instance):
104
+ assert isinstance(ec2_instance, EC2Instance)
105
+
106
+ @Rule(
107
+ "Resources::*",
108
+ "It validates an EC2 Volume is configured correctly",
109
+ filters=[
110
+ lambda data: data if data.get("Type") == 'AWS::EC2::Volume' else None,
111
+ ]
112
+ )
113
+ def validate_ec2_volumes(self, ec2_volume: EC2Volume):
114
+ assert isinstance(ec2_volume, EC2Volume), "Not a valid EC2 Volume"
@@ -0,0 +1,42 @@
1
+ import yaml
2
+ from cfn_check.rendering import Renderer
3
+ from cfn_check.cli.utils.files import open_template, Loader, create_tag
4
+ def test():
5
+
6
+ renderer = Renderer()
7
+ data = {}
8
+
9
+ tags = [
10
+ 'Ref',
11
+ 'Sub',
12
+ 'Join',
13
+ 'Select',
14
+ 'Split',
15
+ 'GetAtt',
16
+ 'GetAZs',
17
+ 'ImportValue',
18
+ 'Equals',
19
+ 'If',
20
+ 'Not',
21
+ 'And',
22
+ 'Or',
23
+ 'Condition',
24
+ 'FindInMap',
25
+ ]
26
+
27
+ for tag in tags:
28
+ new_tag = create_tag(tag)
29
+ Loader.add_constructor(f'!{tag}', new_tag)
30
+
31
+ _, template = open_template('template.yaml')
32
+
33
+ data = renderer.render(template)
34
+
35
+
36
+ with open('rendered.yaml', 'w') as yml:
37
+ yaml.safe_dump(data, yml)
38
+
39
+
40
+
41
+
42
+ test()
File without changes
@@ -1,21 +0,0 @@
1
- import yaml
2
- import pathlib
3
-
4
-
5
- class Loader(yaml.SafeLoader):
6
- pass
7
-
8
- def create_tag(tag):
9
- def constructor(loader: Loader, node):
10
- if isinstance(node, yaml.ScalarNode):
11
- return node.value
12
- elif isinstance(node, yaml.SequenceNode):
13
- return loader.construct_sequence(node)
14
- elif isinstance(node, yaml.MappingNode):
15
- return loader.construct_mapping(node)
16
- return constructor
17
-
18
-
19
- def find_templates(path, file_pattern):
20
- return list(pathlib.Path(path).rglob(file_pattern))
21
-