Repository: aws-cloudformation/cfn-lint

Issue:
E1024 Fn::Cidr should allow Fn::If that returns valid types
*cfn-lint version: (`0.52.0+`)*

*Description of issue.*

We cut down Subnets from the VPC CIDR by scoping down CIDRs in increasing sizes and using the `Fn::Cidr` function with select to pick the appropriate subnet.

Currently, it looks like `E1024` allows a Cidr Range, Ref, GetAtt, Sub or a Select function, but doesn't allow an If function that returns a valid type. The below syntax is valid in CloudFormation, and because both possible return values are appropriately typed should not return an error.

```yaml
  ExampleSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      # /28 block #1 (VPC Allocation), inside /26 block #3, inside /24 block #3 (prod) or block #15 (non-prod)
      CidrBlock: !Select [ 1, !Cidr [
        !Select [ 3, !Cidr [
          !If [
            IsProd,
            !Select [ 3, !Cidr [ !GetAtt MyVPC.CidrBlock, 4, 8 ]],
            !Select [ 15, !Cidr [ !GetAtt MyVPC.CidrBlock, 16, 8 ]]
          ],
          4, 6
        ]],
        2, 4
      ]]
      VpcId: !Ref MyVPC
```

Tested on cfn-lint versions:
* `0.52.0`
* `0.54.2`



Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/functions/Cidr.py b/src/cfnlint/rules/functions/Cidr.py
--- a/src/cfnlint/rules/functions/Cidr.py
+++ b/src/cfnlint/rules/functions/Cidr.py
@@ -18,6 +18,91 @@ class Cidr(CloudFormationLintRule):
     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-cidr.html'
     tags = ['functions', 'cidr']
 
+    supported_functions = [
+        'Fn::FindInMap',
+        'Fn::Select',
+        'Ref',
+        'Fn::GetAtt',
+        'Fn::Sub',
+        'Fn::ImportValue',
+    ]
+
+    def check_ip_block(self, value, path):
+        matches = []
+        if isinstance(value, dict):
+            if len(value) == 1:
+                for index_key, _ in value.items():
+                    if index_key not in self.supported_functions:
+                        if index_key == 'Fn::If':
+                            if len(value.get('Fn::If')) == 3 and isinstance(value.get('Fn::If'), list):
+                                matches.extend(self.check_ip_block(value.get('Fn::If')[1], path=path[:] + [index_key , 1]))
+                                matches.extend(self.check_ip_block(value.get('Fn::If')[2], path=path[:] + [index_key , 2]))
+                        else:
+                            message = 'Cidr ipBlock should be Cidr Range, Ref, GetAtt, Sub or Select for {0}'
+                            matches.append(RuleMatch(
+                                path, message.format('/'.join(map(str, value)))))
+        elif isinstance(value, (six.text_type, six.string_types)):
+            if not re.match(REGEX_CIDR, value):
+                message = 'Cidr ipBlock should be a Cidr Range based string for {0}'
+                matches.append(RuleMatch(
+                    path, message.format('/'.join(map(str, path)))))
+        else:
+            message = 'Cidr ipBlock should be a string for {0}'
+            matches.append(RuleMatch(
+                path, message.format('/'.join(map(str, path)))))
+
+        return matches
+
+    def check_count(self, value, path):
+        matches = []
+        count_parameters = []
+        if isinstance(value, dict):
+            if len(value) == 1:
+                for index_key, index_value in value.items():
+                    if index_key not in self.supported_functions:
+                        if index_key == 'Fn::If':
+                            if len(value.get('Fn::If')) == 3 and isinstance(value.get('Fn::If'), list):
+                                matches.extend(self.check_count(value.get('Fn::If')[1], path=path[:] + [index_key , 1]))
+                                matches.extend(self.check_count(value.get('Fn::If')[2], path=path[:] + [index_key , 2]))
+                        else:
+                            message = 'Cidr count should be Int, Ref, or Select for {0}'
+                            matches.append(RuleMatch(
+                                path, message.format('/'.join(map(str, path)))))
+                    if index_key == 'Ref':
+                        count_parameters.append(index_value)
+        elif not isinstance(value, six.integer_types):
+            message = 'Cidr count should be a int for {0}'
+            extra_args = {'actual_type': type(value).__name__, 'expected_type': six.integer_types[0].__name__}
+            matches.append(RuleMatch(
+                path, message.format('/'.join(map(str, path))), **extra_args))
+
+        return count_parameters, matches
+
+    def check_size_mask(self, value, path):
+        matches = []
+        size_mask_parameters = []
+        if isinstance(value, dict):
+            if len(value) == 1:
+                for index_key, index_value in value.items():
+                    if index_key not in self.supported_functions:
+                        if index_key == 'Fn::If':
+                            if len(value.get('Fn::If')) == 3 and isinstance(value.get('Fn::If'), list):
+                                matches.extend(self.check_size_mask(value.get('Fn::If')[1], path=path[:] + [index_key , 1]))
+                                matches.extend(self.check_size_mask(value.get('Fn::If')[2], path=path[:] + [index_key , 2]))
+                        else:
+                            message = 'Cidr sizeMask should be Int, Ref, or Select for {0}'
+                            matches.append(RuleMatch(
+                                path, message.format('/'.join(map(str, path)))))
+                    if index_key == 'Ref':
+                        size_mask_parameters.append(index_value)
+        elif not isinstance(value, six.integer_types):
+            message = 'Cidr sizeMask should be a int for {0}'
+            extra_args = {'actual_type': type(value).__name__, 'expected_type': six.integer_types[0].__name__}
+            matches.append(RuleMatch(
+                path, message.format('/'.join(map(str, path))), **extra_args))
+
+        return size_mask_parameters, matches
+
     def check_parameter_count(self, cfn, parameter_name):
         """Check Count Parameter if used"""
         matches = []
