""" Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 """ import cfnlint.helpers from cfnlint.helpers import RESOURCE_SPECS from cfnlint.rules import CloudFormationLintRule, RuleMatch class ValueRefGetAtt(CloudFormationLintRule): """Check if Resource Properties are correct""" id = "E3008" shortdesc = "Check values of properties for valid Refs and GetAtts" description = "Checks resource properties for Ref and GetAtt values" tags = ["resources", "ref", "getatt"] def initialize(self, cfn): """Initialize the rule""" for resource_type_spec in RESOURCE_SPECS.get(cfn.regions[0]).get( "ResourceTypes" ): self.resource_property_types.append(resource_type_spec) for property_type_spec in RESOURCE_SPECS.get(cfn.regions[0]).get( "PropertyTypes" ): self.resource_sub_property_types.append(property_type_spec) def is_value_a_list(self, path, property_name): """ Determines if the value checked is a list or a value in a list We need to handle conditions in the path that could be nested, etc. ['Resources', 'LoadBalancer', 'Properties', 'Subnets', 'Fn::If', 2, 'Fn::If', 2] Numbers preceeded by a Fn::If should be removed and check repeated. """ if path[-1] != property_name: # Property doesn't match the property name # Check if its a number and a condition if isinstance(path[-1], int) and path[-2] == "Fn::If": return self.is_value_a_list(path[:-2], property_name) return False return True def check_value_ref(self, value, path, **kwargs): """Check Ref""" matches = [] cfn = kwargs.get("cfn") value_specs = kwargs.get("value_specs", {}).get("Ref") list_value_specs = kwargs.get("list_value_specs", {}).get("Ref") property_type = kwargs.get("property_type") property_name = kwargs.get("property_name") if ( path[-1] == "Ref" and property_type == "List" and self.is_value_a_list(path[:-1], property_name) ): specs = list_value_specs else: specs = value_specs if not specs: # If no Ref's are specified, just skip # Opposite of GetAtt you will always have a Ref to a Parameter so if this is # None it just hasn't been defined and we can skip return matches if value in cfn.template.get("Parameters", {}): param = cfn.template.get("Parameters").get(value, {}) parameter_type = param.get("Type") valid_parameter_types = [] for parameter in specs.get("Parameters"): for param_type in ( RESOURCE_SPECS.get(cfn.regions[0]) .get("ParameterTypes") .get(parameter) ): valid_parameter_types.append(param_type) if not specs.get("Parameters"): message = 'Property "{0}" has no valid Refs to Parameters at {1}' matches.append( RuleMatch( path, message.format(property_name, "/".join(map(str, path))) ) ) elif parameter_type not in valid_parameter_types: message = 'Property "{0}" can Ref to parameter of types [{1}] at {2}' matches.append( RuleMatch( path, message.format( property_name, ", ".join(map(str, valid_parameter_types)), "/".join(map(str, path)), ), ) ) if value in cfn.template.get("Resources", {}): resource = cfn.template.get("Resources").get(value, {}) resource_type = resource.get("Type") if not specs.get("Resources"): message = 'Property "{0}" has no valid Refs to Resources at {1}' matches.append( RuleMatch( path, message.format(property_name, "/".join(map(str, path))) ) ) elif resource_type not in specs.get("Resources"): message = 'Property "{0}" can Ref to resources of types [{1}] at {2}' matches.append( RuleMatch( path, message.format( property_name, ", ".join(map(str, specs.get("Resources"))), "/".join(map(str, path)), ), ) ) return matches def check_value_getatt(self, value, path, **kwargs): """Check GetAtt""" matches = [] cfn = kwargs.get("cfn") value_specs = kwargs.get("value_specs", {}).get("GetAtt") list_value_specs = kwargs.get("list_value_specs", {}).get("GetAtt") property_type = kwargs.get("property_type") property_name = kwargs.get("property_name") # You can sometimes get a list or a string with . in it if isinstance(value, list): resource_name = value[0] if len(value[1:]) == 1: resource_attribute = value[1].split(".") else: resource_attribute = value[1:] elif isinstance(value, str): resource_name = value.split(".")[0] resource_attribute = value.split(".")[1:] is_value_a_list = self.is_value_a_list(path[:-1], property_name) if path[-1] == "Fn::GetAtt" and property_type == "List" and is_value_a_list: specs = list_value_specs else: specs = value_specs resource_type = ( cfn.template.get("Resources", {}).get(resource_name, {}).get("Type") ) if cfnlint.helpers.is_custom_resource(resource_type): # A custom resource voids the spec. Move on return matches if ( resource_type in [ "AWS::CloudFormation::Stack", "AWS::ServiceCatalog::CloudFormationProvisionedProduct", ] and resource_attribute[0] == "Outputs" ): # Nested Stack Outputs # if its a string type we are good and return matches # if its a list its a failure as Outputs can only be strings if is_value_a_list and property_type == "List": message = ( "CloudFormation stack outputs need to be strings not lists at {0}" ) matches.append( RuleMatch(path, message.format("/".join(map(str, path)))) ) return matches if specs is None: # GetAtt specs aren't specified skip return matches if not specs: # GetAtt is specified but empty so there are no valid options message = 'Property "{0}" has no valid Fn::GetAtt options at {1}' matches.append( RuleMatch(path, message.format(property_name, "/".join(map(str, path)))) ) return matches if resource_type not in specs: message = ( 'Property "{0}" can Fn::GetAtt to a resource of types [{1}] at {2}' ) matches.append( RuleMatch( path, message.format( property_name, ", ".join(map(str, specs)), "/".join(map(str, path)), ), ) ) elif isinstance(specs[resource_type], list): found = False for allowed_att in specs[resource_type]: if ".".join(map(str, resource_attribute)) == allowed_att: found = True if not found: message = ( 'Property "{0}" can Fn::GetAtt to a resource attribute "{1}" at {2}' ) matches.append( RuleMatch( path, message.format( property_name, specs[resource_type], "/".join(map(str, path)), ), ) ) elif ".".join(map(str, resource_attribute)) != specs[resource_type]: message = ( 'Property "{0}" can Fn::GetAtt to a resource attribute "{1}" at {2}' ) matches.append( RuleMatch( path, message.format( property_name, specs[resource_type], "/".join(map(str, path)) ), ) ) return matches def _get_value_specs(self, value_type, region): value_specs = RESOURCE_SPECS.get(region).get("ValueTypes").get(value_type, {}) if value_specs == "CACHED": value_specs = ( RESOURCE_SPECS.get("us-east-1").get("ValueTypes").get(value_type, {}) ) return value_specs def check(self, cfn, properties, value_specs, property_specs, path): """Check itself""" matches = [] for p_value, p_path in properties.items_safe(path[:]): for prop in p_value: if prop in value_specs: value = value_specs.get(prop).get("Value", {}) if value: value_type = value.get("ValueType", "") list_value_type = value.get("ListValueType", "") property_type = ( property_specs.get("Properties").get(prop).get("Type") ) value_type_specs = self._get_value_specs( value_type, cfn.regions[0] ) list_value_specs = self._get_value_specs( list_value_type, cfn.regions[0] ) matches.extend( cfn.check_value( p_value, prop, p_path, check_ref=self.check_value_ref, check_get_att=self.check_value_getatt, value_specs=value_type_specs, list_value_specs=list_value_specs, cfn=cfn, property_type=property_type, property_name=prop, ) ) return matches def match_resource_sub_properties(self, properties, property_type, path, cfn): """Match for sub properties""" matches = [] specs = ( RESOURCE_SPECS.get(cfn.regions[0]) .get("PropertyTypes") .get(property_type, {}) .get("Properties", {}) ) property_specs = ( RESOURCE_SPECS.get(cfn.regions[0]).get("PropertyTypes").get(property_type) ) matches.extend(self.check(cfn, properties, specs, property_specs, path)) return matches def match_resource_properties(self, properties, resource_type, path, cfn): """Check CloudFormation Properties""" matches = [] specs = ( RESOURCE_SPECS.get(cfn.regions[0]) .get("ResourceTypes") .get(resource_type, {}) .get("Properties", {}) ) resource_specs = ( RESOURCE_SPECS.get(cfn.regions[0]).get("ResourceTypes").get(resource_type) ) matches.extend(self.check(cfn, properties, specs, resource_specs, path)) return matches