Repository: aws-cloudformation/cfn-lint

Issue:
E0002 Unknown exception while processing rule E2532: can only concatenate list (not "NoneType") to list
*cfn-lint version: 0.7.1

I have an existing SAM template that creates a Step Function amongst other resources. Since installing version 0.7.1 (from 0.6, I believe) it produces this error.

I'm not sure if it is the Step Function JSON that is causing the failure so I've added the complete template.yaml file below.

```
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: 
Globals:
  Function:
    Runtime: python3.6
Parameters:
  EnvironmentType:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - test
      - prod
    Description: 'The environment to create the resource in'
  UserEnv:
    Type: String
    Description: 'Specific to the user, e.g. dev-neil'
  SecurityStack:
    Description: The name of the stack that contains the SNS topics and KMS IDs
    Type: String
  GeneralStack:
    Description: Stack for general resources
    Type: String
Conditions:
  IsProd:
    Fn::Equals:
    - Ref: EnvironmentType
    - prod

Resources:
  Queue:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: eloqua_queue.lambda_handler
      Description: 'Populates a queue of items to process'
      MemorySize: 128
      Timeout: 30
      Role: !GetAtt EloquaRole.Arn
      Environment:
        Variables:
          USERENV:
            Ref: UserEnv
          ENV:
            Ref: EnvironmentType
          START_YYYYMMDD: "today-2"
          END_YYYYMMDD: "today"
      DeadLetterQueue:
        Type: SNS
        TargetArn:
          Fn::ImportValue:
              !Sub "${GeneralStack}:SNSTopic"

  QueueLog:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Join [ "/", [ "", "aws", "lambda", !Ref "Queue" ] ]
      RetentionInDays: 60

  Process:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: eloqua.lambda_handler
      Description: 'Pulls data from Eloqua and pushes it to a file in S3'
      MemorySize: 320
      Timeout: 300
      Role: !GetAtt EloquaRole.Arn
      Environment:
        Variables:
          USERENV:
            Ref: UserEnv
          ENV:
            Ref: EnvironmentType
      DeadLetterQueue:
        Type: SNS
        TargetArn:
          Fn::ImportValue:
              !Sub "${GeneralStack}:SNSTopic"
  ProcessLog:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Join [ "/", [ "", "aws", "lambda", !Ref "Process" ] ]
      RetentionInDays: 60

  ClearQueue:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: clear_queue.lambda_handler
      Description: 'If the step function fails, clear the queue ready for a retry'
      MemorySize: 128
      Timeout: 15
      Role: !GetAtt EloquaRole.Arn
      Environment:
        Variables:
          USERENV:
            Ref: UserEnv
          ENV:
            Ref: EnvironmentType
      DeadLetterQueue:
        Type: SNS
        TargetArn:
          Fn::ImportValue:
              !Sub "${GeneralStack}:SNSTopic"
  ClearQueueLog:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Join [ "/", [ "", "aws", "lambda", !Ref "ClearQueue" ] ]
      RetentionInDays: 60

  ClearS3:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: clear_s3.lambda_handler
      Description: 'If the step function fails, clear the S3 path ready for the retry'
      MemorySize: 128
      Timeout: 15
      Role: !GetAtt EloquaRole.Arn
      Environment:
        Variables:
          USERENV:
            Ref: UserEnv
          ENV:
            Ref: EnvironmentType
      DeadLetterQueue:
        Type: SNS
        TargetArn:
          Fn::ImportValue:
              !Sub "${GeneralStack}:SNSTopic"
  ClearS3Log:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Join [ "/", [ "", "aws", "lambda", !Ref "ClearS3" ] ]
      RetentionInDays: 60

  StepFunctionError:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: step_function_error_mail.lambda_handler
      Description: 'An extra control to email if the step function does not complete as expected'
      MemorySize: 128
      Timeout: 15
      Role: !GetAtt EloquaRole.Arn
      Environment:
        Variables:
          USERENV:
            Ref: UserEnv
          ENV:
            Ref: EnvironmentType
      DeadLetterQueue:
        Type: SNS
        TargetArn:
          Fn::ImportValue:
              !Sub "${GeneralStack}:SNSTopic"
  StepFunctionErrorLog:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Join [ "/", [ "", "aws", "lambda", !Ref "StepFunctionError" ] ]
      RetentionInDays: 60

  EloquaTrigger:
    Type: "AWS::Events::Rule"
    Condition: IsProd
    Properties:
      Description: Runs the Eloqua Step Function
      Name: !Sub "${EnvironmentType}-eloqua-${AWS::Region}"
      ScheduleExpression: "cron(2 2 * * ? *)"
      State: "ENABLED"
      Targets:
        - Arn: !Ref EloquaStepFunction
          Id: EloquaStep1
          RoleArn:
            Fn::ImportValue:
              !Sub "${SecurityStack}:StepFunctionTriggerRoleArn"

  EloquaStepFunction:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      StateMachineName: !Sub "${EnvironmentType}-eloqua"
      RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/service-role/StatesExecutionRole-${AWS::Region}"
      DefinitionString:
        !Sub
          - |-
            {
              "Comment": "Put Eloqua activities and dates on a queue and put the data in S3",
              "StartAt": "eloqua-put-on-queue",
              "States": {
                "eloqua-put-on-queue": {
                  "Type": "Task",
                  "Resource": "${QueueArn}",
                  "Next": "eloqua-get",
                  "Catch": [ {
                    "ErrorEquals": ["States.ALL"],
                    "Next": "clear-s3"
                   } ]
                 },
                "eloqua-get": {
                  "Type": "Task",
                  "Resource": "${ProcessArn}",
                  "Next": "ChoiceStateMessagesOnQueue",
                  "Catch": [ {
                    "ErrorEquals": ["States.ALL"],
                    "Next": "clear-s3"
                  } ]
                },
                "clear-s3": {
                  "Type": "Task",
                  "Resource": "${ClearS3Arn}",
                  "Next": "clear-queue"
                },
                "clear-queue": {
                  "Type": "Task",
                  "Resource": "${ClearQueueArn}",
                  "Next": "send-email-failure"
                },
                "send-email-failure": {
                  "Type": "Task",
                  "Resource": "${StepFunctionErrorArn}",
                  "Next": "finish"
                },
                "finish": {
                  "Type": "Pass",
                  "Result": "Nothing to do",
                  "End": true
                },
                "ChoiceStateMessagesOnQueue": {
                  "Type" : "Choice",
                  "Choices": [
                  {
                    "Variable": "$.messagesonqueue",
                    "StringEquals": "Yes",
                    "Next": "eloqua-get"
                  },
                  {
                    "Variable": "$.messagesonqueue",
                    "StringEquals": "No",
                    "Next": "finish"
                  }
                  ],
                  "Default": "DefaultState"
                },
                "DefaultState": {
                  "Type": "Fail",
                  "Cause": "No Matches!"
                }
              }
            }
          - {QueueArn: !GetAtt [ Queue, Arn ],
             ProcessArn: !GetAtt [ Process, Arn ],
             ClearQueueArn: !GetAtt [ ClearQueue, Arn ],
             ClearS3Arn: !GetAtt [ ClearS3, Arn ],
             StepFunctionErrorArn: !GetAtt [ StepFunctionError, Arn ]}

  EloquaRole:
    Type: "AWS::IAM::Role"
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      RoleName:
        !Sub '${EnvironmentType}-lambda-eloqua-${AWS::Region}'
      Policies:
        -
          PolicyName:
            !Sub '${EnvironmentType}-lambda-eloqua-${AWS::Region}'
          PolicyDocument:
            Version: '2012-10-17' # Policy Document
            Statement:
              -
                Effect: "Allow"
                Action:
                  - "ssm:GetParameters"
                Resource:
                  - !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentType}/eloqua/creds'
              -
                Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "sqs:ListQueues"
                Resource:
                  - "*"
              -
                Effect: "Allow"
                Action:
                  - "s3:List*"
                Resource:
                  - !Sub 'arn:aws:s3:::mybucket-${AWS::Region}-*'
              -
                Effect: "Allow"
                Action:
                  - "s3:Get*"
                  - "s3:Put*"
                  - "s3:DeleteObject"
                Resource:
                  - !Sub 'arn:aws:s3:::mybucket-${AWS::Region}-*/eloqua/*/staging/*'
              -
                Effect: "Allow"
                Action:
                  - "sns:Publish"
                Resource:
                  - !Sub 'arn:aws:sns:${AWS::Region}:${AWS::AccountId}:${EnvironmentType}*'
              -
                Effect: Allow
                Action:
                  - "sqs:CreateQueue"
                  - "sqs:ReceiveMessage"
                  - "sqs:DeleteMessage"
                  - "sqs:GetQueueAttributes"
                  - "sqs:SendMessage"
                  - "sqs:PurgeQueue"
                Resource:
                  - !Sub "arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${UserEnv}-lambda-eloqua-activities.fifo"

Outputs:
  EloquaRole:
    Value: !Ref EloquaRole
    Export:
      Name: !Join [ ":", [ !Ref "AWS::StackName", EloquaRole ] ]
```


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/stepfunctions/StateMachine.py b/src/cfnlint/rules/resources/stepfunctions/StateMachine.py
--- a/src/cfnlint/rules/resources/stepfunctions/StateMachine.py
+++ b/src/cfnlint/rules/resources/stepfunctions/StateMachine.py
@@ -51,7 +51,7 @@ def _check_state_json(self, def_json, state_name, path):
         state_key_types = {
             'Pass': ['Result', 'ResultPath'],
             'Task': ['Resource', 'ResultPath', 'Retry', 'Catch', 'TimeoutSeconds', 'HeartbeatSeconds'],
-            'Choices': ['Choices', 'Default'],
+            'Choice': ['Choices', 'Default'],
             'Wait': ['Seconds', 'Timestamp', 'SecondsPath', 'TimestampPath'],
             'Succeed': [],
             'Fail': ['Cause', 'Error'],
@@ -60,7 +60,7 @@ def _check_state_json(self, def_json, state_name, path):
         state_required_types = {
             'Pass': [],
             'Task': ['Resource'],
-            'Choices': ['Choices'],
+            'Choice': ['Choices'],
             'Wait': [],
             'Succeed': [],
             'Fail': [],
@@ -75,15 +75,19 @@ def _check_state_json(self, def_json, state_name, path):
 
         state_type = def_json.get('Type')
 
-        for state_key, _ in def_json.items():
-            if state_key not in common_state_keys + state_key_types.get(state_type):
-                message = 'State Machine Definition key (%s) for State (%s) of Type (%s) is not valid' % (state_key, state_name, state_type)
-                matches.append(RuleMatch(path, message))
-        for req_key in common_state_required_keys + state_required_types.get(state_type):
-            if req_key not in def_json:
-                message = 'State Machine Definition required key (%s) for State (%s) of Type (%s) is missing' % (req_key, state_name, state_type)
-                matches.append(RuleMatch(path, message))
-                return matches
+        if state_type in state_key_types:
+            for state_key, _ in def_json.items():
+                if state_key not in common_state_keys + state_key_types.get(state_type, []):
+                    message = 'State Machine Definition key (%s) for State (%s) of Type (%s) is not valid' % (state_key, state_name, state_type)
+                    matches.append(RuleMatch(path, message))
+            for req_key in common_state_required_keys + state_required_types.get(state_type, []):
+                if req_key not in def_json:
+                    message = 'State Machine Definition required key (%s) for State (%s) of Type (%s) is missing' % (req_key, state_name, state_type)
+                    matches.append(RuleMatch(path, message))
+                    return matches
+        else:
+            message = 'State Machine Definition Type (%s) is not valid' % (state_type)
+            matches.append(RuleMatch(path, message))
 
         return matches
 


Test patch:
diff --git a/test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml b/test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml
--- a/test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml
+++ b/test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml
@@ -25,13 +25,18 @@ Resources:
       # Missing StartsAt
       # ByeWorld is missing Resource
       # HelloWorld is missing Type
+      # Type doesn't exist
       DefinitionString:
         Fn::Sub: |-
           {
             "States": {
               "HelloWorld": {
                 "Resource": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:HelloFunction",
-                "End": true
+                "Next": "GoodDay"
+              },
+              "GoodDay": {
+                "Type": "DNE",
+                "Next": "ByeWorld"
               },
               "ByeWorld": {
                 "Type": "Task",
diff --git a/test/rules/resources/stepfunctions/test_state_machine.py b/test/rules/resources/stepfunctions/test_state_machine.py
--- a/test/rules/resources/stepfunctions/test_state_machine.py
+++ b/test/rules/resources/stepfunctions/test_state_machine.py
@@ -34,4 +34,4 @@ def test_file_positive(self):
 
     def test_file_negative_alias(self):
         """Test failure"""
-        self.helper_file_negative('fixtures/templates/bad/resources/stepfunctions/state_machine.yaml', 4)
+        self.helper_file_negative('fixtures/templates/bad/resources/stepfunctions/state_machine.yaml', 5)