diff mbox series

[1/3] utils/generate-cyclonedx: support including external tree information

Message ID 20250505160327.3422096-2-fiona.klute@gmx.de
State New
Headers show
Series Add external tree information to CycloneDX SBOM | expand

Commit Message

Fiona Klute May 5, 2025, 4:03 p.m. UTC
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>
---
 utils/generate-cyclonedx | 57 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

Comments

Titouan Christophe May 16, 2025, 4:51 p.m. UTC | #1
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 mbox series

Patch

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 {}
+                )
             },
         },
     }