From 15009146334a2c9a83720ba747ea9ca4dca7547f Mon Sep 17 00:00:00 2001 From: Alex Flom Date: Thu, 27 Jul 2023 15:36:04 -0600 Subject: [PATCH 01/24] feat: adds ability to process exports from SSP and write Markdown by component Adds ExportInterface and ExportWriter classes Adds Markdown generation to ssp-generate Add MarkdownWriter for leveraged statements Signed-off-by: Jennifer Power Signed-off-by: Alex Flom --- .gitignore | 2 + tests/test_utils.py | 39 +++++- .../trestle/core/commands/author/ssp_test.py | 95 +++++++++++++ tests/trestle/core/crm/__init__.py | 16 +++ .../core/crm/exports_interface_test.py | 70 ++++++++++ tests/trestle/core/crm/exports_writer_test.py | 130 ++++++++++++++++++ tests/trestle/core/inheritance_writer_test.py | 100 ++++++++++++++ trestle/common/const.py | 32 +++++ trestle/core/commands/author/ssp.py | 29 ++++ trestle/core/crm/__init__.py | 14 ++ trestle/core/crm/export_interface.py | 112 +++++++++++++++ trestle/core/crm/export_writer.py | 117 ++++++++++++++++ trestle/core/inheritance_writer.py | 129 +++++++++++++++++ 13 files changed, 881 insertions(+), 4 deletions(-) create mode 100644 tests/trestle/core/crm/__init__.py create mode 100644 tests/trestle/core/crm/exports_interface_test.py create mode 100644 tests/trestle/core/crm/exports_writer_test.py create mode 100644 tests/trestle/core/inheritance_writer_test.py create mode 100644 trestle/core/crm/__init__.py create mode 100644 trestle/core/crm/export_interface.py create mode 100644 trestle/core/crm/export_writer.py create mode 100644 trestle/core/inheritance_writer.py diff --git a/.gitignore b/.gitignore index dac7a874c..17dcce0bd 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ site tmp_bin_test .mypy_cache +/venv.trestle/ + diff --git a/tests/test_utils.py b/tests/test_utils.py index d167de1db..922335f9b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -441,10 +441,13 @@ def load_from_json( shutil.copy2(src_path, dst_path) -def setup_for_ssp(tmp_trestle_dir: pathlib.Path, - prof_name: str, - output_name: str, - use_yaml: bool = False) -> Tuple[argparse.Namespace, pathlib.Path]: +def setup_for_ssp( + tmp_trestle_dir: pathlib.Path, + prof_name: str, + output_name: str, + use_yaml: bool = False, + leveraged_ssp_name: str = '' +) -> Tuple[argparse.Namespace, pathlib.Path]: """Create the comp_def, profile and catalog content needed for ssp-generate.""" comp_names = 'comp_def_a,comp_def_b' for comp_name in comp_names.split(','): @@ -455,10 +458,15 @@ def setup_for_ssp(tmp_trestle_dir: pathlib.Path, load_from_json(tmp_trestle_dir, local_prof_name, local_prof_name, prof.Profile) load_from_json(tmp_trestle_dir, 'simplified_nist_catalog', 'simplified_nist_catalog', cat.Catalog) yaml_path = YAML_TEST_DATA_PATH / 'good_simple.yaml' if use_yaml else None + + if leveraged_ssp_name: + load_from_json(tmp_trestle_dir, leveraged_ssp_name, leveraged_ssp_name, ssp.SystemSecurityPlan) + args = argparse.Namespace( trestle_root=tmp_trestle_dir, profile=prof_name, compdefs=comp_names, + leveraged_ssp=leveraged_ssp_name, output=output_name, verbose=0, overwrite_header_values=False, @@ -637,6 +645,29 @@ def gen_and_assemble_first_ssp(prof_name: str, ssp_name: str, gen_args: Any, mon execute_command_and_assert(ssp_assemble, 0, monkeypatch) +def generate_test_by_comp() -> ssp.ByComponent: + """Generate a by-component assembly for testing.""" + by_comp = generators.generate_sample_model(ssp.ByComponent) + by_comp.export = generators.generate_sample_model(ssp.Export) + by_comp.export.provided = [] + by_comp.export.responsibilities = [] + + isolated_provided = generators.generate_sample_model(ssp.Provided) + isolated_responsibility = generators.generate_sample_model(ssp.Responsibility) + + set_provided = generators.generate_sample_model(ssp.Provided) + set_responsibility = generators.generate_sample_model(ssp.Responsibility) + + set_responsibility.provided_uuid = set_provided.uuid + + by_comp.export.provided.append(isolated_provided) + by_comp.export.provided.append(set_provided) + by_comp.export.responsibilities.append(isolated_responsibility) + by_comp.export.responsibilities.append(set_responsibility) + + return by_comp + + class FileChecker: """Check for changes in files after test operations.""" diff --git a/tests/trestle/core/commands/author/ssp_test.py b/tests/trestle/core/commands/author/ssp_test.py index 34288b135..a3d9e3fa2 100644 --- a/tests/trestle/core/commands/author/ssp_test.py +++ b/tests/trestle/core/commands/author/ssp_test.py @@ -256,6 +256,101 @@ def test_ssp_generate_header_edit(tmp_trestle_dir: pathlib.Path) -> None: assert len(co) == 3 +def test_ssp_generate_with_inheritance(tmp_trestle_dir: pathlib.Path) -> None: + """Test ssp-generate with inheritance view.""" + args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name, False, 'leveraged_ssp') + ssp_cmd = SSPGenerate() + assert ssp_cmd._run(args) == 0 + + # Test output for each type of file + + # Find export files under This System + this_system_dir = tmp_trestle_dir / ssp_name / const.INHERITANCE_VIEW_DIR / 'This System' + + expected_uuid = '11111111-0000-4000-9009-001001002001' + ac_21 = this_system_dir / 'ac-2.1' + test_provided = ac_21 / f'{expected_uuid}.md' + assert test_provided.exists() + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(test_provided) + assert tree is not None + assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == expected_uuid + + expected_provided = """# Provided Statement Description + +Consumer-appropriate description of what may be inherited. + +In the context of the application component in satisfaction of AC-2.1.""" + + # Confirm markdown content + node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == expected_provided + + expected_uuid = '11111111-0000-4000-9009-002001001001' + ac_2_stm = this_system_dir / 'ac-2_stmt.a' + test_provided = ac_2_stm / f'{expected_uuid}.md' + assert test_provided.exists() + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(test_provided) + assert tree is not None + assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == expected_uuid + + expected_responsibility = """# Responsibility Statement Description + +Leveraging system's responsibilities with respect to inheriting this capability. + +In the context of the application component in satisfaction of AC-2, part a. +""" + + # Confirm markdown content + node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == expected_responsibility + + # Fine export files under Application + application_dir = tmp_trestle_dir / ssp_name / const.INHERITANCE_VIEW_DIR / 'Application' + + expected_provided_uuid = '11111111-0000-4000-9009-002001002001' + expected_responsibility_uuid = '11111111-0000-4000-9009-002001002002' + ac_2_stm = application_dir / 'ac-2_stmt.a' + test_provided = ac_2_stm / f'{expected_provided_uuid}_{expected_responsibility_uuid}.md' + assert test_provided.exists() + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(test_provided) + assert tree is not None + assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == expected_provided_uuid + assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == expected_responsibility_uuid + + expected_provided = """# Provided Statement Description + +Consumer-appropriate description of what may be inherited. + +In the context of the application component in satisfaction of AC-2, part a. +""" + + expected_responsibility = """# Responsibility Statement Description + +Leveraging system's responsibilities with respect to inheriting this capability. + +In the context of the application component in satisfaction of AC-2, part a. +""" + + # Confirm markdown content + node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == expected_provided + + node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == expected_responsibility + + def test_ssp_assemble(tmp_trestle_dir: pathlib.Path) -> None: """Test ssp assemble from cli.""" gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name) diff --git a/tests/trestle/core/crm/__init__.py b/tests/trestle/core/crm/__init__.py new file mode 100644 index 000000000..4223677d9 --- /dev/null +++ b/tests/trestle/core/crm/__init__.py @@ -0,0 +1,16 @@ +# -*- mode:python; coding:utf-8 -*- + +# Copyright (c) 2020 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Tests crm package.""" diff --git a/tests/trestle/core/crm/exports_interface_test.py b/tests/trestle/core/crm/exports_interface_test.py new file mode 100644 index 000000000..f7726de60 --- /dev/null +++ b/tests/trestle/core/crm/exports_interface_test.py @@ -0,0 +1,70 @@ +# -*- mode:python; coding:utf-8 -*- +# Copyright (c) 2020 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Tests for the ExportInterface class.""" + +from tests import test_utils + +import trestle.oscal.ssp as ossp +from trestle.core.crm.export_interface import ExportInterface + +test_profile = 'simple_test_profile' +test_ssp = 'leveraged_ssp' + + +def test_get_isolated_responsibilities() -> None: + """Test retrieving isolated responsibilities statements.""" + by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() + expected_responsibility = 1 + expected_uuid = by_comp.export.responsibilities[0].uuid + + export_interface: ExportInterface = ExportInterface(by_comp) + + result = export_interface.get_isolated_responsibilities() + + assert len(result) == expected_responsibility + assert result[0].uuid == expected_uuid + + +def test_get_isolated_provided() -> None: + """Test retrieving isolated provided statements.""" + by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() + expected_provided = 1 + expected_uuid = by_comp.export.provided[0].uuid + + export_interface: ExportInterface = ExportInterface(by_comp) + + result = export_interface.get_isolated_provided() + + assert len(result) == expected_provided + assert result[0].uuid == expected_uuid + + +def test_get_export_sets() -> None: + """Test retrieving export set statements.""" + by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() + expected_set = 1 + expected_responsibility_uuid = by_comp.export.responsibilities[1].uuid + expected_provided_uuid = by_comp.export.provided[1].uuid + + export_interface: ExportInterface = ExportInterface(by_comp) + + result = export_interface.get_export_sets() + + result_set = result[0] + + assert len(result) == expected_set + assert result_set[0].uuid == expected_responsibility_uuid + assert result_set[0].provided_uuid == expected_provided_uuid + assert result_set[1].uuid == expected_provided_uuid diff --git a/tests/trestle/core/crm/exports_writer_test.py b/tests/trestle/core/crm/exports_writer_test.py new file mode 100644 index 000000000..5394f3a54 --- /dev/null +++ b/tests/trestle/core/crm/exports_writer_test.py @@ -0,0 +1,130 @@ +# -*- mode:python; coding:utf-8 -*- +# Copyright (c) 2020 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Tests for the ExportWriter class.""" + +import os +import pathlib +from typing import List, Tuple +from unittest.mock import Mock, patch + +import pytest + +from tests import test_utils + +import trestle.core.generators as gens +import trestle.oscal.ssp as ossp +from trestle.common.err import TrestleError +from trestle.common.model_utils import ModelUtils +from trestle.core.crm.export_interface import ExportInterface +from trestle.core.crm.export_writer import ExportWriter +from trestle.core.inheritance_writer import ( + LeveragedStatements, + StatementProvided, + StatementResponsibility, + StatementTree, +) +from trestle.core.models.file_content_type import FileContentType + +test_profile = 'simple_test_profile' +test_ssp = 'leveraged_ssp' + + +def custom_side_effect(file_path: pathlib.Path) -> None: + """Write a test file.""" + with open(file_path, 'w') as file: + file.write('test') + + +def test_write_exports_as_markdown(tmp_trestle_dir: pathlib.Path) -> None: + """Test happy path for writing markdown with a mock LeveragedStatement.""" + _ = test_utils.setup_for_inherit(tmp_trestle_dir, test_profile, '', test_ssp) + ssp, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, test_ssp, ossp.SystemSecurityPlan, FileContentType.JSON) + + inherited_path = tmp_trestle_dir.joinpath('inherited') + writer = ExportWriter(inherited_path, ssp) + + mock = Mock(spec=LeveragedStatements) + mock.write_statement_md.side_effect = custom_side_effect + + with patch('trestle.core.crm.export_writer.ExportWriter._statement_types_from_exports') as mock_process: + return_value: List[Tuple[str, LeveragedStatements]] = {'filepath': mock} + mock_process.return_value = return_value + writer.write_exports_as_markdown() + + assert os.path.exists(inherited_path.joinpath('This System', 'ac-2.1', 'filepath.md')) + # Check that directory are not created when no exports exists + assert not os.path.exists(inherited_path.joinpath('Application', 'ac2')) + + +def test_write_exports_as_markdown_invalid_ssp(tmp_trestle_dir: pathlib.Path) -> None: + """Test triggering an error with an invalid SSP input.""" + _ = test_utils.setup_for_inherit(tmp_trestle_dir, test_profile, '', test_ssp) + ssp, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, test_ssp, ossp.SystemSecurityPlan, FileContentType.JSON) + + # Delete a component that is used to create an invalid SSP + ssp.system_implementation.components.remove(ssp.system_implementation.components[2]) + + inherited_path = tmp_trestle_dir.joinpath('inherited') + writer = ExportWriter(inherited_path, ssp) + + with pytest.raises(TrestleError, match=r'Component .* is not in the system implementation'): + writer.write_exports_as_markdown() + + +def test_statement_types_from_exports(tmp_trestle_dir: pathlib.Path) -> None: + """Test generated LeveragedStatements and filenames with SSP input.""" + expected_provided = 1 + expected_responsibility = 1 + expected_set = 1 + + ssp = gens.generate_sample_model(ossp.SystemSecurityPlan) + + inherited_path = tmp_trestle_dir.joinpath('inherited') + writer = ExportWriter(inherited_path, ssp) + + by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() + export_interface: ExportInterface = ExportInterface(by_comp) + + result_leveraged_statements = writer._statement_types_from_exports(export_interface) + provided: List[StatementProvided] = [] + responsibility: List[StatementResponsibility] = [] + sets: List[StatementTree] = [] + + for leveraged_stm in result_leveraged_statements.values(): + if isinstance(leveraged_stm, StatementProvided): + provided.append(leveraged_stm) + elif isinstance(leveraged_stm, StatementResponsibility): + responsibility.append(leveraged_stm) + elif isinstance(leveraged_stm, StatementTree): + sets.append(leveraged_stm) + + assert len(provided) == expected_provided + assert len(responsibility) == expected_responsibility + assert len(sets) == expected_set + + +def test_statement_types_no_exports(tmp_trestle_dir: pathlib.Path) -> None: + """Test generated LeveragedStatements and filenames with no exports.""" + ssp = gens.generate_sample_model(ossp.SystemSecurityPlan) + + inherited_path = tmp_trestle_dir.joinpath('inherited') + writer = ExportWriter(inherited_path, ssp) + + by_comp = gens.generate_sample_model(ossp.ByComponent) + export_interface: ExportInterface = ExportInterface(by_comp) + + result_leveraged_statements = writer._statement_types_from_exports(export_interface) + + assert len(result_leveraged_statements) == 0 diff --git a/tests/trestle/core/inheritance_writer_test.py b/tests/trestle/core/inheritance_writer_test.py new file mode 100644 index 000000000..dec235217 --- /dev/null +++ b/tests/trestle/core/inheritance_writer_test.py @@ -0,0 +1,100 @@ +# -*- mode:python; coding:utf-8 -*- + +# Copyright (c) 2021 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Tests for control input output methods.""" + +import pathlib + +import trestle.common.const as const +import trestle.core.inheritance_writer as inheritancewriter +from trestle.core.markdown.markdown_api import MarkdownAPI + +provided_uuid = '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' +provided_statement_desc = 'provided statement description' +resp_uuid = '4b34c68f-75fa-4b38-baf0-e50158c13ac2' +resp_statement_desc = 'resp statement description' +satisfied_statement_desc = 'satisfied statement description' + + +def test_write_inheritance_tree(tmp_path: pathlib.Path) -> None: + """Test writing statements with both provided and responsibility.""" + statement_tree_path = tmp_path.joinpath('statement_tree.md') + + statement = inheritancewriter.StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc) + + statement.write_statement_md(statement_tree_path) + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_tree_path) + assert tree is not None + assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == provided_uuid + assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == resp_uuid + + # Confirm markdown content + node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == '# Provided Statement Description\n\nprovided statement description\n' + node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == '# Responsibility Statement Description\n\nresp statement description\n' + node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == ( + '# Satisfied Statement Description\n\n\nREPLACE_ME' + ) + + +def test_write_inheritance_provided(tmp_path: pathlib.Path) -> None: + """Test writing statements with only provided.""" + statement_provided_path = tmp_path.joinpath('statement_provided.md') + + statement = inheritancewriter.StatementProvided(provided_uuid, provided_statement_desc) + + statement.write_statement_md(statement_provided_path) + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_provided_path) + assert tree is not None + assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == provided_uuid + + # Confirm markdown content + node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == '# Provided Statement Description\n\nprovided statement description' + + +def test_write_inheritance_responsibility(tmp_path: pathlib.Path) -> None: + """Test writing statements with only responsibility.""" + statement_resp_path = tmp_path.joinpath('statement_req.md') + + statement = inheritancewriter.StatementResponsibility(resp_uuid, resp_statement_desc) + + statement.write_statement_md(statement_resp_path) + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_resp_path) + assert tree is not None + assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == resp_uuid + + # Confirm markdown content + node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == '# Responsibility Statement Description\n\nresp statement description\n' + node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == """# Satisfied Statement Description\n + +REPLACE_ME""" diff --git a/trestle/common/const.py b/trestle/common/const.py index 02c37b00e..6f9a02912 100644 --- a/trestle/common/const.py +++ b/trestle/common/const.py @@ -370,6 +370,10 @@ STATUS_REMARKS = 'status-remarks' +PROVIDED_STATEMENT_DESCRIPTION = 'Provided Statement Description' + +RESPONSIBILITY_STATEMENT_DESCRIPTION = 'Responsibility Statement Description' + # Following 5 are allowed state tokens for # SSP -> ControlImplementation -> ImplementedRequirements -> ByComponents -> common.ImplementationStatus -> State # Also -> ImplementedRequirements -> Statements -> ByComponents ... @@ -417,6 +421,11 @@ RULES_WARNING = '' # noqa E501 +SATISFIED_STATEMENT_COMMENT = ( + '' +) + THIS_SYSTEM_PROMPT = '### ' + SSP_MAIN_COMP_NAME RESPONSIBLE_ROLE = 'responsible-role' @@ -431,6 +440,10 @@ COMP_DEF_RULES_PARAM_VALS_TAG = TRESTLE_TAG + 'comp-def-rules-param-vals' +TRESTLE_LEVERAGING_COMP_TAG = TRESTLE_TAG + 'leveraging-comp' + +TRESTLE_STATEMENT_TAG = TRESTLE_TAG + 'statement' + PARAM_VALUES_TAG = TRESTLE_TAG + 'param-values' COMP_DEF_RULES_TAG = TRESTLE_TAG + 'comp-def-rules' @@ -465,6 +478,10 @@ REPLACE_ME = 'REPLACE_ME' +PROVIDED_UUID = 'provided-uuid' + +RESPONSIBILITY_UUID = 'responsibility-uuid' + YAML_PROPS_COMMENT = """ # Add or modify control properties here # Properties may be at the control or part level # Add control level properties like this: @@ -526,6 +543,13 @@ # """ +YAML_LEVERAGED_COMMENT = """ # Add or modify leveraged SSP Statements here. +""" + +YAML_LEVERAGING_COMP_COMMENT = """ # Leveraged statements can be optionally associated with components in this system. + # Associate leveraged statements to Components of this system here: +""" + DISPLAY_NAME = 'display-name' TRESTLE_GENERIC_NS = 'https://ibm.github.io/compliance-trestle/schemas/oscal' @@ -572,3 +596,11 @@ ORIGINATION_CUSTOMER_PROVIDED = 'customer-provided' ORIGINATION_INHERITED = 'inherited' + +# Constant relation to the inheritance view Markdown + +INHERITANCE_VIEW_DIR = 'inheritance' + +HELP_LEVERAGED = 'Name of the SSP to be leveraged.' + +SATISFIED_STATEMENT_DESCRIPTION = 'Satisfied Statement Description' diff --git a/trestle/core/commands/author/ssp.py b/trestle/core/commands/author/ssp.py index 45bb30e64..ed98ed9a4 100644 --- a/trestle/core/commands/author/ssp.py +++ b/trestle/core/commands/author/ssp.py @@ -42,6 +42,7 @@ from trestle.core.control_context import ContextPurpose, ControlContext from trestle.core.control_interface import ControlInterface, ParameterRep from trestle.core.control_reader import ControlReader +from trestle.core.crm.export_writer import ExportWriter from trestle.core.models.file_content_type import FileContentType from trestle.core.profile_resolver import ProfileResolver from trestle.core.remote.cache import FetcherFactory @@ -63,6 +64,10 @@ def _init_arguments(self) -> None: '-o', '--output', help='Name of the output generated ssp markdown folder', required=True, type=str ) # noqa E501 self.add_argument('-cd', '--compdefs', help=const.HELP_COMPDEFS, required=False, type=str) + + ls_help_str = 'Leveraged ssp with inheritable controls href or name in the trestle_workspace' + self.add_argument('-ls', '--leveraged-ssp', help=ls_help_str, required=False, type=str) + self.add_argument('-y', '--yaml-header', help=const.HELP_YAML_PATH, required=False, type=str) self.add_argument( '-fo', '--force-overwrite', help=const.HELP_FO_OUTPUT, required=False, action='store_true', default=False @@ -100,6 +105,7 @@ def _run(self, args: argparse.Namespace) -> int: trestle_root, args.profile, compdef_name_list, + args.leveraged_ssp, md_path, yaml_header, args.overwrite_header_values, @@ -114,6 +120,7 @@ def _generate_ssp_markdown( trestle_root: pathlib.Path, profile_name_or_href: str, compdef_name_list: List[str], + leveraged_ssp_name_or_href: str, md_path: pathlib.Path, yaml_header: Dict[str, Any], overwrite_header_values: bool, @@ -183,6 +190,28 @@ def _generate_ssp_markdown( catalog_api.write_catalog_as_markdown() + # Generate inheritance view after controls view completes + if leveraged_ssp_name_or_href: + # if file not recognized as URI form, assume it represents name of file in trestle directory + ssp: ossp.SystemSecurityPlan + ssp_in_trestle_dir = '://' not in leveraged_ssp_name_or_href + ssp_href = leveraged_ssp_name_or_href + if ssp_in_trestle_dir: + local_path = f'{const.MODEL_DIR_SSP}/{leveraged_ssp_name_or_href}/system-security-plan.json' + ssp_path = trestle_root / local_path + _, _, ssp = ModelUtils.load_distributed(ssp_path, trestle_root) + else: + fetcher = FetcherFactory.get_fetcher(trestle_root, ssp_href) + ssp = fetcher.get_oscal() + + inheritance_view_path: pathlib.Path = md_path.joinpath(const.INHERITANCE_VIEW_DIR) + inheritance_view_path.mkdir(exist_ok=True) + logger.debug(f'Creating content for inheritance view in {inheritance_view_path}') + + export_writer: ExportWriter = ExportWriter(inheritance_view_path, ssp) + + export_writer.write_exports_as_markdown() + return CmdReturnCodes.SUCCESS.value diff --git a/trestle/core/crm/__init__.py b/trestle/core/crm/__init__.py new file mode 100644 index 000000000..c67882572 --- /dev/null +++ b/trestle/core/crm/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Package of classes relating to leveraging and leveraged SSPs.""" diff --git a/trestle/core/crm/export_interface.py b/trestle/core/crm/export_interface.py new file mode 100644 index 000000000..85af37c8b --- /dev/null +++ b/trestle/core/crm/export_interface.py @@ -0,0 +1,112 @@ +# Copyright (c) 2021 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Provide interface to ssp allowing queries and operations for exports, inherited, and satisfied statements.""" + +import logging +import uuid +from typing import Dict, List, Tuple + +import trestle.oscal.ssp as ossp +from trestle.common.err import TrestleError +from trestle.common.list_utils import as_dict, as_list + +logger = logging.getLogger(__name__) + + +class ExportInterface: + """ + Interface to query exported provided and responsibility statements from. + + The by-component export statement is parse and the responsibility and provided statement + are separated into three catagories: + + isolated responsibilities - A responsibility with no provided statement + isolated provided - A provided statement with no referring responsibility statements + export set - A set with a single responsibility and referred provided statement + """ + + def __init__(self, by_comp: ossp.ByComponent): + """Initialize export writer for a single by-component assembly.""" + self._by_comp: ossp.ByComponent = by_comp + + self._provided_dict: Dict[uuid.UUID, ossp.Provided] = {} + self._responsibility_dict: Dict[uuid.UUID, ossp.Responsibility] = {} + self._responsibility_by_provided: Dict[uuid.UUID, List[ossp.Responsibility]] = {} + + if by_comp.export: + self._provided_dict = self._create_provided_dict() + self._responsibility_dict = self._create_responsibility_dict() + self._responsibility_by_provided = self._create_responsibility_by_provided_dict() + + def _create_provided_dict(self) -> Dict[uuid.UUID, ossp.Provided]: + provided_dict: Dict[uuid.UUID, ossp.Provided] = {} + for provided in as_list(self._by_comp.export.provided): + provided_dict[provided.uuid] = provided + return provided_dict + + def _create_responsibility_dict(self) -> Dict[uuid.UUID, ossp.Responsibility]: + responsibility_dict: Dict[uuid.UUID, ossp.Provided] = {} + for responsibility in as_list(self._by_comp.export.responsibilities): + responsibility_dict[responsibility.uuid] = responsibility + return responsibility_dict + + def _create_responsibility_by_provided_dict(self) -> Dict[uuid.UUID, List[ossp.Responsibility]]: + responsibility_by_provided: Dict[uuid.UUID, List[ossp.Responsibility]] = {} + for responsibility in as_list(self._by_comp.export.responsibilities): + if responsibility.provided_uuid is None: + continue + if responsibility.provided_uuid not in responsibility_by_provided: + responsibility_by_provided[responsibility.provided_uuid] = [responsibility] + else: + existing_list: List[ossp.Responsibility] = responsibility_by_provided[responsibility.provided_uuid] + existing_list.append(responsibility) + responsibility_by_provided[responsibility.provided_uuid] = existing_list + return responsibility_by_provided + + def get_isolated_responsibilities(self) -> List[ossp.Responsibility]: + """Return all isolated exported responsibilities.""" + all_responsibilities: List[ossp.Responsibility] = [] + for resp in as_dict(self._responsibility_dict).values(): + if resp.provided_uuid is None: + all_responsibilities.append(resp) + return all_responsibilities + + def get_isolated_provided(self) -> List[ossp.Responsibility]: + """Return all isolated exported provided capabilities.""" + all_provided: List[ossp.Provided] = [] + for provided in as_dict(self._provided_dict).values(): + if not self._provided_has_responsibilities(provided.uuid): + all_provided.append(provided) + return all_provided + + def get_export_sets(self) -> List[Tuple[ossp.Responsibility, ossp.Provided]]: + """Return a dictionary of every responsibility relationship with provided.""" + all_export_sets: List[Tuple[ossp.Responsibility, ossp.Provided]] = [] + for provided_uuid, responsibilities in as_dict(self._responsibility_by_provided).items(): + + # Ensure the provided object exists in the dictionary. + # If it doesn't this is a bug. + try: + provided = self._provided_dict[provided_uuid] + except KeyError: + raise TrestleError(f'Provided capability {provided_uuid} not found') + + for responsibility in responsibilities: + shared_responsibility: Tuple[ossp.Responsibility, ossp.Provided] = (responsibility, provided) + all_export_sets.append(shared_responsibility) + return all_export_sets + + def _provided_has_responsibilities(self, provided_uuid: uuid.UUID) -> bool: + """Return whether a provided UUID has responsibilities.""" + return provided_uuid in self._responsibility_by_provided diff --git a/trestle/core/crm/export_writer.py b/trestle/core/crm/export_writer.py new file mode 100644 index 000000000..e8b397716 --- /dev/null +++ b/trestle/core/crm/export_writer.py @@ -0,0 +1,117 @@ +# Copyright (c) 2021 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Provided interface to write provided and responsibility exports statements to Markdown.""" + +import logging +import pathlib +import uuid +from typing import Dict + +import trestle.common.const as const +import trestle.oscal.ssp as ossp +from trestle.common.err import TrestleError +from trestle.common.list_utils import as_list +from trestle.core.crm.export_interface import ExportInterface +from trestle.core.inheritance_writer import ( + LeveragedStatements, + StatementProvided, + StatementResponsibility, + StatementTree, +) + +logger = logging.getLogger(__name__) + + +class ExportWriter: + """ + Exported writer. + + Export writer handles all operation related writing provided and responsibility exported statements + to Markdown. + """ + + def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): + """ + Initialize export writer. + + Arguments: + root_path: A root path object where all markdown files and directories should be written. + ssp: A system security plan with exports + """ + self._ssp = ssp + self._root_path = root_path + + def write_exports_as_markdown(self) -> None: + """Write export statement for leveraged SSP as the inheritance Markdown view.""" + # Find all the components and create paths for name + paths_by_comp: Dict[uuid.UUD, pathlib.Path] = {} + for component in as_list(self._ssp.system_implementation.components): + paths_by_comp[component.uuid] = self._root_path.joinpath(component.title) + + # Process all information under exports in control implementation + for implemented_requirement in as_list(self._ssp.control_implementation.implemented_requirements): + for by_comp in as_list(implemented_requirement.by_components): + try: + comp_markdown_path: pathlib.Path = paths_by_comp[by_comp.component_uuid] + self._process_by_component(by_comp, comp_markdown_path, implemented_requirement.control_id) + except KeyError: + raise TrestleError(f'Component id {by_comp.component_uuid} is not in the system implementation') + + for stm in as_list(implemented_requirement.statements): + statement_id = getattr(stm, 'statement_id', f'{implemented_requirement.control_id}_smt') + for by_comp in stm.by_components: + try: + comp_markdown_path: pathlib.Path = paths_by_comp[by_comp.component_uuid] + self._process_by_component(by_comp, comp_markdown_path, statement_id) + except KeyError: + raise TrestleError(f'Component id {by_comp.uuid} is not in the system implementation') + + def _process_by_component(self, by_comp: ossp.ByComponent, comp_path: pathlib.Path, control_id: str) -> None: + """Complete the Markdown writing operations for each by-component assembly.""" + export_interface: ExportInterface = ExportInterface(by_comp=by_comp) + + leveraged_statements: Dict[str, LeveragedStatements] = self._statement_types_from_exports(export_interface) + + # Only create the directory if leveraged statements exist. If not return. + if not leveraged_statements: + logger.debug(f'Component {by_comp.component_uuid} has no exports for control {control_id}') + return + + control_path: pathlib.Path = comp_path.joinpath(control_id) + control_path.mkdir(exist_ok=True, parents=True) + + for statement_file_path, leveraged_stm in leveraged_statements.items(): + statement_path: pathlib.Path = control_path.joinpath(f'{statement_file_path}{const.MARKDOWN_FILE_EXT}') + leveraged_stm.write_statement_md(statement_path) + + def _statement_types_from_exports(self, export_interface: ExportInterface) -> Dict[str, LeveragedStatements]: + """Process all exports and return a file basename and LeveragedStatement object for each.""" + all_statements: Dict[str, LeveragedStatements] = {} + + for responsibility in export_interface.get_isolated_responsibilities(): + responsibility_stm = StatementResponsibility(responsibility.uuid, responsibility.description) + all_statements[responsibility.uuid] = responsibility_stm + + for provided in export_interface.get_isolated_provided(): + provided_stm = StatementProvided(provided.uuid, provided.description) + all_statements[provided.uuid] = provided_stm + + for responsibility, provided in export_interface.get_export_sets(): + set_stm = StatementTree( + provided.uuid, provided.description, responsibility.uuid, responsibility.description + ) + path = f'{provided.uuid}_{responsibility.uuid}' + all_statements[path] = set_stm + + return all_statements diff --git a/trestle/core/inheritance_writer.py b/trestle/core/inheritance_writer.py new file mode 100644 index 000000000..baf3bdf1e --- /dev/null +++ b/trestle/core/inheritance_writer.py @@ -0,0 +1,129 @@ +# Copyright (c) 2022 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Handle writing of inherited statements to markdown.""" +import logging +import pathlib +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional + +import trestle.common.const as const +from trestle.core.markdown.md_writer import MDWriter + +logger = logging.getLogger(__name__) + + +class LeveragedStatements(ABC): + """Abstract class for managing leveraged statements.""" + + def __init__(self): + """Initialize the class.""" + self._md_file: Optional[MDWriter] = None + self.header_comment_dict: Dict[str, str] = {} + self.header_comment_dict[const.TRESTLE_LEVERAGING_COMP_TAG] = const.YAML_LEVERAGING_COMP_COMMENT + self.header_comment_dict[const.TRESTLE_STATEMENT_TAG] = const.YAML_LEVERAGED_COMMENT + self.merged_header_dict: Dict[str, Any] = {} + self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = '' + self.merged_header_dict[const.TRESTLE_LEVERAGING_COMP_TAG] = {const.NAME: const.REPLACE_ME} + + @abstractmethod + def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Write inheritance information to a single markdown file.""" + + +class StatementTree(LeveragedStatements): + """Concrete class for managing provided and responsibility statements.""" + + def __init__( + self, + provided_uuid: str, + provided_description: str, + responsibility_uuid: str, + responsibility_description: str, + ): + """Initialize the class.""" + self.provided_uuid = provided_uuid + self.provided_description = provided_description + self.responsibility_uuid = responsibility_uuid + self.responsibility_description = responsibility_description + self.satisfied_description = const.REPLACE_ME + super().__init__() + + def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Write a provided and responsibility statements to a markdown file.""" + self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) + + statement_dict: Dict[str, str] = {} + statement_dict[const.PROVIDED_UUID] = self.provided_uuid + statement_dict[const.RESPONSIBILITY_UUID] = self.responsibility_uuid + + self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = statement_dict + self._md_file.add_yaml_header(self.merged_header_dict) + + self._md_file.new_header(level=1, title=const.PROVIDED_STATEMENT_DESCRIPTION) + self._md_file.new_line(self.provided_description) + self._md_file.new_header(level=1, title=const.RESPONSIBILITY_STATEMENT_DESCRIPTION) + self._md_file.new_line(self.responsibility_description) + self._md_file.new_header(level=1, title=const.SATISFIED_STATEMENT_DESCRIPTION) + self._md_file.new_line(const.SATISFIED_STATEMENT_COMMENT) + self._md_file.new_line(self.satisfied_description) + + self._md_file.write_out() + + +class StatementProvided(LeveragedStatements): + """Concrete class for managing provided statements.""" + + def __init__(self, provided_uuid: str, provided_description: str): + """Initialize the class.""" + self.provided_uuid = provided_uuid + self.provided_description = provided_description + super().__init__() + + def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Write provided statements to a markdown file.""" + self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) + + self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = {const.PROVIDED_UUID: self.provided_uuid} + self._md_file.add_yaml_header(self.merged_header_dict) + + self._md_file.new_header(level=1, title=const.PROVIDED_STATEMENT_DESCRIPTION) + self._md_file.new_line(self.provided_description) + self._md_file.write_out() + + +class StatementResponsibility(LeveragedStatements): + """Concrete class for managing responsibility statements.""" + + def __init__(self, responsibility_uuid: str, responsibility_description: str): + """Initialize the class.""" + self.responsibility_uuid = responsibility_uuid + self.responsibility_description = responsibility_description + self.satisfied_description = const.REPLACE_ME + + super().__init__() + + def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Write responsibility statements to a markdown file.""" + self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) + + self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = {const.RESPONSIBILITY_UUID: self.responsibility_uuid} + self._md_file.add_yaml_header(self.merged_header_dict) + + self._md_file.new_header(level=1, title=const.RESPONSIBILITY_STATEMENT_DESCRIPTION) + self._md_file.new_line(self.responsibility_description) + self._md_file.new_header(level=1, title=const.SATISFIED_STATEMENT_DESCRIPTION) + self._md_file.new_line(const.SATISFIED_STATEMENT_COMMENT) + self._md_file.new_line(self.satisfied_description) + + self._md_file.write_out() From c5d6f4d26fc13096b902b1410e701e6ea979985d Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Tue, 1 Aug 2023 18:38:10 -0400 Subject: [PATCH 02/24] feat: adds InheritanceMarkdownReader for reading leveraged statement markdown Adds InheritanceMarkdownReader for processing into a leveraging SSP context Adds persistance for components and satisified statements during updates Changes leveraging component from a single dictionary to a list Signed-off-by: Jennifer Power --- .../trestle/core/commands/author/ssp_test.py | 12 +- tests/trestle/core/crm/exports_writer_test.py | 2 +- .../core/crm/leveraged_statements_test.py | 272 ++++++++++++++++ tests/trestle/core/inheritance_writer_test.py | 100 ------ trestle/core/crm/export_interface.py | 6 +- trestle/core/crm/export_writer.py | 26 +- trestle/core/crm/leveraged_statements.py | 302 ++++++++++++++++++ trestle/core/inheritance_writer.py | 129 -------- 8 files changed, 599 insertions(+), 250 deletions(-) create mode 100644 tests/trestle/core/crm/leveraged_statements_test.py delete mode 100644 tests/trestle/core/inheritance_writer_test.py create mode 100644 trestle/core/crm/leveraged_statements.py delete mode 100644 trestle/core/inheritance_writer.py diff --git a/tests/trestle/core/commands/author/ssp_test.py b/tests/trestle/core/commands/author/ssp_test.py index a3d9e3fa2..84b6bc568 100644 --- a/tests/trestle/core/commands/author/ssp_test.py +++ b/tests/trestle/core/commands/author/ssp_test.py @@ -276,7 +276,9 @@ def test_ssp_generate_with_inheritance(tmp_trestle_dir: pathlib.Path) -> None: md_api = MarkdownAPI() header, tree = md_api.processor.process_markdown(test_provided) assert tree is not None - assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'REPLACE_ME'}] assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == expected_uuid expected_provided = """# Provided Statement Description @@ -298,7 +300,9 @@ def test_ssp_generate_with_inheritance(tmp_trestle_dir: pathlib.Path) -> None: md_api = MarkdownAPI() header, tree = md_api.processor.process_markdown(test_provided) assert tree is not None - assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'REPLACE_ME'}] assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == expected_uuid expected_responsibility = """# Responsibility Statement Description @@ -325,7 +329,9 @@ def test_ssp_generate_with_inheritance(tmp_trestle_dir: pathlib.Path) -> None: md_api = MarkdownAPI() header, tree = md_api.processor.process_markdown(test_provided) assert tree is not None - assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' + + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'REPLACE_ME'}] assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == expected_provided_uuid assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == expected_responsibility_uuid diff --git a/tests/trestle/core/crm/exports_writer_test.py b/tests/trestle/core/crm/exports_writer_test.py index 5394f3a54..fbf010b8d 100644 --- a/tests/trestle/core/crm/exports_writer_test.py +++ b/tests/trestle/core/crm/exports_writer_test.py @@ -29,7 +29,7 @@ from trestle.common.model_utils import ModelUtils from trestle.core.crm.export_interface import ExportInterface from trestle.core.crm.export_writer import ExportWriter -from trestle.core.inheritance_writer import ( +from trestle.core.crm.leveraged_statements import ( LeveragedStatements, StatementProvided, StatementResponsibility, diff --git a/tests/trestle/core/crm/leveraged_statements_test.py b/tests/trestle/core/crm/leveraged_statements_test.py new file mode 100644 index 000000000..253e5ab6a --- /dev/null +++ b/tests/trestle/core/crm/leveraged_statements_test.py @@ -0,0 +1,272 @@ +# -*- mode:python; coding:utf-8 -*- + +# Copyright (c) 2021 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Tests for control input output methods.""" + +import pathlib +from typing import Any, Dict + +import trestle.common.const as const +from trestle.core.crm.leveraged_statements import ( + InheritanceMarkdownReader, StatementProvided, StatementResponsibility, StatementTree +) +from trestle.core.markdown.markdown_api import MarkdownAPI +from trestle.core.markdown.md_writer import MDWriter + +provided_uuid = '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' +provided_statement_desc = 'provided statement description' +resp_uuid = '4b34c68f-75fa-4b38-baf0-e50158c13ac2' +resp_statement_desc = 'resp statement description' +satisfied_statement_desc = 'satisfied statement description' + + +def add_authored_content(test_file: pathlib.Path, yaml_header: Dict[str, Any]) -> None: + """Update the yaml header with a test component and satisfied description to simulate editing.""" + md_writer = MDWriter(test_file) + yaml_header[const.TRESTLE_LEVERAGING_COMP_TAG] = [{'name': 'My_Comp'}] + md_writer.add_yaml_header(yaml_header) + md_writer.new_header(level=1, title=const.SATISFIED_STATEMENT_DESCRIPTION) + md_writer.new_line(const.SATISFIED_STATEMENT_COMMENT) + md_writer.new_line('My Satisfied Description') + md_writer.write_out() + + +def test_write_inheritance_tree(tmp_path: pathlib.Path) -> None: + """Test writing statements with both provided and responsibility.""" + statement_tree_path = tmp_path.joinpath('statement_tree.md') + + statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc) + + statement.write_statement_md(statement_tree_path) + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_tree_path) + assert tree is not None + + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'REPLACE_ME'}] + + assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == provided_uuid + assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == resp_uuid + + # Confirm markdown content + node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == '# Provided Statement Description\n\nprovided statement description\n' + node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == '# Responsibility Statement Description\n\nresp statement description\n' + node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == ( + '# Satisfied Statement Description\n\n' + ) + + # Update the component mapping and run again to make sure it persists + add_authored_content(statement_tree_path, header) + + statement.write_statement_md(statement_tree_path) + + # Reread the Markdown + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_tree_path) + + # Ensure My_Comp and satisfied description persists + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'My_Comp'}] + node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == """# Satisfied Statement Description\n +\nMy Satisfied Description""" + + +def test_write_inheritance_provided(tmp_path: pathlib.Path) -> None: + """Test writing statements with only provided.""" + statement_provided_path = tmp_path.joinpath('statement_provided.md') + + statement = StatementProvided(provided_uuid, provided_statement_desc) + + statement.write_statement_md(statement_provided_path) + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_provided_path) + assert tree is not None + + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'REPLACE_ME'}] + + assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == provided_uuid + + # Confirm markdown content + node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == '# Provided Statement Description\n\nprovided statement description' + + # Update the component mapping and run again to make sure it persists + add_authored_content(statement_provided_path, header) + + statement.write_statement_md(statement_provided_path) + + # Reread the markdown + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_provided_path) + + # Ensure My_Comp persists + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'My_Comp'}] + node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) + assert node is None + + +def test_write_inheritance_responsibility(tmp_path: pathlib.Path) -> None: + """Test writing statements with only responsibility.""" + statement_resp_path = tmp_path.joinpath('statement_req.md') + + statement = StatementResponsibility(resp_uuid, resp_statement_desc) + + statement.write_statement_md(statement_resp_path) + + # confirm content in yaml header + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_resp_path) + assert tree is not None + + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'REPLACE_ME'}] + + assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == resp_uuid + + # Confirm markdown content + node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == '# Responsibility Statement Description\n\nresp statement description\n' + node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == """# Satisfied Statement Description\n +""" + + # Update the component mapping and run again to make sure it persists + add_authored_content(statement_resp_path, header) + + statement.write_statement_md(statement_resp_path) + + # Reread the Markdown + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(statement_resp_path) + + # Ensure My_Comp and satisfied description persists + comp_header_value = header[const.TRESTLE_LEVERAGING_COMP_TAG] + assert comp_header_value == [{'name': 'My_Comp'}] + node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) + assert node.content.raw_text == """# Satisfied Statement Description\n +\nMy Satisfied Description""" + + +def test_process_leveraged_statement_default_mapping(tmp_path: pathlib.Path) -> None: + """Test processing leveraged statement markdown with no set mapping.""" + statement_tree_path = tmp_path.joinpath('statement_tree.md') + + statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc) + + statement.write_statement_md(statement_tree_path) + + md_reader: InheritanceMarkdownReader = InheritanceMarkdownReader(statement_tree_path) + + leveraging_information = md_reader.process_leveraged_statement_markdown() + + assert leveraging_information is None + + +def test_process_leveraged_statement_markdown_tree(tmp_path: pathlib.Path) -> None: + """Test processing a statement tree in Markdown.""" + statement_tree_path = tmp_path.joinpath('statement_tree.md') + + # Add test mapped component + test_header: Dict[str, Any] = {} + add_authored_content(statement_tree_path, test_header) + + statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc) + + statement.write_statement_md(statement_tree_path) + + md_reader: InheritanceMarkdownReader = InheritanceMarkdownReader(statement_tree_path) + + leveraging_information = md_reader.process_leveraged_statement_markdown() + + assert leveraging_information + + assert 'My_Comp' in leveraging_information.leveraging_comp_titles + + assert leveraging_information.inherited is not None + inherited = leveraging_information.inherited + assert inherited.provided_uuid == provided_uuid + assert inherited.description == provided_statement_desc + + assert leveraging_information.satisfied is not None + satisfied = leveraging_information.satisfied + assert satisfied.responsibility_uuid == resp_uuid + assert satisfied.description == 'My Satisfied Description' + + +def test_process_leveraged_statement_markdown_provided(tmp_path: pathlib.Path) -> None: + """Test processing a statement provided markdown.""" + statement_provided_path = tmp_path.joinpath('statement_provided.md') + + # Add test mapped component + test_header: Dict[str, Any] = {} + add_authored_content(statement_provided_path, test_header) + + statement = StatementProvided(provided_uuid, provided_statement_desc) + + statement.write_statement_md(statement_provided_path) + + md_reader: InheritanceMarkdownReader = InheritanceMarkdownReader(statement_provided_path) + + leveraging_information = md_reader.process_leveraged_statement_markdown() + + assert leveraging_information + + assert 'My_Comp' in leveraging_information.leveraging_comp_titles + + assert leveraging_information.inherited is not None + inherited = leveraging_information.inherited + assert inherited.provided_uuid == provided_uuid + assert inherited.description == provided_statement_desc + + assert leveraging_information.satisfied is None + + +def test_process_leveraged_statement_markdown_responsibility(tmp_path: pathlib.Path) -> None: + """Test processing a statement responsibility Markdown.""" + statement_resp_path = tmp_path.joinpath('statement_req.md') + + # Add test mapped component + test_header: Dict[str, Any] = {} + add_authored_content(statement_resp_path, test_header) + + statement = StatementResponsibility(resp_uuid, resp_statement_desc) + + statement.write_statement_md(statement_resp_path) + + md_reader: InheritanceMarkdownReader = InheritanceMarkdownReader(statement_resp_path) + + leveraging_information = md_reader.process_leveraged_statement_markdown() + + assert leveraging_information + + assert 'My_Comp' in leveraging_information.leveraging_comp_titles + + assert leveraging_information.inherited is None + + assert leveraging_information.satisfied is not None + satisfied = leveraging_information.satisfied + assert satisfied.responsibility_uuid == resp_uuid + assert satisfied.description == 'My Satisfied Description' diff --git a/tests/trestle/core/inheritance_writer_test.py b/tests/trestle/core/inheritance_writer_test.py deleted file mode 100644 index dec235217..000000000 --- a/tests/trestle/core/inheritance_writer_test.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- mode:python; coding:utf-8 -*- - -# Copyright (c) 2021 IBM Corp. All rights reserved. -# -# 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 -# -# https://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. -"""Tests for control input output methods.""" - -import pathlib - -import trestle.common.const as const -import trestle.core.inheritance_writer as inheritancewriter -from trestle.core.markdown.markdown_api import MarkdownAPI - -provided_uuid = '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' -provided_statement_desc = 'provided statement description' -resp_uuid = '4b34c68f-75fa-4b38-baf0-e50158c13ac2' -resp_statement_desc = 'resp statement description' -satisfied_statement_desc = 'satisfied statement description' - - -def test_write_inheritance_tree(tmp_path: pathlib.Path) -> None: - """Test writing statements with both provided and responsibility.""" - statement_tree_path = tmp_path.joinpath('statement_tree.md') - - statement = inheritancewriter.StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc) - - statement.write_statement_md(statement_tree_path) - - # confirm content in yaml header - md_api = MarkdownAPI() - header, tree = md_api.processor.process_markdown(statement_tree_path) - assert tree is not None - assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' - assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == provided_uuid - assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == resp_uuid - - # Confirm markdown content - node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) - assert node.content.raw_text == '# Provided Statement Description\n\nprovided statement description\n' - node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) - assert node.content.raw_text == '# Responsibility Statement Description\n\nresp statement description\n' - node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) - assert node.content.raw_text == ( - '# Satisfied Statement Description\n\n\nREPLACE_ME' - ) - - -def test_write_inheritance_provided(tmp_path: pathlib.Path) -> None: - """Test writing statements with only provided.""" - statement_provided_path = tmp_path.joinpath('statement_provided.md') - - statement = inheritancewriter.StatementProvided(provided_uuid, provided_statement_desc) - - statement.write_statement_md(statement_provided_path) - - # confirm content in yaml header - md_api = MarkdownAPI() - header, tree = md_api.processor.process_markdown(statement_provided_path) - assert tree is not None - assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' - assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == provided_uuid - - # Confirm markdown content - node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) - assert node.content.raw_text == '# Provided Statement Description\n\nprovided statement description' - - -def test_write_inheritance_responsibility(tmp_path: pathlib.Path) -> None: - """Test writing statements with only responsibility.""" - statement_resp_path = tmp_path.joinpath('statement_req.md') - - statement = inheritancewriter.StatementResponsibility(resp_uuid, resp_statement_desc) - - statement.write_statement_md(statement_resp_path) - - # confirm content in yaml header - md_api = MarkdownAPI() - header, tree = md_api.processor.process_markdown(statement_resp_path) - assert tree is not None - assert header[const.TRESTLE_LEVERAGING_COMP_TAG]['name'] == 'REPLACE_ME' - assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == resp_uuid - - # Confirm markdown content - node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) - assert node.content.raw_text == '# Responsibility Statement Description\n\nresp statement description\n' - node = tree.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) - assert node.content.raw_text == """# Satisfied Statement Description\n - -REPLACE_ME""" diff --git a/trestle/core/crm/export_interface.py b/trestle/core/crm/export_interface.py index 85af37c8b..855b9f829 100644 --- a/trestle/core/crm/export_interface.py +++ b/trestle/core/crm/export_interface.py @@ -28,7 +28,7 @@ class ExportInterface: """ Interface to query exported provided and responsibility statements from. - The by-component export statement is parse and the responsibility and provided statement + The by-component export statement is parsed and the responsibility and provided statements are separated into three catagories: isolated responsibilities - A responsibility with no provided statement @@ -56,7 +56,7 @@ def _create_provided_dict(self) -> Dict[uuid.UUID, ossp.Provided]: return provided_dict def _create_responsibility_dict(self) -> Dict[uuid.UUID, ossp.Responsibility]: - responsibility_dict: Dict[uuid.UUID, ossp.Provided] = {} + responsibility_dict: Dict[uuid.UUID, ossp.Responsibility] = {} for responsibility in as_list(self._by_comp.export.responsibilities): responsibility_dict[responsibility.uuid] = responsibility return responsibility_dict @@ -82,7 +82,7 @@ def get_isolated_responsibilities(self) -> List[ossp.Responsibility]: all_responsibilities.append(resp) return all_responsibilities - def get_isolated_provided(self) -> List[ossp.Responsibility]: + def get_isolated_provided(self) -> List[ossp.Provided]: """Return all isolated exported provided capabilities.""" all_provided: List[ossp.Provided] = [] for provided in as_dict(self._provided_dict).values(): diff --git a/trestle/core/crm/export_writer.py b/trestle/core/crm/export_writer.py index e8b397716..82a3b4da6 100644 --- a/trestle/core/crm/export_writer.py +++ b/trestle/core/crm/export_writer.py @@ -23,7 +23,7 @@ from trestle.common.err import TrestleError from trestle.common.list_utils import as_list from trestle.core.crm.export_interface import ExportInterface -from trestle.core.inheritance_writer import ( +from trestle.core.crm.leveraged_statements import ( LeveragedStatements, StatementProvided, StatementResponsibility, @@ -35,9 +35,9 @@ class ExportWriter: """ - Exported writer. + By-Component Assembly Exports writer. - Export writer handles all operation related writing provided and responsibility exported statements + Export writer handles all operations related to writing provided and responsibility exported statements to Markdown. """ @@ -49,13 +49,13 @@ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): root_path: A root path object where all markdown files and directories should be written. ssp: A system security plan with exports """ - self._ssp = ssp - self._root_path = root_path + self._ssp: ossp.SystemSecurityPlan = ssp + self._root_path: pathlib.Path = root_path def write_exports_as_markdown(self) -> None: """Write export statement for leveraged SSP as the inheritance Markdown view.""" # Find all the components and create paths for name - paths_by_comp: Dict[uuid.UUD, pathlib.Path] = {} + paths_by_comp: Dict[uuid.UUID, pathlib.Path] = {} for component in as_list(self._ssp.system_implementation.components): paths_by_comp[component.uuid] = self._root_path.joinpath(component.title) @@ -70,7 +70,7 @@ def write_exports_as_markdown(self) -> None: for stm in as_list(implemented_requirement.statements): statement_id = getattr(stm, 'statement_id', f'{implemented_requirement.control_id}_smt') - for by_comp in stm.by_components: + for by_comp in as_list(stm.by_components): try: comp_markdown_path: pathlib.Path = paths_by_comp[by_comp.component_uuid] self._process_by_component(by_comp, comp_markdown_path, statement_id) @@ -100,18 +100,16 @@ def _statement_types_from_exports(self, export_interface: ExportInterface) -> Di all_statements: Dict[str, LeveragedStatements] = {} for responsibility in export_interface.get_isolated_responsibilities(): - responsibility_stm = StatementResponsibility(responsibility.uuid, responsibility.description) - all_statements[responsibility.uuid] = responsibility_stm + all_statements[responsibility.uuid + ] = StatementResponsibility(responsibility.uuid, responsibility.description) for provided in export_interface.get_isolated_provided(): - provided_stm = StatementProvided(provided.uuid, provided.description) - all_statements[provided.uuid] = provided_stm + all_statements[provided.uuid] = StatementProvided(provided.uuid, provided.description) for responsibility, provided in export_interface.get_export_sets(): - set_stm = StatementTree( + path = f'{provided.uuid}_{responsibility.uuid}' + all_statements[path] = StatementTree( provided.uuid, provided.description, responsibility.uuid, responsibility.description ) - path = f'{provided.uuid}_{responsibility.uuid}' - all_statements[path] = set_stm return all_statements diff --git a/trestle/core/crm/leveraged_statements.py b/trestle/core/crm/leveraged_statements.py new file mode 100644 index 000000000..495e1b6dd --- /dev/null +++ b/trestle/core/crm/leveraged_statements.py @@ -0,0 +1,302 @@ +# Copyright (c) 2022 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Handle writing of inherited statements to markdown.""" +import logging +import pathlib +import re +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + +import trestle.common.const as const +import trestle.core.generators as gens +import trestle.oscal.ssp as ssp +from trestle.common.err import TrestleError +from trestle.core.markdown.docs_markdown_node import DocsMarkdownNode +from trestle.core.markdown.markdown_api import MarkdownAPI +from trestle.core.markdown.md_writer import MDWriter + +logger = logging.getLogger(__name__) + +component_mapping_default: List[Dict[str, str]] = [{'name': const.REPLACE_ME}] + + +class LeveragedStatements(ABC): + """Abstract class for managing leveraged statements.""" + + def __init__(self) -> None: + """Initialize the class.""" + self._md_file: Optional[MDWriter] = None + self.header_comment_dict: Dict[str, str] = { + const.TRESTLE_LEVERAGING_COMP_TAG: const.YAML_LEVERAGING_COMP_COMMENT, + const.TRESTLE_STATEMENT_TAG: const.YAML_LEVERAGED_COMMENT + } + self.merged_header_dict: Dict[str, Any] = { + const.TRESTLE_STATEMENT_TAG: '', const.TRESTLE_LEVERAGING_COMP_TAG: component_mapping_default + } + + @abstractmethod + def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Write inheritance information to a single markdown file.""" + + +class StatementTree(LeveragedStatements): + """Concrete class for managing provided and responsibility statements.""" + + def __init__( + self, provided_uuid: str, provided_description: str, responsibility_uuid: str, responsibility_description: str + ): + """Initialize the class.""" + self.provided_uuid = provided_uuid + self.provided_description = provided_description + self.responsibility_uuid = responsibility_uuid + self.responsibility_description = responsibility_description + self.satisfied_description = '' + super().__init__() + + def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Write a provided and responsibility statements to a markdown file.""" + self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) + + if self._md_file.exists(): + return self.update_statement_md(leveraged_statement_file) + + self._add_generated_content() + self._md_file.write_out() + + def update_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Update provided and responsibility statements in a markdown file.""" + md_reader = InheritanceMarkdownReader(leveraged_statement_file) + + self.merged_header_dict[const.TRESTLE_LEVERAGING_COMP_TAG] = md_reader.get_leveraged_component_header_value() + + satisfied_description = md_reader.get_satisfied_description() + if satisfied_description is not None: + self.satisfied_description = satisfied_description + + self._add_generated_content() + self._md_file.write_out() + + def _add_generated_content(self) -> None: + statement_dict: Dict[str, str] = { + const.PROVIDED_UUID: self.provided_uuid, const.RESPONSIBILITY_UUID: self.responsibility_uuid + } + + self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = statement_dict + self._md_file.add_yaml_header(self.merged_header_dict) + + self._md_file.new_header(level=1, title=const.PROVIDED_STATEMENT_DESCRIPTION) + self._md_file.new_line(self.provided_description) + self._md_file.new_header(level=1, title=const.RESPONSIBILITY_STATEMENT_DESCRIPTION) + self._md_file.new_line(self.responsibility_description) + self._md_file.new_header(level=1, title=const.SATISFIED_STATEMENT_DESCRIPTION) + self._md_file.new_line(const.SATISFIED_STATEMENT_COMMENT) + self._md_file.new_line(self.satisfied_description) + + +class StatementProvided(LeveragedStatements): + """Concrete class for managing provided statements.""" + + def __init__(self, provided_uuid: str, provided_description: str): + """Initialize the class.""" + self.provided_uuid = provided_uuid + self.provided_description = provided_description + super().__init__() + + def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Write provided statements to a markdown file.""" + self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) + + if self._md_file.exists(): + return self.update_statement_md(leveraged_statement_file) + + self._add_generated_content() + self._md_file.write_out() + + def update_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Update provided statements in a markdown file.""" + md_reader = InheritanceMarkdownReader(leveraged_statement_file) + + self.merged_header_dict[const.TRESTLE_LEVERAGING_COMP_TAG] = md_reader.get_leveraged_component_header_value() + + self._add_generated_content() + self._md_file.write_out() + + def _add_generated_content(self) -> None: + self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = {const.PROVIDED_UUID: self.provided_uuid} + self._md_file.add_yaml_header(self.merged_header_dict) + + self._md_file.new_header(level=1, title=const.PROVIDED_STATEMENT_DESCRIPTION) + self._md_file.new_line(self.provided_description) + + +class StatementResponsibility(LeveragedStatements): + """Concrete class for managing responsibility statements.""" + + def __init__(self, responsibility_uuid: str, responsibility_description: str): + """Initialize the class.""" + self.responsibility_uuid = responsibility_uuid + self.responsibility_description = responsibility_description + self.satisfied_description = '' + super().__init__() + + def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Write responsibility statements to a markdown file.""" + self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) + + if self._md_file.exists(): + return self.update_statement_md(leveraged_statement_file) + + self._add_generated_content() + self._md_file.write_out() + + def update_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: + """Update responsibility statements in a markdown file.""" + md_reader = InheritanceMarkdownReader(leveraged_statement_file) + + self.merged_header_dict[const.TRESTLE_LEVERAGING_COMP_TAG] = md_reader.get_leveraged_component_header_value() + + satisfied_description = md_reader.get_satisfied_description() + if satisfied_description is not None: + self.satisfied_description = satisfied_description + + self._add_generated_content() + self._md_file.write_out() + + def _add_generated_content(self) -> None: + self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = {const.RESPONSIBILITY_UUID: self.responsibility_uuid} + self._md_file.add_yaml_header(self.merged_header_dict) + + self._md_file.new_header(level=1, title=const.RESPONSIBILITY_STATEMENT_DESCRIPTION) + self._md_file.new_line(self.responsibility_description) + self._md_file.new_header(level=1, title=const.SATISFIED_STATEMENT_DESCRIPTION) + self._md_file.new_line(const.SATISFIED_STATEMENT_COMMENT) + self._md_file.new_line(self.satisfied_description) + + +@dataclass +class InheritanceInfo: + """Class to capture component inheritance information.""" + + leveraging_comp_titles: List[str] + inherited: Optional[ssp.Inherited] + satisfied: Optional[ssp.Satisfied] + + +class InheritanceMarkdownReader: + """Class to read leveraged statement information from Markdown.""" + + def __init__(self, leveraged_statement_file: str) -> None: + """Initialize the class.""" + # Save the file name for logging + self._leveraged_statement_file = leveraged_statement_file + + md_api: MarkdownAPI = MarkdownAPI() + + yaml_header, inheritance_md = md_api.processor.process_markdown(leveraged_statement_file) + self._yaml_header: Dict[str, Any] = yaml_header + self._inheritance_md: DocsMarkdownNode = inheritance_md + + def process_leveraged_statement_markdown(self) -> Optional[InheritanceInfo]: + """ + Read inheritance information from Markdown. + + Returns: + Optional InheritanceInfo: A list of mapped component titles, an optional satisfied statement and an optional + inherited statement + + Notes: + Returns inheritance information in the context of the leveraging SSP. If no leveraging component titles are + mapped in the yaml header None will be returned. The satisfied and inherited fields are + generated and returned to added information to by-component assemblies for + the mapped leveraging components. + + """ + leveraging_comps: List[str] = [] + inherited_statement: Optional[ssp.Inherited] = None + satisfied_statement: Optional[ssp.Satisfied] = None + + leveraging_comp_header_value: List[Dict[str, str]] = self._yaml_header[const.TRESTLE_LEVERAGING_COMP_TAG] + + # If there are no mapped components, return early + if not leveraging_comp_header_value or leveraging_comp_header_value == component_mapping_default: + return None + else: + for comp_dicts in leveraging_comp_header_value: + for comp_title in comp_dicts.values(): + leveraging_comps.append(comp_title) + + statement_info: Dict[str, str] = self._yaml_header[const.TRESTLE_STATEMENT_TAG] + + if const.PROVIDED_UUID in statement_info: + # Set inherited + + provided_description = self.get_provided_description() + if provided_description is None: + raise TrestleError(f'Provided statement cannot be empty in {self._leveraged_statement_file}') + + inherited_statement = gens.generate_sample_model(ssp.Inherited) + + inherited_statement.description = provided_description + inherited_statement.provided_uuid = statement_info[const.PROVIDED_UUID] + + if const.RESPONSIBILITY_UUID in statement_info: + # Set satisfied + satisfied_description = self.get_satisfied_description() + if satisfied_description is None: + raise TrestleError(f'Satisfied statement cannot be empty in {self._leveraged_statement_file}') + + satisfied_statement = gens.generate_sample_model(ssp.Satisfied) + + satisfied_statement.description = satisfied_description + satisfied_statement.responsibility_uuid = statement_info[const.RESPONSIBILITY_UUID] + + return InheritanceInfo(leveraging_comps, inherited_statement, satisfied_statement) + + def get_satisfied_description(self) -> Optional[str]: + """Return the satisfied description in the Markdown.""" + node = self._inheritance_md.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) + if node is not None: + return self.strip_heading_and_comments(node.content.raw_text) + else: + return None + + def get_provided_description(self) -> Optional[str]: + """Return the provided description in the Markdown.""" + node = self._inheritance_md.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) + if node is not None: + return self.strip_heading_and_comments(node.content.raw_text) + else: + return None + + def get_leveraged_component_header_value(self) -> Dict[str, str]: + """Provide the leveraged component value in the yaml header.""" + return self._yaml_header[const.TRESTLE_LEVERAGING_COMP_TAG] + + @staticmethod + def strip_heading_and_comments(markdown_text: str) -> str: + """Remove the heading and comments from lines to get the multi-line paragraph.""" + heading_pattern = r'^#+.*$' + comment_pattern = r'' + + # Remove headings and comments + markdown_text = re.sub(heading_pattern, '', markdown_text, flags=re.MULTILINE) + markdown_text = re.sub(comment_pattern, '', markdown_text, flags=re.DOTALL) + + markdown_text = '\n'.join(line.strip() for line in markdown_text.splitlines()) + + # Remove consecutive empty lines + markdown_text = re.sub(r'\n{2,}', '\n\n', markdown_text) + + return markdown_text.strip() diff --git a/trestle/core/inheritance_writer.py b/trestle/core/inheritance_writer.py deleted file mode 100644 index baf3bdf1e..000000000 --- a/trestle/core/inheritance_writer.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) 2022 IBM Corp. All rights reserved. -# -# 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 -# -# https://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. -"""Handle writing of inherited statements to markdown.""" -import logging -import pathlib -from abc import ABC, abstractmethod -from typing import Any, Dict, Optional - -import trestle.common.const as const -from trestle.core.markdown.md_writer import MDWriter - -logger = logging.getLogger(__name__) - - -class LeveragedStatements(ABC): - """Abstract class for managing leveraged statements.""" - - def __init__(self): - """Initialize the class.""" - self._md_file: Optional[MDWriter] = None - self.header_comment_dict: Dict[str, str] = {} - self.header_comment_dict[const.TRESTLE_LEVERAGING_COMP_TAG] = const.YAML_LEVERAGING_COMP_COMMENT - self.header_comment_dict[const.TRESTLE_STATEMENT_TAG] = const.YAML_LEVERAGED_COMMENT - self.merged_header_dict: Dict[str, Any] = {} - self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = '' - self.merged_header_dict[const.TRESTLE_LEVERAGING_COMP_TAG] = {const.NAME: const.REPLACE_ME} - - @abstractmethod - def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: - """Write inheritance information to a single markdown file.""" - - -class StatementTree(LeveragedStatements): - """Concrete class for managing provided and responsibility statements.""" - - def __init__( - self, - provided_uuid: str, - provided_description: str, - responsibility_uuid: str, - responsibility_description: str, - ): - """Initialize the class.""" - self.provided_uuid = provided_uuid - self.provided_description = provided_description - self.responsibility_uuid = responsibility_uuid - self.responsibility_description = responsibility_description - self.satisfied_description = const.REPLACE_ME - super().__init__() - - def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: - """Write a provided and responsibility statements to a markdown file.""" - self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) - - statement_dict: Dict[str, str] = {} - statement_dict[const.PROVIDED_UUID] = self.provided_uuid - statement_dict[const.RESPONSIBILITY_UUID] = self.responsibility_uuid - - self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = statement_dict - self._md_file.add_yaml_header(self.merged_header_dict) - - self._md_file.new_header(level=1, title=const.PROVIDED_STATEMENT_DESCRIPTION) - self._md_file.new_line(self.provided_description) - self._md_file.new_header(level=1, title=const.RESPONSIBILITY_STATEMENT_DESCRIPTION) - self._md_file.new_line(self.responsibility_description) - self._md_file.new_header(level=1, title=const.SATISFIED_STATEMENT_DESCRIPTION) - self._md_file.new_line(const.SATISFIED_STATEMENT_COMMENT) - self._md_file.new_line(self.satisfied_description) - - self._md_file.write_out() - - -class StatementProvided(LeveragedStatements): - """Concrete class for managing provided statements.""" - - def __init__(self, provided_uuid: str, provided_description: str): - """Initialize the class.""" - self.provided_uuid = provided_uuid - self.provided_description = provided_description - super().__init__() - - def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: - """Write provided statements to a markdown file.""" - self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) - - self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = {const.PROVIDED_UUID: self.provided_uuid} - self._md_file.add_yaml_header(self.merged_header_dict) - - self._md_file.new_header(level=1, title=const.PROVIDED_STATEMENT_DESCRIPTION) - self._md_file.new_line(self.provided_description) - self._md_file.write_out() - - -class StatementResponsibility(LeveragedStatements): - """Concrete class for managing responsibility statements.""" - - def __init__(self, responsibility_uuid: str, responsibility_description: str): - """Initialize the class.""" - self.responsibility_uuid = responsibility_uuid - self.responsibility_description = responsibility_description - self.satisfied_description = const.REPLACE_ME - - super().__init__() - - def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: - """Write responsibility statements to a markdown file.""" - self._md_file = MDWriter(leveraged_statement_file, self.header_comment_dict) - - self.merged_header_dict[const.TRESTLE_STATEMENT_TAG] = {const.RESPONSIBILITY_UUID: self.responsibility_uuid} - self._md_file.add_yaml_header(self.merged_header_dict) - - self._md_file.new_header(level=1, title=const.RESPONSIBILITY_STATEMENT_DESCRIPTION) - self._md_file.new_line(self.responsibility_description) - self._md_file.new_header(level=1, title=const.SATISFIED_STATEMENT_DESCRIPTION) - self._md_file.new_line(const.SATISFIED_STATEMENT_COMMENT) - self._md_file.new_line(self.satisfied_description) - - self._md_file.write_out() From f59cf187eaab1ebc31d115fe96214025b7aea952 Mon Sep 17 00:00:00 2001 From: Alex Flom Date: Thu, 10 Aug 2023 13:23:12 -0600 Subject: [PATCH 03/24] feat: Adds reader class for inheritance markdown Adds ExportReader class Removes ExportInterface class Adds a single ByComponentInterface class to interact with the model in terms of inheritance Signed-off-by: Jennifer Power Co-authored-by: Jennifer Power --- tests/data/json/leveraging_ssp.json | 281 ++++++++++++++++++ .../trestle/core/commands/author/ssp_test.py | 24 ++ .../trestle/core/crm/bycomp_interface_test.py | 109 +++++++ .../core/crm/exports_interface_test.py | 70 ----- tests/trestle/core/crm/exports_reader_test.py | 199 +++++++++++++ tests/trestle/core/crm/exports_writer_test.py | 10 +- trestle/core/catalog/catalog_reader.py | 10 + trestle/core/commands/author/ssp.py | 8 + ...xport_interface.py => bycomp_interface.py} | 94 +++++- trestle/core/crm/export_reader.py | 186 ++++++++++++ trestle/core/crm/export_writer.py | 9 +- trestle/core/crm/leveraged_statements.py | 2 +- 12 files changed, 905 insertions(+), 97 deletions(-) create mode 100644 tests/data/json/leveraging_ssp.json create mode 100644 tests/trestle/core/crm/bycomp_interface_test.py delete mode 100644 tests/trestle/core/crm/exports_interface_test.py create mode 100644 tests/trestle/core/crm/exports_reader_test.py rename trestle/core/crm/{export_interface.py => bycomp_interface.py} (50%) create mode 100644 trestle/core/crm/export_reader.py diff --git a/tests/data/json/leveraging_ssp.json b/tests/data/json/leveraging_ssp.json new file mode 100644 index 000000000..e3bfb232c --- /dev/null +++ b/tests/data/json/leveraging_ssp.json @@ -0,0 +1,281 @@ +{ + "system-security-plan": { + "uuid": "bb9219b1-e51c-4680-abb0-616a43bbfbb1", + "metadata": { + "title": "Leveraging SaaS System Security Plan", + "last-modified": "2021-06-08T13:57:35.4515-04:00", + "version": "0.1", + "oscal-version": "1.0.0", + "roles": [ + { + "id": "admin", + "title": "Administrator" + } + ], + "parties": [ + { + "uuid": "22222222-0000-4000-9000-100000000001", + "type": "person" + }, + { + "uuid": "22222222-0000-4000-9000-100000000002", + "type": "person", + "remarks": "Leveraged Authorization POC" + } + ] + }, + "import-profile": { + "href": "trestle://profiles/simple_test_profile/profile.json" + }, + "system-characteristics": { + "system-ids": [ + { + "id": "saas_system_iaas_customer" + } + ], + "system-name": "Leveraging SaaS System", + "description": "An example of three customers leveraging an authorized SaaS, which is running on an authorized IaaS.\n\n```\n\nCust-A Cust-B Cust-C\n | | |\n +---------+---------+\n |\n +-------------------+\n | Leveraging SaaS |\n | this file |\n +-------------------+\n |\n |\n +-------------------+\n | Leveraged IaaS |\n +-------------------+\n \n```\n\nIn this example, the IaaS SSP specifies customer responsibilities for certain controls.\n\nThe SaaS must address these for the control to be fully satisfied.\n\nThe SaaS provider may either implement these directly or pass the responsibility on to their customers. Both may be necessary.\n\nFor any given control, the Leveraged IaaS SSP must describe:\n\n1. HOW the IaaS is directly satisfying the control\n1. WHAT responsibilities are left for the Leveraging SaaS (or their customers) to implement.\n\n\nFor any given control, the Leveraging SaaS SSP must describe:\n\n1. WHAT is being inherited from the underlying IaaS\n1. HOW the SaaS is directly satisfying the control.\n1. WHAT responsibilities are left for the SaaS customers to implement. (The SaaS customers are Cust-A, B and C)\n", + "security-sensitivity-level": "low", + "system-information": { + "information-types": [ + { + "title": "System and Network Monitoring", + "description": "This system handles information pertaining to audit events.", + "categorizations": [ + { + "system": "https://doi.org/10.6028/NIST.SP.800-60v2r1", + "information-type-ids": [ + "C.3.5.8" + ] + } + ], + "confidentiality-impact": { + "base": "fips-199-moderate", + "selected": "fips-199-low", + "adjustment-justification": "This impact has been adjusted to low as an example of how to perform this type of adjustment." + }, + "integrity-impact": { + "base": "fips-199-moderate", + "selected": "fips-199-low", + "adjustment-justification": "This impact has been adjusted to low as an example of how to perform this type of adjustment." + }, + "availability-impact": { + "base": "fips-199-moderate", + "selected": "fips-199-low", + "adjustment-justification": "This impact has been adjusted to low as an example of how to perform this type of adjustment." + } + } + ] + }, + "security-impact-level": { + "security-objective-confidentiality": "fips-199-low", + "security-objective-integrity": "fips-199-low", + "security-objective-availability": "fips-199-low" + }, + "status": { + "state": "operational" + }, + "authorization-boundary": { + "description": "The virtualized components deployed on the CSP IaaS." + }, + "remarks": "Most system-characteristics content does not support the example, and is included to meet the minimum SSP syntax requirements." + }, + "system-implementation": { + "leveraged-authorizations": [ + { + "uuid": "22222222-0000-4000-9000-300000000001", + "title": "CSP IaaS [Leveraged System]", + "links": [ + { + "href": "./oscal_leveraged-example_ssp.json", + "rel": "OSCAL-SSP-XML" + } + ], + "party-uuid": "22222222-0000-4000-9000-100000000002", + "date-authorized": "2018-01-01" + } + ], + "users": [ + { + "uuid": "22222222-0000-4000-9000-200000000001", + "role-ids": [ + "admin" + ], + "authorized-privileges": [ + { + "title": "Administrator", + "functions-performed": [ + "Manages the components within the SaaS." + ] + } + ] + } + ], + "components": [ + { + "uuid": "80511208-2643-4d2a-bef4-d593ba86b73f", + "type": "this-system", + "title": "This System", + "description": "The system described by this SSP.\n\nThis text was auto-generated by the OSCAL M3-RC1 data upgrade converter.", + "status": { + "state": "operational" + } + }, + { + "uuid": "22222222-0000-4000-9001-000000000001", + "type": "this-system", + "title": "THIS SYSTEM (SaaS)", + "description": "This Leveraging SaaS.\n\nThe entire system as depicted in the system authorization boundary", + "props": [ + { + "name": "implementation-point", + "value": "system" + } + ], + "status": { + "state": "operational" + } + }, + { + "uuid": "22222222-0000-4000-9001-000000000002", + "type": "system", + "title": " **LEVERAGED SYSTEM (IaaS)** ", + "description": "If the leveraged system owner provides a UUID for their system (such as in an OSCAL-based CRM), it should be used as the UUID for this component.", + "props": [ + { + "name": "implementation-point", + "value": "external" + }, + { + "name": "leveraged-authorization-uuid", + "value": "22222222-0000-4000-9000-300000000001" + }, + { + "name": "inherited-uuid", + "value": "11111111-0000-4000-9001-000000000001" + } + ], + "status": { + "state": "operational" + } + }, + { + "uuid": "22222222-0000-4000-9001-000000000003", + "type": "appliance", + "title": "Access Control Appliance", + "description": "An access control virtual appliance, wich performs XYZ functions.", + "props": [ + { + "name": "implementation-point", + "value": "internal" + }, + { + "name": "virtual", + "value": "yes" + } + ], + "status": { + "state": "operational" + } + }, + { + "uuid": "22222222-0000-4000-9001-000000000004", + "type": "application", + "title": "Leveraged Application", + "description": "Inherited from underlying IaaS.", + "props": [ + { + "name": "implementation-point", + "value": "external" + }, + { + "name": "leveraged-authorization-uuid", + "value": "22222222-0000-4000-9000-300000000001" + }, + { + "name": "inherited-uuid", + "value": "11111111-0000-4000-9001-000000000002" + } + ], + "status": { + "state": "operational" + } + } + ] + }, + "control-implementation": { + "description": "This is a collection of control responses.", + "implemented-requirements": [ + { + "uuid": "22222222-0000-4000-9009-002000000000", + "control-id": "ac-2", + "set-parameters": [ + { + "param-id": "ac-2_prm_1", + "values": [ + "privileged and non-privileged" + ] + } + ], + "by-components": [ + { + "component-uuid": "22222222-0000-4000-9001-000000000003", + "uuid": "22222222-0000-4000-9009-002001003000", + "description": " *duplicated/tailored description of what was inherited, and description of what was configured.* \n\nConsumer-appropriate description of what may be inherited.\n\nIn the context of the application component in satisfaction of AC-2." + } + ], + "statements": [ + { + "statement-id": "ac-2_stmt.a", + "uuid": "22222222-0000-4000-9009-002001000000", + "by-components": [ + { + "component-uuid": "22222222-0000-4000-9001-000000000001", + "uuid": "22222222-0000-4000-9009-002001001000", + "description": "Response for the \\\"This System\\\" component.\n\nOverall description of how \\\"This System\\\" satisfies AC-2, Part a.\n\nResponse for the \\\"This System\\\" component.\n\nOverall description of how \\\"This System\\\" satisfies AC-2, Part a.\n\nResponse for the \\\"This System\\\" component.\n\nOverall description of how \\\"This System\\\" satisfies AC-2, Part a.\n\nResponse for the \\\"This System\\\" component.\n\nOverall description of how \\\"This System\\\" satisfies AC-2, Part a.", + "props": [ + { + "name": "responsibility", + "value": "provider" + } + ] + }, + { + "component-uuid": "22222222-0000-4000-9001-000000000002", + "uuid": "22222222-0000-4000-9009-002001002000", + "description": "Describe how this internal virtual appliance satisfies AC-2, Part a.", + "satisfied": [ + { + "uuid": "22222222-0000-4000-9009-002001002001", + "responsibility-uuid": "11111111-0000-4000-9009-002001001001", + "description": "Description that directly addresses how the consumer responsibility was satisfied.", + "responsible-roles": [ + { + "role-id": "role-id" + } + ] + } + ] + } + ], + "remarks": "a. Identifies and selects the following types of information system accounts to support organizational missions/business functions: [Assignment: privileged and non-privileged];" + } + ], + "remarks": "The organization:\n\na. Identifies and selects the following types of information system accounts to support organizational missions/business functions: [Assignment: organization-defined information system account types];\n\nb. Assigns account managers for information system accounts;\n\nc. Establishes conditions for group and role membership;\n\nOmitted: d. through j." + } + ] + }, + "back-matter": { + "resources": [ + { + "uuid": "22222222-0000-4000-9999-000000000001", + "rlinks": [ + { + "href": "./attachments/SaaS_ac_proc.docx" + } + ] + } + ] + } + } + } \ No newline at end of file diff --git a/tests/trestle/core/commands/author/ssp_test.py b/tests/trestle/core/commands/author/ssp_test.py index 84b6bc568..ff71d6bcb 100644 --- a/tests/trestle/core/commands/author/ssp_test.py +++ b/tests/trestle/core/commands/author/ssp_test.py @@ -548,6 +548,30 @@ def test_ssp_generate_resolved_catalog(tmp_trestle_dir: pathlib.Path) -> None: resolved_catalog.oscal_write(new_catalog_path) +def test_ssp_assemble_w_inhert(tmp_trestle_dir: pathlib.Path) -> None: + """Test ssp assemble from cli.""" + gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name, False, 'leveraged_ssp') + args_compdefs = gen_args.compdefs + + # first create the markdown + ssp_gen = SSPGenerate() + assert ssp_gen._run(gen_args) == 0 + + # now assemble the edited controls into json ssp + ssp_assemble = SSPAssemble() + args = argparse.Namespace( + trestle_root=tmp_trestle_dir, + markdown=ssp_name, + output=ssp_name, + verbose=0, + regenerate=False, + name=None, + compdefs=args_compdefs, + version=None + ) + assert ssp_assemble._run(args) == 0 + + def test_ssp_filter(tmp_trestle_dir: pathlib.Path) -> None: """Test the ssp filter.""" # FIXME enhance coverage diff --git a/tests/trestle/core/crm/bycomp_interface_test.py b/tests/trestle/core/crm/bycomp_interface_test.py new file mode 100644 index 000000000..767d99685 --- /dev/null +++ b/tests/trestle/core/crm/bycomp_interface_test.py @@ -0,0 +1,109 @@ +# -*- mode:python; coding:utf-8 -*- +# Copyright (c) 2020 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Tests for the ByComponentInterface class.""" + +from tests import test_utils + +import trestle.core.generators as gens +import trestle.oscal.ssp as ossp +from trestle.core.crm.bycomp_interface import ByComponentInterface + +test_provided_uuid = '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' +test_responsibility_uuid = '4b34c68f-75fa-4b38-baf0-e50158c13ac2' + + +def test_get_isolated_responsibilities() -> None: + """Test retrieving isolated responsibilities statements.""" + by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() + expected_responsibility = 1 + expected_uuid = by_comp.export.responsibilities[0].uuid # type: ignore + + bycomp_interface: ByComponentInterface = ByComponentInterface(by_comp) + + result = bycomp_interface.get_isolated_responsibilities() + + assert len(result) == expected_responsibility + assert result[0].uuid == expected_uuid + + +def test_get_isolated_provided() -> None: + """Test retrieving isolated provided statements.""" + by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() + expected_provided = 1 + expected_uuid = by_comp.export.provided[0].uuid # type: ignore + + bycomp_interface: ByComponentInterface = ByComponentInterface(by_comp) + + result = bycomp_interface.get_isolated_provided() + + assert len(result) == expected_provided + assert result[0].uuid == expected_uuid + + +def test_get_export_sets() -> None: + """Test retrieving export set statements.""" + by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() + expected_set = 1 + expected_responsibility_uuid = by_comp.export.responsibilities[1].uuid # type: ignore + expected_provided_uuid = by_comp.export.provided[1].uuid # type: ignore + + bycomp_interface: ByComponentInterface = ByComponentInterface(by_comp) + + result = bycomp_interface.get_export_sets() + + result_set = result[0] + + assert len(result) == expected_set + assert result_set[0].uuid == expected_responsibility_uuid + assert result_set[0].provided_uuid == expected_provided_uuid + assert result_set[1].uuid == expected_provided_uuid + + +def test_reconcile_inheritance_by_component() -> None: + """Test retrieving isolated responsibilities statements.""" + by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() + by_comp.inherited = [] + by_comp.satisfied = [] + + # Set up default inherited and satisfied statements + inherited = gens.generate_sample_model(ossp.Inherited) + inherited.provided_uuid = test_provided_uuid + inherited.description = 'inherited description' + satisfied = gens.generate_sample_model(ossp.Satisfied) + satisfied.responsibility_uuid = test_responsibility_uuid + satisfied.description = 'satisfied description' + + by_comp.inherited.append(inherited) + by_comp.satisfied.append(satisfied) + + bycomp_interface: ByComponentInterface = ByComponentInterface(by_comp) + + # Create new inherited and satisfied statements and update the description + new_inherited = gens.generate_sample_model(ossp.Inherited) + new_inherited.provided_uuid = test_provided_uuid + new_inherited.description = 'new inherited description' + new_satisfied = gens.generate_sample_model(ossp.Satisfied) + new_satisfied.responsibility_uuid = test_responsibility_uuid + new_satisfied.description = 'new satisfied description' + + result_by_comp = bycomp_interface.reconcile_inheritance_by_component([new_inherited], [new_satisfied]) + + # Ensure that the resulting by_component has one of each statement and the uuids match the originals + assert len(result_by_comp.inherited) == 1 + assert len(result_by_comp.satisfied) == 1 + assert result_by_comp.inherited[0].uuid == inherited.uuid # type: ignore + assert result_by_comp.satisfied[0].uuid == satisfied.uuid # type: ignore + assert result_by_comp.inherited[0].description == new_inherited.description # type: ignore + assert result_by_comp.satisfied[0].description == new_satisfied.description # type: ignore diff --git a/tests/trestle/core/crm/exports_interface_test.py b/tests/trestle/core/crm/exports_interface_test.py deleted file mode 100644 index f7726de60..000000000 --- a/tests/trestle/core/crm/exports_interface_test.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- mode:python; coding:utf-8 -*- -# Copyright (c) 2020 IBM Corp. All rights reserved. -# -# 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 -# -# https://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. -"""Tests for the ExportInterface class.""" - -from tests import test_utils - -import trestle.oscal.ssp as ossp -from trestle.core.crm.export_interface import ExportInterface - -test_profile = 'simple_test_profile' -test_ssp = 'leveraged_ssp' - - -def test_get_isolated_responsibilities() -> None: - """Test retrieving isolated responsibilities statements.""" - by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() - expected_responsibility = 1 - expected_uuid = by_comp.export.responsibilities[0].uuid - - export_interface: ExportInterface = ExportInterface(by_comp) - - result = export_interface.get_isolated_responsibilities() - - assert len(result) == expected_responsibility - assert result[0].uuid == expected_uuid - - -def test_get_isolated_provided() -> None: - """Test retrieving isolated provided statements.""" - by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() - expected_provided = 1 - expected_uuid = by_comp.export.provided[0].uuid - - export_interface: ExportInterface = ExportInterface(by_comp) - - result = export_interface.get_isolated_provided() - - assert len(result) == expected_provided - assert result[0].uuid == expected_uuid - - -def test_get_export_sets() -> None: - """Test retrieving export set statements.""" - by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() - expected_set = 1 - expected_responsibility_uuid = by_comp.export.responsibilities[1].uuid - expected_provided_uuid = by_comp.export.provided[1].uuid - - export_interface: ExportInterface = ExportInterface(by_comp) - - result = export_interface.get_export_sets() - - result_set = result[0] - - assert len(result) == expected_set - assert result_set[0].uuid == expected_responsibility_uuid - assert result_set[0].provided_uuid == expected_provided_uuid - assert result_set[1].uuid == expected_provided_uuid diff --git a/tests/trestle/core/crm/exports_reader_test.py b/tests/trestle/core/crm/exports_reader_test.py new file mode 100644 index 000000000..63619a9f6 --- /dev/null +++ b/tests/trestle/core/crm/exports_reader_test.py @@ -0,0 +1,199 @@ +# Copyright (c) 2020 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Tests for the ssp_generator module.""" + +import pathlib + +from tests import test_utils + +import trestle.common.const as const +import trestle.core.crm.export_reader as exportreader +import trestle.oscal.ssp as ossp +from trestle.common.model_utils import ModelUtils +from trestle.core.models.file_content_type import FileContentType + +leveraged_ssp = 'leveraged_ssp' +leveraging_ssp = 'my_ssp' + +expected_appliance_uuid = '22222222-0000-4000-9001-000000000003' +expected_saas_uuid = '22222222-0000-4000-9001-000000000001' + +inheritance_text = """--- +x-trestle-statement: + # Add or modify leveraged SSP Statements here. + provided-uuid: 18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114 + responsibility-uuid: 4b34c68f-75fa-4b38-baf0-e50158c13ac2 +x-trestle-leveraging-comp: + # Leveraged statements can be optionally associated with components in this system. + # Associate leveraged statements to Components of this system here: + - name: Access Control Appliance + - name: THIS SYSTEM (SaaS) +--- + +# Provided Statement Description + +provided statement description + +# Responsibility Statement Description + +resp statement description + +# Satisfied Statement Description + + +My Satisfied Description +""" + +inheritance_text_2 = """--- +x-trestle-statement: + # Add or modify leveraged SSP Statements here. + provided-uuid: 18ac4e2a-b5f2-46e4-94fa-cc84ab6fe115 + responsibility-uuid: 4b34c68f-75fa-4b38-baf0-e50158c13ac3 +x-trestle-leveraging-comp: + # Leveraged statements can be optionally associated with components in this system. + # Associate leveraged statements to Components of this system here: + - name: Access Control Appliance +--- + +# Provided Statement Description + +provided statement description + +# Responsibility Statement Description + +resp statement description + +# Satisfied Statement Description + + +My Satisfied Description +""" + + +def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: + """Test exports reader with inheritance view.""" + ipath = tmp_trestle_dir.joinpath(leveraging_ssp, const.INHERITANCE_VIEW_DIR) + + ac_appliance_dir = ipath.joinpath('Access Control Appliance') + ac_2 = ac_appliance_dir.joinpath('ac-2') + ac_2.mkdir(parents=True) + + file = ac_2 / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(inheritance_text) + + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) + + orig_ssp, _ = ModelUtils.load_model_for_class( + tmp_trestle_dir, + leveraging_ssp, + ossp.SystemSecurityPlan, + FileContentType.JSON) + + reader = exportreader.ExportReader(ipath, orig_ssp) # type: ignore + ssp = reader.read_exports_from_markdown() + + implemented_requirements = ssp.control_implementation.implemented_requirements + + assert implemented_requirements[0].control_id == 'ac-2' + assert implemented_requirements[0].by_components[0].component_uuid == expected_appliance_uuid # type: ignore + + by_comp = implemented_requirements[0].by_components[0] # type: ignore + + assert by_comp.inherited[0].provided_uuid == '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' # type: ignore + assert by_comp.satisfied[0].responsibility_uuid == '4b34c68f-75fa-4b38-baf0-e50158c13ac2' # type: ignore + assert by_comp.satisfied[0].description == 'My Satisfied Description' # type: ignore + + assert implemented_requirements[0].by_components[1].component_uuid == expected_saas_uuid # type: ignore + by_comp = implemented_requirements[0].by_components[1] # type: ignore + + assert by_comp.inherited[0].provided_uuid == '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' # type: ignore + assert by_comp.satisfied[0].responsibility_uuid == '4b34c68f-75fa-4b38-baf0-e50158c13ac2' # type: ignore + assert by_comp.satisfied[0].description == 'My Satisfied Description' # type: ignore + + +def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: + """Test reading inheritance view directory.""" + ipath = tmp_trestle_dir.joinpath(leveraging_ssp, const.INHERITANCE_VIEW_DIR) + ac_appliance_dir = ipath.joinpath('Access Control Appliance') + ac_2 = ac_appliance_dir.joinpath('ac-2') + ac_2.mkdir(parents=True) + + file = ac_2 / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(inheritance_text) + + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) + + orig_ssp, _ = ModelUtils.load_model_for_class( + tmp_trestle_dir, + leveraging_ssp, + ossp.SystemSecurityPlan, + FileContentType.JSON) + + reader = exportreader.ExportReader(ipath, orig_ssp) # type: ignore + markdown_dict: exportreader.InheritanceViewDict = reader._read_inheritance_markdown_directory() + + assert len(markdown_dict) == 1 + assert 'ac-2' in markdown_dict + assert len(markdown_dict['ac-2']) == 2 + assert expected_appliance_uuid in markdown_dict['ac-2'] + + inheritance_info = markdown_dict['ac-2'][expected_appliance_uuid] + + assert inheritance_info[0][0].provided_uuid == '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' + assert inheritance_info[1][0].responsibility_uuid == '4b34c68f-75fa-4b38-baf0-e50158c13ac2' + assert inheritance_info[1][0].description == 'My Satisfied Description' + + +def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_trestle_dir: pathlib.Path) -> None: + """Test reading inheritance view directory with components that span multiple leveraged components.""" + ipath = tmp_trestle_dir.joinpath(leveraging_ssp, const.INHERITANCE_VIEW_DIR) + + ac_appliance_dir = ipath.joinpath('Access Control Appliance') + ac_2 = ac_appliance_dir.joinpath('ac-2') + ac_2.mkdir(parents=True) + + file = ac_2 / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(inheritance_text) + + this_system_dir = ipath.joinpath('This System') + ac_2 = this_system_dir.joinpath('ac-2') + ac_2.mkdir(parents=True) + + file = ac_2 / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(inheritance_text_2) + + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) + + orig_ssp, _ = ModelUtils.load_model_for_class( + tmp_trestle_dir, + leveraging_ssp, + ossp.SystemSecurityPlan, + FileContentType.JSON) + + reader = exportreader.ExportReader(ipath, orig_ssp) # type: ignore + markdown_dict: exportreader.InheritanceViewDict = reader._read_inheritance_markdown_directory() + + assert len(markdown_dict) == 1 + assert 'ac-2' in markdown_dict + assert len(markdown_dict['ac-2']) == 2 + + assert expected_appliance_uuid in markdown_dict['ac-2'] + inheritance_info = markdown_dict['ac-2'][expected_appliance_uuid] + + assert len(inheritance_info[0]) == 2 + assert len(inheritance_info[1]) == 2 diff --git a/tests/trestle/core/crm/exports_writer_test.py b/tests/trestle/core/crm/exports_writer_test.py index fbf010b8d..fd61851ba 100644 --- a/tests/trestle/core/crm/exports_writer_test.py +++ b/tests/trestle/core/crm/exports_writer_test.py @@ -27,7 +27,7 @@ import trestle.oscal.ssp as ossp from trestle.common.err import TrestleError from trestle.common.model_utils import ModelUtils -from trestle.core.crm.export_interface import ExportInterface +from trestle.core.crm.bycomp_interface import ByComponentInterface from trestle.core.crm.export_writer import ExportWriter from trestle.core.crm.leveraged_statements import ( LeveragedStatements, @@ -95,9 +95,9 @@ def test_statement_types_from_exports(tmp_trestle_dir: pathlib.Path) -> None: writer = ExportWriter(inherited_path, ssp) by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() - export_interface: ExportInterface = ExportInterface(by_comp) + bycomp_interface: ByComponentInterface = ByComponentInterface(by_comp) - result_leveraged_statements = writer._statement_types_from_exports(export_interface) + result_leveraged_statements = writer._statement_types_from_exports(bycomp_interface) provided: List[StatementProvided] = [] responsibility: List[StatementResponsibility] = [] sets: List[StatementTree] = [] @@ -123,8 +123,8 @@ def test_statement_types_no_exports(tmp_trestle_dir: pathlib.Path) -> None: writer = ExportWriter(inherited_path, ssp) by_comp = gens.generate_sample_model(ossp.ByComponent) - export_interface: ExportInterface = ExportInterface(by_comp) + bycomp_interface: ByComponentInterface = ByComponentInterface(by_comp) - result_leveraged_statements = writer._statement_types_from_exports(export_interface) + result_leveraged_statements = writer._statement_types_from_exports(bycomp_interface) assert len(result_leveraged_statements) == 0 diff --git a/trestle/core/catalog/catalog_reader.py b/trestle/core/catalog/catalog_reader.py index 49f1415db..e4d612a1f 100644 --- a/trestle/core/catalog/catalog_reader.py +++ b/trestle/core/catalog/catalog_reader.py @@ -357,8 +357,18 @@ def read_ssp_md_content( """ for group_path in CatalogInterface._get_group_ids_and_dirs(md_path).values(): for control_file in group_path.glob('*.md'): + skip = False + for file in control_file.parents: + if file.name == const.INHERITANCE_VIEW_DIR: + skip = True + break + if skip: + continue + control_id = control_file.stem + md_header, control_comp_dict = CatalogReader._read_comp_info_from_md(control_file, context) + for comp_name, comp_info_dict in control_comp_dict.items(): if comp_name not in comp_dict: err_msg = f'Control {control_id} references component {comp_name} not defined in a component-definition.' # noqa E501 diff --git a/trestle/core/commands/author/ssp.py b/trestle/core/commands/author/ssp.py index ed98ed9a4..eaa63aa10 100644 --- a/trestle/core/commands/author/ssp.py +++ b/trestle/core/commands/author/ssp.py @@ -15,6 +15,7 @@ import argparse import logging +import os import pathlib from typing import Any, Dict, List, Optional, Set @@ -42,6 +43,7 @@ from trestle.core.control_context import ContextPurpose, ControlContext from trestle.core.control_interface import ControlInterface, ParameterRep from trestle.core.control_reader import ControlReader +from trestle.core.crm.export_reader import ExportReader from trestle.core.crm.export_writer import ExportWriter from trestle.core.models.file_content_type import FileContentType from trestle.core.profile_resolver import ProfileResolver @@ -583,6 +585,12 @@ def _run(self, args: argparse.Namespace) -> int: # TODO if the ssp already existed then components may need to be removed if not ref'd by imp_reqs self._generate_roles_in_metadata(ssp) + # If this is a leveraging SSP, update it with the retrieved the exports from the leveraged SSP + ipath = pathlib.Path(md_path, const.INHERITANCE_VIEW_DIR) + if os.path.exists(ipath): + reader = ExportReader(ipath, ssp) + ssp = reader.read_exports_from_markdown() + ssp.import_profile.href = profile_href if args.version: diff --git a/trestle/core/crm/export_interface.py b/trestle/core/crm/bycomp_interface.py similarity index 50% rename from trestle/core/crm/export_interface.py rename to trestle/core/crm/bycomp_interface.py index 855b9f829..662263688 100644 --- a/trestle/core/crm/export_interface.py +++ b/trestle/core/crm/bycomp_interface.py @@ -11,58 +11,67 @@ # 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. -"""Provide interface to ssp allowing queries and operations for exports, inherited, and satisfied statements.""" +"""Provide interface to by-component allowing queries and operations for exports/inheritance statements.""" +import copy import logging -import uuid from typing import Dict, List, Tuple import trestle.oscal.ssp as ossp from trestle.common.err import TrestleError -from trestle.common.list_utils import as_dict, as_list +from trestle.common.list_utils import as_dict, as_list, none_if_empty logger = logging.getLogger(__name__) -class ExportInterface: +class ByComponentInterface: """ - Interface to query exported provided and responsibility statements from. + Interface to query and modify by-component assembly inheritance contents. - The by-component export statement is parsed and the responsibility and provided statements + The by-component is contained in two separate forms: As an actual OSCAL by-component assembly, + and and multiple dicts providing direct lookup of inheritance statement by uuid. + + The dicts are created by the ByComponentInterface constructor, parsed and the responsibility and provided statements are separated into three catagories: isolated responsibilities - A responsibility with no provided statement isolated provided - A provided statement with no referring responsibility statements export set - A set with a single responsibility and referred provided statement + + For updating ByComponent inheritance and satisfied statements, the interface provides a method to reconcile the + the by-component assembly and merge input inherited and satisfied statements. """ def __init__(self, by_comp: ossp.ByComponent): """Initialize export writer for a single by-component assembly.""" self._by_comp: ossp.ByComponent = by_comp - self._provided_dict: Dict[uuid.UUID, ossp.Provided] = {} - self._responsibility_dict: Dict[uuid.UUID, ossp.Responsibility] = {} - self._responsibility_by_provided: Dict[uuid.UUID, List[ossp.Responsibility]] = {} + self._provided_dict: Dict[str, ossp.Provided] = {} + self._responsibility_dict: Dict[str, ossp.Responsibility] = {} + self._responsibility_by_provided: Dict[str, List[ossp.Responsibility]] = {} + + self._inherited_dict: Dict[str, ossp.Inherited] = self._create_inherited_dict() + self._satisfied_dict: Dict[str, ossp.Satisfied] = self._create_satisfied_dict() if by_comp.export: self._provided_dict = self._create_provided_dict() self._responsibility_dict = self._create_responsibility_dict() self._responsibility_by_provided = self._create_responsibility_by_provided_dict() - def _create_provided_dict(self) -> Dict[uuid.UUID, ossp.Provided]: - provided_dict: Dict[uuid.UUID, ossp.Provided] = {} + def _create_provided_dict(self) -> Dict[str, ossp.Provided]: + provided_dict: Dict[str, ossp.Provided] = {} for provided in as_list(self._by_comp.export.provided): provided_dict[provided.uuid] = provided return provided_dict - def _create_responsibility_dict(self) -> Dict[uuid.UUID, ossp.Responsibility]: - responsibility_dict: Dict[uuid.UUID, ossp.Responsibility] = {} + def _create_responsibility_dict(self) -> Dict[str, ossp.Responsibility]: + responsibility_dict: Dict[str, ossp.Responsibility] = {} for responsibility in as_list(self._by_comp.export.responsibilities): responsibility_dict[responsibility.uuid] = responsibility return responsibility_dict - def _create_responsibility_by_provided_dict(self) -> Dict[uuid.UUID, List[ossp.Responsibility]]: - responsibility_by_provided: Dict[uuid.UUID, List[ossp.Responsibility]] = {} + def _create_responsibility_by_provided_dict(self) -> Dict[str, List[ossp.Responsibility]]: + responsibility_by_provided: Dict[str, List[ossp.Responsibility]] = {} for responsibility in as_list(self._by_comp.export.responsibilities): if responsibility.provided_uuid is None: continue @@ -74,6 +83,18 @@ def _create_responsibility_by_provided_dict(self) -> Dict[uuid.UUID, List[ossp.R responsibility_by_provided[responsibility.provided_uuid] = existing_list return responsibility_by_provided + def _create_inherited_dict(self) -> Dict[str, ossp.Inherited]: + inherited_dict: Dict[str, ossp.Inherited] = {} + for inherited in as_list(self._by_comp.inherited): + inherited_dict[str(inherited.provided_uuid)] = inherited + return inherited_dict + + def _create_satisfied_dict(self) -> Dict[str, ossp.Satisfied]: + satisfied_dict: Dict[str, ossp.Satisfied] = {} + for satisfied in as_list(self._by_comp.satisfied): + satisfied_dict[str(satisfied.responsibility_uuid)] = satisfied + return satisfied_dict + def get_isolated_responsibilities(self) -> List[ossp.Responsibility]: """Return all isolated exported responsibilities.""" all_responsibilities: List[ossp.Responsibility] = [] @@ -107,6 +128,47 @@ def get_export_sets(self) -> List[Tuple[ossp.Responsibility, ossp.Provided]]: all_export_sets.append(shared_responsibility) return all_export_sets - def _provided_has_responsibilities(self, provided_uuid: uuid.UUID) -> bool: + def reconcile_inheritance_by_component( + self, incoming_inherited: List[ossp.Inherited], incoming_satisfied: List[ossp.Satisfied] + ) -> ossp.ByComponent: + """ + Reconcile the inherited and satisfied statements in the by-component assembly with changes from the export. + + Notes: + A statement is determined as existing if the provided uuid or responsibility uuid is in the existing in the + by-component assembly. If existing, the description will be updated if it has changed. + + Any existing inherited or satisfied statements that are not in the incoming export will be removed. + If a statement is in the incoming export, but not in the existing by-component assembly, it will be added. + """ + new_inherited: List[ossp.Inherited] = [] + new_satisfied: List[ossp.Satisfied] = [] + + # Create a copy of the input by-component assembly to reconcile and return + new_by_comp: ossp.ByComponent = copy.deepcopy(self._by_comp) + + for statement in incoming_inherited: + if statement.provided_uuid in self._inherited_dict: + existing_statement = self._inherited_dict[str(statement.provided_uuid)] + # Update the description if it has changed + existing_statement.description = statement.description + statement = existing_statement + new_inherited.append(statement) + + new_by_comp.inherited = none_if_empty(new_inherited) + + for statement in incoming_satisfied: + if statement.responsibility_uuid in self._satisfied_dict: + existing_statement = self._satisfied_dict[str(statement.responsibility_uuid)] + # Update the description if it has changed + existing_statement.description = statement.description + statement = existing_statement + new_satisfied.append(statement) + + new_by_comp.satisfied = none_if_empty(new_satisfied) + + return new_by_comp + + def _provided_has_responsibilities(self, provided_uuid: str) -> bool: """Return whether a provided UUID has responsibilities.""" return provided_uuid in self._responsibility_by_provided diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py new file mode 100644 index 000000000..ee190763f --- /dev/null +++ b/trestle/core/crm/export_reader.py @@ -0,0 +1,186 @@ +# Copyright (c) 2021 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Provided interface to read inheritance statements from Markdown.""" + +import logging +import os +import pathlib +from typing import Dict, List, Tuple + +import trestle.core.generators as gens +import trestle.oscal.ssp as ossp +from trestle.common.list_utils import as_list, none_if_empty +from trestle.core.crm.bycomp_interface import ByComponentInterface +from trestle.core.crm.leveraged_statements import InheritanceMarkdownReader + +logger = logging.getLogger(__name__) + +# Provide name for this type +# Containing dictionary that is keyed by by_component uuid with a tuple of inherited and satisfied statements +ByComponentDict = Dict[str, Tuple[List[ossp.Inherited], List[ossp.Satisfied]]] + +# Provide name for this type +# Containing dictionary that is keyed by control id with a dictionary of by_component information +InheritanceViewDict = Dict[str, ByComponentDict] + + +class ExportReader: + """ + By-Component Assembly Exports Markdown reader. + + Export reader handles all operations related to reading authored inherited and satisfied statements from exports + in Markdown. + """ + + def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): + """ + Initialize export reader. + + Arguments: + root_path: A root path object where an SSP's inheritance markdown is located. + ssp: A system security plan with exports + """ + self._ssp: ossp.SystemSecurityPlan = ssp + self._root_path: pathlib.Path = root_path + + def read_exports_from_markdown(self) -> ossp.SystemSecurityPlan: + """Read inheritance markdown and update the SSP with the inheritance information.""" + impl_requirements: List[ossp.ImplementedRequirement] = [] + markdown_dict: InheritanceViewDict = self._read_inheritance_markdown_directory() + + for implemented_requirement in as_list(self._ssp.control_implementation.implemented_requirements): + + # If the control id existing in the markdown, then update the by_components + if implemented_requirement.control_id in markdown_dict: + + new_by_comp: List[ossp.ByComponent] = [] + by_comp_dict: ByComponentDict = markdown_dict[implemented_requirement.control_id] + + for by_comp in as_list(implemented_requirement.by_components): + + if by_comp.component_uuid in by_comp_dict: + comp_inheritance_info = by_comp_dict[by_comp.component_uuid] + + bycomp_interface = ByComponentInterface(by_comp) + by_comp = bycomp_interface.reconcile_inheritance_by_component( + comp_inheritance_info[0], comp_inheritance_info[1] + ) + + # Delete the entry from the by_comp_dict once processed to avoid duplicates + del by_comp_dict[by_comp.component_uuid] + + new_by_comp.append(by_comp) + + # Add any new by_components that were not in the original implemented requirement + new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) + + implemented_requirement.by_components = new_by_comp + + # Update any implemented requirements statements assemblies + new_statements: List[ossp.Statement] = [] + + for stm in as_list(implemented_requirement.statements): + statement_id = getattr(stm, 'statement_id', f'{implemented_requirement.control_id}_smt') + + # If the statement id existing in the markdown, then update the by_components + if statement_id in markdown_dict: + + new_by_comp: List[ossp.ByComponent] = [] + by_comp_dict: ByComponentDict = markdown_dict[statement_id] + + for by_comp in as_list(stm.by_components): + + if by_comp.component_uuid in by_comp_dict: + comp_inheritance_info = by_comp_dict[by_comp.component_uuid] + + bycomp_interface = ByComponentInterface(by_comp) + by_comp = bycomp_interface.reconcile_inheritance_by_component( + comp_inheritance_info[0], comp_inheritance_info[1] + ) + + # Delete the entry from the by_comp_dict once processed to avoid duplicates + del by_comp_dict[by_comp.component_uuid] + + new_by_comp.append(by_comp) + + # Add any new by_components that were not in the original statement + new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) + + stm.by_components = new_by_comp + + new_statements.append(stm) + + implemented_requirement.statements = none_if_empty(new_statements) + impl_requirements.append(implemented_requirement) + + self._ssp.control_implementation.implemented_requirements = impl_requirements + return self._ssp + + def _read_inheritance_markdown_directory(self) -> InheritanceViewDict: + """Read all inheritance markdown files and return a dictionary of all the information.""" + markdown_dict: InheritanceViewDict = {} + + # Creating a dictionary to find the component uuid by title for faster lookup + uuid_by_title: Dict[str, str] = {} + for component in as_list(self._ssp.system_implementation.components): + uuid_by_title[component.title] = component.uuid + + for comp_dir in os.listdir(self._root_path): + for control_dir in os.listdir(self._root_path.joinpath(comp_dir)): + + # Initialize the by component dictionary for the control directory + # If it exists in the markdown dictionary, then update it with the new information + by_comp_dict: ByComponentDict = {} + if control_dir in markdown_dict: + by_comp_dict = markdown_dict[control_dir] + + for file in os.listdir(self._root_path.joinpath(comp_dir, control_dir)): + reader = InheritanceMarkdownReader(self._root_path.joinpath(comp_dir, control_dir, file)) + leveraged_info = reader.process_leveraged_statement_markdown() + + # If there is no leveraged information, then skip this file + if leveraged_info is None: + continue + + for comp in leveraged_info.leveraging_comp_titles: + comp_uuid = uuid_by_title[comp] + inherited: List[ossp.Inherited] = [] + satisfied: List[ossp.Satisfied] = [] + + # If the component uuid exists in the by_component dictionary, then update it + if comp_uuid in by_comp_dict: + inherited = by_comp_dict[comp_uuid][0] + satisfied = by_comp_dict[comp_uuid][1] + + if leveraged_info.inherited is not None: + inherited.append(leveraged_info.inherited) + if leveraged_info.satisfied is not None: + satisfied.append(leveraged_info.satisfied) + + by_comp_dict[comp_uuid] = (inherited, satisfied) + + markdown_dict[control_dir] = by_comp_dict + return markdown_dict + + @staticmethod + def _add_new_by_comps(by_comp_dict: ByComponentDict) -> List[ossp.ByComponent]: + """Add new by_components to the implemented requirement.""" + new_by_comp: List[ossp.ByComponent] = [] + for comp_uuid, inheritance_info in by_comp_dict.items(): + by_comp: ossp.ByComponent = gens.generate_sample_model(ossp.ByComponent) + by_comp.component_uuid = comp_uuid + by_comp.inherited = none_if_empty(inheritance_info[0]) + by_comp.satisfied = none_if_empty(inheritance_info[1]) + new_by_comp.append(by_comp) + return new_by_comp diff --git a/trestle/core/crm/export_writer.py b/trestle/core/crm/export_writer.py index 82a3b4da6..e6079553b 100644 --- a/trestle/core/crm/export_writer.py +++ b/trestle/core/crm/export_writer.py @@ -15,14 +15,13 @@ import logging import pathlib -import uuid from typing import Dict import trestle.common.const as const import trestle.oscal.ssp as ossp from trestle.common.err import TrestleError from trestle.common.list_utils import as_list -from trestle.core.crm.export_interface import ExportInterface +from trestle.core.crm.bycomp_interface import ByComponentInterface from trestle.core.crm.leveraged_statements import ( LeveragedStatements, StatementProvided, @@ -55,7 +54,7 @@ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): def write_exports_as_markdown(self) -> None: """Write export statement for leveraged SSP as the inheritance Markdown view.""" # Find all the components and create paths for name - paths_by_comp: Dict[uuid.UUID, pathlib.Path] = {} + paths_by_comp: Dict[str, pathlib.Path] = {} for component in as_list(self._ssp.system_implementation.components): paths_by_comp[component.uuid] = self._root_path.joinpath(component.title) @@ -79,7 +78,7 @@ def write_exports_as_markdown(self) -> None: def _process_by_component(self, by_comp: ossp.ByComponent, comp_path: pathlib.Path, control_id: str) -> None: """Complete the Markdown writing operations for each by-component assembly.""" - export_interface: ExportInterface = ExportInterface(by_comp=by_comp) + export_interface: ByComponentInterface = ByComponentInterface(by_comp=by_comp) leveraged_statements: Dict[str, LeveragedStatements] = self._statement_types_from_exports(export_interface) @@ -95,7 +94,7 @@ def _process_by_component(self, by_comp: ossp.ByComponent, comp_path: pathlib.Pa statement_path: pathlib.Path = control_path.joinpath(f'{statement_file_path}{const.MARKDOWN_FILE_EXT}') leveraged_stm.write_statement_md(statement_path) - def _statement_types_from_exports(self, export_interface: ExportInterface) -> Dict[str, LeveragedStatements]: + def _statement_types_from_exports(self, export_interface: ByComponentInterface) -> Dict[str, LeveragedStatements]: """Process all exports and return a file basename and LeveragedStatement object for each.""" all_statements: Dict[str, LeveragedStatements] = {} diff --git a/trestle/core/crm/leveraged_statements.py b/trestle/core/crm/leveraged_statements.py index 495e1b6dd..ba97298e0 100644 --- a/trestle/core/crm/leveraged_statements.py +++ b/trestle/core/crm/leveraged_statements.py @@ -197,7 +197,7 @@ class InheritanceInfo: class InheritanceMarkdownReader: """Class to read leveraged statement information from Markdown.""" - def __init__(self, leveraged_statement_file: str) -> None: + def __init__(self, leveraged_statement_file: pathlib.Path) -> None: """Initialize the class.""" # Save the file name for logging self._leveraged_statement_file = leveraged_statement_file From 7c572ffab946c20bf68c83f678044068bf8f310d Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Thu, 24 Aug 2023 17:02:18 -0400 Subject: [PATCH 04/24] docs: updates documentation with usage and API references updates for inheritance Signed-off-by: Jennifer Power --- .../trestle.core.crm.bycomp_interface.md | 2 + .../trestle.core.crm.export_reader.md | 2 + .../trestle.core.crm.export_writer.md | 2 + .../trestle.core.crm.leveraged_statements.md | 2 + .../ssp_profile_catalog_authoring.md | 125 ++++++++++++++++++ mkdocs.yml | 5 + 6 files changed, 138 insertions(+) create mode 100644 docs/api_reference/trestle.core.crm.bycomp_interface.md create mode 100644 docs/api_reference/trestle.core.crm.export_reader.md create mode 100644 docs/api_reference/trestle.core.crm.export_writer.md create mode 100644 docs/api_reference/trestle.core.crm.leveraged_statements.md diff --git a/docs/api_reference/trestle.core.crm.bycomp_interface.md b/docs/api_reference/trestle.core.crm.bycomp_interface.md new file mode 100644 index 000000000..a6bc38dde --- /dev/null +++ b/docs/api_reference/trestle.core.crm.bycomp_interface.md @@ -0,0 +1,2 @@ +::: trestle.core.crm.bycomp_interface +handler: python diff --git a/docs/api_reference/trestle.core.crm.export_reader.md b/docs/api_reference/trestle.core.crm.export_reader.md new file mode 100644 index 000000000..27301c488 --- /dev/null +++ b/docs/api_reference/trestle.core.crm.export_reader.md @@ -0,0 +1,2 @@ +::: trestle.core.crm.export_reader +handler: python diff --git a/docs/api_reference/trestle.core.crm.export_writer.md b/docs/api_reference/trestle.core.crm.export_writer.md new file mode 100644 index 000000000..311b57102 --- /dev/null +++ b/docs/api_reference/trestle.core.crm.export_writer.md @@ -0,0 +1,2 @@ +::: trestle.core.crm.export_writer +handler: python diff --git a/docs/api_reference/trestle.core.crm.leveraged_statements.md b/docs/api_reference/trestle.core.crm.leveraged_statements.md new file mode 100644 index 000000000..943d725f1 --- /dev/null +++ b/docs/api_reference/trestle.core.crm.leveraged_statements.md @@ -0,0 +1,2 @@ +::: trestle.core.crm.leveraged_statements +handler: python diff --git a/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md b/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md index 38a11b252..b8d32faec 100644 --- a/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md +++ b/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md @@ -1050,6 +1050,131 @@ As with all the `assemble` tools, you may optionally specify a `--name` for a co If you do not specify component-defintions during assembly, the markdown should not refer to any components other than `This System`. Thus you may first generate markdown with `ssp-generate` and no component-definitions specified - and then you may assemble that ssp with `ssp-assemble` and no component-definitions specified - but only if there are no components other than `This System` referenced in the markdown. You may add new component implementation details to the markdown later, but any new components must be defined in a component-defintion file, and that file must be specified when `ssp-assemble` is run. +## Inheritance view + +The inheritance view is generated by setting the `--leveraged-ssp` flag with `trestle author ssp-generate`. It contains information relating to exported information such as inherited capabilities and consumer responsibilities that can be used to populate the inheritance information in the assembled SSP. When used, a directory named "inheritance" is created within the markdown directory. This directory serves as a designated space for mapping inherited capabilities and responsibilities onto components in the assemble SSP and authoring satisfied statements for responsibilities. + +Example usage for creation of the markdown: + +`trestle author ssp-generate --profile my_prof --compdefs "compdef_a,compdef_b" --yaml /my_yaml_dir/header.yaml --leveraged-ssp my_provider_ssp --output my_ssp` + +In this example the leveraged ssp has previously been imported into the trestle directory, but it can be fetched from remote location. + +The generated markdown output with the inheritance view will be placed in the trestle subdirectory `my_ssp/inheritance` with a subdirectory for each component in the leveraged ssp with directories separated by control and statement id below. + +An example of this directory structure is below. + +```text +. +├── Application +│ ├── ac-1_stmt.a +│ │ └── 11111111-0000-4000-9009-001001002006.md +│ ├── ac-2.1 +│ │ └── 11111111-0000-4000-9009-001001002004.md +│ └── ac-2_stmt.a +│ └── 11111111-0000-4000-9009-002001002001_11111111-0000-4000-9009-002001002002.md +└── This System + ├── ac-1_stmt.a + │ └── 11111111-0000-4000-9009-001002002001.md + ├── ac-2.1 + │ └── 11111111-0000-4000-9009-001001002001.md + └── ac-2_stmt.a + └── 11111111-0000-4000-9009-002001001001.md +``` + +The leveraged components are used as the top level directory to allow any non-leveraged components to be easily skipped or removed. Each markdown file is named in accordance with the uuid of the exported statement to ensure statement description updates can be applied. + +There are three types of markdown files that can be generated from this process. + +The examples below demonstrate these types: + +
+ +Example of inheritance provided only markdown after ssp-generate + +```markdown +--- +x-trestle-statement: + # Add or modify leveraged SSP Statements here. + provided-uuid: 11111111-0000-4000-9009-001002002001 +x-trestle-leveraging-comp: + # Leveraged statements can be optionally associated with components in this system. + # Associate leveraged statements to Components of this system here: + - name: REPLACE_ME +--- + +# Provided Statement Description + +Consumer_appropriate description of what may be inherited. +``` + +
+ +
+ +Example of inheritance consumer responsibility only markdown after ssp-generate + +```markdown +--- +x-trestle-statement: + # Add or modify leveraged SSP Statements here. + responsibility-uuid: 11111111-0000-4000-9009-002001001001 +x-trestle-leveraging-comp: + # Leveraged statements can be optionally associated with components in this system. + # Associate leveraged statements to Components of this system here: + - name: REPLACE_ME +--- + +# Responsibility Statement Description + +Leveraging system's responsibilities with respect to inheriting this capability. + +# Satisfied Statement Description + + +``` + +
+ +
+ +Example of inheritance shared responsibility markdown after ssp-generate + +```markdown +--- +x-trestle-statement: + # Add or modify leveraged SSP Statements here. + provided-uuid: 11111111-0000-4000-9009-002001002001 + responsibility-uuid: 11111111-0000-4000-9009-002001002002 +x-trestle-leveraging-comp: + # Leveraged statements can be optionally associated with components in this system. + # Associate leveraged statements to Components of this system here: + - name: REPLACE_ME +--- + +# Provided Statement Description + +Consumer-appropriate description of what may be inherited. + +# Responsibility Statement Description + +Leveraging system's responsibilities with respect to inheriting this capability. + +# Satisfied Statement Description + + +``` + +
+ +Some additional information and tips about this markdown are below: + +- Do not change the statement UUIDs in the YAML header. This is used in the assembled JSON to link the statements in the leveraged SSP to the components in the leveraging SSP. +- When mapping components in the YAML header, use the component title. If you do not wish to map a component to a particular inherited capability or responsibility, just leave the file as is. Files without mapped components or that contain the default "REPLACE ME" entry will be skipped. +- If the file exists, just the editable information will be preserved when regenerating existing inheritance view markdown. This includes the information under `Satisfied Statement Description` and the mapped components in the YAML header. + +After manually editing the inheritance view markdown, the `trestle author ssp-assemble` command can be run without modifications for the inheritance view use case. During assemble, the inheritance directory is detected and the information will be assembled into the SSP. The by-component assemblies will be updated or added under existing implemented requirement or statement sections with the information from the markdown. +
diff --git a/mkdocs.yml b/mkdocs.yml index 38b112540..5c2d5f124 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -113,6 +113,11 @@ nav: - control_interface: api_reference/trestle.core.control_interface.md - control_reader: api_reference/trestle.core.control_reader.md - control_writer: api_reference/trestle.core.control_writer.md + - crm: + - bycomp_interface: api_reference/trestle.core.crm.bycomp_interface.md + - export_reader: api_reference/trestle.core.crm.export_reader.md + - export_writer: api_reference/trestle.core.crm.export_writer.md + - leveraged_statements: api_reference/trestle.core.crm.leveraged_statements.md - docs_control_writer: api_reference/trestle.core.docs_control_writer.md - draw_io: api_reference/trestle.core.draw_io.md - duplicates_validator: api_reference/trestle.core.duplicates_validator.md From 2f7f2172ebeb193c82ff3c4e7a9e01b2a29a4359 Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Mon, 28 Aug 2023 14:38:25 -0400 Subject: [PATCH 05/24] chore: updates AgileAuthoring class for ssp-generate arg changes Signed-off-by: Jennifer Power --- trestle/core/repository.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/trestle/core/repository.py b/trestle/core/repository.py index 2b89305de..b53962a48 100644 --- a/trestle/core/repository.py +++ b/trestle/core/repository.py @@ -620,6 +620,7 @@ def generate_ssp_markdown( profile: str, output: str, compdefs: str, + leveraged_ssp: str = '', force_overwrite: bool = False, yaml_header: str = '', overwrite_header_values: bool = False @@ -633,6 +634,7 @@ def generate_ssp_markdown( profile=profile, output=output, compdefs=compdefs, + leveraged_ssp=leveraged_ssp, trestle_root=self.root_dir, force_overwrite=force_overwrite, yaml_header=yaml_header, From e6897afff01cb022442ead8b3c566dc36c8fae11 Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Wed, 30 Aug 2023 16:59:40 -0400 Subject: [PATCH 06/24] docs: updates returns section in InheritanceMarkdownReader docstring Signed-off-by: Jennifer Power --- trestle/core/crm/leveraged_statements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trestle/core/crm/leveraged_statements.py b/trestle/core/crm/leveraged_statements.py index ba97298e0..95168b600 100644 --- a/trestle/core/crm/leveraged_statements.py +++ b/trestle/core/crm/leveraged_statements.py @@ -213,8 +213,8 @@ def process_leveraged_statement_markdown(self) -> Optional[InheritanceInfo]: Read inheritance information from Markdown. Returns: - Optional InheritanceInfo: A list of mapped component titles, an optional satisfied statement and an optional - inherited statement + Optional InheritanceInfo - A list of mapped component titles, an optional satisfied statement and an optional + inherited statement Notes: Returns inheritance information in the context of the leveraging SSP. If no leveraging component titles are From db08a0fdd61549f18a1caedb31f7a173faab32ca Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Wed, 30 Aug 2023 17:07:37 -0400 Subject: [PATCH 07/24] fix: updates line length on return statement in InheritanceMarkdownReader Signed-off-by: Jennifer Power --- trestle/core/crm/leveraged_statements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trestle/core/crm/leveraged_statements.py b/trestle/core/crm/leveraged_statements.py index 95168b600..2bf7879fd 100644 --- a/trestle/core/crm/leveraged_statements.py +++ b/trestle/core/crm/leveraged_statements.py @@ -213,8 +213,8 @@ def process_leveraged_statement_markdown(self) -> Optional[InheritanceInfo]: Read inheritance information from Markdown. Returns: - Optional InheritanceInfo - A list of mapped component titles, an optional satisfied statement and an optional - inherited statement + Optional InheritanceInfo - A list of mapped component titles, an optional satisfied statement and an + optional inherited statement Notes: Returns inheritance information in the context of the leveraging SSP. If no leveraging component titles are From 01a5a3358b7f1c35d98ea5ab32d51bbc107ff5a0 Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Wed, 30 Aug 2023 18:12:08 -0400 Subject: [PATCH 08/24] refactor: updates markdown heading and comment strip function to remove regex Signed-off-by: Jennifer Power --- trestle/core/crm/leveraged_statements.py | 36 +++++++++++++++--------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/trestle/core/crm/leveraged_statements.py b/trestle/core/crm/leveraged_statements.py index 2bf7879fd..e5e03dbb0 100644 --- a/trestle/core/crm/leveraged_statements.py +++ b/trestle/core/crm/leveraged_statements.py @@ -14,7 +14,6 @@ """Handle writing of inherited statements to markdown.""" import logging import pathlib -import re from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any, Dict, List, Optional @@ -287,16 +286,25 @@ def get_leveraged_component_header_value(self) -> Dict[str, str]: @staticmethod def strip_heading_and_comments(markdown_text: str) -> str: """Remove the heading and comments from lines to get the multi-line paragraph.""" - heading_pattern = r'^#+.*$' - comment_pattern = r'' - - # Remove headings and comments - markdown_text = re.sub(heading_pattern, '', markdown_text, flags=re.MULTILINE) - markdown_text = re.sub(comment_pattern, '', markdown_text, flags=re.DOTALL) - - markdown_text = '\n'.join(line.strip() for line in markdown_text.splitlines()) - - # Remove consecutive empty lines - markdown_text = re.sub(r'\n{2,}', '\n\n', markdown_text) - - return markdown_text.strip() + lines = markdown_text.split('\n') + non_heading_comment_lines = [] + inside_heading = False + inside_comment = False + + for line in lines: + if line.startswith('#'): + inside_heading = True + elif line.startswith(''): + inside_comment = False + continue + elif line.strip() == '': + inside_heading = False + inside_comment = False + + if not inside_heading and not inside_comment: + non_heading_comment_lines.append(line) + + stripped_markdown = '\n'.join(non_heading_comment_lines).strip() + return stripped_markdown From 3e1ce19665f8197caabd6b2a3b0f4ddfe9afa34c Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Fri, 1 Sep 2023 19:34:26 -0400 Subject: [PATCH 09/24] test: adds inheritance view testing for ssp-assemble Signed-off-by: Jennifer Power --- tests/test_utils.py | 14 +++++++++++ .../trestle/core/commands/author/ssp_test.py | 23 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 922335f9b..36e01b194 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -242,6 +242,20 @@ def replace_line_in_file_after_tag(file_path: pathlib.Path, tag: str, new_line: return False +def replace_in_file(file_path: pathlib.Path, search_text: str, replace_text: str) -> None: + """Replace all occurrences of search_text with replace_text in file_path.""" + if not file_path.exists(): + raise TrestleError(f'Test file {file_path} not found.') + + with open(file_path, 'r') as file: + file_content = file.read() + + updated_content = file_content.replace(search_text, replace_text) + + with open(file_path, 'w') as file: + file.write(updated_content) + + def substitute_text_in_file(file_path: pathlib.Path, tag: str, new_str: str) -> bool: """Substitute first match of string with new string in file.""" if not file_path.exists(): diff --git a/tests/trestle/core/commands/author/ssp_test.py b/tests/trestle/core/commands/author/ssp_test.py index ff71d6bcb..2dd68da19 100644 --- a/tests/trestle/core/commands/author/ssp_test.py +++ b/tests/trestle/core/commands/author/ssp_test.py @@ -548,8 +548,8 @@ def test_ssp_generate_resolved_catalog(tmp_trestle_dir: pathlib.Path) -> None: resolved_catalog.oscal_write(new_catalog_path) -def test_ssp_assemble_w_inhert(tmp_trestle_dir: pathlib.Path) -> None: - """Test ssp assemble from cli.""" +def test_ssp_assemble_with_inheritance(tmp_trestle_dir: pathlib.Path) -> None: + """Test ssp assemble from cli with inheritance view.""" gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name, False, 'leveraged_ssp') args_compdefs = gen_args.compdefs @@ -557,6 +557,14 @@ def test_ssp_assemble_w_inhert(tmp_trestle_dir: pathlib.Path) -> None: ssp_gen = SSPGenerate() assert ssp_gen._run(gen_args) == 0 + this_system_dir = tmp_trestle_dir / ssp_name / const.INHERITANCE_VIEW_DIR / 'This System' + + expected_uuid = '11111111-0000-4000-9009-001001002001' + ac_21 = this_system_dir / 'ac-2.1' + test_provided = ac_21 / f'{expected_uuid}.md' + + test_utils.replace_in_file(test_provided, 'REPLACE_ME', 'comp_aa') + # now assemble the edited controls into json ssp ssp_assemble = SSPAssemble() args = argparse.Namespace( @@ -571,6 +579,17 @@ def test_ssp_assemble_w_inhert(tmp_trestle_dir: pathlib.Path) -> None: ) assert ssp_assemble._run(args) == 0 + ssp, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, ssp_name, ossp.SystemSecurityPlan, FileContentType.JSON) + + imp_reqs = ssp.control_implementation.implemented_requirements + imp_req = next((i_req for i_req in imp_reqs if i_req.control_id == 'ac-2.1'), None) + inherited = imp_req.by_components[1].inherited[0] # type: ignore + assert inherited.description == ( + 'Consumer-appropriate description of what may be inherited.\n\n\ +In the context of the application component in satisfaction of AC-2.1.' + ) + assert inherited.provided_uuid == expected_uuid + def test_ssp_filter(tmp_trestle_dir: pathlib.Path) -> None: """Test the ssp filter.""" From 33119d568d7e0c012ed9e26e0b9de12b12392e4c Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Wed, 6 Sep 2023 18:06:57 -0400 Subject: [PATCH 10/24] chore: adds more context to ExportReader class comments Signed-off-by: Jennifer Power --- trestle/core/crm/export_reader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py index ee190763f..7ad145797 100644 --- a/trestle/core/crm/export_reader.py +++ b/trestle/core/crm/export_reader.py @@ -40,7 +40,8 @@ class ExportReader: By-Component Assembly Exports Markdown reader. Export reader handles all operations related to reading authored inherited and satisfied statements from exports - in Markdown. + in Markdown. The reader will read all the markdown files in the exports directory and update the SSP with the + inheritance. """ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): @@ -49,7 +50,7 @@ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): Arguments: root_path: A root path object where an SSP's inheritance markdown is located. - ssp: A system security plan with exports + ssp: A system security plan object that will be updated with the inheritance information. """ self._ssp: ossp.SystemSecurityPlan = ssp self._root_path: pathlib.Path = root_path From 8baa8bb408ec99f1076a21102a8cb2fc1efdc887 Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Tue, 12 Sep 2023 16:15:41 -0400 Subject: [PATCH 11/24] feat: updates ssp-generate to filter control implementation for leveraged_ssp Signed-off-by: Jennifer Power --- trestle/core/commands/author/ssp.py | 67 +++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/trestle/core/commands/author/ssp.py b/trestle/core/commands/author/ssp.py index eaa63aa10..1272e03e7 100644 --- a/trestle/core/commands/author/ssp.py +++ b/trestle/core/commands/author/ssp.py @@ -194,27 +194,60 @@ def _generate_ssp_markdown( # Generate inheritance view after controls view completes if leveraged_ssp_name_or_href: - # if file not recognized as URI form, assume it represents name of file in trestle directory - ssp: ossp.SystemSecurityPlan - ssp_in_trestle_dir = '://' not in leveraged_ssp_name_or_href - ssp_href = leveraged_ssp_name_or_href - if ssp_in_trestle_dir: - local_path = f'{const.MODEL_DIR_SSP}/{leveraged_ssp_name_or_href}/system-security-plan.json' - ssp_path = trestle_root / local_path - _, _, ssp = ModelUtils.load_distributed(ssp_path, trestle_root) - else: - fetcher = FetcherFactory.get_fetcher(trestle_root, ssp_href) - ssp = fetcher.get_oscal() + self._generate_inheritance_markdown(trestle_root, leveraged_ssp_name_or_href, resolved_catalog, md_path) + + return CmdReturnCodes.SUCCESS.value + + def _generate_inheritance_markdown( + self, + trestle_root: pathlib.Path, + leveraged_ssp_name_or_href: str, + resolved_catalog: CatalogInterface, + md_path: str + ) -> None: + """ + Generate markdown for inheritance view. - inheritance_view_path: pathlib.Path = md_path.joinpath(const.INHERITANCE_VIEW_DIR) - inheritance_view_path.mkdir(exist_ok=True) - logger.debug(f'Creating content for inheritance view in {inheritance_view_path}') + Notes: + This will create the inheritance view markdown files in the same directory as the ssp markdown files. + The information will be from the leveraged ssp, but filtered by the chose profile to ensure only relevant + control are present for mapping. + """ + # if file not recognized as URI form, assume it represents name of file in trestle directory + ssp: ossp.SystemSecurityPlan + ssp_in_trestle_dir = '://' not in leveraged_ssp_name_or_href + ssp_href = leveraged_ssp_name_or_href + if ssp_in_trestle_dir: + local_path = f'{const.MODEL_DIR_SSP}/{leveraged_ssp_name_or_href}/system-security-plan.json' + ssp_path = trestle_root / local_path + _, _, ssp = ModelUtils.load_distributed(ssp_path, trestle_root) + else: + fetcher = FetcherFactory.get_fetcher(trestle_root, ssp_href) + try: + ssp, _ = fetcher.get_oscal() + except TrestleError as e: + raise TrestleError(f'Unable to fetch ssp from {ssp_href}: {e}') - export_writer: ExportWriter = ExportWriter(inheritance_view_path, ssp) + inheritance_view_path: pathlib.Path = md_path.joinpath(const.INHERITANCE_VIEW_DIR) + inheritance_view_path.mkdir(exist_ok=True) + logger.debug(f'Creating content for inheritance view in {inheritance_view_path}') - export_writer.write_exports_as_markdown() + # Filter the ssp implemented requirement by the catalog specified + catalog_api: CatalogAPI = CatalogAPI(catalog=resolved_catalog) + control_imp: ossp.ControlImplementation = ssp.control_implementation - return CmdReturnCodes.SUCCESS.value + new_imp_requirements: List[ossp.ImplementedRequirement] = [] + for imp_requirement in as_list(control_imp.implemented_requirements): + control = catalog_api._catalog_interface.get_control(imp_requirement.control_id) + if control is not None: + new_imp_requirements.append(imp_requirement) + control_imp.implemented_requirements = new_imp_requirements + + ssp.control_implementation = control_imp + + export_writer: ExportWriter = ExportWriter(inheritance_view_path, ssp) + + export_writer.write_exports_as_markdown() class SSPAssemble(AuthorCommonCommand): From 59dbaa52da8de78e56137815a91e849d6235050d Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Tue, 12 Sep 2023 16:17:05 -0400 Subject: [PATCH 12/24] refactor: updates ExportWriter to reduce code duplication Signed-off-by: Jennifer Power --- trestle/core/crm/bycomp_interface.py | 6 ++---- trestle/core/crm/export_writer.py | 28 ++++++++++++---------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/trestle/core/crm/bycomp_interface.py b/trestle/core/crm/bycomp_interface.py index 662263688..b92c8abba 100644 --- a/trestle/core/crm/bycomp_interface.py +++ b/trestle/core/crm/bycomp_interface.py @@ -80,7 +80,6 @@ def _create_responsibility_by_provided_dict(self) -> Dict[str, List[ossp.Respons else: existing_list: List[ossp.Responsibility] = responsibility_by_provided[responsibility.provided_uuid] existing_list.append(responsibility) - responsibility_by_provided[responsibility.provided_uuid] = existing_list return responsibility_by_provided def _create_inherited_dict(self) -> Dict[str, ossp.Inherited]: @@ -118,10 +117,9 @@ def get_export_sets(self) -> List[Tuple[ossp.Responsibility, ossp.Provided]]: # Ensure the provided object exists in the dictionary. # If it doesn't this is a bug. - try: - provided = self._provided_dict[provided_uuid] - except KeyError: + if provided_uuid not in self._provided_dict: raise TrestleError(f'Provided capability {provided_uuid} not found') + provided: ossp.Provided = self._provided_dict[provided_uuid] for responsibility in responsibilities: shared_responsibility: Tuple[ossp.Responsibility, ossp.Provided] = (responsibility, provided) diff --git a/trestle/core/crm/export_writer.py b/trestle/core/crm/export_writer.py index e6079553b..3ac0dd64e 100644 --- a/trestle/core/crm/export_writer.py +++ b/trestle/core/crm/export_writer.py @@ -51,35 +51,31 @@ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): self._ssp: ossp.SystemSecurityPlan = ssp self._root_path: pathlib.Path = root_path - def write_exports_as_markdown(self) -> None: - """Write export statement for leveraged SSP as the inheritance Markdown view.""" # Find all the components and create paths for name - paths_by_comp: Dict[str, pathlib.Path] = {} + self._paths_by_comp: Dict[str, pathlib.Path] = {} for component in as_list(self._ssp.system_implementation.components): - paths_by_comp[component.uuid] = self._root_path.joinpath(component.title) + self._paths_by_comp[component.uuid] = self._root_path.joinpath(component.title) + def write_exports_as_markdown(self) -> None: + """Write export statement for leveraged SSP as the inheritance Markdown view.""" # Process all information under exports in control implementation for implemented_requirement in as_list(self._ssp.control_implementation.implemented_requirements): for by_comp in as_list(implemented_requirement.by_components): - try: - comp_markdown_path: pathlib.Path = paths_by_comp[by_comp.component_uuid] - self._process_by_component(by_comp, comp_markdown_path, implemented_requirement.control_id) - except KeyError: - raise TrestleError(f'Component id {by_comp.component_uuid} is not in the system implementation') + self._process_by_component(by_comp, implemented_requirement.control_id) for stm in as_list(implemented_requirement.statements): statement_id = getattr(stm, 'statement_id', f'{implemented_requirement.control_id}_smt') for by_comp in as_list(stm.by_components): - try: - comp_markdown_path: pathlib.Path = paths_by_comp[by_comp.component_uuid] - self._process_by_component(by_comp, comp_markdown_path, statement_id) - except KeyError: - raise TrestleError(f'Component id {by_comp.uuid} is not in the system implementation') + self._process_by_component(by_comp, statement_id) - def _process_by_component(self, by_comp: ossp.ByComponent, comp_path: pathlib.Path, control_id: str) -> None: + def _process_by_component(self, by_comp: ossp.ByComponent, control_id: str) -> None: """Complete the Markdown writing operations for each by-component assembly.""" - export_interface: ByComponentInterface = ByComponentInterface(by_comp=by_comp) + if by_comp.component_uuid not in self._paths_by_comp: + raise TrestleError(f'Component id {by_comp.component_uuid} is not in the system implementation') + + comp_path: pathlib.Path = self._paths_by_comp[by_comp.component_uuid] + export_interface: ByComponentInterface = ByComponentInterface(by_comp=by_comp) leveraged_statements: Dict[str, LeveragedStatements] = self._statement_types_from_exports(export_interface) # Only create the directory if leveraged statements exist. If not return. From b1d438e37193912436a3a5c0e75902a83798cf2e Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Tue, 12 Sep 2023 16:18:09 -0400 Subject: [PATCH 13/24] fix: updates ExportReader to add new statements if present in the inheritance view Signed-off-by: Jennifer Power --- tests/trestle/core/crm/exports_reader_test.py | 44 ++++++++ trestle/core/crm/export_reader.py | 105 ++++++++++++++---- 2 files changed, 125 insertions(+), 24 deletions(-) diff --git a/tests/trestle/core/crm/exports_reader_test.py b/tests/trestle/core/crm/exports_reader_test.py index 63619a9f6..b576b8862 100644 --- a/tests/trestle/core/crm/exports_reader_test.py +++ b/tests/trestle/core/crm/exports_reader_test.py @@ -80,6 +80,30 @@ My Satisfied Description """ +unmapped_inheritance = """--- +x-trestle-statement: + # Add or modify leveraged SSP Statements here. + provided-uuid: 18ac4e2a-b5f2-46e4-94fa-cc84ab6fe115 + responsibility-uuid: 4b34c68f-75fa-4b38-baf0-e50158c13ac3 +x-trestle-leveraging-comp: + # Leveraged statements can be optionally associated with components in this system. + # Associate leveraged statements to Components of this system here: + - name: REPLACE_ME +--- + +# Provided Statement Description + +provided statement description + +# Responsibility Statement Description + +resp statement description + +# Satisfied Statement Description + + +""" + def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: """Test exports reader with inheritance view.""" @@ -93,6 +117,14 @@ def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: with open(file, 'w') as f: f.write(inheritance_text) + # test with a statement + ac_2a = ac_appliance_dir.joinpath('ac-2_smt.a') + ac_2a.mkdir(parents=True) + + file = ac_2a / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(inheritance_text) + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) orig_ssp, _ = ModelUtils.load_model_for_class( @@ -122,11 +154,16 @@ def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: assert by_comp.satisfied[0].responsibility_uuid == '4b34c68f-75fa-4b38-baf0-e50158c13ac2' # type: ignore assert by_comp.satisfied[0].description == 'My Satisfied Description' # type: ignore + # Ensure that the statement is also added to the SSP + assert implemented_requirements[0].statements is not None + assert implemented_requirements[0].statements[0].statement_id == 'ac-2_smt.a' + def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: """Test reading inheritance view directory.""" ipath = tmp_trestle_dir.joinpath(leveraging_ssp, const.INHERITANCE_VIEW_DIR) ac_appliance_dir = ipath.joinpath('Access Control Appliance') + ac_2 = ac_appliance_dir.joinpath('ac-2') ac_2.mkdir(parents=True) @@ -134,6 +171,13 @@ def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: with open(file, 'w') as f: f.write(inheritance_text) + ac_21 = ac_appliance_dir.joinpath('ac-2.1') + ac_21.mkdir(parents=True) + # Ensure this file does not get added to the dictionary + file = ac_21 / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(unmapped_inheritance) + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) orig_ssp, _ = ModelUtils.load_model_for_class( diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py index 7ad145797..2e28be484 100644 --- a/trestle/core/crm/export_reader.py +++ b/trestle/core/crm/export_reader.py @@ -40,8 +40,7 @@ class ExportReader: By-Component Assembly Exports Markdown reader. Export reader handles all operations related to reading authored inherited and satisfied statements from exports - in Markdown. The reader will read all the markdown files in the exports directory and update the SSP with the - inheritance. + in Markdown. """ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): @@ -50,17 +49,45 @@ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): Arguments: root_path: A root path object where an SSP's inheritance markdown is located. - ssp: A system security plan object that will be updated with the inheritance information. + ssp: A system security plan with exports """ self._ssp: ossp.SystemSecurityPlan = ssp + + # Create a dictionary of implemented requirements keyed by control id for merging operations + self._implemented_requirements: Dict[str, ossp.ImplementedRequirement] = self._create_impl_req_dict() + self._root_path: pathlib.Path = root_path + def _create_impl_req_dict(self) -> Dict[str, ossp.ImplementedRequirement]: + """Create a dictionary of implemented requirements keyed by control id.""" + impl_req_dict: Dict[str, ossp.ImplementedRequirement] = {} + for impl_req in as_list(self._ssp.control_implementation.implemented_requirements): + impl_req_dict[impl_req.control_id] = impl_req + return impl_req_dict + def read_exports_from_markdown(self) -> ossp.SystemSecurityPlan: """Read inheritance markdown and update the SSP with the inheritance information.""" - impl_requirements: List[ossp.ImplementedRequirement] = [] + # Read the information from the markdown files into a dictionary for quick lookup markdown_dict: InheritanceViewDict = self._read_inheritance_markdown_directory() - for implemented_requirement in as_list(self._ssp.control_implementation.implemented_requirements): + # Merge the markdown information into existing the implemented requirements + self._merge_exports_implemented_requirements(markdown_dict) + + # Process remaining markdown information that was not in the implemented requirements + for control_id, by_comp_dict in markdown_dict.items(): + logging.info(f'Adding control mapping {control_id} to implemented requirements') + self._add_control_mappings_to_implemented_requirements(control_id, by_comp_dict) + + impl_requirements: List[ossp.ImplementedRequirement] = [] + for impl_req in self._implemented_requirements.values(): + impl_requirements.append(impl_req) + + self._ssp.control_implementation.implemented_requirements = impl_requirements + return self._ssp + + def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceViewDict) -> None: + """Merge all exported inheritance info from the markdown into the implemented requirement dict.""" + for implemented_requirement in self._implemented_requirements.values(): # If the control id existing in the markdown, then update the by_components if implemented_requirement.control_id in markdown_dict: @@ -85,9 +112,11 @@ def read_exports_from_markdown(self) -> ossp.SystemSecurityPlan: # Add any new by_components that were not in the original implemented requirement new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) - implemented_requirement.by_components = new_by_comp + # Delete the entry from the markdown_dict once processed to avoid duplicates + del markdown_dict[implemented_requirement.control_id] + # Update any implemented requirements statements assemblies new_statements: List[ossp.Statement] = [] @@ -117,16 +146,55 @@ def read_exports_from_markdown(self) -> ossp.SystemSecurityPlan: # Add any new by_components that were not in the original statement new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) - stm.by_components = new_by_comp + # Delete the entry from the markdown_dict once processed to avoid duplicates + del markdown_dict[statement_id] + new_statements.append(stm) implemented_requirement.statements = none_if_empty(new_statements) - impl_requirements.append(implemented_requirement) - self._ssp.control_implementation.implemented_requirements = impl_requirements - return self._ssp + def _add_control_mappings_to_implemented_requirements( + self, control_mapping: str, by_comps: ByComponentDict + ) -> None: + """Add control mappings to implemented requirements.""" + # Determine if the control id is actually a statement id + if '_smt.' in control_mapping: + control_id = control_mapping.split('_smt')[0] + implemented_req = self._add_or_get_implemented_requirement(control_id) + statement = gens.generate_sample_model(ossp.Statement) + statement.statement_id = control_mapping + statement.by_components = ExportReader._add_new_by_comps(by_comps) + implemented_req.statements = as_list(implemented_req.statements) + implemented_req.statements.append(statement) + implemented_req.statements = sorted(implemented_req.statements, key=lambda x: x.statement_id) + else: + implemented_req = self._add_or_get_implemented_requirement(control_mapping) + implemented_req.by_components = as_list(implemented_req.by_components) + implemented_req.by_components.extend(ExportReader._add_new_by_comps(by_comps)) + + def _add_or_get_implemented_requirement(self, control_id: str) -> ossp.ImplementedRequirement: + """Add or get implemented requirement from implemented requirements dictionary.""" + if control_id in self._implemented_requirements: + return self._implemented_requirements[control_id] + + implemented_requirement = gens.generate_sample_model(ossp.ImplementedRequirement) + implemented_requirement.control_id = control_id + self._implemented_requirements[control_id] = implemented_requirement + return implemented_requirement + + @staticmethod + def _add_new_by_comps(by_comp_dict: ByComponentDict) -> List[ossp.ByComponent]: + """Add new by_components to the implemented requirement.""" + new_by_comp: List[ossp.ByComponent] = [] + for comp_uuid, inheritance_info in by_comp_dict.items(): + by_comp: ossp.ByComponent = gens.generate_sample_model(ossp.ByComponent) + by_comp.component_uuid = comp_uuid + by_comp.inherited = none_if_empty(inheritance_info[0]) + by_comp.satisfied = none_if_empty(inheritance_info[1]) + new_by_comp.append(by_comp) + return new_by_comp def _read_inheritance_markdown_directory(self) -> InheritanceViewDict: """Read all inheritance markdown files and return a dictionary of all the information.""" @@ -170,18 +238,7 @@ def _read_inheritance_markdown_directory(self) -> InheritanceViewDict: satisfied.append(leveraged_info.satisfied) by_comp_dict[comp_uuid] = (inherited, satisfied) - - markdown_dict[control_dir] = by_comp_dict + # If there is information in the by_component dictionary, then update the markdown dictionary + if by_comp_dict: + markdown_dict[control_dir] = by_comp_dict return markdown_dict - - @staticmethod - def _add_new_by_comps(by_comp_dict: ByComponentDict) -> List[ossp.ByComponent]: - """Add new by_components to the implemented requirement.""" - new_by_comp: List[ossp.ByComponent] = [] - for comp_uuid, inheritance_info in by_comp_dict.items(): - by_comp: ossp.ByComponent = gens.generate_sample_model(ossp.ByComponent) - by_comp.component_uuid = comp_uuid - by_comp.inherited = none_if_empty(inheritance_info[0]) - by_comp.satisfied = none_if_empty(inheritance_info[1]) - new_by_comp.append(by_comp) - return new_by_comp From 80b41e7abe2babd28504bcbd627d55712dc80dee Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Wed, 13 Sep 2023 16:49:09 -0400 Subject: [PATCH 14/24] fix: update logging to debug in ExportReader Signed-off-by: Jennifer Power --- trestle/core/crm/export_reader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py index 2e28be484..f89de13b9 100644 --- a/trestle/core/crm/export_reader.py +++ b/trestle/core/crm/export_reader.py @@ -40,7 +40,8 @@ class ExportReader: By-Component Assembly Exports Markdown reader. Export reader handles all operations related to reading authored inherited and satisfied statements from exports - in Markdown. + in Markdown. The reader will read all the markdown files in the exports directory and update the SSP with the + inheritance. """ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): @@ -49,7 +50,7 @@ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): Arguments: root_path: A root path object where an SSP's inheritance markdown is located. - ssp: A system security plan with exports + ssp: A system security plan object that will be updated with the inheritance information. """ self._ssp: ossp.SystemSecurityPlan = ssp @@ -75,7 +76,7 @@ def read_exports_from_markdown(self) -> ossp.SystemSecurityPlan: # Process remaining markdown information that was not in the implemented requirements for control_id, by_comp_dict in markdown_dict.items(): - logging.info(f'Adding control mapping {control_id} to implemented requirements') + logging.debug(f'Adding control mapping {control_id} to implemented requirements') self._add_control_mappings_to_implemented_requirements(control_id, by_comp_dict) impl_requirements: List[ossp.ImplementedRequirement] = [] From 27b28e6b11c2fe3525a524c4ae7d3fed57a17fba Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Fri, 15 Sep 2023 14:41:43 -0400 Subject: [PATCH 15/24] refactor: simplify code in read_exports_from_markdown Signed-off-by: Jennifer Power --- trestle/core/crm/export_reader.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py index f89de13b9..dcf88519a 100644 --- a/trestle/core/crm/export_reader.py +++ b/trestle/core/crm/export_reader.py @@ -79,11 +79,7 @@ def read_exports_from_markdown(self) -> ossp.SystemSecurityPlan: logging.debug(f'Adding control mapping {control_id} to implemented requirements') self._add_control_mappings_to_implemented_requirements(control_id, by_comp_dict) - impl_requirements: List[ossp.ImplementedRequirement] = [] - for impl_req in self._implemented_requirements.values(): - impl_requirements.append(impl_req) - - self._ssp.control_implementation.implemented_requirements = impl_requirements + self._ssp.control_implementation.implemented_requirements = list(self._implemented_requirements.values()) return self._ssp def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceViewDict) -> None: From d78567e91322850be337eaf1c268c9a527cacbea Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Fri, 22 Sep 2023 08:53:41 -0400 Subject: [PATCH 16/24] tests: simplify tests for ExportReader test data generation Signed-off-by: Jennifer Power --- tests/test_utils.py | 45 ++++++ tests/trestle/core/crm/exports_reader_test.py | 141 +++++------------- 2 files changed, 85 insertions(+), 101 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 36e01b194..c044e8503 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -682,6 +682,51 @@ def generate_test_by_comp() -> ssp.ByComponent: return by_comp +def generate_test_inheritance_md( + provided_uuid: str, responsibility_uuid: str, leveraged_statement_names: List[str] +) -> str: + """ + Generate a inheritance statement with placeholders replaced by provided values. + + Args: + provided_uuid (str): UUID for provided statement. + responsibility_uuid (str): UUID for responsibility statement. + leveraged_statement_names (list of str): Names for leveraged statements (as a list). + leveraged_ssp_href (str): Href for leveraged SSP. + + Returns: + str: The template with placeholders replaced. + """ + # Convert the list of leveraged statement names into a YAML list + leveraged_statement_list = '\n'.join([f' - name: {name}' for name in leveraged_statement_names]) + + md_template = f"""--- +x-trestle-statement: + # Add or modify leveraged SSP Statements here. + provided-uuid: {provided_uuid} + responsibility-uuid: {responsibility_uuid} +x-trestle-leveraging-comp: + # Leveraged statements can be optionally associated with components in this system. + # Associate leveraged statements to Components of this system here: +{leveraged_statement_list} +--- + +# Provided Statement Description + +provided statement description + +# Responsibility Statement Description + +resp statement description + +# Satisfied Statement Description + + +My Satisfied Description + """ + return md_template + + class FileChecker: """Check for changes in files after test operations.""" diff --git a/tests/trestle/core/crm/exports_reader_test.py b/tests/trestle/core/crm/exports_reader_test.py index b576b8862..e90b73897 100644 --- a/tests/trestle/core/crm/exports_reader_test.py +++ b/tests/trestle/core/crm/exports_reader_test.py @@ -29,90 +29,21 @@ expected_appliance_uuid = '22222222-0000-4000-9001-000000000003' expected_saas_uuid = '22222222-0000-4000-9001-000000000001' -inheritance_text = """--- -x-trestle-statement: - # Add or modify leveraged SSP Statements here. - provided-uuid: 18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114 - responsibility-uuid: 4b34c68f-75fa-4b38-baf0-e50158c13ac2 -x-trestle-leveraging-comp: - # Leveraged statements can be optionally associated with components in this system. - # Associate leveraged statements to Components of this system here: - - name: Access Control Appliance - - name: THIS SYSTEM (SaaS) ---- +example_provided_uuid = '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' +example_responsibility_uuid = '4b34c68f-75fa-4b38-baf0-e50158c13ac2' -# Provided Statement Description -provided statement description - -# Responsibility Statement Description - -resp statement description - -# Satisfied Statement Description - - -My Satisfied Description -""" - -inheritance_text_2 = """--- -x-trestle-statement: - # Add or modify leveraged SSP Statements here. - provided-uuid: 18ac4e2a-b5f2-46e4-94fa-cc84ab6fe115 - responsibility-uuid: 4b34c68f-75fa-4b38-baf0-e50158c13ac3 -x-trestle-leveraging-comp: - # Leveraged statements can be optionally associated with components in this system. - # Associate leveraged statements to Components of this system here: - - name: Access Control Appliance ---- - -# Provided Statement Description - -provided statement description - -# Responsibility Statement Description - -resp statement description - -# Satisfied Statement Description - - -My Satisfied Description -""" - -unmapped_inheritance = """--- -x-trestle-statement: - # Add or modify leveraged SSP Statements here. - provided-uuid: 18ac4e2a-b5f2-46e4-94fa-cc84ab6fe115 - responsibility-uuid: 4b34c68f-75fa-4b38-baf0-e50158c13ac3 -x-trestle-leveraging-comp: - # Leveraged statements can be optionally associated with components in this system. - # Associate leveraged statements to Components of this system here: - - name: REPLACE_ME ---- - -# Provided Statement Description - -provided statement description - -# Responsibility Statement Description - -resp statement description - -# Satisfied Statement Description - - -""" - - -def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: - """Test exports reader with inheritance view.""" - ipath = tmp_trestle_dir.joinpath(leveraging_ssp, const.INHERITANCE_VIEW_DIR) - - ac_appliance_dir = ipath.joinpath('Access Control Appliance') +def prep_inheritance_dir(ac_appliance_dir: pathlib.Path) -> None: + """Prepare inheritance directory with basic information.""" ac_2 = ac_appliance_dir.joinpath('ac-2') ac_2.mkdir(parents=True) + inheritance_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Access Control Appliance', 'THIS SYSTEM (SaaS)'] + ) + file = ac_2 / f'{expected_appliance_uuid}.md' with open(file, 'w') as f: f.write(inheritance_text) @@ -125,6 +56,13 @@ def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: with open(file, 'w') as f: f.write(inheritance_text) + +def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: + """Test exports reader with inheritance view.""" + inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) + ac_appliance_dir = inheritance_path.joinpath('Access Control Appliance') + prep_inheritance_dir(ac_appliance_dir) + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) orig_ssp, _ = ModelUtils.load_model_for_class( @@ -133,7 +71,7 @@ def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: ossp.SystemSecurityPlan, FileContentType.JSON) - reader = exportreader.ExportReader(ipath, orig_ssp) # type: ignore + reader = exportreader.ExportReader(inheritance_path, orig_ssp) # type: ignore ssp = reader.read_exports_from_markdown() implemented_requirements = ssp.control_implementation.implemented_requirements @@ -161,22 +99,22 @@ def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: """Test reading inheritance view directory.""" - ipath = tmp_trestle_dir.joinpath(leveraging_ssp, const.INHERITANCE_VIEW_DIR) - ac_appliance_dir = ipath.joinpath('Access Control Appliance') + inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) + ac_appliance_dir = inheritance_path.joinpath('Access Control Appliance') + prep_inheritance_dir(ac_appliance_dir) - ac_2 = ac_appliance_dir.joinpath('ac-2') - ac_2.mkdir(parents=True) - - file = ac_2 / f'{expected_appliance_uuid}.md' - with open(file, 'w') as f: - f.write(inheritance_text) + unmapped_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=[const.REPLACE_ME] + ) ac_21 = ac_appliance_dir.joinpath('ac-2.1') ac_21.mkdir(parents=True) # Ensure this file does not get added to the dictionary file = ac_21 / f'{expected_appliance_uuid}.md' with open(file, 'w') as f: - f.write(unmapped_inheritance) + f.write(unmapped_text) test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) @@ -186,10 +124,10 @@ def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: ossp.SystemSecurityPlan, FileContentType.JSON) - reader = exportreader.ExportReader(ipath, orig_ssp) # type: ignore + reader = exportreader.ExportReader(inheritance_path, orig_ssp) # type: ignore markdown_dict: exportreader.InheritanceViewDict = reader._read_inheritance_markdown_directory() - assert len(markdown_dict) == 1 + assert len(markdown_dict) == 2 assert 'ac-2' in markdown_dict assert len(markdown_dict['ac-2']) == 2 assert expected_appliance_uuid in markdown_dict['ac-2'] @@ -203,17 +141,18 @@ def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_trestle_dir: pathlib.Path) -> None: """Test reading inheritance view directory with components that span multiple leveraged components.""" - ipath = tmp_trestle_dir.joinpath(leveraging_ssp, const.INHERITANCE_VIEW_DIR) + inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) - ac_appliance_dir = ipath.joinpath('Access Control Appliance') - ac_2 = ac_appliance_dir.joinpath('ac-2') - ac_2.mkdir(parents=True) + ac_appliance_dir = inheritance_path.joinpath('Access Control Appliance') + prep_inheritance_dir(ac_appliance_dir) - file = ac_2 / f'{expected_appliance_uuid}.md' - with open(file, 'w') as f: - f.write(inheritance_text) + inheritance_text_2 = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Access Control Appliance'] + ) - this_system_dir = ipath.joinpath('This System') + this_system_dir = inheritance_path.joinpath('This System') ac_2 = this_system_dir.joinpath('ac-2') ac_2.mkdir(parents=True) @@ -229,10 +168,10 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr ossp.SystemSecurityPlan, FileContentType.JSON) - reader = exportreader.ExportReader(ipath, orig_ssp) # type: ignore + reader = exportreader.ExportReader(inheritance_path, orig_ssp) # type: ignore markdown_dict: exportreader.InheritanceViewDict = reader._read_inheritance_markdown_directory() - assert len(markdown_dict) == 1 + assert len(markdown_dict) == 2 assert 'ac-2' in markdown_dict assert len(markdown_dict['ac-2']) == 2 From 648085d7cbbae993f430102f1c6a0ecdb6b6c3bf Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Fri, 22 Sep 2023 10:15:54 -0400 Subject: [PATCH 17/24] refactor: reduce code duplication in ExportReader methods Signed-off-by: Jennifer Power --- tests/trestle/core/crm/exports_reader_test.py | 46 ++++++++++++++ trestle/core/crm/export_reader.py | 63 ++++++++----------- 2 files changed, 72 insertions(+), 37 deletions(-) diff --git a/tests/trestle/core/crm/exports_reader_test.py b/tests/trestle/core/crm/exports_reader_test.py index e90b73897..b4e397ca7 100644 --- a/tests/trestle/core/crm/exports_reader_test.py +++ b/tests/trestle/core/crm/exports_reader_test.py @@ -14,11 +14,15 @@ """Tests for the ssp_generator module.""" import pathlib +import uuid + +import pytest from tests import test_utils import trestle.common.const as const import trestle.core.crm.export_reader as exportreader +import trestle.core.generators as gens import trestle.oscal.ssp as ossp from trestle.common.model_utils import ModelUtils from trestle.core.models.file_content_type import FileContentType @@ -33,6 +37,16 @@ example_responsibility_uuid = '4b34c68f-75fa-4b38-baf0-e50158c13ac2' +@pytest.fixture(scope='function') +def sample_implemented_requirement() -> ossp.ImplementedRequirement: + """Return a valid ComponentDefinition object with some contents.""" + # one component has no properties - the other has two + impl_req: ossp.ImplementedRequirement = gens.generate_sample_model(ossp.ImplementedRequirement) + by_comp: ossp.ByComponent = gens.generate_sample_model(ossp.ByComponent) + impl_req.by_components = [by_comp] + return impl_req + + def prep_inheritance_dir(ac_appliance_dir: pathlib.Path) -> None: """Prepare inheritance directory with basic information.""" ac_2 = ac_appliance_dir.joinpath('ac-2') @@ -180,3 +194,35 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr assert len(inheritance_info[0]) == 2 assert len(inheritance_info[1]) == 2 + + +def test_update_type_with_by_comp(sample_implemented_requirement: ossp.ImplementedRequirement) -> None: + """Test update type with by component.""" + test_ssp: ossp.SystemSecurityPlan = gens.generate_sample_model(ossp.SystemSecurityPlan) + reader = exportreader.ExportReader('', test_ssp) + + test_inherited: ossp.Inherited = gens.generate_sample_model(ossp.Inherited) + test_satisfied: ossp.Satisfied = gens.generate_sample_model(ossp.Satisfied) + + test_comp_uuid = str(uuid.uuid4()) + + test_by_comp_dict: exportreader.ByComponentDict = {test_comp_uuid: ([test_inherited], [test_satisfied])} + + assert len(sample_implemented_requirement.by_components) == 1 + + reader._update_type_with_by_comp(sample_implemented_requirement, test_by_comp_dict) + + # Ensure a new by_comp was added, but the original was not removed + assert len(sample_implemented_requirement.by_components) == 2 + + # Test update the existing without adding a new component + test_satisfied.description = 'Updated Description' + test_by_comp_dict: exportreader.ByComponentDict = {test_comp_uuid: ([test_inherited], [test_satisfied])} + reader._update_type_with_by_comp(sample_implemented_requirement, test_by_comp_dict) + + assert len(sample_implemented_requirement.by_components) == 2 + new_by_comp = sample_implemented_requirement.by_components[1] # type: ignore + + assert new_by_comp.component_uuid == test_comp_uuid + assert new_by_comp.satisfied is not None + assert new_by_comp.satisfied[0].description == 'Updated Description' diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py index dcf88519a..ae7d827b0 100644 --- a/trestle/core/crm/export_reader.py +++ b/trestle/core/crm/export_reader.py @@ -20,6 +20,7 @@ import trestle.core.generators as gens import trestle.oscal.ssp as ossp +from trestle.common.common_types import TypeWithByComps from trestle.common.list_utils import as_list, none_if_empty from trestle.core.crm.bycomp_interface import ByComponentInterface from trestle.core.crm.leveraged_statements import InheritanceMarkdownReader @@ -89,27 +90,9 @@ def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceView # If the control id existing in the markdown, then update the by_components if implemented_requirement.control_id in markdown_dict: - new_by_comp: List[ossp.ByComponent] = [] by_comp_dict: ByComponentDict = markdown_dict[implemented_requirement.control_id] - for by_comp in as_list(implemented_requirement.by_components): - - if by_comp.component_uuid in by_comp_dict: - comp_inheritance_info = by_comp_dict[by_comp.component_uuid] - - bycomp_interface = ByComponentInterface(by_comp) - by_comp = bycomp_interface.reconcile_inheritance_by_component( - comp_inheritance_info[0], comp_inheritance_info[1] - ) - - # Delete the entry from the by_comp_dict once processed to avoid duplicates - del by_comp_dict[by_comp.component_uuid] - - new_by_comp.append(by_comp) - - # Add any new by_components that were not in the original implemented requirement - new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) - implemented_requirement.by_components = new_by_comp + self._update_type_with_by_comp(implemented_requirement, by_comp_dict) # Delete the entry from the markdown_dict once processed to avoid duplicates del markdown_dict[implemented_requirement.control_id] @@ -123,34 +106,40 @@ def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceView # If the statement id existing in the markdown, then update the by_components if statement_id in markdown_dict: - new_by_comp: List[ossp.ByComponent] = [] by_comp_dict: ByComponentDict = markdown_dict[statement_id] - for by_comp in as_list(stm.by_components): + self._update_type_with_by_comp(stm, by_comp_dict) - if by_comp.component_uuid in by_comp_dict: - comp_inheritance_info = by_comp_dict[by_comp.component_uuid] + # Delete the entry from the markdown_dict once processed to avoid duplicates + del markdown_dict[statement_id] - bycomp_interface = ByComponentInterface(by_comp) - by_comp = bycomp_interface.reconcile_inheritance_by_component( - comp_inheritance_info[0], comp_inheritance_info[1] - ) + new_statements.append(stm) - # Delete the entry from the by_comp_dict once processed to avoid duplicates - del by_comp_dict[by_comp.component_uuid] + implemented_requirement.statements = none_if_empty(new_statements) - new_by_comp.append(by_comp) + def _update_type_with_by_comp(self, with_bycomp: TypeWithByComps, by_comp_dict: ByComponentDict) -> None: + """Update the by_components for a type with by_components.""" + new_by_comp: List[ossp.ByComponent] = [] - # Add any new by_components that were not in the original statement - new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) - stm.by_components = new_by_comp + by_comp: ossp.ByComponent + for by_comp in as_list(with_bycomp.by_components): - # Delete the entry from the markdown_dict once processed to avoid duplicates - del markdown_dict[statement_id] + if by_comp.component_uuid in by_comp_dict: + comp_inheritance_info = by_comp_dict[by_comp.component_uuid] - new_statements.append(stm) + bycomp_interface = ByComponentInterface(by_comp) + by_comp = bycomp_interface.reconcile_inheritance_by_component( + comp_inheritance_info[0], comp_inheritance_info[1] + ) - implemented_requirement.statements = none_if_empty(new_statements) + # Delete the entry from the by_comp_dict once processed to avoid duplicates + del by_comp_dict[by_comp.component_uuid] + + new_by_comp.append(by_comp) + + # Add any new by_components that were not in the original statement + new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) + with_bycomp.by_components = none_if_empty(new_by_comp) def _add_control_mappings_to_implemented_requirements( self, control_mapping: str, by_comps: ByComponentDict From 6de26a1958f2ec9f85d68018ef75a94bf78786b8 Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Mon, 25 Sep 2023 12:22:00 -0400 Subject: [PATCH 18/24] fix: allows inheritance info to be removed when component is unmapped Signed-off-by: Jennifer Power --- tests/trestle/core/crm/exports_reader_test.py | 37 ++++++++++++++++++- trestle/core/crm/export_reader.py | 26 +++++++------ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/tests/trestle/core/crm/exports_reader_test.py b/tests/trestle/core/crm/exports_reader_test.py index b4e397ca7..7a33929ce 100644 --- a/tests/trestle/core/crm/exports_reader_test.py +++ b/tests/trestle/core/crm/exports_reader_test.py @@ -141,11 +141,13 @@ def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: reader = exportreader.ExportReader(inheritance_path, orig_ssp) # type: ignore markdown_dict: exportreader.InheritanceViewDict = reader._read_inheritance_markdown_directory() - assert len(markdown_dict) == 2 + assert len(markdown_dict) == 3 assert 'ac-2' in markdown_dict assert len(markdown_dict['ac-2']) == 2 assert expected_appliance_uuid in markdown_dict['ac-2'] + assert len(markdown_dict['ac-2.1']) == 0 + inheritance_info = markdown_dict['ac-2'][expected_appliance_uuid] assert inheritance_info[0][0].provided_uuid == '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' @@ -166,6 +168,12 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr leveraged_statement_names=['Access Control Appliance'] ) + unmapped_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=[const.REPLACE_ME] + ) + this_system_dir = inheritance_path.joinpath('This System') ac_2 = this_system_dir.joinpath('ac-2') ac_2.mkdir(parents=True) @@ -174,6 +182,13 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr with open(file, 'w') as f: f.write(inheritance_text_2) + ac_2a = this_system_dir.joinpath('ac-2_smt.a') + ac_2a.mkdir(parents=True) + + file = ac_2a / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(unmapped_text) + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) orig_ssp, _ = ModelUtils.load_model_for_class( @@ -195,6 +210,16 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr assert len(inheritance_info[0]) == 2 assert len(inheritance_info[1]) == 2 + assert 'ac-2_smt.a' in markdown_dict + assert len(markdown_dict['ac-2_smt.a']) == 2 + + assert expected_appliance_uuid in markdown_dict['ac-2_smt.a'] + inheritance_info = markdown_dict['ac-2_smt.a'][expected_appliance_uuid] + + # Only leveraging from one component + assert len(inheritance_info[0]) == 1 + assert len(inheritance_info[1]) == 1 + def test_update_type_with_by_comp(sample_implemented_requirement: ossp.ImplementedRequirement) -> None: """Test update type with by component.""" @@ -226,3 +251,13 @@ def test_update_type_with_by_comp(sample_implemented_requirement: ossp.Implement assert new_by_comp.component_uuid == test_comp_uuid assert new_by_comp.satisfied is not None assert new_by_comp.satisfied[0].description == 'Updated Description' + + # Test removing the existing inheritance info + test_by_comp_dict: exportreader.ByComponentDict = {} + reader._update_type_with_by_comp(sample_implemented_requirement, test_by_comp_dict) + + new_by_comp = sample_implemented_requirement.by_components[1] # type: ignore + + assert new_by_comp.component_uuid == test_comp_uuid + assert new_by_comp.satisfied is None + assert new_by_comp.inherited is None diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py index ae7d827b0..97b9f62bf 100644 --- a/trestle/core/crm/export_reader.py +++ b/trestle/core/crm/export_reader.py @@ -77,8 +77,9 @@ def read_exports_from_markdown(self) -> ossp.SystemSecurityPlan: # Process remaining markdown information that was not in the implemented requirements for control_id, by_comp_dict in markdown_dict.items(): - logging.debug(f'Adding control mapping {control_id} to implemented requirements') - self._add_control_mappings_to_implemented_requirements(control_id, by_comp_dict) + if by_comp_dict: + logging.debug(f'Adding control mapping {control_id} to implemented requirements') + self._add_control_mappings_to_implemented_requirements(control_id, by_comp_dict) self._ssp.control_implementation.implemented_requirements = list(self._implemented_requirements.values()) return self._ssp @@ -124,17 +125,19 @@ def _update_type_with_by_comp(self, with_bycomp: TypeWithByComps, by_comp_dict: by_comp: ossp.ByComponent for by_comp in as_list(with_bycomp.by_components): + # If the by_component uuid exists in the by_comp_dict, then update it + # If not, clear the by_component inheritance information + comp_inheritance_info: Tuple[List[ossp.Inherited], List[ossp.Satisfied]] = ([], []) if by_comp.component_uuid in by_comp_dict: comp_inheritance_info = by_comp_dict[by_comp.component_uuid] - - bycomp_interface = ByComponentInterface(by_comp) - by_comp = bycomp_interface.reconcile_inheritance_by_component( - comp_inheritance_info[0], comp_inheritance_info[1] - ) - # Delete the entry from the by_comp_dict once processed to avoid duplicates del by_comp_dict[by_comp.component_uuid] + bycomp_interface = ByComponentInterface(by_comp) + by_comp = bycomp_interface.reconcile_inheritance_by_component( + comp_inheritance_info[0], comp_inheritance_info[1] + ) + new_by_comp.append(by_comp) # Add any new by_components that were not in the original statement @@ -224,7 +227,8 @@ def _read_inheritance_markdown_directory(self) -> InheritanceViewDict: satisfied.append(leveraged_info.satisfied) by_comp_dict[comp_uuid] = (inherited, satisfied) - # If there is information in the by_component dictionary, then update the markdown dictionary - if by_comp_dict: - markdown_dict[control_dir] = by_comp_dict + + # Add the by_component dictionary to the markdown dictionary for the control directory + markdown_dict[control_dir] = by_comp_dict + return markdown_dict From 8442cfeb26f5612f39947bbde4be59733b4d4ac4 Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Thu, 28 Sep 2023 16:46:52 -0400 Subject: [PATCH 19/24] feat: adds leveraged authorization updates to system implementation Adds SSPInheritanceAPI class for interacting with leveraged auth information Adds trestle global tags to markdown to store SSP location info Signed-off-by: Jennifer Power Co-authored-by: Alex Flom --- tests/test_utils.py | 5 +- tests/trestle/core/crm/exports_reader_test.py | 151 +++++++++++++-- tests/trestle/core/crm/exports_writer_test.py | 9 +- .../core/crm/leveraged_statements_test.py | 19 +- .../core/crm/ssp_inheritance_api_test.py | 174 +++++++++++++++++ trestle/common/const.py | 8 + trestle/core/commands/author/ssp.py | 39 +--- trestle/core/crm/export_reader.py | 89 ++++++--- trestle/core/crm/export_writer.py | 17 +- trestle/core/crm/leveraged_statements.py | 43 +++- trestle/core/crm/ssp_inheritance_api.py | 183 ++++++++++++++++++ 11 files changed, 637 insertions(+), 100 deletions(-) create mode 100644 tests/trestle/core/crm/ssp_inheritance_api_test.py create mode 100644 trestle/core/crm/ssp_inheritance_api.py diff --git a/tests/test_utils.py b/tests/test_utils.py index c044e8503..21a642ef4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -683,7 +683,7 @@ def generate_test_by_comp() -> ssp.ByComponent: def generate_test_inheritance_md( - provided_uuid: str, responsibility_uuid: str, leveraged_statement_names: List[str] + provided_uuid: str, responsibility_uuid: str, leveraged_statement_names: List[str], leveraged_ssp_href: str ) -> str: """ Generate a inheritance statement with placeholders replaced by provided values. @@ -709,6 +709,9 @@ def generate_test_inheritance_md( # Leveraged statements can be optionally associated with components in this system. # Associate leveraged statements to Components of this system here: {leveraged_statement_list} +x-trestle-global: + leveraged-ssp: + href: {leveraged_ssp_href} --- # Provided Statement Description diff --git a/tests/trestle/core/crm/exports_reader_test.py b/tests/trestle/core/crm/exports_reader_test.py index 7a33929ce..d72457a3b 100644 --- a/tests/trestle/core/crm/exports_reader_test.py +++ b/tests/trestle/core/crm/exports_reader_test.py @@ -24,6 +24,7 @@ import trestle.core.crm.export_reader as exportreader import trestle.core.generators as gens import trestle.oscal.ssp as ossp +from trestle.common.err import TrestleError from trestle.common.model_utils import ModelUtils from trestle.core.models.file_content_type import FileContentType @@ -47,17 +48,11 @@ def sample_implemented_requirement() -> ossp.ImplementedRequirement: return impl_req -def prep_inheritance_dir(ac_appliance_dir: pathlib.Path) -> None: +def prep_inheritance_dir(ac_appliance_dir: pathlib.Path, inheritance_text: str) -> None: """Prepare inheritance directory with basic information.""" ac_2 = ac_appliance_dir.joinpath('ac-2') ac_2.mkdir(parents=True) - inheritance_text = test_utils.generate_test_inheritance_md( - provided_uuid=example_provided_uuid, - responsibility_uuid=example_responsibility_uuid, - leveraged_statement_names=['Access Control Appliance', 'THIS SYSTEM (SaaS)'] - ) - file = ac_2 / f'{expected_appliance_uuid}.md' with open(file, 'w') as f: f.write(inheritance_text) @@ -75,7 +70,13 @@ def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: """Test exports reader with inheritance view.""" inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) ac_appliance_dir = inheritance_path.joinpath('Access Control Appliance') - prep_inheritance_dir(ac_appliance_dir) + inheritance_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Access Control Appliance', 'THIS SYSTEM (SaaS)'], + leveraged_ssp_href='trestle://leveraged_ssp.json' + ) + prep_inheritance_dir(ac_appliance_dir, inheritance_text) test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) @@ -85,7 +86,7 @@ def test_read_exports_from_markdown(tmp_trestle_dir: pathlib.Path) -> None: ossp.SystemSecurityPlan, FileContentType.JSON) - reader = exportreader.ExportReader(inheritance_path, orig_ssp) # type: ignore + reader = exportreader.ExportReader(inheritance_path, orig_ssp) ssp = reader.read_exports_from_markdown() implemented_requirements = ssp.control_implementation.implemented_requirements @@ -115,12 +116,19 @@ def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: """Test reading inheritance view directory.""" inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) ac_appliance_dir = inheritance_path.joinpath('Access Control Appliance') - prep_inheritance_dir(ac_appliance_dir) + inheritance_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Access Control Appliance', 'THIS SYSTEM (SaaS)'], + leveraged_ssp_href='trestle://leveraged_ssp.json' + ) + prep_inheritance_dir(ac_appliance_dir, inheritance_text) unmapped_text = test_utils.generate_test_inheritance_md( provided_uuid=example_provided_uuid, responsibility_uuid=example_responsibility_uuid, - leveraged_statement_names=[const.REPLACE_ME] + leveraged_statement_names=[const.REPLACE_ME], + leveraged_ssp_href='trestle://leveraged_ssp.json' ) ac_21 = ac_appliance_dir.joinpath('ac-2.1') @@ -138,7 +146,7 @@ def test_read_inheritance_markdown_dir(tmp_trestle_dir: pathlib.Path) -> None: ossp.SystemSecurityPlan, FileContentType.JSON) - reader = exportreader.ExportReader(inheritance_path, orig_ssp) # type: ignore + reader = exportreader.ExportReader(inheritance_path, orig_ssp) markdown_dict: exportreader.InheritanceViewDict = reader._read_inheritance_markdown_directory() assert len(markdown_dict) == 3 @@ -160,18 +168,26 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) ac_appliance_dir = inheritance_path.joinpath('Access Control Appliance') - prep_inheritance_dir(ac_appliance_dir) + inheritance_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Access Control Appliance', 'THIS SYSTEM (SaaS)'], + leveraged_ssp_href='trestle://leveraged_ssp.json' + ) + prep_inheritance_dir(ac_appliance_dir, inheritance_text) inheritance_text_2 = test_utils.generate_test_inheritance_md( provided_uuid=example_provided_uuid, responsibility_uuid=example_responsibility_uuid, - leveraged_statement_names=['Access Control Appliance'] + leveraged_statement_names=['Access Control Appliance'], + leveraged_ssp_href='trestle://leveraged_ssp.json' ) unmapped_text = test_utils.generate_test_inheritance_md( provided_uuid=example_provided_uuid, responsibility_uuid=example_responsibility_uuid, - leveraged_statement_names=[const.REPLACE_ME] + leveraged_statement_names=[const.REPLACE_ME], + leveraged_ssp_href='trestle://leveraged_ssp.json' ) this_system_dir = inheritance_path.joinpath('This System') @@ -197,7 +213,7 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr ossp.SystemSecurityPlan, FileContentType.JSON) - reader = exportreader.ExportReader(inheritance_path, orig_ssp) # type: ignore + reader = exportreader.ExportReader(inheritance_path, orig_ssp) markdown_dict: exportreader.InheritanceViewDict = reader._read_inheritance_markdown_directory() assert len(markdown_dict) == 2 @@ -221,6 +237,109 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr assert len(inheritance_info[1]) == 1 +def test_read_inheritance_markdown_dir_with_invalid_mapping(tmp_trestle_dir: pathlib.Path) -> None: + """Test reading inheritance view directory with a component that does not exist.""" + inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) + + invalid_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Invalid Component'], + leveraged_ssp_href='trestle://leveraged_ssp.json' + ) + + this_system_dir = inheritance_path.joinpath('This System') + ac_2 = this_system_dir.joinpath('ac-2') + ac_2.mkdir(parents=True) + + file = ac_2 / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(invalid_text) + + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) + + orig_ssp, _ = ModelUtils.load_model_for_class( + tmp_trestle_dir, + leveraging_ssp, + ossp.SystemSecurityPlan, + FileContentType.JSON) + + with pytest.raises(TrestleError): + reader = exportreader.ExportReader(inheritance_path, orig_ssp) + _ = reader._read_inheritance_markdown_directory() + + +def test_get_leveraged_ssp_reference(tmp_trestle_dir: pathlib.Path) -> None: + """Test retrieving leveraged SSP reference from Markdown.""" + inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) + + ac_appliance_dir = inheritance_path.joinpath('Access Control Appliance') + inheritance_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Access Control Appliance', 'THIS SYSTEM (SaaS)'], + leveraged_ssp_href='trestle://leveraged_ssp.json' + ) + prep_inheritance_dir(ac_appliance_dir, inheritance_text) + + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) + + orig_ssp, _ = ModelUtils.load_model_for_class( + tmp_trestle_dir, + leveraging_ssp, + ossp.SystemSecurityPlan, + FileContentType.JSON) + + reader = exportreader.ExportReader(inheritance_path, orig_ssp) + assert reader.get_leveraged_ssp_href() == 'trestle://leveraged_ssp.json' + + +def test_get_leveraged_components(tmp_trestle_dir: pathlib.Path) -> None: + """Test leveraged mapped components from Markdown.""" + inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) + + ac_appliance_dir = inheritance_path.joinpath('Access Control Appliance') + unmapped_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=[const.REPLACE_ME], + leveraged_ssp_href='trestle://leveraged_ssp.json' + ) + prep_inheritance_dir(ac_appliance_dir, unmapped_text) + + this_system_dir = inheritance_path.joinpath('This System') + ac_2 = this_system_dir.joinpath('ac-2') + ac_2.mkdir(parents=True) + + inheritance_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Access Control Appliance', 'THIS SYSTEM (SaaS)'], + leveraged_ssp_href='trestle://leveraged_ssp.json' + ) + + file = ac_2 / f'{expected_appliance_uuid}.md' + with open(file, 'w') as f: + f.write(inheritance_text) + + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) + + orig_ssp, _ = ModelUtils.load_model_for_class( + tmp_trestle_dir, + leveraging_ssp, + ossp.SystemSecurityPlan, + FileContentType.JSON) + + reader = exportreader.ExportReader(inheritance_path, orig_ssp) + _ = reader.read_exports_from_markdown() + + leveraged_components = reader.get_leveraged_components() + + assert len(leveraged_components) == 1 + assert 'Access Control Appliance' not in leveraged_components + assert 'This System' in leveraged_components + + def test_update_type_with_by_comp(sample_implemented_requirement: ossp.ImplementedRequirement) -> None: """Test update type with by component.""" test_ssp: ossp.SystemSecurityPlan = gens.generate_sample_model(ossp.SystemSecurityPlan) diff --git a/tests/trestle/core/crm/exports_writer_test.py b/tests/trestle/core/crm/exports_writer_test.py index fd61851ba..0c4dbfeca 100644 --- a/tests/trestle/core/crm/exports_writer_test.py +++ b/tests/trestle/core/crm/exports_writer_test.py @@ -39,6 +39,7 @@ test_profile = 'simple_test_profile' test_ssp = 'leveraged_ssp' +test_ref = 'trestle://leveraged_ssp.json' def custom_side_effect(file_path: pathlib.Path) -> None: @@ -53,7 +54,7 @@ def test_write_exports_as_markdown(tmp_trestle_dir: pathlib.Path) -> None: ssp, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, test_ssp, ossp.SystemSecurityPlan, FileContentType.JSON) inherited_path = tmp_trestle_dir.joinpath('inherited') - writer = ExportWriter(inherited_path, ssp) + writer = ExportWriter(inherited_path, ssp, test_ref) mock = Mock(spec=LeveragedStatements) mock.write_statement_md.side_effect = custom_side_effect @@ -77,7 +78,7 @@ def test_write_exports_as_markdown_invalid_ssp(tmp_trestle_dir: pathlib.Path) -> ssp.system_implementation.components.remove(ssp.system_implementation.components[2]) inherited_path = tmp_trestle_dir.joinpath('inherited') - writer = ExportWriter(inherited_path, ssp) + writer = ExportWriter(inherited_path, ssp, test_ref) with pytest.raises(TrestleError, match=r'Component .* is not in the system implementation'): writer.write_exports_as_markdown() @@ -92,7 +93,7 @@ def test_statement_types_from_exports(tmp_trestle_dir: pathlib.Path) -> None: ssp = gens.generate_sample_model(ossp.SystemSecurityPlan) inherited_path = tmp_trestle_dir.joinpath('inherited') - writer = ExportWriter(inherited_path, ssp) + writer = ExportWriter(inherited_path, ssp, test_ref) by_comp: ossp.ByComponent = test_utils.generate_test_by_comp() bycomp_interface: ByComponentInterface = ByComponentInterface(by_comp) @@ -120,7 +121,7 @@ def test_statement_types_no_exports(tmp_trestle_dir: pathlib.Path) -> None: ssp = gens.generate_sample_model(ossp.SystemSecurityPlan) inherited_path = tmp_trestle_dir.joinpath('inherited') - writer = ExportWriter(inherited_path, ssp) + writer = ExportWriter(inherited_path, ssp, test_ref) by_comp = gens.generate_sample_model(ossp.ByComponent) bycomp_interface: ByComponentInterface = ByComponentInterface(by_comp) diff --git a/tests/trestle/core/crm/leveraged_statements_test.py b/tests/trestle/core/crm/leveraged_statements_test.py index 253e5ab6a..4edd64885 100644 --- a/tests/trestle/core/crm/leveraged_statements_test.py +++ b/tests/trestle/core/crm/leveraged_statements_test.py @@ -31,6 +31,8 @@ resp_statement_desc = 'resp statement description' satisfied_statement_desc = 'satisfied statement description' +test_href = 'trestle://ssp/ssp.json' + def add_authored_content(test_file: pathlib.Path, yaml_header: Dict[str, Any]) -> None: """Update the yaml header with a test component and satisfied description to simulate editing.""" @@ -47,7 +49,7 @@ def test_write_inheritance_tree(tmp_path: pathlib.Path) -> None: """Test writing statements with both provided and responsibility.""" statement_tree_path = tmp_path.joinpath('statement_tree.md') - statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc) + statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc, test_href) statement.write_statement_md(statement_tree_path) @@ -61,6 +63,7 @@ def test_write_inheritance_tree(tmp_path: pathlib.Path) -> None: assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == provided_uuid assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == resp_uuid + assert header[const.TRESTLE_GLOBAL_TAG][const.LEVERAGED_SSP][const.HREF] == test_href # Confirm markdown content node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) @@ -94,7 +97,7 @@ def test_write_inheritance_provided(tmp_path: pathlib.Path) -> None: """Test writing statements with only provided.""" statement_provided_path = tmp_path.joinpath('statement_provided.md') - statement = StatementProvided(provided_uuid, provided_statement_desc) + statement = StatementProvided(provided_uuid, provided_statement_desc, test_href) statement.write_statement_md(statement_provided_path) @@ -107,6 +110,7 @@ def test_write_inheritance_provided(tmp_path: pathlib.Path) -> None: assert comp_header_value == [{'name': 'REPLACE_ME'}] assert header[const.TRESTLE_STATEMENT_TAG][const.PROVIDED_UUID] == provided_uuid + assert header[const.TRESTLE_GLOBAL_TAG][const.LEVERAGED_SSP][const.HREF] == test_href # Confirm markdown content node = tree.get_node_for_key(const.PROVIDED_STATEMENT_DESCRIPTION, False) @@ -132,7 +136,7 @@ def test_write_inheritance_responsibility(tmp_path: pathlib.Path) -> None: """Test writing statements with only responsibility.""" statement_resp_path = tmp_path.joinpath('statement_req.md') - statement = StatementResponsibility(resp_uuid, resp_statement_desc) + statement = StatementResponsibility(resp_uuid, resp_statement_desc, test_href) statement.write_statement_md(statement_resp_path) @@ -145,6 +149,7 @@ def test_write_inheritance_responsibility(tmp_path: pathlib.Path) -> None: assert comp_header_value == [{'name': 'REPLACE_ME'}] assert header[const.TRESTLE_STATEMENT_TAG][const.RESPONSIBILITY_UUID] == resp_uuid + assert header[const.TRESTLE_GLOBAL_TAG][const.LEVERAGED_SSP][const.HREF] == test_href # Confirm markdown content node = tree.get_node_for_key(const.RESPONSIBILITY_STATEMENT_DESCRIPTION, False) @@ -174,7 +179,7 @@ def test_process_leveraged_statement_default_mapping(tmp_path: pathlib.Path) -> """Test processing leveraged statement markdown with no set mapping.""" statement_tree_path = tmp_path.joinpath('statement_tree.md') - statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc) + statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc, test_href) statement.write_statement_md(statement_tree_path) @@ -193,7 +198,7 @@ def test_process_leveraged_statement_markdown_tree(tmp_path: pathlib.Path) -> No test_header: Dict[str, Any] = {} add_authored_content(statement_tree_path, test_header) - statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc) + statement = StatementTree(provided_uuid, provided_statement_desc, resp_uuid, resp_statement_desc, test_href) statement.write_statement_md(statement_tree_path) @@ -224,7 +229,7 @@ def test_process_leveraged_statement_markdown_provided(tmp_path: pathlib.Path) - test_header: Dict[str, Any] = {} add_authored_content(statement_provided_path, test_header) - statement = StatementProvided(provided_uuid, provided_statement_desc) + statement = StatementProvided(provided_uuid, provided_statement_desc, test_href) statement.write_statement_md(statement_provided_path) @@ -252,7 +257,7 @@ def test_process_leveraged_statement_markdown_responsibility(tmp_path: pathlib.P test_header: Dict[str, Any] = {} add_authored_content(statement_resp_path, test_header) - statement = StatementResponsibility(resp_uuid, resp_statement_desc) + statement = StatementResponsibility(resp_uuid, resp_statement_desc, test_href) statement.write_statement_md(statement_resp_path) diff --git a/tests/trestle/core/crm/ssp_inheritance_api_test.py b/tests/trestle/core/crm/ssp_inheritance_api_test.py new file mode 100644 index 000000000..f4ebbc704 --- /dev/null +++ b/tests/trestle/core/crm/ssp_inheritance_api_test.py @@ -0,0 +1,174 @@ +# -*- mode:python; coding:utf-8 -*- +# Copyright (c) 2020 IBM Corp. All rights reserved. +# +# 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 +# +# https://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. +"""Tests for SSP Inheritance API.""" + +import copy +import logging +import pathlib + +from tests import test_utils + +import trestle.oscal.ssp as ossp +from trestle.common import const +from trestle.common.model_utils import ModelUtils +from trestle.core.crm.ssp_inheritance_api import SSPInheritanceAPI +from trestle.core.models.file_content_type import FileContentType + +logger = logging.getLogger(__name__) + +leveraging_ssp = 'my_ssp' +leveraged_ssp = 'leveraged_ssp' + +expected_application_uuid = '11111111-0000-4000-9001-000000000002' +example_provided_uuid = '18ac4e2a-b5f2-46e4-94fa-cc84ab6fe114' +example_responsibility_uuid = '4b34c68f-75fa-4b38-baf0-e50158c13ac2' + + +def prep_dir(component_dir: pathlib.Path) -> None: + """Prep dir.""" + ac_2 = component_dir.joinpath('ac-2') + ac_2.mkdir(parents=True) + + inheritance_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=['Access Control Appliance', 'THIS SYSTEM (SaaS)'], + leveraged_ssp_href='trestle://system-security-plans/leveraged_ssp/system-security-plan.json' + ) + + file = ac_2 / f'{expected_application_uuid}.md' + with open(file, 'w') as f: + f.write(inheritance_text) + + # test with a statement + ac_2a = component_dir.joinpath('ac-2_smt.a') + ac_2a.mkdir(parents=True) + + file = ac_2a / f'{expected_application_uuid}.md' + with open(file, 'w') as f: + f.write(inheritance_text) + + +def unmapped_prep_dir(component_dir: pathlib.Path) -> None: + """Unmapped prep dir.""" + ac_2 = component_dir.joinpath('ac-2') + ac_2.mkdir(parents=True) + + unmapped_text = test_utils.generate_test_inheritance_md( + provided_uuid=example_provided_uuid, + responsibility_uuid=example_responsibility_uuid, + leveraged_statement_names=[const.REPLACE_ME], + leveraged_ssp_href='trestle://system-security-plans/leveraged_ssp/system-security-plan.json' + ) + + file = ac_2 / f'{expected_application_uuid}.md' + with open(file, 'w') as f: + f.write(unmapped_text) + + # test with a statement + ac_2a = component_dir.joinpath('ac-2_smt.a') + ac_2a.mkdir(parents=True) + + file = ac_2a / f'{expected_application_uuid}.md' + with open(file, 'w') as f: + f.write(unmapped_text) + + +def test_update_ssp_inheritance(tmp_trestle_dir: pathlib.Path) -> None: + """Test that a leveraged authorization is created.""" + inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) + application_dir = inheritance_path.joinpath('Application') + prep_dir(application_dir) + + test_utils.load_from_json(tmp_trestle_dir, 'leveraged_ssp', leveraged_ssp, ossp.SystemSecurityPlan) + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) + + orig_ssp, _ = ModelUtils.load_model_for_class( + tmp_trestle_dir, + leveraging_ssp, + ossp.SystemSecurityPlan, + FileContentType.JSON) + + components = orig_ssp.system_implementation.components + + assert len(components) == 5 + assert len(orig_ssp.system_implementation.leveraged_authorizations) == 1 + + ssp_inheritance_api = SSPInheritanceAPI(inheritance_path, tmp_trestle_dir) + ssp_inheritance_api.update_ssp_inheritance(orig_ssp) + + assert orig_ssp.system_implementation.leveraged_authorizations is not None + + assert len(orig_ssp.system_implementation.leveraged_authorizations) == 1 + + auth = orig_ssp.system_implementation.leveraged_authorizations[0] + + assert auth.links is not None + assert len(auth.links) == 1 + assert auth.links[0].href == 'trestle://system-security-plans/leveraged_ssp/system-security-plan.json' + + components = orig_ssp.system_implementation.components + + # This reduce to 4 by removing old leveraged components and adding application + assert len(components) == 4 + + # Verify that all expected components are present + component_titles = [obj.title for obj in components] + + assert 'Access Control Appliance' in component_titles + assert 'THIS SYSTEM (SaaS)' in component_titles + assert 'Application' in component_titles + assert 'This System' in component_titles + + assert components[3].title == 'Application' + assert components[3].props is not None + assert len(components[3].props) == 3 + assert components[3].props[0].name == 'implementation-point' + assert components[3].props[0].value == 'external' + assert components[3].props[1].name == 'leveraged-authorization-uuid' + assert components[3].props[1].value == auth.uuid + assert components[3].props[2].name == 'inherited-uuid' + assert components[3].props[2].value == expected_application_uuid + + # Run twice and assert with no changes that the ssp is the same + copy_ssp = copy.deepcopy(orig_ssp) + ssp_inheritance_api.update_ssp_inheritance(orig_ssp) + assert ModelUtils.models_are_equivalent(orig_ssp, copy_ssp) # type: ignore + + +def test_no_leveraged_comps(tmp_trestle_dir: pathlib.Path) -> None: + """Test that a leveraged authorization is not created.""" + inheritance_path = tmp_trestle_dir.joinpath(leveraged_ssp, const.INHERITANCE_VIEW_DIR) + application_dir = inheritance_path.joinpath('Application') + unmapped_prep_dir(application_dir) + + test_utils.load_from_json(tmp_trestle_dir, 'leveraged_ssp', leveraged_ssp, ossp.SystemSecurityPlan) + test_utils.load_from_json(tmp_trestle_dir, 'leveraging_ssp', leveraging_ssp, ossp.SystemSecurityPlan) + + orig_ssp, _ = ModelUtils.load_model_for_class( + tmp_trestle_dir, + leveraging_ssp, + ossp.SystemSecurityPlan, + FileContentType.JSON) + + components = orig_ssp.system_implementation.components + + assert len(components) == 5 + assert len(orig_ssp.system_implementation.leveraged_authorizations) == 1 + + ssp_inheritance_api = SSPInheritanceAPI(inheritance_path, tmp_trestle_dir) + ssp_inheritance_api.update_ssp_inheritance(orig_ssp) + + assert orig_ssp.system_implementation.leveraged_authorizations is None diff --git a/trestle/common/const.py b/trestle/common/const.py index 57b296e58..19b703704 100644 --- a/trestle/common/const.py +++ b/trestle/common/const.py @@ -288,6 +288,8 @@ PROFILE = 'profile' +LEVERAGED_SSP = 'leveraged-ssp' + TITLE = 'title' NAME = 'name' @@ -350,6 +352,12 @@ LEV_AUTH_UUID = 'leveraged-authorization-uuid' +INHERITED_UUID = 'inherited-uuid' + +IMPLEMENTATION_POINT = 'implementation-point' + +IMPLEMENTATION_POINT_EXTERNAL = 'external' + STATUS_INHERITED = 'inherited' STATUS_PARTIALLY_IMPLEMENTED = 'partially-implemented' diff --git a/trestle/core/commands/author/ssp.py b/trestle/core/commands/author/ssp.py index 1272e03e7..4066ddb3e 100644 --- a/trestle/core/commands/author/ssp.py +++ b/trestle/core/commands/author/ssp.py @@ -43,8 +43,7 @@ from trestle.core.control_context import ContextPurpose, ControlContext from trestle.core.control_interface import ControlInterface, ParameterRep from trestle.core.control_reader import ControlReader -from trestle.core.crm.export_reader import ExportReader -from trestle.core.crm.export_writer import ExportWriter +from trestle.core.crm.ssp_inheritance_api import SSPInheritanceAPI from trestle.core.models.file_content_type import FileContentType from trestle.core.profile_resolver import ProfileResolver from trestle.core.remote.cache import FetcherFactory @@ -214,40 +213,21 @@ def _generate_inheritance_markdown( control are present for mapping. """ # if file not recognized as URI form, assume it represents name of file in trestle directory - ssp: ossp.SystemSecurityPlan ssp_in_trestle_dir = '://' not in leveraged_ssp_name_or_href ssp_href = leveraged_ssp_name_or_href if ssp_in_trestle_dir: local_path = f'{const.MODEL_DIR_SSP}/{leveraged_ssp_name_or_href}/system-security-plan.json' - ssp_path = trestle_root / local_path - _, _, ssp = ModelUtils.load_distributed(ssp_path, trestle_root) - else: - fetcher = FetcherFactory.get_fetcher(trestle_root, ssp_href) - try: - ssp, _ = fetcher.get_oscal() - except TrestleError as e: - raise TrestleError(f'Unable to fetch ssp from {ssp_href}: {e}') + ssp_href = const.TRESTLE_HREF_HEADING + local_path inheritance_view_path: pathlib.Path = md_path.joinpath(const.INHERITANCE_VIEW_DIR) inheritance_view_path.mkdir(exist_ok=True) logger.debug(f'Creating content for inheritance view in {inheritance_view_path}') - # Filter the ssp implemented requirement by the catalog specified - catalog_api: CatalogAPI = CatalogAPI(catalog=resolved_catalog) - control_imp: ossp.ControlImplementation = ssp.control_implementation - - new_imp_requirements: List[ossp.ImplementedRequirement] = [] - for imp_requirement in as_list(control_imp.implemented_requirements): - control = catalog_api._catalog_interface.get_control(imp_requirement.control_id) - if control is not None: - new_imp_requirements.append(imp_requirement) - control_imp.implemented_requirements = new_imp_requirements - - ssp.control_implementation = control_imp + ssp_inheritance_api = SSPInheritanceAPI(inheritance_view_path, trestle_root) - export_writer: ExportWriter = ExportWriter(inheritance_view_path, ssp) - - export_writer.write_exports_as_markdown() + # Filter the ssp implemented requirements by the catalog specified + catalog_api: CatalogAPI = CatalogAPI(catalog=resolved_catalog) + ssp_inheritance_api.write_inheritance_as_markdown(ssp_href, catalog_api) class SSPAssemble(AuthorCommonCommand): @@ -619,10 +599,9 @@ def _run(self, args: argparse.Namespace) -> int: self._generate_roles_in_metadata(ssp) # If this is a leveraging SSP, update it with the retrieved the exports from the leveraged SSP - ipath = pathlib.Path(md_path, const.INHERITANCE_VIEW_DIR) - if os.path.exists(ipath): - reader = ExportReader(ipath, ssp) - ssp = reader.read_exports_from_markdown() + inheritance_markdown_path = md_path.joinpath(const.INHERITANCE_VIEW_DIR) + if os.path.exists(inheritance_markdown_path): + SSPInheritanceAPI(inheritance_markdown_path, trestle_root).update_ssp_inheritance(ssp) ssp.import_profile.href = profile_href diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py index 97b9f62bf..7f831de07 100644 --- a/trestle/core/crm/export_reader.py +++ b/trestle/core/crm/export_reader.py @@ -21,6 +21,7 @@ import trestle.core.generators as gens import trestle.oscal.ssp as ossp from trestle.common.common_types import TypeWithByComps +from trestle.common.err import TrestleError from trestle.common.list_utils import as_list, none_if_empty from trestle.core.crm.bycomp_interface import ByComponentInterface from trestle.core.crm.leveraged_statements import InheritanceMarkdownReader @@ -52,12 +53,20 @@ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): Arguments: root_path: A root path object where an SSP's inheritance markdown is located. ssp: A system security plan object that will be updated with the inheritance information. + + Notes: + The mapped components list is used to track which components have been mapped to controls in the markdown. + It can be retrieved with the get_leveraged_components method. This will be empty until the + read_exports_from_markdown method is called. """ self._ssp: ossp.SystemSecurityPlan = ssp # Create a dictionary of implemented requirements keyed by control id for merging operations self._implemented_requirements: Dict[str, ossp.ImplementedRequirement] = self._create_impl_req_dict() + # List of component titles that have been mapped to controls in the Markdown + self._mapped_components: List[str] = [] + self._root_path: pathlib.Path = root_path def _create_impl_req_dict(self) -> Dict[str, ossp.ImplementedRequirement]: @@ -84,6 +93,30 @@ def read_exports_from_markdown(self) -> ossp.SystemSecurityPlan: self._ssp.control_implementation.implemented_requirements = list(self._implemented_requirements.values()) return self._ssp + def get_leveraged_ssp_href(self) -> str: + """Get the href of the leveraged SSP from a markdown file.""" + comp_dirs = os.listdir(self._root_path) + if len(comp_dirs) == 0: + raise TrestleError('No components were found in the markdown.') + + control_dirs = os.listdir(self._root_path.joinpath(comp_dirs[0])) + if len(control_dirs) == 0: + raise TrestleError('No controls were found in the markdown for component {comp_dirs[0]}.') + + control_dir = self._root_path.joinpath(comp_dirs[0], control_dirs[0]) + + files = [f for f in os.listdir(control_dir) if os.path.isfile(os.path.join(control_dir, f))] + if len(files) == 0: + raise TrestleError(f'No files were found in the markdown for control {control_dirs[0]}.') + + markdown_file_path = control_dir.joinpath(files[0]) + reader = InheritanceMarkdownReader(markdown_file_path) + return reader.get_leveraged_ssp_reference() + + def get_leveraged_components(self) -> List[str]: + """Get a list of component titles that have been mapped to controls in the Markdown.""" + return self._mapped_components + def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceViewDict) -> None: """Merge all exported inheritance info from the markdown into the implemented requirement dict.""" for implemented_requirement in self._implemented_requirements.values(): @@ -91,13 +124,11 @@ def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceView # If the control id existing in the markdown, then update the by_components if implemented_requirement.control_id in markdown_dict: - by_comp_dict: ByComponentDict = markdown_dict[implemented_requirement.control_id] + # Delete the entry from the markdown_dict once processed to avoid duplicates + by_comp_dict: ByComponentDict = markdown_dict.pop(implemented_requirement.control_id) self._update_type_with_by_comp(implemented_requirement, by_comp_dict) - # Delete the entry from the markdown_dict once processed to avoid duplicates - del markdown_dict[implemented_requirement.control_id] - # Update any implemented requirements statements assemblies new_statements: List[ossp.Statement] = [] @@ -107,13 +138,11 @@ def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceView # If the statement id existing in the markdown, then update the by_components if statement_id in markdown_dict: - by_comp_dict: ByComponentDict = markdown_dict[statement_id] + # Delete the entry from the markdown_dict once processed to avoid duplicates + by_comp_dict: ByComponentDict = markdown_dict.pop(statement_id) self._update_type_with_by_comp(stm, by_comp_dict) - # Delete the entry from the markdown_dict once processed to avoid duplicates - del markdown_dict[statement_id] - new_statements.append(stm) implemented_requirement.statements = none_if_empty(new_statements) @@ -123,15 +152,12 @@ def _update_type_with_by_comp(self, with_bycomp: TypeWithByComps, by_comp_dict: new_by_comp: List[ossp.ByComponent] = [] by_comp: ossp.ByComponent + comp_inheritance_info: Tuple[List[ossp.Inherited], List[ossp.Satisfied]] for by_comp in as_list(with_bycomp.by_components): # If the by_component uuid exists in the by_comp_dict, then update it # If not, clear the by_component inheritance information - comp_inheritance_info: Tuple[List[ossp.Inherited], List[ossp.Satisfied]] = ([], []) - if by_comp.component_uuid in by_comp_dict: - comp_inheritance_info = by_comp_dict[by_comp.component_uuid] - # Delete the entry from the by_comp_dict once processed to avoid duplicates - del by_comp_dict[by_comp.component_uuid] + comp_inheritance_info = by_comp_dict.pop(by_comp.component_uuid, ([], [])) bycomp_interface = ByComponentInterface(by_comp) by_comp = bycomp_interface.reconcile_inheritance_by_component( @@ -195,31 +221,36 @@ def _read_inheritance_markdown_directory(self) -> InheritanceViewDict: uuid_by_title[component.title] = component.uuid for comp_dir in os.listdir(self._root_path): - for control_dir in os.listdir(self._root_path.joinpath(comp_dir)): + is_comp_leveraged = False + for control_dir in os.listdir(os.path.join(self._root_path, comp_dir)): + control_dir_path = os.path.join(self._root_path, comp_dir, control_dir) - # Initialize the by component dictionary for the control directory + # Initialize the by_component dictionary for the control directory # If it exists in the markdown dictionary, then update it with the new information - by_comp_dict: ByComponentDict = {} - if control_dir in markdown_dict: - by_comp_dict = markdown_dict[control_dir] + by_comp_dict = markdown_dict.get(control_dir, {}) - for file in os.listdir(self._root_path.joinpath(comp_dir, control_dir)): - reader = InheritanceMarkdownReader(self._root_path.joinpath(comp_dir, control_dir, file)) + for file in os.listdir(control_dir_path): + file_path = pathlib.Path(control_dir_path).joinpath(file) + reader = InheritanceMarkdownReader(file_path) leveraged_info = reader.process_leveraged_statement_markdown() # If there is no leveraged information, then skip this file if leveraged_info is None: continue + # If a file has leveraged information, then set the flag to indicate the component is leveraged + is_comp_leveraged = True + for comp in leveraged_info.leveraging_comp_titles: - comp_uuid = uuid_by_title[comp] - inherited: List[ossp.Inherited] = [] - satisfied: List[ossp.Satisfied] = [] + if comp not in uuid_by_title: + keys_as_string = ', '.join(uuid_by_title.keys()) + raise TrestleError( + f'Component "{comp}" does not exist in the {self._ssp.metadata.title} SSP. ' + f'Please use options: {keys_as_string}.' + ) - # If the component uuid exists in the by_component dictionary, then update it - if comp_uuid in by_comp_dict: - inherited = by_comp_dict[comp_uuid][0] - satisfied = by_comp_dict[comp_uuid][1] + comp_uuid = uuid_by_title[comp] + inherited, satisfied = by_comp_dict.get(comp_uuid, ([], [])) if leveraged_info.inherited is not None: inherited.append(leveraged_info.inherited) @@ -228,7 +259,9 @@ def _read_inheritance_markdown_directory(self) -> InheritanceViewDict: by_comp_dict[comp_uuid] = (inherited, satisfied) - # Add the by_component dictionary to the markdown dictionary for the control directory markdown_dict[control_dir] = by_comp_dict + if is_comp_leveraged: + self._mapped_components.append(comp_dir) + return markdown_dict diff --git a/trestle/core/crm/export_writer.py b/trestle/core/crm/export_writer.py index 3ac0dd64e..83c81f91d 100644 --- a/trestle/core/crm/export_writer.py +++ b/trestle/core/crm/export_writer.py @@ -40,7 +40,7 @@ class ExportWriter: to Markdown. """ - def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): + def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan, leveraged_ssp_href: str): """ Initialize export writer. @@ -50,6 +50,7 @@ def __init__(self, root_path: pathlib.Path, ssp: ossp.SystemSecurityPlan): """ self._ssp: ossp.SystemSecurityPlan = ssp self._root_path: pathlib.Path = root_path + self._leveraged_ssp_href: str = leveraged_ssp_href # Find all the components and create paths for name self._paths_by_comp: Dict[str, pathlib.Path] = {} @@ -95,16 +96,22 @@ def _statement_types_from_exports(self, export_interface: ByComponentInterface) all_statements: Dict[str, LeveragedStatements] = {} for responsibility in export_interface.get_isolated_responsibilities(): - all_statements[responsibility.uuid - ] = StatementResponsibility(responsibility.uuid, responsibility.description) + all_statements[responsibility.uuid] = StatementResponsibility( + responsibility.uuid, responsibility.description, self._leveraged_ssp_href + ) for provided in export_interface.get_isolated_provided(): - all_statements[provided.uuid] = StatementProvided(provided.uuid, provided.description) + all_statements[provided.uuid + ] = StatementProvided(provided.uuid, provided.description, self._leveraged_ssp_href) for responsibility, provided in export_interface.get_export_sets(): path = f'{provided.uuid}_{responsibility.uuid}' all_statements[path] = StatementTree( - provided.uuid, provided.description, responsibility.uuid, responsibility.description + provided.uuid, + provided.description, + responsibility.uuid, + responsibility.description, + self._leveraged_ssp_href ) return all_statements diff --git a/trestle/core/crm/leveraged_statements.py b/trestle/core/crm/leveraged_statements.py index e5e03dbb0..a842960d1 100644 --- a/trestle/core/crm/leveraged_statements.py +++ b/trestle/core/crm/leveraged_statements.py @@ -34,7 +34,7 @@ class LeveragedStatements(ABC): """Abstract class for managing leveraged statements.""" - def __init__(self) -> None: + def __init__(self, leveraged_ssp_reference: str) -> None: """Initialize the class.""" self._md_file: Optional[MDWriter] = None self.header_comment_dict: Dict[str, str] = { @@ -42,7 +42,13 @@ def __init__(self) -> None: const.TRESTLE_STATEMENT_TAG: const.YAML_LEVERAGED_COMMENT } self.merged_header_dict: Dict[str, Any] = { - const.TRESTLE_STATEMENT_TAG: '', const.TRESTLE_LEVERAGING_COMP_TAG: component_mapping_default + const.TRESTLE_STATEMENT_TAG: '', + const.TRESTLE_LEVERAGING_COMP_TAG: component_mapping_default, + const.TRESTLE_GLOBAL_TAG: { + const.LEVERAGED_SSP: { + const.HREF: leveraged_ssp_reference + } + } } @abstractmethod @@ -54,15 +60,20 @@ class StatementTree(LeveragedStatements): """Concrete class for managing provided and responsibility statements.""" def __init__( - self, provided_uuid: str, provided_description: str, responsibility_uuid: str, responsibility_description: str - ): + self, + provided_uuid: str, + provided_description: str, + responsibility_uuid: str, + responsibility_description: str, + leveraged_ssp_reference: str + ) -> None: """Initialize the class.""" self.provided_uuid = provided_uuid self.provided_description = provided_description self.responsibility_uuid = responsibility_uuid self.responsibility_description = responsibility_description self.satisfied_description = '' - super().__init__() + super().__init__(leveraged_ssp_reference) def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: """Write a provided and responsibility statements to a markdown file.""" @@ -107,11 +118,16 @@ def _add_generated_content(self) -> None: class StatementProvided(LeveragedStatements): """Concrete class for managing provided statements.""" - def __init__(self, provided_uuid: str, provided_description: str): + def __init__( + self, + provided_uuid: str, + provided_description: str, + leveraged_ssp_reference: str, + ) -> None: """Initialize the class.""" self.provided_uuid = provided_uuid self.provided_description = provided_description - super().__init__() + super().__init__(leveraged_ssp_reference) def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: """Write provided statements to a markdown file.""" @@ -143,12 +159,17 @@ def _add_generated_content(self) -> None: class StatementResponsibility(LeveragedStatements): """Concrete class for managing responsibility statements.""" - def __init__(self, responsibility_uuid: str, responsibility_description: str): + def __init__( + self, + responsibility_uuid: str, + responsibility_description: str, + leveraged_ssp_reference: str, + ) -> None: """Initialize the class.""" self.responsibility_uuid = responsibility_uuid self.responsibility_description = responsibility_description self.satisfied_description = '' - super().__init__() + super().__init__(leveraged_ssp_reference) def write_statement_md(self, leveraged_statement_file: pathlib.Path) -> None: """Write responsibility statements to a markdown file.""" @@ -263,6 +284,10 @@ def process_leveraged_statement_markdown(self) -> Optional[InheritanceInfo]: return InheritanceInfo(leveraging_comps, inherited_statement, satisfied_statement) + def get_leveraged_ssp_reference(self) -> str: + """Return the leveraged SSP reference in the yaml header.""" + return self._yaml_header[const.TRESTLE_GLOBAL_TAG][const.LEVERAGED_SSP][const.HREF] + def get_satisfied_description(self) -> Optional[str]: """Return the satisfied description in the Markdown.""" node = self._inheritance_md.get_node_for_key(const.SATISFIED_STATEMENT_DESCRIPTION, False) diff --git a/trestle/core/crm/ssp_inheritance_api.py b/trestle/core/crm/ssp_inheritance_api.py new file mode 100644 index 000000000..37cae36fa --- /dev/null +++ b/trestle/core/crm/ssp_inheritance_api.py @@ -0,0 +1,183 @@ +# -*- mode:python; coding:utf-8 -*- +# Copyright (c) 2020 IBM Corp. All rights reserved. +# +# 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 + +# https://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. +"""API for updating inheritance information in SSPs.""" + +import logging +import pathlib +from typing import Dict, List, Optional + +import trestle.core.generators as gens +import trestle.oscal.common as common +import trestle.oscal.ssp as ossp +from trestle.common import const +from trestle.common.err import TrestleError +from trestle.common.list_utils import as_list, none_if_empty +from trestle.core.catalog.catalog_api import CatalogAPI +from trestle.core.crm.export_reader import ExportReader +from trestle.core.crm.export_writer import ExportWriter +from trestle.core.remote.cache import FetcherFactory + +logger = logging.getLogger(__name__) + + +class SSPInheritanceAPI(): + """API for updating inheritance information in SSPs through inheritance markdown.""" + + def __init__(self, inheritance_md_path: pathlib.Path, trestle_root: pathlib.Path) -> None: + """Initialize the SSP Inheritance API class.""" + self._inheritance_markdown_path: pathlib.Path = inheritance_md_path + self._trestle_root: pathlib.Path = trestle_root + + def write_inheritance_as_markdown( + self, leveraged_ssp_reference: str, catalog_api: Optional[CatalogAPI] = None + ) -> None: + """ + Write inheritance information to markdown. + + Args: + leveraged_ssp: SSP to write inheritance information from. + catalog_api: Catalog API to filter inheritance information by catalog. + + Notes: + If a catalog API is provided, the written controls in the markdown will be filtered by the catalog. + """ + fetcher = FetcherFactory.get_fetcher(self._trestle_root, leveraged_ssp_reference) + leveraged_ssp: ossp.SystemSecurityPlan + try: + leveraged_ssp, _ = fetcher.get_oscal() + except TrestleError as e: + raise TrestleError(f'Unable to fetch ssp from {leveraged_ssp_reference}: {e}') + + if catalog_api is not None: + control_imp: ossp.ControlImplementation = leveraged_ssp.control_implementation + + new_imp_requirements: List[ossp.ImplementedRequirement] = [] + for imp_requirement in as_list(control_imp.implemented_requirements): + control = catalog_api._catalog_interface.get_control(imp_requirement.control_id) + if control is not None: + new_imp_requirements.append(imp_requirement) + control_imp.implemented_requirements = new_imp_requirements + + leveraged_ssp.control_implementation = control_imp + + export_writer: ExportWriter = ExportWriter( + self._inheritance_markdown_path, leveraged_ssp, leveraged_ssp_reference + ) + export_writer.write_exports_as_markdown() + + def update_ssp_inheritance(self, ssp: ossp.SystemSecurityPlan) -> None: + """ + Update inheritance information in SSP. + + Args: + ssp: SSP to update with inheritance information. + """ + logger.debug('Reading inheritance information from markdown.') + reader = ExportReader(self._inheritance_markdown_path, ssp) + ssp = reader.read_exports_from_markdown() + + # Reader get reference + leveraged_ssp_reference = reader.get_leveraged_ssp_href() + + fetcher = FetcherFactory.get_fetcher(self._trestle_root, leveraged_ssp_reference) + leveraged_ssp: ossp.SystemSecurityPlan + try: + leveraged_ssp, _ = fetcher.get_oscal() + except TrestleError as e: + raise TrestleError(f'Unable to fetch ssp from {leveraged_ssp_reference}: {e}') + + link: common.Link = common.Link(href=leveraged_ssp_reference) + leveraged_auths: List[ossp.LeveragedAuthorization] = [] + leveraged_auth: ossp.LeveragedAuthorization = gens.generate_sample_model(ossp.LeveragedAuthorization) + leveraged_components: List[str] = reader.get_leveraged_components() + + if not leveraged_components: + logger.warn( + 'No leveraged components mapped in inheritance view.', + 'No leveraged authorization will be added to the ssp' + ) + else: + if self._is_present_in_ssp(ssp, link): + if ssp.system_implementation.leveraged_authorizations is not None: + leveraged_auth = ssp.system_implementation.leveraged_authorizations[0] + else: + leveraged_auth.links = as_list(leveraged_auth.links) + leveraged_auth.links.append(link) + + # Set the title of the leveraged authorization + leveraged_auth.title = f'Leveraged Authorization for {leveraged_ssp.metadata.title}' + leveraged_auths.append(leveraged_auth) + # Overwrite the leveraged authorization in the SSP. The only leveraged authorization should be the one + # coming from inheritance view + ssp.system_implementation.leveraged_authorizations = none_if_empty(leveraged_auths) + + # Reconcile the current leveraged components with the leveraged components in the inheritance view + mapped_components: Dict[str, ossp.SystemComponent] = {} + for component in as_list(leveraged_ssp.system_implementation.components): + if component.title in leveraged_components: + mapped_components[component.uuid] = component + + new_components: List[ossp.SystemComponent] = [] + for component in as_list(ssp.system_implementation.components): + props_dict: Dict[str, str] = {} + for prop in as_list(component.props): + props_dict[prop.name] = prop.value + + # If this component is part of the original SSP components, add + # and continue + if const.LEV_AUTH_UUID not in props_dict: + new_components.append(component) + continue + + # If the leveraged component already exists, update the title, description, type, and status + original_comp_uuid = props_dict[const.INHERITED_UUID] + if original_comp_uuid in mapped_components: + original_component = mapped_components.pop(original_comp_uuid) + self._update_leveraged_system_component(component, original_component, leveraged_auth.uuid) + new_components.append(component) + + # Add any remaining components to the new components + for component in mapped_components.values(): + new_component: ossp.SystemComponent = gens.generate_sample_model(ossp.SystemComponent) + self._update_leveraged_system_component(new_component, component, leveraged_auth.uuid) + logger.debug(f'Adding component {new_component.title} to components.') + new_components.append(new_component) + + ssp.system_implementation.components = new_components + + @staticmethod + def _update_leveraged_system_component( + new_comp: ossp.SystemComponent, original_comp: ossp.SystemComponent, leveraged_auth_id: str + ) -> None: + """Create a leveraged system component in the context of a leveraging system component.""" + new_comp.type = original_comp.type + new_comp.title = original_comp.title + new_comp.description = original_comp.description + new_comp.status = original_comp.status + + new_comp.props = [] + new_comp.props.append( + common.Property(name=const.IMPLEMENTATION_POINT, value=const.IMPLEMENTATION_POINT_EXTERNAL) + ) + new_comp.props.append(common.Property(name=const.LEV_AUTH_UUID, value=leveraged_auth_id)) + new_comp.props.append(common.Property(name=const.INHERITED_UUID, value=original_comp.uuid)) + + def _is_present_in_ssp(self, ssp: ossp.SystemSecurityPlan, link: common.Link) -> bool: + if (ssp.system_implementation.leveraged_authorizations is not None + and ssp.system_implementation.leveraged_authorizations[0].links is not None + and ssp.system_implementation.leveraged_authorizations[0].links[0].href == link.href): + return True + else: + return False From f87e4679fd865733590f68051a75b6eb432213fc Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Thu, 28 Sep 2023 17:17:16 -0400 Subject: [PATCH 20/24] docs: add docs updates for SSPInheritanceAPI class Signed-off-by: Jennifer Power --- docs/api_reference/trestle.core.crm.ssp_inheritance_api.md | 2 ++ mkdocs.yml | 1 + trestle/core/crm/ssp_inheritance_api.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 docs/api_reference/trestle.core.crm.ssp_inheritance_api.md diff --git a/docs/api_reference/trestle.core.crm.ssp_inheritance_api.md b/docs/api_reference/trestle.core.crm.ssp_inheritance_api.md new file mode 100644 index 000000000..74d3c7457 --- /dev/null +++ b/docs/api_reference/trestle.core.crm.ssp_inheritance_api.md @@ -0,0 +1,2 @@ +::: trestle.core.crm.ssp_inheritance_api +handler: python diff --git a/mkdocs.yml b/mkdocs.yml index 5c2d5f124..c4c463040 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -118,6 +118,7 @@ nav: - export_reader: api_reference/trestle.core.crm.export_reader.md - export_writer: api_reference/trestle.core.crm.export_writer.md - leveraged_statements: api_reference/trestle.core.crm.leveraged_statements.md + - ssp_inheritance_api: api_reference/trestle.core.crm.ssp_inheritance_api.md - docs_control_writer: api_reference/trestle.core.docs_control_writer.md - draw_io: api_reference/trestle.core.draw_io.md - duplicates_validator: api_reference/trestle.core.duplicates_validator.md diff --git a/trestle/core/crm/ssp_inheritance_api.py b/trestle/core/crm/ssp_inheritance_api.py index 37cae36fa..48d595c55 100644 --- a/trestle/core/crm/ssp_inheritance_api.py +++ b/trestle/core/crm/ssp_inheritance_api.py @@ -47,7 +47,7 @@ def write_inheritance_as_markdown( Write inheritance information to markdown. Args: - leveraged_ssp: SSP to write inheritance information from. + leveraged_ssp_reference: Location of the SSP to write inheritance information from. catalog_api: Catalog API to filter inheritance information by catalog. Notes: From 807bb286faa7b452002b93f010fb3f38ab1d5de5 Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Thu, 28 Sep 2023 19:06:28 -0400 Subject: [PATCH 21/24] chore: updates warning message for leveraged authorization with comps Signed-off-by: Jennifer Power --- trestle/core/crm/ssp_inheritance_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trestle/core/crm/ssp_inheritance_api.py b/trestle/core/crm/ssp_inheritance_api.py index 48d595c55..6b6ca7f18 100644 --- a/trestle/core/crm/ssp_inheritance_api.py +++ b/trestle/core/crm/ssp_inheritance_api.py @@ -105,8 +105,8 @@ def update_ssp_inheritance(self, ssp: ossp.SystemSecurityPlan) -> None: if not leveraged_components: logger.warn( - 'No leveraged components mapped in inheritance view.', - 'No leveraged authorization will be added to the ssp' + 'No leveraged components mapped to the SSP. ' + 'Please edit the inheritance markdown to include the leveraged authorization.' ) else: if self._is_present_in_ssp(ssp, link): From 725f6980f83b040fec4145cfebd4c80af2f7196f Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Thu, 28 Sep 2023 19:19:29 -0400 Subject: [PATCH 22/24] fix: updates ssp-assemble to ensure existing leveraged comps persist Signed-off-by: Jennifer Power --- trestle/core/commands/author/ssp.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/trestle/core/commands/author/ssp.py b/trestle/core/commands/author/ssp.py index 4066ddb3e..3ba69d421 100644 --- a/trestle/core/commands/author/ssp.py +++ b/trestle/core/commands/author/ssp.py @@ -563,10 +563,18 @@ def _run(self, args: argparse.Namespace) -> int: raise TrestleError('Original ssp has no system component.') comp_dict[const.SSP_MAIN_COMP_NAME] = sys_comp + ssp_sys_imp_comps = ssp.system_implementation.components + # Gather the leveraged components to add back after the merge + leveraged_comps: Dict[str, ossp.SystemComponent] = {} + for sys_comp in ssp_sys_imp_comps: + if sys_comp.props is not None: + prop_names = [x.name for x in sys_comp.props] + if const.LEV_AUTH_UUID in prop_names: + leveraged_comps[sys_comp.title] = sys_comp + # Verifies older compdefs in an ssp no longer exist in newly provided ones comp_titles = [x.title for x in comp_dict.values()] - ssp_sys_imp_comps = ssp.system_implementation.components - diffs = [x for x in ssp_sys_imp_comps if x.title not in comp_titles] + diffs = [x for x in ssp_sys_imp_comps if x.title not in comp_titles and x.title not in leveraged_comps] if diffs: for diff in diffs: logger.warning( @@ -581,6 +589,9 @@ def _run(self, args: argparse.Namespace) -> int: CatalogReader.read_ssp_md_content(md_path, ssp, comp_dict, part_id_map_by_label, context) new_file_content_type = FileContentType.path_to_content_type(orig_ssp_path) + + # Add the leveraged comps back to the final ssp + ssp.system_implementation.components.extend(list(leveraged_comps.values())) else: # create a sample ssp to hold all the parts ssp = gens.generate_sample_model(ossp.SystemSecurityPlan) From a10ba54267798f17d201876bc6c407183f2ff48e Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Fri, 27 Oct 2023 13:49:01 -0400 Subject: [PATCH 23/24] fix: adds fixes to address PR feedback Signed-off-by: Jennifer Power --- .../ssp_profile_catalog_authoring.md | 6 +++--- .../crm/{exports_reader_test.py => export_reader_test.py} | 0 .../crm/{exports_writer_test.py => export_writer_test.py} | 0 trestle/core/commands/author/ssp.py | 6 +++--- trestle/core/crm/bycomp_interface.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename tests/trestle/core/crm/{exports_reader_test.py => export_reader_test.py} (100%) rename tests/trestle/core/crm/{exports_writer_test.py => export_writer_test.py} (100%) diff --git a/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md b/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md index b8d32faec..a1c3e4eda 100644 --- a/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md +++ b/docs/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring.md @@ -1052,7 +1052,7 @@ If you do not specify component-defintions during assembly, the markdown should ## Inheritance view -The inheritance view is generated by setting the `--leveraged-ssp` flag with `trestle author ssp-generate`. It contains information relating to exported information such as inherited capabilities and consumer responsibilities that can be used to populate the inheritance information in the assembled SSP. When used, a directory named "inheritance" is created within the markdown directory. This directory serves as a designated space for mapping inherited capabilities and responsibilities onto components in the assemble SSP and authoring satisfied statements for responsibilities. +The inheritance view is generated by setting the `--leveraged-ssp` flag with `trestle author ssp-generate`. It contains information relating to exported information such as inherited capabilities and customer responsibilities that can be used to populate the inheritance information in the assembled SSP. When used, a directory named "inheritance" is created within the markdown directory. This directory serves as a designated space for mapping inherited capabilities and responsibilities onto components in the assemble SSP and authoring satisfied statements for responsibilities. Example usage for creation of the markdown: @@ -1105,14 +1105,14 @@ x-trestle-leveraging-comp: # Provided Statement Description -Consumer_appropriate description of what may be inherited. +Customer_appropriate description of what may be inherited. ```
-Example of inheritance consumer responsibility only markdown after ssp-generate +Example of inheritance customer responsibility only markdown after ssp-generate ```markdown --- diff --git a/tests/trestle/core/crm/exports_reader_test.py b/tests/trestle/core/crm/export_reader_test.py similarity index 100% rename from tests/trestle/core/crm/exports_reader_test.py rename to tests/trestle/core/crm/export_reader_test.py diff --git a/tests/trestle/core/crm/exports_writer_test.py b/tests/trestle/core/crm/export_writer_test.py similarity index 100% rename from tests/trestle/core/crm/exports_writer_test.py rename to tests/trestle/core/crm/export_writer_test.py diff --git a/trestle/core/commands/author/ssp.py b/trestle/core/commands/author/ssp.py index 3ba69d421..b8868ee51 100644 --- a/trestle/core/commands/author/ssp.py +++ b/trestle/core/commands/author/ssp.py @@ -209,8 +209,8 @@ def _generate_inheritance_markdown( Notes: This will create the inheritance view markdown files in the same directory as the ssp markdown files. - The information will be from the leveraged ssp, but filtered by the chose profile to ensure only relevant - control are present for mapping. + The information will be from the leveraged ssp, but filtered by the chosen profile to ensure only relevant + controls are present for mapping. """ # if file not recognized as URI form, assume it represents name of file in trestle directory ssp_in_trestle_dir = '://' not in leveraged_ssp_name_or_href @@ -609,7 +609,7 @@ def _run(self, args: argparse.Namespace) -> int: # TODO if the ssp already existed then components may need to be removed if not ref'd by imp_reqs self._generate_roles_in_metadata(ssp) - # If this is a leveraging SSP, update it with the retrieved the exports from the leveraged SSP + # If this is a leveraging SSP, update it with the retrieved exports from the leveraged SSP inheritance_markdown_path = md_path.joinpath(const.INHERITANCE_VIEW_DIR) if os.path.exists(inheritance_markdown_path): SSPInheritanceAPI(inheritance_markdown_path, trestle_root).update_ssp_inheritance(ssp) diff --git a/trestle/core/crm/bycomp_interface.py b/trestle/core/crm/bycomp_interface.py index b92c8abba..d4fd885ec 100644 --- a/trestle/core/crm/bycomp_interface.py +++ b/trestle/core/crm/bycomp_interface.py @@ -29,7 +29,7 @@ class ByComponentInterface: Interface to query and modify by-component assembly inheritance contents. The by-component is contained in two separate forms: As an actual OSCAL by-component assembly, - and and multiple dicts providing direct lookup of inheritance statement by uuid. + and multiple dicts providing direct lookup of inheritance statement by uuid. The dicts are created by the ByComponentInterface constructor, parsed and the responsibility and provided statements are separated into three catagories: @@ -133,7 +133,7 @@ def reconcile_inheritance_by_component( Reconcile the inherited and satisfied statements in the by-component assembly with changes from the export. Notes: - A statement is determined as existing if the provided uuid or responsibility uuid is in the existing in the + A statement is determined as existing if the provided uuid or responsibility uuid is in the existing by-component assembly. If existing, the description will be updated if it has changed. Any existing inherited or satisfied statements that are not in the incoming export will be removed. From b1f601df87e3d99660dcd70a19e8d3e8a74e409b Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Fri, 27 Oct 2023 16:31:36 -0400 Subject: [PATCH 24/24] refactor: polishes SSPInheritanceAPI class to reduce complexity Signed-off-by: Jennifer Power --- trestle/core/crm/ssp_inheritance_api.py | 78 ++++++++++++++----------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/trestle/core/crm/ssp_inheritance_api.py b/trestle/core/crm/ssp_inheritance_api.py index 6b6ca7f18..3f3c2dd40 100644 --- a/trestle/core/crm/ssp_inheritance_api.py +++ b/trestle/core/crm/ssp_inheritance_api.py @@ -53,12 +53,7 @@ def write_inheritance_as_markdown( Notes: If a catalog API is provided, the written controls in the markdown will be filtered by the catalog. """ - fetcher = FetcherFactory.get_fetcher(self._trestle_root, leveraged_ssp_reference) - leveraged_ssp: ossp.SystemSecurityPlan - try: - leveraged_ssp, _ = fetcher.get_oscal() - except TrestleError as e: - raise TrestleError(f'Unable to fetch ssp from {leveraged_ssp_reference}: {e}') + leveraged_ssp: ossp.SystemSecurityPlan = self._fetch_leveraged_ssp(leveraged_ssp_reference) if catalog_api is not None: control_imp: ossp.ControlImplementation = leveraged_ssp.control_implementation @@ -88,15 +83,9 @@ def update_ssp_inheritance(self, ssp: ossp.SystemSecurityPlan) -> None: reader = ExportReader(self._inheritance_markdown_path, ssp) ssp = reader.read_exports_from_markdown() - # Reader get reference leveraged_ssp_reference = reader.get_leveraged_ssp_href() - fetcher = FetcherFactory.get_fetcher(self._trestle_root, leveraged_ssp_reference) - leveraged_ssp: ossp.SystemSecurityPlan - try: - leveraged_ssp, _ = fetcher.get_oscal() - except TrestleError as e: - raise TrestleError(f'Unable to fetch ssp from {leveraged_ssp_reference}: {e}') + leveraged_ssp: ossp.SystemSecurityPlan = self._fetch_leveraged_ssp(leveraged_ssp_reference) link: common.Link = common.Link(href=leveraged_ssp_reference) leveraged_auths: List[ossp.LeveragedAuthorization] = [] @@ -109,21 +98,42 @@ def update_ssp_inheritance(self, ssp: ossp.SystemSecurityPlan) -> None: 'Please edit the inheritance markdown to include the leveraged authorization.' ) else: - if self._is_present_in_ssp(ssp, link): - if ssp.system_implementation.leveraged_authorizations is not None: - leveraged_auth = ssp.system_implementation.leveraged_authorizations[0] + existing_leveraged_auth: ossp.LeveragedAuthorization = self._leveraged_auth_from_existing( + as_list(ssp.system_implementation.leveraged_authorizations), link + ) + if existing_leveraged_auth is not None: + leveraged_auth = existing_leveraged_auth else: leveraged_auth.links = as_list(leveraged_auth.links) leveraged_auth.links.append(link) - # Set the title of the leveraged authorization leveraged_auth.title = f'Leveraged Authorization for {leveraged_ssp.metadata.title}' leveraged_auths.append(leveraged_auth) + # Overwrite the leveraged authorization in the SSP. The only leveraged authorization should be the one # coming from inheritance view ssp.system_implementation.leveraged_authorizations = none_if_empty(leveraged_auths) - # Reconcile the current leveraged components with the leveraged components in the inheritance view + self._reconcile_components(ssp, leveraged_ssp, leveraged_components, leveraged_auth) + + def _fetch_leveraged_ssp(self, leveraged_ssp_reference: str) -> ossp.SystemSecurityPlan: + """Fetch the leveraged SSP.""" + leveraged_ssp: ossp.SystemSecurityPlan + fetcher = FetcherFactory.get_fetcher(self._trestle_root, leveraged_ssp_reference) + try: + leveraged_ssp, _ = fetcher.get_oscal() + except TrestleError as e: + raise TrestleError(f'Unable to fetch ssp from {leveraged_ssp_reference}: {e}') + return leveraged_ssp + + def _reconcile_components( + self, + ssp: ossp.SystemSecurityPlan, + leveraged_ssp: ossp.SystemSecurityPlan, + leveraged_components: List[str], + leveraged_auth: ossp.LeveragedAuthorization + ) -> None: + """Reconcile components in the leveraging SSP with those in the leveraged SSP.""" mapped_components: Dict[str, ossp.SystemComponent] = {} for component in as_list(leveraged_ssp.system_implementation.components): if component.title in leveraged_components: @@ -131,9 +141,7 @@ def update_ssp_inheritance(self, ssp: ossp.SystemSecurityPlan) -> None: new_components: List[ossp.SystemComponent] = [] for component in as_list(ssp.system_implementation.components): - props_dict: Dict[str, str] = {} - for prop in as_list(component.props): - props_dict[prop.name] = prop.value + props_dict: Dict[str, str] = {prop.name: prop.value for prop in as_list(component.props)} # If this component is part of the original SSP components, add # and continue @@ -167,17 +175,17 @@ def _update_leveraged_system_component( new_comp.description = original_comp.description new_comp.status = original_comp.status - new_comp.props = [] - new_comp.props.append( - common.Property(name=const.IMPLEMENTATION_POINT, value=const.IMPLEMENTATION_POINT_EXTERNAL) - ) - new_comp.props.append(common.Property(name=const.LEV_AUTH_UUID, value=leveraged_auth_id)) - new_comp.props.append(common.Property(name=const.INHERITED_UUID, value=original_comp.uuid)) - - def _is_present_in_ssp(self, ssp: ossp.SystemSecurityPlan, link: common.Link) -> bool: - if (ssp.system_implementation.leveraged_authorizations is not None - and ssp.system_implementation.leveraged_authorizations[0].links is not None - and ssp.system_implementation.leveraged_authorizations[0].links[0].href == link.href): - return True - else: - return False + new_comp.props = [ + common.Property(name=const.IMPLEMENTATION_POINT, value=const.IMPLEMENTATION_POINT_EXTERNAL), + common.Property(name=const.LEV_AUTH_UUID, value=leveraged_auth_id), + common.Property(name=const.INHERITED_UUID, value=original_comp.uuid) + ] + + def _leveraged_auth_from_existing( + self, leveraged_authorizations: List[ossp.LeveragedAuthorization], criteria_link: common.Link + ) -> Optional[ossp.LeveragedAuthorization]: + """Return the leveraged authorization if it is present in the ssp.""" + for leveraged_auth in leveraged_authorizations: + if leveraged_auth.links and any(link.href == criteria_link.href for link in leveraged_auth.links): + return leveraged_auth + return None