From 11983d11f958c46c810aa27d9623138a3055e748 Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Thu, 25 Dec 2025 15:03:25 +0100 Subject: [PATCH 1/2] feat: Have correct `file:///` uri for files in SBOM external references --- ChangeLog.md | 1 + capycli/bom/download_sources.py | 9 ++++++--- capycli/bom/legacy.py | 2 ++ capycli/bom/legacy_cx.py | 17 +++++++++++++---- capycli/common/capycli_bom_support.py | 20 +++++++++++++++----- capycli/dependencies/javascript.py | 4 ++++ capycli/dependencies/python.py | 10 ++++++++-- tests/test_bom_downloadsources.py | 23 +++++++++++++---------- 8 files changed, 62 insertions(+), 24 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 113d5297..71e58f97 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -22,6 +22,7 @@ components have been scanned and how many warnings and errors there are. * Adapt `getdependencies python` to the Poetry 2.x pyproject.toml format. * `getdependencies python` now also supports uv and its `uv.lock` file. +* Have correct `file:///` uri for files in SBOM external references. ## 2.9.1 diff --git a/capycli/bom/download_sources.py b/capycli/bom/download_sources.py index 83f1ac51..cf5493b2 100644 --- a/capycli/bom/download_sources.py +++ b/capycli/bom/download_sources.py @@ -110,14 +110,17 @@ def download_sources(self, sbom: Bom, source_folder: str) -> None: new = False ext_ref = CycloneDxSupport.get_ext_ref( component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT) + file_uri = path + if not file_uri.startswith("file://"): + file_uri = "file:///" + file_uri if not ext_ref: ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.SOURCE_FILE_COMMENT, - url=XsUri(path)) + url=XsUri(file_uri)) new = True else: - ext_ref.url = XsUri(path) + ext_ref.url = XsUri(file_uri) ext_ref.hashes.add(HashType( alg=HashAlgorithm.SHA_1, content=sha1)) @@ -188,7 +191,7 @@ def run(self, args: Any) -> None: sys.exit(ResultCode.RESULT_ERROR_READING_BOM) if args.verbose: - print_text(" " + str(len(bom.components)) + "components written to SBOM file") + print_text(" " + str(len(bom.components)) + "components read from SBOM file") source_folder = "./" if args.source: diff --git a/capycli/bom/legacy.py b/capycli/bom/legacy.py index 099d3427..36519d94 100644 --- a/capycli/bom/legacy.py +++ b/capycli/bom/legacy.py @@ -205,6 +205,8 @@ def legacy_component_to_cdx(item: Dict[str, Any]) -> Component: binaryFile = item.get("BinaryFile", "") if binaryFile: + if not binaryFile.startswith("file://"): + binaryFile = "file:///" + binaryFile ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.BINARY_FILE_COMMENT, diff --git a/capycli/bom/legacy_cx.py b/capycli/bom/legacy_cx.py index c162031d..7fd3dde5 100644 --- a/capycli/bom/legacy_cx.py +++ b/capycli/bom/legacy_cx.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2023 Siemens +# Copyright (c) 2023-2025 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -66,10 +66,13 @@ def _convert_component(cls, component: Component) -> Component: # extra handling prop = CycloneDxSupport.get_property(component, "source-file") if prop: + file_uri = prop.value + if not file_uri.startswith("file://"): + file_uri = "file:///" + file_uri ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.SOURCE_FILE_COMMENT, - url=XsUri(prop.value)) + url=XsUri(file_uri)) prop2 = CycloneDxSupport.get_property(component, "source-file-hash") if prop2: ext_ref.hashes.add(HashType( @@ -80,10 +83,13 @@ def _convert_component(cls, component: Component) -> Component: prop = CycloneDxSupport.get_property(component, "source-file-url") if prop: + file_uri = prop.value + if not file_uri.startswith("file://"): + file_uri = "file:///" + file_uri ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.SOURCE_URL_COMMENT, - url=XsUri(prop.value)) + url=XsUri(file_uri)) prop2 = CycloneDxSupport.get_property(component, "source-file-hash") if prop2: ext_ref.hashes.add(HashType( @@ -112,10 +118,13 @@ def _convert_component(cls, component: Component) -> Component: prop = CycloneDxSupport.get_property(component, "binary-file") if prop: + file_uri = prop.value + if not file_uri.startswith("file://"): + file_uri = "file:///" + file_uri ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.BINARY_FILE_COMMENT, - url=XsUri(prop.value)) + url=XsUri(file_uri)) prop2 = CycloneDxSupport.get_property(component, "binary-file-hash") if prop2: ext_ref.hashes.add(HashType( diff --git a/capycli/common/capycli_bom_support.py b/capycli/common/capycli_bom_support.py index c330d75b..bdb659f1 100644 --- a/capycli/common/capycli_bom_support.py +++ b/capycli/common/capycli_bom_support.py @@ -163,12 +163,18 @@ def update_or_set_ext_ref(comp: Component, type: ExternalReferenceType, comment: @staticmethod def have_relative_ext_ref_path(ext_ref: ExternalReference, rel_to: str) -> str: if isinstance(ext_ref.url, str): - bip = pathlib.PurePath(ext_ref.url) + check_val = ext_ref.url._uri + if check_val.startswith("file:///"): + check_val = check_val[8:] + bip = pathlib.PurePath(check_val) else: - bip = pathlib.PurePath(ext_ref.url._uri) + check_val = ext_ref.url._uri + if check_val.startswith("file:///"): + check_val = check_val[8:] + bip = pathlib.PurePath(check_val) file = bip.as_posix() if os.path.isfile(file): - ext_ref.url = XsUri("file://" + bip.relative_to(rel_to).as_posix()) + ext_ref.url = XsUri("file:///" + bip.relative_to(rel_to).as_posix()) return bip.name @staticmethod @@ -223,7 +229,9 @@ def get_ext_ref_source_file(comp: Component) -> str: if (ext_ref.type == ExternalReferenceType.DISTRIBUTION) \ and (ext_ref.comment == CaPyCliBom.SOURCE_FILE_COMMENT): url = str(ext_ref.url) - if url.startswith("file://"): + if url.startswith("file:///"): + return url[8:] + elif url.startswith("file://"): return url[7:] else: return url @@ -245,7 +253,9 @@ def get_ext_ref_binary_file(comp: Component) -> str: if (ext_ref.type == ExternalReferenceType.DISTRIBUTION) \ and (ext_ref.comment == CaPyCliBom.BINARY_FILE_COMMENT): url = str(ext_ref.url) - if url.startswith("file://"): + if url.startswith("file:///"): + return url[8:] + elif url.startswith("file://"): return url[7:] else: return url diff --git a/capycli/dependencies/javascript.py b/capycli/dependencies/javascript.py index 13b3c694..2e8bfc18 100644 --- a/capycli/dependencies/javascript.py +++ b/capycli/dependencies/javascript.py @@ -62,6 +62,8 @@ def get_dependency(self, data: Dict[str, Any], sbom: Bom) -> Bom: url = dep.get("resolved", "").split('/')[-1] if url: + if not url.startswith("file://"): + url = "file:///" + url ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.BINARY_FILE_COMMENT, @@ -131,6 +133,8 @@ def get_dependency_lockversion3(self, data: Dict[str, Any], sbom: Bom) -> Bom: url = dep.get("resolved", "").split('/')[-1] if url: + if not url.startswith("file://"): + url = "file:///" + url ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.BINARY_FILE_COMMENT, diff --git a/capycli/dependencies/python.py b/capycli/dependencies/python.py index 68881762..586324ba 100644 --- a/capycli/dependencies/python.py +++ b/capycli/dependencies/python.py @@ -314,10 +314,13 @@ def add_meta_data_to_bomitem(self, cxcomp: Component, package_source: str = "") for item in meta["urls"]: if "packagetype" in item: if item["packagetype"] == "bdist_wheel": + file_uri = item["filename"] + if not file_uri.startswith("file://"): + file_uri = "file:///" + file_uri ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.BINARY_FILE_COMMENT, - url=XsUri(item["filename"])) + url=XsUri(file_uri)) cxcomp.external_references.add(ext_ref) LOG.debug(" got binary file") @@ -329,10 +332,13 @@ def add_meta_data_to_bomitem(self, cxcomp: Component, package_source: str = "") LOG.debug(" got binary file url") if item["packagetype"] == "sdist": + file_uri = item["filename"] + if not file_uri.startswith("file://"): + file_uri = "file:///" + file_uri ext_ref = ExternalReference( type=ExternalReferenceType.DISTRIBUTION, comment=CaPyCliBom.SOURCE_FILE_COMMENT, - url=XsUri(item["filename"])) + url=XsUri(file_uri)) cxcomp.external_references.add(ext_ref) LOG.debug(" got source file") diff --git a/tests/test_bom_downloadsources.py b/tests/test_bom_downloadsources.py index dc2bacf5..18087829 100644 --- a/tests/test_bom_downloadsources.py +++ b/tests/test_bom_downloadsources.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2023-2024 Siemens +# Copyright (c) 2023-2025 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -14,6 +14,7 @@ from cyclonedx.model.component import Component from capycli.bom.download_sources import BomDownloadSources +from capycli.common import json_support from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport from capycli.main.result_codes import ResultCode from tests.test_base import AppArguments, TestBase @@ -130,7 +131,7 @@ def test_simple_bom(self) -> None: try: out = self.capture_stdout(sut.run, args) out_bom = CaPyCliBom.read_sbom(args.outputfile) - # capycli.common.json_support.write_json_to_file(out, "STDOUT.TXT") + # json_support.write_json_to_file(out, "STDOUT.TXT") self.assertTrue("Loading SBOM file" in out) self.assertTrue("sbom_for_download.json" in out) # path may vary self.assertIn("SBOM file is not relative to", out) @@ -144,11 +145,10 @@ def test_simple_bom(self) -> None: out_bom.components[0], ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT) self.assertIsNotNone(ext_ref) if ext_ref: # only for mypy - self.assertEqual(ext_ref.url._uri, resultfile) - # if ext_ref.url is XsUri: - # self.assertEqual(ext_ref.url._uri, resultfile) - # else: - # self.assertEqual(ext_ref.url, resultfile) + check_val = ext_ref.url._uri + if check_val.startswith("file:///"): + check_val = check_val[8:] + self.assertEqual(check_val, resultfile) self.delete_file(args.outputfile) return @@ -192,7 +192,7 @@ def test_simple_bom_relative_path(self) -> None: out_bom.components[0], ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT) self.assertIsNotNone(ext_ref) if ext_ref: # only for mypy - self.assertEqual(ext_ref.url._uri, "file://certifi-2022.12.7.tar.gz") + self.assertEqual(ext_ref.url._uri, "file:///certifi-2022.12.7.tar.gz") self.delete_file(args.outputfile) return @@ -275,7 +275,10 @@ def test_simple_bom_no_url(self) -> None: bom.components[0], ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT) self.assertIsNotNone(ext_ref) if ext_ref: # only for mypy - self.assertEqual(str(ext_ref.url), resultfile) + check_val = ext_ref.url._uri + if check_val.startswith("file:///"): + check_val = check_val[8:] + self.assertEqual(check_val, resultfile) self.assertEqual(len(bom.components[1].external_references), 0) return @@ -288,4 +291,4 @@ def test_simple_bom_no_url(self) -> None: if __name__ == "__main__": lib = TestBomDownloadsources() - lib.test_simple_bom() + lib.test_simple_bom_relative_path() From 89effc525a2aec0680f68bf1361c57bccdb96080 Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Thu, 25 Dec 2025 15:04:40 +0100 Subject: [PATCH 2/2] fix: fix flake8 issues --- tests/test_bom_downloadsources.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_bom_downloadsources.py b/tests/test_bom_downloadsources.py index 18087829..aa7c8b2b 100644 --- a/tests/test_bom_downloadsources.py +++ b/tests/test_bom_downloadsources.py @@ -14,7 +14,6 @@ from cyclonedx.model.component import Component from capycli.bom.download_sources import BomDownloadSources -from capycli.common import json_support from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport from capycli.main.result_codes import ResultCode from tests.test_base import AppArguments, TestBase