""" Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 """ from cfnlint.rules import CloudFormationLintRule, RuleMatch class FindInMap(CloudFormationLintRule): """Check if FindInMap values are correct""" id = "E1011" shortdesc = "FindInMap validation of configuration" description = "Making sure the function is a list of appropriate config" source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-findinmap.html" tags = ["functions", "findinmap"] supported_functions = ["Fn::FindInMap", "Ref"] enhanced_supported_functions = [ "Fn::FindInMap", "Ref", "Fn::Select", "Fn::Split", "Fn::Join", "Fn::Sub", "Fn::If", "Fn::Length", "Fn::ToJsonString", "Fn::Sub", ] default_value_key = "DefaultValue" def check_dict(self, obj, tree): """ Check that obj is a dict with Ref as the only key Mappings only support Ref inside them """ matches = [] if isinstance(obj, dict): if len(obj) == 1: for key_name, _ in obj.items(): if key_name not in self.supported_functions: message = "FindInMap only supports [{0}] functions at {1}" matches.append( RuleMatch( tree[:] + [key_name], message.format( ", ".join(map(str, self.supported_functions)), "/".join(map(str, tree)), ), ) ) else: message = ( "FindInMap only supports an object of 1 of [{0}] functions at {1}" ) matches.append( RuleMatch( tree[:], message.format( ", ".join(map(str, self.supported_functions)), "/".join(map(str, tree)), ), ) ) return matches def map_name(self, map_name, mappings, tree): """Check the map name""" matches = [] if isinstance(map_name, (str, dict)): if isinstance(map_name, dict): matches.extend(self.check_dict(map_name, tree[:] + [0])) else: if map_name not in mappings: message = "Map Name {0} does not exist for {0}" matches.append( RuleMatch( tree[:] + [0], message.format(map_name, "/".join(map(str, tree))), ) ) else: message = "Map Name should be a {0}, or string at {1}" matches.append( RuleMatch( tree[:] + [0], message.format( ", ".join(map(str, self.supported_functions)), "/".join(map(str, tree)), ), ) ) return matches def match_key(self, key, tree, key_name, key_index): """Check the validity of a key""" matches = [] if isinstance(key, (str, int)): return matches if isinstance(key, dict): matches.extend(self.check_dict(key, tree[:] + [key_index])) else: message = "FindInMap {0} should be a {1}, string, or int at {2}" matches.append( RuleMatch( tree[:] + [key_index], message.format( key_name, ", ".join(map(str, self.supported_functions)), "/".join(map(str, tree)), ), ) ) return matches def validate_intrinsic_function(self, obj, tree): matches = [] if not isinstance(obj, dict): return matches error_message = ( "FindInMap only supports an object of 1 of [{0}] functions at {1}" ) if len(obj) != 1: matches.append( RuleMatch( tree[:], error_message.format( "/".join(map(str, self.enhanced_supported_functions)), "/".join(map(str, tree)), ), ) ) return matches function_name = next(iter(obj)) if function_name not in self.enhanced_supported_functions: matches.append( RuleMatch( tree[:], error_message.format( "/".join(map(str, self.enhanced_supported_functions)), "/".join(map(str, tree)), ), ) ) return matches def validate_enhanced_map_name(self, map_name, mappings, tree): matches = [] if isinstance(map_name, str): if map_name not in mappings: error_message = "Map Name {0} does not exist for {1}" matches.append( RuleMatch( tree[:] + [0], error_message.format(map_name, "/".join(map(str, tree))), ) ) elif isinstance(map_name, dict): return self.validate_intrinsic_function(map_name, tree) else: error_message = "Map Name should be an intrinsic function that resolves to a string, or a string at {0}" matches.append( RuleMatch(tree[:] + [0], error_message.format("/".join(map(str, tree)))) ) return matches def validate_enhanced_key(self, key, tree, key_name, key_index): matches = [] if isinstance(key, (str, int)): return matches if isinstance(key, dict): return self.validate_intrinsic_function(key, tree) error_message = "FindInMap {0} should be a string, int or an intrinsic function resolves to a string or int at {1}" matches.append( RuleMatch( tree[:] + [key_index], error_message.format(key_name, "/".join(map(str, tree))), ) ) return matches def validate_default_value_shape(self, default_value, tree): matches = [] error_message = "Fn::FindInMap default value must be an object whose key is 'DefaultValue' at {0}" if (not isinstance(default_value, dict)) or (len(default_value) != 1): matches.append( RuleMatch(tree[:], error_message.format("/".join(map(str, tree)))) ) return matches key = next(iter(default_value)) if key != self.default_value_key: matches.append( RuleMatch(tree[:], error_message.format("/".join(map(str, tree)))) ) return matches def validate_enhanced_default_value(self, default_value_dict, tree): matches = [] matches.extend(self.validate_default_value_shape(default_value_dict, tree)) if not isinstance(default_value_dict, dict): return matches if self.default_value_key not in default_value_dict: return matches default_value = default_value_dict[self.default_value_key] if isinstance(default_value, (int, float, str)): return matches if isinstance(default_value, dict): matches.extend(self.validate_intrinsic_function(default_value, tree)) if isinstance(default_value, list): for value in default_value: if isinstance(value, dict): matches.extend(self.validate_intrinsic_function(value, tree)) elif isinstance(value, (int, float, str)): continue else: error_message = "Elements in Fn::FindInMap default value must be a string, number or an intrinsic function resolves to string at {0}" matches.append( RuleMatch( tree[:], error_message.format("/".join(map(str, tree))) ) ) return matches def validate_enhanced_find_in_map(self, find_in_map, cfn): mappings = cfn.get_mappings() tree = find_in_map[:-1] map_obj = find_in_map[-1] matches = [] if not isinstance(map_obj, list): error_message = "FindInMap is a list with 3 or 4 values for {0}" matches.append( RuleMatch(tree[:], error_message.format("/".join(map(str, tree)))) ) return matches if len(map_obj) in (3, 4): map_name = map_obj[0] first_key = map_obj[1] second_key = map_obj[2] matches.extend(self.validate_enhanced_map_name(map_name, mappings, tree)) matches.extend(self.validate_enhanced_key(first_key, tree, "first key", 1)) matches.extend( self.validate_enhanced_key(second_key, tree, "second key", 2) ) if len(map_obj) == 4: default_value = map_obj[3] matches.extend( self.validate_enhanced_default_value(default_value, tree) ) else: error_message = "FindInMap is a list with 3 or 4 values for {0}" matches.append( RuleMatch(tree[:] + [1], error_message.format("/".join(map(str, tree)))) ) return matches def _match_with_language_extensions(self, cfn): matches = [] find_in_maps = cfn.transform_pre["Fn::FindInMap"] for in_map in find_in_maps: matches.extend( self.validate_enhanced_find_in_map(find_in_map=in_map, cfn=cfn) ) return matches def _match(self, cfn): matches = [] findinmaps = cfn.search_deep_keys("Fn::FindInMap") mappings = cfn.get_mappings() for findinmap in findinmaps: tree = findinmap[:-1] map_obj = findinmap[-1] if not isinstance(map_obj, list): message = "FindInMap is a list with 3 values for {0}" matches.append(RuleMatch(tree[:], message.format("/".join(tree)))) continue if len(map_obj) == 3: map_name = map_obj[0] first_key = map_obj[1] second_key = map_obj[2] matches.extend(self.map_name(map_name, mappings, tree)) matches.extend(self.match_key(first_key, tree, "first key", 1)) matches.extend(self.match_key(second_key, tree, "second key", 2)) else: message = "FindInMap is a list with 3 values for {0}" matches.append( RuleMatch(tree[:] + [1], message.format("/".join(map(str, tree)))) ) return matches def match(self, cfn): if cfn.has_language_extensions_transform(): return self._match_with_language_extensions(cfn=cfn) return self._match(cfn=cfn)