Repository: aws-cloudformation/cfn-lint

Issue:
cfn-lint cannot catch "Every Type member must be a string"
*cfn-lint version: (`cfn-lint --version`)*
```
cfn-lint 0.34.0
```

*aws-cli version: (`aws --version`)*
```
aws-cli/1.18.97 Python/3.8.3 Linux/4.15.0-111-generic botocore/1.16.26
```

*Description of issue.*
```
$ cat template.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  CloudformationType:
    Type: String
    Default: AWS::EC2::Instance
    AllowedValues:
    - AWS::EC2::Instance
    - AWS::AutoScaling::LaunchConfiguration
Resources:
  EC2InstanceOrLaunchConfig:
    Type: !Ref CloudformationType
    Properties:
      InstanceType: t3.nano
      ImageId: ami-0b90a8636b6f955c1
$ aws cloudformation deploy --stack-name test --template-file template.yml
An error occurred (ValidationError) when calling the CreateChangeSet operation: Template format error: [/Resources/EC2InstanceOrLaunchConfig] Every Type member must be a string.
$ aws cloudformation validate-template --template-body file://template.yml

An error occurred (ValidationError) when calling the ValidateTemplate operation: Template format error: [/Resources/EC2InstanceOrLaunchConfig] Every Type member must be a string.
$ cfn-lint template.yml
$ echo $?
0
```



Discussion:
Agreed we should have this covered.  I'll get that fixed.

Resolving patch:
diff --git a/src/cfnlint/rules/__init__.py b/src/cfnlint/rules/__init__.py
--- a/src/cfnlint/rules/__init__.py
+++ b/src/cfnlint/rules/__init__.py
@@ -7,6 +7,7 @@
 from datetime import datetime
 import importlib
 import traceback
+import six
 import cfnlint.helpers
 from cfnlint.decode.node import TemplateAttributeError
 
@@ -337,18 +338,19 @@ def run(self, filename, cfn):
         for resource_name, resource_attributes in cfn.get_resources().items():
             resource_type = resource_attributes.get('Type')
             resource_properties = resource_attributes.get('Properties', {})
