def test_to_content_type():
    """Test to_content_type method."""
    assert FileContentType.to_content_type('.json') == FileContentType.JSON
    assert FileContentType.to_content_type('.yaml') == FileContentType.YAML

    with pytest.raises(TrestleError):
        FileContentType.to_content_type('.invalid')
Beispiel #2
0
    def assemble_model(cls, model_alias: str, args: argparse.Namespace) -> int:
        """Assemble a top level OSCAL model within the trestle dist directory."""
        log.set_log_level_from_args(args)
        logger.info(f'Assembling models of type {model_alias}.')

        trestle_root = args.trestle_root  # trestle root is set via command line in args. Default is cwd.
        if not trestle_root or not file_utils.is_valid_project_root(
                args.trestle_root):
            raise TrestleRootError(
                f'Given directory {trestle_root} is not a trestle project.')

        model_names = []
        if args.name:
            model_names = [args.name]
            logger.info(
                f'Assembling single model of type {model_alias}: {args.name}.')
        else:
            model_names = ModelUtils.get_models_of_type(
                model_alias, trestle_root)
            nmodels = len(model_names)
            logger.info(
                f'Assembling {nmodels} found models of type {model_alias}.')
        if len(model_names) == 0:
            logger.info(f'No models found to assemble of type {model_alias}.')
            return CmdReturnCodes.SUCCESS.value

        for model_name in model_names:
            # contruct path to the model file name
            root_model_dir = trestle_root / ModelUtils.model_type_to_model_dir(
                model_alias)

            model_file_type = file_utils.get_contextual_file_type(
                root_model_dir / model_name)

            model_file_name = f'{model_alias}{FileContentType.to_file_extension(model_file_type)}'
            root_model_filepath = root_model_dir / model_name / model_file_name

            if not root_model_filepath.exists():
                raise TrestleError(
                    f'No top level model file at {root_model_dir}')

            # distributed load
            _, _, assembled_model = ModelUtils.load_distributed(
                root_model_filepath, args.trestle_root)
            plural_alias = ModelUtils.model_type_to_model_dir(model_alias)

            assembled_model_dir = trestle_root / const.TRESTLE_DIST_DIR / plural_alias

            assembled_model_filepath = assembled_model_dir / f'{model_name}.{args.extension}'

            plan = Plan()
            plan.add_action(CreatePathAction(assembled_model_filepath, True))
            plan.add_action(
                WriteFileAction(
                    assembled_model_filepath, Element(assembled_model),
                    FileContentType.to_content_type(f'.{args.extension}')))

            plan.execute()

        return CmdReturnCodes.SUCCESS.value
Beispiel #3
0
    def add_from_args(self, args: argparse.Namespace) -> int:
        """Parse args for add element to file."""
        file_path = pathlib.Path(args.file).resolve()

        # Get parent model and then load json into parent model
        parent_model, _ = ModelUtils.get_stripped_model_type(file_path, args.trestle_root)
        parent_object = parent_model.oscal_read(file_path)
        parent_element = Element(parent_object, classname_to_alias(parent_model.__name__, AliasMode.JSON))

        add_plan = Plan()
        # Do _add for each element_path specified in args
        element_paths: List[str] = args.element.split(',')
        for elm_path_str in element_paths:
            element_path = ElementPath(elm_path_str)
            update_action, parent_element = self.add(element_path, parent_element, args.include_optional_fields)
            add_plan.add_action(update_action)

        create_action = CreatePathAction(file_path, True)
        # this will output json or yaml based on type of input file
        write_action = WriteFileAction(file_path, parent_element, FileContentType.to_content_type(file_path.suffix))

        add_plan.add_action(create_action)
        add_plan.add_action(write_action)

        add_plan.execute()
        return CmdReturnCodes.SUCCESS.value
Beispiel #4
0
    def oscal_write(self, path: pathlib.Path) -> None:
        """
        Write out a pydantic data model in an oscal friendly way.

        OSCAL schema mandates that top level elements are wrapped in a singular
        json/yaml field. This function handles both json and yaml output as well
        as formatting of the json.

        Args:
            path: The output file location for the oscal object.

        Raises:
            err.TrestleError: If a unknown file extension is provided.
        """
        content_type = FileContentType.to_content_type(path.suffix)
        # The output will have \r\n newlines on windows and \n newlines elsewhere

        if content_type == FileContentType.YAML:
            write_file = pathlib.Path(path).open('w',
                                                 encoding=const.FILE_ENCODING)
            yaml = YAML(typ='safe')
            yaml.dump(yaml.load(self.oscal_serialize_json()), write_file)
            write_file.flush()
            write_file.close()
        elif content_type == FileContentType.JSON:
            write_file = pathlib.Path(path).open('wb')
            write_file.write(self.oscal_serialize_json_bytes(pretty=True))
            # Flush / close required (by experience) due to flushing issues in tests.
            write_file.flush()
            write_file.close()
