From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mailgw02.zimbra-vnc.de (mailgw02.zimbra-vnc.de [148.251.102.236]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F1FC24657CE; Thu, 7 May 2026 17:39:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=148.251.102.236 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778175586; cv=none; b=AnAP3dqGarfzCdse42DlVRmfUdUYum1sBdTvMVlD4AQzufIE3D+aPpSyK3xjVdsrC4Jbh+fFfm2WLf8bed6DUzshvDkLnUdqfz30sgK3srQA1xTo1cCVOmtRzznf4WAKWoshHWuNvPK40bkK0vcRfbQ13zqMy8vfLotLKgygJ3Y= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778175586; c=relaxed/simple; bh=QamavOIHpABDwUT+OqozP7cWoan23ndKVW6F5vdmrZA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=sF1RqQEMugBA1TmMmyrngOY6YlBc5vtuMw6mny7d8dy5GlLrt7GofwW8hVa+nQxZBcDs83PbkTVYjsAcZLEYAVjcN/GANUY3H0LWDSTPucDC11uMaQp/I5w22+ciSrbqtv+BFN7mSz5OtT1n+KyZRsLcbzpxu5CbP+poxEFALWo= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=tngtech.com; spf=pass smtp.mailfrom=tngtech.com; dkim=pass (2048-bit key) header.d=tngtech.com header.i=@tngtech.com header.b=jwSdzE3x; arc=none smtp.client-ip=148.251.102.236 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=tngtech.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=tngtech.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=tngtech.com header.i=@tngtech.com header.b="jwSdzE3x" Received: from zmproxy.tng.vnc.biz (zimbra-vnc.tngtech.com [35.234.71.156]) by mailgw02.zimbra-vnc.de (Postfix) with ESMTPS id 8D104200CE; Thu, 7 May 2026 19:39:42 +0200 (CEST) Received: from localhost (localhost [127.0.0.1]) by zmproxy.tng.vnc.biz (Postfix) with ESMTP id 639741FB1D0; Thu, 7 May 2026 19:39:42 +0200 (CEST) Received: from zmproxy.tng.vnc.biz ([127.0.0.1]) by localhost (zmproxy.tng.vnc.biz [127.0.0.1]) (amavis, port 10032) with ESMTP id Uny2sXmI7-qU; Thu, 7 May 2026 19:39:41 +0200 (CEST) Received: from localhost (localhost [127.0.0.1]) by zmproxy.tng.vnc.biz (Postfix) with ESMTP id 6E6201FB0E6; Thu, 7 May 2026 19:39:41 +0200 (CEST) DKIM-Filter: OpenDKIM Filter v2.10.3 zmproxy.tng.vnc.biz 6E6201FB0E6 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tngtech.com; s=B14491C6-869D-11EB-BB6C-8DD33D883B31; t=1778175581; bh=AHFuJ3/Gyn1ra4f4OyOv7o2uxqjc/Y8VP5ndWf2OSn0=; h=From:To:Subject:Date:Message-ID:MIME-Version; b=jwSdzE3xzOZIky8TY3P4Qiq7763I+2wiR7VXE2bv7Mj7pGNfDg2hIxwhQqGmU/s64 BooRi2GPf1KHDkdM197JkmqkPqtHp23a6CPujP8DaMTo4F1q8Q9vk5o0JlaoVvVbA8 SQTPf9W9pPtyXFNdu9TCauHQkgX9HcPl957CXPiFK6rZ77Dast+xb6THCmp8sCbpr2 /B7HEyVr07Nll6DKvPMAJRs9olCmAqhh8y6EWkFUQqtpJ+y3cvB3HZprwp9s/UtH5O TjLkj8s2Vp+awMu5qfNmQD5l1FNJdOnjMYtkY6llCV3NUwfTIk6RFtZqOmyqPqgqME zVFL1Ebq5Qhog== X-Virus-Scanned: amavis at zmproxy.tng.vnc.biz Received: from zmproxy.tng.vnc.biz ([127.0.0.1]) by localhost (zmproxy.tng.vnc.biz [127.0.0.1]) (amavis, port 10026) with ESMTP id j5HGrfxK45OZ; Thu, 7 May 2026 19:39:41 +0200 (CEST) Received: from luis-Precision-5480.. (ipservice-092-209-239-167.092.209.pools.vodafone-ip.de [92.209.239.167]) by zmproxy.tng.vnc.biz (Postfix) with ESMTPSA id 14DA71FB1D6; Thu, 7 May 2026 19:39:41 +0200 (CEST) From: Luis To: nathan@kernel.org, nsc@kernel.org Cc: linux-kbuild@vger.kernel.org, linux-kernel@vger.kernel.org, akpm@linux-foundation.org, gregkh@linuxfoundation.org, kstewart@linuxfoundation.org, maximilian.huber@tngtech.com, Luis Augenstein Subject: [PATCH v6 13/15] scripts/sbom: add SPDX build graph Date: Thu, 7 May 2026 19:38:25 +0200 Message-ID: <20260507173827.70949-14-luis.augenstein@tngtech.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260507173827.70949-1-luis.augenstein@tngtech.com> References: <20260507173827.70949-1-luis.augenstein@tngtech.com> Precedence: bulk X-Mailing-List: linux-kbuild@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable From: Luis Augenstein Implement the SPDX build graph to describe the relationships between source files in the source SBOM and output files in the output SBOM. 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 --- .../sbom/sbom/spdx_graph/build_spdx_graphs.py | 17 + .../sbom/sbom/spdx_graph/spdx_build_graph.py | 319 ++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 scripts/sbom/sbom/spdx_graph/spdx_build_graph.py diff --git a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py b/scripts/= sbom/sbom/spdx_graph/build_spdx_graphs.py index f2567d44960..ee24e9eaf60 100644 --- a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py +++ b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py @@ -4,6 +4,7 @@ from datetime import datetime from typing import Protocol =20 +import logging from sbom.config import KernelSpdxDocumentKind from sbom.cmd_graph import CmdGraph from sbom.path_utils import PathStr @@ -11,6 +12,7 @@ from sbom.spdx_graph.kernel_file import KernelFileColle= ction from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorC= ollection from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements from sbom.spdx_graph.spdx_source_graph import SpdxSourceGraph +from sbom.spdx_graph.spdx_build_graph import SpdxBuildGraph from sbom.spdx_graph.spdx_output_graph import SpdxOutputGraph =20 =20 @@ -62,5 +64,20 @@ def build_spdx_graphs( shared_elements=3Dshared_elements, spdx_id_generators=3Dspdx_id_generators, ) + else: + logging.info( + "Skipped creating a dedicated source SBOM because source fil= es cannot be " + "reliably classified when the source and object trees are id= entical. " + "Added source files to the build SBOM instead." + ) + + build_graph =3D SpdxBuildGraph.create( + cmd_graph, + kernel_files, + shared_elements, + output_graph.high_level_build_element, + spdx_id_generators, + ) + spdx_graphs[KernelSpdxDocumentKind.BUILD] =3D build_graph =20 return spdx_graphs diff --git a/scripts/sbom/sbom/spdx_graph/spdx_build_graph.py b/scripts/s= bom/sbom/spdx_graph/spdx_build_graph.py new file mode 100644 index 00000000000..70b1a429806 --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/spdx_build_graph.py @@ -0,0 +1,319 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +from typing import Mapping +from sbom.cmd_graph import CmdGraph +from sbom.path_utils import PathStr +from sbom.spdx import SpdxIdGenerator +from sbom.spdx.build import Build +from sbom.spdx.core import ExternalMap, NamespaceMap, Relationship, Spdx= Document +from sbom.spdx.software import File, Sbom +from sbom.spdx_graph.kernel_file import KernelFileCollection +from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements +from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorC= ollection +from sbom.spdx_graph.spdx_source_graph import source_file_license_elemen= ts + + +@dataclass +class SpdxBuildGraph(SpdxGraph): + """SPDX graph representing build dependencies connecting source file= s and + distributable output files""" + + @classmethod + def create( + cls, + cmd_graph: CmdGraph, + kernel_files: KernelFileCollection, + shared_elements: SharedSpdxElements, + high_level_build_element: Build, + spdx_id_generators: SpdxIdGeneratorCollection, + ) -> "SpdxBuildGraph": + if len(kernel_files.source) > 0: + return _create_spdx_build_graph( + cmd_graph, + kernel_files, + shared_elements, + high_level_build_element, + spdx_id_generators, + ) + else: + return _create_spdx_build_graph_with_mixed_sources( + cmd_graph, + kernel_files, + shared_elements, + high_level_build_element, + spdx_id_generators, + ) + + +def _create_spdx_build_graph( + cmd_graph: CmdGraph, + kernel_files: KernelFileCollection, + shared_elements: SharedSpdxElements, + high_level_build_element: Build, + spdx_id_generators: SpdxIdGeneratorCollection, +) -> SpdxBuildGraph: + """ + Creates an SPDX build graph where source and output files are refere= nced + from external documents. + + Args: + cmd_graph: The dependency graph of a kernel build. + kernel_files: Collection of categorized kernel files involved in= the build. + shared_elements: SPDX elements shared across multiple documents. + high_level_build_element: The high-level Build element reference= d by the build graph. + spdx_id_generators: Collection of generators for SPDX element ID= s. + + Returns: + SpdxBuildGraph: The SPDX build graph connecting source files and= distributable output files. + """ + # SpdxDocument + build_spdx_document =3D SpdxDocument( + spdxId=3Dspdx_id_generators.build.generate(), + profileConformance=3D["core", "software", "build"], + namespaceMap=3D[ + NamespaceMap(prefix=3Dgenerator.prefix, namespace=3Dgenerato= r.namespace) + for generator in [ + spdx_id_generators.build, + spdx_id_generators.source, + spdx_id_generators.output, + spdx_id_generators.base, + ] + if generator.prefix is not None + ], + ) + + # Sbom + build_sbom =3D Sbom( + spdxId=3Dspdx_id_generators.build.generate(), + software_sbomType=3D["build"], + ) + + # Src and object tree elements + obj_tree_element =3D File( + spdxId=3Dspdx_id_generators.build.generate(), + name=3D"$(obj_tree)", + software_fileKind=3D"directory", + ) + obj_tree_contains_relationship =3D Relationship( + spdxId=3Dspdx_id_generators.build.generate(), + relationshipType=3D"contains", + from_=3Dobj_tree_element, + to=3D[], + ) + + # File elements + build_file_elements =3D [file.spdx_file_element for file in kernel_f= iles.build.values()] + file_relationships =3D _file_relationships( + cmd_graph=3Dcmd_graph, + file_elements=3D{key: file.spdx_file_element for key, file in ke= rnel_files.to_dict().items()}, + high_level_build_element=3Dhigh_level_build_element, + spdx_id_generator=3Dspdx_id_generators.build, + ) + + # Update relationships + build_spdx_document.rootElement =3D [build_sbom] + + build_spdx_document.import_ =3D [ + *( + ExternalMap(externalSpdxId=3Dfile.spdx_file_element.spdxId) + for file in (*kernel_files.source.values(), *kernel_files.ex= ternal.values()) + ), + ExternalMap(externalSpdxId=3Dhigh_level_build_element.spdxId), + *(ExternalMap(externalSpdxId=3Dfile.spdx_file_element.spdxId) fo= r file in kernel_files.output.values()), + ] + + build_sbom.rootElement =3D [obj_tree_element] + build_sbom.element =3D [ + obj_tree_element, + obj_tree_contains_relationship, + *build_file_elements, + *file_relationships, + ] + + obj_tree_contains_relationship.to =3D [ + *build_file_elements, + *(file.spdx_file_element for file in kernel_files.output.values(= )), + ] + + # create Spdx graphs + build_graph =3D SpdxBuildGraph( + build_spdx_document, + shared_elements.agent, + shared_elements.creation_info, + build_sbom, + ) + return build_graph + + +def _create_spdx_build_graph_with_mixed_sources( + cmd_graph: CmdGraph, + kernel_files: KernelFileCollection, + shared_elements: SharedSpdxElements, + high_level_build_element: Build, + spdx_id_generators: SpdxIdGeneratorCollection, +) -> SpdxBuildGraph: + """ + Creates an SPDX build graph where only output files are referenced f= rom + an external document. Source files are included directly in the buil= d graph. + + Args: + cmd_graph: The dependency graph of a kernel build. + kernel_files: Collection of categorized kernel files involved in= the build. + shared_elements: SPDX elements shared across multiple documents. + high_level_build_element: The high-level Build element reference= d by the build graph. + spdx_id_generators: Collection of generators for SPDX element ID= s. + + Returns: + SpdxBuildGraph: The SPDX build graph connecting source files and= distributable output files. + """ + # SpdxDocument + build_spdx_document =3D SpdxDocument( + spdxId=3Dspdx_id_generators.build.generate(), + profileConformance=3D["core", "software", "build"], + namespaceMap=3D[ + NamespaceMap(prefix=3Dgenerator.prefix, namespace=3Dgenerato= r.namespace) + for generator in [ + spdx_id_generators.build, + spdx_id_generators.output, + spdx_id_generators.base, + ] + if generator.prefix is not None + ], + ) + + # Sbom + build_sbom =3D Sbom( + spdxId=3Dspdx_id_generators.build.generate(), + software_sbomType=3D["build"], + ) + + # File elements + build_file_elements =3D [file.spdx_file_element for file in kernel_f= iles.build.values()] + external_file_elements =3D [file.spdx_file_element for file in kerne= l_files.external.values()] + file_relationships =3D _file_relationships( + cmd_graph=3Dcmd_graph, + file_elements=3D{key: file.spdx_file_element for key, file in ke= rnel_files.to_dict().items()}, + high_level_build_element=3Dhigh_level_build_element, + spdx_id_generator=3Dspdx_id_generators.build, + ) + + # Source file license elements + source_file_license_identifiers, source_file_license_relationships =3D= source_file_license_elements( + list(kernel_files.build.values()), spdx_id_generators.build + ) + + # Update relationships + build_spdx_document.rootElement =3D [build_sbom] + root_file_elements =3D [file.spdx_file_element for file in kernel_fi= les.output.values()] + build_spdx_document.import_ =3D [ + ExternalMap(externalSpdxId=3Dhigh_level_build_element.spdxId), + *(ExternalMap(externalSpdxId=3Dfile.spdxId) for file in root_fil= e_elements), + ] + + build_sbom.rootElement =3D [*root_file_elements] + build_sbom.element =3D [ + *build_file_elements, + *external_file_elements, + *source_file_license_identifiers, + *source_file_license_relationships, + *file_relationships, + ] + + build_graph =3D SpdxBuildGraph( + build_spdx_document, + shared_elements.agent, + shared_elements.creation_info, + build_sbom, + ) + return build_graph + + +def _file_relationships( + cmd_graph: CmdGraph, + file_elements: Mapping[PathStr, File], + high_level_build_element: Build, + spdx_id_generator: SpdxIdGenerator, +) -> list[Build | Relationship]: + """ + Construct SPDX Build and Relationship elements representing dependen= cy + relationships in the cmd graph. + + Args: + cmd_graph: The dependency graph of a kernel build. + file_elements: Mapping of filesystem paths (PathStr) to their + corresponding SPDX File elements. + high_level_build_element: The SPDX Build element representing th= e overall build process/root. + spdx_id_generator: Generator for unique SPDX IDs. + + Returns: + list[Build | Relationship]: List of SPDX Build and Relationship = elements + """ + high_level_build_ancestorOf_relationship =3D Relationship( + spdxId=3Dspdx_id_generator.generate(), + relationshipType=3D"ancestorOf", + from_=3Dhigh_level_build_element, + completeness=3D"complete", + to=3D[], + ) + + # Create a relationship between each node (output file) + # and its children (input files) + build_and_relationship_elements: list[Build | Relationship] =3D [hig= h_level_build_ancestorOf_relationship] + for node in cmd_graph: + if next(node.children, None) is None: + continue + + # .cmd file dependencies + if node.cmd_file is not None: + build_element =3D Build( + spdxId=3Dspdx_id_generator.generate(), + build_buildType=3Dhigh_level_build_element.build_buildTy= pe, + build_buildId=3Dhigh_level_build_element.build_buildId, + comment=3Dnode.cmd_file.savedcmd, + ) + hasInput_relationship =3D Relationship( + spdxId=3Dspdx_id_generator.generate(), + relationshipType=3D"hasInput", + from_=3Dbuild_element, + to=3D[file_elements[child_node.absolute_path] for child_= node in node.children], + ) + hasOutput_relationship =3D Relationship( + spdxId=3Dspdx_id_generator.generate(), + relationshipType=3D"hasOutput", + from_=3Dbuild_element, + to=3D[file_elements[node.absolute_path]], + ) + build_and_relationship_elements +=3D [ + build_element, + hasInput_relationship, + hasOutput_relationship, + ] + high_level_build_ancestorOf_relationship.to.append(build_ele= ment) + + # incbin dependencies + if len(node.incbin_dependencies) > 0: + incbin_dependsOn_relationship =3D Relationship( + spdxId=3Dspdx_id_generator.generate(), + relationshipType=3D"dependsOn", + comment=3D"\n".join([incbin_dependency.full_statement fo= r incbin_dependency in node.incbin_dependencies]), + from_=3Dfile_elements[node.absolute_path], + to=3D[ + file_elements[incbin_dependency.node.absolute_path] + for incbin_dependency in node.incbin_dependencies + ], + ) + build_and_relationship_elements.append(incbin_dependsOn_rela= tionship) + + # hardcoded dependencies + if len(node.hardcoded_dependencies) > 0: + hardcoded_dependency_relationship =3D Relationship( + spdxId=3Dspdx_id_generator.generate(), + relationshipType=3D"dependsOn", + from_=3Dfile_elements[node.absolute_path], + to=3D[file_elements[n.absolute_path] for n in node.hardc= oded_dependencies], + ) + build_and_relationship_elements.append(hardcoded_dependency_= relationship) + + return build_and_relationship_elements --=20 2.43.0