Repository: aws-cloudformation/cfn-lint

Issue:
Can't use lists directly in a Sub.
*cfn-lint version: (`cfn-lint --version`)*

*Description of issue.*
"Template validation error: Template error: variable pValues in Fn::Sub expression does not resolve to a string"

Please provide as much information as possible:
* Template linting issues:
Subs can't be directly included in a Sub
```
---
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  pKey:
    Type: String
  pValues:
    Type: CommaDelimitedList
Resources:
  MySubscription:
    Type: AWS::SNS::Subscription
    Properties:
      FilterPolicy: !Sub |
        ${pKey}: ${pValues}
```



Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/functions/Sub.py b/src/cfnlint/rules/functions/Sub.py
--- a/src/cfnlint/rules/functions/Sub.py
+++ b/src/cfnlint/rules/functions/Sub.py
@@ -32,44 +32,26 @@ def _test_string(self, cfn, sub_string, parameters, tree):
 
         matches = []
         string_params = cfn.get_sub_parameters(sub_string)
-        get_atts = cfn.get_valid_getatts()
-
-        valid_pseudo_params = [
-            'AWS::Region',
-            'AWS::StackName',
-            'AWS::URLSuffix',
-            'AWS::StackId',
-            'AWS::Region',
-            'AWS::Partition',
-            'AWS::NotificationARNs',
-            'AWS::AccountId'
-        ]
-
-        valid_params = valid_pseudo_params
-        valid_params.extend(cfn.get_parameter_names())
-        valid_params.extend(cfn.get_resource_names())
-        for key, _ in parameters.items():
-            valid_params.append(key)
 
         for string_param in string_params:
-            if isinstance(string_param, (six.string_types, six.text_type)):
-                if string_param not in valid_params:
-                    found = False
-                    for resource, attributes in get_atts.items():
-                        for attribute_name, _ in attributes.items():
-                            if resource == string_param.split('.')[0] and attribute_name == '*':
-                                found = True
-                            elif (resource == string_param.split('.')[0] and
-                                  attribute_name == '.'.join(string_param.split('.')[1:])):
-                                found = True
-                    if not found:
-                        message = 'Parameter {0} for Fn::Sub not found at {1}'
-                        matches.append(RuleMatch(
-                            tree, message.format(string_param, '/'.join(map(str, tree)))))
+            if isinstance(string_param, (six.string_types)):
+                matches.extend(self._test_parameter(string_param, cfn, parameters, tree))
 
         return matches
 