Beispiel #5
0
    def import_model(self, model: OscalBaseModel, name: str, content_type='json') -> ManagedOSCAL:
        """Import OSCAL object into trestle repository."""
        logger.debug(f'Importing model {name} of type {model.__class__.__name__}.')
        model_alias = classname_to_alias(model.__class__.__name__, AliasMode.JSON)
        if parser.to_full_model_name(model_alias) is None:
            raise TrestleError(f'Given model {model_alias} is not a top level model.')

        # Work out output directory and file
        plural_path = ModelUtils.model_type_to_model_dir(model_alias)

        desired_model_dir = self._root_dir / plural_path
        desired_model_path = desired_model_dir / name / (model_alias + '.' + content_type)
        desired_model_path = desired_model_path.resolve()

        if desired_model_path.exists():
            raise TrestleError(f'OSCAL file to be created here: {desired_model_path} exists.')

        content_type = FileContentType.to_content_type(pathlib.Path(desired_model_path).suffix)

        # Prepare actions
        top_element = Element(model)
        create_action = CreatePathAction(desired_model_path, True)
        write_action = WriteFileAction(desired_model_path, top_element, content_type)

        # create a plan to create the directory and imported file.
        import_plan = Plan()
        import_plan.add_action(create_action)
        import_plan.add_action(write_action)
        import_plan.execute()

        # Validate the imported file, rollback if unsuccessful
        success = False
        errmsg = ''
        try:
            success = self.validate_model(model.__class__, name)
            if not success:
                errmsg = f'Validation of model {name} did not pass'
                logger.error(errmsg)
        except Exception as err:
            logger.error(errmsg)
            errmsg = f'Import of model {name} failed. Validation failed with error: {err}'

        if not success:
            # rollback in case of validation error or failure
            logger.debug(f'Rolling back import of model {name} to {desired_model_path}')
            try:
                import_plan.rollback()
            except TrestleError as err:
                logger.error(f'Failed to rollback: {err}. Remove {desired_model_path} to resolve state.')
            else:
                logger.debug(f'Successful rollback of import to {desired_model_path}')

            # raise trestle error
            raise TrestleError(errmsg)

        # all well; model was imported and validated successfully
        logger.debug(f'Model {name} of type {model.__class__.__name__} imported successfully.')
        return ManagedOSCAL(self._root_dir, model.__class__, name)
Beispiel #6
0
    def assemble_model(cls, model_alias: str, object_type: Type[TLO],
                       args: argparse.Namespace) -> int:
        """Assemble a top level OSCAL model within the trestle dist directory."""
        log.set_log_level_from_args(args)
        trestle_root = fs.get_trestle_project_root(Path.cwd())
        if not trestle_root:
            logger.error(
                f'Current working directory {Path.cwd()} is not with a trestle project.'
            )
            return 1
        if not trestle_root == Path.cwd():
            logger.error(
                f'Current working directory {Path.cwd()} is not the top level trestle project directory.'
            )
            return 1

        # contruct path to the model file name
        root_model_dir = Path.cwd() / f'{model_alias}s'
        try:
            model_file_type = fs.get_contextual_file_type(root_model_dir /
                                                          args.name)
        except Exception as e:
            logger.error('No files found in the specified model directory.')
            logger.debug(e)
            return 1

        model_file_name = f'{model_alias}{FileContentType.to_file_extension(model_file_type)}'
        root_model_filepath = root_model_dir / args.name / model_file_name

        if not root_model_filepath.exists():
            logger.error(f'No top level model file at {root_model_dir}')
            return 1

        # distributed load
        _, _, assembled_model = load_distributed(root_model_filepath)
        plural_alias = model_alias if model_alias[
            -1] == 's' else model_alias + 's'
        assembled_model_dir = trestle_root / const.TRESTLE_DIST_DIR / plural_alias

        assembled_model_filepath = assembled_model_dir / f'{args.name}.{args.extension}'

        plan = Plan()
        plan.add_action(CreatePathAction(assembled_model_filepath, True))
        plan.add_action(
            WriteFileAction(
                assembled_model_filepath, Element(assembled_model),
                FileContentType.to_content_type(f'.{args.extension}')))

        try:
            plan.simulate()
            plan.execute()
            return 0
        except Exception as e:
            logger.error(
                'Unknown error executing trestle create operations. Rolling back.'
            )
            logger.debug(e)
            return 1
