diff --git a/capywfa/capywfa.py b/capywfa/capywfa.py index 3f8d50d..7d7b85b 100755 --- a/capywfa/capywfa.py +++ b/capywfa/capywfa.py @@ -7,6 +7,7 @@ # SPDX-License-Identifier: MIT import argparse +import glob import textwrap import sys import os @@ -107,11 +108,30 @@ def pass1_map_bom(bom, sw360_url, sw360_token): return result -def pass3_download_sources(bom): +def pass3_download_sources(bom, pkg_dir): for item in bom.components: - if not (get_cdx(item, "MapResult") == MapResult.NO_MATCH - or (MapBom.is_good_match(get_cdx(item, "MapResult")) - and get_cdx(item, "Sw360SourceFileCheck") != "passed")): + map_result = get_cdx(item, "MapResult") + is_missing_from_sw360 = map_result == MapResult.NO_MATCH + is_in_sw360_but_unverified = ( + MapBom.is_good_match(map_result) + and get_cdx(item, "Sw360SourceFileCheck") != "passed" + ) + needs_download = is_missing_from_sw360 or is_in_sw360_but_unverified + + if needs_download and pkg_dir: + package_name = item.purl.name + version = item.version.removesuffix(".debian") + version = version.split(":", 1)[-1] # Remove epoch if present + pattern = f"{package_name}_{version}*" + matches = glob.glob(os.path.join(pkg_dir, pattern)) + if matches: + set_cdx(item, "SourceFileComment", "sources locally available") + print( + f"Found local source for {item.name} {item.version}: " + f"{os.path.basename(matches[0])}" + ) + + if not needs_download: set_cdx(item, "SourceFileDownload", "skip") return bom @@ -352,7 +372,7 @@ def main(): print("== Pass 3: Download missing and unchecked sources ==") print() - bom = pass3_download_sources(bom) + bom = pass3_download_sources(bom, args.sources) outputbom = write_bom(bom, filename+"-3-download"+extension) missing_source_count = len( [item for item in bom.components diff --git a/capywfa/lst_to_sbom.py b/capywfa/lst_to_sbom.py index e0e645b..789645a 100644 --- a/capywfa/lst_to_sbom.py +++ b/capywfa/lst_to_sbom.py @@ -171,6 +171,8 @@ def main(): bom = lst_to_sbom(format=args.format, package_list=args.package_list) + if os.path.exists(args.output_file): + os.remove(args.output_file) JsonV1Dot6(bom=bom).output_to_file(args.output_file, indent=2) print(f"SBOM written to {args.output_file}") diff --git a/test/test_capywfa.py b/test/test_capywfa.py new file mode 100644 index 0000000..adc4394 --- /dev/null +++ b/test/test_capywfa.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: 2026 Siemens +# SPDX-License-Identifier: MIT + +import os +import tempfile + +from cyclonedx.model.bom import Bom +from cyclonedx.model.component import Component, ComponentType +from packageurl import PackageURL + +from capywfa.capywfa import pass3_download_sources, MapResult +from capywfa.cdx_support import set_cdx, get_cdx + + +def create_test_component(name, version, map_result=None, source_check=None): + """Helper function to create a test component with CDX properties.""" + component = Component( + name=name, + type=ComponentType.LIBRARY, + version=version, + purl=PackageURL("deb", "debian", name, version, {"arch": "source"}) + ) + if map_result is not None: + set_cdx(component, "MapResult", map_result) + if source_check is not None: + set_cdx(component, "Sw360SourceFileCheck", source_check) + return component + + +def test_pass3_download_sources_no_pkg_dir(): + """Test that pass3_download_sources works when no package directory is provided.""" + bom = Bom(components=[ + create_test_component("testpkg", "1.0-1", MapResult.NO_MATCH) + ]) + result = pass3_download_sources(bom, None) + assert result == bom + # Should not set SourceFileComment when pkg_dir is None + assert not get_cdx(bom.components[0], "SourceFileComment") + + +def test_pass3_download_sources_local_package_found(): + """Test that local packages are detected and marked as available.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = os.path.join(tmpdir, "testpkg_1.0-1.dsc") + with open(test_file, "w") as f: + f.write("test") + + bom = Bom(components=[ + create_test_component("testpkg", "1.0-1", MapResult.NO_MATCH) + ]) + + result = pass3_download_sources(bom, tmpdir) + + assert get_cdx(result.components[0], "SourceFileComment") == "sources locally available" + + +def test_pass3_download_sources_local_package_not_found(): + """Test that packages are not marked when local files don't exist.""" + with tempfile.TemporaryDirectory() as tmpdir: + bom = Bom(components=[ + create_test_component("testpkg", "1.0-1", MapResult.NO_MATCH) + ]) + + result = pass3_download_sources(bom, tmpdir) + + # Should not set SourceFileComment when no matching file found + assert not get_cdx(result.components[0], "SourceFileComment") + + +def test_pass3_download_sources_version_with_epoch(): + """Test that version with epoch is handled correctly.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_file = os.path.join(tmpdir, "testpkg_1.0-1.dsc") + with open(test_file, "w") as f: + f.write("test") + + # Component has epoch in version + bom = Bom(components=[ + create_test_component("testpkg", "2:1.0-1", MapResult.NO_MATCH) + ]) + + result = pass3_download_sources(bom, tmpdir) + assert get_cdx(result.components[0], "SourceFileComment") == "sources locally available"