refactor: delegate SBOM scans to Nexus

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a028f0-a42f-7582-89a8-ebaad7343834
This commit is contained in:
tegwick 2026-08-22 20:22:45 +02:00
parent 6d57d1c1b2
commit ad0ba6f2ba
8 changed files with 277 additions and 428 deletions

View file

@ -4,108 +4,130 @@ import json
import subprocess
from pathlib import Path
from repo_manager.sbom import detect_sources, scan_repository
from repo_manager.cli import main
from repo_manager.sbom_client import (
licence_report_from_snapshot,
scan_repository_via_nexus,
)
def _git(repo: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True)
def test_scan_derives_snapshot_and_copyleft_report(tmp_path: Path) -> None:
repo = tmp_path / "example"
repo.mkdir()
_git(repo, "init")
_git(repo, "config", "user.email", "test@example.com")
_git(repo, "config", "user.name", "Test")
(repo / "uv.lock").write_text(
'version = 1\n[[package]]\nname = "pyyaml"\nversion = "6.0.2"\n',
encoding="utf-8",
)
(repo / "package-lock.json").write_text(
json.dumps(
def _snapshot() -> dict:
return {
"schema": "sbom-nexus.snapshot.v1",
"ok": True,
"repo_slug": "example",
"source_revision": "abc123",
"generated_at": "2026-08-22T18:00:00Z",
"entry_count": 1,
"entries": [
{
"packages": {
"": {"name": "root", "version": "1.0.0"},
"node_modules/copyleft": {
"name": "copyleft",
"version": "2.0.0",
"license": "GPL-3.0-only",
},
"node_modules/dev-only": {
"name": "dev-only",
"version": "3.0.0",
"license": "AGPL-3.0-only",
"dev": True,
},
}
"package_name": "pyyaml",
"package_version": "6.0.3",
"ecosystem": "python",
"license_spdx": "MIT",
"is_direct": True,
"is_dev": False,
"source_path": "uv.lock",
}
),
encoding="utf-8",
)
_git(repo, "add", ".")
_git(repo, "commit", "-m", "seed")
result = scan_repository(repo)
assert result["ok"] is True
assert result["schema"] == "repo-manager.sbom-snapshot.v1"
assert result["source_revision"]
assert result["generated_at"].endswith("Z")
assert result["entry_count"] == 3
assert result["licence_report"]["copyleft_direct_count"] == 1
assert result["licence_report"]["copyleft_direct_prod"][0]["package_name"] == "copyleft"
def test_detection_covers_sources_and_skips_dependency_directories(tmp_path: Path) -> None:
(tmp_path / "go.sum").write_text("example.test/mod v1.2.3 h1:abc\n", encoding="utf-8")
terraform = tmp_path / "infra"
terraform.mkdir()
(terraform / ".terraform.lock.hcl").write_text(
'provider "registry.terraform.io/hashicorp/null" {\n version = "3.2.3"\n}\n',
encoding="utf-8",
)
ansible = tmp_path / "deploy" / "ansible"
ansible.mkdir(parents=True)
(ansible / "requirements.yml").write_text("collections:\n - community.general\n", encoding="utf-8")
ignored = tmp_path / "node_modules"
ignored.mkdir()
(ignored / "package-lock.json").write_text("{}", encoding="utf-8")
sources = {str(path.relative_to(tmp_path)) for path, _parser in detect_sources(tmp_path)}
assert sources == {
"deploy/ansible/requirements.yml",
"go.sum",
"infra/.terraform.lock.hcl",
],
"sources": [{"path": "uv.lock", "entry_count": 1, "sha256": "abc"}],
"licence_report": {
"groups": [
{
"license_spdx": "MIT",
"count": 1,
"is_copyleft": False,
}
],
"copyleft_direct_prod": [],
"copyleft_direct_count": 0,
},
"errors": [],
}
def test_go_sum_marks_modules_declared_in_go_mod_as_direct(tmp_path: Path) -> None:
(tmp_path / "go.mod").write_text(
"module example.test/app\n\nrequire example.test/direct v1.2.3\n",
encoding="utf-8",
)
(tmp_path / "go.sum").write_text(
"example.test/direct v1.2.3 h1:abc\n"
"example.test/direct v1.2.3/go.mod h1:def\n"
"example.test/transitive v2.0.0 h1:ghi\n",
encoding="utf-8",
)
def test_scan_delegates_to_sbom_nexus_without_shell(monkeypatch, tmp_path: Path) -> None:
observed = {}
result = scan_repository(tmp_path)
def fake_run(command, **kwargs):
observed["command"] = command
observed["kwargs"] = kwargs
return subprocess.CompletedProcess(command, 0, json.dumps(_snapshot()), "")
assert result["ok"] is True
assert [(entry["package_name"], entry["is_direct"]) for entry in result["entries"]] == [
("example.test/direct", True),
("example.test/transitive", False),
monkeypatch.setenv("SBOM_NEXUS_CLI", "/opt/sbom-nexus/bin/sbom-nexus")
monkeypatch.setattr(subprocess, "run", fake_run)
result = scan_repository_via_nexus(tmp_path, slug="example")
assert observed["command"] == [
"/opt/sbom-nexus/bin/sbom-nexus",
"scan",
str(tmp_path),
"--slug",
"example",
]
assert observed["kwargs"] == {
"capture_output": True,
"text": True,
"check": False,
}
assert result["schema"] == "sbom-nexus.snapshot.v1"
assert result["product_owner"] == "sbom-nexus"
assert result["delegated_by"] == "repo-manager"
def test_invalid_source_is_reported_without_partial_failure(tmp_path: Path) -> None:
(tmp_path / "uv.lock").write_text("not = [valid", encoding="utf-8")
def test_missing_nexus_cli_returns_actionable_error(monkeypatch, tmp_path: Path) -> None:
monkeypatch.delenv("SBOM_NEXUS_CLI", raising=False)
monkeypatch.setattr("repo_manager.sbom_client.shutil.which", lambda _name: None)
result = scan_repository(tmp_path)
result = scan_repository_via_nexus(tmp_path)
assert result["ok"] is False
assert result["entry_count"] == 0
assert result["errors"][0]["source_path"] == "uv.lock"
assert result["schema"] == "sbom-nexus.snapshot.v1"
assert "SBOM_NEXUS_CLI" in result["errors"][0]["detail"]
def test_licence_report_alias_preserves_shape() -> None:
result = licence_report_from_snapshot(_snapshot())
assert set(result) == {
"ok",
"repo_slug",
"source_revision",
"generated_at",
"entry_count",
"licence_report",
"errors",
"delegated_by",
"product_owner",
}
assert result["licence_report"]["copyleft_direct_count"] == 0
def test_cli_scan_preserves_output_file_behavior(monkeypatch, tmp_path: Path, capsys) -> None:
monkeypatch.setattr(
"repo_manager.sbom_client.scan_repository_via_nexus",
lambda path, slug=None: {
**_snapshot(),
"delegated_by": "repo-manager",
"product_owner": "sbom-nexus",
},
)
output = tmp_path / "snapshot.json"
exit_code = main(
[
"sbom",
"scan",
"--path",
str(tmp_path),
"--slug",
"example",
"--output",
str(output),
]
)
assert exit_code == 0
assert json.loads(output.read_text())["schema"] == "sbom-nexus.snapshot.v1"
assert json.loads(capsys.readouterr().out)["product_owner"] == "sbom-nexus"