Beispiel #7
0
    def create_object(cls, model_alias: str, object_type: Type[TLO],
                      args: argparse.Namespace) -> int:
        """Create a top level OSCAL object within the trestle directory, leveraging functionality in add."""
        log.set_log_level_from_args(args)
        trestle_root = fs.get_trestle_project_root(Path.cwd())
        if not trestle_root:
            logger.error(
                f'Current working directory {Path.cwd()} is not with a trestle project.'
            )
            return 1
        plural_path: str
        # Cater to POAM
        if model_alias[-1] == 's':
            plural_path = model_alias
        else:
            plural_path = model_alias + 's'

        desired_model_dir = trestle_root / plural_path / args.name

        desired_model_path = desired_model_dir / (model_alias + '.' +
                                                  args.extension)

        if desired_model_path.exists():
            logger.error(
                f'OSCAL file to be created here: {desired_model_path} exists.')
            logger.error('Aborting trestle create.')
            return 1

        # Create sample model.
        sample_model = generators.generate_sample_model(object_type)
        # Presuming top level level model not sure how to do the typing for this.
        sample_model.metadata.title = f'Generic {model_alias} created by trestle.'  # type: ignore
        sample_model.metadata.last_modified = datetime.now().astimezone()
        sample_model.metadata.oscal_version = trestle.oscal.OSCAL_VERSION
        sample_model.metadata.version = '0.0.0'

        top_element = Element(sample_model, model_alias)

        create_action = CreatePathAction(desired_model_path.absolute(), True)
        write_action = WriteFileAction(
            desired_model_path.absolute(), top_element,
            FileContentType.to_content_type(desired_model_path.suffix))

        # create a plan to write the directory and file.
        try:
            create_plan = Plan()
            create_plan.add_action(create_action)
            create_plan.add_action(write_action)
            create_plan.simulate()
            create_plan.execute()
            return 0
        except Exception as e:
            logger.error(
                'Unknown error executing trestle create operations. Rolling back.'
            )
            logger.debug(e)
            return 1
Beispiel #8
0
    def add(cls, file_path, element_path, parent_model, parent_element):
        """For a file_path and element_path, add a child model to the parent_element of a given parent_model.

        First we find the child model at the specified element path and instantiate it with default values.
        Then we check if there's already existing element at that path, in which case we append the child model
        to the existing list of dict.
        Then we set up an action plan to update the model (specified by file_path) in memory, create a file
        at the same location and write the file.
        """
        element_path_list = element_path.get_full_path_parts()
        if '*' in element_path_list:
            raise err.TrestleError(
                'trestle add does not support Wildcard element path.')
        # Get child model
        try:
            child_model = utils.get_target_model(element_path_list,
                                                 parent_model)
            # Create child element with sample values
            child_object = utils.get_sample_model(child_model)

            if parent_element.get_at(element_path) is not None:
                # The element already exists
                if type(parent_element.get_at(element_path)) is list:
                    child_object = parent_element.get_at(
                        element_path) + child_object
                elif type(parent_element.get_at(element_path)) is dict:
                    child_object = {
                        **parent_element.get_at(element_path),
                        **child_object
                    }
                else:
                    raise err.TrestleError(
                        'Already exists and is not a list or dictionary.')

        except Exception as e:
            raise err.TrestleError(f'Bad element path. {str(e)}')

        update_action = UpdateAction(sub_element=child_object,
                                     dest_element=parent_element,
                                     sub_element_path=element_path)
        create_action = CreatePathAction(file_path.absolute(), True)
        write_action = WriteFileAction(
            file_path.absolute(), parent_element,
            FileContentType.to_content_type(file_path.suffix))

        add_plan = Plan()
        add_plan.add_action(update_action)
        add_plan.add_action(create_action)
        add_plan.add_action(write_action)
        add_plan.simulate()

        add_plan.execute()
Beispiel #9
0
    def oscal_read(cls, path: pathlib.Path) -> Optional['OscalBaseModel']:
        """
        Read OSCAL objects.

        Handles the fact OSCAL wraps top level elements and also deals with both yaml and json.

        Args:
            path: The path of the oscal object to read.
        Returns:
            The oscal object read into trestle oscal models.
        """
        # Create the wrapper model.
        alias = classname_to_alias(cls.__name__, AliasMode.JSON)

        content_type = FileContentType.to_content_type(path.suffix)
        logger.debug(
            f'oscal_read content type {content_type} and alias {alias} from {path}'
        )

        if not path.exists():
            logger.warning(f'path does not exist in oscal_read: {path}')
            return None

        obj: Dict[str, Any] = {}
        try:
            if content_type == FileContentType.YAML:
                yaml = YAML(typ='safe')
                fh = path.open('r', encoding=const.FILE_ENCODING)
                obj = yaml.load(fh)
                fh.close()
            elif content_type == FileContentType.JSON:
                obj = load_file(
                    path,
                    json_loads=cls.__config__.json_loads,
                )
        except Exception as e:
            raise err.TrestleError(f'Error loading file {path} {str(e)}')
        try:
            if not len(obj) == 1:
                raise err.TrestleError(
                    f'Invalid OSCAL file structure, oscal file '
                    f'does not have a single top level key wrapping it. It has {len(obj)} keys.'
                )
            parsed = cls.parse_obj(obj[alias])
        except KeyError:
            raise err.TrestleError(
                f'Provided oscal file does not have top level key key: {alias}'
            )
        except Exception as e:
            raise err.TrestleError(f'Error parsing file {path} {str(e)}')

        return parsed
