""" Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import six from cfnlint import CloudFormationLintRule from cfnlint import 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' ] 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, (six.string_types, 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} doesnt 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 first_key(self, first_key, tree): """ Check the validity of the first key """ matches = [] if isinstance(first_key, (six.string_types, int)): return matches if isinstance(first_key, (dict)): matches.extend(self.check_dict(first_key, tree[:] + [1])) else: message = 'FindInMap first key should be a {0}, string, or int at {1}' matches.append(RuleMatch( tree[:] + [1], message.format( ', '.join(map(str, self.supported_functions)), '/'.join(map(str, tree))))) return matches def second_key(self, second_key, tree): """ Check the validity of the second key """ matches = [] if isinstance(second_key, (six.string_types, int)): return matches if isinstance(second_key, (dict)): matches.extend(self.check_dict(second_key, tree[:] + [2])) else: message = 'FindInMap second key should be a {0}, string, or int at {1}' matches.append(RuleMatch( tree[:] + [2], message.format( ', '.join(map(str, self.supported_functions)), '/'.join(map(str, tree))))) return matches def match(self, cfn): """Check CloudFormation GetAtt""" 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.first_key(first_key, tree)) matches.extend(self.second_key(second_key, tree)) else: message = 'FindInMap is a list with 3 values for {0}' matches.append(RuleMatch( tree[:] + [1], message.format('/'.join(map(str, tree))))) return matches