def check_allowed_pattern(self, allowed_value, allowed_pattern, path): """ Check allowed value against allowed pattern """ message = 'Default should be allowed by AllowedPattern' if not re.match(allowed_pattern, allowed_value): return ([RuleMatch(path, message)]) return []
def check_allowed_values(self, allowed_value, allowed_values, path): """ Check allowed value against allowed values """ message = 'Default should be a value within AllowedValues' if allowed_value not in allowed_values: return ([RuleMatch(path, message)]) return []
def check_az_value(self, value, path): """Check ref for VPC""" matches = list() if path[-1] != 'Fn::GetAZs': message = 'Don\'t hardcode {0} for AvailabilityZones at {1}' full_path = ('/'.join(str(x) for x in path)) matches.append(RuleMatch(path, message.format(value, full_path))) return matches
def check_version(self, action, path): """Check that action type version is valid.""" matches = [] if action.get('ActionTypeId').get('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 check_az_ref(self, value, path, parameters, resources): """Check ref for AZ""" matches = [] allowed_types = ['AWS::EC2::AvailabilityZone::Name', 'String'] if value in resources: message = 'AvailabilityZone can\'t use a Ref to a resource for {0}' matches.append(RuleMatch(path, message.format(('/'.join(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): """ Check runtime value """ matches = list() message = 'You must specify a valid value for runtime at {0}' if value not in self.runtimes: matches.append(RuleMatch(path, message.format(value, ('/'.join(path))))) 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_cron(self, value, path): """Check Cron configuration""" matches = [] # Extract the expression from cron(XXX) cron_expression = value[value.find('(') + 1:value.find(')')] if not cron_expression: matches.append( RuleMatch(path, 'Cron value of ScheduleExpression cannot be empty')) else: # Rate format: cron(Minutes Hours Day-of-month Month Day-of-week Year) items = cron_expression.split(' ') if len(items) != 6: message = 'Cron expression must contain 6 elements (Minutes Hours Day-of-month Month Day-of-week Year), cron contains {} elements' matches.append(RuleMatch(path, message.format(len(items)))) return matches
def check_value(self, value, path, **kwargs): """ Check a primitive value """ matches = [] prim_type = kwargs.get('primitive_type') if prim_type == 'List': valid_values = kwargs.get('valid_values') if valid_values: if value not in valid_values: message = 'Allowed values for {0} are ({1})' matches.append( RuleMatch(path, message.format(kwargs.get('key_name'), ', '.join(map(str, valid_values))))) else: default_message = 'Value for {0} must be of type {1}' if not isinstance(value, self.valid_attributes.get('primitive_types').get(prim_type)): matches.append( RuleMatch(path, default_message.format(kwargs.get('key_name'), prim_type))) return matches
def match(self, cfn): """Check CloudFormation GetAz""" matches = list() getaz_objs = cfn.search_deep_keys('Fn::GetAZs') for getaz_obj in getaz_objs: getaz_value = getaz_obj[-1] if isinstance(getaz_value, six.string_types): if getaz_value != '' and getaz_value not in cfn.regions: message = 'GetAZs should be of empty or string of valid region for {0}' matches.append( RuleMatch( getaz_obj[:-1], message.format('/'.join(map(str, getaz_obj[:-1]))))) elif isinstance(getaz_value, dict): if len(getaz_value) == 1: if isinstance(getaz_value, dict): for key, value in getaz_value.items(): if key != 'Ref' or value != 'AWS::Region': message = 'GetAZs should be of Ref to AWS::Region for {0}' matches.append( RuleMatch( getaz_obj[:-1], message.format('/'.join( map(str, getaz_obj[:-1]))))) else: message = 'GetAZs should be of Ref to AWS::Region for {0}' matches.append( RuleMatch( getaz_obj[:-1], message.format('/'.join( map(str, getaz_obj[:-1]))))) else: message = 'GetAZs should be of Ref to AWS::Region for {0}' matches.append( RuleMatch( getaz_obj[:-1], message.format('/'.join(map(str, getaz_obj[:-1]))))) return matches
def check_txt_record(self, path, recordset): """Check TXT record Configuration""" matches = list() # Check quotation of the records resource_records = recordset.get('ResourceRecords') for index, record in enumerate(resource_records): tree = path[:] + ['ResourceRecords', index] full_path = ('/'.join(str(x) for x in tree)) if not record.startswith('"') or not record.endswith('"'): message = 'TXT record has to be enclosed in double quotation marks (") at {0}' matches.append(RuleMatch(tree, message.format(full_path))) elif len(record) > 255: message = 'The length of the TXT record ({0}) exceeds the limit (255) as {1}' matches.append(RuleMatch(tree, message.format(len(record), full_path))) return matches
def match(self, cfn): """Check CloudFormation And""" matches = [] # Build the list of functions equal_trees = cfn.search_deep_keys('Fn::And') for equal_tree in equal_trees: equal = equal_tree[-1] if not isinstance(equal, list): message = 'Fn::And must be a list of between 2 to 10 conditions' matches.append(RuleMatch(equal_tree[:-1], message.format())) elif not (2 <= len(equal) <= 10): message = 'Fn::And must be a list of between 2 to 10 conditions' matches.append(RuleMatch(equal_tree[:-1], message.format())) else: for index, element in enumerate(equal): if isinstance(element, dict): if len(element) == 1: for element_key in element.keys(): if element_key not in [ 'Fn::And', 'Fn::Or', 'Fn::Not', 'Condition', 'Fn::Equals' ]: message = 'Fn::And list must be another valid condition' matches.append( RuleMatch( equal_tree[:-1] + [index, element_key], message.format())) else: message = 'Fn::And list must be another valid condition' matches.append( RuleMatch(equal_tree[:-1] + [index], message.format())) else: message = 'Fn::And list must be another valid condition' matches.append( RuleMatch(equal_tree[:-1] + [index], message.format())) return matches
def check_az_value(self, value, path): """Check AZ Values""" matches = [] if value not in AVAILABILITY_ZONES: message = 'Not a valid Availbility Zone {0} at {1}' matches.append( RuleMatch(path, message.format(value, ('/'.join(map(str, path)))))) return matches
def check_vpc_value(self, value, path): """Check VPC Values""" matches = [] if not value.startswith('vpc-'): message = 'VpcId needs to be of format vpc-xxxxxxxx at {1}' matches.append( RuleMatch(path, message.format(value, ('/'.join(map(str, path)))))) return matches
def check_ns_record(self, value, path): """Check NS record Configuration""" matches = [] if not isinstance(value, dict): if not re.match(self.REGEX_DOMAINNAME, value): message = 'NS record ({}) does not contain a valid domain name' matches.append(RuleMatch(path, message.format(value))) return matches
def check_ref(self, value, path, parameters, resources): """ Check Memory Size Ref """ matches = list() if value in resources: message = 'Runtime can\'t use a Ref to a resource for {0}' matches.append(RuleMatch(path, message.format(('/'.join(path))))) elif value in parameters: parameter = parameters.get(value, {}) param_type = parameter.get('Type', '') if param_type != 'String': param_path = ['Parameters', value, 'Type'] message = 'Type for Parameter should be String at {0}' matches.append( RuleMatch(param_path, message.format( ('/'.join(param_path))))) return matches
def check_a_record(self, value, path): """Check A record Configuration""" matches = [] # Check if a valid IPv4 address is specified if not re.match(REGEX_IPV4, value): message = 'A record ({}) is not a valid IPv4 address' matches.append(RuleMatch(path, message.format(value))) return matches
def check_value(self, value, path): """Check SecurityGroup descriptions""" matches = list() full_path = ('/'.join(str(x) for x in path)) # Check max length if len(value) > 255: message = 'GroupDescription length ({0}) exceeds the limit (255) at {1}' matches.append( RuleMatch(path, message.format(len(value), full_path))) else: # Check valid characters regex = re.compile(self.description_regex) if not regex.match(value): message = 'GroupDescription contains invalid characters (valid characters are: '\ '"a-zA-Z0-9. _-:/()#,@[]+=&;\\{{}}!$*"") at {0}' matches.append(RuleMatch(path, message.format(full_path))) return matches
def match(self, cfn): """Check CloudFormation Not""" matches = [] # Build the list of functions not_trees = cfn.search_deep_keys('Fn::Not') for not_tree in not_trees: not_value = not_tree[-1] if not isinstance(not_value, list): message = 'Fn::Not must be a list of exactly 1 condition' matches.append(RuleMatch(not_tree[:-1], message.format())) elif not len(not_value) == 1: message = 'Fn::Not must be a list of exactly 1 condition' matches.append(RuleMatch(not_tree[:-1], message.format())) else: for index, element in enumerate(not_value): if isinstance(element, dict): if len(element) == 1: for element_key in element.keys(): if element_key not in [ 'Fn::And', 'Fn::Or', 'Fn::Not', 'Condition', 'Fn::Equals' ]: message = 'Fn::Or list must be another valid condition' matches.append( RuleMatch( not_tree[:-1] + [index, element_key], message.format())) else: message = 'Fn::Or list must be another valid condition' matches.append( RuleMatch(not_tree[:-1] + [index], message.format())) else: message = 'Fn::Or list must be another valid condition' matches.append( RuleMatch(not_tree[:-1] + [index], message.format())) return matches
def check_cidr_value(self, value, path): """Check CIDR Strings""" matches = [] if not re.match(REGEX_CIDR, value): message = 'CidrBlock needs to be of x.x.x.x/y at {0}' matches.append( RuleMatch(path, message.format(('/'.join(['Parameters', value]))))) return matches
def match(self, cfn): """Check CloudFormation IamInstanceProfile Parameters""" matches = list() # Build the list of keys trees = cfn.search_deep_keys('Fn::GetAtt') # Filter only resoureces # Disable pylint for Pylint 2 # pylint: disable=W0110 trees = filter(lambda x: x[0] == 'Resources', trees) for tree in trees: if any(e == 'IamInstanceProfile' for e in tree): obj = tree[-1] objtype = cfn.template.get('Resources', {}).get(obj[0], {}).get('Type') if objtype: if objtype != 'AWS::IAM::InstanceProfile': message = 'Property IamInstanceProfile should relate to AWS::IAM::InstanceProfile for %s' % ( '/'.join(map(str, tree[:-1]))) matches.append(RuleMatch(tree[:-1], message)) else: if obj[1] == 'Arn': message = 'Property IamInstanceProfile shouldn\'t be an ARN for %s' % ( '/'.join(map(str, tree[:-1]))) matches.append(RuleMatch(tree[:-1], message)) # Search Refs trees = cfn.search_deep_keys('Ref') # Filter only resoureces trees = filter(lambda x: x[0] == 'Resources', trees) for tree in trees: if any(e == 'IamInstanceProfile' for e in tree): obj = tree[-1] objtype = cfn.template.get('Resources', {}).get(obj, {}).get('Type') if objtype: if objtype != 'AWS::IAM::InstanceProfile': message = 'Property IamInstanceProfile should relate to AWS::IAM::InstanceProfile for %s' % ( '/'.join(map(str, tree[:-1]))) matches.append(RuleMatch(tree[:-1], message)) return matches
def match(self, cfn): """Check EC2 Security Group Ingress Resource Parameters""" matches = list() resources = cfn.get_resources(resource_type='AWS::EC2::SecurityGroup') for resource_name, resource_object in resources.items(): properties = resource_object.get('Properties', {}) if properties: vpc_id = properties.get('VpcId', None) ingress_rules = properties.get('SecurityGroupIngress') if isinstance(ingress_rules, list): for index, ingress_rule in enumerate(ingress_rules): path = [ 'Resources', resource_name, 'Properties', 'SecurityGroupIngress', index ] matches.extend( self.check_ingress_rule( vpc_id=vpc_id, properties=ingress_rule, path=path, cfn=cfn ) ) resources = None resources = cfn.get_resources(resource_type='AWS::EC2::SecurityGroupIngress') for resource_name, resource_object in resources.items(): properties = resource_object.get('Properties', {}) group_id = properties.get('GroupId', None) group_name = properties.get('GroupName', None) path = ['Resources', resource_name, 'Properties'] if group_id and not group_name: vpc_id = 'vpc-1234567' elif group_name and not group_id: vpc_id = None else: message = "GroupId and GroupName shouldn't be specified together " \ "at {0}" matches.append( RuleMatch(path, message.format('/'.join(map(str, path))))) continue if properties: path = ['Resources', resource_name, 'Properties'] matches.extend( self.check_ingress_rule( vpc_id=vpc_id, properties=properties, path=path, cfn=cfn ) ) return matches
def _check_number_value(self, value, path, **kwargs): """ Check if the value is in the given ranges""" matches = [] number_min = kwargs.get('number_min') number_max = kwargs.get('number_max') # The Python types considered a "number" if sys.version_info < (3, ): number_types = ( float, int, long, ) # pylint: disable=undefined-variable else: number_types = ( float, int, ) if isinstance(value, six.string_types): try: value = float(value) except ValueError: message = 'Value has to be between {0} and {1} at {2}' matches.append( RuleMatch( path, message.format(number_min, number_max, '/'.join(map(str, path))), )) if isinstance(value, number_types): if not (number_min <= value <= number_max): message = 'Value has to be between {0} and {1} at {2}' matches.append( RuleMatch( path, message.format(number_min, number_max, '/'.join(map(str, path))), )) return matches
def check_caa_record(self, path, recordset): """Check CAA 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 (flags tag "value") items = record.split(' ', 2) # Check if the 3 settings are given. if len(items) != 3: message = 'CAA record must contain 3 settings (flags tag "value"), record contains {} settings.' matches.append(RuleMatch(tree, message.format(len(items)))) else: # Check the flag value if not items[0].isdigit(): message = 'CAA record flag setting ({}) should be of type Integer.' matches.append( RuleMatch(tree, message.format(items[0]))) else: if int(items[0]) not in [0, 128]: message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.' matches.append( RuleMatch(tree, message.format(items[0]))) # Check the tag value if not re.match(REGEX_ALPHANUMERIC, items[1]): message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.' matches.append( RuleMatch(tree, message.format(items[0]))) # Check the value if not items[2].startswith('"') or not items[2].endswith( '"'): message = 'CAA record value setting has to be enclosed in double quotation marks (").' matches.append(RuleMatch(tree, message)) return matches
def _value_check(self, value, path, item_type, extra_args): """ Checks non strict """ matches = [] if not self.config['strict']: try: if item_type in ['String']: str(value) elif item_type in ['Boolean']: if value not in ['True', 'true', 'False', 'false']: message = 'Property %s should be of type %s' % ( '/'.join(map(str, path)), item_type) matches.append(RuleMatch(path, message, **extra_args)) elif item_type in ['Integer', 'Long', 'Double']: if isinstance(value, bool): message = 'Property %s should be of type %s' % ( '/'.join(map(str, path)), item_type) matches.append(RuleMatch(path, message, **extra_args)) elif item_type in ['Integer']: int(value) elif item_type in ['Long']: # Some times python will strip the decimals when doing a conversion if isinstance(value, float): message = 'Property %s should be of type %s' % ( '/'.join(map(str, path)), item_type) matches.append( RuleMatch(path, message, **extra_args)) if sys.version_info < (3, ): long(value) # pylint: disable=undefined-variable else: int(value) else: # has to be a Double float(value) except Exception: # pylint: disable=W0703 message = 'Property %s should be of type %s' % ('/'.join( map(str, path)), item_type) matches.append(RuleMatch(path, message, **extra_args)) else: message = 'Property %s should be of type %s' % ('/'.join( map(str, path)), item_type) matches.append(RuleMatch(path, message, **extra_args)) return matches
def check_policy_document(self, value, path, cfn, is_identity_policy, resource_exceptions): """Check policy document""" matches = [] valid_keys = [ 'Version', 'Id', 'Statement', ] valid_versions = [ '2012-10-17', '2008-10-17', date(2012, 10, 17), date(2008, 10, 17) ] if not isinstance(value, dict): message = 'IAM Policy Documents needs to be JSON' matches.append(RuleMatch(path[:], message)) return matches for parent_key, parent_value in value.items(): if parent_key not in valid_keys: message = 'IAM Policy key %s doesn\'t exist.' % (parent_key) matches.append(RuleMatch(path[:] + [parent_key], message)) if parent_key == 'Version': if parent_value not in valid_versions: message = 'IAM Policy Version needs to be one of (%s).' % ( ', '.join(map(str, ['2012-10-17', '2008-10-17']))) matches.append(RuleMatch(path[:] + [parent_key], message)) if parent_key == 'Statement': if isinstance(parent_value, (list)): statements = cfn.get_values(value, 'Statement', path[:]) for statement in statements: matches.extend( self._check_policy_statement( statement['Path'], statement['Value'], is_identity_policy, resource_exceptions)) else: message = 'IAM Policy statement should be of list.' matches.append(RuleMatch(path[:] + [parent_key], message)) return matches
def check_keys(self, map_name, keys, mappings, tree): """ Check the validity of the first key """ matches = [] first_key = keys[0] second_key = keys[1] if isinstance(second_key, (six.string_types, int)): if isinstance(map_name, (six.string_types)): mapping = mappings.get(map_name) if mapping: if isinstance(first_key, (six.string_types, int)): if isinstance(map_name, (six.string_types)): if mapping.get(first_key) is None: message = 'FindInMap first key "{0}" doesn\'t exist in map "{1}" at {3}' matches.append( RuleMatch( tree[:] + [1], message.format( first_key, map_name, first_key, '/'.join(map(str, tree))))) if mapping.get(first_key): # Don't double error if they first key doesn't exist if mapping.get(first_key, {}).get(second_key) is None: message = 'FindInMap second key "{0}" doesn\'t exist in map "{1}" under "{2}" at {3}' matches.append( RuleMatch( tree[:] + [2], message.format( second_key, map_name, first_key, '/'.join(map(str, tree))))) else: for key, value in mapping.items(): if value.get(second_key) is None: message = 'FindInMap second key "{0}" doesn\'t exist in map "{1}" under "{2}" at {3}' matches.append( RuleMatch( tree[:] + [2], message.format( second_key, map_name, key, '/'.join(map(str, tree))))) return matches
def check_stage_count(self, stages, path, scenario): """Check that there is minimum 2 stages.""" matches = [] if len(stages) < 2: message = 'CodePipeline has {} stages. There must be at least two stages.'.format( len(stages)) matches.append( RuleMatch(path, self._format_error_message(message, scenario))) return matches
def check_txt_record(self, path, recordset): """Check TXT record Configuration""" matches = [] # Check quotation of the records resource_records = recordset.get('ResourceRecords') for index, record in enumerate(resource_records): tree = path[:] + ['ResourceRecords', index] if not isinstance(record, dict): if not record.startswith('"') or not record.endswith('"'): message = 'TXT record ({}) has to be enclosed in double quotation marks (")' matches.append(RuleMatch(tree, message.format(record))) elif len(record) > 255: message = 'The length of the TXT record ({}) exceeds the limit (255)' matches.append(RuleMatch(tree, message.format(len(record)))) return matches
def check_cname_record(self, path, recordset): """Check CNAME record Configuration""" matches = [] resource_records = recordset.get('ResourceRecords') if len(resource_records) > 1: message = 'A CNAME recordset can only contain 1 value' matches.append(RuleMatch(path + ['ResourceRecords'], message)) else: for index, record in enumerate(resource_records): if not isinstance(record, dict): tree = path[:] + ['ResourceRecords', index] if (not re.match(self.REGEX_CNAME, record) # ACM Route 53 validation uses invalid CNAMEs starting with `_`, # special-case them rather than complicate the regex. and not record.endswith('.acm-validations.aws.')): message = 'CNAME record ({}) does not contain a valid domain name' matches.append(RuleMatch(tree, message.format(record))) return matches