Esempio n. 1
0
def test_update_last_modified(
        sample_catalog_rich_controls: catalog.Catalog) -> None:
    """Test update timestamps."""
    hour_ago = datetime.now().astimezone() - timedelta(
        seconds=const.HOUR_SECONDS)
    ModelUtils.update_last_modified(sample_catalog_rich_controls, hour_ago)
    assert sample_catalog_rich_controls.metadata.last_modified.__root__ == hour_ago
    ModelUtils.update_last_modified(sample_catalog_rich_controls)
    assert ModelUtils.model_age(
        sample_catalog_rich_controls) < test_utils.NEW_MODEL_AGE_SECONDS
Esempio n. 2
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
Esempio n. 3
0
    def _run(self, args: argparse.Namespace) -> int:
        try:
            log.set_log_level_from_args(args)
            trestle_root = pathlib.Path(args.trestle_root)

            md_path = trestle_root / args.markdown

            # the original, reference ssp name defaults to same as output if name not specified
            # thus in cyclic editing you are reading and writing same json ssp
            orig_ssp_name = args.output
            if args.name:
                orig_ssp_name = args.name
            new_ssp_name = args.output
            # if orig ssp exists - need to load it rather than instantiate new one
            orig_ssp_path = ModelUtils.path_for_top_level_model(
                trestle_root, orig_ssp_name, ossp.SystemSecurityPlan,
                FileContentType.JSON)

            # if output ssp already exists, load it to see if new one is different
            existing_ssp: Optional[ossp.SystemSecurityPlan] = None
            new_ssp_path = ModelUtils.path_for_top_level_model(
                trestle_root, new_ssp_name, ossp.SystemSecurityPlan,
                FileContentType.JSON)
            if new_ssp_path.exists():
                _, _, existing_ssp = ModelUtils.load_distributed(
                    new_ssp_path, trestle_root)

            ssp: ossp.SystemSecurityPlan
            comp_dict: Dict[str, ossp.SystemComponent] = {}

            # need to load imp_reqs from markdown but need component first
            if orig_ssp_path.exists():
                # load the existing json ssp
                _, _, ssp = ModelUtils.load_distributed(
                    orig_ssp_path, trestle_root)
                for component in ssp.system_implementation.components:
                    comp_dict[component.title] = component
                # read the new imp reqs from markdown and have them reference existing components
                imp_reqs = CatalogInterface.read_catalog_imp_reqs(
                    md_path, comp_dict)
                self._merge_imp_reqs(ssp, imp_reqs)
            else:
                # create a sample ssp to hold all the parts
                ssp = gens.generate_sample_model(ossp.SystemSecurityPlan)
                # load the imp_reqs from markdown and create components as needed, referenced by ### headers
                imp_reqs = CatalogInterface.read_catalog_imp_reqs(
                    md_path, comp_dict)

                # create system implementation
                system_imp: ossp.SystemImplementation = gens.generate_sample_model(
                    ossp.SystemImplementation)
                ssp.system_implementation = system_imp

                # create a control implementation to hold the implementated requirements
                control_imp: ossp.ControlImplementation = gens.generate_sample_model(
                    ossp.ControlImplementation)
                control_imp.implemented_requirements = imp_reqs
                control_imp.description = const.SSP_SYSTEM_CONTROL_IMPLEMENTATION_TEXT

                # insert the parts into the ssp
                ssp.control_implementation = control_imp
                ssp.system_implementation = system_imp

                # we don't have access to the original profile so we don't know the href
                import_profile: ossp.ImportProfile = gens.generate_sample_model(
                    ossp.ImportProfile)
                import_profile.href = 'REPLACE_ME'
                ssp.import_profile = import_profile

            # now that we know the complete list of needed components, add them to the sys_imp
            # TODO if the ssp already existed then components may need to be removed if not ref'd by imp_reqs
            component_list: List[ossp.SystemComponent] = []
            for comp in comp_dict.values():
                component_list.append(comp)
            if ssp.system_implementation.components:
                # reconstruct list with same order as existing, but add/remove components as needed
                new_list: List[ossp.SystemComponent] = []
                for comp in ssp.system_implementation.components:
                    if comp in component_list:
                        new_list.append(comp)
                for comp in component_list:
                    if comp not in new_list:
                        new_list.append(comp)
                ssp.system_implementation.components = new_list
            elif component_list:
                ssp.system_implementation.components = component_list
            self._generate_roles_in_metadata(ssp)

            if args.version:
                ssp.metadata.version = com.Version(__root__=args.version)

            if existing_ssp == ssp:
                logger.info(
                    'No changes to assembled ssp so ssp not written out.')
                return CmdReturnCodes.SUCCESS.value

            if args.regenerate:
                ssp, _, _ = ModelUtils.regenerate_uuids(ssp)
            ModelUtils.update_last_modified(ssp)

            # write out the ssp as json
            ModelUtils.save_top_level_model(ssp, trestle_root, new_ssp_name,
                                            FileContentType.JSON)

            return CmdReturnCodes.SUCCESS.value

        except Exception as e:  # pragma: no cover
            return handle_generic_command_exception(
                e, logger, 'Error while assembling SSP')
