Exemple #1
0
def test_to_full_model_name():
    """Test to_full_model_name."""
    tests = [{
        'root_key': 'catalog',
        'name': None,
        'expected': f'{const.PACKAGE_OSCAL}.catalog.Catalog',
    }, {
        'root_key': 'catalog',
        'name': 'group',
        'expected': f'{const.PACKAGE_OSCAL}.catalog.Group',
    }, {
        'root_key': 'target-definition',
        'name': None,
        'expected': f'{const.PACKAGE_OSCAL}.target.TargetDefinition',
    }, {
        'root_key': 'target',
        'name': 'target-definition',
        'expected': f'{const.PACKAGE_OSCAL}.target.TargetDefinition',
    }, {
        'root_key': 'invalid',
        'name': None,
        'expected': None,
    }]

    for test in tests:
        model_name = parser.to_full_model_name(test['root_key'], test['name'])
        assert model_name == test['expected']
Exemple #2
0
    def __init__(self, root_dir: pathlib.Path, model_type: Type[OscalBaseModel], name: str) -> None:
        """Initialize repository OSCAL model object."""
        if not file_utils.is_valid_project_root(root_dir):
            raise TrestleError(f'Provided root directory {str(root_dir)} is not a valid Trestle root directory.')
        self._root_dir = root_dir
        self._model_type = model_type
        self._model_name = name

        # set model alais and dir
        self.model_alias = classname_to_alias(self._model_type.__name__, AliasMode.JSON)
        if parser.to_full_model_name(self.model_alias) is None:
            raise TrestleError(f'Given model {self.model_alias} is not a top level model.')

        plural_path = ModelUtils.model_type_to_model_dir(self.model_alias)
        self.model_dir = self._root_dir / plural_path / self._model_name

        if not self.model_dir.exists() or not self.model_dir.is_dir():
            raise TrestleError(f'Model dir {self._model_name} does not exist.')

        file_content_type = FileContentType.path_to_content_type(self.model_dir / self.model_alias)
        if file_content_type == FileContentType.UNKNOWN:
            raise TrestleError(f'Model file for model {self._model_name} does not exist.')
        self.file_content_type = file_content_type

        filepath = pathlib.Path(
            self.model_dir,
            self.model_alias + FileContentType.path_to_file_extension(self.model_dir / self.model_alias)
        )

        self.filepath = filepath
Exemple #3
0
    def write(self, model: OscalBaseModel) -> bool:
        """Write OSCAL model to repository."""
        logger.debug(f'Writing model {self._model_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.')

        # split directory if the model was split
        split_dir = pathlib.Path(self.model_dir, self.model_alias)

        # Prepare actions; delete split model dir if any, recreate model file, and write to filepath
        top_element = Element(model)
        remove_action = RemovePathAction(split_dir)
        create_action = CreatePathAction(self.filepath, True)
        write_action = WriteFileAction(self.filepath, top_element, self.file_content_type)

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

        import_plan.execute()

        logger.debug(f'Model {self._model_name} written to repository')
        return True
Exemple #4
0
    def delete_model(self, model_type: Type[OscalBaseModel], name: str) -> bool:
        """Delete an OSCAL model from repository."""
        logger.debug(f'Deleting model {name} of type {model_type.__name__}.')
        model_alias = classname_to_alias(model_type.__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.')
        plural_path = ModelUtils.model_type_to_model_dir(model_alias)
        desired_model_dir = self._root_dir / plural_path / name

        if not desired_model_dir.exists() or not desired_model_dir.is_dir():
            raise TrestleError(f'Model {name} does not exist.')
        shutil.rmtree(desired_model_dir)

        # remove model from dist directory if it exists
        dist_model_dir = self._root_dir / const.TRESTLE_DIST_DIR / plural_path
        file_content_type = FileContentType.path_to_content_type(dist_model_dir / name)
        if file_content_type != FileContentType.UNKNOWN:
            file_path = pathlib.Path(
                dist_model_dir, name + FileContentType.path_to_file_extension(dist_model_dir / name)
            )
            logger.debug(f'Deleting model {name} from dist directory.')
            os.remove(file_path)

        logger.debug(f'Model {name} deleted successfully.')
        return True
Exemple #5
0
 def get_oscal(self, force_update=False) -> Tuple[OscalBaseModel, str]:
     """Retrieve the cached file and model name without knowing its model type."""
     model_dict = self.get_raw(force_update)
     root_key = parser.root_key(model_dict)
     model_name = parser.to_full_model_name(root_key)
     if model_name is None:
         raise TrestleError(f'Failed cache read of non top level model with root_key {root_key}')
     return parser.parse_dict(model_dict[root_key], model_name), root_key
Exemple #6
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)
Exemple #7
0
    def list_models(self, model_type: Type[OscalBaseModel]) -> List[str]:
        """List models of a given type in trestle repository."""
        logger.debug(f'Listing models of type {model_type.__name__}.')
        model_alias = classname_to_alias(model_type.__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.')
        models = ModelUtils.get_models_of_type(model_alias, self._root_dir)

        return models
Exemple #8
0
    def get_model(self, model_type: Type[OscalBaseModel], name: str) -> ManagedOSCAL:
        """Get a specific OSCAL model from repository."""
        logger.debug(f'Getting model {name} of type {model_type.__name__}.')
        model_alias = classname_to_alias(model_type.__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.')
        plural_path = ModelUtils.model_type_to_model_dir(model_alias)
        desired_model_dir = self._root_dir / plural_path / name

        if not desired_model_dir.exists() or not desired_model_dir.is_dir():
            raise TrestleError(f'Model {name} does not exist.')

        return ManagedOSCAL(self._root_dir, model_type, name)
Exemple #9
0
    def validate_model(self, model_type: Type[OscalBaseModel], name: str) -> bool:
        """Validate an OSCAL model in repository."""
        logger.debug(f'Validating model {name} of type {model_type.__name__}.')
        success = False

        model_alias = classname_to_alias(model_type.__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.')

        verbose = log.get_current_verbosity_level(logger)
        args = argparse.Namespace(type=model_alias, name=name, trestle_root=self._root_dir, verbose=verbose)

        try:
            ret = validatecmd.ValidateCmd()._run(args)
            if ret == 0:
                success = True
        except Exception as e:
            raise TrestleError(f'Error in validating model: {e}')

        logger.debug(f'Model {name} validated successfully.')
        return success
Exemple #10
0
    def assemble_model(self, model_type: Type[OscalBaseModel], name: str, extension='json') -> bool:
        """Assemble an OSCAL model in repository and publish it to 'dist' directory."""
        logger.debug(f'Assembling model {name} of type {model_type.__name__}.')
        success = False

        model_alias = classname_to_alias(model_type.__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.')

        verbose = log.get_current_verbosity_level(logger)
        args = argparse.Namespace(
            type=model_alias, name=name, extension=extension, trestle_root=self._root_dir, verbose=verbose
        )

        try:
            ret = assemblecmd.AssembleCmd().assemble_model(model_alias, args)
            if ret == 0:
                success = True
        except Exception as e:
            raise TrestleError(f'Error in assembling model: {e}')

        logger.debug(f'Model {name} assembled successfully.')
        return success
Exemple #11
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