diff --git a/cdxev/__main__.py b/cdxev/__main__.py index 1c5d9b68..2c4c2a4b 100644 --- a/cdxev/__main__.py +++ b/cdxev/__main__.py @@ -14,10 +14,11 @@ from cdxev.auxiliary.output import write_sbom from cdxev.build_public_bom import build_public_bom from cdxev.error import AppError, InputFileError -from cdxev.log import configure_logging +from cdxev.log import LogMessage, configure_logging from cdxev.merge import merge from cdxev.merge_vex import merge_vex from cdxev.validator import validate_sbom +from cdxev.validator.warningsngreport import WarningsNgReporter logger: logging.Logger _STATUS_OK = 0 @@ -297,6 +298,14 @@ def create_validation_parser( ), type=str, ) + parser.add_argument( + "--plausability-check", + help=( + "If this flag is set, the plausibility of the bom-refs in the" + "sbom will also be checked" + ), + action="store_true", + ) add_output_argument(parser) @@ -580,27 +589,40 @@ def has_target() -> bool: def invoke_validate(args: argparse.Namespace) -> int: + global logger sbom, file_type = read_sbom(args.input) if args.output is None: output = Path("./issues.json") else: output = args.output report_format = args.report_format - return ( - _STATUS_OK - if validate_sbom( - sbom=sbom, - input_format=file_type, - file=Path(args.input), - report_format=report_format, - output=output, - schema_type=args.schema_type, - filename_regex=args.filename_pattern, - schema_path=args.schema_path, - ) - == _STATUS_OK - else _STATUS_VALIDATION_ERROR - ) + sorted_errors = validate_sbom( + sbom=sbom, + input_format=file_type, + file=Path(args.input), + schema_type=args.schema_type, + filename_regex=args.filename_pattern, + schema_path=args.schema_path, + plausability_check=args.plausability_check, + ) + if len(sorted_errors) == 0: + logger.info("SBOM is compliant to the provided specification schema") + return 0 + else: + if report_format == "warnings-ng": + warnings_ng_handler = WarningsNgReporter(Path(args.input), output) + logger.addHandler(warnings_ng_handler) + for error in sorted_errors: + logger.error( + LogMessage( + message="Invalid SBOM", + description=error.replace( + error[0 : error.find("has the mistake")], "" + ).replace("has the mistake: ", ""), + module_name=error[0 : error.find("has the mistake") - 1], + ) + ) + return _STATUS_OK if sorted_errors == set() else _STATUS_VALIDATION_ERROR def invoke_build_public_bom(args: argparse.Namespace) -> int: diff --git a/cdxev/validator/helper.py b/cdxev/validator/helper.py index f62dcdea..b56221fb 100644 --- a/cdxev/validator/helper.py +++ b/cdxev/validator/helper.py @@ -4,6 +4,11 @@ from importlib import resources from pathlib import Path +from cdxev.auxiliary.identity import ComponentIdentity +from cdxev.auxiliary.sbomFunctions import ( + get_bom_refs_from_components, + get_component_by_ref, +) from cdxev.error import AppError @@ -132,3 +137,226 @@ def get_external_schema(schema_path: Path) -> tuple[dict, Path]: "Could not load schema", ("Path to the provided schema does not exist"), ) + + +def get_non_unique_bom_refs(sbom: dict) -> list: + list_of_bomrefs = get_bom_refs_from_components(sbom.get("components", [])) + list_of_bomrefs.append( + sbom.get("metadata", {}).get("component", {}).get("bom-ref", "") + ) + non_unique_bom_refs = [ + bom_ref for bom_ref in list_of_bomrefs if list_of_bomrefs.count(bom_ref) > 1 + ] + return list(set(non_unique_bom_refs)) + + +def create_error_non_unique_bom_ref(reference: str, sbom: dict) -> str: + """ + Function to create an error dict for not unique bom-refs. + + :param str reference: the not unique bom-ref + :param sbom : the sbom the bom-ref originates from + + :return: dict with error message and error description + """ + list_of_all_components = sbom.get("components", []).copy() + list_of_all_components.append(sbom.get("metadata", {}).get("component", {})) + list_of_component_ids = [] + for component in list_of_all_components: + if component.get("bom-ref", "") == reference: + list_of_component_ids.append( + ComponentIdentity.create(component, allow_unsafe=True) + ) + component_description_string = "" + for component_id in list_of_component_ids: + component_description_string += f"({component_id})" + error = ( + "SBOM has the mistake: found non unique bom-ref. " + + f"The reference ({reference}) is used in several components. Those are" + + component_description_string + ) + return error + + +def get_errors_for_non_unique_bomrefs(sbom: dict) -> list: + list_of_non_unique_bomrefs = get_non_unique_bom_refs(sbom) + errors = [] + for reference in list_of_non_unique_bomrefs: + errors.append(create_error_non_unique_bom_ref(reference, sbom)) + return errors + + +def plausibility_check(sbom: dict) -> list: + """ + Check a sbom for plausability. + The sbom is checked for orphaned bom-refs and + components that depend on themself. + + + :param dict sbom: the sbom. + + :return: 0 if no errors were found, 1 otherwise + :rtype: int + """ + orphaned_bom_refs_errors = check_for_orphaned_bom_refs(sbom) + dependencies_bom_refs = check_logic_of_dependencies(sbom) + united_errors = orphaned_bom_refs_errors + dependencies_bom_refs + return united_errors + + +def check_for_orphaned_bom_refs(sbom: dict) -> list[str]: + """ + Check a sbom for orphaned bom-refs, references that do + not correspond to any component from the sbom. + + :param dict sbom: the sbom. + + :return: list with the notifications of found errors + rtype: list[dict] + """ + list_of_actual_bom_refs = get_bom_refs_from_components(sbom.get("components", [])) + list_of_actual_bom_refs.append( + sbom.get("metadata", {}).get("component", {}).get("bom-ref") + ) + # Check if bom_refs appear in the sbom, that do not + # correspond to a component from the sbom + + # check dependencies + errors = [] + list_of_all_components = sbom.get("components", []).copy() + list_of_all_components.append(sbom.get("metadata", {}).get("component", {})) + for dependency in sbom.get("dependencies", []): + if dependency.get("ref", "") in list_of_actual_bom_refs: + for bom_ref in dependency.get("dependsOn", []): + if bom_ref not in list_of_actual_bom_refs: + component = get_component_by_ref( + dependency.get("ref", ""), list_of_all_components + ) + id = ComponentIdentity.create(component, allow_unsafe=True) + errors.append( + create_error_orphaned_bom_ref( + bom_ref, + "dependencies-dependsOn of reference" + + dependency.get("ref", "") + + f" belonging to component ({id})", + ) + ) + + else: + errors.append( + create_error_orphaned_bom_ref(dependency.get("ref", ""), "dependencies") + ) + + # check compositions + for composition in sbom.get("compositions", []): + for reference in composition.get("assemblies", []): + if reference not in list_of_actual_bom_refs: + errors.append(create_error_orphaned_bom_ref(reference, "compositions")) + for reference in composition.get("dependencies", []): + if reference not in list_of_actual_bom_refs: + errors.append(create_error_orphaned_bom_ref(reference, "compositions")) + # check vulnearabilities + for vulnerability in sbom.get("vulnerabilities", []): + for affected in vulnerability.get("affects", []): + if affected.get("ref", "") not in list_of_actual_bom_refs: + errors.append( + create_error_orphaned_bom_ref( + affected.get("ref", ""), + "vulnerability " + vulnerability.get("id", ""), + ) + ) + return errors + + +def check_logic_of_dependencies(sbom: dict) -> list[str]: + """ + The function checks if the sbom contains circular dependencies, + e.g. components, that depend on themself. + + :param dict sbom: the sbom + :return: list with the notifications of found errors + :rtype: list[dict] + """ + errors = [] + list_of_actual_bom_refs = get_bom_refs_from_components(sbom.get("components", [])) + list_of_actual_bom_refs.append( + sbom.get("metadata", {}).get("component", {}).get("bom-ref") + ) + # Check for circular references in dependencies + for current_reference in list_of_actual_bom_refs: + list_of_upstream_references = get_upstream_dependency_bom_refs( + current_reference, sbom.get("dependencies", []) + ) + if current_reference in list_of_upstream_references: + errors.append(create_error_circular_reference(current_reference, sbom)) + return errors + + +def create_error_orphaned_bom_ref(reference: str, found_in: str) -> str: + """ + Function to create an error dict if orphaned bom_refs were found. + + :param str reference: the orphaned reference + :param str found in: location of the orphaned sbom + + :return: dict with error message and error description + """ + error = ( + f"{found_in} has the mistake: found orphaned bom-ref" + f" The reference ({reference}) does not" + " correspond to any component in the sbom." + ) + return error + + +def create_error_circular_reference(reference: str, sbom: dict) -> str: + """ + Function that creates an error dict if a selfdependend reference was found. + + :param str reference: the reference that depends on itself + :param dict sbom: the sbom + + :return: dict with error message and error description + """ + list_of_all_components = sbom.get("components", []).copy() + list_of_all_components.append(sbom.get("metadata", {}).get("component", {})) + component = get_component_by_ref(reference, list_of_all_components) + id = ComponentIdentity.create(component, allow_unsafe=True) + error = ( + "dependencies has the mistake: found circular reference (selfdependent component)" + f"The component ({id}) depends on itself" + ) + return error + + +def get_upstream_dependency_bom_refs( + start_reference: str, list_of_dependencies: list[dict], recursion_depth: int = 0 +) -> list: + """ + Function that returns the upstream dependencies of a component, + also all the components this component depends on. + + :param str start_reference: reference from which to start the recursion + return every reference this component depends on. + :param dict sbom: the sbom + :recursion_depth: parameter for the internal recursion. + + :return: list with elements the component depends on. + :rtype: list[str] + """ + list_with_dependencies = [] + # prevent endless recursion, max recursion number is qual to the maximal debt + # of the tree, also the number of dependencies given + if recursion_depth < len(list_of_dependencies) + 1: + recursion_depth += 1 + for dependency in list_of_dependencies: + if dependency.get("ref", "") == start_reference: + for reference in dependency.get("dependsOn", ""): + list_with_dependencies.append(reference) + new_deps = get_upstream_dependency_bom_refs( + reference, list_of_dependencies, recursion_depth + ) + for ref in new_deps: + if ref not in list_with_dependencies: + list_with_dependencies.append(ref) + return list_with_dependencies diff --git a/cdxev/validator/validate.py b/cdxev/validator/validate.py index cb385b41..41530701 100644 --- a/cdxev/validator/validate.py +++ b/cdxev/validator/validate.py @@ -1,15 +1,15 @@ -import logging import re from importlib import resources from pathlib import Path from jsonschema import Draft7Validator, FormatChecker, validators -from cdxev.log import LogMessage -from cdxev.validator.helper import open_schema, validate_filename -from cdxev.validator.warningsngreport import WarningsNgReporter - -logger = logging.getLogger(__name__) +from cdxev.validator.helper import ( + get_errors_for_non_unique_bomrefs, + open_schema, + plausibility_check, + validate_filename, +) schema_path = resources.files("cdxev.auxiliary") / "schema" with resources.as_file(schema_path) as path: @@ -25,12 +25,11 @@ def validate_sbom( sbom: dict, input_format: str, file: Path, - report_format: str, - output: Path, schema_type: str = "default", filename_regex: str = "", schema_path: str = "", -) -> int: + plausability_check: bool = False, +) -> set[str]: errors = [] if input_format == "json": sbom_schema, used_schema_path = open_schema( @@ -42,6 +41,13 @@ def validate_sbom( "SBOM has the mistake: file name is not according to the given regex" ) errors.append(message) + non_unique_bom_ref_errors = get_errors_for_non_unique_bomrefs(sbom) + if plausability_check: + plausability_errors = plausibility_check(sbom) + for error in plausability_errors: + errors.append(error) + for error in non_unique_bom_ref_errors: + errors.append(error) resolver = validators.RefResolver( base_uri=f"{used_schema_path.as_uri()}/", # according to documentation referrer has to be True, therefore ignore error from mypy @@ -145,21 +151,4 @@ def validate_sbom( else: errors.append(error_path + error.message) sorted_errors = set(sorted(errors)) - if len(sorted_errors) == 0: - logger.info("SBOM is compliant to the provided specification schema") - return 0 - else: - if report_format == "warnings-ng": - warnings_ng_handler = WarningsNgReporter(file, output) - logger.addHandler(warnings_ng_handler) - for error in sorted_errors: - logger.error( - LogMessage( - message="Invalid SBOM", - description=error.replace( - error[0 : error.find("has the mistake")], "" - ).replace("has the mistake: ", ""), - module_name=error[0 : error.find("has the mistake") - 1], - ) - ) - return 1 + return sorted_errors diff --git a/tests/test_main.py b/tests/test_main.py index a6d43960..3d1b36b9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -401,20 +401,21 @@ class TestValidateCommand(unittest.TestCase): def test_get_validate( self, mock_validate: unittest.mock.Mock, mock_read: unittest.mock.Mock ) -> None: + valid_return = set() with unittest.mock.patch("sys.argv", ["", "validate", "fake_bom.cdx.json"]): - mock_validate.return_value = 0 + mock_validate.return_value = valid_return mock_read.return_value = ({}, "json") result = main() self.assertEqual(result, _STATUS_OK) with unittest.mock.patch("sys.argv", ["", "validate", "fake_bom.cdx.json"]): - mock_validate.return_value = 1 + mock_validate.return_value = {"error"} mock_read.return_value = ({}, "json") result = main() self.assertEqual(result, _STATUS_VALIDATION_ERROR) with unittest.mock.patch( "sys.argv", ["", "validate", "--schema-type=custom", "fake_bom.cdx.json"] ): - mock_validate.return_value = 0 + mock_validate.return_value = valid_return mock_read.return_value = ({}, "json") result = main() self.assertEqual(result, _STATUS_OK) @@ -430,7 +431,7 @@ def test_get_validate( "issues_file.json", ], ): - mock_validate.return_value = 0 + mock_validate.return_value = valid_return mock_read.return_value = ({}, "json") result = main() self.assertEqual(result, _STATUS_OK) @@ -446,7 +447,7 @@ def test_get_validate( ".*", ], ): - mock_validate.return_value = 0 + mock_validate.return_value = valid_return mock_read.return_value = ({}, "json") result = main() self.assertEqual(result, _STATUS_OK) diff --git a/tests/test_validate.py b/tests/test_validate.py index 01766230..8a50d082 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -2,9 +2,16 @@ import os import unittest from pathlib import Path -from unittest import mock +from cdxev.auxiliary.identity import ComponentIdentity from cdxev.error import AppError +from cdxev.validator.helper import ( + check_for_orphaned_bom_refs, + create_error_non_unique_bom_ref, + get_errors_for_non_unique_bomrefs, + get_upstream_dependency_bom_refs, + plausibility_check, +) from cdxev.validator.validate import validate_sbom path_to_folder_with_test_sboms = "tests/auxiliary/test_validate_sboms/" @@ -22,37 +29,32 @@ list_of_specVersions = ["1.3", "1.4"] -def search_for_word_issues(word: str, issue_list: list) -> bool: +def search_for_word_issues(word: str, issue_list: set) -> bool: is_valid = False for issue in issue_list: - if word.lower() in str(issue[0][0]).lower(): + if word.lower() in issue.lower(): is_valid = True return is_valid -@mock.patch("cdxev.validator.validate.logger") def validate_test( sbom: dict, - mock_logger: unittest.mock.Mock, - report_format: str = "stdout", filename_regex: str = "", schema_type: str = "custom", schema_path: str = "", + plausability_check: bool = False, ) -> list: - mock_logger.error.call_args_list = [] - errors_occurred = validate_sbom( + messages = validate_sbom( sbom=sbom, input_format="json", file=Path(path_to_sbom), - report_format=report_format, - output=Path(""), schema_type=schema_type, filename_regex=filename_regex, schema_path=schema_path, + plausability_check=plausability_check, ) - if not errors_occurred: + if not messages: return ["no issue"] - messages = mock_logger.error.call_args_list return messages @@ -92,14 +94,6 @@ def test_custom_schema(self) -> None: issues = validate_test(sbom, schema_type="default") self.assertEqual(issues, ["no issue"]) - def test_warnings_ng_format(self) -> None: - sbom = get_test_sbom() - sbom["components"][0].pop("version") - issues = validate_test(sbom, report_format="warnings-ng") - self.assertTrue( - search_for_word_issues("'version' is a required property", issues) - ) - class TestValidateMetadata(unittest.TestCase): def test_metadata_missing(self) -> None: @@ -451,14 +445,12 @@ def test_use_own_schema(self) -> None: sbom, "json", Path(path_to_sbom), - "", - Path(""), schema_path=( str(Path(__file__).parent.resolve()) + "/auxiliary/test_validate_sboms/test_schema.json" ), ) - self.assertEqual(v, 0) + self.assertEqual(v, set()) def test_use_own_schema_path_does_not_exist(self) -> None: sbom = get_test_sbom() @@ -487,11 +479,253 @@ class TestValidateUseSchemaType(unittest.TestCase): def test_default_schema(self) -> None: sbom = get_test_sbom() v = validate_sbom( - sbom, - "json", - Path(path_to_sbom), - "", - Path(""), + sbom=sbom, + input_format="json", + file=Path(path_to_sbom), schema_type="default", ) - self.assertEqual(v, 0) + self.assertEqual(v, set()) + + +class TestPlausabilityCheck(unittest.TestCase): + def test_not_unique_bom_ref(self) -> None: + sbom = get_test_sbom() + sbom["components"][0]["bom-ref"] = "some-ref" + sbom["components"][-1]["bom-ref"] = "some-ref" + issues = validate_test(sbom) + self.assertEqual(search_for_word_issues("non unique bom-ref", issues), True) + + def test_non_unique_bomref_in_Metadata(self) -> None: + sbom = get_test_sbom() + sbom["metadata"]["component"]["bom-ref"] = "bom-ref_1" + sbom["components"][-1]["bom-ref"] = "bom-ref_1" + issues = validate_test(sbom) + self.assertEqual(search_for_word_issues("non unique bom-ref", issues), True) + + def test_several_non_unique_bomref_in_Metadata(self) -> None: + sbom = get_test_sbom() + sbom["metadata"]["component"]["bom-ref"] = "bom-ref_1" + sbom["components"][-1]["bom-ref"] = "bom-ref_1" + sbom["components"][1]["bom-ref"] = "bom-ref_2" + sbom["components"][-2]["bom-ref"] = "bom-ref_2" + issues = validate_test(sbom) + self.assertEqual(search_for_word_issues("non unique bom-ref", issues), True) + + def test_plausibility_check_valid_sbom(self) -> None: + sbom = get_test_sbom() + self.assertEqual(plausibility_check(sbom), []) + + def test_check_for_orphaned_bom_refs_dependencies(self) -> None: + sbom = get_test_sbom() + sbom["dependencies"][3]["ref"] = "new_reference" + issues = plausibility_check(sbom) + self.assertEqual(search_for_word_issues("dependencies", issues), True) + + def test_check_for_orphaned_bom_refs_dependencies_dependson(self) -> None: + sbom = get_test_sbom() + sbom["dependencies"][3]["dependsOn"].append("new_reference") + issues = plausibility_check(sbom) + self.assertEqual(search_for_word_issues("dependencies", issues), True) + + def test_check_for_orphaned_bom_refs_vulnerabilities(self) -> None: + sbom = get_test_sbom() + sbom["vulnerabilities"] = [ + { + "description": ( + "The application is vulnerable to remote SQL" + " injection and shell upload" + ), + "id": "Vul 1", + "ratings": [ + { + "score": 9.8, + "severity": "critical", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + }, + { + "score": 7.5, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + }, + ], + "affects": [ + {"ref": "sp_eight_component"}, + {"ref": "sp_eleventh_component"}, + ], + } + ] + issues = plausibility_check(sbom) + self.assertEqual(search_for_word_issues("vulnerability", issues), True) + + def test_check_for_orphaned_bom_refs_compositions(self) -> None: + sbom = get_test_sbom() + sbom["compositions"][0]["assemblies"].append("new_reference") + issues = plausibility_check(sbom) + self.assertEqual(search_for_word_issues("compositions", issues), True) + + def test_validate_active_plausibility_check(self) -> None: + sbom = get_test_sbom() + sbom["compositions"][0]["assemblies"].append("new_ref") + issues = validate_test(sbom, plausability_check=True) + self.assertEqual(search_for_word_issues("orphaned bom-ref", issues), True) + + +class TestPlausabilityHelperFunctions(unittest.TestCase): + def test_one_non_unique_bom_ref(self) -> None: + sbom = get_test_sbom() + sbom["components"][0]["bom-ref"] = "bom-ref_1" + sbom["components"][-1]["bom-ref"] = "bom-ref_1" + error = create_error_non_unique_bom_ref("bom-ref_1", sbom) + id_1 = ComponentIdentity.create(sbom["components"][0], allow_unsafe=True) + id_2 = ComponentIdentity.create(sbom["components"][-1], allow_unsafe=True) + expected_error = ( + "SBOM has the mistake: found non unique bom-ref. " + "The reference (bom-ref_1) is used in several components. Those are" + f"({id_1})" + f"({id_2})" + ) + self.assertEqual(error, expected_error) + + def test_get_errors_non_unique_sbom(self) -> None: + sbom = get_test_sbom() + sbom["components"][0]["bom-ref"] = "bom-ref_1" + sbom["components"][-1]["bom-ref"] = "bom-ref_1" + error = get_errors_for_non_unique_bomrefs(sbom) + id_1 = ComponentIdentity.create(sbom["components"][0], allow_unsafe=True) + id_2 = ComponentIdentity.create(sbom["components"][-1], allow_unsafe=True) + expected_error = ( + "SBOM has the mistake: found non unique bom-ref. " + "The reference (bom-ref_1) is used in several components. Those are" + f"({id_1})" + f"({id_2})" + ) + self.assertEqual(error, [expected_error]) + + def test_plausibility_two_orphaned_sbom(self) -> None: + sbom = get_test_sbom() + sbom["dependencies"][3]["dependsOn"].append("new_reference") + sbom["dependencies"][3]["dependsOn"].append("new_reference_2") + list_of_errors = plausibility_check(sbom) + self.assertEqual(search_for_word_issues("dependencies", list_of_errors), True) + self.assertEqual(search_for_word_issues("new_reference", list_of_errors), True) + self.assertEqual( + search_for_word_issues("new_reference_2", list_of_errors), True + ) + + def test_get_a_list_of_upstream_dependencies(self) -> None: + dependencies = [ + { + "ref": "sub_programm", + "dependsOn": [ + "sp_first_component", + "sp_second_component", + "sp_fourth_component", + "sp_fifth_component", + "sp_sixth_component", + ], + }, + { + "ref": "sp_first_component", + "dependsOn": ["sp_seventh_component", "sp_eight_component"], + }, + {"ref": "sp_second_component", "dependsOn": ["sp_seventeenth_component"]}, + {"ref": "sp_fourth_component", "dependsOn": ["sp_seventeenth_component"]}, + {"ref": "sp_fifth_component", "dependsOn": ["sp_seventeenth_component"]}, + {"ref": "sp_sixth_component", "dependsOn": ["sp_seventeenth_component"]}, + { + "ref": "sp_seventh_component", + "dependsOn": ["sp_ninth_component", "sp_twelfth_component"], + }, + { + "ref": "sp_eight_component", + "dependsOn": [ + "sp_tenth_component", + "sp_twelfth_component", + "sp_thirteenth_component", + ], + }, + {"ref": "sp_ninth_component", "dependsOn": ["sp_seventeenth_component"]}, + {"ref": "sp_tenth_component", "dependsOn": ["sp_seventeenth_component"]}, + { + "ref": "sp_eleventh_component", + "dependsOn": ["sp_seventeenth_component", "sp_tenth_component"], + }, + { + "ref": "sp_twelfth_component", + "dependsOn": [ + "sp_thirteenth_component", + "sp_fourteenth_component", + "sp_fifteenth_component", + ], + }, + { + "ref": "sp_thirteenth_component", + "dependsOn": ["sp_seventeenth_component"], + }, + { + "ref": "sp_fourteenth_component", + "dependsOn": ["sp_sixteenth_component", "sp_seventeenth_component"], + }, + { + "ref": "sp_sixteenth_component", + "dependsOn": ["sp_seventeenth_component"], + }, + {"ref": "sp_seventeenth_component", "dependsOn": []}, + { + "ref": "sp_fifteenth_component", + "dependsOn": ["sp_eleventh_component", "sp_seventeenth_component"], + }, + ] + list_of_upstream_dependencies = get_upstream_dependency_bom_refs( + "sub_programm", dependencies + ) + list_of_dependencies = [ + "sp_first_component", + "sp_second_component", + "sp_fourth_component", + "sp_fifth_component", + "sp_sixth_component", + "sp_seventh_component", + "sp_eight_component", + "sp_ninth_component", + "sp_tenth_component", + "sp_eleventh_component", + "sp_twelfth_component", + "sp_thirteenth_component", + "sp_fourteenth_component", + "sp_sixteenth_component", + "sp_seventeenth_component", + "sp_fifteenth_component", + ] + self.assertEqual(set(list_of_upstream_dependencies), set(list_of_dependencies)) + list_of_upstream_dependencies = get_upstream_dependency_bom_refs( + "sp_seventeenth_component", dependencies + ) + self.assertEqual(set(list_of_upstream_dependencies), set([])) + list_of_upstream_dependencies = get_upstream_dependency_bom_refs( + "sp_fifth_component", dependencies + ) + self.assertEqual( + set(list_of_upstream_dependencies), set(["sp_seventeenth_component"]) + ) + list_of_upstream_dependencies = get_upstream_dependency_bom_refs( + "sp_seventh_component", dependencies + ) + list_of_dependencies = [ + "sp_ninth_component", + "sp_twelfth_component", + "sp_seventeenth_component", + "sp_thirteenth_component", + "sp_fourteenth_component", + "sp_fifteenth_component", + "sp_sixteenth_component", + "sp_eleventh_component", + "sp_tenth_component", + ] + self.assertEqual(set(list_of_upstream_dependencies), set(list_of_dependencies)) + + def test_check_for_orphaned_bom_refs_valid_sbom(self) -> None: + sbom = get_test_sbom() + self.assertEqual(check_for_orphaned_bom_refs(sbom), [])