Example #1
0
    def template_validate(self) -> int:
        """Validate the integrity of the template files."""
        logger.info('Checking template file integrity')
        for template_file in self.template_dir.iterdir():
            if (template_file.name
                    not in author_const.REFERENCE_TEMPLATES.values()
                    and template_file.name.lower() != 'readme.md'):
                raise TrestleError(
                    f'Unexpected template file {self.rel_dir(template_file)}')

            if template_file.suffix == '.md':
                try:
                    md_api = MarkdownAPI()
                    md_api.load_validator_with_template(
                        template_file, True, False)
                except Exception as ex:
                    raise TrestleError(
                        f'Template for task {self.task_name} failed to validate due to {ex}'
                    )

            elif template_file.suffix == '.drawio':
                try:
                    _ = DrawIOMetadataValidator(template_file)
                except Exception as ex:
                    raise TrestleError(
                        f'Template for task {self.task_name} failed to validate due to {ex}'
                    )

        logger.info('Templates validated')
        return CmdReturnCodes.SUCCESS.value
Example #2
0
def parse_element_arg(
    model_obj: Union[OscalBaseModel, None],
    element_arg: str,
    relative_path: Optional[pathlib.Path] = None
) -> List[ElementPath]:
    """Parse an element arg string into a list of ElementPath.

    Args:
        model_obj: The OscalBaseModel being inspected to determine available elements that can be split
        element_arg: Single element path, as a string.
        relative_path: Optional relative path (from trestle root) used to validate element args are valid.
    Returns:
        The requested parsed list of ElementPath for use in split
    """
    element_arg = element_arg.strip()

    if element_arg == '*':
        raise TrestleError('Invalid element path containing only a single wildcard.')

    if element_arg == '':
        raise TrestleError('Invalid element path is empty string.')

    # search for wildcards and create paths with its parent path
    path_parts = element_arg.split(ElementPath.PATH_SEPARATOR)
    if len(path_parts) <= 1:
        raise TrestleError(f'Invalid element path "{element_arg}" with only one element and no wildcard')

    element_paths = parse_chain(model_obj, path_parts, relative_path)

    if len(element_paths) <= 0:
        # don't complain if nothing to split
        pass

    return element_paths
Example #3
0
    def validate(self, validate_header: bool, validate_only_header: bool,
                 governed_heading: str, readme_validate: bool,
                 template_version: str, ignore: str) -> int:
        """Validate task."""
        if not self.task_path.is_dir():
            raise TrestleError(
                f'Task directory {self.task_path} does not exist. Exiting validate.'
            )

        for task_instance in self.task_path.iterdir():
            if task_instance.is_dir():
                if file_utils.is_symlink(task_instance):
                    continue
                result = self._measure_template_folder(
                    task_instance, validate_header, validate_only_header,
                    governed_heading, readme_validate, template_version,
                    ignore)
                if not result:
                    raise TrestleError(
                        'Governed-folder validation failed for task' +
                        f'{self.task_name} on directory {self.rel_dir(task_instance)}'
                    )
            else:
                logger.warning(
                    f'Unexpected file {self.rel_dir(task_instance)} identified in {self.task_name}'
                    + ' directory, ignoring.')
        return CmdReturnCodes.SUCCESS.value
Example #4
0
    def setup_template(self, template_version: str) -> int:
        """Create structure to allow markdown template enforcement."""
        if not self.task_path.exists():
            self.task_path.mkdir(exist_ok=True, parents=True)
        elif self.task_path.is_file():
            raise TrestleError(
                f'Task path: {self.rel_dir(self.task_path)} is a file not a directory.'
            )
        if not self.template_dir.exists():
            self.template_dir.mkdir(exist_ok=True, parents=True)
        elif self.template_dir.is_file():
            raise TrestleError(
                f'Template path: {self.rel_dir(self.template_dir)} is a file not a directory.'
            )

        template_file_a_md = self.template_dir / 'a_template.md'
        template_file_another_md = self.template_dir / 'another_template.md'
        template_file_drawio = self.template_dir / 'architecture.drawio'
        TemplateVersioning.write_versioned_template('template.md',
                                                    self.template_dir,
                                                    template_file_a_md,
                                                    template_version)
        TemplateVersioning.write_versioned_template('template.md',
                                                    self.template_dir,
                                                    template_file_another_md,
                                                    template_version)
        TemplateVersioning.write_versioned_template('template.drawio',
                                                    self.template_dir,
                                                    template_file_drawio,
                                                    template_version)

        return CmdReturnCodes.SUCCESS.value