Beispiel #10
0
def load_file(file_name: pathlib.Path) -> Dict[str, Any]:
    """
    Load JSON or YAML file content into a dict.

    This is not intended to be the default load mechanism. It should only be used
    if a OSCAL object type is unknown but the context a user is in.
    """
    content_type = FileContentType.to_content_type(file_name.suffix)
    with file_name.open('r', encoding=const.FILE_ENCODING) as f:
        if content_type == FileContentType.YAML:
            return yaml.load(f, yaml.FullLoader)
        elif content_type == FileContentType.JSON:
            return json.load(f)
Beispiel #11
0
def get_contextual_file_type(path: pathlib.Path) -> FileContentType:
    """Return the file content type for files in the given directory, if it's a trestle project."""
    if not is_valid_project_model_path(path):
        raise err.TrestleError('Trestle project not found.')

    for file_or_directory in path.iterdir():
        if file_or_directory.is_file():
            return FileContentType.to_content_type(file_or_directory.suffix)

    for file_or_directory in path.iterdir():
        if file_or_directory.is_dir():
            return get_contextual_file_type(file_or_directory)

    raise err.TrestleError('No files found in the project.')
Beispiel #12
0
    def _run(self, args: argparse.Namespace) -> int:
        """Add an OSCAL component/subcomponent to the specified component.

        This method takes input a filename and a list of comma-seperated element path. Element paths are field aliases.
        The method first finds the parent model from the file and loads the file into the model.
        Then the method executes 'add' for each of the element paths specified.
        """
        log.set_log_level_from_args(args)
        try:
            args_dict = args.__dict__

            file_path = pathlib.Path(args_dict[const.ARG_FILE])

            # Get parent model and then load json into parent model
            parent_model, parent_alias = fs.get_stripped_contextual_model(
                file_path.absolute())
            parent_object = parent_model.oscal_read(file_path.absolute())
            # FIXME : handle YAML files after detecting file type
            parent_element = Element(
                parent_object,
                utils.classname_to_alias(parent_model.__name__, 'json'))

            add_plan = Plan()

            # Do _add for each element_path specified in args
            element_paths: List[str] = args_dict[const.ARG_ELEMENT].split(',')
            for elm_path_str in element_paths:
                element_path = ElementPath(elm_path_str)
                update_action, parent_element = self.add(
                    element_path, parent_model, parent_element)
                add_plan.add_action(update_action)

            create_action = CreatePathAction(file_path.absolute(), True)
            write_action = WriteFileAction(
                file_path.absolute(), parent_element,
                FileContentType.to_content_type(file_path.suffix))

            add_plan.add_action(create_action)
            add_plan.add_action(write_action)

            add_plan.simulate()
            add_plan.execute()

        except BaseException as err:
            logger.error(f'Add failed: {err}')
            return 1
        return 0
Beispiel #13
0
    def _run(self, args: argparse.Namespace) -> int:
        """Remove an OSCAL component/subcomponent to the specified component.

        This method takes input a filename and a list of comma-seperated element path. Element paths are field aliases.
        The method first finds the parent model from the file and loads the file into the model.
        Then the method executes 'remove' for each of the element paths specified.
        """
        try:
            log.set_log_level_from_args(args)
            args_dict = args.__dict__

            file_path = pathlib.Path(args_dict[const.ARG_FILE]).resolve()
            relative_path = file_path.relative_to(args.trestle_root)
            # Get parent model and then load json into parent model
            parent_model, parent_alias = ModelUtils.get_relative_model_type(
                relative_path)

            parent_object = parent_model.oscal_read(file_path)
            parent_element = Element(parent_object, parent_alias)

            add_plan = Plan()

            # Do _remove for each element_path specified in args
            element_paths: List[str] = str(
                args_dict[const.ARG_ELEMENT]).split(',')
            for elm_path_str in element_paths:
                element_path = ElementPath(elm_path_str)
                remove_action, parent_element = self.remove(
                    element_path, parent_element)
                add_plan.add_action(remove_action)

            create_action = CreatePathAction(file_path, True)
            write_action = WriteFileAction(
                file_path, parent_element,
                FileContentType.to_content_type(file_path.suffix))
            add_plan.add_action(remove_action)
            add_plan.add_action(create_action)
            add_plan.add_action(write_action)

            add_plan.execute()

            return CmdReturnCodes.SUCCESS.value

        except Exception as e:
            return err.handle_generic_command_exception(
                e, logger, 'Error while removing OSCAL component')
