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