We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
E3020 - ACM Validation - Linter error
*cfn-lint version: 0.11.1*

Hello!

So I have the below snippet of code:
```
  r7XXXZZb04efXXXYYYZZ2b8cc0b2aa1f132XXXYYYZZZCNAMERecordSet:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref rHostedZone
      Comment: domain CNAME record.
      Name: !Join ["", ["_75XXXTTT04efa8c2052b8ccXXXYYZZZ.XXXYYYZZZ.", !Ref pDomainName, "."]]
      Type: CNAME
      TTL: '300'
      ResourceRecords:
        - _14062XXXXYYY6b1f37eb361e9XXXYYYZZZ.acm-validations.aws
```

Unfortunately linter reports it as a E3020 error.

```
E3020 CNAME record (_14062XXXXYYY6b1f37eb361e9XXXYYYZZZ.acm-validations.aws) does not contain a valid domain name
dns/dns-production.template:645:7
```

Is that a proper behaviour? Resource is created properly - and the record works as it should - why then linter complains about it?


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/cfnlint/rules/resources/route53/RecordSet.py`
Content:
```
1 """
2   Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 
4   Permission is hereby granted, free of charge, to any person obtaining a copy of this
5   software and associated documentation files (the "Software"), to deal in the Software
6   without restriction, including without limitation the rights to use, copy, modify,
7   merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8   permit persons to whom the Software is furnished to do so.
9 
10   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11   INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12   PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 """
17 import re
18 from cfnlint import CloudFormationLintRule
19 from cfnlint import RuleMatch
20 from cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC
21 
22 
23 class RecordSet(CloudFormationLintRule):
24     """Check Route53 Recordset Configuration"""
25     id = 'E3020'
26     shortdesc = 'Validate Route53 RecordSets'
27     description = 'Check if all RecordSets are correctly configured'
28     source_url = 'https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html'
29     tags = ['resources', 'route53', 'record_set']
30 
31     REGEX_DOMAINNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(.)$')
32     REGEX_TXT = re.compile(r'^("[^"]{1,255}" *)*"[^"]{1,255}"$')
33 
34     def check_a_record(self, path, recordset):
35         """Check A record Configuration"""
36         matches = []
37 
38         resource_records = recordset.get('ResourceRecords')
39         for index, record in enumerate(resource_records):
40 
41             if not isinstance(record, dict):
42                 tree = path[:] + ['ResourceRecords', index]
43 
44                 # Check if a valid IPv4 address is specified
45                 if not re.match(REGEX_IPV4, record):
46                     message = 'A record ({}) is not a valid IPv4 address'
47                     matches.append(RuleMatch(tree, message.format(record)))
48 
49         return matches
50 
51     def check_aaaa_record(self, path, recordset):
52         """Check AAAA record Configuration"""
53         matches = []
54 
55         resource_records = recordset.get('ResourceRecords')
56         for index, record in enumerate(resource_records):
57 
58             if not isinstance(record, dict):
59                 tree = path[:] + ['ResourceRecords', index]
60 
61                 # Check if a valid IPv4 address is specified
62                 if not re.match(REGEX_IPV6, record):
63                     message = 'AAAA record ({}) is not a valid IPv6 address'
64                     matches.append(RuleMatch(tree, message.format(record)))
65 
66         return matches
67 
68     def check_caa_record(self, path, recordset):
69         """Check CAA record Configuration"""
70         matches = []
71 
72         resource_records = recordset.get('ResourceRecords')
73 
74         for index, record in enumerate(resource_records):
75             tree = path[:] + ['ResourceRecords', index]
76 
77             if not isinstance(record, dict):
78                 # Split the record up to the mandatory settings (flags tag "value")
79                 items = record.split(' ', 2)
80 
81                 # Check if the 3 settings are given.
82                 if len(items) != 3:
83                     message = 'CAA record must contain 3 settings (flags tag "value"), record contains {} settings.'
84                     matches.append(RuleMatch(tree, message.format(len(items))))
85                 else:
86                     # Check the flag value
87                     if not items[0].isdigit():
88                         message = 'CAA record flag setting ({}) should be of type Integer.'
89                         matches.append(RuleMatch(tree, message.format(items[0])))
90                     else:
91                         if int(items[0]) not in [0, 128]:
92                             message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.'
93                             matches.append(RuleMatch(tree, message.format(items[0])))
94 
95                     # Check the tag value
96                     if not re.match(REGEX_ALPHANUMERIC, items[1]):
97                         message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.'
98                         matches.append(RuleMatch(tree, message.format(items[0])))
99 
100                     # Check the value
101                     if not items[2].startswith('"') or not items[2].endswith('"'):
102                         message = 'CAA record value setting has to be enclosed in double quotation marks (").'
103                         matches.append(RuleMatch(tree, message))
104 
105         return matches
106 
107     def check_cname_record(self, path, recordset):
108         """Check CNAME record Configuration"""
109         matches = []
110 
111         resource_records = recordset.get('ResourceRecords')
112         if len(resource_records) > 1:
113             message = 'A CNAME recordset can only contain 1 value'
114             matches.append(RuleMatch(path + ['ResourceRecords'], message))
115         else:
116             for index, record in enumerate(resource_records):
117                 if not isinstance(record, dict):
118                     tree = path[:] + ['ResourceRecords', index]
119                     if (not re.match(self.REGEX_DOMAINNAME, record)
120                             # ACM Route 53 validation uses invalid CNAMEs starting with `_`,
121                             # special-case them rather than complicate the regex.
122                             and not record.endswith('.acm-validations.aws.')):
123                         message = 'CNAME record ({}) does not contain a valid domain name'
124                         matches.append(RuleMatch(tree, message.format(record)))
125 
126         return matches
127 
128     def check_mx_record(self, path, recordset):
129         """Check MX record Configuration"""
130         matches = []
131 
132         resource_records = recordset.get('ResourceRecords')
133 
134         for index, record in enumerate(resource_records):
135             tree = path[:] + ['ResourceRecords', index]
136 
137             if not isinstance(record, dict):
138                 # Split the record up to the mandatory settings (priority domainname)
139                 items = record.split(' ')
140 
141                 # Check if the 3 settings are given.
142                 if len(items) != 2:
143                     message = 'MX record must contain 2 settings (priority domainname), record contains {} settings.'
144                     matches.append(RuleMatch(tree, message.format(len(items), record)))
145                 else:
146                     # Check the priority value
147                     if not items[0].isdigit():
148                         message = 'MX record priority setting ({}) should be of type Integer.'
149                         matches.append(RuleMatch(tree, message.format(items[0], record)))
150                     else:
151                         if not 0 <= int(items[0]) <= 65535:
152                             message = 'Invalid MX record priority setting ({}) given, must be between 0 and 65535.'
153                             matches.append(RuleMatch(tree, message.format(items[0], record)))
154 
155                     # Check the domainname value
156                     if not re.match(self.REGEX_DOMAINNAME, items[1]):
157                         matches.append(RuleMatch(tree, message.format(items[1])))
158 
159         return matches
160 
161     def check_txt_record(self, path, recordset):
162         """Check TXT record Configuration"""
163         matches = []
164 
165         # Check quotation of the records
166         resource_records = recordset.get('ResourceRecords')
167 
168         for index, record in enumerate(resource_records):
169             tree = path[:] + ['ResourceRecords', index]
170 
171             if not isinstance(record, dict) and not re.match(self.REGEX_TXT, record):
172                 message = 'TXT record is not structured as one or more items up to 255 characters ' \
173                           'enclosed in double quotation marks at {0}'
174                 matches.append(RuleMatch(
175                     tree,
176                     (
177                         message.format('/'.join(map(str, tree)))
178                     ),
179                 ))
180 
181         return matches
182 
183     def check_recordset(self, path, recordset):
184         """Check record configuration"""
185 
186         matches = []
187         recordset_type = recordset.get('Type')
188 
189         # Skip Intrinsic functions
190         if not isinstance(recordset_type, dict):
191             if not recordset.get('AliasTarget'):
192                 # If no Alias is specified, ResourceRecords has to be specified
193                 if not recordset.get('ResourceRecords'):
194                     message = 'Property ResourceRecords missing at {}'
195                     matches.append(RuleMatch(path, message.format('/'.join(map(str, path)))))
196                 # Record type specific checks
197                 elif recordset_type == 'A':
198                     matches.extend(self.check_a_record(path, recordset))
199                 elif recordset_type == 'AAAA':
200                     matches.extend(self.check_aaaa_record(path, recordset))
201                 elif recordset_type == 'CAA':
202                     matches.extend(self.check_caa_record(path, recordset))
203                 elif recordset_type == 'CNAME':
204                     matches.extend(self.check_cname_record(path, recordset))
205                 elif recordset_type == 'MX':
206                     matches.extend(self.check_mx_record(path, recordset))
207                 elif recordset_type == 'TXT':
208                     matches.extend(self.check_txt_record(path, recordset))
209 
210         return matches
211 
212     def match(self, cfn):
213         """Check RecordSets and RecordSetGroups Properties"""
214 
215         matches = []
216 
217         recordsets = cfn.get_resources(['AWS::Route53::RecordSet'])
218 
219         for name, recordset in recordsets.items():
220             path = ['Resources', name, 'Properties']
221 
222             if isinstance(recordset, dict):
223                 props = recordset.get('Properties')
224                 if props:
225                     matches.extend(self.check_recordset(path, props))
226 
227         recordsetgroups = cfn.get_resource_properties(['AWS::Route53::RecordSetGroup', 'RecordSets'])
228 
229         for recordsetgroup in recordsetgroups:
230             path = recordsetgroup['Path']
231             value = recordsetgroup['Value']
232             if isinstance(value, list):
233                 for index, recordset in enumerate(value):
234                     tree = path[:] + [index]
235                     matches.extend(self.check_recordset(tree, recordset))
236 
237         return matches
238
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/cfnlint/rules/resources/route53/RecordSet.py b/src/cfnlint/rules/resources/route53/RecordSet.py
--- a/src/cfnlint/rules/resources/route53/RecordSet.py
+++ b/src/cfnlint/rules/resources/route53/RecordSet.py
@@ -30,6 +30,7 @@
 
     REGEX_DOMAINNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(.)$')
     REGEX_TXT = re.compile(r'^("[^"]{1,255}" *)*"[^"]{1,255}"$')