Beispiel #14
0
    def _run(self, args: argparse.Namespace) -> int:
        """Split an OSCAL file into elements."""
        logger.debug('Entering trestle split.')
        log.set_log_level_from_args(args)
        # get the Model
        args_raw = args.__dict__
        if args_raw[const.ARG_FILE] is None:
            logger.error(f'Argument "-{const.ARG_FILE_SHORT}" is required')
            return 1

        file_path = pathlib.Path(args_raw[const.ARG_FILE])
        if not file_path.exists():
            logger.error(f'File {file_path} does not exist.')
            return 1
        content_type = FileContentType.to_content_type(file_path.suffix)

        # find the base directory of the file
        file_absolute_path = pathlib.Path(file_path.absolute())
        base_dir = file_absolute_path.parent

        model_type, _ = fs.get_stripped_contextual_model(file_absolute_path)

        # FIXME: Handle list/dicts
        model: OscalBaseModel = model_type.oscal_read(file_path)

        element_paths: List[ElementPath] = cmd_utils.parse_element_args(
            args_raw[const.ARG_ELEMENT].split(','))

        split_plan = self.split_model(model,
                                      element_paths,
                                      base_dir,
                                      content_type,
                                      root_file_name=args_raw[const.ARG_FILE])

        # Simulate the plan
        # if it fails, it would throw errors and get out of this command
        split_plan.simulate()

        # If we are here then simulation passed
        # so move the original file to the trash
        trash.store(file_path, True)

        # execute the plan
        split_plan.execute()
        return 0
Beispiel #15
0
    def create_object(cls, model_alias: str,
                      object_type: Type[TopLevelOscalModel],
                      args: argparse.Namespace) -> int:
        """Create a top level OSCAL object within the trestle directory, leveraging functionality in add."""
        log.set_log_level_from_args(args)
        trestle_root = args.trestle_root  # trestle root is set via command line in args. Default is cwd.
        if not trestle_root or not file_utils.is_valid_project_root(
                args.trestle_root):
            raise err.TrestleRootError(
                f'Given directory {trestle_root} is not a trestle project.')

        plural_path = ModelUtils.model_type_to_model_dir(model_alias)

        desired_model_dir = trestle_root / plural_path / args.output

        desired_model_path = desired_model_dir / (model_alias + '.' +
                                                  args.extension)

        if desired_model_path.exists():
            raise err.TrestleError(
                f'OSCAL file to be created here: {desired_model_path} exists.')

        # Create sample model.
        sample_model = generators.generate_sample_model(
            object_type, include_optional=args.include_optional_fields)
        # Presuming top level level model not sure how to do the typing for this.
        sample_model.metadata.title = f'Generic {model_alias} created by trestle named {args.output}.'  # type: ignore
        sample_model.metadata.last_modified = datetime.now().astimezone()
        sample_model.metadata.oscal_version = trestle.oscal.OSCAL_VERSION
        sample_model.metadata.version = '0.0.0'

        top_element = Element(sample_model, model_alias)

        create_action = CreatePathAction(desired_model_path.resolve(), True)
        write_action = WriteFileAction(
            desired_model_path.resolve(), top_element,
            FileContentType.to_content_type(desired_model_path.suffix))

        # create a plan to write the directory and file.
        create_plan = Plan()
        create_plan.add_action(create_action)
        create_plan.add_action(write_action)
        create_plan.execute()
        return CmdReturnCodes.SUCCESS.value
    def oscal_write(self, path: pathlib.Path) -> None:
        """
        Write out a pydantic data model in an oscal friendly way.

        OSCAL schema mandates that top level elements are wrapped in a singular
        json/yaml field. This function handles both json and yaml output as well
        as formatting of the json.

        Args:
            path: The output file location for the oscal object.

        Raises:
            err.TrestleError: If a unknown file extension is provided.
        """
        class_name = self.__class__.__name__
        # It would be nice to pass through the description but I can't seem to and
        # it does not affect the output
        dynamic_parser = {}
        dynamic_parser[classname_to_alias(
            class_name,
            'field')] = (self.__class__,
                         Field(self,
                               title=classname_to_alias(class_name, 'field'),
                               alias=classname_to_alias(class_name, 'json')))
        wrapper_model = create_model(class_name,
                                     __base__=OscalBaseModel,
                                     **dynamic_parser)  # type: ignore
        # Default behaviour is strange here.
        wrapped_model = wrapper_model(
            **{classname_to_alias(class_name, 'json'): self})
        #
        content_type = FileContentType.to_content_type(path.suffix)
        write_file = pathlib.Path(path).open('w', encoding=const.FILE_ENCODING)
        if content_type == FileContentType.YAML:
            yaml.dump(
                yaml.safe_load(
                    wrapped_model.json(exclude_none=True, by_alias=True)),
                write_file)
        elif content_type == FileContentType.JSON:
            write_file.write(
                wrapped_model.json(exclude_none=True, by_alias=True, indent=2))
