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']
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
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
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
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
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)
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
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)
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
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
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