From: Thomas Perale via buildroot <buildroot@buildroot.org>
To: buildroot@buildroot.org
Cc: Thomas Perale <thomas.perale@mind.be>
Subject: [Buildroot] [PATCH v5 4/8] utils/generate-cyclonedx: encapsulate CycloneDX generation functions
Date: Wed, 11 Mar 2026 15:04:53 +0100 [thread overview]
Message-ID: <20260311140457.140041-5-thomas.perale@mind.be> (raw)
In-Reply-To: <20260311140457.140041-1-thomas.perale@mind.be>
Since the script is starting to get bigger, it's also starting to get
more difficult to keep track of all the different functions.
This commit move all the functions that generate a part of the CycloneDX
SBOM spec under the same `CycloneDX` dataclass.
It also allows to avoid to have to deal with a top-level shared context
for the SBOM generation.
Additionnaly, two structure are created: the `Context` and `Filter` one.
They hold arguments passed from the CLI.
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
utils/generate-cyclonedx | 485 +++++++++++++++++++++------------------
1 file changed, 263 insertions(+), 222 deletions(-)
diff --git a/utils/generate-cyclonedx b/utils/generate-cyclonedx
index facbd6c78f..24c15df94e 100755
--- a/utils/generate-cyclonedx
+++ b/utils/generate-cyclonedx
@@ -18,7 +18,8 @@ import urllib.request
import subprocess
import sys
import re
-from typing import Tuple
+from typing import Mapping, Tuple
+from dataclasses import dataclass, field
CYCLONEDX_VERSION = (1, 6)
@@ -35,13 +36,21 @@ BR2_VERSION_FULL = (
.strip()
)
-# Set of vulnerabilities that were addressed by a patch present in buildroot
-# tree. This set is used to set the analysis of the ignored CVEs to
-# 'resolved_with_pedigree'.
-VULN_WITH_PEDIGREE = set()
-# List of supported SPDX license expression.
-SPDX_LICENSES = []
+@dataclass(frozen=True)
+class Options:
+ cyclonedx_version: Tuple[int, int]
+ """CycloneDX Spec version"""
+ project_name: str
+ """Specify the project name to use in the SBOM metadata"""
+ project_version: str
+ """Specify the project version to use in the SBOM metadata"""
+
+
+@dataclass(frozen=True)
+class Filters:
+ virtual: bool
+ """Includes virtual packages to the output"""
def br2_retrieve_spdx_licenses(version: Tuple[int, int]):
@@ -227,179 +236,245 @@ def split_top_level_comma(subj):
yield substring
-def cyclonedx_license(lic):
- """Given the name of a license, create an individual entry in
- CycloneDX format. In CycloneDX, the 'id' keyword is used for
- names that are recognized as SPDX License abbreviations. All other
- license names are placed under the 'name' keyword.
-
- Args:
- lic (str): Name of the license
-
- Returns:
- dict: An entry for the license in CycloneDX format.
- """
- key = "id" if lic in SPDX_LICENSES else "name"
- return {
- key: lic,
- }
-
-
-def cyclonedx_licenses(lic_list):
- """Create a licenses list formatted for a CycloneDX component
-
- Args:
- lic_list (str): A comma separated list of license names.
-
- Returns:
- dict: A dictionary with license information for the component,
- in CycloneDX format.
- """
- return {
- "licenses": [
- {"license": cyclonedx_license(lic.strip())} for lic in split_top_level_comma(lic_list)
- ]
- }
-
-
-def cyclonedx_patches(patch_list: list[str]):
- """Translate a list of patches from the show-info JSON to a list of
- patches in CycloneDX format.
-
- Args:
- patch_list (list): Array of patch relative paths for a given component.
-
- Returns:
- dict: Patch information in CycloneDX format.
- """
- patch_contents = []
- for patch in patch_list:
- patch_path = brpath / patch
- if patch_path.exists():
- try:
- content = read_patch_file(patch_path)
- except Exception:
- # If the patch can't be read it won't be added to
- # the resulting SBOM.
- print(f"Failed to handle patch: {patch}", file=sys.stderr)
- continue
-
- header = patch_retrieve_header(content)
-
- issue = {}
- cves = extract_cves_from_header(header)
- if cves:
- VULN_WITH_PEDIGREE.update(cves)
- issue = {
- "resolves": [
- {
- "type": "security",
- "name": cve
- } for cve in cves
- ]
- }
+@dataclass
+class CycloneDX:
+ show_info_dict: Mapping
+ """Output of Buildroot `make show-info` converted as a dict."""
+ options: Options
+ """CycloneDX generation options passed from the CLI arguments"""
+ filters: Filters
+ """Filters that apply to the `show_info_dict` content"""
+ _filtered_show_info_dict: Mapping = field(default_factory=dict)
+ """Subset of `show_info_dict` based on the `Filters` passed in the the
+ `filters` argument."""
+ _spdx_licenses: list = field(default_factory=list)
+ """List of supported SPDX license expression. Initialized during the
+ `__post_init__` step, fetched either online or from local copy"""
+ _vuln_with_pedigree: set = field(default_factory=set)
+ """Set of vulnerabilities that were addressed by a patch present in
+ buildroot tree. This set is used to set the analysis of the ignored CVEs to
+ 'resolved_with_pedigree'."""
+
+ def __post_init__(self):
+ self._spdx_licenses = br2_retrieve_spdx_licenses(self.options.cyclonedx_version)
+
+ # Remove rootfs and virtual packages if not explicitly included
+ # from the cli arguments
+ self._filtered_show_info_dict = {
+ k: v for k, v in self.show_info_dict.items()
+ if ("rootfs" not in v["type"]) and (self.filters.virtual or v["virtual"] is False)
+ }
+
+ def _component_licenses_license(self, lic: str):
+ """Given the name of a license, create an individual entry in
+ CycloneDX format. In CycloneDX, the 'id' keyword is used for
+ names that are recognized as SPDX License abbreviations. All other
+ license names are placed under the 'name' keyword.
+
+ Args:
+ lic (str): Name of the license
+
+ Returns:
+ dict: An entry for the license in CycloneDX format.
+ """
+ key = "id" if lic in self._spdx_licenses else "name"
+ return {
+ key: lic,
+ }
+
+ def _component_licenses(self, lic_list: str):
+ """Create a licenses list formatted for a CycloneDX component
+
+ Args:
+ lic_list (str): A comma separated list of license names.
+
+ Returns:
+ dict: A dictionary with license information for the component,
+ in CycloneDX format.
+ """
+ return {
+ "licenses": [
+ {"license": self._component_licenses_license(lic.strip())} for lic in split_top_level_comma(lic_list)
+ ]
+ }
+
+ def _component_patches(self, patch_list: list[str]):
+ """Translate a list of patches from the show-info JSON to a list of
+ patches in CycloneDX format.
+
+ Args:
+ patch_list (list): Array of patch relative paths for a given component.
+
+ Returns:
+ dict: Patch information in CycloneDX format.
+ """
+ patch_contents = []
+ for patch in patch_list:
+ patch_path = brpath / patch
+ if patch_path.exists():
+ try:
+ content = read_patch_file(patch_path)
+ except Exception:
+ # If the patch can't be read it won't be added to
+ # the resulting SBOM.
+ print(f"Failed to handle patch: {patch}", file=sys.stderr)
+ continue
+
+ header = patch_retrieve_header(content)
+
+ issue = {}
+ cves = extract_cves_from_header(header)
+ if cves:
+ self._vuln_with_pedigree.update(cves)
+ issue = {
+ "resolves": [
+ {
+ "type": "security",
+ "name": cve
+ } for cve in cves
+ ]
+ }
- patch_contents.append({
- "diff": {
- "text": {
- "content": content
+ patch_contents.append({
+ "diff": {
+ "text": {
+ "content": content
+ }
+ },
+ **issue
+ })
+ else:
+ # If the patch is not a file it's a tarball or diff url passed
+ # through the `<pkg-name>_PATCH` variable.
+ patch_contents.append({
+ "diff": {
+ "url": patch
}
+ })
+
+ return {
+ "pedigree": {
+ "patches": [{
+ "type": "unofficial",
+ **content
+ } for content in patch_contents]
+ },
+ }
+
+ def _component(self, name: str, comp: dict):
+ """Translate a component from the show-info output, to a component entry in CycloneDX format.
+
+ Args:
+ name (str): Key used for the package in the show-info output.
+ comp (dict): Data about the package as a Python dictionary.
+
+ Returns:
+ dict: Component information in CycloneDX format.
+ """
+ return {
+ "bom-ref": name,
+ "type": "library",
+ **({
+ "name": comp["name"],
+ } if "name" in comp else {}),
+ **({
+ "version": comp["version"],
+ **(self._component_licenses(comp["licenses"]) if "licenses" in comp else {}),
+ } if not comp["virtual"] else {}),
+ **({
+ "cpe": comp["cpe-id"],
+ } if "cpe-id" in comp else {}),
+ **(self._component_patches(comp["patches"]) if comp.get("patches") else {}),
+ "properties": [{
+ "name": "BR_TYPE",
+ "value": comp["type"],
+ }],
+ }
+
+ def _dependency(self, ref: str, depends: list):
+ """Create JSON for dependency relationships between components.
+
+ Args:
+ ref (str): reference to a component bom-ref.
+ depends (list): array of component bom-ref identifier to create the dependencies.
+
+ Returns:
+ dict: Dependency information in CycloneDX format.
+ """
+ return {
+ "ref": ref,
+ "dependsOn": sorted(depends),
+ }
+
+ @property
+ def vulnerabilities(self):
+ """Create a JSON list of vulnerabilities ignored by buildroot and associate
+ the component for which they are solved.
+
+ Args:
+ show_info_dict (dict): The JSON output of the show-info
+ command, parsed into a Python dictionary.
+
+ Returns:
+ list: Solved vulnerabilities list in CycloneDX format.
+ """
+ cves = {}
+
+ for name, comp in self._filtered_show_info_dict.items():
+ for cve in comp.get('ignore_cves', []):
+ cves.setdefault(cve, []).append(name)
+
+ return [{
+ "id": cve,
+ "analysis": {
+ "state": "resolved_with_pedigree" if cve in self._vuln_with_pedigree else "in_triage",
+ "detail": f"The CVE '{cve}' has been marked as ignored by Buildroot"
+ },
+ "affects": [
+ {"ref": bomref} for bomref in components
+ ]
+ } for cve, components in cves.items()]
+
+ @property
+ def cyclonedx(self):
+ return {
+ "bomFormat": "CycloneDX",
+ "$schema": "http://cyclonedx.org/schema/bom-{}.{}.schema.json".format(*self.options.cyclonedx_version),
+ "specVersion": "{}.{}".format(*self.options.cyclonedx_version),
+ "metadata": {
+ "component": {
+ "bom-ref": self.options.project_name,
+ "name": self.options.project_name,
+ "version": self.options.project_version,
+ "type": "firmware",
},
- **issue
- })
- else:
- # If the patch is not a file it's a tarball or diff url passed
- # through the `<pkg-name>_PATCH` variable.
- patch_contents.append({
- "diff": {
- "url": patch
+ "tools": {
+ "components": [
+ {
+ "type": "application",
+ "name": "Buildroot generate-cyclonedx",
+ "version": f"{BR2_VERSION_FULL}",
+ "licenses": [
+ {
+ "license": {
+ "id": "GPL-2.0"
+ }
+ }
+ ]
+ }
+ ],
}
- })
-
- return {
- "pedigree": {
- "patches": [{
- "type": "unofficial",
- **content
- } for content in patch_contents]
- },
- }
-
-
-def cyclonedx_component(name, comp):
- """Translate a component from the show-info output, to a component entry in CycloneDX format.
-
- Args:
- name (str): Key used for the package in the show-info output.
- comp (dict): Data about the package as a Python dictionary.
-
- Returns:
- dict: Component information in CycloneDX format.
- """
- return {
- "bom-ref": name,
- "type": "library",
- **({
- "name": comp["name"],
- } if "name" in comp else {}),
- **({
- "version": comp["version"],
- **(cyclonedx_licenses(comp["licenses"]) if "licenses" in comp else {}),
- } if not comp["virtual"] else {}),
- **({
- "cpe": comp["cpe-id"],
- } if "cpe-id" in comp else {}),
- **(cyclonedx_patches(comp["patches"]) if comp.get("patches") else {}),
- "properties": [{
- "name": "BR_TYPE",
- "value": comp["type"],
- }],
- }
-
-
-def cyclonedx_dependency(ref, depends):
- """Create JSON for dependency relationships between components.
-
- Args:
- ref (str): reference to a component bom-ref.
- depends (list): array of component bom-ref identifier to create the dependencies.
-
- Returns:
- dict: Dependency information in CycloneDX format.
- """
- return {
- "ref": ref,
- "dependsOn": sorted(depends),
- }
-
-
-def cyclonedx_vulnerabilities(show_info_dict):
- """Create a JSON list of vulnerabilities ignored by buildroot and associate
- the component for which they are solved.
-
- Args:
- show_info_dict (dict): The JSON output of the show-info
- command, parsed into a Python dictionary.
-
- Returns:
- list: Solved vulnerabilities list in CycloneDX format.
- """
- cves = {}
-
- for name, comp in show_info_dict.items():
- for cve in comp.get('ignore_cves', []):
- cves.setdefault(cve, []).append(name)
-
- return [{
- "id": cve,
- "analysis": {
- "state": "resolved_with_pedigree" if cve in VULN_WITH_PEDIGREE else "in_triage",
- "detail": f"The CVE '{cve}' has been marked as ignored by Buildroot"
- },
- "affects": [
- {"ref": bomref} for bomref in components
- ]
- } for cve, components in cves.items()]
+ },
+ "components": [
+ self._component(name, comp) for name, comp in self._filtered_show_info_dict.items()
+ ],
+ "dependencies": [
+ self._dependency(self.options.project_name, list(self._filtered_show_info_dict)),
+ *[self._dependency(ref, br2_parse_deps(ref, self.show_info_dict, self.filters.virtual))
+ for ref in self._filtered_show_info_dict],
+ ],
+ "vulnerabilities": self.vulnerabilities,
+ }
def main():
@@ -425,55 +500,21 @@ def main():
parser.print_help()
sys.exit(1)
- SPDX_LICENSES.extend(br2_retrieve_spdx_licenses(CYCLONEDX_VERSION))
+ opt = Options(
+ cyclonedx_version=CYCLONEDX_VERSION,
+ project_name=args.project_name,
+ project_version=args.project_version,
+ )
+
+ filters = Filters(
+ virtual=args.virtual
+ )
show_info_dict = json.load(args.in_file)
- # Remove rootfs and virtual packages if not explicitly included
- # from the cli arguments
- filtered_show_info_dict = {k: v for k, v in show_info_dict.items()
- if ("rootfs" not in v["type"]) and (args.virtual or v["virtual"] is False)}
-
- cyclonedx_dict = {
- "bomFormat": "CycloneDX",
- "$schema": "http://cyclonedx.org/schema/bom-{}.{}.schema.json".format(*CYCLONEDX_VERSION),
- "specVersion": "{}.{}".format(*CYCLONEDX_VERSION),
- "metadata": {
- "component": {
- "bom-ref": args.project_name,
- "name": args.project_name,
- "version": args.project_version,
- "type": "firmware",
- },
- "tools": {
- "components": [
- {
- "type": "application",
- "name": "Buildroot generate-cyclonedx",
- "version": f"{BR2_VERSION_FULL}",
- "licenses": [
- {
- "license": {
- "id": "GPL-2.0"
- }
- }
- ]
- }
- ],
- }
- },
- "components": [
- cyclonedx_component(name, comp) for name, comp in filtered_show_info_dict.items()
- ],
- "dependencies": [
- cyclonedx_dependency(args.project_name, list(filtered_show_info_dict)),
- *[cyclonedx_dependency(ref, br2_parse_deps(ref, show_info_dict, args.virtual))
- for ref in filtered_show_info_dict],
- ],
- "vulnerabilities": cyclonedx_vulnerabilities(show_info_dict),
- }
-
- args.out_file.write(json.dumps(cyclonedx_dict, indent=2))
+ out = CycloneDX(show_info_dict=show_info_dict, options=opt, filters=filters)
+
+ args.out_file.write(json.dumps(out.cyclonedx, indent=2))
args.out_file.write('\n')
--
2.53.0
_______________________________________________
buildroot mailing list
buildroot@buildroot.org
https://lists.buildroot.org/mailman/listinfo/buildroot
next prev parent reply other threads:[~2026-03-11 14:05 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-11 14:04 [Buildroot] [PATCH v5 0/8] Support CycloneDX v1.7 Thomas Perale via buildroot
2026-03-11 14:04 ` [Buildroot] [PATCH v5 1/8] utils/generate-cyclonedx: use tuple for version Thomas Perale via buildroot
2026-04-09 12:08 ` Quentin Schulz via buildroot
2026-04-09 20:27 ` Thomas Perale via buildroot
2026-03-11 14:04 ` [Buildroot] [PATCH v5 2/8] utils/generate-cyclonedx: move license download in a function Thomas Perale via buildroot
2026-04-09 12:12 ` Quentin Schulz via buildroot
2026-03-11 14:04 ` [Buildroot] [PATCH v5 3/8] utils/generate-cyclonedx: move utility function Thomas Perale via buildroot
2026-04-09 12:27 ` Quentin Schulz via buildroot
2026-03-11 14:04 ` Thomas Perale via buildroot [this message]
2026-03-11 14:04 ` [Buildroot] [PATCH v5 5/8] utils/generate-cyclonedx: optional bump to v1.7 Thomas Perale via buildroot
2026-04-09 12:40 ` Quentin Schulz via buildroot
2026-03-11 14:04 ` [Buildroot] [PATCH v5 6/8] utils/generate-cyclonedx: mark host packages as external Thomas Perale via buildroot
2026-04-09 12:58 ` Quentin Schulz via buildroot
2026-04-09 20:42 ` Thomas Perale via buildroot
2026-04-09 20:43 ` Thomas Perale via buildroot
2026-04-10 9:12 ` Quentin Schulz via buildroot
2026-03-11 14:04 ` [Buildroot] [PATCH v5 7/8] utils/generate-cyclonedx: add 'id' property to resolves Thomas Perale via buildroot
2026-04-09 13:22 ` Quentin Schulz via buildroot
2026-04-09 20:24 ` Thomas Perale via buildroot
2026-03-11 14:04 ` [Buildroot] [PATCH v5 8/8] utils/generate-cyclonedx: split vulnerabilities per state Thomas Perale via buildroot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260311140457.140041-5-thomas.perale@mind.be \
--to=buildroot@buildroot.org \
--cc=thomas.perale@mind.be \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox