Пример #1
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 = 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
Пример #2
0
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)
Пример #3
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
Пример #4
0
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)
Пример #5
0
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
Пример #6
0
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
Пример #7
0
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()
Пример #8
0
    def add_from_args(self, args: argparse.Namespace) -> int:
        """Parse args for add element to file."""
        file_path = pathlib.Path(args.file).resolve()

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

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

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

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

        add_plan.execute()
        return CmdReturnCodes.SUCCESS.value
Пример #9
0
    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
Пример #10
0
    def write(self, model: OscalBaseModel) -> bool:
        """Write OSCAL model to repository."""
        logger.debug(f'Writing model {self._model_name}.')
        model_alias = classname_to_alias(model.__class__.__name__, AliasMode.JSON)
        if parser.to_full_model_name(model_alias) is None:
            raise TrestleError(f'Given model {model_alias} is not a top level model.')

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

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

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

        import_plan.execute()

        logger.debug(f'Model {self._model_name} written to repository')
        return True
Пример #11
0
    def assemble_model(cls, model_alias: str, args: argparse.Namespace) -> int:
        """Assemble a top level OSCAL model within the trestle dist directory."""
        log.set_log_level_from_args(args)
        logger.info(f'Assembling models of type {model_alias}.')

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

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

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

            model_file_type = file_utils.get_contextual_file_type(
                root_model_dir / model_name)

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

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

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

            assembled_model_dir = trestle_root / const.TRESTLE_DIST_DIR / plural_alias

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

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

            plan.execute()

        return CmdReturnCodes.SUCCESS.value
Пример #12
0
    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)
Пример #13
0
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
Пример #14
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()
Пример #15
0
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))
Пример #16
0
    def assemble_model(cls, model_alias: str, object_type: Type[TLO],
                       args: argparse.Namespace) -> int:
        """Assemble a top level OSCAL model within the trestle dist directory."""
        log.set_log_level_from_args(args)
        trestle_root = fs.get_trestle_project_root(Path.cwd())
        if not trestle_root:
            logger.error(
                f'Current working directory {Path.cwd()} is not with a trestle project.'
            )
            return 1
        if not trestle_root == Path.cwd():
            logger.error(
                f'Current working directory {Path.cwd()} is not the top level trestle project directory.'
            )
            return 1

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

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

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

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

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

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

        try:
            plan.simulate()
            plan.execute()
            return 0
        except Exception as e:
            logger.error(
                'Unknown error executing trestle create operations. Rolling back.'
            )
            logger.debug(e)
            return 1
Пример #17
0
    def import_model(self, model: OscalBaseModel, name: str, content_type='json') -> ManagedOSCAL:
        """Import OSCAL object into trestle repository."""
        logger.debug(f'Importing model {name} of type {model.__class__.__name__}.')
        model_alias = classname_to_alias(model.__class__.__name__, AliasMode.JSON)
        if parser.to_full_model_name(model_alias) is None:
            raise TrestleError(f'Given model {model_alias} is not a top level model.')

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

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

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

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

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

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

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

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

            # raise trestle error
            raise TrestleError(errmsg)

        # all well; model was imported and validated successfully
        logger.debug(f'Model {name} of type {model.__class__.__name__} imported successfully.')
        return ManagedOSCAL(self._root_dir, model.__class__, name)
Пример #18
0
    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
Пример #19
0
    def create_object(cls, model_alias: str, object_type: Type[TLO],
                      args: argparse.Namespace) -> int:
        """Create a top level OSCAL object within the trestle directory, leveraging functionality in add."""
        log.set_log_level_from_args(args)
        trestle_root = fs.get_trestle_project_root(Path.cwd())
        if not trestle_root:
            logger.error(
                f'Current working directory {Path.cwd()} is not with a trestle project.'
            )
            return 1
        plural_path: str
        # Cater to POAM
        if model_alias[-1] == 's':
            plural_path = model_alias
        else:
            plural_path = model_alias + 's'

        desired_model_dir = trestle_root / plural_path / args.name

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

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

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

        top_element = Element(sample_model, model_alias)

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

        # create a plan to write the directory and file.
        try:
            create_plan = Plan()
            create_plan.add_action(create_action)
            create_plan.add_action(write_action)
            create_plan.simulate()
            create_plan.execute()
            return 0
        except Exception as e:
            logger.error(
                'Unknown error executing trestle create operations. Rolling back.'
            )
            logger.debug(e)
            return 1
Пример #20
0
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
Пример #21
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
Пример #22
0
    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
Пример #24
0
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)
Пример #25
0
    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()
Пример #26
0
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
Пример #27
0
 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
Пример #28
0
    def _run(self, args: argparse.Namespace) -> int:
        """Add an OSCAL component/subcomponent to the specified component.

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

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

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

            add_plan = Plan()

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

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

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

            add_plan.simulate()
            add_plan.execute()

        except BaseException as err:
            logger.error(f'Add failed: {err}')
            return 1
        return 0
Пример #29
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')
Пример #30
0
    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()