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") foo_deps = next(d for d in result["dependencies"] if d["ref"] == "package-foo")
self.assertEqual(foo_deps["dependsOn"], ["package-bar", "skeleton-baz"]) 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 json
import os import os
from pathlib import Path from pathlib import Path
from typing import Iterator
import urllib.parse
import urllib.request import urllib.request
import subprocess import subprocess
import sys 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): def cyclonedx_component(name, comp):
"""Translate a component from the show-info output, to a component entry in CycloneDX format. """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"], "cpe": comp["cpe-id"],
} if "cpe-id" in comp else {}), } if "cpe-id" in comp else {}),
**cyclonedx_external_refs(comp),
**(cyclonedx_patches(comp["patches"]) if comp.get("patches") else {}), **(cyclonedx_patches(comp["patches"]) if comp.get("patches") else {}),
"properties": [{ "properties": [{
"name": "BR_TYPE", "name": "BR_TYPE",