1e72b635cSLuis Augenstein#!/usr/bin/env python3 2e72b635cSLuis Augenstein# SPDX-License-Identifier: GPL-2.0-only OR MIT 3e72b635cSLuis Augenstein# Copyright (C) 2025 TNG Technology Consulting GmbH 4e72b635cSLuis Augenstein 5e72b635cSLuis Augenstein""" 6e72b635cSLuis AugensteinCompute software bill of materials in SPDX format describing a kernel build. 7e72b635cSLuis Augenstein""" 8e72b635cSLuis Augenstein 9*a68a29a1SLuis Augensteinimport json 103fd79200SLuis Augensteinimport logging 119c16c1eaSLuis Augensteinimport os 123fd79200SLuis Augensteinimport sys 139c16c1eaSLuis Augensteinimport time 14*a68a29a1SLuis Augensteinimport uuid 153fd79200SLuis Augensteinimport sbom.sbom_logging as sbom_logging 163fd79200SLuis Augensteinfrom sbom.config import get_config 179c16c1eaSLuis Augensteinfrom sbom.path_utils import is_relative_to 18*a68a29a1SLuis Augensteinfrom sbom.spdx import JsonLdSpdxDocument, SpdxIdGenerator 19*a68a29a1SLuis Augensteinfrom sbom.spdx.core import CreationInfo, SpdxDocument 20*a68a29a1SLuis Augensteinfrom sbom.spdx_graph import SpdxIdGeneratorCollection, build_spdx_graphs 219c16c1eaSLuis Augensteinfrom sbom.cmd_graph import CmdGraph 223fd79200SLuis Augenstein 233fd79200SLuis Augenstein 243fd79200SLuis Augensteindef _exit_with_summary(write_output_on_error: bool = False) -> None: 253fd79200SLuis Augenstein warning_summary = sbom_logging.summarize_warnings() 263fd79200SLuis Augenstein error_summary = sbom_logging.summarize_errors() 273fd79200SLuis Augenstein if warning_summary: 283fd79200SLuis Augenstein logging.warning(warning_summary) 293fd79200SLuis Augenstein if error_summary: 303fd79200SLuis Augenstein logging.error(error_summary) 319c16c1eaSLuis Augenstein if not write_output_on_error: 329c16c1eaSLuis Augenstein logging.info( 339c16c1eaSLuis Augenstein "Use --write-output-on-error to generate output documents even when errors occur. " 349c16c1eaSLuis Augenstein "Note that in this case the generated documents may be incomplete." 359c16c1eaSLuis Augenstein ) 363fd79200SLuis Augenstein sys.exit(1) 373fd79200SLuis Augenstein 38e72b635cSLuis Augenstein 39e72b635cSLuis Augensteindef main(): 403fd79200SLuis Augenstein # Read config 413fd79200SLuis Augenstein config = get_config() 423fd79200SLuis Augenstein 433fd79200SLuis Augenstein # Configure logging 443fd79200SLuis Augenstein logging.basicConfig( 453fd79200SLuis Augenstein level=logging.DEBUG if config.debug else logging.INFO, 463fd79200SLuis Augenstein format="[%(levelname)s] %(message)s", 473fd79200SLuis Augenstein ) 483fd79200SLuis Augenstein 499c16c1eaSLuis Augenstein # Build cmd graph 509c16c1eaSLuis Augenstein logging.debug("Start building cmd graph") 519c16c1eaSLuis Augenstein start_time = time.time() 529c16c1eaSLuis Augenstein cmd_graph = CmdGraph.create(config.root_paths, config) 539c16c1eaSLuis Augenstein logging.debug(f"Built cmd graph in {time.time() - start_time} seconds") 549c16c1eaSLuis Augenstein 559c16c1eaSLuis Augenstein # Save used files document 569c16c1eaSLuis Augenstein if config.generate_used_files: 579c16c1eaSLuis Augenstein if config.src_tree == config.obj_tree: 589c16c1eaSLuis Augenstein logging.info( 599c16c1eaSLuis Augenstein f"Extracting all files from the cmd graph to {config.used_files_file_name} " 609c16c1eaSLuis Augenstein "instead of only source files because source files cannot be " 619c16c1eaSLuis Augenstein "reliably classified when the source and object trees are identical.", 629c16c1eaSLuis Augenstein ) 639c16c1eaSLuis Augenstein used_files = [os.path.relpath(node.absolute_path, config.src_tree) for node in cmd_graph] 649c16c1eaSLuis Augenstein logging.debug(f"Found {len(used_files)} files in cmd graph.") 659c16c1eaSLuis Augenstein else: 669c16c1eaSLuis Augenstein used_files = [ 679c16c1eaSLuis Augenstein os.path.relpath(node.absolute_path, config.src_tree) 689c16c1eaSLuis Augenstein for node in cmd_graph 699c16c1eaSLuis Augenstein if is_relative_to(node.absolute_path, config.src_tree) 709c16c1eaSLuis Augenstein and not is_relative_to(node.absolute_path, config.obj_tree) 719c16c1eaSLuis Augenstein ] 729c16c1eaSLuis Augenstein logging.debug(f"Found {len(used_files)} source files in cmd graph") 739c16c1eaSLuis Augenstein if not sbom_logging.has_errors() or config.write_output_on_error: 749c16c1eaSLuis Augenstein used_files_path = os.path.join(config.output_directory, config.used_files_file_name) 759c16c1eaSLuis Augenstein with open(used_files_path, "w", encoding="utf-8") as f: 769c16c1eaSLuis Augenstein f.write("\n".join(str(file_path) for file_path in used_files)) 779c16c1eaSLuis Augenstein logging.debug(f"Successfully saved {used_files_path}") 789c16c1eaSLuis Augenstein 79*a68a29a1SLuis Augenstein if config.generate_spdx is False: 80*a68a29a1SLuis Augenstein _exit_with_summary(config.write_output_on_error) 81*a68a29a1SLuis Augenstein return 82*a68a29a1SLuis Augenstein 83*a68a29a1SLuis Augenstein # Build SPDX Documents 84*a68a29a1SLuis Augenstein logging.debug("Start generating SPDX graph based on cmd graph") 85*a68a29a1SLuis Augenstein start_time = time.time() 86*a68a29a1SLuis Augenstein 87*a68a29a1SLuis Augenstein # The real uuid will be generated based on the content of the SPDX graphs 88*a68a29a1SLuis Augenstein # to ensure that the same SPDX document is always assigned the same uuid. 89*a68a29a1SLuis Augenstein PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000" 90*a68a29a1SLuis Augenstein spdx_id_base_namespace = f"{config.spdxId_prefix}{PLACEHOLDER_UUID}/" 91*a68a29a1SLuis Augenstein spdx_id_generators = SpdxIdGeneratorCollection( 92*a68a29a1SLuis Augenstein base=SpdxIdGenerator(prefix="p", namespace=spdx_id_base_namespace), 93*a68a29a1SLuis Augenstein source=SpdxIdGenerator(prefix="s", namespace=f"{spdx_id_base_namespace}source/"), 94*a68a29a1SLuis Augenstein build=SpdxIdGenerator(prefix="b", namespace=f"{spdx_id_base_namespace}build/"), 95*a68a29a1SLuis Augenstein output=SpdxIdGenerator(prefix="o", namespace=f"{spdx_id_base_namespace}output/"), 96*a68a29a1SLuis Augenstein ) 97*a68a29a1SLuis Augenstein 98*a68a29a1SLuis Augenstein spdx_graphs = build_spdx_graphs( 99*a68a29a1SLuis Augenstein cmd_graph, 100*a68a29a1SLuis Augenstein spdx_id_generators, 101*a68a29a1SLuis Augenstein config, 102*a68a29a1SLuis Augenstein ) 103*a68a29a1SLuis Augenstein spdx_id_uuid = uuid.uuid5( 104*a68a29a1SLuis Augenstein uuid.NAMESPACE_URL, 105*a68a29a1SLuis Augenstein "".join( 106*a68a29a1SLuis Augenstein json.dumps(element.to_dict()) for spdx_graph in spdx_graphs.values() for element in spdx_graph.to_list() 107*a68a29a1SLuis Augenstein ), 108*a68a29a1SLuis Augenstein ) 109*a68a29a1SLuis Augenstein logging.debug(f"Generated SPDX graph in {time.time() - start_time} seconds") 110*a68a29a1SLuis Augenstein 111*a68a29a1SLuis Augenstein if not sbom_logging.has_errors() or config.write_output_on_error: 112*a68a29a1SLuis Augenstein for kernel_sbom_kind, spdx_graph in spdx_graphs.items(): 113*a68a29a1SLuis Augenstein spdx_graph_objects = spdx_graph.to_list() 114*a68a29a1SLuis Augenstein # Add warning and error summary to creation info comment 115*a68a29a1SLuis Augenstein creation_info = next(element for element in spdx_graph_objects if isinstance(element, CreationInfo)) 116*a68a29a1SLuis Augenstein creation_info.comment = "\n".join([ 117*a68a29a1SLuis Augenstein sbom_logging.summarize_warnings(), 118*a68a29a1SLuis Augenstein sbom_logging.summarize_errors(), 119*a68a29a1SLuis Augenstein ]).strip() 120*a68a29a1SLuis Augenstein # Replace Placeholder uuid with real uuid for spdxIds 121*a68a29a1SLuis Augenstein spdx_document = next(element for element in spdx_graph_objects if isinstance(element, SpdxDocument)) 122*a68a29a1SLuis Augenstein for namespaceMap in spdx_document.namespaceMap: 123*a68a29a1SLuis Augenstein namespaceMap.namespace = namespaceMap.namespace.replace(PLACEHOLDER_UUID, str(spdx_id_uuid)) 124*a68a29a1SLuis Augenstein # Serialize SPDX graph to JSON-LD 125*a68a29a1SLuis Augenstein spdx_doc = JsonLdSpdxDocument(graph=spdx_graph_objects) 126*a68a29a1SLuis Augenstein save_path = os.path.join(config.output_directory, config.spdx_file_names[kernel_sbom_kind]) 127*a68a29a1SLuis Augenstein spdx_doc.save(save_path, config.prettify_json) 128*a68a29a1SLuis Augenstein logging.debug(f"Successfully saved {save_path}") 129*a68a29a1SLuis Augenstein 1303fd79200SLuis Augenstein _exit_with_summary(config.write_output_on_error) 131e72b635cSLuis Augenstein 132e72b635cSLuis Augenstein 133e72b635cSLuis Augenstein# Call main method 134e72b635cSLuis Augensteinif __name__ == "__main__": 135e72b635cSLuis Augenstein main() 136