Example #5
0
    def load_validator_with_template(
            self,
            md_template_path: pathlib.Path,
            validate_yaml_header: bool,
            validate_md_body: bool,
            md_header_to_validate: Optional[str] = None) -> None:
        """Load and initialize markdown validator."""
        try:
            self.processor.governed_header = md_header_to_validate
            template_header, template_tree = self.processor.process_markdown(
                md_template_path)

            if len(template_header) == 0 and validate_yaml_header:
                raise TrestleError(
                    f'Expected yaml header for markdown template where none exists {md_template_path}'
                )

            self.validator = MarkdownValidator(md_template_path,
                                               template_header, template_tree,
                                               validate_yaml_header,
                                               validate_md_body,
                                               md_header_to_validate)
        except TrestleError as e:
            raise TrestleError(
                f'Error while loading markdown template {md_template_path}: {e}.'
            )
Example #6
0
    def delete_model(self, model_type: Type[OscalBaseModel], name: str) -> bool:
        """Delete an OSCAL model from repository."""
        logger.debug(f'Deleting model {name} of type {model_type.__name__}.')
        model_alias = classname_to_alias(model_type.__name__, AliasMode.JSON)
        if parser.to_full_model_name(model_alias) is None:
            raise TrestleError(f'Given model {model_alias} is not a top level model.')
        plural_path = ModelUtils.model_type_to_model_dir(model_alias)
        desired_model_dir = self._root_dir / plural_path / name

        if not desired_model_dir.exists() or not desired_model_dir.is_dir():
            raise TrestleError(f'Model {name} does not exist.')
        shutil.rmtree(desired_model_dir)

        # remove model from dist directory if it exists
        dist_model_dir = self._root_dir / const.TRESTLE_DIST_DIR / plural_path
        file_content_type = FileContentType.path_to_content_type(dist_model_dir / name)
        if file_content_type != FileContentType.UNKNOWN:
            file_path = pathlib.Path(
                dist_model_dir, name + FileContentType.path_to_file_extension(dist_model_dir / name)
            )
            logger.debug(f'Deleting model {name} from dist directory.')
            os.remove(file_path)

        logger.debug(f'Model {name} deleted successfully.')
        return True
Example #7
0
    def setup_template_governed_docs(self, template_version: str) -> int:
        """Create structure to allow markdown template enforcement.

        Returns:
            Unix return code.
        """
        if not self.task_path.exists():
            self.task_path.mkdir(exist_ok=True, parents=True)
        elif self.task_path.is_file():
            raise TrestleError(
                f'Task path: {self.rel_dir(self.task_path)} is a file not a directory.'
            )
        if not self.template_dir.exists():
            self.template_dir.mkdir(exist_ok=True, parents=True)
        elif self.template_dir.is_file():
            raise TrestleError(
                f'Template path: {self.rel_dir(self.template_dir)} is a file not a directory.'
            )
        logger.debug(self.template_dir)
        if not self._validate_template_dir():
            raise TrestleError('Aborting setup')
        template_file = self.template_dir / self.template_name
        if template_file.is_file():
            return CmdReturnCodes.SUCCESS.value
        TemplateVersioning.write_versioned_template('template.md',
                                                    self.template_dir,
                                                    template_file,
                                                    template_version)
        logger.info(
            f'Template file setup for task {self.task_name} at {self.rel_dir(template_file)}'
        )
        logger.info(f'Task directory is {self.rel_dir(self.task_path)}')
        return CmdReturnCodes.SUCCESS.value
