Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add compdef create #22

Merged
merged 3 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
502 changes: 253 additions & 249 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ trestle-bot = "trestlebot.cli:run"
[tool.poetry.dependencies]
python = '^3.8.1'
gitpython = "^3.1.31"
compliance-trestle = "^2.1.1"
compliance-trestle = "^2.2.1"
github3-py = "^4.0.1"

[tool.poetry.group.dev.dependencies]
Expand Down
107 changes: 107 additions & 0 deletions tests/trestlebot/tasks/authored/test_compdef.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/python

# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""Test for Trestle Bot Authored Compdef."""

import pathlib

import pytest
from trestle.common.model_utils import ModelUtils
from trestle.core.models.file_content_type import FileContentType
from trestle.oscal.component import ComponentDefinition

from tests import testutils
from trestlebot.tasks.authored.base_authored import AuthoredObjectException
from trestlebot.tasks.authored.compdef import AuthoredComponentsDefinition


test_prof = "simplified_nist_profile"
test_comp = "test_comp"


def test_create_new_default(tmp_trestle_dir: str) -> None:
"""Test creating new default component definition"""
# Prepare the workspace
trestle_root = pathlib.Path(tmp_trestle_dir)
_ = testutils.setup_for_profile(trestle_root, test_prof, "")
authored_comp = AuthoredComponentsDefinition(tmp_trestle_dir)

authored_comp.create_new_default(test_prof, test_comp, "test", "My desc", "service")

comp, _ = ModelUtils.load_model_for_class(
trestle_root, test_comp, ComponentDefinition, FileContentType.JSON
)

assert comp is not None

assert comp.components is not None
assert comp.components[0] is not None
assert comp.components[0].control_implementations is not None

assert (
len(comp.components[0].control_implementations[0].implemented_requirements)
== 12
)


def test_create_new_default_existing(tmp_trestle_dir: str) -> None:
"""Test creating new default component in existing component definition"""
# Prepare the workspace
trestle_root = pathlib.Path(tmp_trestle_dir)
_ = testutils.setup_for_compdef(trestle_root, test_comp, "")
authored_comp = AuthoredComponentsDefinition(tmp_trestle_dir)

authored_comp.create_new_default(test_prof, test_comp, "test", "My desc", "service")

comp, _ = ModelUtils.load_model_for_class(
trestle_root, test_comp, ComponentDefinition, FileContentType.JSON
)

assert comp is not None

# Check new component
assert comp.components is not None
assert comp.components[1] is not None
assert comp.components[1].control_implementations is not None

assert (
len(comp.components[1].control_implementations[0].implemented_requirements)
== 12
)

# Check existing component
assert comp.components[0] is not None
assert comp.components[0].control_implementations is not None

assert (
len(comp.components[0].control_implementations[0].implemented_requirements) == 2
)


def test_create_new_default_no_profile(tmp_trestle_dir: str) -> None:
"""Test creating new default component definition successfully"""
# Prepare the workspace
trestle_root = pathlib.Path(tmp_trestle_dir)
_ = testutils.setup_for_compdef(trestle_root, test_comp, "")

authored_comp = AuthoredComponentsDefinition(tmp_trestle_dir)

with pytest.raises(
AuthoredObjectException, match="Profile fake does not exist in the workspace"
):
authored_comp.create_new_default(
"fake", test_comp, "test", "My desc", "service"
)
2 changes: 2 additions & 0 deletions trestlebot/tasks/authored/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(self, trestle_root: str) -> None:
super().__init__(trestle_root)

def assemble(self, markdown_path: str, version_tag: str = "") -> None:
"""Run assemble actions for catalog type at the provided path"""
trestle_root = pathlib.Path(self.get_trestle_root())
catalog = os.path.basename(markdown_path)
try:
Expand All @@ -61,6 +62,7 @@ def assemble(self, markdown_path: str, version_tag: str = "") -> None:
raise AuthoredObjectException(f"Trestle assemble failed for {catalog}: {e}")

def regenerate(self, model_path: str, markdown_path: str) -> None:
"""Run assemble actions for catalog type at the provided path"""
trestle_root = self.get_trestle_root()
trestle_path = pathlib.Path(trestle_root)
catalog_generate: CatalogGenerate = CatalogGenerate()
Expand Down
130 changes: 129 additions & 1 deletion trestlebot/tasks/authored/compdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,21 @@

import os
import pathlib
import shutil
from typing import List, Optional, Type

from trestle.common.err import TrestleError
import trestle.core.generators as gens
import trestle.oscal.component as comp
import trestle.oscal.profile as prof
from trestle.common.err import TrestleError, TrestleNotFoundError
from trestle.common.list_utils import as_list
from trestle.common.load_validate import load_validate_model_name
from trestle.common.model_utils import ModelUtils
from trestle.core.catalog.catalog_interface import CatalogInterface
from trestle.core.commands.author.component import ComponentAssemble, ComponentGenerate
from trestle.core.commands.common.return_codes import CmdReturnCodes
from trestle.core.models.file_content_type import FileContentType
from trestle.core.profile_resolver import ProfileResolver

