forked from aws-cloudformation/cfn-lint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathRecordSet.py
266 lines (227 loc) · 11.3 KB
/
RecordSet.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
import re
import six
from cfnlint.rules import CloudFormationLintRule
from cfnlint.rules import RuleMatch
from cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC
class RecordSet(CloudFormationLintRule):
"""Check Route53 Recordset Configuration"""
id = 'E3020'
shortdesc = 'Validate Route53 RecordSets'
description = 'Check if all RecordSets are correctly configured'
source_url = 'https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html'
tags = ['resources', 'route53', 'record_set']
# Regex generated from https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html
REGEX_DOMAINNAME = re.compile(
r'^[a-zA-Z0-9\!\"\#\$\%\&\'\(\)\*\+\,-\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~\.]+$')
REGEX_TXT = re.compile(r'^("[^"]{1,255}" *)*"[^"]{1,255}"$')
REGEX_CNAME_VALIDATIONS = re.compile(r'^.*\.acm-validations\.aws\.?$')
def count_c_names(self, records, path, cfn):
""" Count C Names """
matches = []
scenarios = cfn.get_object_without_nested_conditions(records, path)
for scenario in scenarios:
if len(scenario.get('Object')) > 1:
scenario = scenario.get('Scenario')
message = 'A CNAME recordset can only contain 1 value'
if scenario is None:
message = 'A CNAME recordset can only contain 1 value'
matches.append(
RuleMatch(path, message.format('/'.join(map(str, message)))))
else:
message = 'A CNAME recordset can only contain 1 value {0} at {1}'
scenario_text = ' and '.join(
['when condition "%s" is %s' % (k, v) for (k, v) in scenario.items()])
matches.append(
RuleMatch(path, message.format(scenario_text, '/'.join(map(str, path)))))
return matches
def check_record(self, value, path, record_type, regex, regex_name):
matches = []
if isinstance(value, six.string_types):
if not re.match(regex, value):
message = record_type + ' record ({}) is not a valid ' + regex_name
matches.append(RuleMatch(path, message.format(value)))
return matches
def check_a_record(self, value, path):
return self.check_record(value, path, 'A', REGEX_IPV4, 'IPv4 address')
def check_aaaa_record(self, value, path):
return self.check_record(value, path, 'AAAA', REGEX_IPV6, 'IPv6 address')
def check_caa_record(self, value, path):
"""Check CAA record Configuration"""
matches = []
if isinstance(value, six.string_types):
# Split the record up to the mandatory settings (flags tag "value")
items = value.split(' ', 2)
# Check if the 3 settings are given.
if len(items) != 3:
message = 'CAA record must contain 3 settings (flags tag "value"), record contains {} settings.'
matches.append(RuleMatch(path, message.format(len(items))))
else:
# Check the flag value
if not items[0].isdigit():
message = 'CAA record flag setting ({}) should be of type Integer.'
extra_args = {'actual_type': type(
items[0]).__name__, 'expected_type': int.__name__}
matches.append(RuleMatch(path, message.format(items[0]), **extra_args))
else:
if int(items[0]) not in [0, 128]:
message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.'
matches.append(RuleMatch(path, message.format(items[0])))
# Check the tag value
if not re.match(REGEX_ALPHANUMERIC, items[1]):
message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.'
matches.append(RuleMatch(path, message.format(items[1])))
# Check the value
if not items[2].startswith('"') or not items[2].endswith('"'):
message = 'CAA record value setting has to be enclosed in double quotation marks (").'
matches.append(RuleMatch(path, message))
return matches
def check_cname_record(self, value, path):
"""Check CNAME record Configuration"""
matches = []
if not isinstance(value, dict):
if (not re.match(self.REGEX_DOMAINNAME, value) and
not re.match(self.REGEX_CNAME_VALIDATIONS, value)):
# ACM Route 53 validation uses invalid CNAMEs starting with `_`,
# special-case them rather than complicate the regex.
message = 'CNAME record ({}) does not contain a valid domain name'
matches.append(RuleMatch(path, message.format(value)))
return matches
def check_mx_record(self, value, path):
"""Check MX record Configuration"""
matches = []
if isinstance(value, six.string_types):
# Split the record up to the mandatory settings (priority domainname)
items = value.split(' ')
# Check if the 3 settings are given.
if len(items) != 2:
message = 'MX record must contain 2 settings (priority domainname), record contains {} settings.'
matches.append(RuleMatch(path, message.format(len(items), value)))
else:
# Check the priority value
if not items[0].isdigit():
message = 'MX record priority setting ({}) should be of type Integer.'
extra_args = {'actual_type': type(
items[0]).__name__, 'expected_type': int.__name__}
matches.append(RuleMatch(path, message.format(items[0], value), **extra_args))
else:
if not 0 <= int(items[0]) <= 65535:
message = 'Invalid MX record priority setting ({}) given, must be between 0 and 65535.'
matches.append(RuleMatch(path, message.format(items[0], value)))
# Check the domainname value
if not re.match(self.REGEX_DOMAINNAME, items[1]):
matches.append(RuleMatch(path, message.format(items[1])))
return matches
def check_ns_record(self, value, path):
return self.check_record(value, path, 'NS', self.REGEX_DOMAINNAME, 'domain name')
def check_ptr_record(self, value, path):
return self.check_record(value, path, 'PTR', self.REGEX_DOMAINNAME, 'domain name')
def check_txt_record(self, value, path):
"""Check TXT record Configuration"""
matches = []
if not isinstance(value, dict) and not re.match(self.REGEX_TXT, value):
message = 'TXT record is not structured as one or more items up to 255 characters ' \
'enclosed in double quotation marks at {0}'
matches.append(RuleMatch(
path,
(
message.format('/'.join(map(str, path)))
),
))
return matches
def check_recordset(self, path, recordset, cfn):
"""Check record configuration"""
matches = []
recordset_type = recordset.get('Type')
# Skip Intrinsic functions
if not isinstance(recordset_type, dict):
if not recordset.get('AliasTarget'):
# If no Alias is specified, ResourceRecords has to be specified
if not recordset.get('ResourceRecords'):
return matches
# Record type specific checks
if recordset_type == 'A':
matches.extend(
cfn.check_value(
recordset, 'ResourceRecords', path[:],
check_value=self.check_a_record,
)
)
elif recordset_type == 'AAAA':
matches.extend(
cfn.check_value(
recordset, 'ResourceRecords', path[:],
check_value=self.check_aaaa_record,
)
)
elif recordset_type == 'CAA':
matches.extend(
cfn.check_value(
recordset, 'ResourceRecords', path[:],
check_value=self.check_caa_record,
)
)
elif recordset_type == 'CNAME':
matches.extend(
self.count_c_names(
recordset.get('ResourceRecords'), path[:] + ['ResourceRecords'], cfn
)
)
matches.extend(
cfn.check_value(
recordset, 'ResourceRecords', path[:],
check_value=self.check_cname_record,
)
)
elif recordset_type == 'MX':
matches.extend(
cfn.check_value(
recordset, 'ResourceRecords', path[:],
check_value=self.check_mx_record,
)
)
elif recordset_type == 'NS':
matches.extend(
cfn.check_value(
recordset, 'ResourceRecords', path[:],
check_value=self.check_ns_record,
)
)
elif recordset_type == 'PTR':
matches.extend(
cfn.check_value(
recordset, 'ResourceRecords', path[:],
check_value=self.check_ptr_record,
)
)
elif recordset_type == 'TXT':
matches.extend(
cfn.check_value(
recordset, 'ResourceRecords', path[:],
check_value=self.check_txt_record,
)
)
return matches
def match(self, cfn):
"""Check RecordSets and RecordSetGroups Properties"""
matches = []
recordsets = cfn.get_resources(['AWS::Route53::RecordSet'])
for name, recordset in recordsets.items():
path = ['Resources', name, 'Properties']
if isinstance(recordset, dict):
props = recordset.get('Properties')
if props:
matches.extend(self.check_recordset(path, props, cfn))
recordsetgroups = cfn.get_resource_properties(
['AWS::Route53::RecordSetGroup', 'RecordSets'])
for recordsetgroup in recordsetgroups:
path = recordsetgroup['Path']
value = recordsetgroup['Value']
if isinstance(value, list):
for index, recordset in enumerate(value):
tree = path[:] + [index]
matches.extend(self.check_recordset(tree, recordset, cfn))
return matches