Example #8
0
def relative_resolve(candidate: pathlib.Path, cwd: pathlib.Path) -> pathlib.Path:
    """Resolve a candidate file path relative to a provided cwd.

    This is to circumvent bad behaviour for resolve on windows platforms where the path must exist.

    If a relative dir is passed it presumes the directory is relative to the PROVIDED cwd.
    If relative expansions exist (e.g. ../) the final result must still be within the cwd.

    If an absolute path is provided it tests whether the path is within the cwd or not.

    """
    # Expand user first if applicable.
    candidate = candidate.expanduser()

    if not cwd.is_absolute():
        raise TrestleError('Error handling current working directory. CWD is expected to be absolute.')

    if not candidate.is_absolute():
        new = pathlib.Path(cwd / candidate).resolve()
    else:
        new = candidate.resolve()
    try:
        new.relative_to(cwd)
    except ValueError:
        raise TrestleError(f'Provided dir {candidate} is not relative to {cwd}')
    return new
Example #9
0
def parse_dict(data: Dict[str, Any], model_name: str) -> OscalBaseModel:
    """Load a model from the data dict.

    This functionality is provided for situations when the OSCAL data type is not known ahead of time. Here the model
    has been loaded into memory using json loads or similar and passed as a dict.

    Args:
        data: Oscal data loaded into memory as a dictionary with the `root key` removed.
        model_name: should be of the form 'module.class' from trestle.oscal.* modules

    Returns:
        The oscal model of the desired model.
    """
    if data is None:
        raise TrestleError('data name is required')

    if model_name is None:
        raise TrestleError('model_name is required')

    parts = model_name.split('.')
    class_name = parts.pop()
    module_name = '.'.join(parts)

    logger.debug(f'Loading class "{class_name}" from "{module_name}"')
    module = importlib.import_module(module_name)
    mclass: OscalBaseModel = getattr(module, class_name)
    if mclass is None:
        raise TrestleError(
            f'class "{class_name}" could not be found in "{module_name}"')

    instance = mclass.parse_obj(data)
    return instance
Example #10
0
 def _load(self) -> None:
     """Load the file."""
     if not self.file_path.exists() or self.file_path.is_dir():
         raise TrestleError(
             f'Candidate drawio file {str(self.file_path)} does not exist or is a directory'
         )
     try:
         self.raw_xml = defusedxml.ElementTree.parse(self.file_path,
                                                     forbid_dtd=True)
     except Exception as e:
         raise TrestleError(
             f'Exception loading Element tree from file: {e}')
     self.mx_file = self.raw_xml.getroot()
     if not self.mx_file.tag == 'mxfile':
         raise TrestleError('DrawIO file is not a draw io file (mxfile)')
     self.diagrams = []
     for diagram in list(self.mx_file):
         # Determine if compressed or not
         # Assumption 1 mxGraphModel
         n_children = len(list(diagram))
         if n_children == 0:
             # Compressed object
             self.diagrams.append(self._uncompress(diagram.text))
         elif n_children == 1:
             self.diagrams.append(list(diagram)[0])
         else:
             raise TrestleError('Unhandled behaviour in drawio read.')
