From 1791b7942250435e056dbbc726bbb1f4ad9a9612 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Thu, 9 Apr 2026 10:14:00 +0200 Subject: [PATCH] utils/generate-cyclonedx: add hashes from .hash files to externalReferences BSI TR-03183-2 5.2.5 [1] lists the "Hash value of the source code of the component" under "Optional data fields for each component", and as such CycloneDX "MAY additionally include the [...] information, if it exists". As hash values are available in Buildroot, iterate over .hash file paths from show-info input and read hash values for the source distribution. Add all found hashes to externalReferences source-distribution entries. [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 | 44 +++++++++++++++++++ utils/generate-cyclonedx | 34 ++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/support/testing/tests/utils/test_generate_cyclonedx.py b/support/testing/tests/utils/test_generate_cyclonedx.py index a036d23d7d..0a13fdb6ad 100644 --- a/support/testing/tests/utils/test_generate_cyclonedx.py +++ b/support/testing/tests/utils/test_generate_cyclonedx.py @@ -166,3 +166,47 @@ class TestGenerateCycloneDX(unittest.TestCase): }, ], ) + + def test_external_references_hashes(self): + with tempfile.TemporaryDirectory() as tmpdir: + hash_file = Path(tmpdir) / "foo.hash" + hash_file.write_text( + "# source archive checksums\n" + "sha256 1111111111111111111111111111111111111111111111111111111111111111 foo-1.2.tar.gz\n" + "sha1 2222222222222222222222222222222222222222 foo-1.2.tar.gz\n" + "sha256 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa LICENSE\n" + ) + + info = self._make_show_info() + info["package-foo"]["hashes"] = [str(hash_file)] + info["package-foo"]["downloads"] = [ + { + "source": "foo-1.2.tar.gz", + "uris": [ + "http|https+https://mirror.example.org/foo", + ], + }, + ] + + result = self._run_script(show_info=info) + + foo = self._find_component(result, "package-foo") + self.assertEqual( + foo["externalReferences"], + [ + { + "type": "source-distribution", + "url": "https://mirror.example.org/foo/foo-1.2.tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "1111111111111111111111111111111111111111111111111111111111111111", + }, + { + "alg": "SHA-1", + "content": "2222222222222222222222222222222222222222", + }, + ], + } + ], + ) diff --git a/utils/generate-cyclonedx b/utils/generate-cyclonedx index a3b7293f9a..382d91ce55 100755 --- a/utils/generate-cyclonedx +++ b/utils/generate-cyclonedx @@ -283,6 +283,39 @@ def parse_uris(uris: list[str]) -> Iterator[tuple[list[str], str]]: yield scheme.split("|"), stripped_uri +def cyclonedx_source_hashes(comp, source): + """Create CycloneDX hashes for a source distribution. + + Args: + comp (dict): The component information from the show-info output. + source (str): The source distribution filename to look for in the hash file. + Returns: + dict: Hash information in CycloneDX format, or empty dict + """ + MAPPING = { + "sha1": "SHA-1", + "sha256": "SHA-256", + "sha512": "SHA-512", + "md5": "MD5", + } + + hashes = [] + for hash_file in comp.get("hashes", []): + with Path(hash_file).open() as f: + for line in f: + line = line.strip() + if not line.startswith("#") and line.endswith(f" {source}"): + parts = line.split() + if len(parts) >= 3 and parts[0] in MAPPING: + hashes.append({ + "alg": MAPPING[parts[0]], + "content": parts[1], + }) + if hashes: + return {"hashes": hashes} + return {} + + def cyclonedx_external_refs(comp): """Create CycloneDX external references for a component. @@ -301,6 +334,7 @@ def cyclonedx_external_refs(comp): refs.append({ "type": "source-distribution", "url": f"{uri}/{source}", + **cyclonedx_source_hashes(comp, source), }) if refs: return {"externalReferences": refs}