def propertycheck(self, text, proptype, parenttype, resourcename, path, root): """Check individual properties""" parameternames = self.parameternames matches = [] if root: specs = self.resourcetypes resourcetype = parenttype else: specs = self.propertytypes resourcetype = str.format('{0}.{1}', parenttype, proptype) # Handle tags if resourcetype not in specs: if proptype in specs: resourcetype = proptype else: resourcetype = str.format('{0}.{1}', parenttype, proptype) else: resourcetype = str.format('{0}.{1}', parenttype, proptype) resourcespec = specs[resourcetype].get('Properties', {}) if not resourcespec: if specs[resourcetype].get('Type') == 'List': if isinstance(text, list): property_type = specs[resourcetype].get('ItemType') for index, item in enumerate(text): matches.extend( self.propertycheck( item, property_type, parenttype, resourcename, path[:] + [index], root)) return matches supports_additional_properties = specs[resourcetype].get('AdditionalProperties', False) if text == 'AWS::NoValue': return matches if not isinstance(text, dict): if not self.check_exceptions(parenttype, proptype, text): message = 'Expecting an object at %s' % ('/'.join(map(str, path))) matches.append(RuleMatch(path, message)) return matches # You can put in functions directly in place of objects as long as that is # the only thing there (conditions, select) could all (possibly) # return objects. FindInMap cannot directly return an object. len_of_text = len(text) for prop in text: proppath = path[:] proppath.append(prop) if prop not in resourcespec: if prop in cfnlint.helpers.CONDITION_FUNCTIONS and len_of_text == 1: cond_values = self.cfn.get_condition_values(text[prop]) for cond_value in cond_values: if isinstance(cond_value['Value'], dict): matches.extend(self.propertycheck( cond_value['Value'], proptype, parenttype, resourcename, proppath + cond_value['Path'], root)) elif isinstance(cond_value['Value'], list): for index, item in enumerate(cond_value['Value']): matches.extend( self.propertycheck( item, proptype, parenttype, resourcename, proppath + cond_value['Path'] + [index], root) ) elif text.is_function_returning_object(): self.logger.debug('Ran into function "%s". Skipping remaining checks', prop) elif len(text) == 1 and prop in 'Ref' and text.get(prop) == 'AWS::NoValue': pass elif not supports_additional_properties: message = 'Invalid Property %s' % ('/'.join(map(str, proppath))) matches.append(RuleMatch(proppath, message)) else: if 'Type' in resourcespec[prop]: if resourcespec[prop]['Type'] == 'List': if 'PrimitiveItemType' not in resourcespec[prop]: if isinstance(text[prop], list): for index, item in enumerate(text[prop]): arrproppath = proppath[:] arrproppath.append(index) matches.extend(self.propertycheck( item, resourcespec[prop]['ItemType'], parenttype, resourcename, arrproppath, False)) elif (isinstance(text[prop], dict)): # A list can be be specific as a Conditional matches.extend( self.check_list_for_condition( text, prop, parenttype, resourcename, resourcespec[prop], proppath) ) else: message = 'Property {0} should be of type List for resource {1}' matches.append( RuleMatch( proppath, message.format(prop, resourcename))) else: if isinstance(text[prop], list): primtype = resourcespec[prop]['PrimitiveItemType'] for index, item in enumerate(text[prop]): arrproppath = proppath[:] arrproppath.append(index) matches.extend(self.primitivetypecheck( item, primtype, arrproppath)) elif isinstance(text[prop], dict): if 'Ref' in text[prop]: ref = text[prop]['Ref'] if ref == 'AWS::NotificationARNs': continue if ref in parameternames: param_type = self.cfn.template['Parameters'][ref]['Type'] if param_type: if 'List<' not in param_type and '<List' not in param_type and not param_type == 'CommaDelimitedList': message = 'Property {0} should be of type List or Parameter should ' \ 'be a list for resource {1}' matches.append( RuleMatch( proppath, message.format(prop, resourcename))) else: message = 'Property {0} should be of type List for resource {1}' matches.append( RuleMatch( proppath, message.format(prop, resourcename))) else: message = 'Property {0} should be of type List for resource {1}' matches.append( RuleMatch( proppath, message.format(prop, resourcename))) else: if resourcespec[prop]['Type'] not in ['Map']: matches.extend(self.propertycheck( text[prop], resourcespec[prop]['Type'], parenttype, resourcename, proppath, False)) elif 'PrimitiveType' in resourcespec[prop]: primtype = resourcespec[prop]['PrimitiveType'] matches.extend(self.primitivetypecheck(text[prop], primtype, proppath)) return matches
def match(self, cfn): """Check CloudFormation GetAtt""" matches = [] getatts = cfn.search_deep_keys('Fn::GetAtt') valid_getatts = cfn.get_valid_getatts() valid_attribute_functions = ['Ref'] for getatt in getatts: if len(getatt[-1]) < 2: message = 'Invalid GetAtt for {0}' matches.append( RuleMatch(getatt, message.format('/'.join(map(str, getatt[:-1]))))) continue if isinstance(getatt[-1], six.string_types): resname, restype = getatt[-1].split('.', 1) else: resname = None restype = None if isinstance(getatt[-1][1], six.string_types): resname = getatt[-1][0] restype = '.'.join(getatt[-1][1:]) elif isinstance(getatt[-1][1], dict): # You can ref the secondary part of a getatt resname = getatt[-1][0] restype = getatt[-1][1] if len(restype) == 1: for k in restype.keys(): if k not in valid_attribute_functions: message = 'GetAtt only supports functions "{0}" for attributes at {1}' matches.append( RuleMatch( getatt, message.format( ', '.join( map(str, valid_attribute_functions) ), '/'.join(map(str, getatt[:-1]))))) else: message = 'Invalid GetAtt structure {0} at {1}' matches.append( RuleMatch( getatt, message.format(getatt[-1], '/'.join(map(str, getatt[:-1]))))) # setting restype to None as we can't validate that anymore restype = None else: message = 'Invalid GetAtt structure {0} at {1}' matches.append( RuleMatch( getatt, message.format(getatt[-1], '/'.join(map(str, getatt[:-1]))))) # only check resname if its set. if it isn't it is because of bad structure # and an error is already provided if resname: if resname in valid_getatts: if restype is not None: if restype not in valid_getatts[ resname] and '*' not in valid_getatts[resname]: message = 'Invalid GetAtt {0}.{1} for resource {2}' matches.append( RuleMatch( getatt[:-1], message.format(resname, restype, getatt[1]))) else: message = 'Invalid GetAtt {0}.{1} for resource {2}' matches.append( RuleMatch(getatt, message.format(resname, restype, getatt[1]))) return matches
def check_list_for_condition(self, text, prop, parenttype, resourcename, propspec, path): """Checks lists that are a dict for conditions""" matches = [] if len(text[prop]) == 1: # pylint: disable=R1702 for sub_key, sub_value in text[prop].items(): if sub_key in cfnlint.helpers.CONDITION_FUNCTIONS: if len(sub_value) == 3: for if_i, if_v in enumerate(sub_value[1:]): condition_path = path[:] + [sub_key, if_i + 1] if isinstance(if_v, list): for index, item in enumerate(if_v): arrproppath = condition_path[:] arrproppath.append(index) matches.extend( self.propertycheck( item, propspec['ItemType'], parenttype, resourcename, arrproppath, False)) elif isinstance(if_v, dict): if len(if_v) == 1: for d_k, d_v in if_v.items(): if d_k != 'Ref' or d_v != 'AWS::NoValue': if d_k == 'Fn::GetAtt': resource_name = None if isinstance(d_v, list): resource_name = d_v[0] elif isinstance( d_v, six.string_types): resource_name = d_v.split( '.')[0] if resource_name: resource_type = self.cfn.template.get( 'Resources', {}).get( resource_name, {}).get('Type') if not (resource_type. startswith( 'Custom::')): message = 'Property {0} should be of type List for resource {1} at {2}' matches.append( RuleMatch( condition_path, message.format( prop, resourcename, ('/'.join( str(x) for x in condition_path ))))) else: message = 'Property {0} should be of type List for resource {1} at {2}' matches.append( RuleMatch( condition_path, message.format( prop, resourcename, ('/'.join( str(x) for x in condition_path) )))) else: message = 'Property {0} should be of type List for resource {1} at {2}' matches.append( RuleMatch( condition_path, message.format( prop, resourcename, ('/'.join( str(x) for x in condition_path) )))) else: message = 'Property {0} should be of type List for resource {1} at {2}' matches.append( RuleMatch( condition_path, message.format( prop, resourcename, ('/'.join( str(x) for x in condition_path))))) else: message = 'Invalid !If condition specified at %s' % ( '/'.join(map(str, path))) matches.append(RuleMatch(path, message)) else: # FindInMaps can be lists of objects so skip checking those if sub_key != 'Fn::FindInMap': # if its a GetAtt to a custom resource that custom resource # can return a list of objects so skip. if sub_key == 'Fn::GetAtt': resource_name = None if isinstance(sub_value, list): resource_name = sub_value[0] elif isinstance(sub_value, six.string_types): resource_name = sub_value.split('.')[0] if resource_name: resource_type = self.cfn.template.get( 'Resources', {}).get(resource_name, {}).get('Type') if not (resource_type == 'AWS::CloudFormation::CustomResource' or resource_type.startswith('Custom::')): message = 'Property is an object instead of List at %s' % ( '/'.join(map(str, path))) matches.append(RuleMatch(path, message)) elif not (sub_key == 'Ref' and sub_value == 'AWS::NoValue'): message = 'Property is an object instead of List at %s' % ( '/'.join(map(str, path))) matches.append(RuleMatch(path, message)) else: self.logger.debug( 'Too much logic to handle whats actually in the map "%s" so skipping any more validation.', sub_value) else: message = 'Property is an object instead of List at %s' % ( '/'.join(map(str, path))) matches.append(RuleMatch(path, message)) return matches
def _test_parameter(self, parameter, cfn, parameters, tree): """ Test a parameter """ matches = [] get_atts = cfn.get_valid_getatts() odd_list_params = [ 'CommaDelimitedList', 'AWS::SSM::Parameter::Value<CommaDelimitedList>', ] 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 odd_list_params or template_parameters.get(parameter).startswith( 'AWS::SSM::Parameter::Value<List') or template_parameters.get(parameter).startswith('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] and attribute_name == '*': 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 elif (resource == parameter.split('.')[0] and 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 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 check_value_ref(self, value, path, **kwargs): """Check Ref""" matches = [] cfn = kwargs.get('cfn') if 'Fn::If' in path: self.logger.debug( 'Not able to guarentee that the default value hasn\'t been conditioned out' ) return matches if path[0] == 'Resources' and 'Condition' in cfn.template.get( path[0], {}).get(path[1]): self.logger.debug('Not able to guarentee that the default value ' 'hasn\'t been conditioned out') return matches allowed_pattern = kwargs.get('value_specs', {}).get('AllowedPattern', {}) allowed_pattern_regex = kwargs.get('value_specs', {}).get('AllowedPatternRegex', {}) allowed_pattern_description = kwargs.get('value_specs', {}).get( 'AllowedPatternDescription', {}) if allowed_pattern_regex: if value in cfn.template.get('Parameters', {}): param = cfn.template.get('Parameters').get(value, {}) parameter_values = param.get('AllowedValues') default_value = param.get('Default') parameter_type = param.get('Type') if isinstance(parameter_type, str): if ((not parameter_type.startswith('List<')) and (not parameter_type.startswith( 'AWS::SSM::Parameter::Value<')) and parameter_type not in ['CommaDelimitedList', 'List<String>']): # Check Allowed Values if parameter_values: for index, allowed_value in enumerate( parameter_values): if not re.match(allowed_pattern_regex, str(allowed_value)): param_path = [ 'Parameters', value, 'AllowedValues', index ] description = allowed_pattern_description or 'Valid values must match pattern {0}'.format( allowed_pattern) message = 'You must specify a valid allowed value for {0} ({1}). {2}' matches.append( RuleMatch( param_path, message.format( value, allowed_value, description))) if default_value: # Check Default, only if no allowed Values are specified in the parameter (that's covered by E2015) if not re.match(allowed_pattern_regex, str(default_value)): param_path = ['Parameters', value, 'Default'] description = allowed_pattern_description or 'Valid values must match pattern {0}'.format( allowed_pattern) message = 'You must specify a valid Default value for {0} ({1}). {2}' matches.append( RuleMatch( param_path, message.format(value, default_value, description))) return matches
def match(self, cfn): matches = [] split_objs = cfn.search_deep_keys('Fn::Split') supported_functions = [ 'Fn::Base64', 'Fn::FindInMap', 'Fn::GetAZs', 'Fn::GetAtt', 'Fn::If', 'Fn::ImportValue', 'Fn::Join', 'Fn::Select', 'Fn::Sub', 'Ref', ] for split_obj in split_objs: split_value_obj = split_obj[-1] tree = split_obj[:-1] if isinstance(split_value_obj, list): if len(split_value_obj) == 2: split_delimiter = split_value_obj[0] split_string = split_value_obj[1] if not isinstance(split_delimiter, str): message = 'Split delimiter has to be of type string for {0}' matches.append( RuleMatch(tree + [0], message.format('/'.join(map(str, tree))))) if isinstance(split_string, dict): if len(split_string) == 1: for key, _ in split_string.items(): if key not in supported_functions: message = 'Fn::Split doesn\'t support the function {0} at {1}' matches.append( RuleMatch( tree + [key], message.format( key, '/'.join(map(str, tree))))) else: message = 'Split list of singular function or string for {0}' matches.append( RuleMatch( tree, message.format('/'.join(map(str, tree))))) elif not isinstance(split_string, str): message = 'Split has to be of type string or valid function for {0}' matches.append( RuleMatch(tree, message.format('/'.join(map(str, tree))))) else: message = 'Split should be an array of 2 for {0}' matches.append( RuleMatch(tree, message.format('/'.join(map(str, tree))))) else: message = 'Split should be an array of 2 for {0}' matches.append( RuleMatch(tree, message.format('/'.join(map(str, tree))))) return matches
def match(self, cfn): """Check CloudFormation Outputs""" matches = [] template = cfn.template getatts = cfn.search_deep_keys('Fn::GetAtt') refs = cfn.search_deep_keys('Ref') # If using a getatt make sure the attribute of the resource # is not of Type List for getatt in getatts: if getatt[0] == 'Outputs': if getatt[2] == 'Value': obj = getatt[-1] if isinstance(obj, list): objtype = template.get('Resources', {}).get(obj[0], {}).get('Type') if objtype: attribute = self.resourcetypes.get( objtype, {}).get('Attributes', {}).get(obj[1], {}).get('Type') if attribute == 'List': if getatt[-4] != 'Fn::Join' and getatt[-3] != 1: message = 'Output {0} value {1} is of type list' matches.append(RuleMatch( getatt, message.format(getatt[1], '/'.join(obj)) )) # If using a ref for an output make sure it isn't a # Parameter of Type List for ref in refs: if ref[0] == 'Outputs': if ref[2] == 'Value': obj = ref[-1] if isinstance(obj, six.string_types): param = template.get('Parameters', {}).get(obj) if param: paramtype = param.get('Type') if paramtype: if paramtype.startswith('List<'): if ref[-4] != 'Fn::Join' and ref[-3] != 1: message = 'Output {0} value {1} is of type list' matches.append(RuleMatch( ref, message.format(ref[1], obj) )) # Check if the output values are not lists outputs = cfn.template.get('Outputs', {}) for output_name, output in outputs.items(): value_obj = output.get('Value') if value_obj: if isinstance(value_obj, list): message = 'Output {0} value is of type list' matches.append(RuleMatch( output_name, message.format(output_name) )) return matches
def _check_state_json(self, def_json, state_name, path): """Check State JSON Definition""" matches = [] # https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-common-fields.html common_state_keys = [ 'Next', 'End', 'Type', 'Comment', 'InputPath', 'OutputPath', ] common_state_required_keys = [ 'Type', ] state_key_types = { 'Pass': ['Result', 'ResultPath', 'Parameters'], 'Task': ['Resource', 'ResultPath', 'ResultSelector', 'Retry', 'Catch', 'TimeoutSeconds', 'Parameters', 'HeartbeatSeconds'], 'Map': ['MaxConcurrency', 'Iterator', 'ItemsPath', 'ResultPath', 'ResultSelector', 'Retry', 'Catch', 'Parameters'], 'Choice': ['Choices', 'Default'], 'Wait': ['Seconds', 'Timestamp', 'SecondsPath', 'TimestampPath'], 'Succeed': [], 'Fail': ['Cause', 'Error'], 'Parallel': ['Branches', 'ResultPath', 'ResultSelector', 'Parameters', 'Retry', 'Catch'] } state_required_types = { 'Pass': [], 'Task': ['Resource'], 'Choice': ['Choices'], 'Wait': [], 'Succeed': [], 'Fail': [], 'Parallel': ['Branches'] } for req_key in common_state_required_keys: if req_key not in def_json: message = 'State Machine Definition required key (%s) for State (%s) is missing' % ( req_key, state_name) matches.append(RuleMatch(path, message)) return matches state_type = def_json.get('Type') if state_type in state_key_types: for state_key, _ in def_json.items(): if state_key not in common_state_keys + state_key_types.get(state_type, []): message = 'State Machine Definition key (%s) for State (%s) of Type (%s) is not valid' % ( state_key, state_name, state_type) matches.append(RuleMatch(path, message)) for req_key in common_state_required_keys + state_required_types.get(state_type, []): if req_key not in def_json: message = 'State Machine Definition required key (%s) for State (%s) of Type (%s) is missing' % ( req_key, state_name, state_type) matches.append(RuleMatch(path, message)) return matches else: message = 'State Machine Definition Type (%s) is not valid' % (state_type) matches.append(RuleMatch(path, message)) return matches
def _test_cluster_settings(self, properties, path, pg_properties, pg_path, cfn, scenario): """ test for each scenario """ results = [] pg_conditions = cfn.get_conditions_from_path(cfn.template, pg_path) # test to make sure that any condition that may apply to the path for the Ref # is not applicable if pg_conditions: for c_name, c_value in scenario.items(): if c_name in pg_conditions: if c_value not in pg_conditions.get(c_name): return results if self.is_cluster_enabled( cfn.get_value_from_scenario(pg_properties, scenario)): c_props = cfn.get_value_from_scenario(properties, scenario) automatic_failover = c_props.get('AutomaticFailoverEnabled') if bool_compare(automatic_failover, False): pathmessage = path[:] + ['AutomaticFailoverEnabled'] if scenario is None: message = '"AutomaticFailoverEnabled" must be misssing or True when setting up a cluster at {0}' results.append( RuleMatch( pathmessage, message.format('/'.join(map(str, pathmessage))))) else: message = '"AutomaticFailoverEnabled" must be misssing or True when setting up a cluster when {0} at {1}' scenario_text = ' and '.join([ 'when condition "%s" is %s' % (k, v) for (k, v) in scenario.items() ]) results.append( RuleMatch( pathmessage, message.format(scenario_text, '/'.join(map(str, pathmessage))))) num_node_groups = c_props.get('NumNodeGroups') if not num_node_groups: # only test cache nodes if num node groups aren't specified num_cache_nodes = c_props.get('NumCacheClusters', 0) if num_cache_nodes <= 1: pathmessage = path[:] + ['NumCacheClusters'] if scenario is None: message = '"NumCacheClusters" must be greater than one when creating a cluster at {0}' results.append( RuleMatch( pathmessage, message.format('/'.join(map(str, pathmessage))))) else: message = '"NumCacheClusters" must be greater than one when creating a cluster when {0} at {1}' scenario_text = ' and '.join([ 'when condition "%s" is %s' % (k, v) for (k, v) in scenario.items() ]) results.append( RuleMatch( pathmessage, message.format(scenario_text, '/'.join(map(str, pathmessage))))) return results
def match(self, cfn): matches = [] # Get a list of paths to every leaf node string containing at least one ${parameter} parameter_string_paths = self.match_values(cfn) # We want to search all of the paths to check if each one contains an 'Fn::Sub' for parameter_string_path in parameter_string_paths: if parameter_string_path[0] in ['Parameters']: continue # Exclude the special IAM variables variable = parameter_string_path[-1] if 'Resource' in parameter_string_path: if variable in self.resource_excludes: continue if 'NotResource' in parameter_string_path: if variable in self.resource_excludes: continue if 'Condition' in parameter_string_path: if variable in self.condition_excludes: continue # Step Function State Machine has a Definition Substitution that allows usage of special variables outside of a !Sub # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definitionsubstitutions.html if 'DefinitionString' in parameter_string_path: modified_parameter_string_path = parameter_string_path index = parameter_string_path.index('DefinitionString') modified_parameter_string_path[ index] = 'DefinitionSubstitutions' modified_parameter_string_path = modified_parameter_string_path[: index + 1] modified_parameter_string_path.append(variable[2:-1]) if reduce(lambda c, k: c.get(k, {}), modified_parameter_string_path, cfn.template): continue # Exclude variables that match custom exclude filters, if configured # (for third-party tools that pre-process templates before uploading them to AWS) if self._variable_custom_excluded(variable): continue # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html) if variable.startswith('${!'): continue found_sub = False # Does the path contain an 'Fn::Sub'? for step in parameter_string_path: if step in self.api_excludes: if self._api_exceptions(parameter_string_path[-1]): found_sub = True elif step == 'Fn::Sub' or step in self.excludes: found_sub = True # If we didn't find an 'Fn::Sub' it means a string containing a ${parameter} may not be evaluated correctly if not found_sub: # Remove the last item (the variable) to prevent multiple errors on 1 line errors path = parameter_string_path[:-1] message = 'Found an embedded parameter "{}" outside of an "Fn::Sub" at {}'.format( variable, '/'.join(map(str, path))) matches.append(RuleMatch(path, message)) return matches
def _check_policy_statement(self, branch, statement, is_identity_policy, resource_exceptions): """Check statements""" matches = [] statement_valid_keys = [ 'Action', 'Condition', 'Effect', 'NotAction', 'NotPrincipal', 'NotResource', 'Principal', 'Resource', 'Sid', ] for key, _ in statement.items(): if key not in statement_valid_keys: message = 'IAM Policy statement key %s isn\'t valid' % (key) matches.append( RuleMatch(branch[:] + [key], message)) if 'Effect' not in statement: message = 'IAM Policy statement missing Effect' matches.append( RuleMatch(branch[:], message)) else: for effect, effect_path in statement.get_safe('Effect'): if isinstance(effect, str): if effect not in ['Allow', 'Deny']: message = 'IAM Policy Effect should be Allow or Deny' matches.append( RuleMatch(branch[:] + effect_path, message)) if 'Action' not in statement and 'NotAction' not in statement: message = 'IAM Policy statement missing Action or NotAction' matches.append( RuleMatch(branch[:], message)) if is_identity_policy: if 'Principal' in statement or 'NotPrincipal' in statement: message = 'IAM Resource Policy statement shouldn\'t have Principal or NotPrincipal' matches.append( RuleMatch(branch[:], message)) else: if 'Principal' not in statement and 'NotPrincipal' not in statement: message = 'IAM Resource Policy statement should have Principal or NotPrincipal' matches.append( RuleMatch(branch[:] + ['Principal'], message)) if not resource_exceptions: if 'Resource' not in statement and 'NotResource' not in statement: message = 'IAM Policy statement missing Resource or NotResource' matches.append( RuleMatch(branch[:], message)) resources = statement.get('Resource', []) if isinstance(resources, str): resources = [resources] for index, resource in enumerate(resources): if isinstance(resource, dict): if len(resource) == 1: for k in resource.keys(): if k not in FUNCTIONS_SINGLE: message = 'IAM Policy statement Resource incorrectly formatted' matches.append( RuleMatch(branch[:] + ['Resource', index], message)) else: message = 'IAM Policy statement Resource incorrectly formatted' matches.append( RuleMatch(branch[:] + ['Resource', index], message)) return(matches)
def match(self, cfn): """Check CloudFormation Password Parameters""" matches = [] password_properties = [ 'AccountPassword', 'AdminPassword', 'ADDomainJoinPassword', 'CrossRealmTrustPrincipalPassword', 'KdcAdminPassword', 'Password', 'DbPassword', 'MasterUserPassword', 'PasswordParam' ] parameters = cfn.get_parameter_names() fix_params = [] for password_property in password_properties: # Build the list of refs refs = cfn.search_deep_keys(password_property) trees = [] for tree in refs: if len(tree) > 2: if tree[0] == 'Resources' and tree[2] == 'Properties': trees.append(tree) for tree in trees: obj = tree[-1] if isinstance(obj, (six.string_types)): if re.match(REGEX_DYN_REF, obj): if re.match(REGEX_DYN_REF_SSM, obj): message = 'Password should use a secure dynamic reference for %s' % ( '/'.join(map(str, tree[:-1]))) matches.append(RuleMatch(tree[:-1], message)) else: message = 'Password shouldn\'t be hardcoded for %s' % ( '/'.join(map(str, tree[:-1]))) matches.append(RuleMatch(tree[:-1], message)) elif isinstance(obj, dict): if len(obj) == 1: for key, value in obj.items(): if key == 'Ref': if value in parameters: param = cfn.template['Parameters'][value] if 'NoEcho' in param: if not param['NoEcho']: fix_params.append({ 'Name': value, 'Use': password_property }) else: fix_params.append({ 'Name': value, 'Use': password_property }) else: message = 'Inappropriate map found for password on %s' % ( '/'.join(map(str, tree[:-1]))) matches.append(RuleMatch(tree[:-1], message)) for paramname in fix_params: message = 'Parameter {} used as {}, therefore NoEcho should be True'.format( paramname['Name'], paramname['Use']) tree = ['Parameters', paramname['Name']] matches.append(RuleMatch(tree, message)) return matches
def match(self, cfn): matches = [] cidr_objs = cfn.search_deep_keys('Fn::Cidr') supported_functions = [ 'Fn::FindInMap', 'Fn::Select', 'Ref', 'Fn::GetAtt', 'Fn::Sub', 'Fn::ImportValue' ] count_parameters = [] size_mask_parameters = [] for cidr_obj in cidr_objs: cidr_value_obj = cidr_obj[-1] tree = cidr_obj[:-1] if isinstance(cidr_value_obj, list): if len(cidr_value_obj) in [2, 3]: ip_block_obj = cidr_value_obj[0] count_obj = cidr_value_obj[1] if len(cidr_value_obj) == 3: size_mask_obj = cidr_value_obj[2] else: size_mask_obj = None if isinstance(ip_block_obj, dict): if len(ip_block_obj) == 1: for index_key, _ in ip_block_obj.items(): if index_key not in supported_functions: message = 'Cidr ipBlock should be Cidr Range, Ref, GetAtt, Sub or Select for {0}' matches.append( RuleMatch( tree[:] + [0], message.format('/'.join( map(str, tree[:] + [0]))))) elif isinstance(ip_block_obj, (six.text_type, six.string_types)): if not re.match(REGEX_CIDR, ip_block_obj): message = 'Cidr ipBlock should be a Cidr Range based string for {0}' matches.append( RuleMatch( tree[:] + [0], message.format('/'.join( map(str, tree[:] + [0]))))) else: message = 'Cidr ipBlock should be a string for {0}' matches.append( RuleMatch( tree[:] + [0], message.format('/'.join(map( str, tree[:] + [0]))))) if isinstance(count_obj, dict): if len(count_obj) == 1: for index_key, index_value in count_obj.items(): if index_key not in supported_functions: message = 'Cidr count should be Int, Ref, or Select for {0}' matches.append( RuleMatch( tree[:] + [1], message.format('/'.join( map(str, tree[:] + [1]))))) if index_key == 'Ref': count_parameters.append(index_value) elif not isinstance(count_obj, six.integer_types): message = 'Cidr count should be a int for {0}' extra_args = { 'actual_type': type(count_obj).__name__, 'expected_type': six.integer_types[0].__name__ } matches.append( RuleMatch( tree[:] + [1], message.format('/'.join(map( str, tree[:] + [1]))), **extra_args)) if isinstance(size_mask_obj, dict): if len(size_mask_obj) == 1: for index_key, index_value in size_mask_obj.items( ): if index_key not in supported_functions: message = 'Cidr sizeMask should be Int, Ref, or Select for {0}' matches.append( RuleMatch( tree[:] + [2], message.format('/'.join( map(str, tree[:] + [2]))))) if index_key == 'Ref': size_mask_parameters.append(index_value) elif not isinstance(size_mask_obj, six.integer_types): message = 'Cidr sizeMask should be a int for {0}' extra_args = { 'actual_type': type(count_obj).__name__, 'expected_type': six.integer_types[0].__name__ } matches.append( RuleMatch( tree[:] + [2], message.format('/'.join(map( str, tree[:] + [2]))), **extra_args)) else: message = 'Cidr should be a list of 2 or 3 elements for {0}' matches.append( RuleMatch(tree, message.format('/'.join(map(str, tree))))) else: message = 'Cidr should be a list of 2 or 3 elements for {0}' matches.append( RuleMatch(tree, message.format('/'.join(map(str, tree))))) for count_parameter in set(count_parameters): matches.extend(self.check_parameter_count(cfn, count_parameter)) for size_mask_parameter in set(size_mask_parameters): matches.extend( self.check_parameter_size_mask(cfn, size_mask_parameter)) return matches
def match(self, cfn): """Basic Matching""" matches = [] title_message = 'Parameter {0} Description is not sentence case: {1}' spell_message = 'Parameter {0} contains spelling error(s): {1}' stop_message = 'Parameter {0} must end in a full stop "."' if self.id in cfn.template.get("Metadata", {}).get("QSLint", {}).get("Exclusions", []): return matches if "Parameters" not in cfn.template.keys(): return matches else: custom_dict = self.get_custom_dict() spell = SpellChecker() if "Metadata" in cfn.template.keys(): if "LintSpellExclude" in cfn.template["Metadata"].keys(): # add any proper nouns defined in template metadata custom_dict = custom_dict.union( set(cfn.template["Metadata"]["LintSpellExclude"])) for x in cfn.template["Parameters"]: if "Description" in cfn.template["Parameters"][x].keys(): location = ["Parameters", x, "Description"] description = cfn.template["Parameters"][x]["Description"] stop_error = not (description.strip()[-1] == '.' or description.strip()[-2:] == '."') description = strip_urls(description) spell_errors, title_errors = self.get_errors( description, spell, custom_dict) if stop_error: matches.append( RuleMatch(location, stop_message.format(x))) if title_errors: matches.append( RuleMatch(location, title_message.format(x, title_errors))) if spell_errors: matches.append( RuleMatch(location, spell_message.format(x, spell_errors))) if "Metadata" not in cfn.template.keys(): matches.append( RuleMatch( ["Parameters"], "Template is missing Parameter labels and groups")) elif "AWS::CloudFormation::Interface" not in cfn.template[ "Metadata"].keys(): matches.append( RuleMatch( ["Metadata"], "Template is missing Parameter labels and groups")) elif "ParameterGroups" not in cfn.template["Metadata"][ "AWS::CloudFormation::Interface"].keys(): matches.append( RuleMatch(["Metadata", "AWS::CloudFormation::Interface"], "Template is missing Parameter groups")) elif "ParameterLabels" not in cfn.template["Metadata"][ "AWS::CloudFormation::Interface"].keys(): matches.append( RuleMatch(["Metadata", "AWS::CloudFormation::Interface"], "Template is missing Parameter labels")) else: count = 0 for x in cfn.template["Metadata"][ "AWS::CloudFormation::Interface"]["ParameterGroups"]: title_message = 'Parameter Group name "{0}" is not sentence case: {1}' spell_message = 'Parameter Group name "{0}" contains spelling error(s): {1}' if "Label" not in x.keys(): matches.append( RuleMatch([ "Metadata", "AWS::CloudFormation::Interface", "ParameterGroups", x ], "Template is missing Parameter groups")) elif "default" not in x["Label"].keys(): matches.append( RuleMatch([ "Metadata", "AWS::CloudFormation::Interface", "ParameterGroups", x ], "Template is missing Parameter groups")) else: location = [ "Metadata", "AWS::CloudFormation::Interface", "ParameterGroups", count, "Label", "default" ] description = x["Label"]["default"] spell_errors, title_errors = self.get_errors( description, spell, custom_dict) if title_errors: matches.append( RuleMatch( location, title_message.format( x["Label"]["default"], title_errors))) if spell_errors: matches.append( RuleMatch( location, spell_message.format( x["Label"]["default"], spell_errors))) count += 1 for x in cfn.template["Metadata"][ "AWS::CloudFormation::Interface"]["ParameterLabels"]: title_message = 'Parameter Label is not sentence case: {0}' spell_message = 'Parameter Label contains spelling error(s): {0}' if "default" not in cfn.template["Metadata"][ "AWS::CloudFormation::Interface"][ "ParameterLabels"][x].keys(): matches.append( RuleMatch([ "Metadata", "AWS::CloudFormation::Interface", "ParameterLabels", x ], "Template is missing Parameter labels")) else: location = [ "Metadata", "AWS::CloudFormation::Interface", "ParameterLabels", x, "default" ] description = cfn.template["Metadata"][ "AWS::CloudFormation::Interface"][ "ParameterLabels"][x]["default"] spell_errors, title_errors = self.get_errors( description, spell, custom_dict) if title_errors: matches.append( RuleMatch(location, title_message.format(title_errors))) if spell_errors: matches.append( RuleMatch(location, spell_message.format(spell_errors))) count += 1 return matches
def _check_list_for_condition(self, text, prop, parenttype, resourcename, propspec, path): """ Loop for conditions """ matches = [] if len(text) == 3: for if_i, if_v in enumerate(text[1:]): condition_path = path[:] + [if_i + 1] if isinstance(if_v, list): for index, item in enumerate(if_v): arrproppath = condition_path[:] arrproppath.append(index) matches.extend(self.propertycheck( item, propspec['ItemType'], parenttype, resourcename, arrproppath, False)) elif isinstance(if_v, dict): if len(if_v) == 1: for d_k, d_v in if_v.items(): if d_k != 'Ref' or d_v != 'AWS::NoValue': if d_k == 'Fn::GetAtt': resource_name = None if isinstance(d_v, list): resource_name = d_v[0] elif isinstance(d_v, six.string_types): resource_name = d_v.split('.')[0] if resource_name: resource_type = self.cfn.template.get( 'Resources', {}).get(resource_name, {}).get('Type') if not (resource_type.startswith('Custom::')): message = 'Property {0} should be of type List for resource {1} at {2}' matches.append( RuleMatch( condition_path, message.format(prop, resourcename, ('/'.join(str(x) for x in condition_path))))) elif d_k == 'Fn::If': matches.extend( self._check_list_for_condition( d_v, prop, parenttype, resourcename, propspec, condition_path) ) else: message = 'Property {0} should be of type List for resource {1} at {2}' matches.append( RuleMatch( condition_path, message.format(prop, resourcename, ('/'.join(str(x) for x in condition_path))))) else: message = 'Property {0} should be of type List for resource {1} at {2}' matches.append( RuleMatch( condition_path, message.format(prop, resourcename, ('/'.join(str(x) for x in condition_path))))) else: message = 'Property {0} should be of type List for resource {1} at {2}' matches.append( RuleMatch( condition_path, message.format(prop, resourcename, ('/'.join(str(x) for x in condition_path))))) else: message = 'Invalid !If condition specified at %s' % ( '/'.join(map(str, path))) matches.append(RuleMatch(path, message)) return matches
def check_value(self, value, path, prop, cfn, specs): """Check Role.AssumeRolePolicyDocument is within limits""" matches = [] #pylint: disable=too-many-return-statements def remove_functions(obj): """ Replaces intrinsic functions with string """ if isinstance(obj, dict): new_obj = {} if len(obj) == 1: for k, v in obj.items(): if k in cfnlint.helpers.FUNCTIONS: if k == 'Fn::Sub': if isinstance(v, str): return re.sub(r'\${.*}', '', v) if isinstance(v, list): return re.sub(r'\${.*}', '', v[0]) else: return '' else: new_obj[k] = remove_functions(v) return new_obj else: for k, v in obj.items(): new_obj[k] = remove_functions(v) return new_obj elif isinstance(obj, list): new_list = [] for v in obj: new_list.append(remove_functions(v)) return new_list return obj scenarios = cfn.get_object_without_nested_conditions(value, path) json_max_size = specs.get('JsonMax') for scenario in scenarios: j = remove_functions(scenario['Object'][prop]) if isinstance(j, str): try: j = json.loads(j) except: #pylint: disable=bare-except continue if len( json.dumps(j, separators=(',', ':'), default=self._serialize_date)) > json_max_size: if scenario['Scenario']: message = '{0} JSON text cannot be longer than {1} characters when {2}' scenario_text = ' and '.join([ 'when condition "%s" is %s' % (k, v) for (k, v) in scenario['Scenario'].items() ]) matches.append( RuleMatch( path + [prop], message.format(prop, json_max_size, scenario_text))) else: message = '{0} JSON text cannot be longer than {1} characters' matches.append( RuleMatch( path + [prop], message.format(prop, json_max_size), )) return matches
def propertycheck(self, text, proptype, parenttype, resourcename, tree, root): """Check individual properties""" matches = [] if root: specs = self.resourcetypes resourcetype = parenttype else: specs = self.propertytypes resourcetype = str.format('{0}.{1}', parenttype, proptype) # handle tags if resourcetype not in specs: if proptype in specs: resourcetype = proptype else: resourcetype = str.format('{0}.{1}', parenttype, proptype) else: resourcetype = str.format('{0}.{1}', parenttype, proptype) resourcespec = specs[resourcetype].get('Properties') if not resourcespec: if specs[resourcetype].get('Type') == 'List': if isinstance(text, list): property_type = specs[resourcetype].get('ItemType') for index, item in enumerate(text): matches.extend( self.propertycheck(item, property_type, parenttype, resourcename, tree[:] + [index], root)) else: # this isn't a standard type in the CloudFormation spec so we # can't check required so skip return matches if not isinstance(text, dict): # Covered with Properties not with Required return matches # Return empty matches if we run into a function that is being used to get an object # Selects could be used to return an object when used with a FindInMap if text.is_function_returning_object(): return matches # Check if all required properties are specified resource_objects = [] base_object_properties = {} for key, value in text.items(): if key not in cfnlint.helpers.CONDITION_FUNCTIONS: base_object_properties[key] = value condition_found = False for key, value in text.items(): if key in cfnlint.helpers.CONDITION_FUNCTIONS: condition_found = True cond_values = self.cfn.get_condition_values(value) for cond_value in cond_values: if isinstance(cond_value['Value'], dict): append_object = {} append_object['Path'] = tree[:] + [ key ] + cond_value['Path'] append_object['Value'] = {} for sub_key, sub_value in cond_value['Value'].items(): append_object['Value'][sub_key] = sub_value append_object['Value'].update(base_object_properties) resource_objects.append(append_object) if not condition_found: resource_objects.append({ 'Path': tree[:], 'Value': base_object_properties }) for resource_object in resource_objects: path = resource_object.get('Path') value = resource_object.get('Value') for prop in resourcespec: if resourcespec[prop]['Required']: if prop not in value: message = 'Property {0} missing at {1}' matches.append( RuleMatch( path, message.format(prop, '/'.join(map(str, path))))) # For all specified properties, check all nested properties for prop in value: proptree = path[:] proptree.append(prop) if prop in resourcespec: if 'Type' in resourcespec[prop]: if resourcespec[prop]['Type'] == 'List': if 'PrimitiveItemType' not in resourcespec[prop]: if isinstance(value[prop], list): for index, item in enumerate(value[prop]): arrproptree = proptree[:] arrproptree.append(index) matches.extend( self.propertycheck( item, resourcespec[prop]['ItemType'], parenttype, resourcename, arrproptree, False)) else: if resourcespec[prop]['Type'] not in ['Map']: matches.extend( self.propertycheck( value[prop], resourcespec[prop]['Type'], parenttype, resourcename, proptree, False)) return matches
def match(self, cfn): """Check for RetentionPeriod""" matches = [] retention_attributes_by_resource_type = { 'AWS::Kinesis::Stream': [{ 'Attribute': 'RetentionPeriodHours', 'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesis-stream.html#cfn-kinesis-stream-retentionperiodhours' }], 'AWS::SQS::Queue': [{ 'Attribute': 'MessageRetentionPeriod', 'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-sqs-queue-msgretentionperiod' }], 'AWS::DocDB::DBCluster': [{ 'Attribute': 'BackupRetentionPeriod', 'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-docdb-dbcluster.html#cfn-docdb-dbcluster-backupretentionperiod' }], 'AWS::Synthetics::Canary': [{ 'Attribute': 'SuccessRetentionPeriod', 'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-successretentionperiod' }, { 'Attribute': 'FailureRetentionPeriod', 'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-failureretentionperiod' }], 'AWS::Redshift::Cluster': [{ 'Attribute': 'AutomatedSnapshotRetentionPeriod', 'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-automatedsnapshotretentionperiod' }], 'AWS::RDS::DBInstance': [{ 'Attribute': 'BackupRetentionPeriod', 'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-backupretentionperiod' }], 'AWS::RDS::DBCluster': [{ 'Attribute': 'BackupRetentionPeriod', 'SourceUrl': 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html#cfn-rds-dbcluster-backuprententionperiod' }] } resources = cfn.get_resources() for r_name, r_values in resources.items(): if r_values.get('Type') in retention_attributes_by_resource_type: for attr_def in retention_attributes_by_resource_type[ r_values.get('Type')]: property_sets = r_values.get_safe('Properties') for property_set, path in property_sets: error_path = ['Resources', r_name] + path if not property_set: message = 'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : %s' % '/'.join( str(x) for x in error_path) matches.append(RuleMatch(error_path, message)) else: value = property_set.get(attr_def.get('Attribute')) if not value: message = 'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : %s' % '/'.join( str(x) for x in error_path) matches.append(RuleMatch(error_path, message)) if isinstance(value, dict): # pylint: disable=protected-access refs = cfn._search_deep_keys( 'Ref', value, error_path + [attr_def.get('Attribute')]) for ref in refs: if ref[-1] == 'AWS::NoValue': message = 'The default retention period will delete the data after a pre-defined time. Set an explicit values to avoid data loss on resource : %s' % '/'.join( str(x) for x in ref[0:-1]) matches.append( RuleMatch(ref[0:-1], message)) return matches
def match(self, cfn): """Check CloudFormation Resources""" matches = [] valid_attributes = [ 'Condition', 'CreationPolicy', 'DeletionPolicy', 'DependsOn', 'Metadata', 'Properties', 'Type', 'UpdatePolicy', 'UpdateReplacePolicy', ] valid_custom_attributes = [ 'Condition', 'DeletionPolicy', 'DependsOn', 'Metadata', 'Properties', 'Type', 'UpdateReplacePolicy', 'Version', ] resources = cfn.template.get('Resources', {}) if not isinstance(resources, dict): message = 'Resource not properly configured' matches.append(RuleMatch(['Resources'], message)) else: for resource_name, resource_values in cfn.template.get( 'Resources', {}).items(): self.logger.debug('Validating resource %s base configuration', resource_name) if not isinstance(resource_values, dict): message = 'Resource not properly configured at {0}' matches.append( RuleMatch(['Resources', resource_name], message.format(resource_name))) continue resource_type = resource_values.get('Type', '') check_attributes = [] if resource_type.startswith( 'Custom::' ) or resource_type == 'AWS::CloudFormation::CustomResource': check_attributes = valid_custom_attributes else: check_attributes = valid_attributes for property_key, _ in resource_values.items(): if property_key not in check_attributes: message = 'Invalid resource attribute {0} for resource {1}' matches.append( RuleMatch( ['Resources', resource_name, property_key], message.format(property_key, resource_name))) # validate condition is a string condition = resource_values.get('Condition', '') if not isinstance(condition, six.string_types): message = 'Condition for resource {0} should be a string' matches.append( RuleMatch(['Resources', resource_name, 'Condition'], message.format(resource_name))) resource_type = resource_values.get('Type', '') if not resource_type: message = 'Type not defined for resource {0}' matches.append( RuleMatch(['Resources', resource_name], message.format(resource_name))) else: self.logger.debug('Check resource types by region...') for region, specs in cfnlint.helpers.RESOURCE_SPECS.items( ): if region in cfn.regions: if resource_type not in specs['ResourceTypes']: if not resource_type.startswith( ('Custom::', 'AWS::Serverless::')): message = 'Invalid or unsupported Type {0} for resource {1} in {2}' matches.append( RuleMatch([ 'Resources', resource_name, 'Type' ], message.format( resource_type, resource_name, region))) if 'Properties' not in resource_values: resource_spec = cfnlint.helpers.RESOURCE_SPECS[ cfn.regions[0]] if resource_type in resource_spec['ResourceTypes']: properties_spec = resource_spec['ResourceTypes'][ resource_type]['Properties'] # pylint: disable=len-as-condition if len(properties_spec) > 0: required = 0 for _, property_spec in properties_spec.items(): if property_spec.get('Required', False): required += 1 if required > 0: if resource_type == 'AWS::CloudFormation::WaitCondition' and 'CreationPolicy' in resource_values.keys( ): self.logger.debug( 'Exception to required properties section as CreationPolicy is defined.' ) else: message = 'Properties not defined for resource {0}' matches.append( RuleMatch( ['Resources', resource_name], message.format(resource_name))) 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, six.string_types): 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 == 'AWS::CloudFormation::Stack' 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 '.'.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 match(self, cfn): """Check CloudFormation Select""" matches = [] select_objs = cfn.search_deep_keys('Fn::Select') supported_functions = [ 'Fn::FindInMap', 'Fn::GetAtt', 'Fn::GetAZs', 'Fn::If', 'Fn::Split', 'Fn::Cidr', 'Ref' ] for select_obj in select_objs: select_value_obj = select_obj[-1] tree = select_obj[:-1] if isinstance(select_value_obj, list): if len(select_value_obj) == 2: index_obj = select_value_obj[0] list_of_objs = select_value_obj[1] if isinstance(index_obj, dict): if len(index_obj) == 1: for index_key, _ in index_obj.items(): if index_key not in ['Ref', 'Fn::FindInMap']: message = 'Select index should be an Integer or a function Ref or FindInMap for {0}' matches.append( RuleMatch( tree, message.format('/'.join( map(str, tree))))) elif not isinstance(index_obj, six.integer_types): try: int(index_obj) except ValueError: message = 'Select index should be an Integer or a function of Ref or FindInMap for {0}' matches.append( RuleMatch( tree, message.format('/'.join(map(str, tree))))) if isinstance(list_of_objs, dict): if len(list_of_objs) == 1: for key, _ in list_of_objs.items(): if key not in supported_functions: message = 'Select should use a supported function of {0}' matches.append( RuleMatch( tree, message.format(', '.join( map(str, supported_functions))))) else: message = 'Select should use a supported function of {0}' matches.append( RuleMatch( tree, message.format(', '.join( map(str, supported_functions))))) elif not isinstance(list_of_objs, list): message = 'Select should be an array of values for {0}' matches.append( RuleMatch(tree, message.format('/'.join(map(str, tree))))) else: message = 'Select should be a list of 2 elements for {0}' matches.append( RuleMatch(tree, message.format('/'.join(map(str, tree))))) else: message = 'Select should be a list of 2 elements for {0}' matches.append( RuleMatch(tree, message.format('/'.join(map(str, tree))))) return matches
def check_value_ref(self, value, path, **kwargs): """Check Ref""" matches = list() 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_primitive_type(self, value, item_type, path, strict_check): """Chec item type""" matches = [] if isinstance(value, dict) and item_type == 'Json': return matches if item_type in ['String']: if not isinstance(value, (six.string_types)): extra_args = { 'actual_type': type(value).__name__, 'expected_type': str.__name__ } matches.extend( self._value_check(value, path, item_type, strict_check, extra_args)) elif item_type in ['Boolean']: if not isinstance(value, (bool)): extra_args = { 'actual_type': type(value).__name__, 'expected_type': bool.__name__ } matches.extend( self._value_check(value, path, item_type, strict_check, extra_args)) elif item_type in ['Double']: if not isinstance(value, (float, int)): extra_args = { 'actual_type': type(value).__name__, 'expected_type': [float.__name__, int.__name__] } matches.extend( self._value_check(value, path, item_type, strict_check, extra_args)) elif item_type in ['Integer']: if not isinstance(value, (int)): extra_args = { 'actual_type': type(value).__name__, 'expected_type': int.__name__ } matches.extend( self._value_check(value, path, item_type, strict_check, extra_args)) elif item_type in ['Long']: if sys.version_info < (3, ): integer_types = ( int, long, ) # pylint: disable=undefined-variable else: integer_types = (int, ) if not isinstance(value, integer_types): extra_args = { 'actual_type': type(value).__name__, 'expected_type': ' or '.join([x.__name__ for x in integer_types]) } matches.extend( self._value_check(value, path, item_type, strict_check, extra_args)) elif isinstance(value, list): message = 'Property should be of type %s at %s' % ( item_type, '/'.join(map(str, path))) extra_args = { 'actual_type': type(value).__name__, 'expected_type': list.__name__ } matches.append(RuleMatch(path, message, **extra_args)) return matches
def _match_string_objs(self, join_string_objs, cfn, path): """ Check join list """ matches = [] template_parameters = self._get_parameters(cfn) get_atts = cfn.get_valid_getatts() if isinstance(join_string_objs, dict): if len(join_string_objs) == 1: for key, value in join_string_objs.items(): if key not in self.list_supported_functions: message = 'Fn::Join unsupported function for {0}' matches.append( RuleMatch(path, message.format('/'.join(map(str, path))))) elif key in ['Ref']: if not self._is_ref_a_list(value, template_parameters): message = 'Fn::Join must use a list at {0}' matches.append( RuleMatch( path, message.format('/'.join(map(str, path))))) elif key in ['Fn::GetAtt']: if not self._is_getatt_a_list( self._normalize_getatt(value), get_atts): message = 'Fn::Join must use a list at {0}' matches.append( RuleMatch( path, message.format('/'.join(map(str, path))))) else: message = 'Join list of values should be singular for {0}' matches.append( RuleMatch(path, message.format('/'.join(map(str, path))))) elif not isinstance(join_string_objs, list): message = 'Join list of values for {0}' matches.append( RuleMatch(path, message.format('/'.join(map(str, path))))) else: for string_obj in join_string_objs: if isinstance(string_obj, dict): if len(string_obj) == 1: for key, value in string_obj.items(): if key not in self.singular_supported_functions: message = 'Join unsupported function for {0}' matches.append( RuleMatch( path, message.format('/'.join(map(str, path))))) elif key in ['Ref']: if self._is_ref_a_list(value, template_parameters): message = 'Fn::Join must not be a list at {0}' matches.append( RuleMatch( path, message.format('/'.join( map(str, path))))) elif key in ['Fn::GetAtt']: if self._is_getatt_a_list( self._normalize_getatt(value), get_atts): message = 'Fn::Join must not be a list at {0}' matches.append( RuleMatch( path, message.format('/'.join( map(str, path))))) else: message = 'Join list of values should be singular for {0}' matches.append( RuleMatch(path, message.format('/'.join(map(str, path))))) elif not isinstance(string_obj, six.string_types): message = 'Join list of singular function or string for {0}' matches.append( RuleMatch(path, message.format('/'.join(map(str, path))))) return matches