Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions capywfa/capywfa.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# SPDX-License-Identifier: MIT

import argparse
import glob
import textwrap
import sys
import os
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get your idea to make this code more readable, but NO_MATCH doesn't necessarily mean it's missing, but only we didn't find it. So better use something like is_not_found or is_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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SourceFileComment is a description used in SW360 for the uploaded attachment. main() abuses it to understand if the source file exists, but it's not a flag.

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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions capywfa/lst_to_sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +174 to +175
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, this might be a topic for a separate PR or issue. But in general, what's the goal of this change? I don't think silently overwriting an existing file is a good idea. Perhaps we want a better error message here to avoid a Traceback output?

JsonV1Dot6(bom=bom).output_to_file(args.output_file, indent=2)
print(f"SBOM written to {args.output_file}")

Expand Down
83 changes: 83 additions & 0 deletions test/test_capywfa.py
Original file line number Diff line number Diff line change
@@ -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"
Loading