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]

Reply via email to