| 1 | |
| 2 | |
| 3 | |
| 4 |
|
| 5 | |
| 6 | Compute software bill of materials in SPDX format describing a kernel build.
|
| 7 |
|
| 8 |
|
| 9 | import json
|
| 10 | import logging
|
| 11 | import os
|
| 12 | import sys
|
| 13 | import time
|
| 14 | import uuid
|
| 15 | import sbom.sbom_logging as sbom_logging
|
| 16 | from sbom.config import get_config
|
| 17 | from sbom.path_utils import is_relative_to
|
| 18 | from sbom.spdx import JsonLdSpdxDocument, SpdxIdGenerator
|
| 19 | from sbom.spdx.core import CreationInfo, SpdxDocument
|
| 20 | from sbom.spdx_graph import SpdxIdGeneratorCollection, build_spdx_graphs
|
| 21 | from sbom.cmd_graph import CmdGraph
|
| 22 |
|
| 23 |
|
| 24 | def _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 |
|
| 39 | def main():
|
| 40 | |
| 41 | config = get_config()
|
| 42 |
|
| 43 | |
| 44 | logging.basicConfig(
|
| 45 | level=logging.DEBUG if config.debug else logging.INFO,
|
| 46 | format="[%(levelname)s] %(message)s",
|
| 47 | )
|
| 48 |
|
| 49 | |
| 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 | |
| 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 | |
| 84 | logging.debug("Start generating SPDX graph based on cmd graph")
|
| 85 | start_time = time.time()
|
| 86 |
|
| 87 | |
| 88 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 134 | if __name__ == "__main__":
|
| 135 | main()
|