""" Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 """ from cfnlint.rules import CloudFormationLintRule, RuleMatch class Elb(CloudFormationLintRule): """Check if Elb Resource Properties""" id = "E2503" shortdesc = "Resource ELB Properties" description = "See if Elb Resource Properties are set correctly \ HTTPS has certificate HTTP has no certificate" source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-elb-listener.html" tags = ["properties", "elb"] def __init__(self): """Init""" super().__init__() self.resource_property_types = ["AWS::ElasticLoadBalancingV2::LoadBalancer"] def check_protocol_value(self, value, path, **kwargs): """ Check Protocol Value """ matches = [] if isinstance(value, str): if value.upper() not in kwargs["accepted_protocols"]: message = "Protocol must be {0} is invalid at {1}" matches.append( RuleMatch( path, message.format( (", ".join(kwargs["accepted_protocols"])), ("/".join(map(str, path))), ), ) ) elif value.upper() in kwargs["certificate_protocols"]: if not kwargs["certificates"]: message = ( "Certificates should be specified when using HTTPS for {0}" ) matches.append( RuleMatch(path, message.format(("/".join(map(str, path))))) ) return matches def get_loadbalancer_type(self, properties): """Check if type is application""" elb_type = properties.get("Type", "application") if isinstance(elb_type, str): 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.get_loadbalancer_type(properties) == "application": subnets = properties.get("Subnets") if isinstance(subnets, list): if len(subnets) < 2: if scenario: message = 'You must specify at least two Subnets for load balancers with type "application" {0}' scenario_text = " and ".join( [ f'when condition "{k}" is {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: if scenario: message = 'You must specify at least two SubnetMappings for load balancers with type "application" {0}' scenario_text = " and ".join( [ f'when condition "{k}" is {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 match(self, cfn): """Check ELB Resource Parameters""" matches = [] results = cfn.get_resource_properties(["AWS::ElasticLoadBalancingV2::Listener"]) for result in results: matches.extend( cfn.check_value( result["Value"], "Protocol", result["Path"], check_value=self.check_protocol_value, accepted_protocols=[ "GENEVE", "HTTP", "HTTPS", "TCP", "TCP_UDP", "TLS", "UDP", ], certificate_protocols=["HTTPS", "TLS"], certificates=result["Value"].get("Certificates"), ) ) results = cfn.get_resource_properties( ["AWS::ElasticLoadBalancing::LoadBalancer", "Listeners"] ) for result in results: if isinstance(result["Value"], list): for index, listener in enumerate(result["Value"]): matches.extend( cfn.check_value( listener, "Protocol", result["Path"] + [index], check_value=self.check_protocol_value, accepted_protocols=["HTTP", "HTTPS", "TCP", "SSL"], certificate_protocols=["HTTPS", "SSL"], certificates=listener.get("SSLCertificateId"), ) ) 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( [ f'when condition "{k}" is {v}' for (k, v) in scenario.get("Scenario").items() ] ) message = f'Security groups are not supported for load balancers with type "network" {scenario_text}' matches.append(RuleMatch(path, message)) 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")) ) return matches