forked from aws-cloudformation/cfn-lint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
SubNeeded.py
124 lines (105 loc) · 5.44 KB
/
SubNeeded.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
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
from functools import reduce # pylint: disable=redefined-builtin
import re
import copy
import six
from cfnlint.rules import CloudFormationLintRule
from cfnlint.rules import RuleMatch
class SubNeeded(CloudFormationLintRule):
"""Check if a substitution string exists without a substitution function"""
id = 'E1029'
shortdesc = 'Sub is required if a variable is used in a string'
description = 'If a substitution variable exists in a string but isn\'t wrapped with the Fn::Sub function the deployment will fail.'
source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'
tags = ['functions', 'sub']
exceptions = ['TemplateBody']
def __init__(self):
"""Init"""
super(SubNeeded, self).__init__()
self.config_definition = {
'custom_excludes': {
'default': '',
'type': 'string'
}
}
self.configure()
self.subParameterRegex = re.compile(r'(\$\{[A-Za-z0-9_:\.]+\})')
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, six.string_types): # and re.match(searchRegex, cfnelem):
for variable in re.findall(self.subParameterRegex, cfnelem):
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, []))
# Globals are removed during a transform. They need to be checked manually
results.extend(self._match_values(cfn.template.get('Globals', {}), []))
return results
def _api_exceptions(self, value):
""" Key value exceptions """
parameter_search = re.compile(r'^\$\{stageVariables\..*\}$')
return re.match(parameter_search, value)
def _variable_custom_excluded(self, value):
""" User-defined exceptions for variables, anywhere in the file """
custom_excludes = self.config['custom_excludes']
if custom_excludes:
custom_search = re.compile(custom_excludes)
return re.match(custom_search, value)
return False
def match(self, cfn):
matches = []
refs = cfn.get_valid_refs()
getatts = cfn.get_valid_getatts()
# 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:
# Get variable
var = parameter_string_path[-1]
# Step Function State Machine has a Definition Substitution that allows usage of special variables outside of a !Sub
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definitionsubstitutions.html
if 'DefinitionString' in parameter_string_path:
modified_parameter_string_path = copy.copy(parameter_string_path)
index = parameter_string_path.index('DefinitionString')
modified_parameter_string_path[index] = 'DefinitionSubstitutions'
modified_parameter_string_path = modified_parameter_string_path[:index+1]
modified_parameter_string_path.append(var[2:-1])
if reduce(lambda c, k: c.get(k, {}), modified_parameter_string_path, cfn.template):
continue
# Exclude variables that match custom exclude filters, if configured
# (for third-party tools that pre-process templates before uploading them to AWS)
if self._variable_custom_excluded(var):
continue
# Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)
if var.startswith('${!'):
continue
var_stripped = var[2:-1].strip()
# If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly
if not 'Fn::Sub' in parameter_string_path and parameter_string_path[-2] not in self.exceptions:
if (var_stripped in refs or var_stripped in getatts) or 'DefinitionString' in parameter_string_path:
# Remove the last item (the variable) to prevent multiple errors on 1 line errors
path = parameter_string_path[:-1]
message = 'Found an embedded parameter "{}" outside of an "Fn::Sub" at {}'.format(
var, '/'.join(map(str, path)))
matches.append(RuleMatch(path, message))
return matches