From e4f0fb126d01663162d0e46f69ce0df942e92b16 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Thu, 9 Apr 2026 10:13:58 +0200 Subject: [PATCH] utils/generate-cyclonedx: generate externalReferences with source-distribution BSI TR-03183-2 5.4.2 [1] lists source code URIs under "Additional data fields for each component", and as such "MUST additionally be provided, if it exists". If a http or https source download URI is available from show-info, extract it and include it as an externalReference of type "source-distribution" in the CycloneDX output. [1] https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TR03183/BSI-TR-03183-2_v2_1_0.pdf?__blob=publicationFile&v=5 Signed-off-by: Martin Willi Acked-by: Thomas Perale Signed-off-by: Arnout Vandecappelle --- .../tests/utils/test_generate_cyclonedx.py | 26 ++++++++++ utils/generate-cyclonedx | 47 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/support/testing/tests/utils/test_generate_cyclonedx.py b/support/testing/tests/utils/test_generate_cyclonedx.py index 54250058a0..a036d23d7d 100644 --- a/support/testing/tests/utils/test_generate_cyclonedx.py +++ b/support/testing/tests/utils/test_generate_cyclonedx.py @@ -140,3 +140,29 @@ class TestGenerateCycloneDX(unittest.TestCase): foo_deps = next(d for d in result["dependencies"] if d["ref"] == "package-foo") self.assertEqual(foo_deps["dependsOn"], ["package-bar", "skeleton-baz"]) + + def test_external_references(self): + info = self._make_show_info() + info["package-foo"]["downloads"] = [ + { + "source": "foo-1.2.tar.gz", + "uris": [ + "https+https://sources.buildroot.net/foo", + "http|https+https://mirror.example.org/foo", + ], + }, + ] + + result = self._run_script(show_info=info) + foo = self._find_component(result, "package-foo") + + self.assertIn("externalReferences", foo) + self.assertEqual( + foo["externalReferences"], + [ + { + "type": "source-distribution", + "url": "https://mirror.example.org/foo/foo-1.2.tar.gz", + }, + ], + ) diff --git a/utils/generate-cyclonedx b/utils/generate-cyclonedx index f4d5afd847..a3b7293f9a 100755 --- a/utils/generate-cyclonedx +++ b/utils/generate-cyclonedx @@ -14,6 +14,8 @@ import gzip import json import os from pathlib import Path +from typing import Iterator +import urllib.parse import urllib.request import subprocess import sys @@ -261,6 +263,50 @@ def cyclonedx_patches(patch_list: list[str]): } +def parse_uris(uris: list[str]) -> Iterator[tuple[list[str], str]]: + """Parse download URIs into (schemes, url) tuples. + + Splits the Buildroot URI format "scheme[|scheme]+url" and yields all + Buildroot schemes with the stripped URL, excluding + sources.buildroot.net mirrors. + + Args: + uris (list): Array of URI strings from the show-info output. + Yields: + tuple[list[str], str]: (schemes, url) for each usable URI. + """ + for uri in uris: + scheme, _, stripped_uri = uri.partition("+") + if stripped_uri: + parsed = urllib.parse.urlparse(stripped_uri) + if parsed.hostname != "sources.buildroot.net": + yield scheme.split("|"), stripped_uri + + +def cyclonedx_external_refs(comp): + """Create CycloneDX external references for a component. + + Args: + comp (dict): The component information from the show-info output. + Returns: + dict: External reference information in CycloneDX format, or empty dict + """ + SOURCE_DIST_SCHEMES = {"http", "https"} + + refs = [] + for download in comp.get("downloads", []): + source = download.get("source") + for schemes, uri in parse_uris(download.get("uris", [])): + if set(schemes) & SOURCE_DIST_SCHEMES and source: + refs.append({ + "type": "source-distribution", + "url": f"{uri}/{source}", + }) + if refs: + return {"externalReferences": refs} + return {} + + def cyclonedx_component(name, comp): """Translate a component from the show-info output, to a component entry in CycloneDX format. @@ -284,6 +330,7 @@ def cyclonedx_component(name, comp): **({ "cpe": comp["cpe-id"], } if "cpe-id" in comp else {}), + **cyclonedx_external_refs(comp), **(cyclonedx_patches(comp["patches"]) if comp.get("patches") else {}), "properties": [{ "name": "BR_TYPE",