Example #11
0
    def __init__(self, root_dir: pathlib.Path, model_type: Type[OscalBaseModel], name: str) -> None:
        """Initialize repository OSCAL model object."""
        if not file_utils.is_valid_project_root(root_dir):
            raise TrestleError(f'Provided root directory {str(root_dir)} is not a valid Trestle root directory.')
        self._root_dir = root_dir
        self._model_type = model_type
        self._model_name = name

        # set model alais and dir
        self.model_alias = classname_to_alias(self._model_type.__name__, AliasMode.JSON)
        if parser.to_full_model_name(self.model_alias) is None:
            raise TrestleError(f'Given model {self.model_alias} is not a top level model.')

        plural_path = ModelUtils.model_type_to_model_dir(self.model_alias)
        self.model_dir = self._root_dir / plural_path / self._model_name

        if not self.model_dir.exists() or not self.model_dir.is_dir():
            raise TrestleError(f'Model dir {self._model_name} does not exist.')

        file_content_type = FileContentType.path_to_content_type(self.model_dir / self.model_alias)
        if file_content_type == FileContentType.UNKNOWN:
            raise TrestleError(f'Model file for model {self._model_name} does not exist.')
        self.file_content_type = file_content_type

        filepath = pathlib.Path(
            self.model_dir,
            self.model_alias + FileContentType.path_to_file_extension(self.model_dir / self.model_alias)
        )

        self.filepath = filepath
    def __init__(
        self,
        tmp_path: pathlib.Path,
        template_header: Dict,
        template_tree: MarkdownNode,
        validate_yaml_header: bool,
        validate_md_body: bool,
        md_header_to_validate: Optional[str] = None
    ):
        """Initialize markdown validator."""
        self._validate_yaml_header = validate_yaml_header
        self._validate_md_body = validate_md_body
        self.md_header_to_validate = md_header_to_validate.strip(' ') if md_header_to_validate is not None else None
        self.template_header = template_header
        self.template_tree = template_tree
        self.template_path = tmp_path
        self.template_version = self.extract_template_version(self.template_header)

        if self.template_version not in str(self.template_path):
            raise TrestleError(
                f'Version of the template {self.template_version} does not match the path {self.template_path}.'
                + f'Move the template to the folder {self.template_version}'
            )
        if 'Version' in self.template_header.keys() and self.template_header['Version'] != self.template_version:
            raise TrestleError(f'Version does not match template-version in template: {self.template_path}.')
        self._ignore_headers = []
        for key in self.template_header.keys():
            if key.lower().startswith('x-trestle-'):
                self._ignore_headers.append(key.lower())
                if key.lower() == 'x-trestle-ignore':
                    for key2 in template_header['x-trestle-ignore']:
                        self._ignore_headers.append(key2.lower())
Example #13
0
    def _do_fetch(self) -> None:
        auth = None
        verify = None
        # This order reflects requests library behavior: REQUESTS_CA_BUNDLE comes first.
        for env_var_name in ['REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE']:
            if env_var_name in os.environ:
                if pathlib.Path(os.environ[env_var_name]).exists():
                    verify = os.environ[env_var_name]
                    break
                else:
                    err_str = f'Env var ${env_var_name} found but path does not exist: {os.environ[env_var_name]}'
                    logger.warning(err_str)
                    raise TrestleError(f'Cache update failure with bad inputenv var: {err_str}')
        if self._username is not None and self._password is not None:
            auth = HTTPBasicAuth(self._username, self._password)
        try:
            response = requests.get(self._url, auth=auth, verify=verify)
        except Exception as e:
            raise TrestleError(f'Cache update failure to connect via HTTPS: {self._url} ({e})')

        if response.status_code == 200:
            try:
                result = response.text
            except Exception as err:
                raise TrestleError(f'Cache update failure reading response via HTTPS: {self._url} ({err})')
            else:
                self._cached_object_path.write_text(result, encoding=const.FILE_ENCODING)
        else:
            raise TrestleError(f'GET returned code {response.status_code}: {self._uri}')