+    REGEX_CNAME_VALIDATIONS = re.compile(r'^.*\.acm-validations\.aws\.?$')
 
     def check_a_record(self, path, recordset):
         """Check A record Configuration"""
@@ -116,10 +117,10 @@
             for index, record in enumerate(resource_records):
                 if not isinstance(record, dict):
                     tree = path[:] + ['ResourceRecords', index]
-                    if (not re.match(self.REGEX_DOMAINNAME, record)
-                            # ACM Route 53 validation uses invalid CNAMEs starting with `_`,
-                            # special-case them rather than complicate the regex.
-                            and not record.endswith('.acm-validations.aws.')):
+                    if (not re.match(self.REGEX_DOMAINNAME, record) and
+                            not re.match(self.REGEX_CNAME_VALIDATIONS, record)):
+                        # 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(tree, message.format(record)))

{"golden_diff": "diff --git a/src/cfnlint/rules/resources/route53/RecordSet.py b/src/cfnlint/rules/resources/route53/RecordSet.py\n--- a/src/cfnlint/rules/resources/route53/RecordSet.py\n+++ b/src/cfnlint/rules/resources/route53/RecordSet.py\n@@ -30,6 +30,7 @@\n \n     REGEX_DOMAINNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(.)$')\n     REGEX_TXT = re.compile(r'^(\"[^\"]{1,255}\" *)*\"[^\"]{1,255}\"$')\n+    REGEX_CNAME_VALIDATIONS = re.compile(r'^.*\\.acm-validations\\.aws\\.?$')\n \n     def check_a_record(self, path, recordset):\n         \"\"\"Check A record Configuration\"\"\"\n@@ -116,10 +117,10 @@\n             for index, record in enumerate(resource_records):\n                 if not isinstance(record, dict):\n                     tree = path[:] + ['ResourceRecords', index]\n-                    if (not re.match(self.REGEX_DOMAINNAME, record)\n-                            # ACM Route 53 validation uses invalid CNAMEs starting with `_`,\n-                            # special-case them rather than complicate the regex.\n-                            and not record.endswith('.acm-validations.aws.')):\n+                    if (not re.match(self.REGEX_DOMAINNAME, record) and\n+                            not re.match(self.REGEX_CNAME_VALIDATIONS, record)):\n+                        # ACM Route 53 validation uses invalid CNAMEs starting with `_`,\n+                        # special-case them rather than complicate the regex.\n                         message = 'CNAME record ({}) does not contain a valid domain name'\n                         matches.append(RuleMatch(tree, message.format(record)))\n", "issue": "E3020 - ACM Validation - Linter error\n*cfn-lint version: 0.11.1*\r\n\r\nHello!\r\n\r\nSo I have the below snippet of code:\r\n```\r\n  r7XXXZZb04efXXXYYYZZ2b8cc0b2aa1f132XXXYYYZZZCNAMERecordSet:\r\n    Type: AWS::Route53::RecordSet\r\n    Properties:\r\n      HostedZoneId: !Ref rHostedZone\r\n      Comment: domain CNAME record.\r\n      Name: !Join [\"\", [\"_75XXXTTT04efa8c2052b8ccXXXYYZZZ.XXXYYYZZZ.\", !Ref pDomainName, \".\"]]\r\n      Type: CNAME\r\n      TTL: '300'\r\n      ResourceRecords:\r\n        - _14062XXXXYYY6b1f37eb361e9XXXYYYZZZ.acm-validations.aws\r\n```\r\n\r\nUnfortunately linter reports it as a E3020 error.\r\n\r\n```\r\nE3020 CNAME record (_14062XXXXYYY6b1f37eb361e9XXXYYYZZZ.acm-validations.aws) does not contain a valid domain name\r\ndns/dns-production.template:645:7\r\n```\r\n\r\nIs that a proper behaviour? Resource is created properly - and the record works as it should - why then linter complains about it?\r\n\n", "before_files": [{"content": "\"\"\"\n  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this\n  software and associated documentation files (the \"Software\"), to deal in the Software\n  without restriction, including without limitation the rights to use, copy, modify,\n  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\nimport re\nfrom cfnlint import CloudFormationLintRule\nfrom cfnlint import RuleMatch\nfrom cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC\n\n\nclass RecordSet(CloudFormationLintRule):\n    \"\"\"Check Route53 Recordset Configuration\"\"\"\n    id = 'E3020'\n    shortdesc = 'Validate Route53 RecordSets'\n    description = 'Check if all RecordSets are correctly configured'\n    source_url = 'https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html'\n    tags = ['resources', 'route53', 'record_set']\n\n    REGEX_DOMAINNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(.)$')\n    REGEX_TXT = re.compile(r'^(\"[^\"]{1,255}\" *)*\"[^\"]{1,255}\"$')\n\n    def check_a_record(self, path, recordset):\n        \"\"\"Check A record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        for index, record in enumerate(resource_records):\n\n            if not isinstance(record, dict):\n                tree = path[:] + ['ResourceRecords', index]\n\n                # Check if a valid IPv4 address is specified\n                if not re.match(REGEX_IPV4, record):\n                    message = 'A record ({}) is not a valid IPv4 address'\n                    matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_aaaa_record(self, path, recordset):\n        \"\"\"Check AAAA record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        for index, record in enumerate(resource_records):\n\n            if not isinstance(record, dict):\n                tree = path[:] + ['ResourceRecords', index]\n\n                # Check if a valid IPv4 address is specified\n                if not re.match(REGEX_IPV6, record):\n                    message = 'AAAA record ({}) is not a valid IPv6 address'\n                    matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_caa_record(self, path, recordset):\n        \"\"\"Check CAA record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict):\n                # Split the record up to the mandatory settings (flags tag \"value\")\n                items = record.split(' ', 2)\n\n                # Check if the 3 settings are given.\n                if len(items) != 3:\n                    message = 'CAA record must contain 3 settings (flags tag \"value\"), record contains {} settings.'\n                    matches.append(RuleMatch(tree, message.format(len(items))))\n                else:\n                    # Check the flag value\n                    if not items[0].isdigit():\n                        message = 'CAA record flag setting ({}) should be of type Integer.'\n                        matches.append(RuleMatch(tree, message.format(items[0])))\n                    else:\n                        if int(items[0]) not in [0, 128]:\n                            message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.'\n                            matches.append(RuleMatch(tree, message.format(items[0])))\n\n                    # Check the tag value\n                    if not re.match(REGEX_ALPHANUMERIC, items[1]):\n                        message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.'\n                        matches.append(RuleMatch(tree, message.format(items[0])))\n\n                    # Check the value\n                    if not items[2].startswith('\"') or not items[2].endswith('\"'):\n                        message = 'CAA record value setting has to be enclosed in double quotation marks (\").'\n                        matches.append(RuleMatch(tree, message))\n\n        return matches\n\n    def check_cname_record(self, path, recordset):\n        \"\"\"Check CNAME record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        if len(resource_records) > 1:\n            message = 'A CNAME recordset can only contain 1 value'\n            matches.append(RuleMatch(path + ['ResourceRecords'], message))\n        else:\n            for index, record in enumerate(resource_records):\n                if not isinstance(record, dict):\n                    tree = path[:] + ['ResourceRecords', index]\n                    if (not re.match(self.REGEX_DOMAINNAME, record)\n                            # ACM Route 53 validation uses invalid CNAMEs starting with `_`,\n                            # special-case them rather than complicate the regex.\n                            and not record.endswith('.acm-validations.aws.')):\n                        message = 'CNAME record ({}) does not contain a valid domain name'\n                        matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_mx_record(self, path, recordset):\n        \"\"\"Check MX record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict):\n                # Split the record up to the mandatory settings (priority domainname)\n                items = record.split(' ')\n\n                # Check if the 3 settings are given.\n                if len(items) != 2:\n                    message = 'MX record must contain 2 settings (priority domainname), record contains {} settings.'\n                    matches.append(RuleMatch(tree, message.format(len(items), record)))\n                else:\n                    # Check the priority value\n                    if not items[0].isdigit():\n                        message = 'MX record priority setting ({}) should be of type Integer.'\n                        matches.append(RuleMatch(tree, message.format(items[0], record)))\n                    else:\n                        if not 0 <= int(items[0]) <= 65535:\n                            message = 'Invalid MX record priority setting ({}) given, must be between 0 and 65535.'\n                            matches.append(RuleMatch(tree, message.format(items[0], record)))\n\n                    # Check the domainname value\n                    if not re.match(self.REGEX_DOMAINNAME, items[1]):\n                        matches.append(RuleMatch(tree, message.format(items[1])))\n\n        return matches\n\n    def check_txt_record(self, path, recordset):\n        \"\"\"Check TXT record Configuration\"\"\"\n        matches = []\n\n        # Check quotation of the records\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict) and not re.match(self.REGEX_TXT, record):\n                message = 'TXT record is not structured as one or more items up to 255 characters ' \\\n                          'enclosed in double quotation marks at {0}'\n                matches.append(RuleMatch(\n                    tree,\n                    (\n                        message.format('/'.join(map(str, tree)))\n                    ),\n                ))\n\n        return matches\n\n    def check_recordset(self, path, recordset):\n        \"\"\"Check record configuration\"\"\"\n\n        matches = []\n        recordset_type = recordset.get('Type')\n\n        # Skip Intrinsic functions\n        if not isinstance(recordset_type, dict):\n            if not recordset.get('AliasTarget'):\n                # If no Alias is specified, ResourceRecords has to be specified\n                if not recordset.get('ResourceRecords'):\n                    message = 'Property ResourceRecords missing at {}'\n                    matches.append(RuleMatch(path, message.format('/'.join(map(str, path)))))\n                # Record type specific checks\n                elif recordset_type == 'A':\n                    matches.extend(self.check_a_record(path, recordset))\n                elif recordset_type == 'AAAA':\n                    matches.extend(self.check_aaaa_record(path, recordset))\n                elif recordset_type == 'CAA':\n                    matches.extend(self.check_caa_record(path, recordset))\n                elif recordset_type == 'CNAME':\n                    matches.extend(self.check_cname_record(path, recordset))\n                elif recordset_type == 'MX':\n                    matches.extend(self.check_mx_record(path, recordset))\n                elif recordset_type == 'TXT':\n                    matches.extend(self.check_txt_record(path, recordset))\n\n        return matches\n\n    def match(self, cfn):\n        \"\"\"Check RecordSets and RecordSetGroups Properties\"\"\"\n\n        matches = []\n\n        recordsets = cfn.get_resources(['AWS::Route53::RecordSet'])\n\n        for name, recordset in recordsets.items():\n            path = ['Resources', name, 'Properties']\n\n            if isinstance(recordset, dict):\n                props = recordset.get('Properties')\n                if props:\n                    matches.extend(self.check_recordset(path, props))\n\n        recordsetgroups = cfn.get_resource_properties(['AWS::Route53::RecordSetGroup', 'RecordSets'])\n\n        for recordsetgroup in recordsetgroups:\n            path = recordsetgroup['Path']\n            value = recordsetgroup['Value']\n            if isinstance(value, list):\n                for index, recordset in enumerate(value):\n                    tree = path[:] + [index]\n                    matches.extend(self.check_recordset(tree, recordset))\n\n        return matches\n", "path": "src/cfnlint/rules/resources/route53/RecordSet.py"}], "after_files": [{"content": "\"\"\"\n  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this\n  software and associated documentation files (the \"Software\"), to deal in the Software\n  without restriction, including without limitation the rights to use, copy, modify,\n  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\nimport re\nfrom cfnlint import CloudFormationLintRule\nfrom cfnlint import RuleMatch\nfrom cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC\n\n\nclass RecordSet(CloudFormationLintRule):\n    \"\"\"Check Route53 Recordset Configuration\"\"\"\n    id = 'E3020'\n    shortdesc = 'Validate Route53 RecordSets'\n    description = 'Check if all RecordSets are correctly configured'\n    source_url = 'https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html'\n    tags = ['resources', 'route53', 'record_set']\n\n    REGEX_DOMAINNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(.)$')\n    REGEX_TXT = re.compile(r'^(\"[^\"]{1,255}\" *)*\"[^\"]{1,255}\"$')\n    REGEX_CNAME_VALIDATIONS = re.compile(r'^.*\\.acm-validations\\.aws\\.?$')\n\n    def check_a_record(self, path, recordset):\n        \"\"\"Check A record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        for index, record in enumerate(resource_records):\n\n            if not isinstance(record, dict):\n                tree = path[:] + ['ResourceRecords', index]\n\n                # Check if a valid IPv4 address is specified\n                if not re.match(REGEX_IPV4, record):\n                    message = 'A record ({}) is not a valid IPv4 address'\n                    matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_aaaa_record(self, path, recordset):\n        \"\"\"Check AAAA record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        for index, record in enumerate(resource_records):\n\n            if not isinstance(record, dict):\n                tree = path[:] + ['ResourceRecords', index]\n\n                # Check if a valid IPv4 address is specified\n                if not re.match(REGEX_IPV6, record):\n                    message = 'AAAA record ({}) is not a valid IPv6 address'\n                    matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_caa_record(self, path, recordset):\n        \"\"\"Check CAA record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict):\n                # Split the record up to the mandatory settings (flags tag \"value\")\n                items = record.split(' ', 2)\n\n                # Check if the 3 settings are given.\n                if len(items) != 3:\n                    message = 'CAA record must contain 3 settings (flags tag \"value\"), record contains {} settings.'\n                    matches.append(RuleMatch(tree, message.format(len(items))))\n                else:\n                    # Check the flag value\n                    if not items[0].isdigit():\n                        message = 'CAA record flag setting ({}) should be of type Integer.'\n                        matches.append(RuleMatch(tree, message.format(items[0])))\n                    else:\n                        if int(items[0]) not in [0, 128]:\n                            message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.'\n                            matches.append(RuleMatch(tree, message.format(items[0])))\n\n                    # Check the tag value\n                    if not re.match(REGEX_ALPHANUMERIC, items[1]):\n                        message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.'\n                        matches.append(RuleMatch(tree, message.format(items[0])))\n\n                    # Check the value\n                    if not items[2].startswith('\"') or not items[2].endswith('\"'):\n                        message = 'CAA record value setting has to be enclosed in double quotation marks (\").'\n                        matches.append(RuleMatch(tree, message))\n\n        return matches\n\n    def check_cname_record(self, path, recordset):\n        \"\"\"Check CNAME record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n        if len(resource_records) > 1:\n            message = 'A CNAME recordset can only contain 1 value'\n            matches.append(RuleMatch(path + ['ResourceRecords'], message))\n        else:\n            for index, record in enumerate(resource_records):\n                if not isinstance(record, dict):\n                    tree = path[:] + ['ResourceRecords', index]\n                    if (not re.match(self.REGEX_DOMAINNAME, record) and\n                            not re.match(self.REGEX_CNAME_VALIDATIONS, record)):\n                        # ACM Route 53 validation uses invalid CNAMEs starting with `_`,\n                        # special-case them rather than complicate the regex.\n                        message = 'CNAME record ({}) does not contain a valid domain name'\n                        matches.append(RuleMatch(tree, message.format(record)))\n\n        return matches\n\n    def check_mx_record(self, path, recordset):\n        \"\"\"Check MX record Configuration\"\"\"\n        matches = []\n\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict):\n                # Split the record up to the mandatory settings (priority domainname)\n                items = record.split(' ')\n\n                # Check if the 3 settings are given.\n                if len(items) != 2:\n                    message = 'MX record must contain 2 settings (priority domainname), record contains {} settings.'\n                    matches.append(RuleMatch(tree, message.format(len(items), record)))\n                else:\n                    # Check the priority value\n                    if not items[0].isdigit():\n                        message = 'MX record priority setting ({}) should be of type Integer.'\n                        matches.append(RuleMatch(tree, message.format(items[0], record)))\n                    else:\n                        if not 0 <= int(items[0]) <= 65535:\n                            message = 'Invalid MX record priority setting ({}) given, must be between 0 and 65535.'\n                            matches.append(RuleMatch(tree, message.format(items[0], record)))\n\n                    # Check the domainname value\n                    if not re.match(self.REGEX_DOMAINNAME, items[1]):\n                        matches.append(RuleMatch(tree, message.format(items[1])))\n\n        return matches\n\n    def check_txt_record(self, path, recordset):\n        \"\"\"Check TXT record Configuration\"\"\"\n        matches = []\n\n        # Check quotation of the records\n        resource_records = recordset.get('ResourceRecords')\n\n        for index, record in enumerate(resource_records):\n            tree = path[:] + ['ResourceRecords', index]\n\n            if not isinstance(record, dict) and not re.match(self.REGEX_TXT, record):\n                message = 'TXT record is not structured as one or more items up to 255 characters ' \\\n                          'enclosed in double quotation marks at {0}'\n                matches.append(RuleMatch(\n                    tree,\n                    (\n                        message.format('/'.join(map(str, tree)))\n                    ),\n                ))\n\n        return matches\n\n    def check_recordset(self, path, recordset):\n        \"\"\"Check record configuration\"\"\"\n\n        matches = []\n        recordset_type = recordset.get('Type')\n\n        # Skip Intrinsic functions\n        if not isinstance(recordset_type, dict):\n            if not recordset.get('AliasTarget'):\n                # If no Alias is specified, ResourceRecords has to be specified\n                if not recordset.get('ResourceRecords'):\n                    message = 'Property ResourceRecords missing at {}'\n                    matches.append(RuleMatch(path, message.format('/'.join(map(str, path)))))\n                # Record type specific checks\n                elif recordset_type == 'A':\n                    matches.extend(self.check_a_record(path, recordset))\n                elif recordset_type == 'AAAA':\n                    matches.extend(self.check_aaaa_record(path, recordset))\n                elif recordset_type == 'CAA':\n                    matches.extend(self.check_caa_record(path, recordset))\n                elif recordset_type == 'CNAME':\n                    matches.extend(self.check_cname_record(path, recordset))\n                elif recordset_type == 'MX':\n                    matches.extend(self.check_mx_record(path, recordset))\n                elif recordset_type == 'TXT':\n                    matches.extend(self.check_txt_record(path, recordset))\n\n        return matches\n\n    def match(self, cfn):\n        \"\"\"Check RecordSets and RecordSetGroups Properties\"\"\"\n\n        matches = []\n\n        recordsets = cfn.get_resources(['AWS::Route53::RecordSet'])\n\n        for name, recordset in recordsets.items():\n            path = ['Resources', name, 'Properties']\n\n            if isinstance(recordset, dict):\n                props = recordset.get('Properties')\n                if props:\n                    matches.extend(self.check_recordset(path, props))\n\n        recordsetgroups = cfn.get_resource_properties(['AWS::Route53::RecordSetGroup', 'RecordSets'])\n\n        for recordsetgroup in recordsetgroups:\n            path = recordsetgroup['Path']\n            value = recordsetgroup['Value']\n            if isinstance(value, list):\n                for index, recordset in enumerate(value):\n                    tree = path[:] + [index]\n                    matches.extend(self.check_recordset(tree, recordset))\n\n        return matches\n", "path": "src/cfnlint/rules/resources/route53/RecordSet.py"}]}