You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/07/24 09:25:30 UTC

(tooling-trusted-releases) 03/03: Fix primary component in generated SBOMs where we can do so

This is an automated email from the ASF dual-hosted git repository.

alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit 6e3c6ba92ab131fc83fb08311a059e85b124acf5
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Fri Jul 24 10:17:48 2026 +0100

    Fix primary component in generated SBOMs where we can do so
---
 atr/tasks/sbom.py                    | 28 ++++++++++++++++++++
 tests/unit/test_api_sbom_generate.py | 51 ++++++++++++++++++++++++++++++++++++
 2 files changed, 79 insertions(+)

diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py
index 3187342e..51fb3f06 100644
--- a/atr/tasks/sbom.py
+++ b/atr/tasks/sbom.py
@@ -531,6 +531,7 @@ async def _generate_cyclonedx_core(
             try:
                 sbom_data = json.loads(stdout_str)
                 _strip_temp_prefix(sbom_data, str(temp_dir))
+                _promote_primary_component(sbom_data)
                 log.info(f"Successfully parsed syft output for {artifact_path}")
 
                 # Write the SBOM data to the specified output path
@@ -575,3 +576,30 @@ def _strip_temp_prefix(doc: dict[str, Any], temp_dir: str) -> None:
         name = component.get("name")
         if isinstance(name, str) and name.startswith(temp_dir):
             component["name"] = name.removeprefix(temp_dir)
+
+
+def _promote_primary_component(doc: dict[str, Any]) -> None:
+    # Scanning an archive, syft describes it with a synthetic file component and catalogues the
+    # real package alongside the rest. Where that package is the sole root of the dependency graph
+    # it is the thing the SBOM is really about, so it stands in as the primary component, carrying
+    # its own identifier and rooting the graph on itself
+    metadata = doc.get("metadata")
+    if not (isinstance(metadata, dict) and isinstance(metadata.get("component"), dict)):
+        return
+    root_component = _dependency_graph_root(doc)
+    if root_component is None:
+        return
+    metadata["component"] = root_component
+    doc["components"] = [c for c in doc.get("components", []) if c is not root_component]
+
+
+def _dependency_graph_root(doc: dict[str, Any]) -> dict[str, Any] | None:
+    dependencies = doc.get("dependencies", [])
+    if not dependencies:
+        return None
+    depended_on = {ref for entry in dependencies for ref in entry.get("dependsOn", [])}
+    roots = [entry["ref"] for entry in dependencies if entry.get("ref") not in depended_on]
+    if len(roots) != 1:
+        # No single root means no unambiguous primary, so we leave syft's own choice in place
+        return None
+    return next((c for c in doc.get("components", []) if c.get("bom-ref") == roots[0]), None)
diff --git a/tests/unit/test_api_sbom_generate.py b/tests/unit/test_api_sbom_generate.py
index cd130395..b25e627a 100644
--- a/tests/unit/test_api_sbom_generate.py
+++ b/tests/unit/test_api_sbom_generate.py
@@ -136,6 +136,57 @@ def test_strip_temp_prefix_leaves_component_names_relative_to_the_scan_root() ->
     assert doc["components"][1]["name"] == "plexus-utils"
 
 
+def _syft_shaped_doc() -> dict:
+    # syft describes the scanned archive with a synthetic file component and roots the dependency
+    # graph on the real package it found inside
+    return {
+        "metadata": {"component": {"bom-ref": "file-wrapper", "type": "file", "name": "widget-1.0.jar"}},
+        "components": [
+            {"bom-ref": "pkg:maven/org.example/widget@1.0", "type": "library", "name": "widget", "version": "1.0"},
+            {"bom-ref": "pkg:maven/org.example/helper@2.0", "type": "library", "name": "helper", "version": "2.0"},
+        ],
+        "dependencies": [
+            {"ref": "pkg:maven/org.example/widget@1.0", "dependsOn": ["pkg:maven/org.example/helper@2.0"]},
+        ],
+    }
+
+
+def test_promote_primary_component_replaces_the_file_wrapper_with_the_graph_root() -> None:
+    doc = _syft_shaped_doc()
+
+    sbom._promote_primary_component(doc)
+
+    # The primary is now the real package, not the synthetic file
+    assert doc["metadata"]["component"]["bom-ref"] == "pkg:maven/org.example/widget@1.0"
+    assert doc["metadata"]["component"]["type"] == "library"
+    # It no longer appears twice, and the graph still resolves against it
+    assert [c["bom-ref"] for c in doc["components"]] == ["pkg:maven/org.example/helper@2.0"]
+    assert doc["dependencies"][0]["ref"] == doc["metadata"]["component"]["bom-ref"]
+
+
+def test_promote_primary_component_abstains_without_a_single_root() -> None:
+    # A distribution that bundles sibling modules has no one primary, so the file wrapper stands
+    doc = _syft_shaped_doc()
+    doc["dependencies"] = [
+        {"ref": "pkg:maven/org.example/widget@1.0", "dependsOn": []},
+        {"ref": "pkg:maven/org.example/helper@2.0", "dependsOn": []},
+    ]
+
+    sbom._promote_primary_component(doc)
+
+    assert doc["metadata"]["component"]["bom-ref"] == "file-wrapper"
+    assert len(doc["components"]) == 2
+
+
+def test_promote_primary_component_abstains_without_a_dependency_graph() -> None:
+    doc = _syft_shaped_doc()
+    del doc["dependencies"]
+
+    sbom._promote_primary_component(doc)
+
+    assert doc["metadata"]["component"]["bom-ref"] == "file-wrapper"
+
+
 def test_task_get_results_round_trip_typed_result() -> None:
     task = sql.Task(task_type=sql.TaskType.SBOM_GENERATE, task_args={}, asf_uid="test")
     task.result = results.SBOMGenerate(


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org