Example #14
0
    def __init__(self, trestle_root: pathlib.Path, uri: str) -> None:
        """Initialize SFTP fetcher. Update the expected cache path as per caching specs.

        Args:
            trestle_root: Path of the Trestle project path, i.e., within which .trestle is to be found.
            uri: Reference to the remote file to cache that can be fetched using the sftp:// scheme.
        """
        logger.debug(f'initialize SFTPFetcher for uri {uri}')
        super().__init__(trestle_root, uri)
        # Is this a valid URI, however? Username and password are optional, of course.
        try:
            u = parse.urlparse(self._uri)
        except Exception as e:
            logger.warning(f'SFTP fetcher unable to parse uri {self._uri} error {e}')
            raise TrestleError(f'Unable to parse malformed url {self._uri} error {e}')
        logger.debug(f'SFTP fetcher with parsed uri {u}')
        if not u.hostname:
            logger.debug('SFTP fetcher uri missing hostname')
            logger.warning(f'Malformed URI, cannot parse hostname in URL {self._uri}')
            raise TrestleError(f'Cache request for invalid input URI: missing hostname {self._uri}')
        if not u.path:
            logger.debug('SFTP fetcher uri missing path')
            logger.warning(f'Malformed URI, cannot parse path in URL {self._uri}')
            raise TrestleError(f'Cache request for invalid input URI: missing file path {self._uri}')

        sftp_cached_dir = self._trestle_cache_path / u.hostname
        # Skip any number of back- or forward slashes preceding the URL path (u.path)
        path_parent = pathlib.Path(u.path[re.search('[^/\\\\]', u.path).span()[0]:]).parent
        sftp_cached_dir = sftp_cached_dir / path_parent
        sftp_cached_dir.mkdir(parents=True, exist_ok=True)
        self._cached_object_path = sftp_cached_dir / pathlib.Path(pathlib.Path(u.path).name)
Example #15
0
    def __init__(self,
                 template_path: pathlib.Path,
                 must_be_first_tab: bool = True) -> None:
        """
        Initialize drawio validator.

        Args:
            template_path: Path to a templated drawio file where metadata will be looked up on the first tab only.
            must_be_first_tab: Whether to search the candidate file for a metadata across multiple tabs.
        """
        self.template_path = template_path
        self.must_be_first_tab = must_be_first_tab
        # Load metadat from template
        template_drawio = DrawIO(self.template_path)
        # Zero index as must be first tab
        self.template_metadata = template_drawio.get_metadata()[0]
        self.template_version = MarkdownValidator.extract_template_version(
            self.template_metadata)
        if self.template_version not in str(self.template_path):
            raise TrestleError(
                f'Version of the template {self.template_version} does not match the path {self.template_path}.'
                + f'Move the template to the folder {self.template_version}')
        if 'Version' in self.template_metadata.keys(
        ) and self.template_metadata['Version'] != self.template_version:
            raise TrestleError(
                f'Version does not match template-version in template: {self.template_path}.'
            )
Example #16
0
    def set_at(self, element_path: ElementPath,
               sub_element: OscalBaseModel) -> 'Element':
        """Set a sub_element at the path in the current element.

        Sub element can be Element, OscalBaseModel, list or None type
        It returns the element itself so that chaining operation can be done such as
            `element.set_at(path, sub-element).get()`.
        """
        # convert the element_path to ElementPath if needed
        if isinstance(element_path, str):
            element_path = ElementPath(element_path)

        # convert sub-element to OscalBaseModel if needed
        model_obj = self._get_sub_element_obj(sub_element)

        # find the root-model and element path parts
        _, path_parts = self._split_element_path(element_path)

        # TODO validate that self._elem is of same type as root_model

        # If wildcard is present, check the input type and determine the preceding element
        if element_path.get_last() == ElementPath.WILDCARD:
            # validate the type is either list or OscalBaseModel
            if not isinstance(model_obj, list) and not isinstance(
                    model_obj, OscalBaseModel):
                raise TrestleError(
                    f'The model object needs to be a List or OscalBaseModel for path with "{ElementPath.WILDCARD}"'
                )

            # since wildcard * is there, we need to go one level up for preceding element in the path
            preceding_elm = self.get_preceding_element(
                element_path.get_preceding_path())
        else:
            # get the preceding element in the path
            preceding_elm = self.get_preceding_element(element_path)

        if preceding_elm is None:
            raise TrestleError(
                f'Invalid sub element path {element_path} with no valid preceding element'
            )

        # check if it can be a valid sub_element of the parent
        sub_element_name = element_path.get_element_name().replace('-', '_')
        if hasattr(preceding_elm, sub_element_name) is False:
            raise TrestleError(
                f'Element "{preceding_elm.__class__}" does not have the attribute "{sub_element_name}" '
                f'of type "{model_obj.__class__}"')

        # set the sub-element
        try:
            setattr(preceding_elm, sub_element_name, model_obj)
        except ValidationError:
            sub_element_class = self.get_sub_element_class(
                preceding_elm, sub_element_name)
            raise TrestleError(
                f'Validation error: {sub_element_name} is expected to be "{sub_element_class}", '
                f'but found "{model_obj.__class__}"')

        # returning self will allow to do 'chaining' of commands after set
        return self
