def test_add(tmp_path: pathlib.Path, keep_cwd: pathlib.Path) -> None: """Test Add.add() method used by trestle CreateCmd.""" file_path = pathlib.Path( test_utils.JSON_TEST_DATA_PATH) / 'minimal_catalog_missing_roles.json' minimal_catalog_missing_roles = Catalog.oscal_read(file_path) # 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 _, _ = test_utils.prepare_trestle_project_dir( tmp_path, content_type, minimal_catalog_missing_roles, test_utils.CATALOGS_DIR) # Execute first _add element_path = ElementPath('catalog.metadata.roles') catalog_element = Element(minimal_catalog_missing_roles) expected_update_action_1 = UpdateAction( expected_catalog_roles1.get_at(element_path), catalog_element, element_path) actual_update_action, actual_catalog_roles = Add.add( element_path, catalog_element, False) 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 = Add.add( element_path, catalog_element, False) 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 = Add.add( element_path, catalog_element, False) assert actual_catalog_roles2_rp == expected_catalog_roles2_rp assert actual_update_action3 == expected_update_action_3
def test_element_get_at(sample_nist_component_def: component.ComponentDefinition): """Test element get method.""" element = Element(sample_nist_component_def) # field alias should succeed assert element.get_at( ElementPath('component-definition.metadata.last-modified') ) == sample_nist_component_def.metadata.last_modified # field name should fail assert element.get_at(ElementPath('component-definition.metadata.last_modified')) is None assert element.get() == sample_nist_component_def assert element.get_at() == element.get() assert element.get_at(ElementPath('component-definition.metadata')) == sample_nist_component_def.metadata assert element.get_at( ElementPath('component-definition.metadata.title') ) == sample_nist_component_def.metadata.title assert element.get_at(ElementPath('component-definition.components')) == sample_nist_component_def.components assert element.get_at(ElementPath('component-definition.components.*')) == sample_nist_component_def.components assert element.get_at( ElementPath('component-definition.metadata.parties.*') ) == sample_nist_component_def.metadata.parties assert element.get_at(ElementPath('component-definition.metadata.parties.0') ) == sample_nist_component_def.metadata.parties[0] assert element.get_at(ElementPath('component-definition.metadata.parties.0.uuid') ) == sample_nist_component_def.metadata.parties[0].uuid for index in range(len(sample_nist_component_def.components)): path_str = f'component-definition.components.{index}' assert element.get_at(ElementPath(path_str)) == sample_nist_component_def.components[index] # invalid indexing assert element.get_at(ElementPath('component-definition.metadata.title.0')) is None # invalid path with missing root model assert element.get_at(ElementPath('metadata.title')) is None # element_path with parent path parent_path = ElementPath('component-definition.metadata') element_path = ElementPath('metadata.parties.*', parent_path) assert element.get_at(element_path) == sample_nist_component_def.metadata.parties # element_path with parent path parent_path = ElementPath('component-definition.components.*') element_path = ElementPath('component.control-implementations.*', parent_path) component_list = element.get_at(parent_path) for component_item in component_list: component_element = Element(component_item) assert component_element.get_at(element_path) == component_item.control_implementations # element_path in a list with parent path parent_path = ElementPath('component-definition.components.*') element_path = ElementPath('component.control-implementations.0', parent_path) component_list = element.get_at(parent_path) for component_item in component_list: component_element = Element(component_item) assert component_element.get_at(element_path) == component_item.control_implementations[0]
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_element_get_at(sample_target_def: target.TargetDefinition): """Test element get method.""" element = Element(sample_target_def) # field alias should succeed assert element.get_at( ElementPath('target-definition.metadata.last-modified') ) == sample_target_def.metadata.last_modified # field name should fail assert element.get_at(ElementPath('target-definition.metadata.last_modified')) is None assert element.get() == sample_target_def assert element.get_at() == element.get() assert element.get_at(ElementPath('target-definition.metadata')) == sample_target_def.metadata assert element.get_at(ElementPath('target-definition.metadata.title')) == sample_target_def.metadata.title assert element.get_at(ElementPath('target-definition.targets')) == sample_target_def.targets assert element.get_at(ElementPath('target-definition.targets.*')) == sample_target_def.targets assert element.get_at(ElementPath('target-definition.metadata.parties.*')) == sample_target_def.metadata.parties assert element.get_at(ElementPath('target-definition.metadata.parties.0')) == sample_target_def.metadata.parties[0] assert element.get_at(ElementPath('target-definition.metadata.parties.0.uuid') ) == sample_target_def.metadata.parties[0].uuid for uuid in sample_target_def.targets: path_str = f'target-definition.targets.{uuid}' assert element.get_at(ElementPath(path_str)) == sample_target_def.targets[uuid] # invalid indexing assert element.get_at(ElementPath('target-definition.metadata.title.0')) is None # invalid path with missing root model assert element.get_at(ElementPath('metadata.title')) is None # element_path with parent path parent_path = ElementPath('target-definition.metadata') element_path = ElementPath('metadta.parties.*', parent_path) assert element.get_at(element_path) == sample_target_def.metadata.parties # element_path with parent path parent_path = ElementPath('target-definition.targets.*') element_path = ElementPath('target.target-control-implementations.*', parent_path) targets = element.get_at(parent_path) for key in targets: target = targets[key] target_element = Element(target) assert target_element.get_at(element_path) == target.target_control_implementations # element_path in a list with parent path parent_path = ElementPath('target-definition.targets.*') element_path = ElementPath('target.target-control-implementations.0', parent_path) targets = element.get_at(parent_path) for key in targets: target = targets[key] target_element = Element(target) assert target_element.get_at(element_path) == target.target_control_implementations[0]
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 = 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 = 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 = 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) AddCmd.add(catalog_def_dir / catalog_def_file, element_path, Catalog, catalog_element) actual_catalog_roles = Catalog.oscal_read(catalog_def_dir / catalog_def_file) assert actual_catalog_roles == expected_catalog_roles1 # Execute second _add - this time roles already exists, so this adds a roles object to roles array catalog_element = Element(actual_catalog_roles) AddCmd.add(catalog_def_dir / catalog_def_file, element_path, Catalog, catalog_element) actual_catalog_roles2 = Catalog.oscal_read(catalog_def_dir / catalog_def_file) assert actual_catalog_roles2 == expected_catalog_roles2 # Execute _add for responsible-parties to the same catalog element_path = ElementPath('catalog.metadata.responsible-parties') catalog_element = Element(actual_catalog_roles2) AddCmd.add(catalog_def_dir / catalog_def_file, element_path, Catalog, catalog_element) actual_catalog_roles2_rp = Catalog.oscal_read(catalog_def_dir / catalog_def_file) assert actual_catalog_roles2_rp == expected_catalog_roles2_rp
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 test_update_action(sample_target): """Test update action.""" element = Element(sample_target) metadata = target.Metadata( **{ 'title': 'My simple catalog', 'last-modified': datetime.now().astimezone(), 'version': '0.0.0', 'oscal-version': '1.0.0-Milestone3' }) sub_element_path = ElementPath('metadata') prev_metadata = element.get_at(sub_element_path) uac = UpdateAction(metadata, element, sub_element_path) uac.execute() assert element.get_at(sub_element_path) is not prev_metadata assert element.get_at(sub_element_path) == metadata uac.rollback() assert element.get_at(sub_element_path) == prev_metadata assert element.get_at(sub_element_path) is not metadata
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 test_update_list_sub_element_action(sample_target): """Test setting a list.""" 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.*') uac = UpdateAction(parties, element, sub_element_path) uac.execute() assert element.get_at(sub_element_path) == parties
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 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 test_update_action(sample_nist_component_def): """Test update action.""" element = Element(sample_nist_component_def) metadata = common.Metadata( **{ 'title': 'My simple catalog', 'last-modified': datetime.now().astimezone(), 'version': '0.0.0', 'oscal-version': OSCAL_VERSION }) sub_element_path = ElementPath('component-definition.metadata') prev_metadata = element.get_at(sub_element_path) uac = UpdateAction(metadata, element, sub_element_path) uac.execute() assert element.get_at(sub_element_path) is not prev_metadata assert element.get_at(sub_element_path) == metadata uac.rollback() assert element.get_at(sub_element_path) == prev_metadata assert element.get_at(sub_element_path) is not metadata
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_write_file_action_yaml(tmp_yaml_file: pathlib.Path, sample_nist_component_def): """Test write yaml action.""" element = Element(sample_nist_component_def, 'component-definition') try: # string path should error wa = WriteFileAction(tmp_yaml_file.as_posix(), element, FileContentType.YAML) except TrestleError: pass try: # invalid file extension should error wa = WriteFileAction(tmp_yaml_file, element, FileContentType.JSON) except TrestleError: pass try: # writing to a non-existing file will error wa = WriteFileAction(tmp_yaml_file, element, FileContentType.YAML) wa.execute() except TrestleError: pass tmp_yaml_file.touch() wa = WriteFileAction(tmp_yaml_file, element, FileContentType.YAML) wa.execute() test_utils.verify_file_content(tmp_yaml_file, element.get())
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 prepare_element(sample_target_def): """Prepare a target element for remove tests.""" element = Element(sample_target_def) 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('target-definition.metadata.parties.*') ac = UpdateAction(parties, element, sub_element_path) ac.execute() assert element.get_at(sub_element_path) == parties return element
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_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_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 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 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 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_action_json(tmp_json_file, sample_target_def): """Test write json action.""" element = Element(sample_target_def, 'target-definition') with open(tmp_json_file, 'w+') as writer: wa = WriteAction(writer, element, FileContentType.JSON) wa.execute() test_utils.verify_file_content(tmp_json_file, element.get()) os.remove(tmp_json_file)
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 test_write_action_yaml(tmp_yaml_file, sample_target): """Test write yaml action.""" element = Element(sample_target) with open(tmp_yaml_file, 'w+') as writer: wa = WriteAction(writer, element, FileContentType.YAML) wa.execute() test_utils.verify_file_content(tmp_yaml_file, element.get()) os.remove(tmp_yaml_file)
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_split_model_plans( tmp_path: pathlib.Path, sample_nist_component_def: component.ComponentDefinition) -> None: """Test for split_model method.""" # Assume we are running a command like below # trestle split -f component-definition.yaml -e component-definition.metadata content_type = FileContentType.YAML # prepare trestle project dir with the file component_def_dir, component_def_file = test_utils.prepare_trestle_project_dir( tmp_path, content_type, sample_nist_component_def, test_utils.COMPONENT_DEF_DIR) # read the model from file component_def = component.ComponentDefinition.oscal_read( component_def_file) element = Element(component_def) element_args = ['component-definition.metadata'] element_paths = cmd_utils.parse_element_args(None, element_args) # extract values metadata_file = component_def_dir / element_paths[0].to_file_path( content_type) metadata = element.get_at(element_paths[0]) root_file = component_def_dir / element_paths[0].to_root_path(content_type) remaining_root = element.get().stripped_instance( element_paths[0].get_element_name()) # prepare the plan expected_plan = Plan() expected_plan.add_action(CreatePathAction(metadata_file)) expected_plan.add_action( WriteFileAction(metadata_file, Element(metadata), content_type)) 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(component_def, element_paths, component_def_dir, content_type, '', None) assert expected_plan == split_plan
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)