1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0-only OR MIT 3# Copyright (C) 2025 TNG Technology Consulting GmbH 4 5""" 6Compute software bill of materials in SPDX format describing a kernel build. 7""" 8 9import json 10import logging 11import os 12import sys 13import time 14import uuid 15import sbom.sbom_logging as sbom_logging 16from sbom.config import get_config 17from sbom.path_utils import is_relative_to 18from sbom.spdx import JsonLdSpdxDocument, SpdxIdGenerator 19from sbom.spdx.core import CreationInfo, SpdxDocument 20from sbom.spdx_graph import SpdxIdGeneratorCollection, build_spdx_graphs 21from sbom.cmd_graph import CmdGraph 22 23 24def _exit_with_summary(write_output_on_error: bool = False) -> None: 25 warning_summary = sbom_logging.summarize_warnings() 26 error_summary = sbom_logging.summarize_errors() 27 if warning_summary: 28 logging.warning(warning_summary) 29 if error_summary: 30 logging.error(error_summary) 31 if not write_output_on_error: 32 logging.info( 33 "Use --write-output-on-error to generate output documents even when errors occur. " 34 "Note that in this case the generated documents may be incomplete." 35 ) 36 sys.exit(1) 37 38 39def main(): 40 # Read config 41 config = get_config() 42 43 # Configure logging 44 logging.basicConfig( 45 level=logging.DEBUG if config.debug else logging.INFO, 46 format="[%(levelname)s] %(message)s", 47 ) 48 49 # Build cmd graph 50 logging.debug("Start building cmd graph") 51 start_time = time.time() 52 cmd_graph = CmdGraph.create(config.root_paths, config) 53 logging.debug(f"Built cmd graph in {time.time() - start_time} seconds") 54 55 # Save used files document 56 if config.generate_used_files: 57 if config.src_tree == config.obj_tree: 58 logging.info( 59 f"Extracting all files from the cmd graph to {config.used_files_file_name} " 60 "instead of only source files because source files cannot be " 61 "reliably classified when the source and object trees are identical.", 62 ) 63 used_files = [os.path.relpath(node.absolute_path, config.src_tree) for node in cmd_graph] 64 logging.debug(f"Found {len(used_files)} files in cmd graph.") 65 else: 66 used_files = [ 67 os.path.relpath(node.absolute_path, config.src_tree) 68 for node in cmd_graph 69 if is_relative_to(node.absolute_path, config.src_tree) 70 and not is_relative_to(node.absolute_path, config.obj_tree) 71 ] 72 logging.debug(f"Found {len(used_files)} source files in cmd graph") 73 if not sbom_logging.has_errors() or config.write_output_on_error: 74 used_files_path = os.path.join(config.output_directory, config.used_files_file_name) 75 with open(used_files_path, "w", encoding="utf-8") as f: 76 f.write("\n".join(str(file_path) for file_path in used_files)) 77 logging.debug(f"Successfully saved {used_files_path}") 78 79 if config.generate_spdx is False: 80 _exit_with_summary(config.write_output_on_error) 81 return 82 83 # Build SPDX Documents 84 logging.debug("Start generating SPDX graph based on cmd graph") 85 start_time = time.time() 86 87 # The real uuid will be generated based on the content of the SPDX graphs 88 # to ensure that the same SPDX document is always assigned the same uuid. 89 PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000" 90 spdx_id_base_namespace = f"{config.spdxId_prefix}{PLACEHOLDER_UUID}/" 91 spdx_id_generators = SpdxIdGeneratorCollection( 92 base=SpdxIdGenerator(prefix="p", namespace=spdx_id_base_namespace), 93 source=SpdxIdGenerator(prefix="s", namespace=f"{spdx_id_base_namespace}source/"), 94 build=SpdxIdGenerator(prefix="b", namespace=f"{spdx_id_base_namespace}build/"), 95 output=SpdxIdGenerator(prefix="o", namespace=f"{spdx_id_base_namespace}output/"), 96 ) 97 98 spdx_graphs = build_spdx_graphs( 99 cmd_graph, 100 spdx_id_generators, 101 config, 102 ) 103 spdx_id_uuid = uuid.uuid5( 104 uuid.NAMESPACE_URL, 105 "".join( 106 json.dumps(element.to_dict()) for spdx_graph in spdx_graphs.values() for element in spdx_graph.to_list() 107 ), 108 ) 109 logging.debug(f"Generated SPDX graph in {time.time() - start_time} seconds") 110 111 if not sbom_logging.has_errors() or config.write_output_on_error: 112 for kernel_sbom_kind, spdx_graph in spdx_graphs.items(): 113 spdx_graph_objects = spdx_graph.to_list() 114 # Add warning and error summary to creation info comment 115 creation_info = next(element for element in spdx_graph_objects if isinstance(element, CreationInfo)) 116 creation_info.comment = "\n".join([ 117 sbom_logging.summarize_warnings(), 118 sbom_logging.summarize_errors(), 119 ]).strip() 120 # Replace Placeholder uuid with real uuid for spdxIds 121 spdx_document = next(element for element in spdx_graph_objects if isinstance(element, SpdxDocument)) 122 for namespaceMap in spdx_document.namespaceMap: 123 namespaceMap.namespace = namespaceMap.namespace.replace(PLACEHOLDER_UUID, str(spdx_id_uuid)) 124 # Serialize SPDX graph to JSON-LD 125 spdx_doc = JsonLdSpdxDocument(graph=spdx_graph_objects) 126 save_path = os.path.join(config.output_directory, config.spdx_file_names[kernel_sbom_kind]) 127 spdx_doc.save(save_path, config.prettify_json) 128 logging.debug(f"Successfully saved {save_path}") 129 130 _exit_with_summary(config.write_output_on_error) 131 132 133# Call main method 134if __name__ == "__main__": 135 main() 136