forked from aws-cloudformation/cfn-lint
-
Notifications
You must be signed in to change notification settings - Fork 2
/
HardCodedArnProperties.py
134 lines (118 loc) · 5.34 KB
/
HardCodedArnProperties.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
import regex as re
from cfnlint._typing import RuleMatches
from cfnlint.rules import CloudFormationLintRule, RuleMatch
from cfnlint.template import Template
class HardCodedArnProperties(CloudFormationLintRule):
"""
Checks Resources if ARNs use correctly placed Pseudo Parameters
instead of hardcoded Partition, Region, and Account Number
"""
id = "I3042"
shortdesc = "ARNs should use correctly placed Pseudo Parameters"
description = (
"Checks Resources if ARNs use correctly placed Pseudo Parameters instead of"
" hardcoded Partition, Region, and Account Number"
)
source_url = ""
tags = ["resources"]
# using \r\n inside ${ } because there can be spaces in the sub parameter naming
# using \s for matching outside of Sub parameters as no space will work
regex = re.compile(
r"arn:(\$\{[^:\r\n]*::[^:\r\n]*}|[^:\s]*):[^:\s]+:(\$\{[^:\r\n]*::[^:\r\n]*}|[^:\s]*):(\$\{[^:\r\n]*::[^:\r\n]*}|[^:\s]*)"
)
def __init__(self):
"""Init"""
super().__init__()
self.config_definition = {
"partition": {
"default": True,
"type": "boolean",
},
"region": {
"default": False,
"type": "boolean",
},
"accountId": {
"default": False,
"type": "boolean",
},
}
self.configure()
def _match_values(self, cfnelem, path):
"""Recursively search for values matching the searchRegex"""
values = []
if isinstance(cfnelem, dict):
for key in cfnelem:
pathprop = path[:]
pathprop.append(key)
values.extend(self._match_values(cfnelem[key], pathprop))
elif isinstance(cfnelem, list):
for index, item in enumerate(cfnelem):
pathprop = path[:]
pathprop.append(index)
values.extend(self._match_values(item, pathprop))
else:
# Leaf node
if isinstance(cfnelem, str): # and re.match(searchRegex, cfnelem):
for variable in re.findall(self.regex, cfnelem):
if "Fn::Sub" in path:
values.append(path + [variable])
return values
def match_values(self, cfn):
"""
Search for values in all parts of the templates that match the searchRegex
"""
results = []
results.extend(self._match_values(cfn.template.get("Resources", {}), []))
# Globals are removed during a transform. They need to be checked manually
results.extend(self._match_values(cfn.template.get("Globals", {}), []))
return results
def match(self, cfn: Template) -> RuleMatches:
matches: RuleMatches = []
transforms = cfn.transform_pre["Transform"]
transforms = transforms if isinstance(transforms, list) else [transforms]
if "AWS::Serverless-2016-10-31" in cfn.transform_pre["Transform"]:
return matches
# Get a list of paths to every leaf node string containing at least one ${parameter}
parameter_string_paths = self.match_values(cfn)
# We want to search all of the paths to check if each one contains an 'Fn::Sub'
for parameter_string_path in parameter_string_paths:
path = ["Resources"] + parameter_string_path[:-1]
candidate = parameter_string_path[-1]
# ruff: noqa: E501
# !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# is valid even with aws as the account #. This handles empty string
if self.config["partition"] and not re.match(
r"^\$\{\w+}|\$\{AWS::Partition}|$", candidate[0]
):
# or not re.match(r'^(\$\{\w+}|\$\{AWS::Region}|)$',candidate[1])
# or not re.match(r'^\$\{\w+}|\$\{AWS::AccountId}|aws|$', candidate[2]):
message = (
"ARN in Resource {0} contains hardcoded Partition in ARN or"
" incorrectly placed Pseudo Parameters"
)
matches.append(RuleMatch(path, message.format(path[1])))
if self.config["region"] and not re.match(
r"^(\$\{\w+}|\$\{AWS::Region}|)$", candidate[1]
):
# or or not re.match(r'^\$\{\w+}|\$\{AWS::AccountId}|aws|$', candidate[2]):
message = (
"ARN in Resource {0} contains hardcoded Region in ARN or"
" incorrectly placed Pseudo Parameters"
)
matches.append(RuleMatch(path, message.format(path[1])))
# Lambda is added for authorizer's Uniform Resource Identifier (URI)
# https://github.com/aws-cloudformation/cfn-lint/issues/3716
if self.config["accountId"] and not re.match(
r"^\$\{\w+}|\$\{AWS::AccountId}|aws|lambda|$", candidate[2]
):
message = (
"ARN in Resource {0} contains hardcoded AccountId in ARN or"
" incorrectly placed Pseudo Parameters"
)
matches.append(RuleMatch(path, message.format(path[1])))
return matches