Message ID | 20250505160327.3422096-2-fiona.klute@gmx.de |
---|---|
State | New |
Headers | show |
Series | Add external tree information to CycloneDX SBOM | expand |
Hello, On 5/05/25 18:03, Fiona Klute via buildroot wrote: > From: "Fiona Klute (WIWA)" <fiona.klute@gmx.de> > > External tree information may be important for interpreting SBOM > information, especially if the SBOM lists packages (components) that > do not exist in the listed version of Buildroot itself. > > Signed-off-by: Fiona Klute (WIWA) <fiona.klute@gmx.de> Tested-by: Titouan Christophe <titouan.christophe@mind.be> > --- > utils/generate-cyclonedx | 57 ++++++++++++++++++++++++++++++++++++++++ > 1 file changed, 57 insertions(+) > > diff --git a/utils/generate-cyclonedx b/utils/generate-cyclonedx > index 46f68ac63d..cbb7ef630e 100755 > --- a/utils/generate-cyclonedx > +++ b/utils/generate-cyclonedx > @@ -10,6 +10,7 @@ > > import argparse > import bz2 > +from dataclasses import dataclass > import gzip > import json > import os > @@ -50,6 +51,49 @@ except json.JSONDecodeError: > print(f"Failed to load the SPDX licenses file: {SPDX_SCHEMA_PATH}", file=sys.stderr) > > > +@dataclass > +class ExternalTree: > + name: str > + path: Path > + version: str > + description: str > + > + @classmethod > + def parse_vars(cls, variables: dict[str, dict[str, str]]) -> list['ExternalTree']: > + """Load external trees from JSON data as generated by `make > + show-vars VARS="BR2_EXTERNAL%"`. > + > + Args: > + variables: dict created by loading the JSON data > + > + Returns: > + list of parsed ExternalTree objects > + """ > + return [ > + cls( > + ext, > + Path(variables[f"BR2_EXTERNAL_{ext}_PATH"]["expanded"]), > + variables[f"BR2_EXTERNAL_{ext}_VERSION"]["expanded"], > + variables[f"BR2_EXTERNAL_{ext}_DESC"]["expanded"], > + ) > + for ext in variables["BR2_EXTERNAL_NAMES"]["expanded"].split() > + ] > + > + @property > + def bom_ref(self) -> str: > + return f"buildroot-external:{self.name}" > + > + def as_component(self) -> dict[str, str]: > + """Format self as a component dict ready to be included in > + CycloneDX JSON output.""" > + return { > + "bom-ref": self.bom_ref, > + "name": self.name, > + "type": "firmware", > + "version": self.version, > + } > + > + > def split_top_level_comma(subj): > """Split a string at comma's, but do not split at comma's in between parentheses. > > @@ -277,6 +321,10 @@ def main(): > default=(None if sys.stdin.isatty() else sys.stdin)) > parser.add_argument("-o", "--out-file", nargs="?", type=argparse.FileType("w"), > default=sys.stdout) > + parser.add_argument("-e", "--external-trees", nargs="?", type=argparse.FileType("r"), > + default=None, Nitpick: default=None is not needed here (https://docs.python.org/3/library/argparse.html#default) > + help="Load external trees to list in SBOM from this JSON file, " > + "can be created by running: make show-vars VARS=\"BR2_EXTERNAL%\"") > parser.add_argument("--virtual", default=False, action='store_true', > help="This option includes virtual packages to the CycloneDX output") > > @@ -293,6 +341,11 @@ def main(): > 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)} > > + if args.external_trees is not None: > + external_trees = ExternalTree.parse_vars(json.load(args.external_trees)) > + else: > + external_trees = [] > + > cyclonedx_dict = { > "bomFormat": "CycloneDX", > "$schema": f"http://cyclonedx.org/schema/bom-{CYCLONEDX_VERSION}.schema.json", > @@ -312,6 +365,10 @@ def main(): > "name": "buildroot", > "type": "firmware", > "version": f"{BR2_VERSION_FULL}", > + **( > + {"components": [ext.as_component() for ext in external_trees]} > + if external_trees else {} > + ) > }, > }, > }
diff --git a/utils/generate-cyclonedx b/utils/generate-cyclonedx index 46f68ac63d..cbb7ef630e 100755 --- a/utils/generate-cyclonedx +++ b/utils/generate-cyclonedx @@ -10,6 +10,7 @@ import argparse import bz2 +from dataclasses import dataclass import gzip import json import os @@ -50,6 +51,49 @@ except json.JSONDecodeError: print(f"Failed to load the SPDX licenses file: {SPDX_SCHEMA_PATH}", file=sys.stderr) +@dataclass +class ExternalTree: + name: str + path: Path + version: str + description: str + + @classmethod + def parse_vars(cls, variables: dict[str, dict[str, str]]) -> list['ExternalTree']: + """Load external trees from JSON data as generated by `make + show-vars VARS="BR2_EXTERNAL%"`. + + Args: + variables: dict created by loading the JSON data + + Returns: + list of parsed ExternalTree objects + """ + return [ + cls( + ext, + Path(variables[f"BR2_EXTERNAL_{ext}_PATH"]["expanded"]), + variables[f"BR2_EXTERNAL_{ext}_VERSION"]["expanded"], + variables[f"BR2_EXTERNAL_{ext}_DESC"]["expanded"], + ) + for ext in variables["BR2_EXTERNAL_NAMES"]["expanded"].split() + ] + + @property + def bom_ref(self) -> str: + return f"buildroot-external:{self.name}" + + def as_component(self) -> dict[str, str]: + """Format self as a component dict ready to be included in + CycloneDX JSON output.""" + return { + "bom-ref": self.bom_ref, + "name": self.name, + "type": "firmware", + "version": self.version, + } + + def split_top_level_comma(subj): """Split a string at comma's, but do not split at comma's in between parentheses. @@ -277,6 +321,10 @@ def main(): default=(None if sys.stdin.isatty() else sys.stdin)) parser.add_argument("-o", "--out-file", nargs="?", type=argparse.FileType("w"), default=sys.stdout) + parser.add_argument("-e", "--external-trees", nargs="?", type=argparse.FileType("r"), + default=None, + help="Load external trees to list in SBOM from this JSON file, " + "can be created by running: make show-vars VARS=\"BR2_EXTERNAL%\"") parser.add_argument("--virtual", default=False, action='store_true', help="This option includes virtual packages to the CycloneDX output") @@ -293,6 +341,11 @@ def main(): 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)} + if args.external_trees is not None: + external_trees = ExternalTree.parse_vars(json.load(args.external_trees)) + else: + external_trees = [] + cyclonedx_dict = { "bomFormat": "CycloneDX", "$schema": f"http://cyclonedx.org/schema/bom-{CYCLONEDX_VERSION}.schema.json", @@ -312,6 +365,10 @@ def main(): "name": "buildroot", "type": "firmware", "version": f"{BR2_VERSION_FULL}", + **( + {"components": [ext.as_component() for ext in external_trees]} + if external_trees else {} + ) }, }, }