From 1487b0085cd9017e8134b563a2c0006262011ebc Mon Sep 17 00:00:00 2001 From: Gernot Hillier Date: Thu, 10 Jul 2025 10:00:38 +0200 Subject: [PATCH] fix: support file:// URLs in SBOMs As we need string handling and anyways converted to str everywhere, I also changed the return value of CycloneDxSupport.get_ext_ref_*_file. Fixes #161 --- ChangeLog.md | 1 + capycli/bom/create_components.py | 4 ++-- capycli/bom/legacy.py | 4 ++-- capycli/bom/map_bom.py | 2 +- capycli/common/capycli_bom_support.py | 16 ++++++++++++---- tests/test_bom_create_releases.py | 9 ++++++++- tests/test_bom_filter.py | 2 +- tests/test_bom_map2.py | 12 ++++++------ tests/test_get_dependencies_python.py | 4 ++-- tests/test_legacy_cx.py | 3 +-- 10 files changed, 36 insertions(+), 21 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index f3467949..db04b84c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,6 +9,7 @@ * `bom map` will provide the `purl` from SW360 in the output BOM's components (due to a missing code path, PURL from input BOM was copied to mapping result instead) +* support file:// URLs for local paths in SBOMs ## 2.9.0 diff --git a/capycli/bom/create_components.py b/capycli/bom/create_components.py index e3f4735e..0f392cde 100644 --- a/capycli/bom/create_components.py +++ b/capycli/bom/create_components.py @@ -438,12 +438,12 @@ def upload_file( filehash = None if filetype in ["SOURCE", "SOURCE_SELF"]: url = str(CycloneDxSupport.get_ext_ref_source_url(cx_comp)) - filename = str(CycloneDxSupport.get_ext_ref_source_file(cx_comp)) + filename = CycloneDxSupport.get_ext_ref_source_file(cx_comp) filehash = str(CycloneDxSupport.get_source_file_hash(cx_comp)) if filetype in ["BINARY", "BINARY_SELF"]: url = str(CycloneDxSupport.get_ext_ref_binary_url(cx_comp)) - filename = str(CycloneDxSupport.get_ext_ref_binary_file(cx_comp)) + filename = CycloneDxSupport.get_ext_ref_binary_file(cx_comp) filehash = str(CycloneDxSupport.get_binary_file_hash(cx_comp)) # Note that we retrieve the SHA1 has from the CycloneDX data. diff --git a/capycli/bom/legacy.py b/capycli/bom/legacy.py index 3eb63d05..0d8460f9 100644 --- a/capycli/bom/legacy.py +++ b/capycli/bom/legacy.py @@ -274,9 +274,9 @@ def cdx_component_to_legacy(cls, cx_comp: Component) -> Dict[str, Any]: lcomp["Language"] = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_LANGUAGE) lcomp["SourceUrl"] = str(CycloneDxSupport.get_ext_ref_source_url(cx_comp)) lcomp["RepositoryUrl"] = str(CycloneDxSupport.get_ext_ref_repository(cx_comp)) - lcomp["SourceFile"] = str(CycloneDxSupport.get_ext_ref_source_file(cx_comp)) + lcomp["SourceFile"] = CycloneDxSupport.get_ext_ref_source_file(cx_comp) lcomp["SourceFileHash"] = CycloneDxSupport.get_source_file_hash(cx_comp) - lcomp["BinaryFile"] = str(CycloneDxSupport.get_ext_ref_binary_file(cx_comp)) + lcomp["BinaryFile"] = CycloneDxSupport.get_ext_ref_binary_file(cx_comp) lcomp["BinaryFileHash"] = CycloneDxSupport.get_binary_file_hash(cx_comp) lcomp["BinaryFileUrl"] = str(CycloneDxSupport.get_ext_ref_binary_url(cx_comp)) lcomp["Homepage"] = str(CycloneDxSupport.get_ext_ref_website(cx_comp)) diff --git a/capycli/bom/map_bom.py b/capycli/bom/map_bom.py index 409f3b74..f082bbef 100644 --- a/capycli/bom/map_bom.py +++ b/capycli/bom/map_bom.py @@ -229,7 +229,7 @@ def map_bom_item(self, component: Component, check_similar: bool, result_require break # fourth check: source filename - cmp_src_file = str(CycloneDxSupport.get_ext_ref_source_file(component)) + cmp_src_file = CycloneDxSupport.get_ext_ref_source_file(component) if (("SourceFile" in release) and cmp_src_file and release["SourceFile"]): diff --git a/capycli/common/capycli_bom_support.py b/capycli/common/capycli_bom_support.py index 952ccd29..d3ff8bab 100644 --- a/capycli/common/capycli_bom_support.py +++ b/capycli/common/capycli_bom_support.py @@ -217,11 +217,15 @@ def get_ext_ref_source_url(comp: Component) -> Any: return "" @staticmethod - def get_ext_ref_source_file(comp: Component) -> Any: + def get_ext_ref_source_file(comp: Component) -> str: for ext_ref in comp.external_references: if (ext_ref.type == ExternalReferenceType.DISTRIBUTION) \ and (ext_ref.comment == CaPyCliBom.SOURCE_FILE_COMMENT): - return ext_ref.url + url = str(ext_ref.url) + if url.startswith("file://"): + return url[7:] + else: + return url return "" @@ -235,11 +239,15 @@ def get_ext_ref_binary_url(comp: Component) -> Any: return "" @staticmethod - def get_ext_ref_binary_file(comp: Component) -> Any: + def get_ext_ref_binary_file(comp: Component) -> str: for ext_ref in comp.external_references: if (ext_ref.type == ExternalReferenceType.DISTRIBUTION) \ and (ext_ref.comment == CaPyCliBom.BINARY_FILE_COMMENT): - return ext_ref.url + url = str(ext_ref.url) + if url.startswith("file://"): + return url[7:] + else: + return url return "" diff --git a/tests/test_bom_create_releases.py b/tests/test_bom_create_releases.py index 16dd3228..c0571864 100644 --- a/tests/test_bom_create_releases.py +++ b/tests/test_bom_create_releases.py @@ -664,6 +664,12 @@ def test_upload_file_local(self) -> None: CaPyCliBom.SOURCE_URL_COMMENT, my_url) self.app.upload_file(item, {}, "06a6e7", "SOURCE_SELF", "") + # local filename with file:// prefix + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "file://Readme.md") + self.app.upload_file(item, {}, "06a6e7", "SOURCE_SELF", "") + # local filename guessed from remote url item = Component( name="activemodel", @@ -675,9 +681,10 @@ def test_upload_file_local(self) -> None: CaPyCliBom.SOURCE_URL_COMMENT, my_url) self.app.upload_file(item, {}, "06a6e7", "SOURCE_SELF", "") - assert len(responses.calls) == 2 + assert len(responses.calls) == 3 captured = self.capsys.readouterr() # type: ignore assert "Error" not in captured.out + assert "not found" not in captured.out assert captured.err == "" @responses.activate diff --git a/tests/test_bom_filter.py b/tests/test_bom_filter.py index 622279fc..91695491 100644 --- a/tests/test_bom_filter.py +++ b/tests/test_bom_filter.py @@ -401,7 +401,7 @@ def test_update_single_item(self) -> None: self.assertEqual(component["Version"], bom.components[2].version) self.assertEqual(component["RepositoryId"], bom.components[2].purl.to_string()) self.assertEqual(component["SourceFileUrl"], str(CycloneDxSupport.get_ext_ref_source_url(bom.components[2]))) - self.assertEqual(component["SourceFile"], str(CycloneDxSupport.get_ext_ref_source_file(bom.components[2]))) + self.assertEqual(component["SourceFile"], CycloneDxSupport.get_ext_ref_source_file(bom.components[2])) self.assertEqual( component["Language"], CycloneDxSupport.get_property_value(bom.components[2], CycloneDxSupport.CDX_PROP_LANGUAGE)) diff --git a/tests/test_bom_map2.py b/tests/test_bom_map2.py index b247af09..357e7eb9 100644 --- a/tests/test_bom_map2.py +++ b/tests/test_bom_map2.py @@ -3000,8 +3000,8 @@ def test_update_bom_item(self) -> None: for ext_ref in updated.external_references: self.assertIsInstance(ext_ref.url, XsUri) self.assertEqual("http://123", str(CycloneDxSupport.get_ext_ref_source_url(updated))) - self.assertEqual("123%251.zip", str(CycloneDxSupport.get_ext_ref_source_file(updated))) - self.assertEqual("123%25.dll", str(CycloneDxSupport.get_ext_ref_binary_file(updated))) + self.assertEqual("123%251.zip", CycloneDxSupport.get_ext_ref_source_file(updated)) + self.assertEqual("123%25.dll", CycloneDxSupport.get_ext_ref_binary_file(updated)) self.assertEqual("http://somewhere", str(CycloneDxSupport.get_ext_ref_website(updated))) self.assertEqual("007", CycloneDxSupport.get_property_value(updated, CycloneDxSupport.CDX_PROP_SW360ID)) @@ -3025,8 +3025,8 @@ def test_update_bom_item(self) -> None: self.assertEqual("http://456", str(CycloneDxSupport.get_ext_ref_source_url(comp))) for ext_ref in updated.external_references: self.assertIsInstance(ext_ref.url, XsUri) - self.assertEqual("456.zip", str(CycloneDxSupport.get_ext_ref_source_file(updated))) - self.assertEqual("456.dll", str(CycloneDxSupport.get_ext_ref_binary_file(updated))) + self.assertEqual("456.zip", CycloneDxSupport.get_ext_ref_source_file(updated)) + self.assertEqual("456.dll", CycloneDxSupport.get_ext_ref_binary_file(updated)) self.assertEqual("http://somewhereelse", str(CycloneDxSupport.get_ext_ref_website(updated))) self.assertEqual("888", CycloneDxSupport.get_property_value(updated, CycloneDxSupport.CDX_PROP_SW360ID)) @@ -3051,8 +3051,8 @@ def test_update_bom_item(self) -> None: for ext_ref in updated.external_references: self.assertIsInstance(ext_ref.url, XsUri) self.assertEqual("http://123", str(CycloneDxSupport.get_ext_ref_source_url(updated))) - self.assertEqual("123%251.zip", str(CycloneDxSupport.get_ext_ref_source_file(updated))) - self.assertEqual("123%25.dll", str(CycloneDxSupport.get_ext_ref_binary_file(updated))) + self.assertEqual("123%251.zip", CycloneDxSupport.get_ext_ref_source_file(updated)) + self.assertEqual("123%25.dll", CycloneDxSupport.get_ext_ref_binary_file(updated)) self.assertEqual("http://somewhere", str(CycloneDxSupport.get_ext_ref_website(updated))) self.assertEqual("007", CycloneDxSupport.get_property_value(updated, CycloneDxSupport.CDX_PROP_SW360ID)) self.assertEqual("0815", CycloneDxSupport.get_property_value(updated, CycloneDxSupport.CDX_PROP_COMPONENT_ID)) diff --git a/tests/test_get_dependencies_python.py b/tests/test_get_dependencies_python.py index d6a59692..b21c8a6f 100644 --- a/tests/test_get_dependencies_python.py +++ b/tests/test_get_dependencies_python.py @@ -222,14 +222,14 @@ def test_get_metadata(self) -> None: str(CycloneDxSupport.get_ext_ref_binary_url(sbom.components[0]))) self.assertEqual( "chardet-3.0.4-py2.py3-none-any.whl", - str(CycloneDxSupport.get_ext_ref_binary_file(sbom.components[0]))) + CycloneDxSupport.get_ext_ref_binary_file(sbom.components[0])) self.assertEqual( "https://files.pythonhosted.org/packages/fc/bb/a5768c230/chardet-3.0.4.tar.gz", str(CycloneDxSupport.get_ext_ref_source_url(sbom.components[0]))) self.assertEqual( "chardet-3.0.4.tar.gz", - str(CycloneDxSupport.get_ext_ref_source_file(sbom.components[0]))) + CycloneDxSupport.get_ext_ref_source_file(sbom.components[0])) self.delete_file(self.OUTPUTFILE1) self.delete_file(self.OUTPUTFILE2) diff --git a/tests/test_legacy_cx.py b/tests/test_legacy_cx.py index 67187133..7c54d167 100644 --- a/tests/test_legacy_cx.py +++ b/tests/test_legacy_cx.py @@ -8,7 +8,6 @@ import os -from cyclonedx.model import XsUri from cyclonedx.model.bom_ref import BomRef from sortedcontainers import SortedSet @@ -40,7 +39,7 @@ def assert_components(self, cx_components: SortedSet) -> None: cx_components[0], CycloneDxSupport.CDX_PROP_SRC_FILE_TYPE)) self.assertEqual("James Bond", CycloneDxSupport.get_property_value( cx_components[0], CycloneDxSupport.CDX_PROP_SRC_FILE_COMMENT)) - self.assertEqual(XsUri("colorama-0.4.3.tar.gz"), CycloneDxSupport.get_ext_ref_source_file( + self.assertEqual("colorama-0.4.3.tar.gz", CycloneDxSupport.get_ext_ref_source_file( cx_components[0])) self.assertEqual("python", cx_components[1].name)