Repository: aws-cloudformation/cfn-lint

Issue:
E2503 Security groups are not supported for load balancers with type "network"
*cfn-lint version: (`cfn-lint --version`)*
cfn-lint 0.21.0

*Description of issue.*
Here's a simplified version of our ALB template which reproduces the failure:
```
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Application Load Balancer

Metadata:
  cfn-lint:
    config:
      ignore_checks:
        - W2507 # AWS-Specific parameter types do not allow empty defaults

Parameters:
  Type:
    Type: String
    AllowedValues: ['application', 'network']
    Default: application
    Description: ALB type
  SecurityGroup:
    Type: CommaDelimitedList
    Description: Security group(s) for ALB
    Default: ''
  Subnets:
    Type: List<AWS::EC2::Subnet::Id>
    Description: List of subnets to use for ALB


Conditions:
  IsTypeNetwork: !Equals [!Ref Type, 'network']

Resources:
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: 1.2.3.4
      Name: MyName
      Scheme: internet-facing
      SecurityGroups: !If
        - IsTypeNetwork
        - !Ref 'AWS::NoValue'
        - !Ref SecurityGroup
      Subnets: !Ref Subnets
      Type: !Ref Type
```

Here's the failure:
```
$ cfn-lint stacks/test-alb.template
E2503 Security groups are not supported for load balancers with type "network"
stacks/test-alb.template:36:7
```

This had been linting fine up until today (no recent changes to the template), so I think it may been introduced in the latest release: https://github.com/aws-cloudformation/cfn-python-lint/blob/master/CHANGELOG.md#0210


Discussion:
Hi, 

