cfn-check 0.3.3__py3-none-any.whl → 0.4.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 +70 -0
- cfn_check/cli/root.py +3 -1
- cfn_check/cli/utils/attributes.py +1 -1
- cfn_check/cli/utils/files.py +33 -3
- cfn_check/cli/validate.py +8 -1
- cfn_check/collection/collection.py +59 -1
- cfn_check/evaluation/evaluator.py +5 -1
- cfn_check/rendering/__init__.py +1 -0
- cfn_check/rendering/renderer.py +124 -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.4.0.dist-info}/METADATA +1 -1
- {cfn_check-0.3.3.dist-info → cfn_check-0.4.0.dist-info}/RECORD +19 -15
- example/pydantic_rules.py +102 -3
- example/renderer_test.py +42 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.4.0.dist-info}/WHEEL +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.4.0.dist-info}/entry_points.txt +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.4.0.dist-info}/top_level.txt +0 -0
cfn_check/cli/render.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
from async_logging import LogLevelName, Logger, LoggingConfig
|
|
3
|
+
from cocoa.cli import CLI
|
|
4
|
+
|
|
5
|
+
from cfn_check.cli.utils.files import load_templates, write_to_file
|
|
6
|
+
from cfn_check.rendering import Renderer
|
|
7
|
+
from cfn_check.logging.models import InfoLog
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@CLI.command()
|
|
11
|
+
async def render(
|
|
12
|
+
path: str,
|
|
13
|
+
output_file: str = 'rendered.yml',
|
|
14
|
+
mappings: list[str] | None = None,
|
|
15
|
+
tags: list[str] = [
|
|
16
|
+
'Ref',
|
|
17
|
+
'Sub',
|
|
18
|
+
'Join',
|
|
19
|
+
'Select',
|
|
20
|
+
'Split',
|
|
21
|
+
'GetAtt',
|
|
22
|
+
'GetAZs',
|
|
23
|
+
'ImportValue',
|
|
24
|
+
'Equals',
|
|
25
|
+
'If',
|
|
26
|
+
'Not',
|
|
27
|
+
'And',
|
|
28
|
+
'Or',
|
|
29
|
+
'Condition',
|
|
30
|
+
'FindInMap',
|
|
31
|
+
],
|
|
32
|
+
log_level: LogLevelName = 'info',
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Render a Cloud Formation template
|
|
36
|
+
|
|
37
|
+
@param output_file Path to output the rendered CloudFormation template to
|
|
38
|
+
@param mappings A list of <key>=<value> string pairs specifying Mappings
|
|
39
|
+
@param tags List of CloudFormation intrinsic function tags
|
|
40
|
+
@param log_level The log level to use
|
|
41
|
+
"""
|
|
42
|
+
logging_config = LoggingConfig()
|
|
43
|
+
logging_config.update(
|
|
44
|
+
log_level=log_level,
|
|
45
|
+
log_output='stderr',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
selected_mappings: dict[str, str] | None = None
|
|
49
|
+
|
|
50
|
+
if mappings:
|
|
51
|
+
selected_mappings = dict([
|
|
52
|
+
mapping.split('=', maxsplit=1) for mapping in mappings if len(mapping.split('=', maxsplit=1)) > 0
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
logger = Logger()
|
|
56
|
+
|
|
57
|
+
templates = await load_templates(
|
|
58
|
+
path,
|
|
59
|
+
tags,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert len(templates) == 1 , '❌ Can only render one file'
|
|
63
|
+
|
|
64
|
+
_, template = templates[0]
|
|
65
|
+
renderer = Renderer()
|
|
66
|
+
rendered = renderer.render(template, selected_mappings=selected_mappings)
|
|
67
|
+
|
|
68
|
+
await write_to_file(output_file, rendered)
|
|
69
|
+
|
|
70
|
+
await logger.log(InfoLog(message=f'✅ {path} template rendered'))
|
cfn_check/cli/root.py
CHANGED
|
@@ -7,6 +7,7 @@ from cocoa.ui.components.terminal import Section, SectionConfig
|
|
|
7
7
|
from cocoa.ui.components.header import Header, HeaderConfig
|
|
8
8
|
from cocoa.ui.components.terminal import Terminal, EngineConfig
|
|
9
9
|
|
|
10
|
+
from .render import render
|
|
10
11
|
from .validate import validate
|
|
11
12
|
|
|
12
13
|
async def create_header(
|
|
@@ -47,7 +48,8 @@ async def create_header(
|
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
@CLI.root(
|
|
51
|
+
@CLI.root(
|
|
52
|
+
render,
|
|
51
53
|
validate,
|
|
52
54
|
global_styles=CLIStyle(
|
|
53
55
|
header=create_header,
|
|
@@ -4,7 +4,7 @@ from cfn_check.validation.validator import Validator
|
|
|
4
4
|
|
|
5
5
|
def bind(
|
|
6
6
|
rule_set: Collection,
|
|
7
|
-
validation: Validator
|
|
7
|
+
validation: Validator,
|
|
8
8
|
):
|
|
9
9
|
validation.func = validation.func.__get__(rule_set, rule_set.__class__)
|
|
10
10
|
setattr(rule_set, validation.func.__name__, validation.func)
|
cfn_check/cli/utils/files.py
CHANGED
|
@@ -10,14 +10,14 @@ from cfn_check.loader.loader import (
|
|
|
10
10
|
)
|
|
11
11
|
from cfn_check.shared.types import YamlObject, Data
|
|
12
12
|
|
|
13
|
-
def open_template(path: str) -> YamlObject | None:
|
|
13
|
+
def open_template(path: str) -> tuple[str, YamlObject] | None:
|
|
14
14
|
|
|
15
15
|
if os.path.exists(path) is False:
|
|
16
16
|
return None
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
19
|
with open(path, 'r') as f:
|
|
20
|
-
return yaml.load(f, Loader=Loader)
|
|
20
|
+
return (path, yaml.load(f, Loader=Loader))
|
|
21
21
|
except Exception as e:
|
|
22
22
|
raise e
|
|
23
23
|
|
|
@@ -38,6 +38,16 @@ async def convert_to_cwd(loop: asyncio.AbstractEventLoop):
|
|
|
38
38
|
os.getcwd,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
async def convert_to_absolute(path: str, loop: asyncio.AbstractEventLoop) -> str:
|
|
42
|
+
abspath = pathlib.Path(path)
|
|
43
|
+
|
|
44
|
+
return str(
|
|
45
|
+
await loop.run_in_executor(
|
|
46
|
+
None,
|
|
47
|
+
abspath.absolute,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
41
51
|
async def localize_path(path: str, loop: asyncio.AbstractEventLoop):
|
|
42
52
|
localized = path.replace('~/', '')
|
|
43
53
|
|
|
@@ -99,7 +109,7 @@ async def load_templates(
|
|
|
99
109
|
Loader.add_constructor(f'!{tag}', new_tag)
|
|
100
110
|
|
|
101
111
|
|
|
102
|
-
templates: list[Data] = await asyncio.gather(*[
|
|
112
|
+
templates: list[tuple[str, Data]] = await asyncio.gather(*[
|
|
103
113
|
loop.run_in_executor(
|
|
104
114
|
None,
|
|
105
115
|
open_template,
|
|
@@ -114,3 +124,23 @@ async def load_templates(
|
|
|
114
124
|
assert len(found_templates) > 0, "❌ Could not open any templates"
|
|
115
125
|
|
|
116
126
|
return templates
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def write_to_file(path: str, data: YamlObject):
|
|
130
|
+
loop = asyncio.get_event_loop()
|
|
131
|
+
|
|
132
|
+
if path.startswith('~/'):
|
|
133
|
+
path = await localize_path(path, loop)
|
|
134
|
+
|
|
135
|
+
output_path = await convert_to_absolute(path, loop)
|
|
136
|
+
|
|
137
|
+
await loop.run_in_executor(
|
|
138
|
+
None,
|
|
139
|
+
_write_to_file,
|
|
140
|
+
output_path,
|
|
141
|
+
data,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _write_to_file(path: str, data: YamlObject):
|
|
145
|
+
with open(path, 'w') as yml:
|
|
146
|
+
yaml.safe_dump(data, yml, indent=2)
|
cfn_check/cli/validate.py
CHANGED
|
@@ -58,6 +58,11 @@ async def validate(
|
|
|
58
58
|
file_pattern=file_pattern,
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
+
for file, data in templates:
|
|
62
|
+
for name, rule in rules.data.items():
|
|
63
|
+
rules.data[name] = rule()
|
|
64
|
+
rules.data[name].documents[file] = data
|
|
65
|
+
|
|
61
66
|
validation_set = ValidationSet([
|
|
62
67
|
bind(
|
|
63
68
|
rule,
|
|
@@ -68,7 +73,9 @@ async def validate(
|
|
|
68
73
|
if isinstance(validation, Validator)
|
|
69
74
|
])
|
|
70
75
|
|
|
71
|
-
if validation_error := validation_set.validate(
|
|
76
|
+
if validation_error := validation_set.validate([
|
|
77
|
+
template_data for _, template_data in templates
|
|
78
|
+
]):
|
|
72
79
|
raise validation_error
|
|
73
80
|
|
|
74
81
|
templates_evaluated = len(templates)
|
|
@@ -1,2 +1,60 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
from pydantic import ValidationError
|
|
3
|
+
|
|
4
|
+
from cfn_check.shared.types import Data
|
|
5
|
+
from cfn_check.evaluation.evaluator import Evaluator
|
|
6
|
+
|
|
1
7
|
class Collection:
|
|
2
|
-
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.documents: dict[str, Data] = {}
|
|
11
|
+
self._evaluator = Evaluator()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def query(
|
|
15
|
+
self,
|
|
16
|
+
query: str,
|
|
17
|
+
document: str | None = None,
|
|
18
|
+
filters: list[Callable[[Data], Data]] | None = None
|
|
19
|
+
) -> list[Data] | None:
|
|
20
|
+
|
|
21
|
+
if document and (
|
|
22
|
+
document_data := self.documents.get(document)
|
|
23
|
+
):
|
|
24
|
+
return self._evaluator.match(
|
|
25
|
+
document_data,
|
|
26
|
+
query,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
results: list[tuple[str, Data]] = []
|
|
30
|
+
|
|
31
|
+
for document_data in self.documents.values():
|
|
32
|
+
result = self._evaluator.match(
|
|
33
|
+
document_data,
|
|
34
|
+
query,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
results.extend(result)
|
|
38
|
+
|
|
39
|
+
filtered: list[Data] = []
|
|
40
|
+
if filters:
|
|
41
|
+
try:
|
|
42
|
+
for _, found in results:
|
|
43
|
+
for filter in filters:
|
|
44
|
+
found = filter(found)
|
|
45
|
+
|
|
46
|
+
if found is None:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if found:
|
|
50
|
+
filtered.append(found)
|
|
51
|
+
|
|
52
|
+
return filtered
|
|
53
|
+
|
|
54
|
+
except ValidationError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
found for _, found in results
|
|
59
|
+
]
|
|
60
|
+
|
|
@@ -7,12 +7,14 @@ from cfn_check.shared.types import (
|
|
|
7
7
|
YamlObject,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
+
from cfn_check.rendering import Renderer
|
|
10
11
|
from .parsing import QueryParser
|
|
11
12
|
|
|
12
13
|
class Evaluator:
|
|
13
14
|
|
|
14
15
|
def __init__(self):
|
|
15
16
|
self._query_parser = QueryParser()
|
|
17
|
+
self._renderer = Renderer()
|
|
16
18
|
|
|
17
19
|
def match(
|
|
18
20
|
self,
|
|
@@ -20,7 +22,9 @@ class Evaluator:
|
|
|
20
22
|
path: str,
|
|
21
23
|
):
|
|
22
24
|
items: Items = deque()
|
|
23
|
-
|
|
25
|
+
|
|
26
|
+
rendered = self._renderer.render(resources)
|
|
27
|
+
items.append(rendered)
|
|
24
28
|
|
|
25
29
|
segments = path.split("::")[::-1]
|
|
26
30
|
# Queries can be multi-segment,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .renderer import Renderer as Renderer
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections import deque
|
|
3
|
+
|
|
4
|
+
from cfn_check.shared.types import (
|
|
5
|
+
Data,
|
|
6
|
+
Items,
|
|
7
|
+
YamlObject,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Renderer:
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.parameter_defaults: dict[str, str | int | float | bool | None] = {}
|
|
15
|
+
self.items: Items = deque()
|
|
16
|
+
self._ref_pattern = re.compile(r'^!Ref\s+')
|
|
17
|
+
self._visited: list[str | int] = []
|
|
18
|
+
self._data: YamlObject = {}
|
|
19
|
+
self._mappings: dict[str, dict[str, YamlObject]] = {}
|
|
20
|
+
self._selected_mappings: dict[str, YamlObject] = {}
|
|
21
|
+
self._inputs: dict[str, str] = {}
|
|
22
|
+
|
|
23
|
+
def render(
|
|
24
|
+
self,
|
|
25
|
+
resources: YamlObject,
|
|
26
|
+
selected_mappings: dict[str, str] | None = None,
|
|
27
|
+
):
|
|
28
|
+
data = resources.get("Resources", {})
|
|
29
|
+
self.items.clear()
|
|
30
|
+
self.items.append(data)
|
|
31
|
+
|
|
32
|
+
self._assemble_parameters(resources)
|
|
33
|
+
|
|
34
|
+
self._mappings = resources.get('Mappings', {})
|
|
35
|
+
|
|
36
|
+
if selected_mappings:
|
|
37
|
+
self._assemble_mappings(selected_mappings)
|
|
38
|
+
|
|
39
|
+
while len(self.items) > 0:
|
|
40
|
+
item = self.items.pop()
|
|
41
|
+
|
|
42
|
+
if isinstance(item, list):
|
|
43
|
+
self._visited.append((None, item))
|
|
44
|
+
self.items.extend([
|
|
45
|
+
(idx, val) for idx, val in enumerate(item)
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
elif isinstance(item, dict):
|
|
49
|
+
self._visited.append((None, item))
|
|
50
|
+
self.items.extend(list(item.items()))
|
|
51
|
+
|
|
52
|
+
elif isinstance(item, tuple):
|
|
53
|
+
key, value = item
|
|
54
|
+
self._parse_kv_pair(key, value)
|
|
55
|
+
|
|
56
|
+
last_item = data
|
|
57
|
+
validator = dict(resources)
|
|
58
|
+
validator_data = validator.get("Resources", {})
|
|
59
|
+
for key, value in self._visited:
|
|
60
|
+
|
|
61
|
+
if isinstance(value, str) and (
|
|
62
|
+
_ := self._selected_mappings.get(value)
|
|
63
|
+
):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
if isinstance(key, str) and isinstance(last_item, dict) and key in validator_data:
|
|
67
|
+
last_item[key] = value
|
|
68
|
+
|
|
69
|
+
elif isinstance(key, int) and isinstance(last_item, list) and (
|
|
70
|
+
value in validator_data or self.parameter_defaults.get(validator_data[key]) is not None
|
|
71
|
+
):
|
|
72
|
+
last_item[key] = value
|
|
73
|
+
|
|
74
|
+
if key and isinstance(value, (dict, list)):
|
|
75
|
+
last_item = value
|
|
76
|
+
validator_data = value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
return resources
|
|
80
|
+
|
|
81
|
+
def _parse_kv_pair(self, key: str | int, value: Data):
|
|
82
|
+
|
|
83
|
+
if isinstance(value, list):
|
|
84
|
+
self.items.extend([
|
|
85
|
+
(idx, val) for idx, val in enumerate(value)
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
elif isinstance(value, dict):
|
|
89
|
+
self.items.extend(list(value.items()))
|
|
90
|
+
|
|
91
|
+
else:
|
|
92
|
+
key, value = self._parse_value(key, value)
|
|
93
|
+
|
|
94
|
+
self._visited.append((key, value))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _parse_value(self, key: str | int, value: str | int | float | bool):
|
|
98
|
+
|
|
99
|
+
if val := self.parameter_defaults.get(key):
|
|
100
|
+
value = val
|
|
101
|
+
|
|
102
|
+
elif val := self.parameter_defaults.get(value):
|
|
103
|
+
value = val
|
|
104
|
+
|
|
105
|
+
return key, value
|
|
106
|
+
|
|
107
|
+
def _assemble_parameters(self, resources: YamlObject):
|
|
108
|
+
params: dict[str, Data] = resources.get("Parameters", {})
|
|
109
|
+
for param_name, param in params.items():
|
|
110
|
+
if default := param.get("Default"):
|
|
111
|
+
self.parameter_defaults[param_name] = default
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _assemble_mappings(
|
|
115
|
+
self,
|
|
116
|
+
selected_keys: dict[str, str]
|
|
117
|
+
):
|
|
118
|
+
for key, value in selected_keys.items():
|
|
119
|
+
if (
|
|
120
|
+
mapping := self._mappings.get(key)
|
|
121
|
+
) and (
|
|
122
|
+
selected := mapping.get(value)
|
|
123
|
+
):
|
|
124
|
+
self._selected_mappings[key] = selected
|
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,15 +1,16 @@
|
|
|
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=1Y5FBtLgtis7qgU2AfurdoPt9davSZ8N4_-B9s287_M,1789
|
|
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=GpeaFR12mvhVOPqmgvzr95HrAwpcRf0OeR0xtfHJkV4,3293
|
|
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=wfdaUIi8pQnhsZ1nlMVCagVj83IK17Q32APBRCVTv7Y,1493
|
|
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
|
|
@@ -19,17 +20,20 @@ cfn_check/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
19
20
|
cfn_check/loader/loader.py,sha256=7yiDLLW_vNp_8O47erLXjQQtAB47fU3nimb91N5N_R8,532
|
|
20
21
|
cfn_check/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
22
|
cfn_check/logging/models.py,sha256=-tBaK6p8mJ0cO8h2keEJ-VmtFX_VW4XzwAw2PtqbkF0,490
|
|
23
|
+
cfn_check/rendering/__init__.py,sha256=atcbddYun4YHyY7bVGA9CgEYzzXpYzvkx9_Kg-gnD5w,42
|
|
24
|
+
cfn_check/rendering/renderer.py,sha256=ouaKberHGL-mhqefnjTa1AqRQS_Zzm0sTIribotoLNo,3695
|
|
22
25
|
cfn_check/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
cfn_check/rules/rule.py,sha256=
|
|
26
|
+
cfn_check/rules/rule.py,sha256=_cKNQ5ciJgPj-exmtBUz31cU2lxWYxw2n2NWIlhYc3s,635
|
|
24
27
|
cfn_check/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
28
|
cfn_check/shared/types.py,sha256=-om3DyZsjK_tJd-I8SITkoE55W0nB2WA3LOc87Cs7xI,414
|
|
26
29
|
cfn_check/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
-
cfn_check/validation/validator.py,sha256=
|
|
28
|
-
cfn_check-0.
|
|
29
|
-
example/pydantic_rules.py,sha256=
|
|
30
|
+
cfn_check/validation/validator.py,sha256=Z6S6T_4yQW1IUa5Kv3ohR9U8NDrhTvBadW2FEM8TRL8,1478
|
|
31
|
+
cfn_check-0.4.0.dist-info/licenses/LICENSE,sha256=EbCpGNzOkyQ53ig7J2Iwgmy4Og0dgHe8COo3WylhIKk,1069
|
|
32
|
+
example/pydantic_rules.py,sha256=6NFtDiaqmnYWt6oZIWB7AO_v5LJoZVOGXrmEe2_J_rI,4162
|
|
33
|
+
example/renderer_test.py,sha256=vr5Xb-Gk_B6fb9FTsFCANbmTNEv67ab5tNODhPOSXKE,738
|
|
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.4.0.dist-info/METADATA,sha256=5QubOsO2SPcHmlC_n_QqGQgZvsxXsSCDxYRHUMdTwwg,20459
|
|
36
|
+
cfn_check-0.4.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
|
37
|
+
cfn_check-0.4.0.dist-info/entry_points.txt,sha256=B4lCHoDHmwisABxKgRLShwqqFv7QwwDAFXoAChOnkwg,53
|
|
38
|
+
cfn_check-0.4.0.dist-info/top_level.txt,sha256=hUn9Ya50yY1fpgWxEhG5iMgfMDDVX7qWQnM1xrgZnhM,18
|
|
39
|
+
cfn_check-0.4.0.dist-info/RECORD,,
|
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.render 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|