""" Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 """ import os from cfnlint.decode import decode from cfnlint.rules import CloudFormationLintRule, RuleMatch class NestedStackParameters(CloudFormationLintRule): """Check that nested stack parameters are specified as needed""" id = "E3043" shortdesc = "Validate parameters for in a nested stack" description = ( "Evalute if parameters for a nested stack are specified and " "if parameters are specified for a nested stack that aren't required." ) source_url = "https://github.com/awslabs/cfn-python-lint" tags = ["resources", "cloudformation"] def __init__(self): """Init""" super().__init__() self.resource_property_types.append("AWS::CloudFormation::Stack") def __get_template_parameters(self, filename): try: (tmp, matches) = decode(filename) except: # pylint: disable=bare-except return None if matches: return None return tmp.get("Parameters", {}) def __compare_objects(self, template_parameters, nested_parameters, scenario, path): matches = [] for key in set(template_parameters.keys()) - set(nested_parameters.keys()): if scenario is None: message = 'Specified parameter "{0}" doesn\'t exist in nested stack template at {1}' matches.append( RuleMatch( path + [key], message.format(key, "/".join(map(str, path + [key]))), ) ) else: message = 'Specified parameter "{0}" doesn\'t exist in nested stack template {1}' scenario_text = " and ".join( [f'when condition "{k}" is {v}' for (k, v) in scenario.items()] ) matches.append(RuleMatch(path, message.format(key, scenario_text))) for key in set(nested_parameters.keys()) - set(template_parameters.keys()): if nested_parameters.get(key).get("Default") is None: if scenario is None: message = ( 'Nested stack template parameter "{0}" is not specified at {1}' ) matches.append( RuleMatch(path, message.format(key, "/".join(map(str, path)))) ) else: message = ( 'Nested stack template parameter "{0}" is not specified {1}' ) scenario_text = " and ".join( [f'when condition "{k}" is {v}' for (k, v) in scenario.items()] ) matches.append(RuleMatch(path, message.format(key, scenario_text))) return matches def match_resource_properties(self, properties, _, path, cfn): """Check CloudFormation Properties""" matches = [] # when template is passed via cat (or equivalent filename is none) if cfn.filename: base_dir = os.path.dirname(os.path.abspath(cfn.filename)) parameter_groups = cfn.get_object_without_conditions( obj=properties, property_names=["TemplateURL", "Parameters"] ) for parameter_group in parameter_groups: obj = parameter_group.get("Object") template_url = obj.get("TemplateURL") if isinstance(template_url, str): if not ( template_url.startswith("http://") or template_url.startswith("https://") or template_url.startswith("s3://") ): template_path = os.path.normpath( os.path.join(base_dir, template_url) ) nested_parameters = self.__get_template_parameters( template_path ) template_parameters = obj.get("Parameters") if isinstance(nested_parameters, dict) and isinstance( template_parameters, dict ): matches.extend( self.__compare_objects( template_parameters=template_parameters, nested_parameters=nested_parameters, path=path + ["Parameters"], scenario=parameter_group.get("Scenario"), ) ) return matches