Beispiel #17
0
    def oscal_read(cls, path: pathlib.Path) -> 'OscalBaseModel':
        """
        Read OSCAL objects.

        Handles the fact OSCAL wrap's top level elements and also deals with both yaml and json.
        """
        # Create the wrapper model.
        alias = classname_to_alias(cls.__name__, 'json')

        content_type = FileContentType.to_content_type(path.suffix)

        if content_type == FileContentType.YAML:
            return cls.parse_obj(yaml.safe_load(path.open())[alias])
        elif content_type == FileContentType.JSON:
            obj = load_file(
                path,
                json_loads=cls.__config__.json_loads,
            )
            return cls.parse_obj(obj[alias])
        else:
            raise err.TrestleError('Unknown file type')
Beispiel #18
0
    def _run(self, args: argparse.Namespace) -> int:
        """Remove an OSCAL component/subcomponent to the specified component.

        This method takes input a filename and a list of comma-seperated element path. Element paths are field aliases.
        The method first finds the parent model from the file and loads the file into the model.
        Then the method executes 'remove' for each of the element paths specified.
        """
        log.set_log_level_from_args(args)
        args_dict = args.__dict__

        file_path = pathlib.Path(args_dict[const.ARG_FILE])

        # Get parent model and then load json into parent model
        try:
            parent_model, parent_alias = fs.get_contextual_model_type(file_path.absolute())
        except Exception as err:
            logger.debug(f'fs.get_contextual_model_type() failed: {err}')
            logger.error(f'Remove failed (fs.get_contextual_model_type()): {err}')
            return 1

        try:
            parent_object = parent_model.oscal_read(file_path.absolute())
        except Exception as err:
            logger.debug(f'parent_model.oscal_read() failed: {err}')
            logger.error(f'Remove failed (parent_model.oscal_read()): {err}')
            return 1

        parent_element = Element(parent_object, utils.classname_to_alias(parent_model.__name__, 'json'))

        add_plan = Plan()

        # Do _remove for each element_path specified in args
        element_paths: List[str] = str(args_dict[const.ARG_ELEMENT]).split(',')
        for elm_path_str in element_paths:
            element_path = ElementPath(elm_path_str)
            try:
                remove_action, parent_element = self.remove(element_path, parent_model, parent_element)
            except TrestleError as err:
                logger.debug(f'self.remove() failed: {err}')
                logger.error(f'Remove failed (self.remove()): {err}')
                return 1
            add_plan.add_action(remove_action)

        create_action = CreatePathAction(file_path.absolute(), True)
        write_action = WriteFileAction(
            file_path.absolute(), parent_element, FileContentType.to_content_type(file_path.suffix)
        )
        add_plan.add_action(remove_action)
        add_plan.add_action(create_action)
        add_plan.add_action(write_action)

        try:
            add_plan.simulate()
        except TrestleError as err:
            logger.debug(f'Remove failed at simulate(): {err}')
            logger.error(f'Remove failed (simulate()): {err}')
            return 1

        try:
            add_plan.execute()
        except TrestleError as err:
            logger.debug(f'Remove failed at execute(): {err}')
            logger.error(f'Remove failed (execute()): {err}')
            return 1

        return 0