Esempio n. 4
0
    def assemble_profile(trestle_root: pathlib.Path, orig_profile_name: str,
                         md_name: str, new_profile_name: str,
                         set_parameters: bool, regenerate: bool,
                         version: Optional[str],
                         required_sections: Optional[str],
                         allowed_sections: Optional[List[str]]) -> int:
        """
        Assemble the markdown directory into a json profile model file.

        Args:
            trestle_root: The trestle root directory
            orig_profile_name: Optional name of profile used to generate the markdown (default is new_profile_name)
            md_name: The name of the directory containing the markdown control files for the profile
            new_profile_name: The name of the new json profile.  It can be the same as original to overwrite
            set_parameters: Use the parameters in the yaml header to specify values for setparameters in the profile
            regenerate: Whether to regenerate the uuid's in the profile
            version: Optional version for the assembled profile
            required_sections: Optional List of required sections in assembled profile, as comma-separated short names
            allowed_sections: Optional list of section short names that are allowed, as comma-separated short names

        Returns:
            0 on success, 1 otherwise

        Notes:
            There must already be a json profile model and it will either be updated or a new json profile created.
            The generated markdown shows the current values for parameters of controls being imported, as set by
            the original catalog and any intermediate profiles.  It also shows the current SetParameters being applied
            by this profile.  That list of SetParameters can be edited by changing the assigned values and adding or
            removing SetParameters from that list.  During assembly that list will be used to create the SetParameters
            in the assembled profile if the --set-parameters option is specified.
        """
        # first load the original json profile but call it 'new' because it is being updated with new items
        profile_path = trestle_root / f'profiles/{orig_profile_name}/profile.json'
        _, _, new_profile = ModelUtils.load_distributed(
            profile_path, trestle_root)

        required_sections_list = required_sections.split(
            ',') if required_sections else []

        # load the editable sections of the markdown and create Adds for them
        # then overwrite the Adds in the existing profile with the new ones
        # keep track if any changes were made
        md_dir = trestle_root / md_name
        found_alters, param_dict = CatalogInterface.read_additional_content(
            md_dir, required_sections_list)
        if allowed_sections:
            for alter in found_alters:
                for add in alter.adds:
                    for part in add.parts:
                        if part.name not in allowed_sections:
                            raise TrestleError(
                                f'Profile has alter with name {part.name} not in allowed sections.'
                            )
        ProfileAssemble._replace_alter_adds(new_profile, found_alters)
        if set_parameters:
            ProfileAssemble._replace_modify_set_params(new_profile, param_dict)

        new_profile_dir = trestle_root / f'profiles/{new_profile_name}'
        new_profile_path = new_profile_dir / 'profile.json'

        if version:
            new_profile.metadata.version = com.Version(__root__=version)

        if new_profile_path.exists():
            # first do simple and fast tests of changes so that the full equality check is only done as last resort
            _, _, existing_profile = ModelUtils.load_distributed(
                new_profile_path, trestle_root)
            # need special handling in case modify is None in either or both profiles
            no_changes = not existing_profile.modify and not new_profile.modify
            both_modifies_present = existing_profile.modify and new_profile.modify
            if both_modifies_present:
                no_changes = (existing_profile.modify.alters
                              == new_profile.modify.alters
                              and existing_profile.modify.set_parameters
                              == new_profile.modify.set_parameters)
            if no_changes and existing_profile.metadata.version != new_profile.metadata.version:
                no_changes = False
            # at this point if no known changes then do the full and possibly expensive comparison to confirm
            if no_changes and existing_profile == new_profile:
                logger.info(
                    'Assembled profile has no changes so no update of existing file.'
                )
                return CmdReturnCodes.SUCCESS.value

        if regenerate:
            new_profile, _, _ = ModelUtils.regenerate_uuids(new_profile)
        ModelUtils.update_last_modified(new_profile)

        if new_profile_dir.exists():
            logger.info(
                'Creating profile from markdown and destination profile directory exists, so updating.'
            )
            try:
                shutil.rmtree(str(new_profile_dir))
            except OSError as e:
                raise TrestleError(
                    f'OSError deleting existing catalog directory with rmtree {new_profile_dir}: {e}'
                )
        try:
            new_profile_dir.mkdir()
            new_profile.oscal_write(new_profile_dir / 'profile.json')
        except OSError as e:
            raise TrestleError(
                f'OSError writing profile from markdown to {new_profile_dir}: {e}'
            )
        return CmdReturnCodes.SUCCESS.value