""" Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 """ from cfnlint.helpers import PSEUDOPARAMS, VALID_PARAMETER_TYPES_LIST from cfnlint.rules import CloudFormationLintRule, RuleMatch class Sub(CloudFormationLintRule): """Check if Sub values are correct""" id = "E1019" shortdesc = "Sub validation of parameters" description = "Making sure the sub function is properly configured" source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html" tags = ["functions", "sub"] def _test_string(self, cfn, sub_string, parameters, tree): """Test if a string has appropriate parameters""" matches = [] string_params = cfn.get_sub_parameters(sub_string) for string_param in string_params: if isinstance(string_param, (str)): matches.extend( self._test_parameter(string_param, cfn, parameters, tree) ) return matches def _get_parameters(self, cfn): """Get all Parameter Names""" results = {} parameters = cfn.template.get("Parameters", {}) if isinstance(parameters, dict): for param_name, param_values in parameters.items(): # This rule isn't here to check the Types but we need # something valid if it doesn't exist if isinstance(param_values, dict): results[param_name] = param_values.get("Type", "String") return results def _test_parameters(self, parameters, cfn, tree): """Check parameters for appropriate configuration""" supported_functions = [ "Fn::Base64", "Fn::FindInMap", "Fn::GetAZs", "Fn::GetAtt", "Fn::If", "Fn::ImportValue", "Fn::Join", "Fn::Select", "Fn::Sub", "Ref", "Fn::ToJsonString", ] matches = [] for parameter_name, parameter_value_obj in parameters.items(): param_tree = tree[:] + [parameter_name] if isinstance(parameter_value_obj, dict): if len(parameter_value_obj) == 1: for key, value in parameter_value_obj.items(): if key not in supported_functions: message = ( "Sub parameter should use a valid function for {0}" ) matches.append( RuleMatch( param_tree, message.format("/".join(map(str, tree))) ) ) elif key in ["Ref"]: matches.extend(self._test_parameter(value, cfn, {}, tree)) elif key in ["Fn::GetAtt"]: if isinstance(value, list): # Only test this if all the items are a string if_all_strings = True for v in value: if not isinstance(v, str): # skip things got too complex if_all_strings = False if if_all_strings: matches.extend( self._test_parameter( ".".join(value), cfn, {}, tree ) ) elif isinstance(value, str): matches.extend( self._test_parameter(value, cfn, {}, tree) ) else: message = "Sub parameter should be an object of 1 for {0}" matches.append( RuleMatch(param_tree, message.format("/".join(map(str, tree)))) ) elif isinstance(parameter_value_obj, list): message = "Sub parameter value should be a string for {0}" matches.append( RuleMatch(param_tree, message.format("/".join(map(str, tree)))) ) return matches def _test_parameter(self, parameter, cfn, parameters, tree): """Test a parameter""" matches = [] get_atts = cfn.get_valid_getatts() valid_params = list(PSEUDOPARAMS) valid_params.extend(cfn.get_resource_names()) template_parameters = self._get_parameters(cfn) for key, _ in parameters.items(): valid_params.append(key) if parameter not in valid_params: found = False if parameter in template_parameters: found = True if template_parameters.get(parameter) in VALID_PARAMETER_TYPES_LIST: message = "Fn::Sub cannot use list {0} at {1}" matches.append( RuleMatch( tree, message.format(parameter, "/".join(map(str, tree))) ) ) for resource, attributes in get_atts.items(): for attribute_name, attribute_values in attributes.items(): if resource == parameter.split(".")[0]: if attribute_name == "*": found = True elif attribute_name == ".".join(parameter.split(".")[1:]): if attribute_values.get("Type") == "List": message = "Fn::Sub cannot use list {0} at {1}" matches.append( RuleMatch( tree, message.format( parameter, "/".join(map(str, tree)) ), ) ) found = True else: if ( attribute_name == parameter.split(".")[1] and attribute_values.get("Type") == "Map" ): found = True if not found: message = "Parameter {0} for Fn::Sub not found at {1}" matches.append( RuleMatch(tree, message.format(parameter, "/".join(map(str, tree)))) ) return matches def match(self, cfn): matches = [] sub_objs = cfn.search_deep_keys("Fn::Sub") for sub_obj in sub_objs: sub_value_obj = sub_obj[-1] tree = sub_obj[:-1] if isinstance(sub_value_obj, str): matches.extend(self._test_string(cfn, sub_value_obj, {}, tree)) elif isinstance(sub_value_obj, list): if len(sub_value_obj) == 2: sub_string = sub_value_obj[0] parameters = sub_value_obj[1] if not isinstance(sub_string, str): message = "Subs first element should be of type string for {0}" matches.append( RuleMatch( tree + [0], message.format("/".join(map(str, tree))) ) ) if not isinstance(parameters, dict): message = "Subs second element should be an object for {0}" matches.append( RuleMatch( tree + [1], message.format("/".join(map(str, tree))) ) ) else: matches.extend( self._test_string(cfn, sub_string, parameters, tree + [0]) ) matches.extend(self._test_parameters(parameters, cfn, tree)) else: message = "Sub should be an array of 2 for {0}" matches.append( RuleMatch(tree, message.format("/".join(map(str, tree)))) ) elif isinstance(sub_value_obj, dict): if len(sub_value_obj) == 1: for key, _ in sub_value_obj.items(): if not key == "Fn::Transform": message = ( "Sub should be a string or array of 2 items for {0}" ) matches.append( RuleMatch( tree, message.format("/".join(map(str, tree))) ) ) else: message = "Sub should be a string or array of 2 items for {0}" matches.append( RuleMatch(tree, message.format("/".join(map(str, tree)))) ) else: message = "Sub should be a string or array of 2 items for {0}" matches.append( RuleMatch(tree, message.format("/".join(map(str, tree)))) ) return matches