xref: /linux/scripts/sbom/sbom.py (revision 7dd19adaaac9a161723a24de7c270861063ec6ac)
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