Example #17
0
    def _check_if_exists_and_dir(task_path: Path) -> None:
        if not task_path.exists():
            raise TrestleError(f'Path: {task_path} does not exists.')

        if not task_path.is_dir():
            raise TrestleError(
                f'File {task_path} passed, however template directory is expected.'
            )
Example #18
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)
Example #19
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
Example #20
0
    def execute(self) -> None:
        """Execute the action."""
        if self._element is None:
            raise TrestleError('Element is empty and cannot write')

        if not self._is_writer_valid():
            raise TrestleError('Writer is not provided or closed')

        self._writer.write(self._encode())
        self._writer.flush()
        self._mark_executed()
Example #21
0
    def write_drawio_with_metadata(self,
                                   path: pathlib.Path,
                                   metadata: Dict,
                                   diagram_metadata_idx: int,
                                   target_path: pathlib.Path = None) -> None:
        """
        Write modified metadata to drawio file.

        Writes given metadata to 'object' element attributes inside of the selected drawio diagram element.
        Currently supports writing only uncompressed elements.

        Args:
            path: path to write modified drawio file to
            metadata: dictionary of modified metadata to insert to drawio
            diagram_metadata_idx: index of diagram which metadata was modified
            target_path: if not provided the changes will be written to path
        """
        flattened_dict = self._flatten_dictionary(metadata)
        if diagram_metadata_idx >= len(list(self.diagrams)):
            raise TrestleError(
                f'Drawio file {path} does not contain a diagram for index {diagram_metadata_idx}'
            )

        diagram = list(self.diagrams)[diagram_metadata_idx]
        children = list(diagram)
        root_obj = children[0]
        md_objects = root_obj.findall('object')
        if len(md_objects) == 0:
            raise TrestleError(
                f'Unable to write metadata, diagram in drawio file {path} does not have objects.'
            )

        for key in md_objects[0].attrib.copy():
            if key not in flattened_dict.keys(
            ) and key not in self.banned_keys:
                # outdated key delete
                del md_objects[0].attrib[key]
                continue
            if key in self.banned_keys:
                continue
            md_objects[0].attrib[key] = flattened_dict[key]
        for key in flattened_dict.keys():
            if key in self.banned_keys:
                continue
            md_objects[0].attrib[key] = flattened_dict[key]
        parent_diagram = self.mx_file.findall('diagram')[diagram_metadata_idx]
        if len(parent_diagram.findall('mxGraphModel')) == 0:
            parent_diagram.insert(0, diagram)

        if target_path:
            self.raw_xml.write(target_path)
        else:
            self.raw_xml.write(path)