This is introduced on [0.17.1](https://github.com/aws-cloudformation/cfn-python-lint/blob/master/CHANGELOG.md#0171)

Aparantly conditionals are not extracted correctly?
We did touch this rule with the attribute checks.  While the changes were unrelated to this specific part of the check something else must have changed that caused us to start having this issue.

The real fix here is to get things working with conditions.  I'm going to rework a few things to get that done.  

Resolving patch:
diff --git a/src/cfnlint/rules/resources/elb/Elb.py b/src/cfnlint/rules/resources/elb/Elb.py
--- a/src/cfnlint/rules/resources/elb/Elb.py
+++ b/src/cfnlint/rules/resources/elb/Elb.py
@@ -28,6 +28,11 @@ class Elb(CloudFormationLintRule):
     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-elb-listener.html'
     tags = ['properties', 'elb']
 
+    def __init__(self):
+        """ Init """
+        super(Elb, self).__init__()
+        self.resource_property_types = ['AWS::ElasticLoadBalancingV2::LoadBalancer']
+
     def check_protocol_value(self, value, path, **kwargs):
         """
             Check Protocol Value
@@ -44,31 +49,41 @@ def check_protocol_value(self, value, path, **kwargs):
 
         return matches
 
-    def is_application_loadbalancer(self, properties):
+    def get_loadbalancer_type(self, properties):
         """ Check if type is application """
-        elb_type = properties.get('Type')
-        if not elb_type or elb_type == 'application':
-            return True
-        return False
-
-    def check_alb_subnets(self, properties, path):
+        elb_type = properties.get('Type', 'application')
+        if isinstance(elb_type, six.string_types):
+            if elb_type == 'application':
+                return 'application'
+            return 'network'
+        return None
+
+    def check_alb_subnets(self, properties, path, scenario):
         """ Validate at least two subnets with ALBs"""
         matches = []
-        if self.is_application_loadbalancer(properties):
+        if self.get_loadbalancer_type(properties) == 'application':
             subnets = properties.get('Subnets')
             if isinstance(subnets, list):
                 if len(subnets) < 2:
-                    path = path + ['Subnets']
-                    matches.append(RuleMatch(path, 'You must specify at least two Subnets for load balancers with type "application"'))
+                    if scenario:
+                        message = 'You must specify at least two Subnets for load balancers with type "application" {0}'
+                        scenario_text = ' and '.join(['when condition "%s" is %s' % (k, v) for (k, v) in scenario.items()])
+                        matches.append(RuleMatch(path, message.format(scenario_text)))
+                    else:
+                        matches.append(RuleMatch(path[:] + ['Subnets'], 'You must specify at least two Subnets for load balancers with type "application"'))
             subnet_mappings = properties.get('SubnetMappings')
             if isinstance(subnet_mappings, list):
                 if len(subnet_mappings) < 2:
-                    path = path + ['SubnetMappings']
-                    matches.append(RuleMatch(path, 'You must specify at least two SubnetMappings for load balancers with type "application"'))
+                    if scenario:
+                        message = 'You must specify at least two SubnetMappings for load balancers with type "application" {0}'
+                        scenario_text = ' and '.join(['when condition "%s" is %s' % (k, v) for (k, v) in scenario.items()])
+                        matches.append(RuleMatch(path, message.format(scenario_text)))
+                    else:
+                        matches.append(RuleMatch(path[:] + ['SubnetMappings'], 'You must specify at least two SubnetMappings for load balancers with type "application"'))
 
         return matches
 
-    def check_loadbalancer_allowed_attributes(self, properties, path):
+    def check_loadbalancer_allowed_attributes(self, properties, path, scenario):
         """ Validate loadbalancer attributes per loadbalancer type"""
         matches = []
 
@@ -93,13 +108,17 @@ def check_loadbalancer_allowed_attributes(self, properties, path):
             for item in loadbalancer_attributes:
                 key = item.get('Key')
                 value = item.get('Value')
-                if isinstance(key, six.string_types) and isinstance(value, (six.string_types, bool, int)):
-                    loadbalancer = 'network'
-                    if self.is_application_loadbalancer(properties):
-                        loadbalancer = 'application'
-                    if key not in allowed_attributes['all'] and key not in allowed_attributes[loadbalancer]:
-                        message = 'Attribute "{0}" not allowed for load balancers with type "{1}"'
-                        matches.append(RuleMatch(path, message.format(key, loadbalancer)))
+                if isinstance(key, six.string_types) and isinstance(value, (six.string_types, bool, six.integer_types)):
+                    loadbalancer = self.get_loadbalancer_type(properties)
+                    if loadbalancer:
+                        if key not in allowed_attributes['all'] and key not in allowed_attributes[loadbalancer]:
+                            if scenario:
+                                scenario_text = ' and '.join(['when condition "%s" is %s' % (k, v) for (k, v) in scenario.items()])
+                                message = 'Attribute "{0}" not allowed for load balancers with type "{1}" {2}'
+                                matches.append(RuleMatch(path, message.format(key, loadbalancer, scenario_text)))
+                            else:
+                                message = 'Attribute "{0}" not allowed for load balancers with type "{1}"'
+                                matches.append(RuleMatch(path, message.format(key, loadbalancer)))
 
         return matches
 
@@ -130,15 +149,27 @@ def match(self, cfn):
                             certificate_protocols=['HTTPS', 'SSL'],
                             certificates=listener.get('SSLCertificateId')))
 
-        results = cfn.get_resource_properties(['AWS::ElasticLoadBalancingV2::LoadBalancer'])
-        for result in results:
-            properties = result['Value']
-            if not self.is_application_loadbalancer(properties):
-                if 'SecurityGroups' in properties:
-                    path = result['Path'] + ['SecurityGroups']
-                    matches.append(RuleMatch(path, 'Security groups are not supported for load balancers with type "network"'))
-
-            matches.extend(self.check_alb_subnets(properties, result['Path']))
-            matches.extend(self.check_loadbalancer_allowed_attributes(properties, result['Path']))
+        return matches
+
+    def match_resource_properties(self, resource_properties, _, path, cfn):
+        """ Check Load Balancers """
+        matches = []
+
+        # Play out conditions to determine the relationship between property values
+        scenarios = cfn.get_object_without_nested_conditions(resource_properties, path)
+        for scenario in scenarios:
+            properties = scenario.get('Object')
+            if self.get_loadbalancer_type(properties) == 'network':
+                if properties.get('SecurityGroups'):
+                    if scenario.get('Scenario'):
+                        scenario_text = ' and '.join(['when condition "%s" is %s' % (k, v) for (k, v) in scenario.get('Scenario').items()])
+                        message = 'Security groups are not supported for load balancers with type "network" {0}'
+                        matches.append(RuleMatch(path, message.format(scenario_text)))
+                    else:
+                        path = path + ['SecurityGroups']
+                        matches.append(RuleMatch(path, 'Security groups are not supported for load balancers with type "network"'))
+
+            matches.extend(self.check_alb_subnets(properties, path, scenario.get('Scenario')))
+            matches.extend(self.check_loadbalancer_allowed_attributes(properties, path, scenario.get('Scenario')))
 
         return matches


Test patch:
diff --git a/test/fixtures/templates/bad/properties_elb.yaml b/test/fixtures/templates/bad/resources/elb/properties.yaml
similarity index 90%
rename from test/fixtures/templates/bad/properties_elb.yaml
rename to test/fixtures/templates/bad/resources/elb/properties.yaml
--- a/test/fixtures/templates/bad/properties_elb.yaml
+++ b/test/fixtures/templates/bad/resources/elb/properties.yaml
@@ -20,8 +20,18 @@ Parameters:
   TestParam:
     Type: String
     Default: TCP
+  Type:
+    Type: String
+    AllowedValues: ['application', 'network']
+    Default: application
+    Description: ALB type
+  SecurityGroups:
+    Type: CommaDelimitedList
+    Description: Security group(s) for ALB
+    Default: ''
 Conditions:
   TestCond: !Equals ['a', 'a']
+  IsTypeNetwork: !Equals [!Ref Type, 'network']
 Resources:
   LoadBalancer:
     Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
@@ -192,6 +202,23 @@ Resources:
           Value: 'true'
         InstancePorts:
         - '80'
+  ElasticLoadBalancerConditions:
+    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
+    Properties:
+      Scheme: internet-facing
+      SecurityGroups: !If
+        - IsTypeNetwork
+        - !Ref SecurityGroups
+        - !Ref 'AWS::NoValue'
+      Subnets:
+      - subnet-123456
+      Type: !If
+        - IsTypeNetwork
+        - network
+        - application
+      LoadBalancerAttributes:
+      - Key: load_balancing.cross_zone.enabled
+        Value: 'true'
 Outputs:
   Arn:
     Value: !Ref LoadBalancer
diff --git a/test/fixtures/templates/good/properties_elb.yaml b/test/fixtures/templates/good/resources/elb/properties.yaml
similarity index 100%
rename from test/fixtures/templates/good/properties_elb.yaml
rename to test/fixtures/templates/good/resources/elb/properties.yaml
diff --git a/test/rules/resources/elb/test_elb.py b/test/rules/resources/elb/test_elb.py
--- a/test/rules/resources/elb/test_elb.py
+++ b/test/rules/resources/elb/test_elb.py
@@ -25,7 +25,7 @@ def setUp(self):
         super(TestPropertyElb, self).setUp()
         self.collection.register(Elb())
         self.success_templates = [
-            'test/fixtures/templates/good/properties_elb.yaml'
+            'test/fixtures/templates/good/resources/elb/properties.yaml'
         ]
 
     def test_file_positive(self):
@@ -34,7 +34,7 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/properties_elb.yaml', 7)
+        self.helper_file_negative('test/fixtures/templates/bad/resources/elb/properties.yaml', 10)
 
     def test_alb_subnets(self):
         """ Test ALB Subnet Logic"""
@@ -47,7 +47,7 @@ def test_alb_subnets(self):
             ]
         }
 
-        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'])
+        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'], {})
         self.assertEqual(len(matches), 1)
 
         # No Failure when 2 subnets defined
@@ -59,7 +59,7 @@ def test_alb_subnets(self):
             ]
         }
 
-        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'])
+        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'], {})
         self.assertEqual(len(matches), 0)
 
         # Failure when 1 SubnetMapping defined
@@ -72,7 +72,7 @@ def test_alb_subnets(self):
             ]
         }
 
-        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'])
+        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'], {})
         self.assertEqual(len(matches), 1)
 
         # No Failure when 2 SubnetMapping defined
@@ -84,7 +84,7 @@ def test_alb_subnets(self):
             ]
         }
 
-        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'])
+        matches = rule.check_alb_subnets(props, ['Resources', 'ALB', 'Properties'], {})
         self.assertEqual(len(matches), 0)
 
         # No Failure when 1 Subnet and NLB
@@ -95,7 +95,7 @@ def test_alb_subnets(self):
             ]
         }
 
-        matches = rule.check_alb_subnets(props, ['Resources', 'NLB', 'Properties'])
+        matches = rule.check_alb_subnets(props, ['Resources', 'NLB', 'Properties'], {})
         self.assertEqual(len(matches), 0)
 
     def test_loadbalancer_attributes(self):
@@ -112,7 +112,7 @@ def test_loadbalancer_attributes(self):
             ]
         }
 
-        matches = rule.check_loadbalancer_allowed_attributes(props, ['Resources', 'NLB', 'Properties'])
+        matches = rule.check_loadbalancer_allowed_attributes(props, ['Resources', 'NLB', 'Properties'], {})
         self.assertEqual(len(matches), 0)
 
         props = {
@@ -128,7 +128,7 @@ def test_loadbalancer_attributes(self):
             ]
         }
 
-        matches = rule.check_loadbalancer_allowed_attributes(props, ['Resources', 'ALB', 'Properties'])
+        matches = rule.check_loadbalancer_allowed_attributes(props, ['Resources', 'ALB', 'Properties'], {})
         self.assertEqual(len(matches), 0)
 
         props = {
@@ -145,8 +145,36 @@ def test_loadbalancer_attributes(self):
             ]
         }
 
-        matches = rule.check_loadbalancer_allowed_attributes(props, ['Resources', 'NLB', 'Properties'])
+        matches = rule.check_loadbalancer_allowed_attributes(props, ['Resources', 'LB', 'Properties'], {})
         self.assertEqual(len(matches), 2)
 
+        props = {
+            "Type": "network"
+        }
+
+        elb_type = rule.get_loadbalancer_type(props)
+        self.assertEqual(elb_type, 'network')
+
+        props = {
+            "Type": "application"
+        }
+
+        elb_type = rule.get_loadbalancer_type(props)
+        self.assertEqual(elb_type, 'application')
+
+        props = {}
+
+        elb_type = rule.get_loadbalancer_type(props)
+        self.assertEqual(elb_type, 'application')
+
+        props = {
+            "Type": {
+                "Ref": "LoadBalancerType"
+            }
+        }
+
+        elb_type = rule.get_loadbalancer_type(props)
+        self.assertEqual(elb_type, None)
+