Repository: aws-cloudformation/cfn-lint

Issue:
Since 0.76.0 Update Template Takes 30+ Minutes or fails to finish linting
### CloudFormation Lint Version

0.56.0, 0.76.0, 0.79.10

### What operating system are you using?

amazon linux container, image: aws/codebuild/amazonlinux2-x86_64-standard:5.0

### Describe the bug

After altering a template used for EC2 code modules we ran into an issue where our CodePipeline lint failed to complete linting the template. I had been able to successfully lint the template on my local PC but after extensive testing I was able to find that my local cfn-lint package had not been updated since 0.56.0 (this version was linting quickly and properly evaluating code it seems). The version used in the pipeline lint was originally somewhere in the 0.78 area but I updated the process to use the most recent 0.79.10 version. I also updated my local system to 0.79.10 and both exhibit the same behavior which is that the more conditions are in this particular template the longer the amount of time it takes to execute. It seems exponential as the first condition is instantaneous and subsequent ones are longer and longer.

After testing all of the releases listed on GitHub I determined the issue started in 0.76.0 which seems related to the changes in this PR: 

Interestingly the old template was longer and had the same references to the same conditions. In the previous version the additional disks for the EC2 instance were all external EBS volumes with volume attachments, each using the same condition each of these if statements uses. The amount of conditions and the top of the template is unchanged from that original.

tl;dr: A template that used to lint in 1.5 seconds now doesn't complete or completes in around 30 minutes.

### Expected behavior

To complete linting in a reasonable time frame for supported configurations and properly lint the values inside the conditions. In this case I would prefer under 1 minute of execution but in versions of cfn-lint before 0.76.0 it completes in about 1.5 seconds and properly lints the values inside of the conditions.

### Reproduction template

#CORP::EC2::EC2::MODULE
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  name:
    Description: The name tag for the EC2 instance.
    Type: String
    Default: ""
  imageId:
    Description: Base AMI for the EC2 instance.
    Type: AWS::EC2::Image::Id
    Default: ""
  instanceType:
    Description: EC2 instance type.
    Type: String
    Default: r5.large
  subnetId:
    Description: The ID of the subnet to launch the instance into.
    Type: String
    Default: ""
  osVolumeSize:
    Description: Size of the volume that has the OS.
    Type: Number
    Default: 120
  securityGroupIds:
    Description: The IDs of the security groups.
    Type: List<AWS::EC2::SecurityGroup::Id>
    Default: ""
  instanceKeyName:
    Type: String
    Description: Key pair name
    Default: kpnamegoeshere
  ebsOptimized:
    Type: String
    Description: Indicates whether the instance is optimized for Amazon EBS I/O.
    Default: false
  numberOfAdditionalVolumes:
    Description: Additional volumes in addition to OS volume. Max is 25.
    Type: Number
    MaxValue: 25
    Default: 0
  volume2Size:
    Description: Size of volume 2
    Type: Number
    Default: 40
  volume3Size:
    Description: Size of volume 3
    Type: Number
    Default: 40
  volume4Size:
    Description: Size of volume 4
    Type: Number
    Default: 40
  volume5Size:
    Description: Size of volume 5
    Type: Number
    Default: 40
  volume6Size:
    Description: Size of volume 6
    Type: Number
    Default: 40
  volume7Size:
    Description: Size of volume 7
    Type: Number
    Default: 40
  volume8Size:
    Description: Size of volume 8
    Type: Number
    Default: 40
  volume9Size:
    Description: Size of volume 9
    Type: Number
    Default: 40
  volume10Size:
    Description: Size of volume 10
    Type: Number
    Default: 40
  volume11Size:
    Description: Size of volume 11
    Type: Number
    Default: 40
  volume12Size:
    Description: Size of volume 12
    Type: Number
    Default: 40
  volume13Size:
    Description: Size of volume 13
    Type: Number
    Default: 40
  volume14Size:
    Description: Size of volume 14
    Type: Number
    Default: 40
  volume15Size:
    Description: Size of volume 15
    Type: Number
    Default: 40
  volume16Size:
    Description: Size of volume 16
    Type: Number
    Default: 40
  volume17Size:
    Description: Size of volume 17
    Type: Number
    Default: 40
  volume18Size:
    Description: Size of volume 18
    Type: Number
    Default: 40
  volume19Size:
    Description: Size of volume 19
    Type: Number
    Default: 40
  volume20Size:
    Description: Size of volume 20
    Type: Number
    Default: 40
  volume21Size:
    Description: Size of volume 21
    Type: Number
    Default: 40
  volume22Size:
    Description: Size of volume 22
    Type: Number
    Default: 40
  volume23Size:
    Description: Size of volume 23
    Type: Number
    Default: 40
  volume24Size:
    Description: Size of volume 24
    Type: Number
    Default: 40
  volume25Size:
    Description: Size of volume 25
    Type: Number
    Default: 40
  volume26Size:
    Description: Size of volume 26
    Type: Number
    Default: 40