Example #22
0
    def write_versioned_template(resource_name: str,
                                 task_path: Path,
                                 target_file: Path,
                                 version: Optional[str] = None) -> None:
        """
        Write a template with the header or metadata of a specified version.

        If no version was given the latest version for the task will be used.

        Args:
            resource_name:  Template resource name
            task_path: Task path
            target_file: File path where template will be written
            version: return a resource of a specific version

        Returns:
            A dotted path of a versioned template, list of all available versions
        """
        TemplateVersioning._check_if_exists_and_dir(task_path)
        try:
            templates_resource_path = TRESTLE_RESOURCES + '.templates'

            generic_template = Path(
                resource_filename(templates_resource_path,
                                  resource_name)).resolve()
            if version is None:
                _, version = TemplateVersioning.get_latest_version_for_task(
                    task_path)

            # modify header/metadata in the template
            if generic_template.suffix == '.md':
                md_api = MarkdownAPI()
                header, md_body = md_api.processor.read_markdown_wo_processing(
                    generic_template)
                header[TEMPLATE_VERSION_HEADER] = version
                md_api.write_markdown_with_header(target_file, header, md_body)
                logger.debug(
                    f'Successfully written template markdown to {target_file}')
            elif generic_template.suffix == '.drawio':
                drawio = DrawIO(generic_template)
                metadata = drawio.get_metadata()[0]
                metadata[TEMPLATE_VERSION_HEADER] = version

                drawio.write_drawio_with_metadata(generic_template, metadata,
                                                  0, target_file)
                logger.debug(
                    f'Successfully written template drawio to {target_file}')
            else:
                raise TrestleError(
                    f'Unsupported template file extension {generic_template.suffix}'
                )
        except OSError as e:
            raise TrestleError(f'Error while updating template folder: {e}')
Example #23
0
    def get_model(self, model_type: Type[OscalBaseModel], name: str) -> ManagedOSCAL:
        """Get a specific OSCAL model from repository."""
        logger.debug(f'Getting model {name} of type {model_type.__name__}.')
        model_alias = classname_to_alias(model_type.__name__, AliasMode.JSON)
        if parser.to_full_model_name(model_alias) is None:
            raise TrestleError(f'Given model {model_alias} is not a top level model.')
        plural_path = ModelUtils.model_type_to_model_dir(model_alias)
        desired_model_dir = self._root_dir / plural_path / name

        if not desired_model_dir.exists() or not desired_model_dir.is_dir():
            raise TrestleError(f'Model {name} does not exist.')

        return ManagedOSCAL(self._root_dir, model_type, name)
Example #24
0
    def _parse(self, element_path: str) -> List[str]:
        """Parse the element path and validate."""
        parts: List[str] = element_path.split(self.PATH_SEPARATOR)

        for part in parts:
            if part == '':
                raise TrestleError(
                    f'Invalid path "{element_path}" because there are empty path parts between "{self.PATH_SEPARATOR}" '
                    'or in the beginning')

        if parts[0] == self.WILDCARD:
            raise TrestleError(f'Invalid path {element_path} with wildcard.')
        return parts
Example #25
0
    def rollback(self) -> None:
        """Rollback the action."""
        if not self._is_writer_valid():
            raise TrestleError('Writer is not provided or closed')

        if self._lastStreamPos < 0:
            raise TrestleError(
                'Last stream position is not available to rollback to')

        if self.has_executed():
            self._writer.seek(self._lastStreamPos)
            self._writer.truncate()

        self._mark_rollback()
Example #26
0
    def _copy_config_file(self, root: pathlib.Path) -> None:
        """Copy the initial config.ini file to .trestle directory."""
        try:
            source_path = pathlib.Path(
                resource_filename('trestle.resources',
                                  const.TRESTLE_CONFIG_FILE)).resolve()
            destination_path = (root / pathlib.Path(const.TRESTLE_CONFIG_DIR) /
                                const.TRESTLE_CONFIG_FILE).resolve()
            copyfile(source_path, destination_path)

        except (shutil.SameFileError, OSError) as e:
            raise TrestleError(f'Error while copying config file: {e}')
        except Exception as e:
            raise TrestleError(
                f'Unexpected error while copying config file: {e}')
