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.
- cfn_check/cli/render.py +83 -0
- cfn_check/cli/root.py +3 -1
- cfn_check/cli/utils/attributes.py +1 -1
- cfn_check/cli/utils/files.py +46 -20
- cfn_check/cli/validate.py +8 -1
- cfn_check/collection/collection.py +58 -1
- cfn_check/evaluation/evaluator.py +5 -1
- cfn_check/rendering/__init__.py +1 -0
- cfn_check/rendering/renderer.py +740 -0
- cfn_check/rendering/utils.py +13 -0
- cfn_check/rules/rule.py +3 -0
- cfn_check/validation/validator.py +11 -1
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/METADATA +1 -1
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/RECORD +21 -17
- example/multitag.py +21 -0
- example/pydantic_rules.py +102 -3
- example/renderer_test.py +42 -0
- cfn_check/loader/__init__.py +0 -0
- cfn_check/loader/loader.py +0 -21
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/WHEEL +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/entry_points.txt +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
45
|
+
item = self.model(**item)
|
|
36
46
|
|
|
37
47
|
return self.func(item)
|
|
38
48
|
|
|
@@ -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/
|
|
4
|
-
cfn_check/cli/
|
|
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=
|
|
7
|
-
cfn_check/cli/utils/files.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
28
|
-
cfn_check-0.
|
|
29
|
-
example/
|
|
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.
|
|
32
|
-
cfn_check-0.
|
|
33
|
-
cfn_check-0.
|
|
34
|
-
cfn_check-0.
|
|
35
|
-
cfn_check-0.
|
|
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
|
|
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"
|
example/renderer_test.py
ADDED
|
@@ -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()
|
cfn_check/loader/__init__.py
DELETED
|
File without changes
|
cfn_check/loader/loader.py
DELETED
|
@@ -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
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|