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