-            path = ['Resources', resource_name, 'Properties']
-            for rule in self.rules:
-                matches.extend(
-                    self.run_check(
-                        rule.matchall_resource_properties, filename, rule.id,
-                        filename, cfn, resource_properties, resource_type, path
+            if isinstance(resource_type, six.string_types) and isinstance(resource_properties, dict):
+                path = ['Resources', resource_name, 'Properties']
+                for rule in self.rules:
+                    matches.extend(
+                        self.run_check(
+                            rule.matchall_resource_properties, filename, rule.id,
+                            filename, cfn, resource_properties, resource_type, path
+                        )
                     )
-                )
 
-            matches.extend(
-                self.run_resource(
-                    filename, cfn, resource_type, resource_properties, path))
+                matches.extend(
+                    self.run_resource(
+                        filename, cfn, resource_type, resource_properties, path))
 
         return matches
 
diff --git a/src/cfnlint/rules/resources/Configuration.py b/src/cfnlint/rules/resources/Configuration.py
--- a/src/cfnlint/rules/resources/Configuration.py
+++ b/src/cfnlint/rules/resources/Configuration.py
@@ -16,8 +16,9 @@ class Configuration(CloudFormationLintRule):
     source_url = 'https://github.com/aws-cloudformation/cfn-python-lint'
     tags = ['resources']
 
-    def match(self, cfn):
-        matches = []
+
+    def _check_resource(self, cfn, resource_name, resource_values):
+        """ Check Resource """
 
         valid_attributes = [
             'Condition',
@@ -42,6 +43,97 @@ def match(self, cfn):
             'Version',
         ]
 
+        matches = []
+        if not isinstance(resource_values, dict):
+            message = 'Resource not properly configured at {0}'
+            matches.append(RuleMatch(
+                ['Resources', resource_name],
+                message.format(resource_name)
+            ))
+            return matches
+
+        # validate condition is a string
+        condition = resource_values.get('Condition', '')
+        if not isinstance(condition, six.string_types):
+            message = 'Condition for resource {0} should be a string'
+            matches.append(RuleMatch(
+                ['Resources', resource_name, 'Condition'],
+                message.format(resource_name)
+            ))
+
+        resource_type = resource_values.get('Type', '')
+        check_attributes = []
+        if not isinstance(resource_type, six.string_types):
+            message = 'Type has to be a string at {0}'
+            matches.append(RuleMatch(
+                ['Resources', resource_name],
+                message.format('/'.join(['Resources', resource_name]))
+            ))
+            return matches
+
+        # Type is valid continue analysis
+        if resource_type.startswith('Custom::') or resource_type == 'AWS::CloudFormation::CustomResource':
+            check_attributes = valid_custom_attributes
+        else:
+            check_attributes = valid_attributes
+
+        for property_key, _ in resource_values.items():
+            if property_key not in check_attributes:
+                message = 'Invalid resource attribute {0} for resource {1}'
+                matches.append(RuleMatch(
+                    ['Resources', resource_name, property_key],
+                    message.format(property_key, resource_name)))
+
+        if not resource_type:
+            message = 'Type not defined for resource {0}'
+            matches.append(RuleMatch(
+                ['Resources', resource_name],
+                message.format(resource_name)
+            ))
+        elif not isinstance(resource_type, six.string_types):
+            message = 'Type has to be a string at {0}'
+            matches.append(RuleMatch(
+                ['Resources', resource_name],
+                message.format('/'.join(['Resources', resource_name]))
+            ))
+        else:
+            self.logger.debug('Check resource types by region...')
+            for region, specs in cfnlint.helpers.RESOURCE_SPECS.items():
+                if region in cfn.regions:
+                    if resource_type not in specs['ResourceTypes']:
+                        if not resource_type.startswith(('Custom::', 'AWS::Serverless::')):
+                            message = 'Invalid or unsupported Type {0} for resource {1} in {2}'
+                            matches.append(RuleMatch(
+                                ['Resources', resource_name, 'Type'],
+                                message.format(resource_type, resource_name, region)
+                            ))
+
+        if 'Properties' not in resource_values:
+            resource_spec = cfnlint.helpers.RESOURCE_SPECS[cfn.regions[0]]
+            if resource_type in resource_spec['ResourceTypes']:
+                properties_spec = resource_spec['ResourceTypes'][resource_type]['Properties']
+                # pylint: disable=len-as-condition
+                if len(properties_spec) > 0:
+                    required = 0
+                    for _, property_spec in properties_spec.items():
+                        if property_spec.get('Required', False):
+                            required += 1
+                    if required > 0:
+                        if resource_type == 'AWS::CloudFormation::WaitCondition' and 'CreationPolicy' in resource_values.keys():
+                            self.logger.debug('Exception to required properties section as CreationPolicy is defined.')
+                        else:
+                            message = 'Properties not defined for resource {0}'
+                            matches.append(RuleMatch(
+                                ['Resources', resource_name],
+                                message.format(resource_name)
+                            ))
+
+        return matches
+
+
+    def match(self, cfn):
+        matches = []
+
         resources = cfn.template.get('Resources', {})
         if not isinstance(resources, dict):
             message = 'Resource not properly configured'
@@ -49,73 +141,6 @@ def match(self, cfn):
         else:
             for resource_name, resource_values in cfn.template.get('Resources', {}).items():
                 self.logger.debug('Validating resource %s base configuration', resource_name)
-                if not isinstance(resource_values, dict):
-                    message = 'Resource not properly configured at {0}'
-                    matches.append(RuleMatch(
-                        ['Resources', resource_name],
-                        message.format(resource_name)
-                    ))
-                    continue
-                resource_type = resource_values.get('Type', '')
-                check_attributes = []
-                if resource_type.startswith('Custom::') or resource_type == 'AWS::CloudFormation::CustomResource':
-                    check_attributes = valid_custom_attributes
-                else:
-                    check_attributes = valid_attributes
-
-                for property_key, _ in resource_values.items():
-                    if property_key not in check_attributes:
-                        message = 'Invalid resource attribute {0} for resource {1}'
-                        matches.append(RuleMatch(
-                            ['Resources', resource_name, property_key],
-                            message.format(property_key, resource_name)))
-
-                # validate condition is a string
-                condition = resource_values.get('Condition', '')
-                if not isinstance(condition, six.string_types):
-                    message = 'Condition for resource {0} should be a string'
-                    matches.append(RuleMatch(
-                        ['Resources', resource_name, 'Condition'],
-                        message.format(resource_name)
-                    ))
-
-                resource_type = resource_values.get('Type', '')
-                if not resource_type:
-                    message = 'Type not defined for resource {0}'
-                    matches.append(RuleMatch(
-                        ['Resources', resource_name],
-                        message.format(resource_name)
-                    ))
-                else:
-                    self.logger.debug('Check resource types by region...')
-                    for region, specs in cfnlint.helpers.RESOURCE_SPECS.items():
-                        if region in cfn.regions:
-                            if resource_type not in specs['ResourceTypes']:
-                                if not resource_type.startswith(('Custom::', 'AWS::Serverless::')):
-                                    message = 'Invalid or unsupported Type {0} for resource {1} in {2}'
-                                    matches.append(RuleMatch(
-                                        ['Resources', resource_name, 'Type'],
-                                        message.format(resource_type, resource_name, region)
-                                    ))
-
-                if 'Properties' not in resource_values:
-                    resource_spec = cfnlint.helpers.RESOURCE_SPECS[cfn.regions[0]]
-                    if resource_type in resource_spec['ResourceTypes']:
-                        properties_spec = resource_spec['ResourceTypes'][resource_type]['Properties']
-                        # pylint: disable=len-as-condition
-                        if len(properties_spec) > 0:
-                            required = 0
-                            for _, property_spec in properties_spec.items():
-                                if property_spec.get('Required', False):
-                                    required += 1
-                            if required > 0:
-                                if resource_type == 'AWS::CloudFormation::WaitCondition' and 'CreationPolicy' in resource_values.keys():
-                                    self.logger.debug('Exception to required properties section as CreationPolicy is defined.')
-                                else:
-                                    message = 'Properties not defined for resource {0}'
-                                    matches.append(RuleMatch(
-                                        ['Resources', resource_name],
-                                        message.format(resource_name)
-                                    ))
+                matches.extend(self._check_resource(cfn, resource_name, resource_values))
 
         return matches
diff --git a/src/cfnlint/runner.py b/src/cfnlint/runner.py
--- a/src/cfnlint/runner.py
+++ b/src/cfnlint/runner.py
@@ -68,4 +68,5 @@ def run(self):
                                 break
                         else:
                             return_matches.append(match)
+
         return return_matches


Test patch:
diff --git a/test/fixtures/templates/bad/generic.yaml b/test/fixtures/templates/bad/generic.yaml
--- a/test/fixtures/templates/bad/generic.yaml
+++ b/test/fixtures/templates/bad/generic.yaml
@@ -7,10 +7,10 @@ Mappings:
   runtime:
     us-east-1:
       production:
-      - CidrIp: 0.0.0.0/0
-        IpProtocol: tcp
-        ToPort: 80
-        FromPort: 80
+        - CidrIp: 0.0.0.0/0
+          IpProtocol: tcp
+          ToPort: 80
+          FromPort: 80
 Parameters:
   myParam:
     Type: String
@@ -35,8 +35,7 @@ Resources:
       FakeKey: MadeYouLook
       IamInstanceProfile: !GetAtt RootInstanceProfile.Arn
       BlockDeviceMappings:
-        -
-          DeviceName: /dev/sda
+        - DeviceName: /dev/sda
           Ebs:
             VolumeType: io1
             Iops: !Ref pIops
@@ -45,7 +44,7 @@ Resources:
             BadSubX2Key: Not valid
       NetworkInterfaces:
         - DeviceIndex:
-          - "1"
+            - "1"
           BadKey: true
   MyEC2Instance3:
     Type: "AWS::EC2::Instance"
@@ -62,8 +61,7 @@ Resources:
       AssumeRolePolicyDocument:
         Version: "2012-10-17"
         Statement:
-          -
-            Effect: "Allow"
+          - Effect: "Allow"
             Principal:
               Service:
                 - "ec2.amazonaws.com"
@@ -71,13 +69,11 @@ Resources:
               - "sts:AssumeRole"
       Path: "/"
       Policies:
-        -
-          PolicyName: "root"
+        - PolicyName: "root"
           PolicyDocument1:
             Version: "2012-10-17"
             Statement:
-              -
-                Effect: "Allow"
+              - Effect: "Allow"
                 Action: "*"
                 Resource: "*"
   RolePolicies:
@@ -87,48 +83,45 @@ Resources:
       PolicyDocument:
         Version: "2012-10-17"
         Statement:
-          -
-            Effect: "Allow"
+          - Effect: "Allow"
             Action: "*"
             Resource: "*"
       Roles:
-        -
-          Ref: "RootRole"
+        - Ref: "RootRole"
   RootInstanceProfile:
     Type: "AWS::IAM::InstanceProfile"
     Properties:
       Path: "/"
       Roles:
-        -
-          Ref: "RootRole"
+        - Ref: "RootRole"
 
   # Bad Key under HealthCheck
   ElasticLoadBalancer:
     Type: AWS::ElasticLoadBalancing::LoadBalancer
     Properties:
       AvailabilityZones:
-        Fn::GetAZs: ''
+        Fn::GetAZs: ""
       Instances:
-      - Ref: MyEC2Instance
+        - Ref: MyEC2Instance
       Listeners:
-      - LoadBalancerPort: '80'
-        InstancePort:
-          Ref: WebServerPort
-        Protocol: HTTP
+        - LoadBalancerPort: "80"
+          InstancePort:
+            Ref: WebServerPort
+          Protocol: HTTP
       HealthCheck:
         FakeKey: Another fake key
         Target:
           Fn::Join:
-          - ''
-          - - 'HTTP:'
-            - Ref: WebServerPort
-            - "/"
-        HealthyThreshold: '3'
+            - ""
+            - - "HTTP:"
+              - Ref: WebServerPort
+              - "/"
+        HealthyThreshold: "3"
         # Int which should be string. (No Error)
         UnhealthyThreshold: 5
         # Should be int (Error)
         Interval: Test
-        Timeout: '5'
+        Timeout: "5"
   SecurityGroup:
     Type: AWS::EC2::SecurityGroupIngress
     Properties:
@@ -145,46 +138,46 @@ Resources:
     Type: AWS::ElasticLoadBalancing::LoadBalancer
     Properties:
       Tags:
-      - Fn::If:
-        - IsProduction
-        - Key: Production
-          Value: True
-        - Key: Production
-          Value: False
-      Fn::If:
-      - IsProduction
-      - AvailabilityZones:
-          Fn::GetAZs: ''
-        Listeners:
         - Fn::If:
-          - IsProduction
-          - LoadBalancerPort: '443'
-            InstancePort: '443'
-            Protocol: HTTP
-          - LoadBalancerPort: '443'
-            InstancePort: '443'
-            Protocol: HTTP
-        HealthCheck:
-          Target: HTTP:80/
-          HealthyThreshold: '3'
-          UnhealthyThreshold: '5'
-          Interval: '30'
-          Timeout: '5'
-        ConnectionDrainingPolicy:
-          Enabled: 'true'
-          Timeout: '60'
-      - AvailabilityZones:
-          Fn::GetAZs: ''
-        Listeners:
-        - LoadBalancerPort: '80'
-          InstancePort: '80'
-          Protocol: HTTP
-        HealthCheck:
-          Target: HTTP:80/
-          HealthyThreshold: '3'
-          UnhealthyThreshold: '5'
-          Interval: '30'
-          Timeout: '5'
+            - IsProduction
+            - Key: Production
+              Value: True
+            - Key: Production
+              Value: False
+      Fn::If:
+        - IsProduction
+        - AvailabilityZones:
+            Fn::GetAZs: ""
+          Listeners:
+            - Fn::If:
+                - IsProduction
+                - LoadBalancerPort: "443"
+                  InstancePort: "443"
+                  Protocol: HTTP
+                - LoadBalancerPort: "443"
+                  InstancePort: "443"
+                  Protocol: HTTP
+          HealthCheck:
+            Target: HTTP:80/
+            HealthyThreshold: "3"
+            UnhealthyThreshold: "5"
+            Interval: "30"
+            Timeout: "5"
+          ConnectionDrainingPolicy:
+            Enabled: "true"
+            Timeout: "60"
+        - AvailabilityZones:
+            Fn::GetAZs: ""
+          Listeners:
+            - LoadBalancerPort: "80"
+              InstancePort: "80"
+              Protocol: HTTP
+          HealthCheck:
+            Target: HTTP:80/
+            HealthyThreshold: "3"
+            UnhealthyThreshold: "5"
+            Interval: "30"
+            Timeout: "5"
   # Fail when not using FindInMap
   lambdaMap1:
     Type: AWS::EC2::SecurityGroup
@@ -199,13 +192,13 @@ Resources:
     Properties:
       GroupDescription: test
       SecurityGroupIngress:
-      - Fn::FindInMap: [ runtime, !Ref 'AWS::Region', production ]
+        - Fn::FindInMap: [runtime, !Ref "AWS::Region", production]
   PermitAllInbound:
     Type: AWS::EC2::NetworkAclEntry
     Properties:
       CidrBlock: 10.0.0.0/16
       Icmp: !Ref AWS::NoValue
-      NetworkAclId: '1'
+      NetworkAclId: "1"
       RuleNumber: 1
       Protocol: 1
       RuleAction: allow
@@ -215,7 +208,7 @@ Resources:
       ImageId: "ami-2f726546"
       InstanceType: t1.micro
       KeyName: testkey
-      BlockDeviceMappings: !Ref 'AWS::NoValue'
+      BlockDeviceMappings: !Ref "AWS::NoValue"
       NetworkInterfaces:
         - DeviceIndex: "1"
 Outputs:
diff --git a/test/fixtures/templates/bad/resources/configuration.yaml b/test/fixtures/templates/bad/resources/configuration.yaml
--- a/test/fixtures/templates/bad/resources/configuration.yaml
+++ b/test/fixtures/templates/bad/resources/configuration.yaml
@@ -1,12 +1,19 @@
 ---
 AWSTemplateFormatVersion: "2010-09-09"
 Conditions:
-  isUsEast1: !Equals [!Ref 'AWS::Region', 'us-east-1']
+  isUsEast1: !Equals [!Ref "AWS::Region", "us-east-1"]
+Parameters:
+  CloudformationType:
+    Type: String
+    Default: AWS::EC2::Instance
+    AllowedValues:
+      - AWS::EC2::Instance
+      - AWS::AutoScaling::LaunchConfiguration
 Resources:
   AWSIAMRole1:
     Type: AWS::IAM::Role
     Condition:
-    - isUsEast1
+      - isUsEast1
     Properties:
       AssumeRolePolicyDocument: {}
   AWSIAMRole2:
@@ -14,3 +21,8 @@ Resources:
     Condition: False
     Properties:
       AssumeRolePolicyDocument: {}
+  EC2InstanceOrLaunchConfig:
+    Type: !Ref CloudformationType
+    Properties:
+      InstanceType: t3.nano
+      ImageId: ami-0b90a8636b6f955c1
diff --git a/test/unit/rules/resources/test_configurations.py b/test/unit/rules/resources/test_configurations.py
--- a/test/unit/rules/resources/test_configurations.py
+++ b/test/unit/rules/resources/test_configurations.py
@@ -21,4 +21,4 @@ def test_file_positive(self):
     def test_file_negative(self):
         """Test failure"""
         self.helper_file_negative('test/fixtures/templates/bad/generic.yaml', 2)
-        self.helper_file_negative('test/fixtures/templates/bad/resources/configuration.yaml', 2)
+        self.helper_file_negative('test/fixtures/templates/bad/resources/configuration.yaml', 3)