@@ -75,15 +160,6 @@ def match(self, cfn):
 
         cidr_objs = cfn.search_deep_keys('Fn::Cidr')
 
-        supported_functions = [
-            'Fn::FindInMap',
-            'Fn::Select',
-            'Ref',
-            'Fn::GetAtt',
-            'Fn::Sub',
-            'Fn::ImportValue'
-        ]
-
         count_parameters = []
         size_mask_parameters = []
 
@@ -99,52 +175,15 @@ def match(self, cfn):
                     else:
                         size_mask_obj = None
 
-                    if isinstance(ip_block_obj, dict):
-                        if len(ip_block_obj) == 1:
-                            for index_key, _ in ip_block_obj.items():
-                                if index_key not in supported_functions:
-                                    message = 'Cidr ipBlock should be Cidr Range, Ref, GetAtt, Sub or Select for {0}'
-                                    matches.append(RuleMatch(
-                                        tree[:] + [0], message.format('/'.join(map(str, tree[:] + [0])))))
-                    elif isinstance(ip_block_obj, (six.text_type, six.string_types)):
-                        if not re.match(REGEX_CIDR, ip_block_obj):
-                            message = 'Cidr ipBlock should be a Cidr Range based string for {0}'
-                            matches.append(RuleMatch(
-                                tree[:] + [0], message.format('/'.join(map(str, tree[:] + [0])))))
-                    else:
-                        message = 'Cidr ipBlock should be a string for {0}'
-                        matches.append(RuleMatch(
-                            tree[:] + [0], message.format('/'.join(map(str, tree[:] + [0])))))
-
-                    if isinstance(count_obj, dict):
-                        if len(count_obj) == 1:
-                            for index_key, index_value in count_obj.items():
-                                if index_key not in supported_functions:
-                                    message = 'Cidr count should be Int, Ref, or Select for {0}'
-                                    matches.append(RuleMatch(
-                                        tree[:] + [1], message.format('/'.join(map(str, tree[:] + [1])))))
-                                if index_key == 'Ref':
-                                    count_parameters.append(index_value)
-                    elif not isinstance(count_obj, six.integer_types):
-                        message = 'Cidr count should be a int for {0}'
-                        extra_args = {'actual_type': type(count_obj).__name__, 'expected_type': six.integer_types[0].__name__}
-                        matches.append(RuleMatch(
-                            tree[:] + [1], message.format('/'.join(map(str, tree[:] + [1]))), **extra_args))
-
-                    if isinstance(size_mask_obj, dict):
-                        if len(size_mask_obj) == 1:
-                            for index_key, index_value in size_mask_obj.items():
-                                if index_key not in supported_functions:
-                                    message = 'Cidr sizeMask should be Int, Ref, or Select for {0}'
-                                    matches.append(RuleMatch(
-                                        tree[:] + [2], message.format('/'.join(map(str, tree[:] + [2])))))
-                                if index_key == 'Ref':
-                                    size_mask_parameters.append(index_value)
-                    elif not isinstance(size_mask_obj, six.integer_types):
-                        message = 'Cidr sizeMask should be a int for {0}'
-                        extra_args = {'actual_type': type(count_obj).__name__, 'expected_type': six.integer_types[0].__name__}
-                        matches.append(RuleMatch(
-                            tree[:] + [2], message.format('/'.join(map(str, tree[:] + [2]))), **extra_args))
+                    matches.extend(self.check_ip_block(ip_block_obj, tree[:] + [0]))
+
+                    new_count_parameters, new_matches = self.check_count(count_obj, tree[:] + [1])
+                    count_parameters.extend(new_count_parameters)
+                    matches.extend(new_matches)
+
+                    new_size_mask_parameters , new_matches = self.check_size_mask(size_mask_obj, tree[:] + [2])
+                    size_mask_parameters.extend(new_size_mask_parameters)
+                    matches.extend(new_matches)
 
                 else:
                     message = 'Cidr should be a list of 2 or 3 elements for {0}'