Conditions:
  hasVolume26: !Equals
    - !Ref numberOfAdditionalVolumes
    - 25
  hasVolume25: !Or
    - Condition: hasVolume26
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 24
  hasVolume24: !Or
    - Condition: hasVolume25
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 23
  hasVolume23: !Or
    - Condition: hasVolume24
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 22
  hasVolume22: !Or
    - Condition: hasVolume23
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 21
  hasVolume21: !Or
    - Condition: hasVolume22
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 20
  hasVolume20: !Or
    - Condition: hasVolume21
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 19
  hasVolume19: !Or
    - Condition: hasVolume20
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 18
  hasVolume18: !Or
    - Condition: hasVolume19
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 17
  hasVolume17: !Or
    - Condition: hasVolume18
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 16
  hasVolume16: !Or
    - Condition: hasVolume17
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 15
  hasVolume15: !Or
    - Condition: hasVolume16
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 14
  hasVolume14: !Or
    - Condition: hasVolume15
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 13
  hasVolume13: !Or
    - Condition: hasVolume14
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 12
  hasVolume12: !Or
    - Condition: hasVolume13
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 11
  hasVolume11: !Or
    - Condition: hasVolume12
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 10
  hasVolume10: !Or
    - Condition: hasVolume11
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 9
  hasVolume9: !Or
    - Condition: hasVolume10
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 8
  hasVolume8: !Or
    - Condition: hasVolume9
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 7
  hasVolume7: !Or
    - Condition: hasVolume8
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 6
  hasVolume6: !Or
    - Condition: hasVolume7
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 5
  hasVolume5: !Or
    - Condition: hasVolume6
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 4
  hasVolume4: !Or
    - Condition: hasVolume5
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 3
  hasVolume3: !Or
    - Condition: hasVolume4
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 2
  hasVolume2: !Or
    - Condition: hasVolume3
    - !Equals
      - !Ref numberOfAdditionalVolumes
      - 1
Resources:
  Instance:
    Type: AWS::EC2::Instance
    Properties:
      DisableApiTermination: true
      ImageId: !Ref imageId
      InstanceType: !Ref instanceType
      KeyName: !Ref instanceKeyName
      IamInstanceProfile: ssmEc2DelegatedRole
      Tenancy: default
      SubnetId: !Ref subnetId
      EbsOptimized: !Ref ebsOptimized
      Tags:
      - Key: Name
        Value: !Ref name
      SecurityGroupIds: !Ref securityGroupIds
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            Encrypted: true
            VolumeSize: !Ref osVolumeSize
            VolumeType: gp3
        - !If
          - hasVolume2
          - DeviceName: /dev/sdb
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume2Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume3
          - DeviceName: /dev/sdc
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume3Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume4
          - DeviceName: /dev/sdd
            Ebs:
              Encrypted: true
              VolumeasdfSize: !Ref volume4Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume5
          - DeviceName: /dev/sde
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume5Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume6
          - DeviceName: /dev/sdf
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume6Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume7
          - DeviceName: /dev/sdg
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume7Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume8
          - DeviceName: /dev/sdh
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume8Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume9
          - DeviceName: /dev/sdi
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume9Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume10
          - DeviceName: /dev/sdj
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume10Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume11
          - DeviceName: /dev/sdk
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume11Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume12
          - DeviceName: /dev/sdl
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume12Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume13
          - DeviceName: /dev/sdm
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume13Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume14
          - DeviceName: /dev/sdn
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume14Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume15
          - DeviceName: /dev/sdo
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume15Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume16
          - DeviceName: /dev/sdp
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume16Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume17
          - DeviceName: /dev/sdq
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume17Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume18
          - DeviceName: /dev/sdr
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume18Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume19
          - DeviceName: /dev/sds
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume19Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume20
          - DeviceName: /dev/sdt
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume20Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume21
          - DeviceName: /dev/sdu
            Ebs:
              Encrypted: true
              VolumeasdfSize: !Ref volume21Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume22
          - DeviceName: /dev/sdv
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume22Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume23
          - DeviceName: /dev/sdw
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume23Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume24
          - DeviceName: /dev/sdx
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume24Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume25
          - DeviceName: /dev/sdy
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume25Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'
        - !If
          - hasVolume26
          - DeviceName: /dev/sdz
            Ebs:
              Encrypted: true
              VolumeSize: !Ref volume26Size
              VolumeType: gp3
          - !Ref 'AWS::NoValue'


