From: Luis Augenstein Date: Mon, 18 May 2026 06:20:58 +0000 (+0200) Subject: scripts/sbom: add SPDX output graph X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=b01912114e2c1b378287fcdd013bb9a894d1879e;p=thirdparty%2Fkernel%2Flinux.git scripts/sbom: add SPDX output graph Implement the SPDX output graph which contains the distributable build outputs and high level metadata about the build. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- diff --git a/Makefile b/Makefile index 2443d4c82454..e02d2a614c53 100644 --- a/Makefile +++ b/Makefile @@ -2213,7 +2213,10 @@ quiet_cmd_sbom = GEN $(sbom_targets) --obj-tree $(abspath $(objtree)) \ --roots-file "$(tmp-target)" \ --output-directory $(abspath $(objtree)) \ - --generate-spdx; + --generate-spdx \ + --package-license "GPL-2.0 WITH Linux-syscall-note" \ + --package-version "$(KERNELVERSION)" \ + --write-output-on-error; PHONY += sbom sbom: $(notdir $(KBUILD_IMAGE)) include/generated/autoconf.h $(if $(CONFIG_MODULES),modules modules.order) $(call cmd,sbom) diff --git a/scripts/sbom/sbom/config.py b/scripts/sbom/sbom/config.py index b1dd30790f5b..6811f782943e 100644 --- a/scripts/sbom/sbom/config.py +++ b/scripts/sbom/sbom/config.py @@ -59,6 +59,21 @@ class KernelSbomConfig: spdxId_prefix: str """Prefix to use for all SPDX element IDs.""" + build_type: str + """SPDX buildType property to use for all Build elements.""" + + build_id: str | None + """SPDX buildId property to use for all Build elements.""" + + package_license: str + """License expression applied to all SPDX Packages.""" + + package_version: str | None + """Version string applied to all SPDX Packages.""" + + package_copyright_text: str | None + """Copyright text applied to all SPDX Packages.""" + prettify_json: bool """Whether to pretty-print generated SPDX JSON documents.""" @@ -154,6 +169,40 @@ def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, Any]: default="urn:spdx.dev:", help="The prefix to use for all spdxId properties. (default: urn:spdx.dev:)", ) + spdx_group.add_argument( + "--build-type", + default="urn:spdx.dev:Kbuild", + help="The SPDX buildType property to use for all Build elements. (default: urn:spdx.dev:Kbuild)", + ) + spdx_group.add_argument( + "--build-id", + default=None, + help="The SPDX buildId property to use for all Build elements.\n" + "If not provided the spdxId of the high level Build element is used as the buildId. (default: None)", + ) + spdx_group.add_argument( + "--package-license", + default="NOASSERTION", + help=( + "The SPDX licenseExpression property to use for the LicenseExpression " + "linked to all SPDX Package elements. (default: NOASSERTION)" + ), + ) + spdx_group.add_argument( + "--package-version", + default=None, + help="The SPDX packageVersion property to use for all SPDX Package elements. (default: None)", + ) + spdx_group.add_argument( + "--package-copyright-text", + default=None, + help=( + "The SPDX copyrightText property to use for all SPDX Package elements.\n" + "If not specified, and if a COPYING file exists in the source tree,\n" + "the package-copyright-text is set to the content of this file. " + "(default: None)" + ), + ) spdx_group.add_argument( "--prettify-json", action="store_true", @@ -204,6 +253,16 @@ def get_config() -> KernelSbomConfig: tz=timezone.utc, ) spdxId_prefix = args["spdxId_prefix"] + build_type = args["build_type"] + build_id = args["build_id"] + package_license = args["package_license"] + package_version = args["package_version"] if args["package_version"] is not None else None + package_copyright_text: str | None = None + if args["package_copyright_text"] is not None: + package_copyright_text = args["package_copyright_text"] + elif os.path.isfile(copying_path := os.path.join(src_tree, "COPYING")): + with open(copying_path, "r", encoding="utf-8") as f: + package_copyright_text = f.read() prettify_json = args["prettify_json"] # Hardcoded config @@ -228,6 +287,11 @@ def get_config() -> KernelSbomConfig: write_output_on_error=write_output_on_error, created=created, spdxId_prefix=spdxId_prefix, + build_type=build_type, + build_id=build_id, + package_license=package_license, + package_version=package_version, + package_copyright_text=package_copyright_text, prettify_json=prettify_json, ) diff --git a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py index 0f95f99d560a..2af0fbe6cdbe 100644 --- a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py +++ b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py @@ -10,12 +10,18 @@ from sbom.path_utils import PathStr from sbom.spdx_graph.kernel_file import KernelFileCollection from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements +from sbom.spdx_graph.spdx_output_graph import SpdxOutputGraph class SpdxGraphConfig(Protocol): obj_tree: PathStr src_tree: PathStr created: datetime + build_type: str + build_id: str | None + package_license: str + package_version: str | None + package_copyright_text: str | None def build_spdx_graphs( @@ -38,4 +44,14 @@ def build_spdx_graphs( """ shared_elements = SharedSpdxElements.create(spdx_id_generators.base, config.created) kernel_files = KernelFileCollection.create(cmd_graph, config.obj_tree, config.src_tree, spdx_id_generators) - return {} + output_graph = SpdxOutputGraph.create( + root_files=list(kernel_files.output.values()), + shared_elements=shared_elements, + spdx_id_generators=spdx_id_generators, + config=config, + ) + spdx_graphs: dict[KernelSpdxDocumentKind, SpdxGraph] = { + KernelSpdxDocumentKind.OUTPUT: output_graph, + } + + return spdx_graphs diff --git a/scripts/sbom/sbom/spdx_graph/spdx_output_graph.py b/scripts/sbom/sbom/spdx_graph/spdx_output_graph.py new file mode 100644 index 000000000000..ff9b2c31fb04 --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/spdx_output_graph.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +import os +from typing import Protocol +from sbom.environment import Environment +from sbom.path_utils import PathStr +from sbom.spdx.build import Build +from sbom.spdx.core import DictionaryEntry, NamespaceMap, Relationship, SpdxDocument +from sbom.spdx.simplelicensing import LicenseExpression +from sbom.spdx.software import File, Package, Sbom +from sbom.spdx.spdxId import SpdxIdGenerator +from sbom.spdx_graph.kernel_file import KernelFile +from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements +from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection + + +class SpdxOutputGraphConfig(Protocol): + obj_tree: PathStr + src_tree: PathStr + build_type: str + build_id: str | None + package_license: str + package_version: str | None + package_copyright_text: str | None + + +@dataclass +class SpdxOutputGraph(SpdxGraph): + """SPDX graph representing distributable output files""" + + high_level_build_element: Build + + @classmethod + def create( + cls, + root_files: list[KernelFile], + shared_elements: SharedSpdxElements, + spdx_id_generators: SpdxIdGeneratorCollection, + config: SpdxOutputGraphConfig, + ) -> "SpdxOutputGraph": + """ + Args: + root_files: List of distributable output files which act as roots + of the dependency graph. + shared_elements: Shared SPDX elements used across multiple documents. + spdx_id_generators: Collection of SPDX ID generators. + config: Configuration options. + + Returns: + SpdxOutputGraph: The SPDX output graph. + """ + # SpdxDocument + spdx_document = SpdxDocument( + spdxId=spdx_id_generators.output.generate(), + profileConformance=["core", "software", "build", "simpleLicensing"], + namespaceMap=[ + NamespaceMap(prefix=generator.prefix, namespace=generator.namespace) + for generator in [spdx_id_generators.output, spdx_id_generators.base] + if generator.prefix is not None + ], + ) + + # Sbom + sbom = Sbom( + spdxId=spdx_id_generators.output.generate(), + software_sbomType=["build"], + ) + + # High-level Build elements + config_source_element = KernelFile.create( + absolute_path=os.path.join(config.obj_tree, ".config"), + obj_tree=config.obj_tree, + src_tree=config.src_tree, + spdx_id_generators=spdx_id_generators, + is_output=True, + ).spdx_file_element + high_level_build_element, high_level_build_element_hasOutput_relationship = _high_level_build_elements( + config.build_type, + config.build_id, + config_source_element, + spdx_id_generators.output, + ) + + # Root file elements + root_file_elements: list[File] = [file.spdx_file_element for file in root_files] + + # Package elements + package_elements = [ + Package( + spdxId=spdx_id_generators.output.generate(), + name=_get_package_name(file.name), + software_packageVersion=config.package_version, + software_copyrightText=config.package_copyright_text, + comment=f"Architecture={arch}" if (arch := Environment.ARCH() or Environment.SRCARCH()) else None, + software_primaryPurpose=file.software_primaryPurpose, + ) + for file in root_file_elements + ] + package_hasDistributionArtifact_file_relationships = [ + Relationship( + spdxId=spdx_id_generators.output.generate(), + relationshipType="hasDistributionArtifact", + from_=package, + to=[file], + ) + for package, file in zip(package_elements, root_file_elements) + ] + package_license_expression = LicenseExpression( + spdxId=spdx_id_generators.output.generate(), + simplelicensing_licenseExpression=config.package_license, + ) + package_hasDeclaredLicense_relationships = [ + Relationship( + spdxId=spdx_id_generators.output.generate(), + relationshipType="hasDeclaredLicense", + from_=package, + to=[package_license_expression], + ) + for package in package_elements + ] + + # Update relationships + spdx_document.rootElement = [sbom] + + sbom.rootElement = [*package_elements] + sbom.element = [ + config_source_element, + high_level_build_element, + high_level_build_element_hasOutput_relationship, + *root_file_elements, + *package_elements, + *package_hasDistributionArtifact_file_relationships, + package_license_expression, + *package_hasDeclaredLicense_relationships, + ] + + high_level_build_element_hasOutput_relationship.to = [*root_file_elements] + + output_graph = SpdxOutputGraph( + spdx_document, + shared_elements.agent, + shared_elements.creation_info, + sbom, + high_level_build_element, + ) + return output_graph + + +def _get_package_name(filename: str) -> str: + """ + Generates a SPDX package name from a filename. + Kernel images (bzImage, Image) get a descriptive name, others use the basename of the file. + """ + KERNEL_FILENAMES = ["bzImage", "Image"] + basename = os.path.basename(filename) + return f"Linux Kernel ({basename})" if basename in KERNEL_FILENAMES else basename + + +def _high_level_build_elements( + build_type: str, + build_id: str | None, + config_source_element: File, + spdx_id_generator: SpdxIdGenerator, +) -> tuple[Build, Relationship]: + build_spdxId = spdx_id_generator.generate() + high_level_build_element = Build( + spdxId=build_spdxId, + build_buildType=build_type, + build_buildId=build_id if build_id is not None else build_spdxId, + build_environment=[ + DictionaryEntry(key=key, value=value) + for key, value in Environment.KERNEL_BUILD_VARIABLES().items() + if value + ], + build_configSourceUri=[config_source_element.spdxId], + build_configSourceDigest=config_source_element.verifiedUsing, + ) + + high_level_build_element_hasOutput_relationship = Relationship( + spdxId=spdx_id_generator.generate(), + relationshipType="hasOutput", + from_=high_level_build_element, + to=[], + ) + return high_level_build_element, high_level_build_element_hasOutput_relationship