def test_add(tmp_dir, sample_catalog_minimal): """Test AddCmd.add() method for trestle add.""" # expected catalog after first add of Role file_path = pathlib.Path.joinpath(test_utils.JSON_TEST_DATA_PATH, 'minimal_catalog_roles.json') expected_catalog_roles1 = Element(Catalog.oscal_read(file_path)) # expected catalog after second add of Role file_path = pathlib.Path.joinpath(test_utils.JSON_TEST_DATA_PATH, 'minimal_catalog_roles_double.json') expected_catalog_roles2 = Element(Catalog.oscal_read(file_path)) # expected catalog after add of Responsible-Party file_path = pathlib.Path.joinpath(test_utils.JSON_TEST_DATA_PATH, 'minimal_catalog_roles_double_rp.json') expected_catalog_roles2_rp = Element(Catalog.oscal_read(file_path)) content_type = FileContentType.JSON catalog_def_dir, catalog_def_file = test_utils.prepare_trestle_project_dir( tmp_dir, content_type, sample_catalog_minimal, test_utils.CATALOGS_DIR) # Execute first _add element_path = ElementPath('catalog.metadata.roles') catalog_element = Element(sample_catalog_minimal) expected_update_action_1 = UpdateAction( expected_catalog_roles1.get_at(element_path), catalog_element, element_path) actual_update_action, actual_catalog_roles = AddCmd.add( element_path, Catalog, catalog_element) assert actual_catalog_roles == expected_catalog_roles1 assert actual_update_action == expected_update_action_1 # Execute second _add - this time roles already exists, so this adds a roles object to roles array catalog_element = actual_catalog_roles expected_update_action_2 = UpdateAction( expected_catalog_roles2.get_at(element_path), catalog_element, element_path) actual_update_action2, actual_catalog_roles2 = AddCmd.add( element_path, Catalog, catalog_element) assert actual_catalog_roles2 == expected_catalog_roles2 assert actual_update_action2 == expected_update_action_2 # Execute _add for responsible-parties to the same catalog element_path = ElementPath('catalog.metadata.responsible-parties') catalog_element = actual_catalog_roles2 expected_update_action_3 = UpdateAction( expected_catalog_roles2_rp.get_at(element_path), catalog_element, element_path) actual_update_action3, actual_catalog_roles2_rp = AddCmd.add( element_path, Catalog, catalog_element) assert actual_catalog_roles2_rp == expected_catalog_roles2_rp assert actual_update_action3 == expected_update_action_3
def test_element_set_at(sample_target_def: target.TargetDefinition): """Test element get method.""" element = Element(sample_target_def) metadata = target.Metadata( **{ 'title': 'My simple catalog', 'last-modified': datetime.now(), 'version': '0.0.0', 'oscal-version': '1.0.0-Milestone3' } ) parties: List[target.Party] = [] parties.append( target.Party(**{ 'uuid': 'ff47836c-877c-4007-bbf3-c9d9bd805000', 'name': 'TEST1', 'type': 'organization' }) ) parties.append( target.Party(**{ 'uuid': 'ee88836c-877c-4007-bbf3-c9d9bd805000', 'name': 'TEST2', 'type': 'organization' }) ) assert element.set_at(ElementPath('target-definition.metadata'), metadata).get_at(ElementPath('target-definition.metadata')) == metadata assert element.set_at(ElementPath('target-definition.metadata.parties'), parties).get_at(ElementPath('target-definition.metadata.parties')) == parties assert element.set_at(ElementPath('target-definition.metadata.parties.*'), parties).get_at(ElementPath('target-definition.metadata.parties')) == parties # unset assert element.set_at(ElementPath('target-definition.metadata.parties'), None).get_at(ElementPath('target-definition.metadata.parties')) is None # string element path assert element.set_at('target-definition.metadata.parties', parties).get_at(ElementPath('target-definition.metadata.parties')) == parties with pytest.raises(TrestleError): assert element.set_at(ElementPath('target-definition.metadata'), parties).get_at(ElementPath('target-definition.metadata.parties')) == parties # wildcard requires it to be an OscalBaseModel or list with pytest.raises(TrestleError): assert element.set_at(ElementPath('target-definition.metadata.parties.*'), 'INVALID') # invalid attribute with pytest.raises(TrestleError): assert element.set_at(ElementPath('target-definition.metadata.groups.*'), parties)
def test_split_chained_sub_model_plans( tmp_path: pathlib.Path, simplified_nist_catalog: oscatalog.Catalog, keep_cwd: pathlib.Path) -> None: """Test for split_model method with chained sum models like catalog.metadata.parties.*.""" # Assume we are running a command like below # trestle split -f catalog.json -e catalog.metadata.parties.* # see https://github.com/IBM/compliance-trestle/issues/172 content_type = FileContentType.JSON # prepare trestle project dir with the file catalog_dir, catalog_file = test_utils.prepare_trestle_project_dir( tmp_path, content_type, simplified_nist_catalog, test_utils.CATALOGS_DIR) # read the model from file catalog = oscatalog.Catalog.oscal_read(catalog_file) element = Element(catalog) element_args = ['catalog.metadata.parties.*'] element_paths = cmd_utils.parse_element_args( None, element_args, catalog_dir.relative_to(tmp_path)) assert 2 == len(element_paths) expected_plan = Plan() # prepare to extract metadata and parties metadata_file = catalog_dir / element_paths[0].to_file_path(content_type) metadata_field_alias = element_paths[0].get_element_name() metadata = element.get_at(element_paths[0]) meta_element = Element(metadata, metadata_field_alias) # extract parties parties_dir = catalog_dir / 'catalog/metadata/parties' for i, party in enumerate(meta_element.get_at(element_paths[1], False)): prefix = str(i).zfill(const.FILE_DIGIT_PREFIX_LENGTH) sub_model_actions = SplitCmd.prepare_sub_model_split_actions( party, parties_dir, prefix, content_type) expected_plan.add_actions(sub_model_actions) # stripped metadata stripped_metadata = metadata.stripped_instance( stripped_fields_aliases=['parties']) expected_plan.add_action(CreatePathAction(metadata_file)) expected_plan.add_action( WriteFileAction(metadata_file, Element(stripped_metadata, metadata_field_alias), content_type)) # stripped catalog root_file = catalog_dir / element_paths[0].to_root_path(content_type) remaining_root = element.get().stripped_instance(metadata_field_alias) expected_plan.add_action(CreatePathAction(root_file, True)) expected_plan.add_action( WriteFileAction(root_file, Element(remaining_root), content_type)) split_plan = SplitCmd.split_model(catalog, element_paths, catalog_dir, content_type, '', None) assert expected_plan == split_plan
def test_write_action_json(tmp_json_file, sample_nist_component_def): """Test write json action.""" element = Element(sample_nist_component_def, 'component-definition') with open(tmp_json_file, 'w+', encoding=const.FILE_ENCODING) as writer: wa = WriteAction(writer, element, FileContentType.JSON) wa.execute() writer.flush() writer.close() test_utils.verify_file_content(tmp_json_file, element.get()) os.remove(tmp_json_file)
def test_write_existing_file_rollback(tmp_yaml_file, sample_nist_component_def): """Test rollback.""" # add some content tmp_yaml_file.touch() current_pos = 0 with open(tmp_yaml_file, 'a+', encoding=const.FILE_ENCODING) as fp: fp.write('....\n') current_pos = fp.tell() # write to the file element = Element(sample_nist_component_def, 'component-definition') wa = WriteFileAction(tmp_yaml_file, element, FileContentType.YAML) wa.execute() # verify the file content is not empty with open(tmp_yaml_file, 'a+', encoding=const.FILE_ENCODING) as fp: assert fp.tell() > current_pos # rollback to the original wa.rollback() # # verify the file content is empty with open(tmp_yaml_file, 'a+', encoding=const.FILE_ENCODING) as fp: assert fp.tell() == current_pos
def test_merge_everything_into_catalog_with_hidden_files_in_folders( testdata_dir, tmp_trestle_dir): """Test trestle merge -e 'catalog.*' when metadata and catalog are split and hidden files are present.""" # Assume we are running a command like below # trestle merge -e catalog.* content_type = FileContentType.JSON fext = FileContentType.to_file_extension(content_type) # prepare trestle project dir with the file test_utils.ensure_trestle_config_dir(tmp_trestle_dir) test_data_source = testdata_dir / 'split_merge/step4_split_groups_array/catalogs' catalogs_dir = Path('catalogs/') mycatalog_dir = catalogs_dir / 'mycatalog' # Copy files from test/data/split_merge/step4 shutil.rmtree(catalogs_dir) shutil.copytree(test_data_source, catalogs_dir) # Change directory to mycatalog_dir os.chdir(mycatalog_dir) catalog_file = Path(f'catalog{fext}').resolve() assert catalog_file.exists() # Read files # Create hand-crafter merge plan expected_plan: Plan = Plan() reset_destination_action = CreatePathAction(catalog_file, clear_content=True) expected_plan.add_action(reset_destination_action) _, _, merged_catalog_instance = ModelUtils.load_distributed( catalog_file, tmp_trestle_dir) element = Element(merged_catalog_instance) write_destination_action = WriteFileAction(catalog_file, element, content_type=content_type) expected_plan.add_action(write_destination_action) delete_element_action = RemovePathAction(Path('catalog').resolve()) expected_plan.add_action(delete_element_action) test_utils.make_hidden_file(tmp_trestle_dir / 'catalogs/mycatalog/.DS_Store') test_utils.make_hidden_file(tmp_trestle_dir / 'catalogs/mycatalog/catalog/.DS_Store') test_utils.make_hidden_file( tmp_trestle_dir / 'catalogs/mycatalog/catalog/metadata/.DS_Store') test_utils.make_hidden_file(tmp_trestle_dir / 'catalogs/mycatalog/catalog/groups/.DS_Store') # Call merge() generated_plan = MergeCmd.merge(Path.cwd(), ElementPath('catalog.*'), tmp_trestle_dir) # Assert the generated plan matches the expected plan' assert generated_plan == expected_plan
def test_remove_failure(tmp_path, sample_catalog_minimal): """Test failure of RemoveCmd.remove() method for trestle remove.""" # Note: minimal catalog does have responsible-parties but doesn't have Roles. file_path = pathlib.Path.joinpath(test_utils.JSON_TEST_DATA_PATH, 'minimal_catalog.json') catalog_with_responsible_parties = Element(Catalog.oscal_read(file_path)) # Supply nonexistent element Roles for removal: element_path = ElementPath('catalog.metadata.roles') try: actual_remove_action, actual_catalog_removed_responsible_parties = RemoveCmd.remove( element_path, Catalog, catalog_with_responsible_parties) except Exception: assert True else: AssertionError() # Supply a wildcard element for removal: element_path = ElementPath('catalog.*') try: actual_remove_action, actual_catalog_removed_responsible_parties = RemoveCmd.remove( element_path, Catalog, catalog_with_responsible_parties) except Exception: assert True else: AssertionError()
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
def split_model( cls, model_obj: OscalBaseModel, element_paths: List[ElementPath], base_dir: pathlib.Path, content_type: FileContentType, root_file_name: str, aliases_to_strip: Dict[str, AliasTracker] ) -> Plan: """Split the model at the provided element paths. It returns a plan for the operation """ # initialize plan split_plan = Plan() # loop through the element path list and update the split_plan stripped_field_alias = [] cur_path_index = 0 while cur_path_index < len(element_paths): # extract the sub element name for each of the root path of the path chain element_path = element_paths[cur_path_index] if element_path.get_parent() is None and len(element_path.get()) > 1: stripped_part = element_path.get()[1] if stripped_part == ElementPath.WILDCARD: stripped_field_alias.append('__root__') else: if stripped_part not in stripped_field_alias: stripped_field_alias.append(stripped_part) # split model at the path chain cur_path_index = cls.split_model_at_path_chain( model_obj, element_paths, base_dir, content_type, cur_path_index, split_plan, False, root_file_name, aliases_to_strip ) cur_path_index += 1 # strip the root model object and add a WriteAction stripped_root = model_obj.stripped_instance(stripped_fields_aliases=stripped_field_alias) # If it's an empty model after stripping the fields, don't create path and don't write if set(model_obj.__fields__.keys()) == set(stripped_field_alias): return split_plan if root_file_name != '': root_file = base_dir / root_file_name else: root_file = base_dir / element_paths[0].to_root_path(content_type) split_plan.add_action(CreatePathAction(root_file, True)) wrapper_alias = classname_to_alias(stripped_root.__class__.__name__, AliasMode.JSON) split_plan.add_action(WriteFileAction(root_file, Element(stripped_root, wrapper_alias), content_type)) return split_plan
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 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
def _run(self, args): """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. """ args = args.__dict__ if args[const.ARG_FILE] is None: raise err.TrestleError( f'Argument "-{const.ARG_FILE_SHORT}" is required') if args[const.ARG_ELEMENT] is None: raise err.TrestleError( f'Argument "-{const.ARG_ELEMENT}" is required') file_path = pathlib.Path(args[const.ARG_FILE]) # Get parent model and then load json into parent model parent_model, parent_alias = fs.get_contextual_model_type( file_path.absolute()) parent_object = parent_model.oscal_read(file_path.absolute()) parent_element = Element( parent_object, utils.classname_to_alias(parent_model.__name__, 'json')) # Do _add for each element_path specified in args element_paths: list[str] = args[const.ARG_ELEMENT].split(',') for elm_path_str in element_paths: element_path = ElementPath(elm_path_str) self.add(file_path, element_path, parent_model, parent_element)
def test_write_file_rollback(tmp_yaml_file: pathlib.Path, sample_target): """Test rollback.""" element = Element(sample_target) tmp_yaml_file.touch() wa = WriteFileAction(tmp_yaml_file, element, FileContentType.YAML) wa.execute() test_utils.verify_file_content(tmp_yaml_file, element.get()) # verify the file content is not empty with open(tmp_yaml_file, 'r', encoding='utf8') as read_file: assert read_file.tell() >= 0 wa.rollback() # verify the file content is empty with open(tmp_yaml_file, 'r', encoding='utf8') as read_file: assert read_file.tell() == 0
def test_plan_execution(tmp_path, sample_nist_component_def: component.ComponentDefinition): """Test successful execution of a valid plan.""" content_type = FileContentType.YAML base_dir: pathlib.Path = pathlib.Path.joinpath(tmp_path, 'mycomponent') targets_dir: pathlib.Path = pathlib.Path.joinpath(base_dir, 'components') metadata_yaml: pathlib.Path = pathlib.Path.joinpath(base_dir, 'metadata.yaml') test_utils.ensure_trestle_config_dir(base_dir) # hand craft a split plan split_plan = Plan() split_plan.add_action(CreatePathAction(metadata_yaml)) split_plan.add_action( WriteFileAction( metadata_yaml, Element(sample_nist_component_def.metadata, 'component-definition'), content_type ) ) # Test stringing a plan stringed = str(split_plan) assert len(stringed) > 0 target_files: List[pathlib.Path] = [] for index in range(len(sample_nist_component_def.components)): target_file: pathlib.Path = pathlib.Path.joinpath(targets_dir, f'component_{index}.yaml') target_files.append(target_file) split_plan.add_action(CreatePathAction(target_file)) split_plan.add_action( WriteFileAction(target_file, Element(sample_nist_component_def.components[index], 'target'), content_type) ) # execute the plan split_plan.execute() assert base_dir.exists() assert targets_dir.exists() assert metadata_yaml.exists() for target_file in target_files: assert target_file.exists() split_plan.rollback() assert base_dir.exists() is True assert targets_dir.exists() is False assert metadata_yaml.exists() is False for target_file in target_files: target_file.exists()
def test_element_path_eq(sample_nist_component_def): """Test for magic method eq.""" assert ElementPath('component.metadata') == ElementPath( 'component.metadata') assert not (ElementPath('component.metadata') == ElementPath('component.title')) assert not (ElementPath('component.metadata') == Element(sample_nist_component_def))
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
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 replicate_object(cls, model_alias: str, args: argparse.Namespace) -> int: """ Core replicate routine invoked by subcommands. Args: model_alias: Name of the top level model in the trestle directory. args: CLI arguments Returns: A return code that can be used as standard posix codes. 0 is success. """ logger.debug('Entering replicate_object.') # 1 Bad working directory if not running from current working directory 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(trestle_root): raise TrestleError(f'Given directory: {trestle_root} is not a trestle project.') plural_path = ModelUtils.model_type_to_model_dir(model_alias) # 2 Check that input file given exists. input_file_stem = trestle_root / plural_path / args.name / model_alias content_type = FileContentType.path_to_content_type(input_file_stem) if content_type == FileContentType.UNKNOWN: raise TrestleError( f'Input file {args.name} has no json or yaml file at expected location {input_file_stem}.' ) input_file = input_file_stem.with_suffix(FileContentType.to_file_extension(content_type)) # 3 Distributed load from file _, model_alias, model_instance = ModelUtils.load_distributed(input_file, trestle_root) rep_model_path = trestle_root / plural_path / args.output / ( model_alias + FileContentType.to_file_extension(content_type) ) if rep_model_path.exists(): raise TrestleError(f'OSCAL file to be replicated here: {rep_model_path} exists.') if args.regenerate: logger.debug(f'regenerating uuids for model {input_file}') model_instance, uuid_lut, n_refs_updated = ModelUtils.regenerate_uuids(model_instance) logger.debug(f'{len(uuid_lut)} uuids generated and {n_refs_updated} references updated') # 4 Prepare actions and plan top_element = Element(model_instance) create_action = CreatePathAction(rep_model_path, True) write_action = WriteFileAction(rep_model_path, top_element, content_type) # create a plan to create the directory and imported file. replicate_plan = Plan() replicate_plan.add_action(create_action) replicate_plan.add_action(write_action) replicate_plan.execute() return CmdReturnCodes.SUCCESS.value
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
def test_write_file_rollback(tmp_yaml_file: pathlib.Path, sample_nist_component_def): """Test rollback.""" element = Element(sample_nist_component_def, 'component-definition') tmp_yaml_file.touch() wa = WriteFileAction(tmp_yaml_file, element, FileContentType.YAML) wa.execute() test_utils.verify_file_content(tmp_yaml_file, element.get()) # verify the file content is not empty with open(tmp_yaml_file, 'r', encoding=const.FILE_ENCODING) as read_file: assert read_file.tell() >= 0 wa.rollback() # verify the file content is empty with open(tmp_yaml_file, 'r', encoding=const.FILE_ENCODING) as read_file: assert read_file.tell() == 0
def test_plan_execution(tmp_dir, sample_target_def: target.TargetDefinition): """Test successful execution of a valid plan.""" content_type = FileContentType.YAML base_dir: pathlib.Path = pathlib.Path.joinpath(tmp_dir, 'mytarget') targets_dir: pathlib.Path = pathlib.Path.joinpath(base_dir, 'targets') metadata_yaml: pathlib.Path = pathlib.Path.joinpath( base_dir, 'metadata.yaml') test_utils.ensure_trestle_config_dir(base_dir) # hand craft a split plan split_plan = Plan() split_plan.add_action(CreatePathAction(metadata_yaml)) split_plan.add_action( WriteFileAction( metadata_yaml, Element(sample_target_def.metadata, 'target-definition'), content_type)) target_files: List[pathlib.Path] = [] for tid, t in sample_target_def.targets.items(): target_file: pathlib.Path = pathlib.Path.joinpath( targets_dir, tid + '.yaml') target_files.append(target_file) split_plan.add_action(CreatePathAction(target_file)) split_plan.add_action( WriteFileAction(target_file, Element(t, 'target'), content_type)) # execute the plan split_plan.execute() assert base_dir.exists() assert targets_dir.exists() assert metadata_yaml.exists() for target_file in target_files: assert target_file.exists() split_plan.rollback() assert base_dir.exists() is True assert targets_dir.exists() is False assert metadata_yaml.exists() is False for target_file in target_files: target_file.exists() is False
def split_model(cls, model_obj: OscalBaseModel, element_paths: List[ElementPath], base_dir: pathlib.Path, content_type: FileContentType, root_file_name: str = '') -> Plan: """Split the model at the provided element paths. It returns a plan for the operation """ # assume we ran the command below: # trestle split -f target.yaml # -e 'target-definition.metadata, # target-definition.targets.*.target-control-implementations.*' # initialize plan split_plan = Plan() # loop through the element path list and update the split_plan stripped_field_alias = [] cur_path_index = 0 while cur_path_index < len(element_paths): # extract the sub element name for each of the root path of the path chain element_path = element_paths[cur_path_index] if element_path.get_parent() is None and len( element_path.get()) > 1: stripped_part = element_path.get()[1] if stripped_part == ElementPath.WILDCARD: stripped_field_alias.append('__root__') else: stripped_field_alias.append(stripped_part) # split model at the path chain cur_path_index = cls.split_model_at_path_chain( model_obj, element_paths, base_dir, content_type, cur_path_index, split_plan, False, root_file_name) cur_path_index += 1 # strip the root model object and add a WriteAction stripped_root = model_obj.stripped_instance( stripped_fields_aliases=stripped_field_alias) if root_file_name != '': root_file = base_dir / root_file_name else: root_file = base_dir / element_paths[0].to_root_path(content_type) split_plan.add_action(CreatePathAction(root_file, True)) wrapper_alias = utils.classname_to_alias( stripped_root.__class__.__name__, 'json') split_plan.add_action( WriteFileAction(root_file, Element(stripped_root, wrapper_alias), content_type)) return split_plan
def test_update_list_sub_element_action(sample_target_def): """Test setting a list.""" element = Element(sample_target_def) parties: List[target.Party] = [] parties.append( target.Party(**{ 'uuid': 'ff47836c-877c-4007-bbf3-c9d9bd805000', 'name': 'TEST1', 'type': 'organization' }) ) parties.append( target.Party(**{ 'uuid': 'ee88836c-877c-4007-bbf3-c9d9bd805000', 'name': 'TEST2', 'type': 'organization' }) ) sub_element_path = ElementPath('target-definition.metadata.parties.*') uac = UpdateAction(parties, element, sub_element_path) uac.execute() assert element.get_at(sub_element_path) == parties
def get_dir_base_file_element(item, name: str) -> Element: """Get an wrapped element for the base file in a split directory. If the item is a list, it will return a dict like `{"name": []` If the item is a dict, it will return a dict like `{"name": {}}` """ base_model: Dict[str, Any] = {} if isinstance(item, list): base_model[name] = [] else: base_model[name] = {} return Element(base_model)
def check_split_files(): assert target_def_dir.joinpath( 'target-definition/metadata.yaml').exists() assert target_def_dir.joinpath('target-definition.yaml').exists() assert target_def_dir.joinpath('target-definition/targets').exists() assert target_def_dir.joinpath('target-definition/targets').is_dir() targets: Dict = Element(sample_target_def).get_at( ElementPath('target-definition.targets.*')) for uuid in targets: target_file = target_def_dir / f'target-definition/targets/{uuid}{const.IDX_SEP}defined-target.yaml' assert target_file.exists() assert trash.to_trash_file_path(target_def_file).exists()
def prepare_element(sample_target): """Prepare a target element for remove tests.""" element = Element(sample_target) parties: List[target.Party] = [] parties.append( target.Party(**{ 'uuid': 'ff47836c-877c-4007-bbf3-c9d9bd805000', 'party-name': 'TEST1', 'type': 'organization' }) ) parties.append( target.Party(**{ 'uuid': 'ee88836c-877c-4007-bbf3-c9d9bd805000', 'party-name': 'TEST2', 'type': 'organization' }) ) sub_element_path = ElementPath('metadata.parties.*') ac = UpdateAction(parties, element, sub_element_path) ac.execute() assert element.get_at(sub_element_path) == parties return element
def prepare_sub_model_split_actions( cls, sub_model_item: OscalBaseModel, sub_model_dir: pathlib.Path, file_prefix: str, content_type: FileContentType) -> List[Action]: """Create split actions of sub model.""" actions: List[Action] = [] file_name = cmd_utils.to_model_file_name(sub_model_item, file_prefix, content_type) model_type = utils.classname_to_alias( type(sub_model_item).__name__, 'json') sub_model_file = sub_model_dir / file_name actions.append(CreatePathAction(sub_model_file)) actions.append( WriteFileAction(sub_model_file, Element(sub_model_item, model_type), content_type)) return actions
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
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')
def check_split_files(): assert component_def_dir.joinpath( 'component-definition/metadata.yaml').exists() assert component_def_dir.joinpath('component-definition.yaml').exists() assert component_def_dir.joinpath( 'component-definition/components').exists() assert component_def_dir.joinpath( 'component-definition/components').is_dir() # Confirm that the list items are written with the expected numbered names components: list = Element(sample_nist_component_def).get_at( ElementPath('component-definition.components.*')) for index in range(len(components)): comp_fname = f'{str(index).zfill(const.FILE_DIGIT_PREFIX_LENGTH)}{const.IDX_SEP}defined-component.yaml' component_file = component_def_dir / 'component-definition' / 'components' / comp_fname assert component_file.exists() assert trash.to_trash_file_path(component_def_file).exists()