diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..35ab7fef --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,16 @@ +#!/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. +"""Integration tests between different types of tasks classes.""" diff --git a/tests/integration/test_rules_transform_workflow.py b/tests/integration/test_rules_transform_workflow.py new file mode 100644 index 00000000..7fa83a54 --- /dev/null +++ b/tests/integration/test_rules_transform_workflow.py @@ -0,0 +1,117 @@ +#!/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. + +"""Component test the rules transformation workflow.""" + +import os +import pathlib + +from trestle.common.load_validate import load_validate_model_name +from trestle.oscal import component as comp + +import trestlebot.const as const +from tests.testutils import setup_for_profile +from trestlebot.tasks.assemble_task import AssembleTask +from trestlebot.tasks.authored.compdef import AuthoredComponentDefinition +from trestlebot.tasks.authored.types import AuthoredType +from trestlebot.tasks.regenerate_task import RegenerateTask +from trestlebot.tasks.rule_transform_task import RuleTransformTask +from trestlebot.transformers.yaml_transformer import ToRulesYAMLTransformer + + +test_component_definition = "test_component_definition" +test_profile = "simplified_nist_profile" +test_md_path = "md_compdef" + + +def test_rules_transform_workflow(tmp_trestle_dir: str) -> None: + """Test the rules transformation workflow for component definitions.""" + + trestle_root_path = pathlib.Path(tmp_trestle_dir) + + # Environment setup and initial rule generation + _ = setup_for_profile(trestle_root_path, test_profile, test_profile) + + authored_compdef = AuthoredComponentDefinition(trestle_root=tmp_trestle_dir) + authored_compdef.create_new_default( + profile_name=test_profile, + compdef_name=test_component_definition, + comp_title="Test", + comp_description="Test component definition", + comp_type="service", + ) + + # Transform + transform = RuleTransformTask( + tmp_trestle_dir, const.RULES_VIEW_DIR, ToRulesYAMLTransformer() + ) + transform.execute() + + # Load the component definition + compdef: comp.ComponentDefinition + compdef, _ = load_validate_model_name( + trestle_root_path, test_component_definition, comp.ComponentDefinition + ) + + assert len(compdef.components) == 1 + component = compdef.components[0] + + assert component.title == "Test" + assert component.description == "Test component definition" + assert component.type == "service" + assert len(component.props) == 24 + assert len(component.control_implementations) == 1 + assert ( + component.control_implementations[0].source + == f"trestle://profiles/{test_profile}/profile.json" + ) + + last_modified = compdef.metadata.last_modified + + # Run regenerate + regenerate = RegenerateTask( + tmp_trestle_dir, AuthoredType.COMPDEF.value, test_md_path + ) + regenerate.execute() + + assert os.path.exists( + os.path.join(trestle_root_path, test_md_path, test_component_definition) + ) + + # Run assemble + assemble = AssembleTask(tmp_trestle_dir, AuthoredType.COMPDEF.value, test_md_path) + assemble.execute() + + # Load the component definition + compdef, _ = load_validate_model_name( + trestle_root_path, test_component_definition, comp.ComponentDefinition + ) + + assert len(compdef.components) == 1 + component = compdef.components[0] + + # Asset last modified is updated, but all expected information is still present + assert compdef.metadata.last_modified > last_modified + + assert component.title == "Test" + assert component.description == "Test component definition" + assert component.type == "service" + assert len(component.props) == 24 + assert len(component.control_implementations) == 1 + assert ( + component.control_implementations[0].source + == f"trestle://profiles/{test_profile}/profile.json" + ) diff --git a/tests/testutils.py b/tests/testutils.py index a4962a18..e4020565 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -245,3 +245,17 @@ def write_index_json( with open(file_path, "w") as file: json.dump(data, file, indent=4) + + +def replace_string_in_file(file_path: str, old_string: str, new_string: str) -> None: + """Replace a string in a file.""" + # Read the content of the file + with open(file_path, "r") as file: + file_content = file.read() + + # Replace the old string with the new string + updated_content = file_content.replace(old_string, new_string) + + # Write the updated content back to the file + with open(file_path, "w") as file: + file.write(updated_content) diff --git a/tests/trestlebot/tasks/authored/test_compdef.py b/tests/trestlebot/tasks/authored/test_compdef.py index 49047699..94b40710 100644 --- a/tests/trestlebot/tasks/authored/test_compdef.py +++ b/tests/trestlebot/tasks/authored/test_compdef.py @@ -24,7 +24,7 @@ from tests import testutils from trestlebot.const import RULES_VIEW_DIR, YAML_EXTENSION from trestlebot.tasks.authored.base_authored import AuthoredObjectException -from trestlebot.tasks.authored.compdef import AuthoredComponentsDefinition +from trestlebot.tasks.authored.compdef import AuthoredComponentDefinition from trestlebot.transformers.yaml_transformer import ToRulesYAMLTransformer @@ -37,7 +37,7 @@ def test_create_new_default(tmp_trestle_dir: str) -> None: # 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 = AuthoredComponentDefinition(tmp_trestle_dir) authored_comp.create_new_default(test_prof, test_comp, "test", "My desc", "service") @@ -75,7 +75,9 @@ def test_create_new_default(tmp_trestle_dir: str) -> None: "NIST Special Publication 800-53 Revision 5 MODERATE IMPACT \ BASELINE" ) - assert rule.profile.href == "profiles/simplified_nist_profile/profile.json" + assert ( + rule.profile.href == "trestle://profiles/simplified_nist_profile/profile.json" + ) assert rule.profile.include_controls is not None assert len(rule.profile.include_controls) == 1 assert rule.profile.include_controls[0].id == "ac-5" @@ -87,7 +89,7 @@ def test_create_new_default_no_profile(tmp_trestle_dir: str) -> None: trestle_root = pathlib.Path(tmp_trestle_dir) _ = testutils.setup_for_compdef(trestle_root, test_comp, "") - authored_comp = AuthoredComponentsDefinition(tmp_trestle_dir) + authored_comp = AuthoredComponentDefinition(tmp_trestle_dir) with pytest.raises( AuthoredObjectException, match="Profile fake does not exist in the workspace" diff --git a/tests/trestlebot/tasks/authored/test_types.py b/tests/trestlebot/tasks/authored/test_types.py index 07522116..3f2c500c 100644 --- a/tests/trestlebot/tasks/authored/test_types.py +++ b/tests/trestlebot/tasks/authored/test_types.py @@ -27,7 +27,7 @@ AuthorObjectBase, ) from trestlebot.tasks.authored.catalog import AuthoredCatalog -from trestlebot.tasks.authored.compdef import AuthoredComponentsDefinition +from trestlebot.tasks.authored.compdef import AuthoredComponentDefinition from trestlebot.tasks.authored.profile import AuthoredProfile from trestlebot.tasks.authored.ssp import AuthoredSSP @@ -69,7 +69,7 @@ def test_get_authored_compdef(tmp_trestle_dir: str) -> None: ) assert authored_object.get_trestle_root() == tmp_trestle_dir - assert isinstance(authored_object, AuthoredComponentsDefinition) + assert isinstance(authored_object, AuthoredComponentDefinition) def test_get_authored_ssp(tmp_trestle_dir: str) -> None: diff --git a/tests/trestlebot/tasks/test_assemble_task.py b/tests/trestlebot/tasks/test_assemble_task.py index eba89310..711f9870 100644 --- a/tests/trestlebot/tasks/test_assemble_task.py +++ b/tests/trestlebot/tasks/test_assemble_task.py @@ -22,10 +22,15 @@ from unittest.mock import Mock, patch import pytest +from trestle.common.model_utils import ModelUtils from trestle.core.commands.author.catalog import CatalogGenerate from trestle.core.commands.author.component import ComponentGenerate from trestle.core.commands.author.profile import ProfileGenerate from trestle.core.commands.author.ssp import SSPGenerate +from trestle.core.models.file_content_type import FileContentType +from trestle.oscal import catalog as oscal_cat +from trestle.oscal import component as oscal_comp +from trestle.oscal import profile as oscal_prof from tests import testutils from trestlebot.tasks.assemble_task import AssembleTask @@ -114,6 +119,12 @@ def test_catalog_assemble_task(tmp_trestle_dir: str) -> None: cat_generate = CatalogGenerate() assert cat_generate._run(args) == 0 + # Get original last modified time + cat, _ = ModelUtils.load_model_for_class( + trestle_root, test_cat, oscal_cat.Catalog, FileContentType.JSON + ) + orig_time = cat.metadata.last_modified + assemble_task = AssembleTask( tmp_trestle_dir, AuthoredType.CATALOG.value, @@ -122,6 +133,12 @@ def test_catalog_assemble_task(tmp_trestle_dir: str) -> None: ) assert assemble_task.execute() == 0 + # Get new last modified time and verify catalog was modified + cat, _ = ModelUtils.load_model_for_class( + trestle_root, test_cat, oscal_cat.Catalog, FileContentType.JSON + ) + assert orig_time != cat.metadata.last_modified + def test_profile_assemble_task(tmp_trestle_dir: str) -> None: """Test profile assemble at the task level""" @@ -130,6 +147,13 @@ def test_profile_assemble_task(tmp_trestle_dir: str) -> None: args = testutils.setup_for_profile(trestle_root, test_prof, md_path) profile_generate = ProfileGenerate() assert profile_generate._run(args) == 0 + + # Get original last modified time + prof, _ = ModelUtils.load_model_for_class( + trestle_root, test_prof, oscal_prof.Profile, FileContentType.JSON + ) + orig_time = prof.metadata.last_modified + assemble_task = AssembleTask( tmp_trestle_dir, AuthoredType.PROFILE.value, @@ -138,6 +162,12 @@ def test_profile_assemble_task(tmp_trestle_dir: str) -> None: ) assert assemble_task.execute() == 0 + # Get new last modified time adn verify profile was modified + prof, _ = ModelUtils.load_model_for_class( + trestle_root, test_prof, oscal_prof.Profile, FileContentType.JSON + ) + assert orig_time != prof.metadata.last_modified + def test_compdef_assemble_task(tmp_trestle_dir: str) -> None: """Test compdef assemble at the task level""" @@ -146,6 +176,18 @@ def test_compdef_assemble_task(tmp_trestle_dir: str) -> None: args = testutils.setup_for_compdef(trestle_root, test_comp, md_path) comp_generate = ComponentGenerate() assert comp_generate._run(args) == 0 + + # Get ac-1 markdown file + comp_path = os.path.join(trestle_root, compdef_md_dir, test_comp, test_comp) + ac1_md_path = os.path.join(comp_path, test_prof, "ac", "ac-1.md") + testutils.replace_string_in_file(ac1_md_path, "partial", "implemented") + + # Get original last modified time + comp, _ = ModelUtils.load_model_for_class( + trestle_root, test_comp, oscal_comp.ComponentDefinition, FileContentType.JSON + ) + orig_time = comp.metadata.last_modified + assemble_task = AssembleTask( tmp_trestle_dir, AuthoredType.COMPDEF.value, @@ -154,6 +196,12 @@ def test_compdef_assemble_task(tmp_trestle_dir: str) -> None: ) assert assemble_task.execute() == 0 + # Get new last modified time and verify component was modified + comp, _ = ModelUtils.load_model_for_class( + trestle_root, test_comp, oscal_comp.ComponentDefinition, FileContentType.JSON + ) + assert orig_time != comp.metadata.last_modified + def test_ssp_assemble_task(tmp_trestle_dir: str) -> None: """Test ssp assemble at the task level""" @@ -174,6 +222,10 @@ def test_ssp_assemble_task(tmp_trestle_dir: str) -> None: ) assert assemble_task.execute() == 0 + assert os.path.exists( + os.path.join(tmp_trestle_dir, "system-security-plans", test_ssp_output) + ) + def test_ssp_assemble_task_no_index_path(tmp_trestle_dir: str) -> None: """Test ssp assemble at the task level with failure""" diff --git a/tests/trestlebot/tasks/test_regenerate_task .py b/tests/trestlebot/tasks/test_regenerate_task.py similarity index 97% rename from tests/trestlebot/tasks/test_regenerate_task .py rename to tests/trestlebot/tasks/test_regenerate_task.py index c4e844b4..79ede3be 100644 --- a/tests/trestlebot/tasks/test_regenerate_task .py +++ b/tests/trestlebot/tasks/test_regenerate_task.py @@ -151,6 +151,7 @@ def test_profile_regenerate_task(tmp_trestle_dir: str) -> None: def test_compdef_regenerate_task(tmp_trestle_dir: str) -> None: """Test compdef regenerate at the task level""" trestle_root = pathlib.Path(tmp_trestle_dir) + md_path = os.path.join(compdef_md_dir, test_comp) _ = testutils.setup_for_compdef(trestle_root, test_comp, md_path) @@ -161,7 +162,9 @@ def test_compdef_regenerate_task(tmp_trestle_dir: str) -> None: "", ) assert regenerate_task.execute() == 0 - assert os.path.exists(os.path.join(tmp_trestle_dir, md_path)) + + # The compdef is a special case where each component has a separate markdown directory + assert os.path.exists(os.path.join(tmp_trestle_dir, md_path, test_comp)) def test_ssp_regenerate_task(tmp_trestle_dir: str) -> None: diff --git a/trestlebot/tasks/authored/compdef.py b/trestlebot/tasks/authored/compdef.py index 1dddacc9..09a9d484 100644 --- a/trestlebot/tasks/authored/compdef.py +++ b/trestlebot/tasks/authored/compdef.py @@ -20,6 +20,7 @@ import pathlib from typing import List +import trestle.common.const as const import trestle.oscal.profile as prof from trestle.common.err import TrestleError from trestle.common.model_utils import ModelUtils @@ -41,7 +42,7 @@ from trestlebot.transformers.yaml_transformer import FromRulesYAMLTransformer -class AuthoredComponentsDefinition(AuthorObjectBase): +class AuthoredComponentDefinition(AuthorObjectBase): """ Class for authoring OSCAL Component Definitions in automation """ @@ -62,7 +63,7 @@ def assemble(self, markdown_path: str, version_tag: str = "") -> None: success = authoring.assemble_component_definition_markdown( name=compdef, output=compdef, - markdown_dir=os.path.join(markdown_path, compdef), + markdown_dir=markdown_path, regenerate=False, version=version_tag, ) @@ -83,7 +84,7 @@ def regenerate(self, model_path: str, markdown_path: str) -> None: try: success = authoring.generate_component_definition_markdown( name=comp_name, - output=markdown_path, + output=os.path.join(markdown_path, comp_name), force_overwrite=False, ) if not success: @@ -165,7 +166,8 @@ def add_rules_for_profile( name=f"rule-{control_id}", description=f"Rule for {control_id}", profile=Profile( - href=str(profile_path.relative_to(self._trestle_root)), + href=const.TRESTLE_HREF_HEADING + + str(profile_path.relative_to(self._trestle_root)), description=catalog.metadata.title, include_controls=[Control(id=control_id)], ), diff --git a/trestlebot/tasks/authored/types.py b/trestlebot/tasks/authored/types.py index 4f8dc63e..e34f04b2 100644 --- a/trestlebot/tasks/authored/types.py +++ b/trestlebot/tasks/authored/types.py @@ -25,7 +25,7 @@ AuthorObjectBase, ) from trestlebot.tasks.authored.catalog import AuthoredCatalog -from trestlebot.tasks.authored.compdef import AuthoredComponentsDefinition +from trestlebot.tasks.authored.compdef import AuthoredComponentDefinition from trestlebot.tasks.authored.profile import AuthoredProfile from trestlebot.tasks.authored.ssp import AuthoredSSP, SSPIndex @@ -48,7 +48,7 @@ def get_authored_object( elif input_type == AuthoredType.PROFILE.value: return AuthoredProfile(working_dir) elif input_type == AuthoredType.COMPDEF.value: - return AuthoredComponentsDefinition(working_dir) + return AuthoredComponentDefinition(working_dir) elif input_type == AuthoredType.SSP.value: ssp_index: SSPIndex = SSPIndex(ssp_index_path) return AuthoredSSP(working_dir, ssp_index)