| #!/usr/bin/env python3 |
| # SPDX-License-Identifier: GPL-2.0-only OR MIT |
| # Copyright (C) 2025 TNG Technology Consulting GmbH |
| |
| """ |
| Compute software bill of materials in SPDX format describing a kernel build. |
| """ |
| |
| import json |
| import logging |
| import os |
| import sys |
| import time |
| import uuid |
| import sbom.sbom_logging as sbom_logging |
| from sbom.config import get_config |
| from sbom.path_utils import is_relative_to |
| from sbom.spdx import JsonLdSpdxDocument, SpdxIdGenerator |
| from sbom.spdx.core import CreationInfo, SpdxDocument |
| from sbom.spdx_graph import SpdxIdGeneratorCollection, build_spdx_graphs |
| from sbom.cmd_graph import CmdGraph |
| |
| |
| def _exit_with_summary(write_output_on_error: bool = False) -> None: |
| warning_summary = sbom_logging.summarize_warnings() |
| error_summary = sbom_logging.summarize_errors() |
| if warning_summary: |
| logging.warning(warning_summary) |
| if error_summary: |
| logging.error(error_summary) |
| if not write_output_on_error: |
| logging.info( |
| "Use --write-output-on-error to generate output documents even when errors occur. " |
| "Note that in this case the generated documents may be incomplete." |
| ) |
| sys.exit(1) |
| |
| |
| def main(): |
| # Read config |
| config = get_config() |
| |
| # Configure logging |
| logging.basicConfig( |
| level=logging.DEBUG if config.debug else logging.INFO, |
| format="[%(levelname)s] %(message)s", |
| ) |
| |
| # Build cmd graph |
| logging.debug("Start building cmd graph") |
| start_time = time.time() |
| cmd_graph = CmdGraph.create(config.root_paths, config) |
| logging.debug(f"Built cmd graph in {time.time() - start_time} seconds") |
| |
| # Save used files document |
| if config.generate_used_files: |
| if config.src_tree == config.obj_tree: |
| logging.info( |
| f"Extracting all files from the cmd graph to {config.used_files_file_name} " |
| "instead of only source files because source files cannot be " |
| "reliably classified when the source and object trees are identical.", |
| ) |
| used_files = [os.path.relpath(node.absolute_path, config.src_tree) for node in cmd_graph] |
| logging.debug(f"Found {len(used_files)} files in cmd graph.") |
| else: |
| used_files = [ |
| os.path.relpath(node.absolute_path, config.src_tree) |
| for node in cmd_graph |
| if is_relative_to(node.absolute_path, config.src_tree) |
| and not is_relative_to(node.absolute_path, config.obj_tree) |
| ] |
| logging.debug(f"Found {len(used_files)} source files in cmd graph") |
| if not sbom_logging.has_errors() or config.write_output_on_error: |
| used_files_path = os.path.join(config.output_directory, config.used_files_file_name) |
| with open(used_files_path, "w", encoding="utf-8") as f: |
| f.write("\n".join(str(file_path) for file_path in used_files)) |
| logging.debug(f"Successfully saved {used_files_path}") |
| |
| if config.generate_spdx is False: |
| _exit_with_summary(config.write_output_on_error) |
| return |
| |
| # Build SPDX Documents |
| logging.debug("Start generating SPDX graph based on cmd graph") |
| start_time = time.time() |
| |
| # The real uuid will be generated based on the content of the SPDX graphs |
| # to ensure that the same SPDX document is always assigned the same uuid. |
| PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000" |
| spdx_id_base_namespace = f"{config.spdxId_prefix}{PLACEHOLDER_UUID}/" |
| spdx_id_generators = SpdxIdGeneratorCollection( |
| base=SpdxIdGenerator(prefix="p", namespace=spdx_id_base_namespace), |
| source=SpdxIdGenerator(prefix="s", namespace=f"{spdx_id_base_namespace}source/"), |
| build=SpdxIdGenerator(prefix="b", namespace=f"{spdx_id_base_namespace}build/"), |
| output=SpdxIdGenerator(prefix="o", namespace=f"{spdx_id_base_namespace}output/"), |
| ) |
| |
| spdx_graphs = build_spdx_graphs( |
| cmd_graph, |
| spdx_id_generators, |
| config, |
| ) |
| spdx_id_uuid = uuid.uuid5( |
| uuid.NAMESPACE_URL, |
| "".join( |
| json.dumps(element.to_dict()) for spdx_graph in spdx_graphs.values() for element in spdx_graph.to_list() |
| ), |
| ) |
| logging.debug(f"Generated SPDX graph in {time.time() - start_time} seconds") |
| |
| if not sbom_logging.has_errors() or config.write_output_on_error: |
| for kernel_sbom_kind, spdx_graph in spdx_graphs.items(): |
| spdx_graph_objects = spdx_graph.to_list() |
| # Add warning and error summary to creation info comment |
| creation_info = next(element for element in spdx_graph_objects if isinstance(element, CreationInfo)) |
| creation_info.comment = "\n".join([ |
| sbom_logging.summarize_warnings(), |
| sbom_logging.summarize_errors(), |
| ]).strip() |
| # Replace Placeholder uuid with real uuid for spdxIds |
| spdx_document = next(element for element in spdx_graph_objects if isinstance(element, SpdxDocument)) |
| for namespaceMap in spdx_document.namespaceMap: |
| namespaceMap.namespace = namespaceMap.namespace.replace(PLACEHOLDER_UUID, str(spdx_id_uuid)) |
| # Serialize SPDX graph to JSON-LD |
| spdx_doc = JsonLdSpdxDocument(graph=spdx_graph_objects) |
| save_path = os.path.join(config.output_directory, config.spdx_file_names[kernel_sbom_kind]) |
| spdx_doc.save(save_path, config.prettify_json) |
| logging.debug(f"Successfully saved {save_path}") |
| |
| _exit_with_summary(config.write_output_on_error) |
| |
| |
| # Call main method |
| if __name__ == "__main__": |
| main() |