Example #27
0
def create_trestle_project_with_model(
    top_dir: pathlib.Path, model_obj: OscalBaseModel, model_name: str, monkeypatch: MonkeyPatch
) -> pathlib.Path:
    """Create initialized trestle project and import the model into it."""
    cur_dir = pathlib.Path.cwd()

    # create subdirectory for trestle project
    trestle_root = top_dir / 'my_trestle'
    trestle_root.mkdir()
    os.chdir(trestle_root)

    try:
        testargs = ['trestle', 'init']
        monkeypatch.setattr(sys, 'argv', testargs)
        assert Trestle().run() == 0

        # place model object in top directory outside trestle project
        # so it can be imported
        tmp_model_path = top_dir / (model_name + '.json')
        model_obj.oscal_write(tmp_model_path)

        i = ImportCmd()
        args = argparse.Namespace(
            trestle_root=trestle_root, file=str(tmp_model_path), output=model_name, verbose=0, regenerate=False
        )
        assert i._run(args) == 0
    except Exception as e:
        raise TrestleError(f'Error creating trestle project with model: {e}')
    finally:
        os.chdir(cur_dir)
    return trestle_root
Example #28
0
    def get_oscal_with_model_type(self, model_type: Type[OscalBaseModel], force_update=False) -> OscalBaseModel:
        """Retrieve the cached file as a particular OSCAL model.

        Arguments:
            model_type: Type[OscalBaseModel] Specifies the OSCAL model type of the fetched object.
        """
        self._update_cache(force_update)
        cache_file = self._cached_object_path
        if not cache_file.exists():
            raise TrestleError(f'get_oscal failure for {self._uri}')

        try:
            return model_type.oscal_read(cache_file)
        except Exception as e:
            logger.debug(f'get_oscal failed, error loading cache file for {self._uri} as {model_type}')
            raise TrestleError(f'get_oscal failure for {self._uri}: {e}.') from e
Example #29
0
    def load_top_level_model(
        trestle_root: pathlib.Path,
        model_name: str,
        model_class: Type[TopLevelOscalModel],
        file_content_type: Optional[FileContentType] = None
    ) -> Tuple[Union[OscalBaseModel, List[OscalBaseModel], Dict[
            str, OscalBaseModel]], pathlib.Path]:
        """Load a model by name and model class and infer file content type if not specified.

        If you need to load an existing model but its content type may not be known, use this method.
        But the file content type should be specified if it is somehow known.
        """
        root_model_path = ModelUtils._root_path_for_top_level_model(
            trestle_root, model_name, model_class)
        if file_content_type is None:
            file_content_type = FileContentType.path_to_content_type(
                root_model_path)
        if not FileContentType.is_readable_file(file_content_type):
            raise TrestleError(
                f'Unable to load model {model_name} without specifying json or yaml.'
            )
        full_model_path = root_model_path.with_suffix(
            FileContentType.to_file_extension(file_content_type))
        _, _, model = ModelUtils.load_distributed(full_model_path,
                                                  trestle_root)
        return model, full_model_path
Example #30
0
    def setup(self, template_version: str) -> int:
        """Create template directory and templates."""
        # Step 1 - validation

        if self.task_name and not self.task_path.exists():
            self.task_path.mkdir(exist_ok=True, parents=True)
        elif self.task_name and self.task_path.is_file():
            raise TrestleError(
                f'Task path: {self.rel_dir(self.task_path)} is a file not a directory.'
            )

        if not self.template_dir.exists():
            self.template_dir.mkdir(exist_ok=True, parents=True)
        logger.info(
            f'Populating template files to {self.rel_dir(self.template_dir)}')
        for template in author_const.REFERENCE_TEMPLATES.values():
            destination_path = self.template_dir / template
            TemplateVersioning.write_versioned_template(
                template, self.template_dir, destination_path,
                template_version)

            logger.info(
                f'Template directory populated {self.rel_dir(destination_path)}'
            )
        return CmdReturnCodes.SUCCESS.value