-    def _test_parameters(self, parameters, tree):
+    def _get_parameters(self, cfn):
+        """Get all Parameter Names"""
+        results = {}
+        parameters = cfn.template.get('Parameters', {})
+        if isinstance(parameters, dict):
+            for param_name, param_values in parameters.items():
+                # This rule isn't here to check the Types but we need
+                # something valid if it doesn't exist
+                results[param_name] = param_values.get('Type', 'String')
+
+        return results
+
+    def _test_parameters(self, parameters, cfn, tree):
         """Check parameters for appropriate configuration"""
 
         supported_functions = [
@@ -90,11 +72,18 @@ def _test_parameters(self, parameters, tree):
             param_tree = tree[:] + [parameter_name]
             if isinstance(parameter_value_obj, dict):
                 if len(parameter_value_obj) == 1:
-                    for key, _ in parameter_value_obj.items():
+                    for key, value in parameter_value_obj.items():
                         if key not in supported_functions:
                             message = 'Sub parameter should use a valid function for {0}'
                             matches.append(RuleMatch(
                                 param_tree, message.format('/'.join(map(str, tree)))))
+                        elif key in ['Ref']:
+                            matches.extend(self._test_parameter(value, cfn, {}, tree))
+                        elif key in ['Fn::GetAtt']:
+                            if isinstance(value, list):
+                                matches.extend(self._test_parameter('.'.join(value), cfn, {}, tree))
+                            elif isinstance(value, six.string_types):
+                                matches.extend(self._test_parameter(value, cfn, {}, tree))
                 else:
                     message = 'Sub parameter should be an object of 1 for {0}'
                     matches.append(RuleMatch(
@@ -106,6 +95,68 @@ def _test_parameters(self, parameters, tree):
 
         return matches
 
+    def _test_parameter(self, parameter, cfn, parameters, tree):
+        """ Test a parameter """
+
+        matches = []
+        get_atts = cfn.get_valid_getatts()
+
+        valid_pseudo_params = [
+            'AWS::Region',
+            'AWS::StackName',
+            'AWS::URLSuffix',
+            'AWS::StackId',
+            'AWS::Region',
+            'AWS::Partition',
+            'AWS::NotificationARNs',
+            'AWS::AccountId'
+        ]
+
+        odd_list_params = [
+            'CommaDelimitedList',
+            'AWS::SSM::Parameter::Value<CommaDelimitedList>',
+        ]
+
+        valid_params = valid_pseudo_params
+        valid_params.extend(cfn.get_resource_names())
+        template_parameters = self._get_parameters(cfn)
+
+        for key, _ in parameters.items():
+            valid_params.append(key)
+
+        if parameter not in valid_params:
+            found = False
+            if parameter in template_parameters:
+                found = True
+                if (
+                        template_parameters.get(parameter) in odd_list_params or
+                        template_parameters.get(parameter).startswith('AWS::SSM::Parameter::Value<List') or
+                        template_parameters.get(parameter).startswith('List')):
+                    message = 'Fn::Sub cannot use list {0} at {1}'
+                    matches.append(RuleMatch(
+                        tree, message.format(parameter, '/'.join(map(str, tree)))))
+            for resource, attributes in get_atts.items():
+                for attribute_name, attribute_values in attributes.items():
+                    if resource == parameter.split('.')[0] and attribute_name == '*':
+                        if attribute_values.get('Type') == 'List':
+                            message = 'Fn::Sub cannot use list {0} at {1}'
+                            matches.append(RuleMatch(
+                                tree, message.format(parameter, '/'.join(map(str, tree)))))
+                        found = True
+                    elif (resource == parameter.split('.')[0] and
+                          attribute_name == '.'.join(parameter.split('.')[1:])):
+                        if attribute_values.get('Type') == 'List':
+                            message = 'Fn::Sub cannot use list {0} at {1}'
+                            matches.append(RuleMatch(
+                                tree, message.format(parameter, '/'.join(map(str, tree)))))
+                        found = True
+            if not found:
+                message = 'Parameter {0} for Fn::Sub not found at {1}'
+                matches.append(RuleMatch(
+                    tree, message.format(parameter, '/'.join(map(str, tree)))))
+
+        return matches
+
     def match(self, cfn):
         """Check CloudFormation Join"""
 
@@ -132,7 +183,7 @@ def match(self, cfn):
                             tree + [1], message.format('/'.join(map(str, tree)))))
                     else:
                         matches.extend(self._test_string(cfn, sub_string, parameters, tree + [0]))
-                        matches.extend(self._test_parameters(parameters, tree))
+                        matches.extend(self._test_parameters(parameters, cfn, tree))
                 else:
                     message = 'Sub should be an array of 2 for {0}'
                     matches.append(RuleMatch(


Test patch:
diff --git a/test/fixtures/templates/bad/functions_sub.yaml b/test/fixtures/templates/bad/functions/sub.yaml
similarity index 75%
rename from test/fixtures/templates/bad/functions_sub.yaml
rename to test/fixtures/templates/bad/functions/sub.yaml
--- a/test/fixtures/templates/bad/functions_sub.yaml
+++ b/test/fixtures/templates/bad/functions/sub.yaml
@@ -2,6 +2,9 @@
 AWSTemplateFormatVersion: "2010-09-09"
 Description: >
   Test Sub Functions
+Parameters:
+  CidrBlock:
+    Type: CommaDelimitedList
 Resources:
   myInstance:
     Type: AWS::EC2::Instance
@@ -42,6 +45,24 @@ Resources:
           Fn::Sub:
           - yum install ${package}; echo ${AWS::Region}; echo ${!Literal}"
           - mypackage: httpd
+  myVPc:
+    Type: AWS::EC2::VPC
+    Properties:
+      CidrBlock: !Sub "${CidrBlock}"
+  myVPc2:
+    Type: AWS::EC2::VPC
+    Properties:
+      CidrBlock:
+        Fn::Sub:
+        - "${myCidr}"
+        - myCidr: !Ref CidrBlock
 Outputs:
   myOutput:
     Value: !Sub "${myInstance3.PrivateDnsName1}"
+  list:
+    Value: !Sub "${myVPc.CidrBlockAssociations}"
+  list2:
+    Value:
+      Fn::Sub:
+      - "${myCidr}"
+      - myCidr: !GetAtt myVPc.CidrBlockAssociations
diff --git a/test/fixtures/templates/good/functions_sub.yaml b/test/fixtures/templates/good/functions/sub.yaml
similarity index 100%
rename from test/fixtures/templates/good/functions_sub.yaml
rename to test/fixtures/templates/good/functions/sub.yaml
diff --git a/test/rules/functions/test_sub.py b/test/rules/functions/test_sub.py
--- a/test/rules/functions/test_sub.py
+++ b/test/rules/functions/test_sub.py
@@ -25,7 +25,7 @@ def setUp(self):
         super(TestRulesSub, self).setUp()
         self.collection.register(Sub())
         self.success_templates = [
-            'test/fixtures/templates/good/functions_sub.yaml',
+            'test/fixtures/templates/good/functions/sub.yaml',
         ]
 
     def test_file_positive(self):
@@ -34,4 +34,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/functions_sub.yaml', 9)
+        self.helper_file_negative('test/fixtures/templates/bad/functions/sub.yaml', 13)
diff --git a/test/rules/functions/test_sub_needed.py b/test/rules/functions/test_sub_needed.py
--- a/test/rules/functions/test_sub_needed.py
+++ b/test/rules/functions/test_sub_needed.py
@@ -25,7 +25,7 @@ def setUp(self):
         super(TestSubNeeded, self).setUp()
         self.collection.register(SubNeeded())
         self.success_templates = [
-            'test/fixtures/templates/good/functions_sub.yaml',
+            'test/fixtures/templates/good/functions/sub.yaml',
             'test/fixtures/templates/good/functions/sub_needed.yaml',
         ]
 
diff --git a/test/rules/functions/test_sub_unneeded.py b/test/rules/functions/test_sub_unneeded.py
--- a/test/rules/functions/test_sub_unneeded.py
+++ b/test/rules/functions/test_sub_unneeded.py
@@ -25,7 +25,7 @@ def setUp(self):
         super(TestSubUnneeded, self).setUp()
         self.collection.register(SubUnneeded())
         self.success_templates = [
-            'test/fixtures/templates/good/functions_sub.yaml',
+            'test/fixtures/templates/good/functions/sub.yaml',
         ]
 
     def test_file_positive(self):