Beispiel #19
0
    def perform_split(
        cls, effective_cwd: pathlib.Path, file_name: str, elements: str, trestle_root: pathlib.Path
    ) -> int:
        """Perform the split operation.

        Args:
            effective_cwd: effective directory in which the the split operation is performed
            file_name: file name of model to split, or '' if deduced from elements and cwd
            elements: comma separated list of paths to strip from the file, with quotes removed

        Returns:
            0 on success and 1 on failure
        """
        file_path_list: List[Tuple[str, str]] = []

        if file_name:
            file_path_list.append((file_name, elements))
        else:
            # cwd must be in the model directory if file to split is not specified
            # find top directory for this model based on trestle root and cwd
            model_dir = file_utils.extract_project_model_path(effective_cwd)
            if model_dir is None:
                raise TrestleError('Current directory must be within a model directory if file is not specified')

            content_type: FileContentType = FileContentType.dir_to_content_type(model_dir)

            # determine the file needed for each split path
            element_paths = elements.split(',')
            for path in element_paths:
                element_path = ElementPath(path)
                # if element path is relative use directory context to determine absolute path
                element_path.make_absolute(model_dir, effective_cwd)
                file_path = element_path.find_last_file_in_path(content_type, model_dir)
                # now make the element path relative to the model file to be loaded
                if file_path is None or element_path.make_relative(file_path.relative_to(model_dir)) != 0:
                    raise TrestleError(f'Unable to match element path with files in model directory {element_path}')

                file_path_list.append((file_path, element_path.to_string()))

        # match paths to corresponding files since several paths may be split from the same file
        file_path_dict: Dict[str, str] = {}
        for file_path in file_path_list:
            key = file_path[0]
            path = file_path[1]
            if key not in file_path_dict:
                file_path_dict[key] = path
            else:
                current_path = file_path_dict[key]
                file_path_dict[key] = f'{current_path},{path}'

        for raw_file_name, element_path in file_path_dict.items():
            file_path = file_utils.relative_resolve(pathlib.Path(raw_file_name), effective_cwd)
            # this makes assumptions that the path is relative.
            if not file_path.exists():
                raise TrestleError(f'File {file_path} does not exist.')

            content_type = FileContentType.to_content_type(file_path.suffix)

            # find the base directory of the file
            base_dir = file_path.parent
            model_type, _ = ModelUtils.get_stripped_model_type(file_path, trestle_root)

            model: OscalBaseModel = model_type.oscal_read(file_path)

            if cmd_utils.split_is_too_fine(element_path, model):
                raise TrestleError('Cannot split the model to the level of uuids, strings, etc.')

            # use the model itself to resolve any wildcards and create list of element paths
            logger.debug(f'split calling parse_element_args on {element_path}')
            # use contextual mode to parse

            element_paths: List[ElementPath] = cmd_utils.parse_element_args(
                model, element_path.split(','), base_dir.relative_to(trestle_root)
            )

            # analyze the split tree and determine which aliases should be stripped from each file
            aliases_to_strip = cls.find_aliases_to_strip(element_paths)

            # need the file name relative to the base directory
            file_name_no_path = str(file_path.name)

            split_plan = cls.split_model(
                model, element_paths, base_dir, content_type, file_name_no_path, aliases_to_strip
            )
            trash.store(file_path, True)

            try:
                split_plan.execute()
            except Exception as e:
                trash.recover(file_path, True)
                raise TrestleError(f'Split has failed with error: {e}.')

        return CmdReturnCodes.SUCCESS.value
Beispiel #20
0
    def _run(self, args: argparse.Namespace) -> int:
        """Top level import run command."""
        try:
            log.set_log_level_from_args(args)
            trestle_root = args.trestle_root
            if not file_utils.is_valid_project_root(trestle_root):
                raise TrestleRootError(
                    f'Attempt to import from non-valid trestle project root {trestle_root}'
                )

            input_uri = args.file
            if cache.FetcherFactory.in_trestle_directory(
                    trestle_root, input_uri):
                raise TrestleError(
                    f'Imported file {input_uri} cannot be from current trestle project. Use duplicate instead.'
                )

            content_type = FileContentType.to_content_type(
                '.' + input_uri.split('.')[-1])

            fetcher = cache.FetcherFactory.get_fetcher(trestle_root,
                                                       str(input_uri))

            model_read, parent_alias = fetcher.get_oscal(True)

            plural_path = ModelUtils.model_type_to_model_dir(parent_alias)

            output_name = args.output

            desired_model_dir = trestle_root / plural_path
            desired_model_path: pathlib.Path = desired_model_dir / output_name / parent_alias
            desired_model_path = desired_model_path.with_suffix(
                FileContentType.to_file_extension(content_type)).resolve()

            if desired_model_path.exists():
                raise TrestleError(
                    f'Cannot import because file to be imported here: {desired_model_path} already exists.'
                )

            if args.regenerate:
                logger.debug(
                    f'regenerating uuids in imported file {input_uri}')
                model_read, lut, nchanged = ModelUtils.regenerate_uuids(
                    model_read)
                logger.debug(
                    f'uuid lut has {len(lut.items())} entries and {nchanged} refs were updated'
                )

            top_element = Element(model_read)
            create_action = CreatePathAction(desired_model_path, True)
            write_action = WriteFileAction(desired_model_path, top_element,
                                           content_type)

            # create a plan to create the directory and write the imported file.
            import_plan = Plan()
            import_plan.add_action(create_action)
            import_plan.add_action(write_action)

            import_plan.execute()

            args = argparse.Namespace(file=desired_model_path,
                                      verbose=args.verbose,
                                      trestle_root=args.trestle_root,
                                      type=None,
                                      all=None)
            rollback = False
            try:
                rc = validatecmd.ValidateCmd()._run(args)
                if rc > 0:
                    logger.warning(
                        f'Validation of imported file {desired_model_path} did not pass'
                    )
                    rollback = True
            except TrestleError as err:
                logger.warning(
                    f'Import of {str(input_uri)} failed with validation error: {err}'
                )
                rollback = True

            if rollback:
                logger.debug(
                    f'Rolling back import of {str(input_uri)} to {desired_model_path}'
                )
                try:
                    import_plan.rollback()
                except TrestleError as err:
                    raise TrestleError(
                        f'Import failed in plan rollback: {err}. Manually remove {desired_model_path} to recover.'
                    )
                logger.debug(
                    f'Successful rollback of import to {desired_model_path}')
                return CmdReturnCodes.COMMAND_ERROR.value

            return CmdReturnCodes.SUCCESS.value

        except Exception as e:  # pragma: no cover
            return handle_generic_command_exception(
                e, logger, 'Error while importing OSCAL file')
