You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/09/14 13:51:46 UTC

(tooling-trusted-releases) 01/01: Fix a problem with duplicate keys in SBOM augmentations

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

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

commit a1a1d1a688828d92e27cde57073f4b857ba2c0fb
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Mon Sep 14 14:50:57 2026 +0100

    Fix a problem with duplicate keys in SBOM augmentations
---
 atr/sbom/cli.py                     |  4 +-
 atr/sbom/conformance.py             |  9 +++--
 tests/unit/test_sbom_conformance.py | 73 +++++++++++++++++++++++++++++++++++++
 3 files changed, 80 insertions(+), 6 deletions(-)

diff --git a/atr/sbom/cli.py b/atr/sbom/cli.py
index c3c427bc..3dfcedd5 100644
--- a/atr/sbom/cli.py
+++ b/atr/sbom/cli.py
@@ -167,10 +167,10 @@ def command_where(bundle: models.bundle.Bundle) -> None:
                 print(f"metadata.{error.property.name}")
                 print()
             case models.conformance.MissingComponentProperty():
-                components = bundle.bom.components
+                components = bundle.doc.get("components", [])
                 primary_component = bundle.bom.metadata.component if bundle.bom.metadata else None
                 if (error.index is not None) and len(components) > 0:
-                    print(components[error.index].as_json(SCHEMA_VERSIONS[bundle.spec_version]))
+                    print(orjson.dumps(components[error.index]).decode())
                     print()
                 elif primary_component is not None:
                     print(primary_component.as_json(SCHEMA_VERSIONS[bundle.spec_version]))
diff --git a/atr/sbom/conformance.py b/atr/sbom/conformance.py
index 3810b174..9b80b9a1 100644
--- a/atr/sbom/conformance.py
+++ b/atr/sbom/conformance.py
@@ -22,6 +22,7 @@ import urllib.parse
 from typing import TYPE_CHECKING, Any, Final
 
 import aiohttp
+import cyclonedx.model.component
 import orjson
 
 from . import constants, models
@@ -29,8 +30,6 @@ from .maven import cache_read, cache_write
 from .utilities import ensure_metadata, get_pointer, resolve_pointer
 
 if TYPE_CHECKING:
-    from cyclonedx.model.component import Component
-
     from .models.bundle import Bundle
 
 
@@ -288,8 +287,10 @@ def ntia_2021_issues(
     else:
         errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA))
 
-    components: list[Component] = list(bom_value.components)
-    for index, component in enumerate(components):
+    for index, component_data in enumerate(bundle.doc.get("components", [])):
+        component = cyclonedx.model.component.Component.from_json(data=component_data)
+        if component is None:
+            raise ValueError(f"Error importing component at /components/{index}")
         component_type = component.type
         component_friendly_name = component.name
         if component_type:
diff --git a/tests/unit/test_sbom_conformance.py b/tests/unit/test_sbom_conformance.py
new file mode 100644
index 00000000..48314908
--- /dev/null
+++ b/tests/unit/test_sbom_conformance.py
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+import pathlib
+from typing import Any
+
+import pytest
+
+import atr.sbom.cli as cli
+import atr.sbom.utilities as utilities
+
+
+@pytest.mark.parametrize("references", ["unique", "absent", "repeated"])
+async def test_augmentation_preserves_component_positions(references: str, capsys: pytest.CaptureFixture[str]) -> None:
+    components: list[dict[str, Any]] = [
+        {
+            "type": "library",
+            "name": "z-existing",
+            "version": "1",
+            "group": "org.apache.example",
+            "supplier": {"name": "Original PMC", "url": ["https://example.apache.org/"]},
+        },
+        {"type": "library", "name": "a-missing", "version": "1", "group": "org.apache.example"},
+        {"type": "library", "name": "a-missing", "version": "1", "group": "org.apache.example"},
+    ]
+    for index, component in enumerate(components):
+        if references != "absent":
+            component["bom-ref"] = str(index) if (references == "unique") else "shared"
+    doc = {
+        "bomFormat": "CycloneDX",
+        "specVersion": "1.6",
+        "version": 1,
+        "metadata": {
+            "component": {"type": "application", "name": "root", "version": "1", "purl": "pkg:generic/root@1"},
+            "supplier": {"name": "Root supplier"},
+            "manufacturer": {"name": "Root manufacturer"},
+            "timestamp": "2026-09-14T00:00:00Z",
+        },
+        "components": components,
+    }
+    bundle = utilities.text_to_bundle(json.dumps(doc), pathlib.Path("bom.cdx.json"))
+
+    cli.command_where(bundle)
+    assert [json.loads(line) for line in capsys.readouterr().out.splitlines() if line] == components[1:]
+
+    patch_ops = await utilities.bundle_to_ntia_patch(bundle)
+    version, patched = utilities.apply_patch("augment", "00001", bundle, patch_ops)
+
+    assert version == 2
+    assert bundle.doc == doc
+    assert patched["components"][0] == components[0]
+    assert [component["name"] for component in patched["components"]] == ["z-existing", "a-missing", "a-missing"]
+    for index in (1, 2):
+        assert patched["components"][index] == components[index] | {
+            "supplier": {"name": "The Apache Software Foundation", "url": ["https://apache.org/"]}
+        }
+    augmented = utilities.text_to_bundle(json.dumps(patched), bundle.path)
+    assert await utilities.bundle_to_ntia_patch(augmented) == []


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