def match(self, cfn): """Basic Rule Matching""" 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 # 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 match(self, cfn): """Check CloudFormation Metadata Parameters Exist""" matches = [] strinterface = 'AWS::CloudFormation::Interface' parameters = cfn.get_parameter_names() metadata_obj = cfn.template.get('Metadata', {}) if metadata_obj: interfaces = metadata_obj.get(strinterface, {}) if isinstance(interfaces, dict): # Check Parameter Group Parameters paramgroups = interfaces.get('ParameterGroups', []) if isinstance(paramgroups, list): for index, value in enumerate(paramgroups): if 'Parameters' in value: for paramindex, paramvalue in enumerate( value['Parameters']): if paramvalue not in parameters: message = 'Metadata Interface parameter doesn\'t exist {0}' matches.append( RuleMatch([ 'Metadata', strinterface, 'ParameterGroups', index, 'Parameters', paramindex ], message.format(paramvalue))) paramlabels = interfaces.get('ParameterLabels', {}) if isinstance(paramlabels, dict): for param in paramlabels: if param not in parameters: message = 'Metadata Interface parameter doesn\'t exist {0}' matches.append( RuleMatch([ 'Metadata', strinterface, 'ParameterLabels', param ], message.format(param))) return matches
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] resources = cfn.get_resources(["AWS::CloudFront::Distribution"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) default_cache_behavior = properties.get( "DistributionConfig", {}).get("DefaultCacheBehavior", {}) path = [ "Resources", resource_name, "Properties", "DistributionConfig", "DefaultCacheBehavior", ] if not default_cache_behavior.get("ResponseHeadersPolicyId"): message = ( f"Property {'/'.join(path)}/ResponseHeadersPolicyId is missing" ) matches.append(RuleMatch(path, message)) for index, cache_behavior in enumerate( properties.get("DistributionConfig", {}).get("CacheBehaviors", [])): path = [ "Resources", resource_name, "Properties", "DistributionConfig", "CacheBehaviors", index, ] if not cache_behavior.get("ResponseHeadersPolicyId"): message = f"Property ResponseHeadersPolicyId missing at {'/'.join([str(p) for p in path])}" matches.append(RuleMatch(path, message)) return matches
def check(self, properties, atleastoneprops, path, cfn): """Check itself""" matches = [] for atleastoneprop in atleastoneprops: for (safe_properties, safe_path) in properties.items_safe(path): property_sets = cfn.get_object_without_conditions( safe_properties, atleastoneprop) for property_set in property_sets: count = 0 for prop in atleastoneprop: if prop in property_set['Object']: count += 1 if count == 0: if property_set['Scenario'] is None: message = 'At least one of [{0}] should be specified for {1}' matches.append( RuleMatch( path, message.format( ', '.join(map(str, atleastoneprop)), '/'.join(map(str, safe_path))))) else: scenario_text = ' and '.join([ 'when condition "%s" is %s' % (k, v) for (k, v) in property_set['Scenario'].items() ]) message = 'At least one of [{0}] should be specified {1} at {2}' matches.append( RuleMatch( path, message.format( ', '.join(map(str, atleastoneprop)), scenario_text, '/'.join(map(str, safe_path))))) return matches
def check_attributes(self, cfn, properties, spec_type, path): """ Check the properties against the spec """ matches = [] spec = self.valid_attributes.get('sub').get(spec_type) if isinstance(properties, dict): for p_value_safe, p_path_safe in properties.items_safe(path): for p_name, p_value in p_value_safe.items(): if p_name in spec: up_type_spec = spec.get(p_name) if 'Type' in up_type_spec: matches.extend( self.check_attributes( cfn, p_value, up_type_spec.get('Type'), p_path_safe[:] + [p_name])) else: matches.extend( cfn.check_value( obj={p_name: p_value}, key=p_name, path=p_path_safe[:], check_value=self.check_value, valid_values=up_type_spec.get( 'ValidValues', []), primitive_type=up_type_spec.get( 'PrimitiveType'), key_name=p_name)) else: message = 'UpdatePolicy doesn\'t support type {0}' matches.append( RuleMatch(path[:] + [p_name], message.format(p_name))) else: message = '{0} should be an object' matches.append(RuleMatch(path, message.format(path[-1]))) return matches
def match(self, cfn): """Find all strings and match a deny list of sub strings""" message = '"{0}" may be interpreted as a biased term. Consider a more inclusive alternative, such as {1}' matches = [] def recurse_template(item, path=None): if path is None: path = [] if isinstance(item, dict): for k, v in item.items(): p = path.copy() p.append(k) recurse_template(k, p) recurse_template(v, p) if isinstance(item, list): for i in range(len(item) - 1): p = path.copy() p.append(i) recurse_template(item[i], p) if isinstance(item, str): t = match(item) if t: matches.append(RuleMatch(path, message.format(t[0], t[1]))) return matches recurse_template(cfn.template) return matches if self.id in cfn.template.get("Metadata", {}).get("QSLint", {}).get("Exclusions", []): return matches if "Metadata" in cfn.template.keys(): if "AWS::CloudFormation::Interface" in cfn.template[ "Metadata"].keys(): if "ParameterGroups" in cfn.template["Metadata"][ "AWS::CloudFormation::Interface"].keys(): for x in cfn.template["Metadata"][ "AWS::CloudFormation::Interface"][ "ParameterGroups"]: labels += x['Parameters'] if "Parameters" not in cfn.template.keys(): return matches else: for x in cfn.template["Parameters"]: if str(x) not in labels: matches.append( RuleMatch(["Parameters", x], message.format(x))) return matches
def match(self, cfn): """Check CloudFormation Resources""" matches = [] resources = cfn.template.get('Resources', {}) for resource_name in resources: path = ['Resources', resource_name] if len(resource_name) > LIMITS['resources']['name']: message = 'The length of resource name ({0}) exceeds the limit ({1})' matches.append(RuleMatch(path, message.format(len(resource_name), LIMITS['resources']['name']))) return matches
def check_ref(self, value, parameters, resources, path): # pylint: disable=W0613 """ Check Ref """ matches = [] iam_path = resources.get(value, {}).get('Properties', {}).get('Path') if not iam_path: return matches if iam_path != '/': message = 'When using a Ref to IAM resource the Path must be \'/\'. Switch to GetAtt if the Path has to be \'{}\'.' matches.append( RuleMatch(path, message.format(iam_path))) return matches
def check_az_ref(self, value, path, parameters, resources): """Check ref for AZ""" matches = [] allowed_types = [ 'AWS::EC2::AvailabilityZone::Name', 'String', 'AWS::SSM::Parameter::Value<AWS::EC2::AvailabilityZone::Name>' ] if value in resources: message = 'AvailabilityZone can\'t use a Ref to a resource for {0}' matches.append( RuleMatch(path, message.format(('/'.join(map(str, path)))))) elif value in parameters: parameter = parameters.get(value, {}) param_type = parameter.get('Type', '') if param_type not in allowed_types: param_path = ['Parameters', value, 'Type'] message = 'Availability Zone should be of type [{0}] for {1}' matches.append( RuleMatch( param_path, message.format(', '.join(map(str, allowed_types)), '/'.join(map(str, param_path))))) return matches
def match(self, cfn): """Check CloudFormation Mapping""" matches = [] resources = cfn.template.get('Resources', {}) for resource_name, _ in resources.items(): if not re.match(REGEX_ALPHANUMERIC, resource_name): message = 'Resources {0} has invalid name. Name has to be alphanumeric.' matches.append( RuleMatch(['Resources', resource_name], message.format(resource_name))) return matches
def check_obj(self, obj, required_attributes, path, _): matches = [] for safe_obj, safe_path in obj.items_safe(path): for required_attribute in required_attributes: if required_attribute not in safe_obj: message = 'Property {0} missing at {1}' matches.append( RuleMatch( safe_path, message.format(required_attribute, '/'.join(map(str, safe_path))))) return matches
def check_value(self, value, path): """Count ScheduledExpression value""" matches = [] # Value is either "cron()" or "rate()" if value.startswith('rate(') and value.endswith(')'): matches.extend(self.check_rate(value, path)) elif value.startswith('cron(') and value.endswith(')'): matches.extend(self.check_cron(value, path)) else: message = 'Invalid ScheduledExpression specified ({}). Value has to be either cron() or rate()' matches.append(RuleMatch(path, message.format(value))) return matches
def match(self, cfn): """Check CloudFormation Outputs""" matches = [] outputs = cfn.template.get('Outputs', {}) for output_name in outputs: path = ['Outputs', output_name] if len(output_name) > LIMITS['outputs']['name']: message = 'The length of output name ({0}) exceeds the limit ({1})' matches.append(RuleMatch(path, message.format(len(output_name), LIMITS['outputs']['name']))) return matches
def match(self, cfn): """ Match against Lambda functions without an explicity Timeout """ matches = [] for key, value in cfn.get_resources(["AWS::Lambda::Function"]).items(): timeout = value.get("Properties", {}).get("Timeout", None) if timeout is None: matches.append(RuleMatch(["Resources", key], self._message.format(key))) return matches
def match(self, cfn): """ Match against Lambda functions without tracing enabled """ matches = [] for key, value in cfn.get_resources(["AWS::Lambda::Function"]).items(): tracing_mode = value.get("Properties", {}).get("TracingConfig", {}).get("Mode", None) if tracing_mode != "Active": matches.append(RuleMatch(["Resources", key], self._message.format(key))) return matches
def match(self, cfn): """Check CloudFormation Conditions""" matches = [] conditions = cfn.template.get('Conditions', {}) if conditions: for condname, condobj in conditions.items(): if not isinstance(condobj, dict): message = 'Condition {0} has invalid property' matches.append(RuleMatch( ['Conditions', condname], message.format(condname) )) else: if len(condobj) != 1: message = 'Condition {0} has to many intrinsic conditions' matches.append(RuleMatch( ['Conditions', condname], message.format(condname) )) return matches
def check_rate(self, value, path): """Check Rate configuration""" matches = [] # Extract the expression from rate(XXX) rate_expression = value[value.find('(')+1:value.find(')')] if not rate_expression: matches.append(RuleMatch(path, 'Rate value of ScheduleExpression cannot be empty')) else: # Rate format: rate(Value Unit) items = rate_expression.split(' ') if len(items) != 2: message = 'Rate expression must contain 2 elements (Value Unit), rate contains {} elements' matches.append(RuleMatch(path, message.format(len(items)))) else: # Check the Value if not items[0].isdigit(): message = 'Rate Value ({}) should be of type Integer.' extra_args = {'actual_type': type(items[0]).__name__, 'expected_type': int.__name__} matches.append(RuleMatch(path, message.format(items[0]), **extra_args)) return matches
def match(self, cfn): """Check CloudFormation GetAtt""" matches = [] fnnots = cfn.search_deep_keys('Fn::Not') for fnnot in fnnots: if not isinstance(fnnot[-1], list): message = 'Function Not {0} should be a list' matches.append( RuleMatch(fnnot, message.format('/'.join(map(str, fnnot[:-2]))))) return matches
def match(self, cfn): """Check CloudFormation Parameters""" matches = [] for paramname, paramvalue in cfn.get_parameters().items(): for propname, _ in paramvalue.items(): if propname not in self.valid_keys: message = 'Parameter {0} has invalid property {1}' matches.append( RuleMatch(['Parameters', paramname, propname], message.format(paramname, propname))) return matches
def match(self, cfn): """Check CloudFormation Mappings""" matches = [] pattern = re.compile("^([m][A-Z_0-9]+[a-zA-Z0-9]*)+$") mappings = cfn.template.get('Mappings', {}) if mappings: for mappingname, val in mappings.items(): if pattern.match(mappingname): message = 'Mapping {0} should begin with a lowercase \'m\' and follow mCamelCase' matches.append( RuleMatch(['Mappings', mappingname], message.format(mappingname))) return matches
def match(self, cfn): """Check CloudFormation Outputs""" matches = [] outputs = cfn.template.get('Outputs', {}) if outputs: for output_name, output_value in outputs.items(): if 'Value' not in output_value: message = 'Output {0} is missing property {1}' matches.append(RuleMatch( ['Outputs', output_name, 'Value'], message.format(output_name, 'Value') )) if 'Export' in output_value: if 'Name' not in output_value['Export']: message = 'Output {0} is missing property {1}' matches.append(RuleMatch( ['Outputs', output_name, 'Export'], message.format(output_name, 'Name') )) return matches
def check_metadata_keys(self, cfn): """ Ensure reserved metadata key AWS::CloudFormation::Module is not used """ modules = cfn.get_modules().keys() matches = [] reserved_key = 'AWS::CloudFormation::Module' refs = cfn.search_deep_keys(reserved_key) for ref in refs: if (ref[1] in modules) and (len(ref) > 3): if ref[0] == 'Resources' and ref[2] == 'Metadata': matches.append( RuleMatch( ref, 'The Metadata key {} is reserved'.format( reserved_key))) return matches
def check_version(self, action, path): """Check that action type version is valid.""" matches = [] REGEX_VERSION_STRING = re.compile(r'^[0-9A-Za-z_-]+$') LENGTH_MIN = 1 LENGTH_MAX = 9 version = action.get('ActionTypeId', {}).get('Version') if isinstance(version, dict): self.logger.debug( 'Unable to validate version when an object is used. Skipping') elif isinstance(version, (six.string_types)): if not LENGTH_MIN <= len(version) <= LENGTH_MAX: message = 'Version string ({0}) must be between {1} and {2} characters in length.' matches.append( RuleMatch(path + ['ActionTypeId', 'Version'], message.format(version, LENGTH_MIN, LENGTH_MAX))) elif not re.match(REGEX_VERSION_STRING, version): message = 'Version string must match the pattern [0-9A-Za-z_-]+.' matches.append( RuleMatch(path + ['ActionTypeId', 'Version'], message)) return matches
def match(self, cfn): """Check CloudFormation Parameters""" matches = [] pattern = re.compile("^([p][A-Z_0-9]+[a-zA-Z0-9]*)+$") parameters = cfn.template.get('Parameters', {}) if parameters: for paramname, val in parameters.items(): if not pattern.match(paramname): message = 'Parameters {0} should begin with a lowercase \'p\' and follow pCamelCase' matches.append( RuleMatch(['Parameters', paramname], message.format(paramname))) return matches
def match(self, cfn): """Check CloudFormation Resources""" matches = [] graph = Graph(cfn) for cycle in graph.get_cycles(cfn): source, target = cycle[:2] message = 'Circular Dependencies for resource {0}. Circular dependency with [{1}]'.format( source, target) path = ['Resources', source] matches.append(RuleMatch(path, message)) return matches
def match(self, cfn): """Basic Matching""" matches = [] # Only check if the file exists. The template could be passed in using stdIn if cfn.filename: if Path(cfn.filename).is_file(): statinfo = os.stat(cfn.filename) if statinfo.st_size > LIMITS['template']['body']: message = 'The template file size ({0} bytes) exceeds the limit ({1} bytes)' matches.append( RuleMatch(['Template'], message.format(statinfo.st_size, LIMITS['template']['body']))) return matches
def match(self, cfn): matches = [] ref_objs = cfn.search_deep_keys('Ref') for ref_obj in ref_objs: value = ref_obj[-1] if not isinstance(value, (six.string_types)): message = 'Ref can only be a string for {0}' matches.append( RuleMatch(ref_obj[:-1], message.format('/'.join(map(str, ref_obj[:-1]))))) return matches
def match(self, cfn): """Check CloudFormation Mapping""" matches = [] parameters = cfn.template.get('Parameters', {}) for parameter_name, _ in parameters.items(): if not re.match(REGEX_ALPHANUMERIC, parameter_name): message = 'Parameter {0} has invalid name. Name has to be alphanumeric.' matches.append( RuleMatch(['Parameters', parameter_name], message.format(parameter_name))) return matches
def check_allowed_pattern(self, allowed_value, allowed_pattern, path): """ Check allowed value against allowed pattern """ message = 'Default should be allowed by AllowedPattern' try: if not re.match(allowed_pattern, str(allowed_value)): return ([RuleMatch(path, message)]) except re.error as ex: self.logger.debug( 'Regex pattern "%s" isn\'t supported by Python: %s', allowed_pattern, ex) return []
def findComplementOfLists(listA, listB, message): matches = [] for listAElement in listA: logGroupName = listAElement[1]['Properties']['LogGroupName'] isFound = False for listBElement in listB: if listBElement[1]['Properties'][ 'LogGroupName'] == logGroupName: isFound = True break if not isFound: path = ['Resources', listAElement[0]] matches.append(RuleMatch(path, message)) return matches