Skip to content

Commit 519506c

Browse files
kftsehkkddejong
andauthored
implement custom rule IS DEFINED (#2656)
Co-authored-by: Kevin DeJong <[email protected]>
1 parent 9727e51 commit 519506c

File tree

10 files changed

+291
-4
lines changed

10 files changed

+291
-4
lines changed

docs/custom_rules.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,16 @@ The specified operator to be used for this rule. The supported values are define
4545
| NOT_IN | Checks the specified property is not equal to or not contained by the array value |
4646
| \>= | Checks the specified property is greater than or equal to the value given |
4747
| <= | Checks the specified property is less than or equal to the value given |
48+
| IS | Checks the specified property is defined or not defined, the value must be one of DEFINED or NOT_DEFINED |
4849

4950
#### Value
5051

5152
The value which the operator is comparing against (e.g `CompareMe`).
5253

5354
Multi-word inputs are accepted (e.g `Compare Me`). Array inputs are also accepted for set operations (e.g `[Apples, Oranges, Pears]`).
5455

56+
For operator `IS`, the value must be one of `DEFINED` or `NOT_DEFINED`.
57+
5558
#### Error Level (Optional)
5659

5760
To specify the error level any breach of this rule is categorized. The supported values include all existing error levels (e.g `ERROR` or `WARN`)
@@ -71,6 +74,12 @@ This rule validates all EC2 instances in a template aren’t using the instance
7174
AWS::EC2::Instance InstanceType != "p3.2xlarge"
7275
```
7376

77+
This rule specify all lambda function in a template must specify environment variable `NODE_ENV`.
78+
79+
```
80+
AWS::Lambda::Function Environment.Variables.NODE_ENV IS DEFINED
81+
```
82+
7483
To include this rules, include your custom rules text file using the `-z custom_rules.txt` argument when running cfn-lint.
7584

7685

src/cfnlint/rules/custom/Operators.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@
22
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
SPDX-License-Identifier: MIT-0
44
"""
5+
56
# pylint: disable=cyclic-import
67
import cfnlint.rules
78

8-
OPERATOR = ["EQUALS", "NOT_EQUALS", "==", "!=", "IN", "NOT_IN", ">=", "<="]
9+
OPERATOR = [
10+
"EQUALS",
11+
"NOT_EQUALS",
12+
"==",
13+
"!=",
14+
"IN",
15+
"NOT_IN",
16+
">=",
17+
"<=",
18+
"IS DEFINED",
19+
"IS NOT_DEFINED",
20+
]
921

1022

1123
def CreateCustomRule(
@@ -81,6 +93,109 @@ def match_resource_properties(self, properties, _, path, cfn):
8193
)
8294

8395

96+
def CreateCustomIsDefinedRule(rule_id, resourceType, prop, value, error_message):
97+
class CustomIsDefinedRule(cfnlint.rules.CloudFormationLintRule):
98+
def __init__(
99+
self,
100+
rule_id,
101+
resourceType,
102+
prop,
103+
value,
104+
error_message,
105+
description,
106+
shortdesc,
107+
):
108+
super().__init__()
109+
self.id = rule_id
110+
self.resource_property_types.append(resourceType)
111+
self.property_chain = prop.split(".")
112+
if value == "DEFINED":
113+
self.is_defined = True
114+
elif value == "NOT_DEFINED":
115+
self.is_defined = False
116+
else:
117+
raise ValueError("IS must follow either DEFINED or NOT_DEFINED")
118+
self.error_message = error_message
119+
self.description = description
120+
self.shortdesc = shortdesc
121+
122+
def _split_inset_properties(self, property_chain):
123+
if property_chain:
124+
if len(property_chain) > 1:
125+
return property_chain[0], property_chain[1:]
126+
return property_chain[0], []
127+
128+
return None, []
129+
130+
def _check_value(self, value, path, property_chain, cfn):
131+
matches = []
132+
child_property, new_property_chain = self._split_inset_properties(
133+
property_chain
134+
)
135+
if self.is_defined and (
136+
value is None or value.get(child_property, None) is None
137+
):
138+
matches.append(
139+
cfnlint.rules.RuleMatch(
140+
path, error_message or f"{path} must be defined"
141+
)
142+
)
143+
if child_property is not None:
144+
matches.extend(
145+
cfn.check_value(
146+
value,
147+
child_property,
148+
path,
149+
check_value=self._check_value,
150+
property_chain=new_property_chain,
151+
cfn=cfn,
152+
)
153+
)
154+
return matches
155+
if not self.is_defined and value is not None:
156+
matches.append(
157+
cfnlint.rules.RuleMatch(
158+
path, error_message or f"{path} must not be defined"
159+
)
160+
)
161+
return matches
162+
163+
def match_resource_properties(self, properties, _, path, cfn):
164+
child_property, new_property_chain = self._split_inset_properties(
165+
self.property_chain
166+
)
167+
matches = []
168+
# here does nothing when the value is not defined, this is checked separately below
169+
matches.extend(
170+
cfn.check_value(
171+
properties,
172+
child_property,
173+
path,
174+
check_value=self._check_value,
175+
property_chain=new_property_chain,
176+
cfn=cfn,
177+
)
178+
)
179+
# check child exists separately when checking is_defined
180+
if self.is_defined and properties.get(child_property, None) is None:
181+
matches.append(
182+
cfnlint.rules.RuleMatch(
183+
path, error_message or f"{path} must be defined"
184+
)
185+
)
186+
return matches
187+
188+
return CustomIsDefinedRule(
189+
rule_id,
190+
resourceType,
191+
prop,
192+
value,
193+
error_message,
194+
shortdesc=f"Custom rule to check for value is {value}",
195+
description=f"Created from the custom rules parameter. This rule will check if a property value is {value}",
196+
)
197+
198+
84199
def CreateEqualsRule(rule_id, resourceType, prop, value, error_message):
85200
def rule_func(value, expected_value, path):
86201
matches = []

src/cfnlint/rules/custom/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ def process_sets(raw_value):
8888
return cfnlint.rules.custom.Operators.CreateLesserRule(
8989
error_level + str(rule_id), resourceType, prop, value, error_message
9090
)
91+
if operator == "IS":
92+
if value in ["DEFINED", "NOT_DEFINED"]:
93+
return cfnlint.rules.custom.Operators.CreateCustomIsDefinedRule(
94+
error_level + str(rule_id), resourceType, prop, value, error_message
95+
)
96+
return cfnlint.rules.custom.Operators.CreateInvalidRule(
97+
"E" + str(rule_id), f"{operator} {value}"
98+
)
9199

92100
return cfnlint.rules.custom.Operators.CreateInvalidRule(
93101
"E" + str(rule_id), operator

test/fixtures/custom_rules/good/custom_rule_perfect.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ AWS::IAM::Role AssumeRolePolicyDocument.Version NOT_IN [2012-10-16,2012-11-20,20
55
AWS::IAM::Policy PolicyName EQUALS "root" WARN ABC
66
AWS::IAM::Policy PolicyName IN [2012-10-16,root,2012-10-18] ERROR ABC
77
AWS::IAM::Policy PolicyName NOT_EQUALS "user" WARN ABC
8-
AWS::IAM::Policy PolicyName NOT_IN [2012-10-16,2012-11-20,2012-10-18] ERROR ABC
8+
AWS::IAM::Policy PolicyName NOT_IN [2012-10-16,2012-11-20,2012-10-18] ERROR ABC
9+
AWS::Lambda::Function Environment.Variables.NODE_ENV IS DEFINED
10+
AWS::Lambda::Function Environment.Variables.PRIVATE_KEY IS NOT_DEFINED
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
AWSTemplateFormatVersion: "2010-09-09"
3+
Description: >
4+
Testing for IS DEFINED rule
5+
Resources:
6+
LambdaExecutionRole:
7+
Type: AWS::IAM::Role
8+
Properties:
9+
AssumeRolePolicyDocument:
10+
Version: "2012-10-17"
11+
Statement:
12+
- Effect: Allow
13+
Principal:
14+
Service:
15+
- lambda.amazonaws.com
16+
- ec2.amazonaws.com
17+
Action:
18+
- sts:AssumeRole
19+
Path: "/"
20+
LambdaFunctionTestDefined:
21+
Type: AWS::Lambda::Function
22+
Properties:
23+
Handler: index.handler
24+
Role: !GetAtt LambdaExecutionRole.Arn
25+
Environment:
26+
Variables:
27+
NODE_ENV: ""
28+
Code: ./
29+
Runtime: nodejs18.x
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
AWSTemplateFormatVersion: "2010-09-09"
3+
Description: >
4+
Testing for IS NOT_DEFINED rule
5+
Resources:
6+
LambdaExecutionRole:
7+
Type: AWS::IAM::Role
8+
Properties:
9+
AssumeRolePolicyDocument:
10+
Version: "2012-10-17"
11+
Statement:
12+
- Effect: Allow
13+
Principal:
14+
Service:
15+
- lambda.amazonaws.com
16+
- ec2.amazonaws.com
17+
Action:
18+
- sts:AssumeRole
19+
Path: "/"
20+
LambdaFunctionTestNotDefined1:
21+
Type: AWS::Lambda::Function
22+
Properties:
23+
Handler: index.handler
24+
Role: !GetAtt LambdaExecutionRole.Arn
25+
Code: ./
26+
Runtime: nodejs18.x
27+
LambdaFunctionTestNotDefined2:
28+
Type: AWS::Lambda::Function
29+
Properties:
30+
Handler: index.handler
31+
Role: !GetAtt LambdaExecutionRole.Arn
32+
Environment:
33+
Variables:
34+
OTHER_VARS: ""
35+
Code: ./
36+
Runtime: nodejs18.x

test/fixtures/templates/good/generic.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,21 @@ Resources:
157157
ResourceSignal:
158158
Timeout: PT15M
159159
Count: 1
160+
LambdaFunction:
161+
Type: AWS::Lambda::Function
162+
Properties:
163+
Code:
164+
ZipFile: |
165+
exports.handler = function(event, context) {
166+
console.log('Hello World');
167+
context.done(null, 'Hello World');
168+
};
169+
Environment:
170+
Variables:
171+
NODE_ENV: !Ref pEnvironment
172+
Handler: index.handler
173+
Role: "arn:aws:iam::123456789012:role/lambda_basic_execution"
174+
Runtime: nodejs18.x
160175
Outputs:
161176
ElasticIP:
162177
Value: !Sub "${ElasticIP}/32"

test/unit/module/test_template.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def test_build_graph(self):
4949
IamPipeline [color=black, label="IamPipeline\\n<AWS::CloudFormation::Stack>", shape=ellipse, type=Resource];
5050
CustomResource [color=black, label="CustomResource\\n<Custom::Function>", shape=ellipse, type=Resource];
5151
WaitCondition [color=black, label="WaitCondition\\n<AWS::CloudFormation::WaitCondition>", shape=ellipse, type=Resource];
52+
LambdaFunction [color=black, label="LambdaFunction\\n<AWS::Lambda::Function>", shape=ellipse, type=Resource];
5253
RolePolicies -> RootRole [color=black, key=0, label=Ref, source_paths="['Properties', 'Roles', 0]"];
5354
RootInstanceProfile -> RootRole [color=black, key=0, label=Ref, source_paths="['Properties', 'Roles', 0]"];
5455
MyEC2Instance -> RootInstanceProfile [color=black, key=0, label=Ref, source_paths="['Properties', 'IamInstanceProfile']"];
@@ -71,7 +72,7 @@ def test_build_graph(self):
7172

7273
def test_get_resources_success(self):
7374
"""Test Success on Get Resources"""
74-
valid_resource_count = 12
75+
valid_resource_count = 13
7576
resources = self.template.get_resources()
7677
assert (
7778
len(resources) == valid_resource_count
@@ -121,7 +122,7 @@ def test_get_parameter_names(self):
121122

122123
def test_get_valid_refs(self):
123124
"""Get Valid REFs"""
124-
valid_ref_count = 27
125+
valid_ref_count = 28
125126
refs = self.template.get_valid_refs()
126127
assert len(refs) == valid_ref_count, "Expected {} refs, got {}".format(
127128
valid_ref_count, len(refs)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
from test.unit.rules import BaseRuleTestCase
6+
7+
from cfnlint.rules.custom.Operators import CreateCustomIsDefinedRule
8+
9+
10+
class TestIsDefinedRule(BaseRuleTestCase):
11+
"""Test template mapping configurations"""
12+
13+
def setUp(self):
14+
"""Setup"""
15+
super(TestIsDefinedRule, self).setUp()
16+
self.collection.register(
17+
CreateCustomIsDefinedRule(
18+
"E9001",
19+
"AWS::Lambda::Function",
20+
"Environment.Variables.NODE_ENV",
21+
"DEFINED",
22+
"NODE_ENV should be defined",
23+
)
24+
)
25+
26+
success_templates = ["test/fixtures/templates/good/custom/is-defined.yaml"]
27+
28+
def test_file_positive(self):
29+
"""Test Positive"""
30+
self.helper_file_positive()
31+
32+
def test_file_negative(self):
33+
"""Test failure"""
34+
self.helper_file_negative(
35+
"test/fixtures/templates/good/custom/is-not-defined.yaml", 2
36+
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
from test.unit.rules import BaseRuleTestCase
6+
7+
from cfnlint.rules.custom.Operators import CreateCustomIsDefinedRule
8+
9+
10+
class TestIsDefinedRule(BaseRuleTestCase):
11+
"""Test template mapping configurations"""
12+
13+
def setUp(self):
14+
"""Setup"""
15+
super(TestIsDefinedRule, self).setUp()
16+
self.collection.register(
17+
CreateCustomIsDefinedRule(
18+
"E9001",
19+
"AWS::Lambda::Function",
20+
"Environment.Variables.NODE_ENV",
21+
"NOT_DEFINED",
22+
"NODE_ENV should not be defined",
23+
)
24+
)
25+
26+
success_templates = ["test/fixtures/templates/good/custom/is-not-defined.yaml"]
27+
28+
def test_file_positive(self):
29+
"""Test Positive"""
30+
self.helper_file_positive()
31+
32+
def test_file_negative(self):
33+
"""Test failure"""
34+
self.helper_file_negative(
35+
"test/fixtures/templates/good/custom/is-defined.yaml", 1
36+
)

0 commit comments

Comments
 (0)