Repository: aws-cloudformation/cfn-lint

Issue:
Fn:If argument count not checked
*cfn-lint version:*
0.25.3

*Description of issue.*
cfn-lint does not currently appear to check the number of arguments passed to Fn::If, while there may be cases where this is not practical to check, eg where an existing list is passed.

I believe it would probably be possible to detect the following case, where a list of two elements is passed explicitly.  I would guess that this may also be true for various other intrinsic functions, but have not tested that.

````
Description: "Test - If argument count"
Parameters:
    TestParam:
        Type: String
        Default: Value

Conditions:
    HasParam: !Not [ !Equals [ !Ref TestParam, '' ] ]

Resources:
    SSM:
        Type: AWS::SSM::Parameter
        Condition: HasParam
        Properties:
            Type: String
            Description: "Just a test"
            Name: "/This-Is-Just-A-Test"
            Value: !If
              - HasParam
              - ValueIfTrue
```

 


Discussion:
Agreed.  Should be an easy thing to add.

Resolving patch:
diff --git a/src/cfnlint/__init__.py b/src/cfnlint/__init__.py
--- a/src/cfnlint/__init__.py
+++ b/src/cfnlint/__init__.py
@@ -799,6 +799,7 @@ def get_object_without_conditions(self, obj):
                 ]
         """
         results = []
+
         scenarios = self.get_conditions_scenarios_from_object([obj])
 
         if not isinstance(obj, dict):
@@ -856,9 +857,10 @@ def get_conditions_from_property(value):
                     for k, v in value.items():
                         if k == 'Fn::If':
                             if isinstance(v, list) and len(v) == 3:
-                                results.add(v[0])
-                                results = results.union(get_conditions_from_property(v[1]))
-                                results = results.union(get_conditions_from_property(v[2]))
+                                if isinstance(v[0], six.string_types):
+                                    results.add(v[0])
+                                    results = results.union(get_conditions_from_property(v[1]))
+                                    results = results.union(get_conditions_from_property(v[2]))
             elif isinstance(value, list):
                 for v in value:
                     results = results.union(get_conditions_from_property(v))
diff --git a/src/cfnlint/rules/conditions/Exists.py b/src/cfnlint/rules/conditions/Exists.py
--- a/src/cfnlint/rules/conditions/Exists.py
+++ b/src/cfnlint/rules/conditions/Exists.py
@@ -28,9 +28,8 @@ def match(self, cfn):
         iftrees = cfn.search_deep_keys('Fn::If')
         for iftree in iftrees:
             if isinstance(iftree[-1], list):
-                ref_conditions[iftree[-1][0]] = iftree
-            else:
-                ref_conditions[iftree[-1]] = iftree
+                if isinstance(iftree[-1][0], six.string_types):
+                    ref_conditions[iftree[-1][0]] = iftree
 
         # Get resource's Conditions
         for resource_name, resource_values in cfn.get_resources().items():
diff --git a/src/cfnlint/rules/functions/If.py b/src/cfnlint/rules/functions/If.py
--- a/src/cfnlint/rules/functions/If.py
+++ b/src/cfnlint/rules/functions/If.py
@@ -25,16 +25,23 @@ def match(self, cfn):
 
         # Get the conditions used in the functions
         for iftree in iftrees:
-            if isinstance(iftree[-1], list):
-                if_condition = iftree[-1][0]
+            ifs = iftree[-1]
+            if isinstance(ifs, list):
+                if_condition = ifs[0]
+                if len(ifs) != 3:
+                    message = 'Fn::If must be a list of 3 elements.'
+                    matches.append(RuleMatch(
+                        iftree[:-1], message
+                    ))
+                if not isinstance(if_condition, six.string_types):
+                    message = 'Fn::If first element must be a condition and a string.'
+                    matches.append(RuleMatch(
+                        iftree[:-1] + [0], message
+                    ))
             else:
-                if_condition = iftree[-1]
-
-            if not isinstance(if_condition, six.string_types):
-                message = 'Fn::If first elements must be a condition and a string.'
+                message = 'Fn::If must be a list of 3 elements.'
                 matches.append(RuleMatch(
-                    iftree[:-1] + [0],
-                    message.format(if_condition)
+                    iftree[:-1], message
                 ))
 
         return matches


Test patch:
diff --git a/test/fixtures/templates/bad/functions/if.yaml b/test/fixtures/templates/bad/functions/if.yaml
--- a/test/fixtures/templates/bad/functions/if.yaml
+++ b/test/fixtures/templates/bad/functions/if.yaml
@@ -8,10 +8,22 @@ Parameters:
     Type: String
     Default: isDevelopment
 Conditions:
-  isProduction: !Equals [!Ref myEnvironment, 'prd']
-  isDevelopment: !Equals [!Ref myEnvironment, 'dev']
+  isProduction: !Equals [!Ref myEnvironment, "prd"]
+  isDevelopment: !Equals [!Ref myEnvironment, "dev"]
 Resources:
   myInstance:
     Type: AWS::EC2::Instance
     Properties:
-      ImageId: !If [!Ref myCondition, 'ami-123456', 'ami-abcdef']
+      ImageId: !If [!Ref myCondition, "ami-123456", "ami-abcdef"]
+  myInstance2:
+    Type: AWS::EC2::Instance
+    Properties:
+      ImageId: !If [myCondition, "ami-123456"]
+  myInstance3:
+    Type: AWS::EC2::Instance
+    Properties:
+      ImageId:
+        Fn::If:
+          myCondition:
+            - "ami-123456"
+            - "ami-abcdef"
diff --git a/test/unit/rules/functions/test_if.py b/test/unit/rules/functions/test_if.py
--- a/test/unit/rules/functions/test_if.py
+++ b/test/unit/rules/functions/test_if.py
@@ -20,4 +20,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/functions/if.yaml', 1)
+        self.helper_file_negative('test/fixtures/templates/bad/functions/if.yaml', 3)