Exemple #1
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
Exemple #2
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
Exemple #3
0
    def add(cls, file_path, element_path, parent_model, parent_element):
        """For a file_path and element_path, add a child model to the parent_element of a given parent_model.

        First we find the child model at the specified element path and instantiate it with default values.
        Then we check if there's already existing element at that path, in which case we append the child model
        to the existing list of dict.
        Then we set up an action plan to update the model (specified by file_path) in memory, create a file
        at the same location and write the file.
        """
        element_path_list = element_path.get_full_path_parts()
        if '*' in element_path_list:
            raise err.TrestleError(
                'trestle add does not support Wildcard element path.')
        # Get child model
        try:
            child_model = utils.get_target_model(element_path_list,
                                                 parent_model)
            # Create child element with sample values
            child_object = utils.get_sample_model(child_model)

            if parent_element.get_at(element_path) is not None:
                # The element already exists
                if type(parent_element.get_at(element_path)) is list:
                    child_object = parent_element.get_at(
                        element_path) + child_object
                elif type(parent_element.get_at(element_path)) is dict:
                    child_object = {
                        **parent_element.get_at(element_path),
                        **child_object
                    }
                else:
                    raise err.TrestleError(
                        'Already exists and is not a list or dictionary.')

        except Exception as e:
            raise err.TrestleError(f'Bad element path. {str(e)}')

        update_action = UpdateAction(sub_element=child_object,
                                     dest_element=parent_element,
                                     sub_element_path=element_path)
        create_action = CreatePathAction(file_path.absolute(), True)
        write_action = WriteFileAction(
            file_path.absolute(), parent_element,
            FileContentType.to_content_type(file_path.suffix))

        add_plan = Plan()
        add_plan.add_action(update_action)
        add_plan.add_action(create_action)
        add_plan.add_action(write_action)
        add_plan.simulate()

        add_plan.execute()
