This is an automated email from the ASF dual-hosted git repository. sbp pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 86ca289242044fc9f3c7ae6a61dd0e76a806591d Author: Alastair McFarlane <[email protected]> AuthorDate: Mon Dec 15 15:38:40 2025 +0000 Link to vulnerability details from report, include more info in error model, don't error on missing PURL for files. --- atr/get/sbom.py | 17 +++++++++++------ atr/models/results.py | 2 +- atr/sbom/cli.py | 3 ++- atr/sbom/conformance.py | 14 ++++++++++++-- atr/sbom/models/bom.py | 2 ++ atr/sbom/models/conformance.py | 1 + atr/sbom/osv.py | 15 ++++++++------- atr/tasks/sbom.py | 6 +++--- start-dev.sh | 2 +- 9 files changed, 41 insertions(+), 21 deletions(-) diff --git a/atr/get/sbom.py b/atr/get/sbom.py index ce75a5a..c85d477 100644 --- a/atr/get/sbom.py +++ b/atr/get/sbom.py @@ -194,10 +194,12 @@ def _vulnerability_component_details(block: htm.Block, component: results.OSVCom for vuln in component.vulnerabilities: vuln_id = vuln.get("id", "Unknown") vuln_summary = vuln.get("summary", "No summary available") + vuln_refs = [r for r in vuln.get("references", []) if r.get("type", "") == "WEB"] + vuln_primary_ref = vuln_refs[0] if (len(vuln_refs) > 0) else {} vuln_modified = vuln.get("modified", "Unknown") vuln_severity = _extract_vulnerability_severity(vuln) - vuln_header = [htm.strong(".me-2")[vuln_id]] + vuln_header = [htm.a(href=vuln_primary_ref.get("url", ""), target="_blank")[htm.strong(".me-2")[vuln_id]]] if vuln_severity != "Unknown": vuln_header.append(htm.span(".badge.bg-warning.text-dark")[vuln_severity]) @@ -256,13 +258,15 @@ def _vulnerability_scan_results(block: htm.Block, task: sql.Task) -> None: return components = task_result.components - ignored_count = task_result.ignored_count + ignored = task_result.ignored + ignored_count = len(ignored) if not components: block.p["No vulnerabilities found."] if ignored_count > 0: - component_word = "component" if (ignored_count == 1) else "components" - block.p[f"{ignored_count} {component_word} were ignored due to missing PURL or version information."] + component_word = "component was" if (ignored_count == 1) else "components were" + block.p[f"{ignored_count} {component_word} ignored due to missing PURL or version information:"] + block.p[f"{','.join(ignored)}"] return block.p[f"Found vulnerabilities in {len(components)} components:"] @@ -271,8 +275,9 @@ def _vulnerability_scan_results(block: htm.Block, task: sql.Task) -> None: _vulnerability_component_details(block, component) if ignored_count > 0: - component_word = "component" if (ignored_count == 1) else "components" - block.p[f"{ignored_count} {component_word} were ignored due to missing PURL or version information."] + component_word = "component was" if (ignored_count == 1) else "components were" + block.p[f"{ignored_count} {component_word} ignored due to missing PURL or version information:"] + block.p[f"{','.join(ignored)}"] def _vulnerability_scan_section( diff --git a/atr/models/results.py b/atr/models/results.py index d09a58a..9db1b55 100644 --- a/atr/models/results.py +++ b/atr/models/results.py @@ -58,7 +58,7 @@ class SBOMOSVScan(schema.Strict): revision_number: str = schema.description("Revision number") file_path: str = schema.description("Relative path to the scanned SBOM file") components: list[OSVComponent] = schema.description("Components with vulnerabilities") - ignored_count: int = schema.description("Number of components ignored") + ignored: list[str] = schema.description("Components ignored") class SbomQsScore(schema.Strict): diff --git a/atr/sbom/cli.py b/atr/sbom/cli.py index 4c8721f..4ef3bec 100644 --- a/atr/sbom/cli.py +++ b/atr/sbom/cli.py @@ -70,7 +70,8 @@ def command_missing(bundle: models.bundle.Bundle) -> None: def command_osv(bundle: models.bundle.Bundle) -> None: - results, ignored_count = asyncio.run(osv.scan_bundle(bundle)) + results, ignored = asyncio.run(osv.scan_bundle(bundle)) + ignored_count = len(ignored) if ignored_count > 0: print(f"Warning: {ignored_count} components ignored (missing purl or version)") for component_result in results: diff --git a/atr/sbom/conformance.py b/atr/sbom/conformance.py index f3e868a..ae39e1c 100644 --- a/atr/sbom/conformance.py +++ b/atr/sbom/conformance.py @@ -289,7 +289,8 @@ def ntia_2021_issues( cpe_is_none = bom_value.metadata.component.cpe is None purl_is_none = bom_value.metadata.component.purl is None swid_is_none = bom_value.metadata.component.swid is None - if cpe_is_none and purl_is_none and swid_is_none: + type_is_file = bom_value.metadata.component.type == "file" + if cpe_is_none and purl_is_none and swid_is_none and (not type_is_file): warnings.append( models.conformance.MissingComponentProperty( property=models.conformance.ComponentProperty.IDENTIFIER @@ -307,11 +308,16 @@ def ntia_2021_issues( errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA)) for index, component in enumerate(bom_value.components or []): + component_type = component.type + component_friendly_name = component.name + if component_type is not None: + component_friendly_name = f"{component_type}: {component_friendly_name}" if component.supplier is None: errors.append( models.conformance.MissingComponentProperty( property=models.conformance.ComponentProperty.SUPPLIER, index=index, + component=component_friendly_name, ) ) @@ -320,6 +326,7 @@ def ntia_2021_issues( models.conformance.MissingComponentProperty( property=models.conformance.ComponentProperty.NAME, index=index, + component=component_friendly_name, ) ) @@ -328,17 +335,20 @@ def ntia_2021_issues( models.conformance.MissingComponentProperty( property=models.conformance.ComponentProperty.VERSION, index=index, + component=component_friendly_name, ) ) component_cpe_is_none = component.cpe is None component_purl_is_none = component.purl is None component_swid_is_none = component.swid is None - if component_cpe_is_none and component_purl_is_none and component_swid_is_none: + component_type_is_file = component_type == "file" + if component_cpe_is_none and component_purl_is_none and component_swid_is_none and (not component_type_is_file): warnings.append( models.conformance.MissingComponentProperty( property=models.conformance.ComponentProperty.IDENTIFIER, index=index, + component=component_friendly_name, ) ) diff --git a/atr/sbom/models/bom.py b/atr/sbom/models/bom.py index b5c0a4b..60c872f 100644 --- a/atr/sbom/models/bom.py +++ b/atr/sbom/models/bom.py @@ -28,6 +28,7 @@ class Swid(Lax): class Supplier(Lax): name: str | None = None + url: str | None = None class License(Lax): @@ -51,6 +52,7 @@ class Component(Lax): swid: Swid | None = None licenses: list[LicenseChoice] | None = None scope: str | None = None + type: str | None = None class ToolComponent(Lax): diff --git a/atr/sbom/models/conformance.py b/atr/sbom/models/conformance.py index 95faaa1..2d14a04 100644 --- a/atr/sbom/models/conformance.py +++ b/atr/sbom/models/conformance.py @@ -57,6 +57,7 @@ class MissingProperty(Strict): class MissingComponentProperty(Strict): kind: Literal["missing_component_property"] = "missing_component_property" property: ComponentProperty + component: str | None = None index: int | None = None def __str__(self) -> str: diff --git a/atr/sbom/osv.py b/atr/sbom/osv.py index f6b80cc..fc84892 100644 --- a/atr/sbom/osv.py +++ b/atr/sbom/osv.py @@ -28,11 +28,12 @@ _DEBUG: bool = os.environ.get("DEBUG_SBOM_TOOL") == "1" _OSV_API_BASE: str = "https://api.osv.dev/v1" -async def scan_bundle(bundle: models.bundle.Bundle) -> tuple[list[models.osv.ComponentVulnerabilities], int]: +async def scan_bundle(bundle: models.bundle.Bundle) -> tuple[list[models.osv.ComponentVulnerabilities], list[str]]: components = bundle.bom.components or [] - queries, ignored_count = _scan_bundle_build_queries(components) + queries, ignored = _scan_bundle_build_queries(components) if _DEBUG: print(f"[DEBUG] Scanning {len(queries)} components for vulnerabilities") + ignored_count = len(ignored) if ignored_count > 0: print(f"[DEBUG] {ignored_count} components ignored (missing purl or version)") async with aiohttp.ClientSession() as session: @@ -43,7 +44,7 @@ async def scan_bundle(bundle: models.bundle.Bundle) -> tuple[list[models.osv.Com result: list[models.osv.ComponentVulnerabilities] = [] for purl, vulns in component_vulns_map.items(): result.append(models.osv.ComponentVulnerabilities(purl=purl, vulnerabilities=vulns)) - return result, ignored_count + return result, ignored def _component_purl_with_version(component: models.bom.Component) -> str | None: @@ -125,17 +126,17 @@ async def _paginate_query( def _scan_bundle_build_queries( components: list[models.bom.Component], -) -> tuple[list[tuple[str, dict[str, Any]]], int]: +) -> tuple[list[tuple[str, dict[str, Any]]], list[str]]: queries: list[tuple[str, dict[str, Any]]] = [] - ignored_count = 0 + ignored = [] for component in components: purl_with_version = _component_purl_with_version(component) if purl_with_version is None: - ignored_count += 1 + ignored.append(component.name) continue query = {"package": {"purl": purl_with_version}} queries.append((purl_with_version, query)) - return queries, ignored_count + return queries, ignored async def _scan_bundle_fetch_vulnerabilities( diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py index 9503e65..e837a87 100644 --- a/atr/tasks/sbom.py +++ b/atr/tasks/sbom.py @@ -139,7 +139,7 @@ async def osv_scan(args: FileArgs) -> results.Results | None: if not (full_path.endswith(".cdx.json") and os.path.isfile(full_path)): raise SBOMScanningError("SBOM file does not exist", {"file_path": args.file_path}) bundle = sbom.utilities.path_to_bundle(pathlib.Path(full_path)) - vulnerabilities, ignored_count = await sbom.osv.scan_bundle(bundle) + vulnerabilities, ignored = await sbom.osv.scan_bundle(bundle) components = [results.OSVComponent(purl=v.purl, vulnerabilities=v.vulnerabilities) for v in vulnerabilities] return results.SBOMOSVScan( kind="sbom_osv_scan", @@ -148,7 +148,7 @@ async def osv_scan(args: FileArgs) -> results.Results | None: revision_number=args.revision_number, file_path=args.file_path, components=components, - ignored_count=ignored_count, + ignored=ignored, ) @@ -256,7 +256,7 @@ async def _generate_cyclonedx_core(artifact_path: str, output_path: str) -> dict log.info(f"Using root directory: {extract_dir}") # Run syft to generate the CycloneDX SBOM - syft_command = ["syft", extract_dir, "-o", "cyclonedx-json"] + syft_command = ["syft", extract_dir, "-o", "cyclonedx-json", "--base-path", f"{temp_dir!s}"] log.info(f"Running syft: {' '.join(syft_command)}") try: diff --git a/start-dev.sh b/start-dev.sh index c9bba96..ae7a915 100755 --- a/start-dev.sh +++ b/start-dev.sh @@ -13,4 +13,4 @@ fi echo "Starting hypercorn on ${BIND}" >> /opt/atr/state/hypercorn.log exec hypercorn --reload --bind "${BIND}" \ - --keyfile key.pem --certfile cert.pem atr.server:app >> /opt/atr/state/hypercorn.log 2>&1 + --keyfile key.pem --certfile cert.pem atr.server:app | tee /opt/atr/state/hypercorn.log 2>&1 --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
