cfn-check 0.1.1__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/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .rules.rules import Rules as Rules
2
+ from .rules.rule import Rule as Rule
File without changes
cfn_check/cli/root.py ADDED
@@ -0,0 +1,72 @@
1
+ import asyncio
2
+ import sys
3
+
4
+ from cocoa.cli import CLI, CLIStyle
5
+ from cocoa.ui.config.mode import TerminalMode
6
+ from cocoa.ui.components.terminal import Section, SectionConfig
7
+ from cocoa.ui.components.header import Header, HeaderConfig
8
+ from cocoa.ui.components.terminal import Terminal, EngineConfig
9
+
10
+ from .validate import validate
11
+
12
+ async def create_header(
13
+ terminal_mode: TerminalMode = "full",
14
+ ):
15
+
16
+ cfn_check_terminal_mode = "extended"
17
+ if terminal_mode == "ci":
18
+ cfn_check_terminal_mode = "compatability"
19
+
20
+ header = Section(
21
+ SectionConfig(height="xx-small", width="large"),
22
+ components=[
23
+ Header(
24
+ "header",
25
+ HeaderConfig(
26
+ header_text="cfn-check",
27
+ color="indian_red_3",
28
+ attributes=["bold"],
29
+ terminal_mode=cfn_check_terminal_mode,
30
+ font='cyberpunk'
31
+ ),
32
+ ),
33
+ ],
34
+ )
35
+
36
+ terminal = Terminal(
37
+ [
38
+ header,
39
+ ],
40
+ config=EngineConfig(
41
+ max_height=40,
42
+ max_width=120
43
+ )
44
+ )
45
+
46
+ return await terminal.render_once()
47
+
48
+
49
+
50
+ @CLI.root(
51
+ validate,
52
+ global_styles=CLIStyle(
53
+ header=create_header,
54
+ flag_description_color="white",
55
+ error_color="indian_red_3",
56
+ error_attributes=["italic"],
57
+ flag_color="indian_red_3",
58
+ text_color="slate_blue_2",
59
+ subcommand_color="slate_blue_2",
60
+ indentation=5,
61
+ terminal_mode="extended",
62
+ ),
63
+ )
64
+ async def cfn_check():
65
+ '''
66
+ Check and test CloudFormation
67
+ '''
68
+
69
+
70
+
71
+ def run():
72
+ asyncio.run(CLI.run(sys.argv[1:]))
File without changes
@@ -0,0 +1,12 @@
1
+ from cfn_check.rules.rules import Rules
2
+ from cfn_check.validation.validator import Validator
3
+
4
+
5
+ def bind(
6
+ rule_set: Rules,
7
+ validation: Validator
8
+ ):
9
+ validation.func = validation.func.__get__(rule_set, rule_set.__class__)
10
+ setattr(rule_set, validation.func.__name__, validation.func)
11
+
12
+ return validation
@@ -0,0 +1,68 @@
1
+
2
+ import asyncio
3
+ import os
4
+ import yaml
5
+ from cfn_check.loader.loader import (
6
+ Loader,
7
+ create_tag,
8
+ find_templates,
9
+ )
10
+ from cfn_check.shared.types import YamlObject, Data
11
+
12
+ def open_template(path: str) -> YamlObject | Exception:
13
+ try:
14
+ with open(path, 'r') as f:
15
+ return yaml.load(f, Loader=Loader)
16
+ except (Exception, ) as e:
17
+ raise e
18
+
19
+ def is_file(path: str) -> bool:
20
+ return os.path.isdir(path) is False
21
+
22
+
23
+ async def load_templates(
24
+ path: str,
25
+ tags: list[str],
26
+ file_pattern: str | None = None,
27
+ ):
28
+ loop = asyncio.get_event_loop()
29
+
30
+ if await loop.run_in_executor(
31
+ None,
32
+ is_file,
33
+ path,
34
+ ) or file_pattern is None:
35
+ template_filepaths = [
36
+ path,
37
+ ]
38
+
39
+ elif file_pattern:
40
+
41
+ template_filepaths = await loop.run_in_executor(
42
+ None,
43
+ find_templates,
44
+ path,
45
+ file_pattern,
46
+ )
47
+
48
+ assert len(template_filepaths) > 0 , '❌ No matching files found'
49
+
50
+ for tag in tags:
51
+ new_tag = await loop.run_in_executor(
52
+ None,
53
+ create_tag,
54
+ tag,
55
+ )
56
+
57
+ Loader.add_constructor(f'!{tag}', new_tag)
58
+
59
+
60
+ templates: list[Data] = await asyncio.gather(*[
61
+ loop.run_in_executor(
62
+ None,
63
+ open_template,
64
+ template_path,
65
+ ) for template_path in template_filepaths
66
+ ])
67
+
68
+ return templates
@@ -0,0 +1,81 @@
1
+ import inspect
2
+
3
+ from async_logging import LogLevelName, Logger, LoggingConfig
4
+ from cocoa.cli import CLI, ImportType
5
+
6
+ from cfn_check.cli.utils.attributes import bind
7
+ from cfn_check.cli.utils.files import load_templates
8
+ from cfn_check.evaluation.validate import run_validations
9
+ from cfn_check.logging.models import InfoLog
10
+ from cfn_check.rules.rules import Rules
11
+ from cfn_check.validation.validator import Validator
12
+
13
+
14
+ @CLI.command()
15
+ async def validate(
16
+ path: str,
17
+ file_pattern: str | None = None,
18
+ rules: ImportType[Rules] = None,
19
+ tags: list[str] = [
20
+ 'Ref',
21
+ 'Sub',
22
+ 'Join',
23
+ 'Select',
24
+ 'Split',
25
+ 'GetAtt',
26
+ 'GetAZs',
27
+ 'ImportValue',
28
+ 'Equals',
29
+ 'If',
30
+ 'Not',
31
+ 'And',
32
+ 'Or',
33
+ 'Condition',
34
+ 'FindInMap',
35
+ ],
36
+ log_level: LogLevelName = 'info',
37
+ ):
38
+ '''
39
+ Validate Cloud Foundation
40
+
41
+ @param rules Path to a file containing Rules
42
+ @param file_pattern A string pattern used to find template files
43
+ @param tags List of CloudFormation intrinsic function tags
44
+ @param log_level The log level to use
45
+ '''
46
+
47
+ logging_config = LoggingConfig()
48
+ logging_config.update(
49
+ log_level=log_level,
50
+ log_output='stderr',
51
+ )
52
+
53
+ logger = Logger()
54
+
55
+ templates = await load_templates(
56
+ path,
57
+ tags,
58
+ file_pattern=file_pattern,
59
+ )
60
+
61
+ validations: list[Validator] = [
62
+ bind(
63
+ rule,
64
+ validation,
65
+ )
66
+ for rule in rules.data.values()
67
+ for _, validation in inspect.getmembers(rule)
68
+ if isinstance(validation, Validator)
69
+ ]
70
+
71
+ if validation_error := run_validations(
72
+ templates,
73
+ validations,
74
+ ):
75
+ raise validation_error
76
+
77
+ checks_passed = len(validations)
78
+ templates_evaluated = len(templates)
79
+
80
+ await logger.log(InfoLog(message=f'✅ {checks_passed} validations met for {templates_evaluated} templates'))
81
+
File without changes
@@ -0,0 +1,20 @@
1
+ from typing import TypeVar
2
+
3
+ from pydantic import ValidationError
4
+
5
+ from cfn_check.validation.validator import Validator
6
+ from cfn_check.shared.types import (
7
+ YamlList,
8
+ YamlObject,
9
+ YamlValueBase,
10
+ )
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ def check(
16
+ matched: YamlList | YamlObject | YamlValueBase,
17
+ assertion: Validator[T],
18
+ ) -> Exception | ValidationError | None:
19
+ if err := assertion(matched):
20
+ return err
@@ -0,0 +1,33 @@
1
+ import textwrap
2
+ from pydantic import ValidationError
3
+ from cfn_check.rules.rule import Validator
4
+
5
+
6
+ def assemble_validation_error(
7
+ errors: list[
8
+ tuple[
9
+ Validator,
10
+ Exception | ValidationError,
11
+ ],
12
+ ],
13
+ ) -> Exception:
14
+ validation_error: Exception | None = None
15
+ if len(errors) > 0:
16
+
17
+ error_message = textwrap.indent(
18
+ '\n'.join([
19
+ (
20
+ f'Rule: {validator.name} failed\n'
21
+ f'Query: {validator.query}\n'
22
+ f'{str(err)}\n'
23
+ )
24
+ for validator, err in errors
25
+ ]),
26
+ '\t',
27
+ )
28
+
29
+ validation_error = Exception(
30
+ f'\n{error_message}',
31
+ )
32
+
33
+ return validation_error
@@ -0,0 +1,137 @@
1
+ import re
2
+ from collections import deque
3
+ from typing import Deque
4
+ from cfn_check.shared.types import (
5
+ Data,
6
+ Items,
7
+ YamlList,
8
+ YamlObject,
9
+ YamlValueBase,
10
+ )
11
+
12
+
13
+ numbers_pattern = re.compile(r'\d+')
14
+
15
+ def search(
16
+ resources: YamlObject,
17
+ path: str,
18
+ ):
19
+ items: Items = deque()
20
+ items.append(resources)
21
+
22
+ segments = path.split("::")[::-1]
23
+ # Queries can be multi-segment,
24
+ # so we effectively perform per-segment
25
+ # repeated DFS searches, returning the matches
26
+ # for each segment
27
+
28
+ composite_keys: list[str] = []
29
+
30
+ while len(segments):
31
+ query = segments.pop()
32
+ items, keys = search_with_query(items, query)
33
+
34
+ if len(composite_keys) == 0:
35
+ composite_keys.extend(keys)
36
+
37
+ else:
38
+ updated_keys: list[str] = []
39
+ for composite_key in composite_keys:
40
+ while len(keys):
41
+ key = keys.pop()
42
+
43
+ updated_keys.append(f'{composite_key}.{key}')
44
+
45
+ composite_keys = updated_keys
46
+
47
+ results: list[tuple[str, Data]] = []
48
+ for idx, item in enumerate(list(items)):
49
+ results.append((
50
+ composite_keys[idx],
51
+ item,
52
+ ))
53
+
54
+ return results
55
+
56
+
57
+ def parse_list_query(query: str):
58
+
59
+ queries = query.strip('[]').split(',')
60
+
61
+ if len(queries) < 1:
62
+ return None
63
+
64
+ indexes = []
65
+ for query in queries:
66
+
67
+ if match := numbers_pattern.match(query):
68
+ indexes.append(
69
+ int(match.group(0))
70
+ )
71
+
72
+ return indexes
73
+
74
+
75
+ def parse_list_matches(
76
+ query: str,
77
+ node: YamlList,
78
+ ):
79
+ if indexes := parse_list_query(query):
80
+ return [
81
+ item
82
+ for idx, item in enumerate(node)
83
+ if idx in indexes
84
+ ]
85
+
86
+ return [
87
+ str(idx) for idx in indexes
88
+ ], node
89
+
90
+
91
+ def search_with_query(
92
+ items: Items,
93
+ query: str,
94
+ ) -> tuple[Items, Deque[str]]:
95
+
96
+ found: Items = deque()
97
+
98
+ keys: Deque[str] = deque()
99
+
100
+ while len(items):
101
+ node = items.pop()
102
+
103
+ key: (
104
+ str | None
105
+ ) = None
106
+ value: (
107
+ YamlValueBase | YamlList | YamlObject | None
108
+ ) = None
109
+
110
+ if isinstance(node, dict):
111
+ items.extend(node.items())
112
+
113
+ elif query.startswith('[') and query.startswith(']'):
114
+ indexes, matched = parse_list_matches(query, node)
115
+ keys.extend(indexes)
116
+ found.extend(matched)
117
+
118
+ elif isinstance(node, list):
119
+ items.extend(node)
120
+
121
+ elif isinstance(node, tuple):
122
+ key, value = node
123
+
124
+ else:
125
+ # If we encounter just
126
+ # a raw YAML int/bool/etc
127
+ # then we should just
128
+ # skip to the next iteration
129
+ continue
130
+
131
+ if (
132
+ key == query or query == "*"
133
+ ) and value:
134
+ keys.append(key)
135
+ found.append(value)
136
+
137
+ return found, keys
@@ -0,0 +1,51 @@
1
+ from pydantic import ValidationError
2
+
3
+ from cfn_check.validation.validator import Validator
4
+ from .check import check
5
+ from .errors import assemble_validation_error
6
+ from .search import search
7
+
8
+ def run_validations(
9
+ templates: list[str],
10
+ validators: list[Validator],
11
+ ):
12
+ errors: list[Exception | ValidationError] = []
13
+
14
+ for template in templates:
15
+ for validator in validators:
16
+ if errs := run_validation(
17
+ validator,
18
+ template,
19
+ ):
20
+ errors.extend([
21
+ (
22
+ validator,
23
+ err
24
+ ) for err in errs
25
+ ])
26
+
27
+ if validation_error := assemble_validation_error(errors):
28
+ return validation_error
29
+
30
+
31
+ def run_validation(
32
+ validator: Validator,
33
+ template: str,
34
+ ):
35
+ found = search(template, validator.query)
36
+
37
+ assert len(found) > 0, "❌ No results matching rule"
38
+
39
+ errors: list[Exception | ValidationError] = []
40
+
41
+
42
+ for matched in found:
43
+ if err := check(
44
+ matched,
45
+ validator,
46
+ ):
47
+ errors.append(err)
48
+
49
+ if len(errors) > 0:
50
+ return errors
51
+
File without changes
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,20 @@
1
+ from async_logging import Entry, LogLevel
2
+
3
+
4
+ class DebugLog(Entry, kw_only=True):
5
+ level: LogLevel = LogLevel.DEBUG
6
+
7
+ class InfoLog(Entry, kw_only=True):
8
+ level: LogLevel = LogLevel.INFO
9
+
10
+ class ErrorLog(Entry, kw_only=True):
11
+ error: str | bytes
12
+ level: LogLevel = LogLevel.ERROR
13
+
14
+ class FatalLog(Entry, kw_only=True):
15
+ error: str | bytes
16
+ level: LogLevel = LogLevel.FATAL
17
+
18
+ class CriticalLog(Entry, kw_only=True):
19
+ error: str | bytes
20
+ level: LogLevel = LogLevel.CRITICAL
File without changes
@@ -0,0 +1,25 @@
1
+ from typing import TypeVar
2
+ from pydantic import BaseModel, JsonValue
3
+ from typing import Callable
4
+ from cfn_check.validation.validator import Validator
5
+
6
+
7
+ T = TypeVar("T", bound= JsonValue | BaseModel)
8
+
9
+
10
+ class Rule:
11
+
12
+ def __init__(
13
+ self,
14
+ query: str,
15
+ name: str,
16
+ ):
17
+ self.query = query
18
+ self.name = name
19
+
20
+ def __call__(self, func: Callable[[T], None]):
21
+ return Validator[T](
22
+ func,
23
+ self.query,
24
+ self.name,
25
+ )
@@ -0,0 +1,2 @@
1
+ class Rules:
2
+ pass
File without changes
@@ -0,0 +1,17 @@
1
+ from typing import Deque
2
+
3
+
4
+ YamlValueBase = str | bool | int | float | None
5
+ YamlObjectBase = dict[str, YamlValueBase]
6
+ YamlListBase = dict[YamlValueBase]
7
+ YamlObject = dict[str, YamlValueBase | YamlListBase | YamlObjectBase]
8
+ YamlList = list[YamlValueBase | YamlObjectBase | YamlObject]
9
+
10
+ Data = YamlList | YamlObject | YamlValueBase
11
+
12
+ Items = Deque[
13
+ YamlObject | YamlList | tuple[
14
+ str,
15
+ Data
16
+ ]
17
+ ]
File without changes
@@ -0,0 +1,47 @@
1
+ from typing import TypeVar, Generic
2
+ from pydantic import BaseModel, ValidationError, JsonValue
3
+ from typing import Callable, get_type_hints
4
+
5
+ from cfn_check.shared.types import Data
6
+
7
+
8
+ T = TypeVar("T", bound= JsonValue | BaseModel)
9
+
10
+
11
+ class Validator(Generic[T]):
12
+ def __init__(
13
+ self,
14
+ func: Callable[[T], None],
15
+ query: str,
16
+ name: str,
17
+ ):
18
+ self.func = func
19
+ self.query = query
20
+ self.name = name
21
+
22
+ self.model: BaseModel | None = None
23
+
24
+ for _, param in get_type_hints(
25
+ self.func,
26
+ ).items():
27
+ if param in BaseModel.__subclasses__():
28
+ self.model = param
29
+
30
+ def __call__(self, arg: tuple[str, Data]):
31
+
32
+ try:
33
+ path, item = arg
34
+ if self.model and isinstance(item, dict):
35
+ arg = self.model(**item)
36
+
37
+ return self.func(item)
38
+
39
+ except ValidationError as err:
40
+ return Exception(
41
+ f'Path: {path}\n❌ Validation Error: {str(err)}'
42
+ )
43
+
44
+ except Exception as err:
45
+ return Exception(
46
+ f'Path: {path}\nError: {str(err)}'
47
+ )
@@ -0,0 +1,247 @@
1
+ Metadata-Version: 2.4
2
+ Name: cfn-check
3
+ Version: 0.1.1
4
+ Summary: Validate Cloud Formation
5
+ Author-email: Ada Lundhe <adalundhe@lundhe.audio>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Ada Lündhé
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/adalundhe/cfn-check
29
+ Keywords: cloud-formation,testing,aws,cli
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Operating System :: OS Independent
33
+ Requires-Python: >=3.12
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: pydantic
37
+ Requires-Dist: pyyaml
38
+ Requires-Dist: hyperlight-cocoa
39
+ Requires-Dist: async-logging
40
+ Dynamic: license-file
41
+
42
+ # <b>CFN Check</b>
43
+ <b>A tool for checking CloudFormation</b>
44
+
45
+ [![PyPI version](https://img.shields.io/pypi/v/cfn-check?color=blue)](https://pypi.org/project/cfn-check/)
46
+ [![License](https://img.shields.io/github/license/adalundhe/cfn-check)](https://github.com/adalundhe/cfn-check/blob/main/LICENSE)
47
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/adalundhe/cfn-check/blob/main/CODE_OF_CONDUCT.md)
48
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cfn-check?color=red)](https://pypi.org/project/cfn-check/)
49
+
50
+
51
+ | Package | Cocoa |
52
+ | ----------- | ----------- |
53
+ | Version | 0.1.0 |
54
+ | Download | https://pypi.org/project/cfn-check/ |
55
+ | Source | https://github.com/adalundhe/cfn-check |
56
+ | Keywords | cloud-formation, testing, aws, cli |
57
+
58
+
59
+ CFN-Check is a small, fast, friendly tool for validating AWS CloudFormation YAML templates. It is code-driven, with
60
+ rules written as simple, `Rule` decorator wrapped python class methods for `Rules`-inheriting classes.
61
+
62
+ <br/>
63
+
64
+ # Why CFN-Check?
65
+
66
+ AWS has its own tools for validating Cloud Formation - `cfn-lint` and `cfn-guard`. `cfn-check` aims to solve
67
+ problems inherint to `cfn-lint` more than `cfn-guard`, primarily:
68
+
69
+ - Confusing, unclear syntax around rules configuration
70
+ - Inability to parse non-resource wildcards
71
+ - Inability to validate non-resource template data
72
+ - Inabillity to use structured models to validate input
73
+
74
+ In comparison to `cfn-guard`, `cfn-check` is pure Python, thus
75
+ avoiding YADSL (Yet Another DSL) headaches. It also proves
76
+ significantly more configurable/modular/hackable as a result.
77
+
78
+ CFN-Check uses a combination of simple depth-first-search tree
79
+ parsing, friendly `cfn-lint` like query syntax, `Pydantic` models,
80
+ and `pytest`-like assert-driven checks to make validating your
81
+ Cloud Formation easy while offering both CLI and Python API interfaces.
82
+
83
+ <br/>
84
+
85
+ # Getting Started
86
+
87
+ `cfn-check` requires:
88
+
89
+ - `Python 3.12`
90
+ - Any number of valid CloudFormation templates or a path to said templates.
91
+ - A `.py` file containing at least one `Rules` class with at least one valid `@Rule()` decorated method
92
+
93
+ To get started (we recommend using `uv`), run:
94
+
95
+ ```bash
96
+ uv venv
97
+ source .venv/bin/activate
98
+
99
+ uv pip install cfn-check
100
+
101
+ touch rules.py
102
+ touch template.yaml
103
+ ```
104
+
105
+ Next open the `rules.py` file and create a basic Python class
106
+ as below.
107
+
108
+ ```python
109
+ from cfn_check import Rules, Rule
110
+
111
+
112
+ class ValidateResourceType(Rules):
113
+
114
+ @Rule(
115
+ "Resources::*::Type",
116
+ "It checks Resource::Type is correctly definined",
117
+ )
118
+ def validate_test(self, value: str):
119
+ assert value is not None, '❌ Resource Type not defined'
120
+ assert isinstance(value, str), '❌ Resource Type not a string'
121
+ ```
122
+
123
+ This provides us a basic rule set that validates that the `Type` field of our CloudFormation template(s) exists and is the correct data type.
124
+
125
+ > [!NOTE]
126
+ > Don't worry about adding an `__init__()` method to this class!
127
+
128
+ Next open the `template.yaml` file and paste the following CloudFormation:
129
+
130
+ ```yaml
131
+ AWSTemplateFormatVersion: '2010-09-09'
132
+ Parameters:
133
+ ExistingSecurityGroups:
134
+ Type: List<AWS::EC2::SecurityGroup::Id>
135
+ ExistingVPC:
136
+ Type: AWS::EC2::VPC::Id
137
+ Description: The VPC ID that includes the security groups in the ExistingSecurityGroups parameter.
138
+ InstanceType:
139
+ Type: String
140
+ Default: t2.micro
141
+ AllowedValues:
142
+ - t2.micro
143
+ - m1.small
144
+ Mappings:
145
+ AWSInstanceType2Arch:
146
+ t2.micro:
147
+ Arch: HVM64
148
+ m1.small:
149
+ Arch: HVM64
150
+ AWSRegionArch2AMI:
151
+ us-east-1:
152
+ HVM64: ami-0ff8a91507f77f867
153
+ HVMG2: ami-0a584ac55a7631c0c
154
+ Resources:
155
+ SecurityGroup:
156
+ Type: AWS::EC2::SecurityGroup
157
+ Properties:
158
+ GroupDescription: Allow HTTP traffic to the host
159
+ VpcId: !Ref ExistingVPC
160
+ SecurityGroupIngress:
161
+ - IpProtocol: tcp
162
+ FromPort: 80
163
+ ToPort: 80
164
+ CidrIp: 0.0.0.0/0
165
+ SecurityGroupEgress:
166
+ - IpProtocol: tcp
167
+ FromPort: 80
168
+ ToPort: 80
169
+ CidrIp: 0.0.0.0/0
170
+ AllSecurityGroups:
171
+ Type: Custom::Split
172
+ Properties:
173
+ ServiceToken: !GetAtt AppendItemToListFunction.Arn
174
+ List: !Ref ExistingSecurityGroups
175
+ AppendedItem: !Ref SecurityGroup
176
+ AppendItemToListFunction:
177
+ Type: AWS::Lambda::Function
178
+ Properties:
179
+ Handler: index.handler
180
+ Role: !GetAtt LambdaExecutionRole.Arn
181
+ Code:
182
+ ZipFile: !Join
183
+ - ''
184
+ - - var response = require('cfn-response');
185
+ - exports.handler = function(event, context) {
186
+ - ' var responseData = {Value: event.ResourceProperties.List};'
187
+ - ' responseData.Value.push(event.ResourceProperties.AppendedItem);'
188
+ - ' response.send(event, context, response.SUCCESS, responseData);'
189
+ - '};'
190
+ Runtime: nodejs20.x
191
+ MyEC2Instance:
192
+ Type: AWS::EC2::Instance
193
+ Properties:
194
+ ImageId: !FindInMap
195
+ - AWSRegionArch2AMI
196
+ - !Ref AWS::Region
197
+ - !FindInMap
198
+ - AWSInstanceType2Arch
199
+ - !Ref InstanceType
200
+ - Arch
201
+ SecurityGroupIds: !GetAtt AllSecurityGroups.Value
202
+ InstanceType: !Ref InstanceType
203
+ LambdaExecutionRole:
204
+ Type: AWS::IAM::Role
205
+ Properties:
206
+ AssumeRolePolicyDocument:
207
+ Version: '2012-10-17'
208
+ Statement:
209
+ - Effect: Allow
210
+ Principal:
211
+ Service:
212
+ - lambda.amazonaws.com
213
+ Action:
214
+ - sts:AssumeRole
215
+ Path: /
216
+ Policies:
217
+ - PolicyName: root
218
+ PolicyDocument:
219
+ Version: '2012-10-17'
220
+ Statement:
221
+ - Effect: Allow
222
+ Action:
223
+ - logs:*
224
+ Resource: arn:aws:logs:*:*:*
225
+ Outputs:
226
+ AllSecurityGroups:
227
+ Description: Security Groups that are associated with the EC2 instance
228
+ Value: !Join
229
+ - ', '
230
+ - !GetAtt AllSecurityGroups.Value
231
+ ```
232
+
233
+ This represents a basic configuration for an AWS Lambda function.
234
+
235
+ Finally, run:
236
+
237
+ ```bash
238
+ cfn-lint validate -r rules.py template.yaml
239
+ ```
240
+
241
+ which outputs:
242
+
243
+ ```
244
+ 2025-09-17T01:46:41.542078+00:00 - INFO - 19783474 - /Users/adalundhe/Documents/Duckbill/cfn-check/cfn_check/cli/validate.py:validate.80 - ✅ 1 validations met for 1 templates
245
+ ```
246
+
247
+ Congrats! You've just made the cloud a bit better place!
@@ -0,0 +1,30 @@
1
+ cfn_check/__init__.py,sha256=k1-xUfaN07znTID-I4uJcP3l1cWG3Fr-hPFoB5NVKHk,76
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=qUQ-4BFZAfqCh2lc12dkR6EwimXmRJCfw_0cLvOzkK8,1987
5
+ cfn_check/cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ cfn_check/cli/utils/attributes.py,sha256=zKOZhyWYsU1qcBUqQg8ppbHqrnwPNEcj2FI0eP29gzY,319
7
+ cfn_check/cli/utils/files.py,sha256=W2lf42qAlTOJ-YJeBWsK99FjnsxZtmit241cLyURg2A,1448
8
+ cfn_check/evaluation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ cfn_check/evaluation/check.py,sha256=5GgoZY0YfyOeOP92VJVH4Y3AYB7jUGjxwsr0qsbVGeo,414
10
+ cfn_check/evaluation/errors.py,sha256=yPJdtRYo67le4yMC9sYqcboCnkqKsJ3KPbSPFY2-Pi8,773
11
+ cfn_check/evaluation/search.py,sha256=WCi4mvvrRQyI3k05ohsFiIM8lKe5K6f3y-YsItu7rK0,2959
12
+ cfn_check/evaluation/validate.py,sha256=sLqtwVqdybCoE9zXfg_u6To22yJSvczxXAu1EKnTNs4,1187
13
+ cfn_check/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ cfn_check/loader/loader.py,sha256=7yiDLLW_vNp_8O47erLXjQQtAB47fU3nimb91N5N_R8,532
15
+ cfn_check/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ cfn_check/logging/models.py,sha256=-tBaK6p8mJ0cO8h2keEJ-VmtFX_VW4XzwAw2PtqbkF0,490
17
+ cfn_check/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ cfn_check/rules/rule.py,sha256=r6-eKCUdPaNIena2NDv3J_QlcWes1KLObI9ukRZZz1o,500
19
+ cfn_check/rules/rules.py,sha256=d7VvF6nlXER1puY9lEbQjFQv7RJMmD-wF4RoDCm8mQ0,22
20
+ cfn_check/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ cfn_check/shared/types.py,sha256=-om3DyZsjK_tJd-I8SITkoE55W0nB2WA3LOc87Cs7xI,414
22
+ cfn_check/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ cfn_check/validation/validator.py,sha256=FGPeb8Uc8lvX3Y5rs-fxeJKIOqzUXwXh_gCFcy6d3b0,1182
24
+ cfn_check-0.1.1.dist-info/licenses/LICENSE,sha256=EbCpGNzOkyQ53ig7J2Iwgmy4Og0dgHe8COo3WylhIKk,1069
25
+ example/rules.py,sha256=gEAX52iK76XBdfiilqfqnxh6j8rACp7v7dwbb_EQ-1o,359
26
+ cfn_check-0.1.1.dist-info/METADATA,sha256=26wu7hNXHtzg6hAZer8_lNcNFRM9mJ_R2Ji1JkW-jd4,8447
27
+ cfn_check-0.1.1.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
28
+ cfn_check-0.1.1.dist-info/entry_points.txt,sha256=B4lCHoDHmwisABxKgRLShwqqFv7QwwDAFXoAChOnkwg,53
29
+ cfn_check-0.1.1.dist-info/top_level.txt,sha256=hUn9Ya50yY1fpgWxEhG5iMgfMDDVX7qWQnM1xrgZnhM,18
30
+ cfn_check-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cfn-check = cfn_check.cli.root:run
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ada Lündhé
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ cfn_check
2
+ example
example/rules.py ADDED
@@ -0,0 +1,12 @@
1
+ from cfn_check import Rules, Rule
2
+
3
+
4
+ class ValidateResourceTypes(Rules):
5
+
6
+ @Rule(
7
+ "Resources::*::Type",
8
+ "It checks Resource::Type is correctly definined",
9
+ )
10
+ def validate_test(self, value: str):
11
+ assert value is not None, '❌ Resource Type not defined'
12
+ assert isinstance(value, str), '❌ Resource Type not a string'