Test patch:
diff --git a/test/fixtures/templates/bad/functions_cidr.yaml b/test/fixtures/templates/bad/functions_cidr.yaml
--- a/test/fixtures/templates/bad/functions_cidr.yaml
+++ b/test/fixtures/templates/bad/functions_cidr.yaml
@@ -1,58 +1,106 @@
 AWSTemplateFormatVersion: 2010-09-09
 Parameters:
-    cidrBlock :
-        Type: String
-    count:
-        Type: String
-    count1:
-        Type: Number
-        MinValue: 0
-        MaxValue: 3000
-    maskSizeForIPv4:
-        Type: String
-    maskSizeForIPv6:
-        Type: String
-    maskSizeForIPv41:
-        Type: Number
-        MinValue: 0
-        MaxValue: 750
+  cidrBlock:
+    Type: String
+  count:
+    Type: String
+  count1:
+    Type: Number
+    MinValue: 0
+    MaxValue: 3000
+  maskSizeForIPv4:
+    Type: String
+  maskSizeForIPv6:
+    Type: String
+  maskSizeForIPv41:
+    Type: Number
+    MinValue: 0
+    MaxValue: 750
+Conditions:
+  IsProd: !Equals [!Ref AWS::AccountId, '123456789012']
 Resources:
-    Subnet1:
-        Type: AWS::EC2::Subnet
-        Properties:
-            AssignIpv6AddressOnCreation: true
-            VpcId: !Ref VPC
-            #Test Ipv4 CidrBlock
-            CidrBlock: !Select [0, !Cidr [!Ref cidrBlock, 'a', '/32']]
-            #Test Ipv6 CidrBlock
-            Ipv6CidrBlock: !Select [0, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], !Ref count, !Ref maskSizeForIPv6]]
-        DependsOn: Ipv6VPCCidrBlock
-    Subnet2:
-        Type: AWS::EC2::Subnet
-        Properties:
-            AssignIpv6AddressOnCreation: true
-            VpcId: !Ref VPC
-            #Test Ipv4 CidrBlock
-            CidrBlock: !Select [1, !Cidr [!Ref cidrBlock, !Ref count1, !Ref maskSizeForIPv41]]
-            #Test Ipv6 CidrBlock
-            Ipv6CidrBlock: !Select [1, !Cidr [!FindInMap [0, !GetAtt VPC.Ipv6CidrBlocks], !Ref count, !Ref maskSizeForIPv6]]
-        DependsOn: Ipv6VPCCidrBlock
-    Subnet3:
-        Type: AWS::EC2::Subnet
-        Properties:
-            AssignIpv6AddressOnCreation: true
-            VpcId: !Ref VPC
-            #Test Ipv4 CidrBlock
-            CidrBlock: !Select [0, !Cidr [!Ref cidrBlock, !Ref count, !Ref maskSizeForIPv4]]
-            #Test Ipv6 CidrBlock
-            Ipv6CidrBlock: !Select [0, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], !Ref count, !Ref maskSizeForIPv6]]
-        DependsOn: Ipv6VPCCidrBlock
-    VPC:
-        Type: AWS::EC2::VPC
-        Properties:
-          CidrBlock: !Ref cidrBlock
-    Ipv6VPCCidrBlock:
-        Type: AWS::EC2::VPCCidrBlock
-        Properties:
-            AmazonProvidedIpv6CidrBlock: true
-            VpcId: !Ref VPC
+  Subnet1:
+    Type: AWS::EC2::Subnet
+    Properties:
+      AssignIpv6AddressOnCreation: true
+      VpcId: !Ref VPC
+      #Test Ipv4 CidrBlock
+      CidrBlock: !Select [0, !Cidr [!Ref cidrBlock, "a", "/32"]]
+      #Test Ipv6 CidrBlock
+      Ipv6CidrBlock:
+        !Select [
+          0,
+          !Cidr [
+            !Select [0, !GetAtt VPC.Ipv6CidrBlocks],
+            !Ref count,
+            !Ref maskSizeForIPv6,
+          ],
+        ]
+    DependsOn: Ipv6VPCCidrBlock
+  Subnet2:
+    Type: AWS::EC2::Subnet
+    Properties:
+      AssignIpv6AddressOnCreation: true
+      VpcId: !Ref VPC
+      #Test Ipv4 CidrBlock
+      CidrBlock:
+        !Select [1, !Cidr [!Ref cidrBlock, !Ref count1, !Ref maskSizeForIPv41]]
+      #Test Ipv6 CidrBlock
+      Ipv6CidrBlock:
+        !Select [
+          1,
+          !Cidr [
+            !FindInMap [0, !GetAtt VPC.Ipv6CidrBlocks],
+            !Ref count,
+            !Ref maskSizeForIPv6,
+          ],
+        ]
+    DependsOn: Ipv6VPCCidrBlock
+  Subnet3:
+    Type: AWS::EC2::Subnet
+    Properties:
+      AssignIpv6AddressOnCreation: true
+      VpcId: !Ref VPC
+      #Test Ipv4 CidrBlock
+      CidrBlock:
+        !Select [0, !Cidr [!Ref cidrBlock, !Ref count, !Ref maskSizeForIPv4]]
+      #Test Ipv6 CidrBlock
+      Ipv6CidrBlock:
+        !Select [
+          0,
+          !Cidr [
+            !Select [0, !GetAtt VPC.Ipv6CidrBlocks],
+            !Ref count,
+            !Ref maskSizeForIPv6,
+          ],
+        ]
+    DependsOn: Ipv6VPCCidrBlock
+  VPC:
+    Type: AWS::EC2::VPC
+    Properties:
+      CidrBlock: !Ref cidrBlock
+  Ipv6VPCCidrBlock:
+    Type: AWS::EC2::VPCCidrBlock
+    Properties:
+      AmazonProvidedIpv6CidrBlock: true
+      VpcId: !Ref VPC
+  SubnetWithConditions:
+    Type: AWS::EC2::Subnet
+    Properties:
+      # /28 block #1 (VPC Allocation), inside /26 block #3, inside /24 block #3 (prod) or block #15 (non-prod)
+      CidrBlock:
+        Fn::Select:
+          - 1
+          - Fn::Cidr:
+              - Fn::Select:
+                  - 3
+                  - Fn::Cidr:
+                      - Fn::If:
+                        - IsProd
+                        - "a"
+                        - "b"
+                      - 4
+                      - 6
+              - 2
+              - 4
+      VpcId: "vpc-123456"
diff --git a/test/fixtures/templates/good/functions/cidr.yaml b/test/fixtures/templates/good/functions/cidr.yaml
--- a/test/fixtures/templates/good/functions/cidr.yaml
+++ b/test/fixtures/templates/good/functions/cidr.yaml
@@ -17,6 +17,8 @@ Mappings:
   CidrBitsMap:
     "2":
       AZCidrBits: 16
