Repository: aws-cloudformation/cfn-lint

Issue:
Nesting FindInMap not supported
Don't ask why, but we have the following CloudFormation:

```
CidrBlock: !FindInMap [!FindInMap ["Environment", !Ref "Environment", "Subnets"], "eu-west-1a", "Management"]
```

Which causes an error:
```
E1011 Map Name should be a string for Resources/XXX/Properties/CidrBlock/Fn::FindInMap
../xxx.yaml:
```
The FindInMap rule does not support recursive FindInMaps.



Discussion:
Should work though.  Fixing shortly.  Ref and FindInMap are the documented approved functions and should work for all 3 properties for findinmap.
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-findinmap.html#w2ab2c21c28c26c13


Resolving patch:
diff --git a/src/cfnlint/__init__.py b/src/cfnlint/__init__.py
--- a/src/cfnlint/__init__.py
+++ b/src/cfnlint/__init__.py
@@ -569,6 +569,9 @@ def _search_deep_keys(self, searchText, cfndict, path):
                 if key == searchText:
                     pathprop.append(cfndict[key])
                     keys.append(pathprop)
+                    # pop the last element off for nesting of found elements for
+                    # dict and list checks
+                    pathprop = pathprop[:-1]
                 if isinstance(cfndict[key], dict):
                     keys.extend(self._search_deep_keys(searchText, cfndict[key], pathprop))
                 elif isinstance(cfndict[key], list):
diff --git a/src/cfnlint/rules/functions/FindInMap.py b/src/cfnlint/rules/functions/FindInMap.py
--- a/src/cfnlint/rules/functions/FindInMap.py
+++ b/src/cfnlint/rules/functions/FindInMap.py
@@ -26,19 +26,35 @@ class FindInMap(CloudFormationLintRule):
     description = 'Making sure the function is a list of appropriate config'
     tags = ['base', 'functions', 'getatt']
 
-    def check_ref(self, obj, tree):
+    supported_functions = [
+        'Fn::FindInMap',
+        'Ref'
+    ]
+
+    def check_dict(self, obj, tree):
         """
             Check that obj is a dict with Ref as the only key
             Mappings only support Ref inside them
         """
         matches = list()
+
         if isinstance(obj, dict):
             if len(obj) == 1:
                 for key_name, _ in obj.items():
-                    if key_name not in ['Ref', 'Fn::FindInMap']:
-                        message = 'FindInMap only supports Ref and Fn::FindInMap for {0}'
+                    if key_name not in self.supported_functions:
+                        message = 'FindInMap only supports [{0}] functions at {1}'
                         matches.append(RuleMatch(
-                            tree[:] + [key_name], message.format('/'.join(map(str, tree)))))
+                            tree[:] + [key_name],
+                            message.format(
+                                ', '.join(map(str, self.supported_functions)),
+                                '/'.join(map(str, tree)))))
+            else:
+                message = 'FindInMap only supports an object of 1 of [{0}] functions at {1}'
+                matches.append(RuleMatch(
+                    tree[:],
+                    message.format(
+                        ', '.join(map(str, self.supported_functions)),
+                        '/'.join(map(str, tree)))))
 
         return matches
 
@@ -61,31 +77,44 @@ def match(self, cfn):
                 map_name = map_obj[0]
                 first_key = map_obj[1]
                 second_key = map_obj[2]
-                if not isinstance(map_name, six.string_types):
-                    message = 'Map Name should be a string for {0}'
-                    matches.append(RuleMatch(
-                        tree[:] + [0], message.format('/'.join(map(str, tree)))))
+
+                if isinstance(map_name, (six.string_types, six.text_type, dict)):
+                    if isinstance(map_name, dict):
+                        matches.extend(self.check_dict(map_name, tree[:] + [0]))
+                    else:
+                        if map_name not in mappings:
+                            message = 'Map Name {0} doesnt exist for {0}'
+                            matches.append(RuleMatch(
+                                tree[:] + [0], message.format(map_name, '/'.join(map(str, tree)))))
                 else:
-                    if map_name not in mappings:
-                        message = 'Map Name {0} doesnt exist for {0}'
-                        matches.append(RuleMatch(
-                            tree[:] + [0], message.format(map_name, '/'.join(map(str, tree)))))
+                    message = 'Map Name should be a {0}, or string at {1}'
+                    matches.append(RuleMatch(
+                        tree[:] + [0],
+                        message.format(
+                            ', '.join(map(str, self.supported_functions)),
+                            '/'.join(map(str, tree)))))
 
-                if isinstance(first_key, (six.string_types, dict, int)):
+                if isinstance(first_key, (six.text_type, six.string_types, dict, int)):
                     if isinstance(first_key, dict):
-                        matches.extend(self.check_ref(first_key, tree[:] + [1]))
+                        matches.extend(self.check_dict(first_key, tree[:] + [1]))
                 else:
-                    message = 'Map Name should be a string, int, FindInMap, or Ref for {0}'
+                    message = 'Map first key should be a {0}, string, or int at {1}'
                     matches.append(RuleMatch(
-                        tree[:] + [1], message.format('/'.join(map(str, tree)))))
+                        tree[:] + [1],
+                        message.format(
+                            ', '.join(map(str, self.supported_functions)),
+                            '/'.join(map(str, tree)))))
 
