Beispiel #1
0
    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
Beispiel #2
0
    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
Beispiel #3
0
    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
Beispiel #4
0
    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
Beispiel #5
0
    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
Beispiel #6
0
    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
Beispiel #7
0
    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
Beispiel #8
0
    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
Beispiel #9
0
    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
Beispiel #10
0
    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
Beispiel #11
0
    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)
Beispiel #12
0
    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
Beispiel #13
0
    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
Beispiel #14
0
 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
Beispiel #15
0
    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
Beispiel #16
0
    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
Beispiel #17
0
    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
Beispiel #21
0
    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
Beispiel #24
0
    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