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 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 check_vpc_ref(self, value, path, parameters, resources): """Check ref for VPC""" matches = [] allowed_types = [ 'String', 'AWS::SSM::Parameter::Value<String>', ] if value in resources: message = 'DefaultTenancy 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, {}) parameter_type = parameter.get('Type', None) if parameter_type not in allowed_types: path_error = ['Parameters', value, 'Type'] message = 'DefaultTenancy parameter should be of type [{0}] for {1}' matches.append( RuleMatch( path_error, message.format(', '.join(map(str, allowed_types)), '/'.join(map(str, path_error))))) return matches
def match(self, cfn): """Check ELB Resource Parameters""" matches = [] results = cfn.get_resource_properties( ['AWS::ElasticLoadBalancingV2::Listener']) for result in results: matches.extend( cfn.check_value( result['Value'], 'Protocol', result['Path'], check_value=self.check_protocol_value, accepted_protocols=['HTTP', 'HTTPS', 'TCP', 'TLS'], certificate_protocols=['HTTPS', 'TLS'], certificates=result['Value'].get('Certificates'))) results = cfn.get_resource_properties( ['AWS::ElasticLoadBalancing::LoadBalancer', 'Listeners']) for result in results: if isinstance(result['Value'], list): for index, listener in enumerate(result['Value']): matches.extend( cfn.check_value( listener, 'Protocol', result['Path'] + [index], check_value=self.check_protocol_value, accepted_protocols=['HTTP', 'HTTPS', 'TCP', 'SSL'], certificate_protocols=['HTTPS', 'SSL'], certificates=listener.get('SSLCertificateId'))) results = cfn.get_resource_properties( ['AWS::ElasticLoadBalancingV2::LoadBalancer']) for result in results: properties = result['Value'] elb_type = properties.get('Type') if elb_type == 'network': if 'SecurityGroups' in properties: path = result['Path'] + ['SecurityGroups'] matches.append( RuleMatch( path, 'Security groups are not supported for load balancers with type "network"' )) matches.extend(self.check_alb_subnets(properties, result['Path'])) return matches
def check_txt_record(self, value, path): """Check TXT record Configuration""" matches = [] if not isinstance(value, dict) and not re.match(self.REGEX_TXT, value): message = 'TXT record is not structured as one or more items up to 255 characters ' \ 'enclosed in double quotation marks at {0}' matches.append( RuleMatch( path, (message.format('/'.join(map(str, path)))), )) return matches
def match(self, cfn): """Check CloudFormation GetAtt""" matches = list() 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 check_names_unique(self, value, path): """Check that stage names are unique.""" matches = [] stage_names = set() for sidx, stage in enumerate(value): if stage.get('Name') in stage_names: message = 'All stage names within a pipeline must be unique. ({name})'.format( name=stage.get('Name'), ) matches.append(RuleMatch(path + [sidx, 'Name'], message)) stage_names.add(stage.get('Name')) return matches
def match(self, cfn): """Check CloudFormation Mapping""" matches = list() outputs = cfn.template.get('Outputs', {}) for output_name, _ in outputs.items(): if not re.match(REGEX_ALPHANUMERIC, output_name): message = 'Output {0} has invalid name. Name has to be alphanumeric.' matches.append( RuleMatch(['Outputs', output_name], message.format(output_name))) return matches
def check_owner(self, action, path): """Check that action type owner is valid.""" matches = [] owner = action.get('ActionTypeId').get('Owner') if owner not in self.VALID_OWNER_STRINGS and owner is not None: message = ( 'For all currently supported action types, the only valid owner ' 'strings are {owners}').format( owners=', '.join(list(self.VALID_OWNER_STRINGS))) matches.append(RuleMatch(path + ['ActionTypeId', 'Owner'], message)) 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 check_version(self, action, path): """Check that action type version is valid.""" matches = [] version = action.get('ActionTypeId', {}).get('Version') if isinstance(version, dict): self.logger.debug('Unable to validate version when an object is used. Skipping') elif version != '1': message = 'For all currently supported action types, the only valid version string is "1".' matches.append(RuleMatch( path + ['ActionTypeId', 'Version'], message )) 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 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 match(self, cfn): """Basic Matching""" matches = list() top_level = [] for x in cfn.template: top_level.append(x) if x not in self.valid_keys: message = "Top level item {0} isn't valid" matches.append(RuleMatch( [x], message.format(x) )) for y in self.required_keys: if y not in top_level: message = "Missing top level item {0} to file module" matches.append(RuleMatch( ['AWSTemplateFormatVersion'], message.format(y) )) return matches
def match(self, cfn): """Check CloudFormation Mappings""" matches = [] mappings = cfn.template.get('Mappings', {}) for mapping_name in mappings: path = ['Mappings', mapping_name] if len(mapping_name) > LIMITS['mappings']['name']: message = 'The length of mapping name ({0}) exceeds the limit ({1})' matches.append(RuleMatch(path, message.format(len(mapping_name), LIMITS['mappings']['name']))) return matches
def check_cidr_ref(self, value, path, parameters, resources): """Check ref for VPC""" matches = list() if value in parameters: parameter = parameters.get(value, {}) allowed_pattern = parameter.get('AllowedPattern', None) allowed_values = parameter.get('AllowedValues', None) if not allowed_pattern and not allowed_values: param_path = ['Parameters', value] message = 'AllowedPattern and/or AllowedValues for Parameter should be specified at {1}. Example for AllowedPattern "{0}"' matches.append(RuleMatch(param_path, message.format(self.cidr_regex, ('/'.join(param_path))))) return matches
def check_names_unique(self, action, path, action_names): """Check that action names are unique.""" matches = [] action_name = action.get('Name') if isinstance(action_name, six.string_types): if action.get('Name') in action_names: message = 'All action names within a stage must be unique. ({name})'.format( name=action.get('Name') ) matches.append(RuleMatch(path + ['Name'], message)) action_names.add(action.get('Name')) return matches
def match(self, cfn): """Check CloudFormation Parameters""" matches = list() # Check number of parameters against the defined limit parameters = cfn.template.get('Parameters', {}) if len(parameters) > LIMITS['parameters']['number']: message = 'Maximum number of parameters ({0}) exceeded' matches.append( RuleMatch(['Parameters'], message.format(LIMITS['parameters']['number']))) return matches
def check_first_stage(self, stages, path): """Validate the first stage of a pipeline has source actions.""" matches = [] if len(stages) < 1: # pylint: disable=C1801 self.logger.debug( 'Stages was empty. Should have been caught by generic linting.' ) return matches # pylint: disable=R1718 first_stage = set([ a.get('ActionTypeId').get('Category') for a in stages[0]['Actions'] ]) if first_stage and 'Source' not in first_stage: message = 'The first stage of a pipeline must contain at least one source action.' matches.append(RuleMatch(path + [0], message)) if len(first_stage) != 1: message = 'The first stage of a pipeline must contain only source actions.' matches.append(RuleMatch(path + [0], message)) 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 check_mx_record(self, path, recordset): """Check MX record Configuration""" matches = [] resource_records = recordset.get('ResourceRecords') for index, record in enumerate(resource_records): tree = path[:] + ['ResourceRecords', index] if not isinstance(record, dict): # Split the record up to the mandatory settings (priority domainname) items = record.split(' ') # Check if the 3 settings are given. if len(items) != 2: message = 'MX record must contain 2 settings (priority domainname), record contains {} settings.' matches.append( RuleMatch(tree, message.format(len(items), record))) else: # Check the priority value if not items[0].isdigit(): message = 'MX record priority setting ({}) should be of type Integer.' matches.append( RuleMatch(tree, message.format(items[0], record))) else: if not 0 <= int(items[0]) <= 65535: message = 'Invalid MX record priority setting ({}) given, must be between 0 and 65535.' matches.append( RuleMatch(tree, message.format(items[0], record))) # Check the domainname value if not re.match(self.REGEX_DOMAINNAME, items[1]): matches.append( RuleMatch(tree, message.format(items[1]))) return matches
def match(self, cfn): """Check Ec2 Ebs Resource Parameters""" matches = list() results = cfn.get_resource_properties( ['AWS::EC2::Instance', 'BlockDeviceMappings']) results.extend( cfn.get_resource_properties([ 'AWS::AutoScaling::LaunchConfiguration', 'BlockDeviceMappings' ])) for result in results: path = result['Path'] for index, properties in enumerate(result['Value']): virtual_name = properties.get('VirtualName') ebs = properties.get('Ebs') if not virtual_name and not ebs: pathmessage = path[:] + [index] message = "Property ebs and virtual_name cannot be used together for {0}" matches.append( RuleMatch( pathmessage, message.format('/'.join(map(str, pathmessage))))) elif virtual_name: # switch to regex if not re.match(r'^ephemeral[0-9]$', virtual_name): pathmessage = path[:] + [index, 'VirtualName'] message = "Property VirtualName should be of type ephemeral(n) for {0}" matches.append( RuleMatch( pathmessage, message.format('/'.join(map(str, pathmessage))))) elif ebs: matches.extend( self._checkEbs(cfn, ebs, path[:] + [index, 'Ebs'])) return matches
def match(self, cfn): """Check CloudFormation Join""" matches = list() split_objs = cfn.search_deep_keys('Fn::Split') supported_functions = [ 'Fn::Base64', 'Fn::FindInMap', 'Fn::GetAtt', 'Fn::GetAZs', 'Fn::ImportValue', 'Fn::If', 'Fn::Join', 'Fn::Select', '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, six.string_types): 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 = 'Split unsupported function for {0}' matches.append( RuleMatch( tree + [key], message.format('/'.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, six.string_types): 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 check_value(self, key, path, res_type): """Check resource names for UpdateReplacePolicy""" matches = [] valid_values = [ 'Delete', 'Retain', 'Snapshot' ] valid_snapshot_types = [ 'AWS::EC2::Volume', 'AWS::ElastiCache::CacheCluster', 'AWS::ElastiCache::ReplicationGroup', 'AWS::Neptune::DBCluster', 'AWS::RDS::DBCluster', 'AWS::RDS::DBInstance', 'AWS::Redshift::Cluster' ] if not isinstance(key, (six.text_type, six.string_types)): message = 'UpdateReplacePolicy values should be of string at {0}' matches.append(RuleMatch(path, message.format('/'.join(map(str, path))))) return matches if key not in valid_values: message = 'UpdateReplacePolicy should be only one of {0} at {1}' matches.append(RuleMatch( path, message.format(', '.join(map(str, valid_values)), '/'.join(map(str, path))))) if key == 'Snapshot' and res_type not in valid_snapshot_types: message = 'UpdateReplacePolicy cannot be Snapshot for resources of type {0} at {1}' matches.append(RuleMatch( path, message.format(res_type, '/'.join(map(str, path))))) return matches
def match(self, cfn): """Check CloudFormation ImportValue""" matches = list() iv_objs = cfn.search_deep_keys('Fn::ImportValue') supported_functions = [ 'Fn::Base64', 'Fn::FindInMap', 'Fn::If', 'Fn::Join', 'Fn::Select', 'Fn::Split', 'Fn::Sub', 'Ref' ] for iv_obj in iv_objs: iv_value = iv_obj[-1] tree = iv_obj[:-1] if isinstance(iv_value, dict): if len(iv_value) == 1: for key, _ in iv_value.items(): if key not in supported_functions: message = "ImportValue should be using supported function for {0}" matches.append(RuleMatch( tree, message.format('/'.join(map(str, tree[:-1]))))) else: message = "ImportValue should have one mapping for {0}" matches.append(RuleMatch( tree, message.format('/'.join(map(str, tree[:-1]))))) elif not isinstance(iv_value, six.string_types): message = "ImportValue should have supported function or string for {0}" matches.append(RuleMatch( tree, message.format('/'.join(map(str, tree))))) return matches
def match(self, cfn): """Check CloudFormation VpcId Parameters""" matches = list() # Build the list of refs trees = cfn.search_deep_keys('VpcId') parameters = cfn.get_parameter_names() allowed_types = [ 'AWS::EC2::VPC::Id', 'AWS::SSM::Parameter::Value<AWS::EC2::VPC::Id>' ] fix_param_types = set() trees = [x for x in trees if x[0] == 'Resources'] for tree in trees: obj = tree[-1] if isinstance(obj, dict): if len(obj) == 1: for key in obj: if key == 'Ref': paramname = obj[key] if paramname in parameters: param = cfn.template['Parameters'][paramname] if 'Type' in param: paramtype = param['Type'] if paramtype not in allowed_types: fix_param_types.add(paramname) else: message = 'Innappropriate map found for vpcid on %s' % ( '/'.join(map(str, tree[:-1]))) matches.append(RuleMatch(tree[:-1], message)) for paramname in fix_param_types: message = 'Parameter %s should be of type %s' % (paramname, ', '.join(map(str, allowed_types))) tree = ['Parameters', paramname] matches.append(RuleMatch(tree, message)) return matches
def check_sgid_ref(self, value, path, parameters, resources): """Check ref for VPC""" matches = [] allowed_types = [ 'AWS::SSM::Parameter::Value<AWS::EC2::SecurityGroup::Id>', 'AWS::EC2::SecurityGroup::Id', 'String' ] if value in parameters: parameter_properties = parameters.get(value) parameter_type = parameter_properties.get('Type') if parameter_type not in allowed_types: path_error = ['Parameters', value, 'Type'] message = 'Security Group Id Parameter should be of type [{0}] for {1}' matches.append( RuleMatch( path_error, message.format( ', '.join(map(str, allowed_types)), '/'.join(map(str, path_error))))) if value in resources: resource = resources.get(value, {}) resource_type = resource.get('Type', '') if resource_type != 'AWS::EC2::SecurityGroup': message = 'Security Group Id resources should be of type AWS::EC2::SecurityGroup for {0}' matches.append( RuleMatch(path, message.format('/'.join(map(str, path))))) else: resource_properties = resource.get('Properties', {}) vpc_property = resource_properties.get('VpcId', None) if not vpc_property: message = 'Security Group Id should reference a VPC based AWS::EC2::SecurityGroup for {0}' matches.append( RuleMatch(path, message.format('/'.join(map(str, path))))) return matches
def match(self, cfn): """Check CloudFormation Conditions""" matches = [] ref_conditions = {} # Get all defined conditions conditions = cfn.template.get('Conditions', {}) # Get all "If's" that reference a Condition iftrees = cfn.search_deep_keys('Fn::If') for iftree in iftrees: if isinstance(iftree[-1], list): ref_conditions[iftree[-1][0]] = iftree else: ref_conditions[iftree[-1]] = iftree # Get resource's Conditions for resource_name, resource_values in cfn.get_resources().items(): condition = resource_values.get('Condition') if isinstance(condition, six.string_types): # make sure its a string path = ['Resources', resource_name, 'Condition'] ref_conditions[condition] = path # Get conditions used by another condition condtrees = cfn.search_deep_keys('Condition') for condtree in condtrees: if condtree[0] == 'Conditions': if isinstance(condtree[-1], (str, six.text_type, six.string_types)): path = ['Conditions', condtree[-1]] ref_conditions[condtree[-1]] = path # Get Output Conditions for _, output_values in cfn.template.get('Outputs', {}).items(): if 'Condition' in output_values: path = ['Outputs', output_values['Condition']] ref_conditions[output_values['Condition']] = path # Check if all the conditions are defined for ref_condition, ref_path in ref_conditions.items(): if ref_condition not in conditions: message = 'Condition {0} is not defined.' matches.append(RuleMatch( ref_path, message.format(ref_condition) )) return matches
def match(self, cfn): """Check CloudFormation Conditions""" matches = list() ref_conditions = list() conditions = cfn.template.get('Conditions', {}) if conditions: # Get all "If's" that reference a Condition iftrees = cfn.search_deep_keys('Fn::If') for iftree in iftrees: if isinstance(iftree[-1], list): ref_conditions.append(iftree[-1][0]) else: ref_conditions.append(iftree[-1]) # Get resource's Conditions for _, resource_values in cfn.get_resources().items(): if 'Condition' in resource_values: ref_conditions.append(resource_values['Condition']) # Get conditions used by another condition condtrees = cfn.search_deep_keys('Condition') for condtree in condtrees: if condtree[0] == 'Conditions': if isinstance(condtree[-1], (str, six.text_type, six.string_types)): ref_conditions.append(condtree[-1]) # Get resource's Conditions for _, resource_values in cfn.get_resources().items(): if 'Condition' in resource_values: ref_conditions.append(resource_values['Condition']) # Get Output Conditions for _, output_values in cfn.template.get('Outputs', {}).items(): if 'Condition' in output_values: ref_conditions.append(output_values['Condition']) # Check if the confitions are used for condname, _ in conditions.items(): if condname not in ref_conditions: message = 'Condition {0} not used' matches.append(RuleMatch( ['Conditions', condname], message.format(condname) )) return matches
def check(self, properties, exclusions, path): """Check itself""" matches = list() for prop in properties: if prop in exclusions: for excl_property in exclusions[prop]: if excl_property in properties: message = 'Parameter {0} should NOT exist with {1} for {2}' matches.append(RuleMatch( path + [prop], message.format(excl_property, prop, '/'.join(map(str, path))) )) return matches