+Conditions:
+  IsProd: !Equals [!Ref AWS::AccountId, '123456789012']
 Resources:
   Subnet:
     Type: 'AWS::EC2::Subnet'
@@ -28,3 +30,37 @@ Resources:
     Properties:
       VpcId: 'vpc-123456'
       CidrBlock: !Select [ 2, !Cidr [ {'Fn::ImportValue': 'vpc-CidrBlock'}, 4, !FindInMap [CidrBitsMap, !Ref Size, AZCidrBits] ]]
+  MyVPC:
+    Type: AWS::EC2::VPC
+    Properties:
+      CidrBlock: 10.0.0.0/16
+  SubnetWithConditions:
+      Type: AWS::EC2::Subnet
+      Properties:
+        # /28 block #1 (VPC Allocation), inside /26 block #3, inside /24 block #3 (prod) or block #15 (non-prod)
+        CidrBlock:
+          Fn::Select:
+          - 1
+          - Fn::Cidr:
+            - Fn::Select:
+              - 3
+              - Fn::Cidr:
+                - Fn::If:
+                  - IsProd
+                  - Fn::Select:
+                    - 3
+                    - Fn::Cidr:
+                      - Fn::GetAtt: MyVPC.CidrBlock
+                      - 4
+                      - 8
+                  - Fn::Select:
+                    - 15
+                    - Fn::Cidr: 
+                      - Fn::GetAtt: MyVPC.CidrBlock
+                      - 16
+                      - 8
+                - 4
+                - 6
+            - 2
+            - 4
+        VpcId: 'vpc-123456'
diff --git a/test/unit/rules/functions/test_cidr.py b/test/unit/rules/functions/test_cidr.py
--- a/test/unit/rules/functions/test_cidr.py
+++ b/test/unit/rules/functions/test_cidr.py
@@ -27,4 +27,4 @@ def test_file_positive_extra(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/functions_cidr.yaml', 9)
+        self.helper_file_negative('test/fixtures/templates/bad/functions_cidr.yaml', 11)