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.

@@ -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)
@@ -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(templates):
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
- pass
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
- items.append(resources)
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
- 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.4.0
4
4
  Summary: Validate Cloud Formation
5
5
  Author-email: Ada Lundhe <adalundhe@lundhe.audio>
6
6
  License: MIT License
@@ -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/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=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=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=GpeaFR12mvhVOPqmgvzr95HrAwpcRf0OeR0xtfHJkV4,3293
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=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=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
@@ -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=r6-eKCUdPaNIena2NDv3J_QlcWes1KLObI9ukRZZz1o,500
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=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
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.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.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 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.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()