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 <martin@strongswan.org>
Acked-by: Thomas Perale <thomas.perale@mind.be>
Signed-off-by: Arnout Vandecappelle <arnout@rnout.be>
This commit is contained in:
Martin Willi
2026-04-09 10:13:58 +02:00
committed by Arnout Vandecappelle
parent cc41cc3fcd
commit e4f0fb126d
2 changed files with 73 additions and 0 deletions

View File

@@ -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",
},
],
)

View File

@@ -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",