from trestlebot.tasks.authored.base_authored import (
AuthoredObjectException,
Expand All @@ -41,6 +52,7 @@ def __init__(self, trestle_root: str) -> None:
super().__init__(trestle_root)

def assemble(self, markdown_path: str, version_tag: str = "") -> None:
"""Run assemble actions for compdef type at the provided path"""
trestle_root = pathlib.Path(self.get_trestle_root())
compdef = os.path.basename(markdown_path)
try:
Expand All @@ -60,6 +72,7 @@ def assemble(self, markdown_path: str, version_tag: str = "") -> None:
raise AuthoredObjectException(f"Trestle assemble failed for {compdef}: {e}")

def regenerate(self, model_path: str, markdown_path: str) -> None:
"""Run assemble actions for compdef type at the provided path"""
trestle_root = self.get_trestle_root()
trestle_path = pathlib.Path(trestle_root)
comp_generate: ComponentGenerate = ComponentGenerate()
Expand All @@ -79,3 +92,118 @@ def regenerate(self, model_path: str, markdown_path: str) -> None:
raise AuthoredObjectException(
f"Trestle generate failed for {comp_name}: {e}"
)

def create_new_default(
self,
profile_name: str,
compdef_name: str,
comp_title: str,
comp_description: str,
comp_type: str,
) -> None:
"""Create the new component definition with default info"""
trestle_root: pathlib.Path = pathlib.Path(self.get_trestle_root())

existing_profile_path = ModelUtils.get_model_path_for_name_and_class(
trestle_root, profile_name, prof.Profile
)

if existing_profile_path is None:
raise AuthoredObjectException(
f"Profile {profile_name} does not exist in the workspace"
)

catalog = ProfileResolver.get_resolved_profile_catalog(
trestle_root,
existing_profile_path.as_posix(),
)

controls = CatalogInterface.get_control_ids_from_catalog(catalog)

comp_data: Type[comp.ComponentDefinition]
existing_comp_data_path: Optional[pathlib.Path]

# Attempt to load the existing compdef if not found create a new instance
try:
comp_data, comp_data_path = load_validate_model_name(
trestle_root,
compdef_name,
comp.ComponentDefinition,
FileContentType.JSON,
) # type: ignore
existing_comp_data_path = pathlib.Path(comp_data_path)
except TrestleNotFoundError:
comp_data = gens.generate_sample_model(comp.ComponentDefinition) # type: ignore
existing_comp_data_path = ModelUtils.get_model_path_for_name_and_class(
trestle_root,
compdef_name,
comp.ComponentDefinition,
FileContentType.JSON,
)
if existing_comp_data_path is None:
raise AuthoredObjectException(
f"Error defining workspace name for component {compdef_name}"
)

component = gens.generate_sample_model(comp.DefinedComponent)
component.type = comp_type
component.title = comp_title
component.description = comp_description
component.control_implementations = []

get_control_implementation(
component=component,
source=existing_profile_path.as_posix(),
description="",
controls=controls,
)

comp_data.components = as_list(comp_data.components)
comp_data.components.append(component)

cd_path = pathlib.Path(existing_comp_data_path)
if cd_path.parent.exists():
shutil.rmtree(str(cd_path.parent))

ModelUtils.update_last_modified(comp_data) # type: ignore

cd_path.parent.mkdir(parents=True, exist_ok=True)
comp_data.oscal_write(path=cd_path) # type: ignore


def get_control_implementation(
component: comp.DefinedComponent, source: str, description: str, controls: List[str]
) -> comp.ControlImplementation:
"""Find or create control implementation."""

component.control_implementations = as_list(component.control_implementations)
for control_implementation in component.control_implementations:
if (
control_implementation.source == source
and control_implementation.description == description
):
return control_implementation

control_implementation = gens.generate_sample_model(comp.ControlImplementation)
control_implementation.source = source
control_implementation.description = description
control_implementation.implemented_requirements = []

for control_id in controls:
get_implemented_requirement(control_implementation, control_id)

component.control_implementations.append(control_implementation)
return control_implementation


def get_implemented_requirement(
control_implementation: comp.ControlImplementation, control_id: str
) -> comp.ImplementedRequirement:
"""Find or create implemented requirement."""
for implemented_requirement in control_implementation.implemented_requirements:
if implemented_requirement.control_id == control_id:
return implemented_requirement
implemented_requirement = gens.generate_sample_model(comp.ImplementedRequirement)
implemented_requirement.control_id = control_id
control_implementation.implemented_requirements.append(implemented_requirement)
return implemented_requirement
2 changes: 2 additions & 0 deletions trestlebot/tasks/authored/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(self, trestle_root: str) -> None:
super().__init__(trestle_root)

def assemble(self, markdown_path: str, version_tag: str = "") -> None:
"""Run assemble actions for profile type at the provided path"""
trestle_root = pathlib.Path(self.get_trestle_root())
profile = os.path.basename(markdown_path)
try:
Expand All @@ -64,6 +65,7 @@ def assemble(self, markdown_path: str, version_tag: str = "") -> None:
raise AuthoredObjectException(f"Trestle assemble failed for {profile}: {e}")

def regenerate(self, model_path: str, markdown_path: str) -> None:
"""Run assemble actions for profile type at the provided path"""
trestle_root = self.get_trestle_root()
trestle_path = pathlib.Path(trestle_root)
profile_generate: ProfileGenerate = ProfileGenerate()
Expand Down