mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-18 05:39:50 +08:00
* feat(sbom): add SBOM generation, license resolution, and CSV export tooling Add mise-integrated SBOM pipeline for container images using Syft. Includes license resolution via crates.io/npm/PyPI APIs and CycloneDX JSON to CSV conversion. Adds agent skill for on-demand SBOM operations. Closes #237 * fix(sbom): chain task dependencies to run generate → resolve → csv sequentially * fix(sbom): add concurrent license resolution, progress logging, and exclude dev artifacts * feat(notices): add mise run notices to generate THIRD-PARTY-NOTICES with full license texts Use cargo-about for Rust crate licenses and pip-licenses for Python packages. Produces a single attribution file with per-package copyright notices and full license text for open-source compliance.
82 lines
2.4 KiB
Python
82 lines
2.4 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:
|
|
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()
|