Discussion:
Replicated this.  Looking into possible fixes.

Resolving patch:
diff --git a/src/cfnlint/rules/resources/properties/Exclusive.py b/src/cfnlint/rules/resources/properties/Exclusive.py
--- a/src/cfnlint/rules/resources/properties/Exclusive.py
+++ b/src/cfnlint/rules/resources/properties/Exclusive.py
@@ -32,49 +32,48 @@ def __init__(self):
     def check(self, properties, exclusions, path, cfn):
         """Check itself"""
         matches = []
-
-        property_sets = cfn.get_object_without_conditions(properties)
-        for property_set in property_sets:
-            obj = property_set["Object"].clean()
-            for prop in obj:
-                if prop in exclusions:
-                    for excl_property in exclusions[prop]:
-                        if excl_property in obj:
-                            if property_set["Scenario"] is None:
-                                message = (
-                                    "Property {0} should NOT exist with {1} for {2}"
-                                )
-                                matches.append(
-                                    RuleMatch(
-                                        path + [prop],
-                                        message.format(
-                                            excl_property,
-                                            prop,
-                                            "/".join(map(str, path)),
-                                        ),
-                                    )
-                                )
-                            else:
-                                scenario_text = " and ".join(
-                                    [
-                                        f'when condition "{k}" is {v}'
-                                        for (k, v) in property_set["Scenario"].items()
-                                    ]
-                                )
-                                message = (
-                                    "Property {0} should NOT exist with {1} {2} for {3}"
-                                )
-                                matches.append(
-                                    RuleMatch(
-                                        path + [prop],
-                                        message.format(
-                                            excl_property,
-                                            prop,
-                                            scenario_text,
-                                            "/".join(map(str, path)),
-                                        ),
-                                    )
-                                )
+        for p_value, p_path in properties.items_safe(path[:]):
+            for k, v in exclusions.items():
+                property_sets = cfn.get_object_without_conditions(p_value, [k] + v)
+                for property_set in property_sets:
+                    obj = property_set["Object"].clean()
+                    for prop in obj:
+                        if prop in exclusions:
+                            for excl_property in exclusions[prop]:
+                                if excl_property in obj:
+                                    if property_set["Scenario"] is None:
+                                        message = "Property {0} should NOT exist with {1} for {2}"
+                                        matches.append(
+                                            RuleMatch(
+                                                p_path + [prop],
+                                                message.format(
+                                                    excl_property,
+                                                    prop,
+                                                    "/".join(map(str, p_path)),
+                                                ),
+                                            )
+                                        )
+                                    else:
+                                        scenario_text = " and ".join(
+                                            [
+                                                f'when condition "{k}" is {v}'
+                                                for (k, v) in property_set[
+                                                    "Scenario"
+                                                ].items()
+                                            ]
+                                        )
+                                        message = "Property {0} should NOT exist with {1} {2} for {3}"
+                                        matches.append(
+                                            RuleMatch(
+                                                p_path + [prop],
+                                                message.format(
+                                                    excl_property,
+                                                    prop,
+                                                    scenario_text,
+                                                    "/".join(map(str, p_path)),
+                                                ),
+                                            )
+                                        )
 
         return matches
 


Test patch: