예제 #1
0
def test_get_control_param_dict(tmp_trestle_dir: pathlib.Path) -> None:
    """Test getting the param dict of a control."""
    test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True)
    prof_a_path = ModelUtils.path_for_top_level_model(tmp_trestle_dir,
                                                      'test_profile_a',
                                                      prof.Profile,
                                                      FileContentType.JSON)
    catalog = ProfileResolver.get_resolved_profile_catalog(
        tmp_trestle_dir, prof_a_path)
    catalog_interface = CatalogInterface(catalog)
    control = catalog_interface.get_control('ac-1')
    param_dict = ControlIOReader.get_control_param_dict(control, False)
    # confirm profile value is used
    assert ControlIOReader.param_values_as_str(
        param_dict['ac-1_prm_1']) == 'all alert personnel'
    # confirm original param label is used since no value was assigned
    assert ControlIOReader.param_to_str(param_dict['ac-1_prm_7'],
                                        ParameterRep.VALUE_OR_LABEL_OR_CHOICES
                                        ) == 'organization-defined events'
    param = control.params[0]
    param.values = None
    param.select = common.ParameterSelection(
        how_many=common.HowMany.one_or_more, choice=['choice 1', 'choice 2'])
    param_dict = ControlIOReader.get_control_param_dict(control, False)
    assert ControlIOReader.param_to_str(
        param_dict['ac-1_prm_1'],
        ParameterRep.VALUE_OR_LABEL_OR_CHOICES) == 'choice 1, choice 2'
def test_profile_resolver(tmp_trestle_dir: pathlib.Path) -> None:
    """Test the resolver."""
    test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True)

    prof_a_path = ModelUtils.path_for_top_level_model(
        tmp_trestle_dir, 'test_profile_a', prof.Profile, FileContentType.JSON
    )
    cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_a_path)
    interface = CatalogInterface(cat)
    # added part ac-1_expevid from prof a
    list1 = find_string_in_all_controls_prose(interface, 'Detailed evidence logs')
    # modify param ac-3.3_prm_2 in prof b
    list2 = find_string_in_all_controls_prose(interface, 'full and complete compliance')

    assert len(list1) == 1
    assert len(list2) == 1

    assert interface.get_count_of_controls_in_catalog(False) == 6

    assert interface.get_count_of_controls_in_catalog(True) == 7

    assert len(cat.controls) == 4

    assert interface.get_dependent_control_ids('ac-3') == ['ac-3.3']

    control = interface.get_control('a-1')
    assert control.parts[0].parts[0].id == 'a-1_deep'
    assert control.parts[0].parts[0].prose == 'Extra added part in subpart'
def test_profile_resolver_merge(sample_catalog_rich_controls: cat.Catalog) -> None:
    """Test profile resolver merge."""
    profile = gens.generate_sample_model(prof.Profile)
    method = prof.Method.merge
    combine = prof.Combine(method=method)
    profile.merge = prof.Merge(combine=combine)
    merge = Merge(profile)

    # merge into empty catalog
    merged = gens.generate_sample_model(cat.Catalog)
    new_merged = merge._merge_catalog(merged, sample_catalog_rich_controls)
    catalog_interface = CatalogInterface(new_merged)
    assert catalog_interface.get_count_of_controls_in_catalog(True) == 5

    # add part to first control and merge, then make sure it is there
    part = com.Part(name='foo', title='added part')
    control_id = sample_catalog_rich_controls.controls[0].id
    cat_with_added_part = copy.deepcopy(sample_catalog_rich_controls)
    cat_with_added_part.controls[0].parts.append(part)
    final_merged = merge._merge_catalog(sample_catalog_rich_controls, cat_with_added_part)
    catalog_interface = CatalogInterface(final_merged)
    assert catalog_interface.get_count_of_controls_in_catalog(True) == 5
    assert catalog_interface.get_control(control_id).parts[-1].name == 'foo'

    # add part to first control and merge but with use-first.  The part should not be there at end.
    method = prof.Method.use_first
    combine = prof.Combine(method=method)
    profile.merge = prof.Merge(combine=combine)
    merge = Merge(profile)
    final_merged = merge._merge_catalog(sample_catalog_rich_controls, cat_with_added_part)
    catalog_interface = CatalogInterface(final_merged)
    assert catalog_interface.get_count_of_controls_in_catalog(True) == 5
    assert len(catalog_interface.get_control(control_id).parts) == 1

    # now force a merge with keep
    profile.merge = None
    merge_keep = Merge(profile)
    merged_keep = merge_keep._merge_catalog(new_merged, sample_catalog_rich_controls)
    assert CatalogInterface(merged_keep).get_count_of_controls_in_catalog(True) == 10
def test_add_props(tmp_trestle_dir: pathlib.Path) -> None:
    """Test all types of property additions."""
    test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True)
    prof_f_path = ModelUtils.path_for_top_level_model(
        tmp_trestle_dir, 'test_profile_f', prof.Profile, FileContentType.JSON
    )
    cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_f_path)
    interface = CatalogInterface(cat)
    ac_3 = interface.get_control('ac-3')

    assert len(ac_3.props) == 6
    assert ac_3.props[-1].value == 'four'

    for part in ac_3.parts:
        if part.id == 'ac-3_stmt':
            assert len(part.props) == 4

    ac_5 = interface.get_control('ac-5')
    for part in ac_5.parts:
        if part.id == 'ac-5_stmt':
            for sub_part in part.parts:
                if sub_part.id == 'ac-5_smt.a':
                    assert len(sub_part.props) == 4
예제 #5
0
def catalog_interface_equivalent(cat_int_a: CatalogInterface, cat_b: cat.Catalog, strong=True) -> bool:
    """Test equivalence of catalog dict contents in various ways."""
    cat_int_b = CatalogInterface(cat_b)
    if cat_int_b.get_count_of_controls_in_dict() != cat_int_a.get_count_of_controls_in_dict():
        logger.error('count of controls is different')
        return False
    for a in cat_int_a.get_all_controls_from_dict():
        try:
            b = cat_int_b.get_control(a.id)
        except Exception as e:
            logger.error(f'error finding control {a.id} {e}')
        if not controls_equivalent(a, b, strong):
            logger.error(f'controls differ: {a.id}')
            return False
    return True
def test_parameter_resolution(tmp_trestle_dir: pathlib.Path) -> None:
    """Test whether expected order of operations is preserved for parameter substution."""
    test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True)

    prof_e_path = ModelUtils.path_for_top_level_model(
        tmp_trestle_dir, 'test_profile_e', prof.Profile, FileContentType.JSON
    )
    profile_e_parameter_string = '## Override value ##'
    profile_a_value = 'all alert personnel'

    # based on 800-53 rev 5
    cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_e_path)
    interface = CatalogInterface(cat)
    control = interface.get_control('ac-1')
    locations = interface.find_string_in_control(control, profile_e_parameter_string)
    locations_a = interface.find_string_in_control(control, profile_a_value)
    assert len(locations) == 1
    assert len(locations_a) == 0
    assert len(control.params[1].constraints) == 1
def test_all_positions_for_alter_can_be_resolved(tmp_trestle_dir: pathlib.Path) -> None:
    """Test that all alter adds positions can be resolved."""
    cat_path = test_utils.JSON_TEST_DATA_PATH / test_utils.SIMPLIFIED_NIST_CATALOG_NAME
    repo = Repository(tmp_trestle_dir)
    repo.load_and_import_model(cat_path, 'nist_cat')

    prof_d_path = test_utils.JSON_TEST_DATA_PATH / 'test_profile_d.json'
    repo.load_and_import_model(prof_d_path, 'test_profile_d')

    cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_d_path)

    interface = CatalogInterface(cat)
    control_a1 = interface.get_control('ac-1')
    control_a2 = interface.get_control('ac-2')

    assert control_a1.parts[0].id == 'ac-1_first_lev1'
    assert control_a1.parts[1].parts[3].id == 'ac-1_last_lev2'
    assert control_a1.parts[2].id == 'ac-1_after1_ac-1_smt_lev1'
    assert control_a1.parts[3].id == 'ac-1_after2_ac-1_smt_lev1'
    assert control_a1.parts[1].parts[0].parts[1].id == 'ac-1_smt_before1_a.2_lev3'
    assert control_a1.parts[1].parts[0].parts[2].id == 'ac-1_smt_before2_a.2_lev3'
    assert control_a1.parts[1].parts[0].parts[3].parts[0].id == 'ac-1_smt_inside1_at_the_end_a.2_lev4'
    assert control_a1.parts[1].parts[0].parts[3].parts[1].id == 'ac-1_smt_inside2_at_the_end_a.2_lev4'
    assert control_a2.parts[0].id == 'ac-2_implgdn_lev1'
예제 #8
0
    def filter_ssp(self, trestle_root: pathlib.Path, ssp_name: str,
                   profile_name: str, out_name: str, regenerate: bool,
                   version: Optional[str]) -> int:
        """
        Filter the ssp based on controls included by the profile and output new ssp.

        Args:
            trestle_root: root directory of the trestle project
            ssp_name: name of the ssp model
            profile_name: name of the profile model used for filtering
            out_name: name of the output ssp model with filtered controls
            regenerate: whether to regenerate the uuid's in the ssp
            version: new version for the model

        Returns:
            0 on success, 1 otherwise
        """
        ssp: ossp.SystemSecurityPlan

        ssp, _ = ModelUtils.load_top_level_model(trestle_root, ssp_name,
                                                 ossp.SystemSecurityPlan,
                                                 FileContentType.JSON)
        profile_path = ModelUtils.path_for_top_level_model(
            trestle_root, profile_name, prof.Profile, FileContentType.JSON)

        prof_resolver = ProfileResolver()
        catalog = prof_resolver.get_resolved_profile_catalog(
            trestle_root, profile_path)
        catalog_interface = CatalogInterface(catalog)

        # The input ssp should reference a superset of the controls referenced by the profile
        # Need to cull references in the ssp to controls not in the profile
        # Also make sure the output ssp contains imp reqs for all controls in the profile
        control_imp = ssp.control_implementation
        ssp_control_ids: Set[str] = set()

        new_set_params: List[ossp.SetParameter] = []
        for set_param in as_list(control_imp.set_parameters):
            control = catalog_interface.get_control_by_param_id(
                set_param.param_id)
            if control is not None:
                new_set_params.append(set_param)
                ssp_control_ids.add(control.id)
        control_imp.set_parameters = new_set_params if new_set_params else None

        new_imp_requirements: List[ossp.ImplementedRequirement] = []
        for imp_requirement in as_list(control_imp.implemented_requirements):
            control = catalog_interface.get_control(imp_requirement.control_id)
            if control is not None:
                new_imp_requirements.append(imp_requirement)
                ssp_control_ids.add(control.id)
        control_imp.implemented_requirements = new_imp_requirements

        # make sure all controls in the profile have implemented reqs in the final ssp
        if not ssp_control_ids.issuperset(catalog_interface.get_control_ids()):
            raise TrestleError(
                'Unable to filter the ssp because the profile references controls not in it.'
            )

        ssp.control_implementation = control_imp
        if regenerate:
            ssp, _, _ = ModelUtils.regenerate_uuids(ssp)
        if version:
            ssp.metadata.version = com.Version(__root__=version)
        ModelUtils.update_last_modified(ssp)

        ModelUtils.save_top_level_model(ssp, trestle_root, out_name,
                                        FileContentType.JSON)

        return CmdReturnCodes.SUCCESS.value
예제 #9
0
class Modify(Pipeline.Filter):
    """Modify the controls based on the profile."""
    def __init__(
        self,
        profile: prof.Profile,
        change_prose: bool = False,
        block_adds: bool = False,
        block_params: bool = False,
        params_format: str = None,
        param_rep: ParameterRep = ParameterRep.VALUE_OR_LABEL_OR_CHOICES
    ) -> None:
        """Initialize the filter."""
        self._profile = profile
        self._catalog_interface: Optional[CatalogInterface] = None
        self._block_adds = block_adds
        self._block_params = block_params
        self._change_prose = change_prose
        self._params_format = params_format
        self._param_rep = param_rep
        logger.debug(
            f'modify initialize filter with profile {profile.metadata.title}')

    @staticmethod
    def _replace_ids_with_text(prose: str, param_rep: ParameterRep,
                               param_dict: Dict[str, common.Parameter]) -> str:
        """Find all instances of param_ids in prose and replace each with corresponding parameter representation.

        Need to check all values in dict for a match
        Reject matches where the string has an adjacent alphanumeric char: param_1 and param_10 or aparam_1
        """
        for param in param_dict.values():
            if param.id not in prose:
                continue
            # create the replacement text for the param_id
            param_str = ControlIOReader.param_to_str(param, param_rep)
            # non-capturing groups are odd in re.sub so capture all 3 groups and replace the middle one
            pattern = r'(^|[^a-zA-Z0-9_])' + param.id + r'($|[^a-zA-Z0-9_])'
            prose = re.sub(pattern, r'\1' + param_str + r'\2', prose)
        return prose

    @staticmethod
    def _replace_params(
        text: str,
        param_dict: Dict[str, common.Parameter],
        params_format: Optional[str] = None,
        param_rep: ParameterRep = ParameterRep.VALUE_OR_LABEL_OR_CHOICES
    ) -> str:
        """
        Replace params found in moustaches with values from the param_dict.

        A single line of prose may contain multiple moustaches.
        """
        # first check if there are any moustache patterns in the text
        if param_rep == ParameterRep.LEAVE_MOUSTACHE:
            return text
        staches: List[str] = re.findall(r'{{.*?}}', text)
        if not staches:
            return text
        # now have list of all staches including braces, e.g. ['{{foo}}', '{{bar}}']
        # clean the staches so they just have the param ids
        param_ids = []
        for stache in staches:
            # remove braces so these are just param_ids but may have extra chars
            stache_contents = stache[2:(-2)]
            param_id = stache_contents.replace('insert: param,', '').strip()
            param_ids.append(param_id)

        # now replace original stache text with param values
        for i, _ in enumerate(staches):
            # A moustache may refer to a param_id not listed in the control's params
            if param_ids[i] not in param_dict:
                logger.warning(
                    f'Control prose references param {param_ids[i]} not found in the control.'
                )
            elif param_dict[param_ids[i]] is not None:
                param = param_dict[param_ids[i]]
                param_str = ControlIOReader.param_to_str(
                    param, param_rep, False, False, params_format)
                text = text.replace(staches[i], param_str, 1)
            else:
                logger.warning(
                    f'Control prose references param {param_ids[i]} with no specified value.'
                )
        return text

    @staticmethod
    def _replace_part_prose(
        control: cat.Control,
        part: common.Part,
        param_dict: Dict[str, common.Parameter],
        params_format: Optional[str] = None,
        param_rep: ParameterRep = ParameterRep.VALUE_OR_LABEL_OR_CHOICES
    ) -> None:
        """Replace the part prose according to set_param."""
        if part.prose is not None:
            fixed_prose = Modify._replace_params(part.prose, param_dict,
                                                 params_format, param_rep)
            # change the prose in the control itself
            part.prose = fixed_prose
        for prt in as_list(part.parts):
            Modify._replace_part_prose(control, prt, param_dict, params_format,
                                       param_rep)
        for sub_control in as_list(control.controls):
            for prt in as_list(sub_control.parts):
                Modify._replace_part_prose(sub_control, prt, param_dict,
                                           params_format, param_rep)

    @staticmethod
    def _replace_control_prose(
        control: cat.Control,
        param_dict: Dict[str, common.Parameter],
        params_format: Optional[str] = None,
        param_rep: ParameterRep = ParameterRep.VALUE_OR_LABEL_OR_CHOICES
    ) -> None:
        """Replace the control prose according to set_param."""
        for part in as_list(control.parts):
            if part.prose is not None:
                fixed_prose = Modify._replace_params(part.prose, param_dict,
                                                     params_format, param_rep)
                # change the prose in the control itself
                part.prose = fixed_prose
            for prt in as_list(part.parts):
                Modify._replace_part_prose(control, prt, param_dict,
                                           params_format, param_rep)
        for sub_control in as_list(control.controls):
            prts: List[common.Part] = as_list(sub_control.parts)
            for prt in prts:
                Modify._replace_part_prose(sub_control, prt, param_dict,
                                           params_format, param_rep)

    @staticmethod
    def _add_contents_as_list(add: prof.Add) -> List[OBT]:
        add_list = []
        add_list.extend(as_list(add.props))
        add_list.extend(as_list(add.parts))
        add_list.extend(as_list(add.links))
        return add_list

    @staticmethod
    def _add_adds_to_part(part: common.Part, add: prof.Add) -> None:
        for attr in ['params', 'props', 'parts', 'links']:
            add_list = getattr(add, attr, None)
            if add_list:
                Modify._add_attr_to_part(part, add_list, attr, add.position)

    @staticmethod
    def _add_to_list(input_list: List[OBT], add: prof.Add) -> bool:
        """Add the contents of the add according to its by_id and position.

        Return True on success or False if id needed and not found.

        This is only called when by_id is not None.
        The add will be inserted if the id is found, or return False if not.
        This allows a separate recursive routine to search sub-lists for the id.
        """
        add_list = Modify._add_contents_as_list(add)
        # Test here for matched by_id attribute.
        try:
            for index in range(len(input_list)):
                if input_list[index].id == add.by_id:
                    if add.position == prof.Position.after:
                        for offset, new_item in enumerate(add_list):
                            input_list.insert(index + 1 + offset, new_item)
                        return True
                    elif add.position == prof.Position.before:
                        for offset, new_item in enumerate(add_list):
                            input_list.insert(index + offset, new_item)
                        return True
                    # if starting or ending, the adds go directly into this part according to type
                    Modify._add_adds_to_part(input_list[index], add)
                    return True
        except AttributeError:
            raise TrestleError(
                'Cannot use "after" or "before" modifications for a list where elements'
                + ' do not contain the referenced by_id attribute.')
        return False

    @staticmethod
    def _add_to_parts(parts: List[common.Part], add: prof.Add) -> bool:
        """
        Add the add to the parts.

        This is only called if add.by_id is not None.
        """
        if Modify._add_to_list(parts, add):
            return True
        for part in parts:
            if part.parts is not None and Modify._add_to_parts(
                    part.parts, add):
                return True
        return False

    @staticmethod
    def _add_attr_to_part(part: common.Part, items: List[OBT], attr: str,
                          position: prof.Position) -> None:
        attr_list = as_list(getattr(part, attr, None))
        if position in [prof.Position.starting, prof.Position.before]:
            items.extend(attr_list)
            attr_list = items
        else:
            attr_list.extend(items)
        setattr(part, attr, attr_list)

    @staticmethod
    def _add_attr_to_control(control: cat.Control, items: List[OBT], attr: str,
                             position: prof.Position) -> None:
        attr_list = as_list(getattr(control, attr, None))
        if position in [prof.Position.starting, prof.Position.before]:
            items.extend(attr_list)
            attr_list = items
        else:
            attr_list.extend(items)
        setattr(control, attr, attr_list)

    @staticmethod
    def _add_to_control(control: cat.Control, add: prof.Add) -> None:
        """First step in applying Add to control."""
        control.parts = as_list(control.parts)
        if add.by_id is None or add.by_id == control.id:
            # add contents will be added to the control directly and with no recursion
            for attr in ['params', 'props', 'parts', 'links']:
                add_list = getattr(add, attr, None)
                if add_list:
                    Modify._add_attr_to_control(control, add_list, attr,
                                                add.position)
            return
        else:
            # this is only called if by_id is not None
            if not Modify._add_to_parts(control.parts, add):
                logger.warning(
                    f'Could not find id for add in control {control.id}: {add.by_id}'
                )

    def _set_parameter_in_control(self, set_param: prof.SetParameter) -> None:
        """
        Find the control with the param_id in it and set the parameter value.

        This does not recurse because expectation is that only top level params will be set.
        """
        control = self._catalog_interface.get_control_by_param_id(
            set_param.param_id)
        if control is None:
            raise TrestleError(
                f'Set parameter object in profile does not have a corresponding param-id: "{set_param.param_id}"'
            )
        control.params = as_list(control.params)
        param_ids = [param.id for param in control.params]
        if set_param.param_id not in param_ids:
            raise TrestleNotFoundError(
                f'Param id {set_param.param_id} not found in control {control.id}'
            )
        index = param_ids.index(set_param.param_id)
        param = control.params[index]
        param.values = set_param.values
        param.constraints = set_param.constraints
        param.guidelines = set_param.guidelines
        param.links = set_param.links
        param.props = set_param.props
        param.select = set_param.select
        param.usage = set_param.usage
        control.params[index] = param
        self._catalog_interface.replace_control(control)

    def _change_prose_with_param_values(self):
        """Go through all controls and change prose based on param values."""
        param_dict: Dict[str, common.Paramter] = {}
        # build the full mapping of params to values
        for control in self._catalog_interface.get_all_controls_from_dict():
            param_dict.update(
                ControlIOReader.get_control_param_dict(control, False))
        # insert param values into prose of all controls
        for control in self._catalog_interface.get_all_controls_from_dict():
            self._replace_control_prose(control, param_dict,
                                        self._params_format, self._param_rep)

    def _modify_controls(self, catalog: cat.Catalog) -> cat.Catalog:
        """Modify the controls based on the profile."""
        logger.debug(
            f'modify specify catalog {catalog.metadata.title} for profile {self._profile.metadata.title}'
        )
        self._catalog_interface = CatalogInterface(catalog)
        alters: Optional[List[prof.Alter]] = None
        # find the modify and alters
        if self._profile.modify:
            # change all parameter values
            if self._profile.modify.set_parameters and not self._block_params:
                set_param_list = self._profile.modify.set_parameters
                for set_param in set_param_list:
                    self._set_parameter_in_control(set_param)
            alters = self._profile.modify.alters

        if alters is not None:
            title = self._profile.metadata.title
            for alter in alters:
                if alter.control_id is None:
                    logger.warning(
                        f'Alter must have control id specified in profile {title}.'
                    )
                    continue
                id_ = alter.control_id
                if alter.removes is not None:
                    logger.warning(
                        f'Alter not supported for removes in profile {title} control {id_}'
                    )
                    continue
                # we want a warning about adds even if adds are blocked, as in profile generate
                if alter.adds is None:
                    logger.warning(
                        f'Alter has no adds in profile {title} control {id_}')
                    continue
                if not self._block_adds:
                    for add in alter.adds:
                        if add.position is None and add.parts is not None:
                            msg = f'Alter/Add position is not specified in profile {title} control {id_}'
                            msg += ' when adding part, so defaulting to ending.'
                            logger.warning(msg)
                            add.position = prof.Position.ending
                        control = self._catalog_interface.get_control(id_)
                        if control is None:
                            logger.warning(
                                f'Alter/Add refers to control {id_} but it is not found in the import '
                                +
                                f'for profile {self._profile.metadata.title}')
                        else:
                            self._add_to_control(control, add)
                            self._catalog_interface.replace_control(control)

        if self._change_prose:
            # go through all controls and fix the prose based on param values
            self._change_prose_with_param_values()

        catalog = self._catalog_interface.get_catalog()

        # update the original profile metadata with new contents
        # roles and responsible-parties will be pulled in with new uuid's
        new_metadata = self._profile.metadata
        new_metadata.title = f'{catalog.metadata.title}: Resolved by profile {self._profile.metadata.title}'
        links: List[common.Link] = []
        for import_ in self._profile.imports:
            links.append(
                common.Link(**{
                    'href': import_.href,
                    'rel': 'resolution-source'
                }))
        new_metadata.links = links

        # move catalog controls from dummy group '' into the catalog
        for group in as_list(catalog.groups):
            if not group.id:
                catalog.controls = group.controls
                catalog.groups.remove(group)
                break

        catalog.metadata = new_metadata

        return catalog

    def process(self,
                catalog_iter: Iterator[cat.Catalog]) -> Iterator[cat.Catalog]:
        """Make the modifications to the controls based on the profile."""
        catalog = next(catalog_iter)
        logger.debug(
            f'modify process with catalog {catalog.metadata.title} using profile {self._profile.metadata.title}'
        )
        yield self._modify_controls(catalog)