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 <maximilian.huber@tngtech.com>
Signed-off-by: Maximilian Huber <maximilian.huber@tngtech.com>
Signed-off-by: Luis Augenstein <luis.augenstein@tngtech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Luis Augenstein 2026-05-18 08:20:58 +02:00 committed by Greg Kroah-Hartman
parent ef0675c871
commit b01912114e
4 changed files with 272 additions and 2 deletions

View File

@ -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)

View File

@ -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,
)

View File

@ -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

View File

@ -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