Files
OpenShell/deploy/sbom/sbom_to_csv.py
Mesut Oezdil 85c52bba85 fix(sbom): handle SPDX expression licenses in extract_licenses (#1898)
CycloneDX allows licenses as either {"license": {"id": "..."}} or
{"expression": "MIT OR Apache-2.0"}. The expression form was silently
dropped, producing an empty license field in the CSV output.
2026-06-22 17:36:24 -05:00

85 lines
2.5 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Convert CycloneDX SBOM JSON files to CSV.
Usage:
python sbom_to_csv.py # convert all *.cdx.json in deploy/sbom/output/
python sbom_to_csv.py file1.json ... # convert specific files
"""
from __future__ import annotations
import csv
import json
import sys
from pathlib import Path
def extract_licenses(component: dict) -> str:
"""Pull license IDs/names from CycloneDX license entries."""
licenses = component.get("licenses", [])
ids = []
for entry in licenses:
if "expression" in entry:
ids.append(entry["expression"])
else:
lic = entry.get("license", {})
ids.append(lic.get("id") or lic.get("name", ""))
return " | ".join(filter(None, ids))
def sbom_to_csv(json_path: Path) -> Path:
"""Read a CycloneDX JSON SBOM and write a CSV beside it."""
with json_path.open() as f:
sbom = json.load(f)
csv_path = json_path.with_suffix(".csv")
components = sbom.get("components", [])
with csv_path.open("w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["name", "version", "type", "purl", "licenses", "bom-ref"])
for comp in components:
writer.writerow(
[
comp.get("name", ""),
comp.get("version", ""),
comp.get("type", ""),
comp.get("purl", ""),
extract_licenses(comp),
comp.get("bom-ref", ""),
]
)
return csv_path
def _find_sbom_files() -> list[Path]:
"""Find SBOM JSON files in the default output directory."""
repo_root = Path(__file__).resolve().parent.parent.parent
output_dir = repo_root / "deploy" / "sbom" / "output"
return sorted(output_dir.glob("*.cdx.json"))
def main() -> None:
files = [Path(p) for p in sys.argv[1:]] if len(sys.argv) > 1 else _find_sbom_files()
if not files:
print("No SBOM JSON files found.")
print("Run 'mise run sbom:generate' first, or pass file paths as arguments.")
sys.exit(1)
for path in files:
csv_path = sbom_to_csv(path)
with csv_path.open() as count_fh:
components_count = sum(1 for _ in count_fh) - 1 # minus header
print(f"{path.name} -> {csv_path.name} ({components_count} components)")
if __name__ == "__main__":
main()