-                if isinstance(second_key, (six.string_types, dict, int)):
+                if isinstance(second_key, (six.text_type, six.string_types, dict, int)):
                     if isinstance(second_key, dict):
-                        matches.extend(self.check_ref(second_key, tree[:] + [2]))
+                        matches.extend(self.check_dict(second_key, tree[:] + [2]))
                 else:
-                    message = 'Map Name should be a string, int, FindInMap, or Ref for {0}'
+                    message = 'Map second key should be a {0}, string, or int at {1}'
                     matches.append(RuleMatch(
-                        tree[:] + [2], message.format('/'.join(tree))))
+                        tree[:] + [2],
+                        message.format(
+                            ', '.join(map(str, self.supported_functions)),
+                            '/'.join(tree))))
 
             else:
                 message = 'FindInMap is a list with 3 values for {0}'


Test patch:
diff --git a/test/rules/__init__.py b/test/rules/__init__.py
--- a/test/rules/__init__.py
+++ b/test/rules/__init__.py
@@ -39,6 +39,7 @@ class BaseRuleTestCase(BaseTestCase):
         'templates/good/properties_elb.yaml',
         'templates/good/functions_sub.yaml',
         'templates/good/functions_cidr.yaml',
+        'templates/good/functions_findinmap.yaml',
         'templates/good/resources_lambda.yaml',
         'templates/good/transform_serverless_api.yaml',
         'templates/good/transform_serverless_function.yaml',
diff --git a/test/rules/functions/test_find_in_map.py b/test/rules/functions/test_find_in_map.py
--- a/test/rules/functions/test_find_in_map.py
+++ b/test/rules/functions/test_find_in_map.py
@@ -31,4 +31,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('templates/bad/functions_findinmap.yaml', 3)
+        self.helper_file_negative('templates/bad/functions_findinmap.yaml', 5)
diff --git a/test/templates/bad/functions_findinmap.yaml b/test/templates/bad/functions_findinmap.yaml
--- a/test/templates/bad/functions_findinmap.yaml
+++ b/test/templates/bad/functions_findinmap.yaml
@@ -21,3 +21,16 @@ Resources:
           - azmap
           - !Ref "AWS::Region"
           - 0
+  myInstance2:
+    Type: AWS::EC2::Instance
+    Properties:
+      ImageId: !FindInMap [ !FindInMap [ myMapDne, !Ref 'AWS::Region', !Ref 'AWS::Region' ], !Ref 'AWS::Region', 32 ]
+  myInstance3:
+    Type: AWS::EC2::Instance
+    Properties:
+      ImageId:
+        Fn::FindInMap:
+        - Ref: myInstance
+          Fn::FindInMap: [ amimap, !Ref 'AWS::Region', !Ref 'AWS::Region' ]
+        - TopLevelKey
+        - SecondLevelKey
diff --git a/test/templates/bad/issues.yaml b/test/templates/bad/issues.yaml
new file mode 100644
--- /dev/null
+++ b/test/templates/bad/issues.yaml
@@ -0,0 +1,16 @@
+AWSTemplateFormatVersion: 2010-09-09
+Resources:
+  RDSOptionGroup:
+    Type: "AWS::RDS::OptionGroup"
+    Properties:
+      EngineName: "oracle-se2"
+      MajorEngineVersion: "12.1"
+      OptionGroupDescription: "SSL and Timezone options"
+      OptionConfigurations:
+        - OptionName: "SSL"
+          VpcSecurityGroupMemberships:
+            - !Ref "PrivateSecurityGroup"
+        - OptionName: "Timezone"
+          OptionSettings:
+            - Name: "TIME_ZONE"
+              Value: "Europe/Amsterdam"
diff --git a/test/templates/good/functions_findinmap.yaml b/test/templates/good/functions_findinmap.yaml
new file mode 100644
--- /dev/null
+++ b/test/templates/good/functions_findinmap.yaml
@@ -0,0 +1,30 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Description: >
+  Test Good Sub Strings
+Mappings:
+  myMap:
+    us-east-1:
+      32: ami-1234567
+      64: ami-abcdefg
+Resources:
+  myInstance1:
+    Type: AWS::EC2::Instance
+    Properties:
+      ImageId: !FindInMap [ !FindInMap [ myMap, !Ref 'AWS::Region', !Ref 'AWS::Region' ], !Ref 'AWS::Region', 32 ]
+  myInstance2:
+    Type: AWS::EC2::Instance
+    Properties:
+      ImageId:
+        Fn::FindInMap:
+        - !FindInMap [ myMap, !Ref 'AWS::Region', !Ref 'AWS::Region'  ]
+        - !FindInMap [ myMap, !Ref 'AWS::Region', !Ref 'AWS::Region'  ]
+        - !FindInMap [ myMap, !Ref 'AWS::Region', !Ref 'AWS::Region'  ]
+  myInstance3:
+    Type: AWS::EC2::Instance
+    Properties:
+      ImageId:
+        Fn::FindInMap:
+        - !Ref myInstance1
+        - !Ref myInstance1
+        - !Ref myInstance1