feat: add controlled source ingestion and replay
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 38s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02b22-9638-76d2-bbff-b7ea1770b118
This commit is contained in:
tegwick 2026-08-22 23:57:37 +02:00
parent b95fba9a9f
commit 879012c776
16 changed files with 1156 additions and 154 deletions

View file

@ -1,11 +1,14 @@
from __future__ import annotations
from contextlib import contextmanager
from datetime import UTC, datetime, timedelta
from pathlib import Path
from fastapi.testclient import TestClient
from sbom_nexus import api
from sbom_nexus.api import create_app
from sbom_nexus.source_fetch import ControlledSource
def client_for(tmp_path: Path) -> TestClient:
@ -19,6 +22,7 @@ def register(
checkout_path: str | None = None,
last_sbom_at: datetime | None = None,
active: bool = True,
source_ref: dict | None = None,
) -> None:
response = client.put(
f"/repositories/{slug}",
@ -26,6 +30,7 @@ def register(
"checkout_path": checkout_path,
"last_sbom_at": last_sbom_at.isoformat() if last_sbom_at else None,
"active": active,
"source_ref": source_ref,
},
)
assert response.status_code == 200
@ -135,6 +140,96 @@ def test_checkout_scan_records_provenance_and_success(tmp_path: Path) -> None:
assert detail["entries"][0]["snapshot_at"] == detail["snapshot_at"]
def test_ingest_operation_key_replays_without_a_second_snapshot(tmp_path: Path) -> None:
repo = tmp_path / "idempotent"
repo.mkdir()
(repo / "requirements.txt").write_text("fastapi==0.136.1\n", encoding="utf-8")
client = client_for(tmp_path)
register(client, "idempotent", checkout_path=str(repo))
headers = {
"Idempotency-Key": "activity-operation-1",
"X-Activity-Core-Operation-ID": "activity-operation-1",
}
first = client.post("/sbom/idempotent/ingest", headers=headers)
second = client.post("/sbom/idempotent/ingest", headers=headers)
assert first.status_code == 200
assert second.json() == first.json()
assert len(client.get("/sbom/snapshots/?repo_slug=idempotent").json()) == 1
conflict = client.post(
"/sbom/idempotent/skip",
headers=headers,
json={"reason": "no-checkout"},
)
assert conflict.status_code == 409
def test_skip_operation_key_replays_original_outcome(tmp_path: Path) -> None:
client = client_for(tmp_path)
register(client, "skip-replay")
headers = {"Idempotency-Key": "skip-operation-1"}
first = client.post(
"/sbom/skip-replay/skip",
headers=headers,
json={"reason": "source-unavailable", "detail": "not projected"},
)
second = client.post(
"/sbom/skip-replay/skip",
headers=headers,
json={"reason": "source-unavailable", "detail": "not projected"},
)
assert first.status_code == 200
assert second.json() == first.json()
assert len(client.get("/sbom/snapshots/?repo_slug=skip-replay").json()) == 1
def test_controlled_source_records_explicit_revision_and_archive_provenance(
tmp_path: Path, monkeypatch
) -> None:
extracted = tmp_path / "extracted"
extracted.mkdir()
(extracted / "requirements.txt").write_text(
"fastapi==0.136.1\n", encoding="utf-8"
)
revision = "a" * 40
source_ref = {
"kind": "forgejo-archive-v1",
"repository": "coulomb/controlled",
"revision": revision,
"observed_ref": "refs/heads/main",
"observed_at": "2026-08-22T20:00:00Z",
}
@contextmanager
def fake_fetch(repo_slug: str, selected: dict):
assert repo_slug == "controlled"
assert selected["revision"] == revision
yield ControlledSource(
root=extracted,
provenance={**selected, "archive_sha256": "b" * 64, "archive_bytes": 42},
)
monkeypatch.setattr(api, "fetch_controlled_source", fake_fetch)
client = client_for(tmp_path)
register(client, "controlled", source_ref=source_ref)
result = client.post(
"/sbom/controlled/ingest",
json={"source_ref": source_ref},
headers={"Idempotency-Key": "controlled-operation-1"},
)
assert result.status_code == 200
assert result.json()["source_revision"] == revision
detail = client.get(f"/sbom/snapshots/{result.json()['snapshot_id']}").json()
assert detail["source"] == "forgejo-archive-v1"
assert detail["source_provenance"]["archive_sha256"] == "b" * 64
assert detail["sources"][0]["path"] == "requirements.txt"
def test_historical_import_is_idempotent(tmp_path: Path) -> None:
client = client_for(tmp_path)
payload = {

View file

@ -23,9 +23,16 @@ def test_initial_migration_upgrades_and_downgrades(tmp_path: Path) -> None:
assert set(inspect(engine).get_table_names()) == {
"alembic_version",
"entries",
"operation_receipts",
"repositories",
"snapshots",
}
assert "source_ref_json" in {
column["name"] for column in inspect(engine).get_columns("repositories")
}
assert "source_provenance_json" in {
column["name"] for column in inspect(engine).get_columns("snapshots")
}
assert {index["name"] for index in inspect(engine).get_indexes("entries")} == {
"ix_entries_license",
"ix_entries_repo",

View file

@ -12,6 +12,9 @@ def test_projection_sync_is_projection_only_and_reconciles(monkeypatch) -> None:
"status": "active",
"local_path": "/fallback/active",
"host_paths": {"build-host": "/repos/active"},
"remote_url": "forgejo-remote:coulomb/active-repo.git",
"git_fingerprint": "a" * 40,
"last_state_synced_at": "2026-08-22T20:00:00Z",
},
{
"slug": "retired-repo",
@ -51,10 +54,13 @@ def test_projection_sync_is_projection_only_and_reconciles(monkeypatch) -> None:
assert result["counts"] == {
"active": 1,
"with_checkout_path": 2,
"with_source_ref": 1,
"inactive": 1,
"without_source_ref": 1,
}
assert result["reconciliation"]["matched_repo_count"] == 2
assert target["active-repo"]["checkout_path"] == "/repos/active"
assert target["active-repo"]["source_ref"]["revision"] == "a" * 40
assert target["retired-repo"]["active"] is False
assert all("/sbom/" not in path for _, _, path in calls)

View file

@ -0,0 +1,58 @@
from __future__ import annotations
import io
import tarfile
from pathlib import Path
import pytest
from sbom_nexus.source_fetch import SourceRejected, _extract, validate_source_ref
def _archive(path: Path, members: dict[str, bytes]) -> None:
with tarfile.open(path, "w:gz") as bundle:
for name, content in members.items():
info = tarfile.TarInfo(name)
info.size = len(content)
bundle.addfile(info, io.BytesIO(content))
def test_validate_source_ref_binds_repository_to_slug() -> None:
revision = "a" * 40
assert validate_source_ref(
"demo",
{
"kind": "forgejo-archive-v1",
"repository": "coulomb/demo",
"revision": revision,
},
)["revision"] == revision
with pytest.raises(SourceRejected, match="does not match"):
validate_source_ref(
"demo",
{
"kind": "forgejo-archive-v1",
"repository": "coulomb/other",
"revision": revision,
},
)
def test_extract_rejects_path_traversal(tmp_path: Path) -> None:
archive = tmp_path / "unsafe.tar.gz"
_archive(archive, {"repo/../../escaped": b"nope"})
with pytest.raises(SourceRejected, match="unsafe path"):
_extract(archive, tmp_path / "out")
def test_extract_accepts_one_regular_root(tmp_path: Path) -> None:
archive = tmp_path / "safe.tar.gz"
_archive(archive, {"repo/requirements.txt": b"fastapi==0.136.1\n"})
destination = tmp_path / "out"
destination.mkdir()
root = _extract(archive, destination)
assert root == destination / "repo"
assert (root / "requirements.txt").read_text() == "fastapi==0.136.1\n"