Beispiel #21
0
    def _run(self, args: argparse.Namespace) -> int:
        """Top level import run command."""
        log.set_log_level_from_args(args)

        logger.debug('Entering import run.')

        # 1. Validate input arguments are as expected.
        # This code block may never be reached as the argument is declared to be required.

        # 1.1 Check that input file given exists.
        input_file = pathlib.Path(args.file)
        if not input_file.exists():
            logger.error(f'Input file {args.file} does not exist.')
            return 1

        # 1.2 Bad working directory if not running from current working directory
        cwd = pathlib.Path.cwd().resolve()
        trestle_root = fs.get_trestle_project_root(cwd)
        if trestle_root is None:
            logger.error(
                f'Current working directory: {cwd} is not within a trestle project.'
            )
            return 1

        # 2. Importing a file that is already inside a trestle-initialized dir is bad
        trestle_root = trestle_root.resolve()
        try:
            input_file.absolute().relative_to(trestle_root)
        except ValueError:
            # An exception here is good: it means that the input file is not inside a trestle dir.
            pass
        else:
            logger.error(
                'Input file cannot be from current trestle project. Use duplicate instead.'
            )
            return 1

        # 3. Work out typing information from input suffix.
        try:
            content_type = FileContentType.to_content_type(input_file.suffix)
        except TrestleError as err:
            logger.debug(f'FileContentType.to_content_type() failed: {err}')
            logger.error(
                f'Import failed, could not work out content type from file suffix: {err}'
            )
            return 1

        # 4. Load input and parse for model

        # 4.1 Load from file
        try:
            data = fs.load_file(input_file.absolute())
        except JSONDecodeError as err:
            logger.debug(f'fs.load_file() failed: {err}')
            logger.error(f'Import failed, JSON error loading file: {err}')
            return 1
        except TrestleError as err:
            logger.debug(f'fs.load_file() failed: {err}')
            logger.error(f'Import failed, error loading file: {err}')
            return 1
        except PermissionError as err:
            logger.debug(f'fs.load_file() failed: {err}')
            logger.error(
                f'Import failed, access permission error loading file: {err}')
            return 1

        # 4.2 root key check
        try:
            parent_alias = parser.root_key(data)
        except TrestleError as err:
            logger.debug(f'parser.root_key() failed: {err}')
            logger.error(
                f'Import failed, failed to parse input file for root key: {err}'
            )
            return 1

        # 4.3 parse the model
        parent_model_name = parser.to_full_model_name(parent_alias)
        try:
            parent_model = parser.parse_file(input_file.absolute(),
                                             parent_model_name)
        except TrestleError as err:
            logger.debug(f'parser.parse_file() failed: {err}')
            logger.error(
                f'Import failed, failed to parse valid contents of input file: {err}'
            )
            return 1

        # 5. Work out output directory and file
        plural_path: str
        plural_path = parent_alias
        # Cater to POAM
        if parent_alias[-1] != 's':
            plural_path = parent_alias + 's'

        desired_model_dir = trestle_root / plural_path
        # args.output is presumed to be assured as it is declared to be required
        if args.output:
            desired_model_path = desired_model_dir / args.output / (
                parent_alias + input_file.suffix)

        if desired_model_path.exists():
            logger.error(
                f'OSCAL file to be created here: {desired_model_path} exists.')
            logger.error('Aborting trestle import.')
            return 1

        # 6. Prepare actions and plan
        top_element = Element(parent_model.oscal_read(input_file))
        create_action = CreatePathAction(desired_model_path.absolute(), True)
        write_action = WriteFileAction(desired_model_path.absolute(),
                                       top_element, content_type)

        # create a plan to create the directory and imported file.
        import_plan = Plan()
        import_plan.add_action(create_action)
        import_plan.add_action(write_action)

        try:
            import_plan.simulate()
        except TrestleError as err:
            logger.debug(f'import_plan.simulate() failed: {err}')
            logger.error(
                f'Import failed, error in testing import operation: {err}')
            return 1

        try:
            import_plan.execute()
        except TrestleError as err:
            logger.debug(f'import_plan.execute() failed: {err}')
            logger.error(
                f'Import failed, error in actual import operation: {err}')
            return 1

        # 7. Leave the rest to trestle split
        return 0