feat(ops): verify release provenance evidence pairs
This commit is contained in:
@@ -130,8 +130,15 @@ def _provenance_kwargs(repo: Path, head: str) -> dict:
|
||||
"operation_id": "op-1",
|
||||
"phase": "pre",
|
||||
"checkout_dir": repo,
|
||||
"expected_oci_revision": head,
|
||||
"image_digest": "sha256:" + ("a" * 64),
|
||||
"observed_evidence": {
|
||||
"schema_version": 1,
|
||||
"target_id": "service-a",
|
||||
"target_kind": "compose-service",
|
||||
"image_id": "sha256:" + ("b" * 64),
|
||||
"image_digest": "sha256:" + ("a" * 64),
|
||||
"oci_revision": head,
|
||||
"observed_at": "2026-09-20T02:00:00Z",
|
||||
},
|
||||
"runtime_files": ["tracked.txt"],
|
||||
"capture_commands": {
|
||||
"pending_slots": [
|
||||
@@ -151,7 +158,7 @@ def test_collect_release_provenance_passes_on_clean_checkout(tmp_path):
|
||||
report = collect_release_provenance(**_provenance_kwargs(repo, head))
|
||||
assert report["checkout"]["git_clean"] is True
|
||||
assert report["checkout"]["detached_head"] is True
|
||||
assert report["artifact_identity"]["oci_revision_matches_head"] is True
|
||||
assert report["observed_evidence"]["source"] == "operator_observed"
|
||||
assert report["runtime_file_verification"][0]["matches_expected_revision"] is True
|
||||
assert report["gate"]["passed"] is True
|
||||
assert report["operation_id"] == "op-1"
|
||||
@@ -181,7 +188,7 @@ def test_collect_release_provenance_blocks_on_revision_or_runtime_mismatch(tmp_p
|
||||
kwargs = _provenance_kwargs(repo, old_head)
|
||||
report = collect_release_provenance(**kwargs)
|
||||
assert report["checkout"]["git_clean"] is True
|
||||
assert report["artifact_identity"]["oci_revision_matches_head"] is False
|
||||
assert report["checkout"]["head_revision"] != report["observed_evidence"]["oci_revision"]
|
||||
assert report["runtime_file_verification"][0]["matches_expected_revision"] is False
|
||||
assert report["gate"]["passed"] is False
|
||||
|
||||
@@ -192,13 +199,58 @@ def test_isolate_dirty_checkout_files_copies_dirty_and_untracked(tmp_path):
|
||||
(repo / "new.txt").write_text("new\n", encoding="utf-8")
|
||||
isolated = tmp_path / "isolated"
|
||||
manifest = isolate_dirty_checkout_files(checkout_dir=repo, isolate_dir=isolated)
|
||||
assert set(manifest["dirty_paths"]) == {"tracked.txt", "new.txt"}
|
||||
assert set(manifest["copied_paths"]) == {"tracked.txt", "new.txt"}
|
||||
entries = {entry["path"]: entry for entry in manifest["entries"]}
|
||||
assert set(entries) == {"tracked.txt", "new.txt"}
|
||||
assert all(entry["source_type"] == "regular" for entry in entries.values())
|
||||
assert all(entry["matches"] is True for entry in entries.values())
|
||||
snapshot = Path(manifest["isolate_dir"])
|
||||
assert snapshot.parent == isolated
|
||||
assert (snapshot / "tracked.txt").read_text(encoding="utf-8") == "dirty\n"
|
||||
assert (snapshot / "new.txt").read_text(encoding="utf-8") == "new\n"
|
||||
assert (snapshot / "manifest.json").is_file()
|
||||
assert (snapshot / "manifest.json").stat().st_mode & 0o222 == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_kind", ["absolute", "relative_escape"])
|
||||
def test_isolate_dirty_checkout_files_rejects_symlink_escape_without_mutating_source(
|
||||
tmp_path, target_kind
|
||||
):
|
||||
repo, _ = _init_git_repo(tmp_path)
|
||||
outside = tmp_path / "outside.txt"
|
||||
outside.write_text("outside\n", encoding="utf-8")
|
||||
link = repo / "escape-link"
|
||||
target = str(outside) if target_kind == "absolute" else "../outside.txt"
|
||||
link.symlink_to(target)
|
||||
isolated = tmp_path / "isolated"
|
||||
|
||||
with pytest.raises(ValueError, match="symlink target escapes checkout"):
|
||||
isolate_dirty_checkout_files(checkout_dir=repo, isolate_dir=isolated)
|
||||
|
||||
assert link.is_symlink()
|
||||
assert link.readlink() == Path(target)
|
||||
assert outside.read_text(encoding="utf-8") == "outside\n"
|
||||
if isolated.exists():
|
||||
assert not list(isolated.glob("*/manifest.json"))
|
||||
|
||||
|
||||
def test_isolate_dirty_checkout_files_preserves_internal_relative_symlink(tmp_path):
|
||||
repo, _ = _init_git_repo(tmp_path)
|
||||
nested = repo / "nested"
|
||||
nested.mkdir()
|
||||
(nested / "target.txt").write_text("target\n", encoding="utf-8")
|
||||
link = repo / "internal-link"
|
||||
link.symlink_to("nested/target.txt")
|
||||
|
||||
manifest = isolate_dirty_checkout_files(
|
||||
checkout_dir=repo, isolate_dir=tmp_path / "isolated"
|
||||
)
|
||||
|
||||
entry = next(item for item in manifest["entries"] if item["path"] == "internal-link")
|
||||
copied = Path(manifest["isolate_dir"]) / "internal-link"
|
||||
assert entry["source_type"] == "symlink"
|
||||
assert entry["copied_type"] == "symlink"
|
||||
assert entry["matches"] is True
|
||||
assert copied.readlink() == Path("nested/target.txt")
|
||||
assert link.readlink() == Path("nested/target.txt")
|
||||
|
||||
|
||||
def test_collect_release_provenance_rejects_runtime_path_escape(tmp_path):
|
||||
@@ -226,8 +278,15 @@ def test_collect_release_provenance_reports_git_failure_context(tmp_path):
|
||||
operation_id="op-1",
|
||||
phase="pre",
|
||||
checkout_dir=not_repo,
|
||||
expected_oci_revision="0" * 40,
|
||||
image_digest="sha256:" + ("a" * 64),
|
||||
observed_evidence={
|
||||
"schema_version": 1,
|
||||
"target_id": "service-a",
|
||||
"target_kind": "compose-service",
|
||||
"image_id": "sha256:" + ("b" * 64),
|
||||
"image_digest": "sha256:" + ("a" * 64),
|
||||
"oci_revision": "0" * 40,
|
||||
"observed_at": "2026-09-20T02:00:00Z",
|
||||
},
|
||||
runtime_files=["tracked.txt"],
|
||||
capture_commands={
|
||||
"pending_slots": [
|
||||
@@ -291,8 +350,7 @@ def test_empty_broker_log_is_explicitly_captured_as_no_matching_events(tmp_path)
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value"),
|
||||
[
|
||||
("expected_oci_revision", None),
|
||||
("image_digest", "not-a-digest"),
|
||||
("observed_evidence", {}),
|
||||
("runtime_files", []),
|
||||
("capture_commands", {}),
|
||||
("instance", "../escape"),
|
||||
|
||||
Reference in New Issue
Block a user