Repository: aws-cloudformation/cfn-lint

Issue:
cfn-lint 0.49.1 does not catch `/` as an invalid character in a Mapping element name
*cfn-lint version: cfn-lint 0.49.1*

*cfn-lint did not catch `/` as an invalid character in a Mapping element name*

cfn-lint passed successfully with this mapping included in the template:
```yaml
Mappings:
  NameServers:
    10.90.0.0/16:
      NameServer1: 10.90.0.10
      NameServer2: 10.90.4.10
    10.91.0.0/16:
      NameServer1: 10.91.0.10
      NameServer2: 10.91.4.10
```

However AWS rejected it:
> Template format error: Mappings element name '10.93.0.0/16' must be non-empty and can contain only alphanumerics, '-' or '.'

![Screen Shot 2021-05-12 at 11 34 41](https://user-images.githubusercontent.com/7205587/117905164-0e917c00-b316-11eb-8132-2da240ff892a.png)



Discussion:
Thanks for reporting, does seem to be missing that level:

<img width="656" alt="Screen Shot 2021-05-11 at 9 49 34 PM" src="https://user-images.githubusercontent.com/7014355/117906384-ce44e600-b2a2-11eb-9ad7-1d900c5ec4bd.png">

@PatMyron According to the [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html) `The name can contain only alphanumeric characters (A-Za-z0-9)`  This isn't fully true as you can do regions like `us-east-1` in the key names. Do you know what characters are valid.

We actually disabled the alphanumeric check for this because I couldn't find what a correct regex would be.
https://github.com/aws-cloudformation/cfn-python-lint/blob/853522ff0e4c977331ac677a47059cf96b6f3189/src/cfnlint/rules/mappings/KeyName.py#L40
I think I could convert to `^[a-zA-Z0-9._-]{1,255}$` but not sure that is valid for all levels either.  Going to test a few things.
```yaml
Mappings:
  region-name:
    us-east-1:
      key-1: value
```

`Mappings name 'region-name' is non alphanumeric.`
`Mappings attribute name 'key-1' must contain only alphanumeric characters.`

So it appears just the key where `us-east-1` has a different pattern :/

Resolving patch:
diff --git a/src/cfnlint/rules/mappings/KeyName.py b/src/cfnlint/rules/mappings/KeyName.py
--- a/src/cfnlint/rules/mappings/KeyName.py
+++ b/src/cfnlint/rules/mappings/KeyName.py
@@ -17,14 +17,26 @@ class KeyName(CloudFormationLintRule):
     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html'
     tags = ['mappings']
 
-    def check_key(self, key, path, check_alphanumeric=True):
+    def check_attribute(self, key, path):
+        """ Check the key name for string and alphanumeric"""
+        matches = []
+        if not isinstance(key, six.string_types):
+            message = 'Mapping attribute ({0}) has to be a string.'
+            matches.append(RuleMatch(path[:], message.format(key)))
+        elif not re.match(REGEX_ALPHANUMERIC, key):
+            message = 'Mapping attribute ({0}) has invalid name. Name has to be alphanumeric.'
+            matches.append(RuleMatch(path[:], message.format(key)))
+
+        return matches
+
+    def check_key(self, key, path):
         """ Check the key name for string and alphanumeric"""
         matches = []
         if not isinstance(key, six.string_types):
             message = 'Mapping key ({0}) has to be a string.'
             matches.append(RuleMatch(path[:], message.format(key)))
-        elif not re.match(REGEX_ALPHANUMERIC, key) and check_alphanumeric:
-            message = 'Mapping key ({0}) has invalid name. Name has to be alphanumeric.'
+        elif not re.match('^[a-zA-Z0-9.-]{1,255}$', key):
+            message = 'Mapping key ({0}) has invalid name. Name has to be alphanumeric, \'-\' or \'.\''
             matches.append(RuleMatch(path[:], message.format(key)))
 
         return matches
@@ -37,11 +49,11 @@ def match(self, cfn):
             if isinstance(mapping_value, dict):
                 for key_name, key_value in mapping_value.items():
                     matches.extend(self.check_key(
-                        key_name, ['Mappings', mapping_name, key_name], False))
+                        key_name, ['Mappings', mapping_name, key_name]))
                     if isinstance(key_value, dict):
                         for sub_key_name, _ in key_value.items():
                             matches.extend(
-                                self.check_key(
+                                self.check_attribute(
                                     sub_key_name, ['Mappings', mapping_name, key_name, sub_key_name]))
 
         return matches


Test patch:
diff --git a/test/fixtures/templates/bad/mappings/key_name.yaml b/test/fixtures/templates/bad/mappings/key_name.yaml
--- a/test/fixtures/templates/bad/mappings/key_name.yaml
+++ b/test/fixtures/templates/bad/mappings/key_name.yaml
@@ -7,4 +7,8 @@ Mappings:
     us-east-1a:
       123456789012: "ami-951945d0"
       us-east-1b: "ami-951945d1"
+  NameServers:
+    10.90.0.0/16:
+      NameServer1: 10.90.0.10
+      NameServer2: 10.90.4.10
 Resources: {}
diff --git a/test/unit/rules/mappings/test_key_name.py b/test/unit/rules/mappings/test_key_name.py
--- a/test/unit/rules/mappings/test_key_name.py
+++ b/test/unit/rules/mappings/test_key_name.py
@@ -23,4 +23,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/mappings/key_name.yaml', 3)
+        self.helper_file_negative('test/fixtures/templates/bad/mappings/key_name.yaml', 4)