Exemple #4
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
def test_remove(tmp_path, sample_catalog_minimal):
    """Test RemoveCmd.remove() method for trestle remove: removing Roles and Responsible-Parties."""
    # 1. Remove responsible-parties
    # 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))

    # minimal catalog with responsible-parties (dict) removed
    file_path = pathlib.Path.joinpath(
        test_utils.JSON_TEST_DATA_PATH,
        'minimal_catalog_no_responsible-parties.json')
    expected_catalog_responsible_parties_removed = Element(
        Catalog.oscal_read(file_path))

    # Target path for removal:
    element_path = ElementPath('catalog.metadata.responsible-parties')
    expected_remove_action = RemoveAction(catalog_with_responsible_parties,
                                          element_path)

    # Call remove() method
    actual_remove_action, actual_catalog_removed_responsible_parties = RemoveCmd.remove(
        element_path, Catalog, catalog_with_responsible_parties)

    # 1.1 Assertion about action
    assert expected_remove_action == actual_remove_action

    add_plan = Plan()
    add_plan.add_action(actual_remove_action)
    add_plan.simulate()
    add_plan.execute()

    # 1.2 Assertion about resulting element after removal
    assert expected_catalog_responsible_parties_removed == actual_catalog_removed_responsible_parties

    # 2. Remove roles
    # 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_without_roles = Element(Catalog.oscal_read(file_path))

    # minimal catalog with Roles
    file_path = pathlib.Path.joinpath(test_utils.JSON_TEST_DATA_PATH,
                                      'minimal_catalog_roles.json')
    catalog_with_roles = Element(Catalog.oscal_read(file_path))

    # Target path for removal:
    element_path = ElementPath('catalog.metadata.roles')
    expected_remove_action = RemoveAction(catalog_with_roles, element_path)

    # Call remove() method
    actual_remove_action, actual_catalog_removed_roles = RemoveCmd.remove(
        element_path, Catalog, catalog_with_roles)

    # 2.1 Assertion about action
    assert expected_remove_action == actual_remove_action

    add_plan = Plan()
    add_plan.add_action(actual_remove_action)
    add_plan.simulate()
    add_plan.execute()

    # 2.2 Assertion about resulting element after removal
    assert catalog_without_roles == actual_catalog_removed_roles
    def _run(self, args: argparse.Namespace) -> int:
        """Top level import run command."""
        log.set_log_level_from_args(args)

        logger.debug('Entering import run.')

        # 1. Validate input arguments are as expected.
        # This code block may never be reached as the argument is declared to be required.

        # 1.1 Check that input file given exists.
        input_file = pathlib.Path(args.file)
        if not input_file.exists():
            logger.error(f'Input file {args.file} does not exist.')
            return 1

        # 1.2 Bad working directory if not running from current working directory
        cwd = pathlib.Path.cwd().resolve()
        trestle_root = fs.get_trestle_project_root(cwd)
        if trestle_root is None:
            logger.error(
                f'Current working directory: {cwd} is not within a trestle project.'
            )
            return 1

        # 2. Importing a file that is already inside a trestle-initialized dir is bad
        trestle_root = trestle_root.resolve()
        try:
            input_file.absolute().relative_to(trestle_root)
        except ValueError:
            # An exception here is good: it means that the input file is not inside a trestle dir.
            pass
        else:
            logger.error(
                'Input file cannot be from current trestle project. Use duplicate instead.'
            )
            return 1

        # 3. Work out typing information from input suffix.
        try:
            content_type = FileContentType.to_content_type(input_file.suffix)
        except TrestleError as err:
            logger.debug(f'FileContentType.to_content_type() failed: {err}')
            logger.error(
                f'Import failed, could not work out content type from file suffix: {err}'
            )
            return 1

        # 4. Load input and parse for model

        # 4.1 Load from file
        try:
            data = fs.load_file(input_file.absolute())
        except JSONDecodeError as err:
            logger.debug(f'fs.load_file() failed: {err}')
            logger.error(f'Import failed, JSON error loading file: {err}')
            return 1
        except TrestleError as err:
            logger.debug(f'fs.load_file() failed: {err}')
            logger.error(f'Import failed, error loading file: {err}')
            return 1
        except PermissionError as err:
            logger.debug(f'fs.load_file() failed: {err}')
            logger.error(
                f'Import failed, access permission error loading file: {err}')
            return 1

        # 4.2 root key check
        try:
            parent_alias = parser.root_key(data)
        except TrestleError as err:
            logger.debug(f'parser.root_key() failed: {err}')
            logger.error(
                f'Import failed, failed to parse input file for root key: {err}'
            )
            return 1

        # 4.3 parse the model
        parent_model_name = parser.to_full_model_name(parent_alias)
        try:
            parent_model = parser.parse_file(input_file.absolute(),
                                             parent_model_name)
        except TrestleError as err:
            logger.debug(f'parser.parse_file() failed: {err}')
            logger.error(
                f'Import failed, failed to parse valid contents of input file: {err}'
            )
            return 1

        # 5. Work out output directory and file
        plural_path: str
        plural_path = parent_alias
        # Cater to POAM
        if parent_alias[-1] != 's':
            plural_path = parent_alias + 's'

        desired_model_dir = trestle_root / plural_path
        # args.output is presumed to be assured as it is declared to be required
        if args.output:
            desired_model_path = desired_model_dir / args.output / (
                parent_alias + input_file.suffix)

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

        # 6. Prepare actions and plan
        top_element = Element(parent_model.oscal_read(input_file))
        create_action = CreatePathAction(desired_model_path.absolute(), True)
        write_action = WriteFileAction(desired_model_path.absolute(),
                                       top_element, content_type)

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

        try:
            import_plan.simulate()
        except TrestleError as err:
            logger.debug(f'import_plan.simulate() failed: {err}')
            logger.error(
                f'Import failed, error in testing import operation: {err}')
            return 1

        try:
            import_plan.execute()
        except TrestleError as err:
            logger.debug(f'import_plan.execute() failed: {err}')
            logger.error(
                f'Import failed, error in actual import operation: {err}')
            return 1

        # 7. Leave the rest to trestle split
        return 0
Exemple #7
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.
        """
        log.set_log_level_from_args(args)
        args_dict = args.__dict__

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

        # Get parent model and then load json into parent model
        try:
            parent_model, parent_alias = fs.get_contextual_model_type(file_path.absolute())
        except Exception as err:
            logger.debug(f'fs.get_contextual_model_type() failed: {err}')
            logger.error(f'Remove failed (fs.get_contextual_model_type()): {err}')
            return 1

        try:
            parent_object = parent_model.oscal_read(file_path.absolute())
        except Exception as err:
            logger.debug(f'parent_model.oscal_read() failed: {err}')
            logger.error(f'Remove failed (parent_model.oscal_read()): {err}')
            return 1

        parent_element = Element(parent_object, utils.classname_to_alias(parent_model.__name__, 'json'))

        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)
            try:
                remove_action, parent_element = self.remove(element_path, parent_model, parent_element)
            except TrestleError as err:
                logger.debug(f'self.remove() failed: {err}')
                logger.error(f'Remove failed (self.remove()): {err}')
                return 1
            add_plan.add_action(remove_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(remove_action)
        add_plan.add_action(create_action)
        add_plan.add_action(write_action)

        try:
            add_plan.simulate()
        except TrestleError as err:
            logger.debug(f'Remove failed at simulate(): {err}')
            logger.error(f'Remove failed (simulate()): {err}')
            return 1

        try:
            add_plan.execute()
        except TrestleError as err:
            logger.debug(f'Remove failed at execute(): {err}')
            logger.error(f'Remove failed (execute()): {err}')
            return 1

        return 0