audit-labs/evidence-seal
Tamper-evident seals and chain of custody for audit evidence.
clone: git clone https://gitbay.org/audit-labs/evidence-seal.git
v0.1.0: tests/test_manifest.py · raw
1"""Tests for building, verifying, and chaining manifests."""
2
3import pytest
4
5from evidence_seal.manifest import (
6 build_manifest,
7 compute_id,
8 verify_chain,
9 verify_manifest,
10)
11
12
13@pytest.fixture
14def pkg(tmp_path):
15 d = tmp_path / "aws_audit_acme_2026-01-01"
16 d.mkdir()
17 (d / "iam.csv").write_text("user,mfa\nalice,true\n", encoding="utf-8")
18 (d / "summary.txt").write_text("ok\n", encoding="utf-8")
19 return d
20
21
22def test_build_manifest_shape(pkg):
23 m = build_manifest(pkg, metadata={"engagement": "ACME"})
24 assert m["subject"] == "aws_audit_acme_2026-01-01"
25 assert m["file_count"] == 2
26 assert m["metadata"] == {"engagement": "ACME"}
27 assert [f["path"] for f in m["files"]] == ["iam.csv", "summary.txt"]
28 assert m["id"] == compute_id(m)
29
30
31def test_verify_clean(pkg):
32 m = build_manifest(pkg)
33 result = verify_manifest(pkg, m)
34 assert result.intact
35 assert result.checked == 2
36
37
38def test_verify_detects_modification(pkg):
39 m = build_manifest(pkg)
40 (pkg / "iam.csv").write_text("user,mfa\nalice,false\n", encoding="utf-8")
41 result = verify_manifest(pkg, m)
42 assert not result.intact
43 assert result.modified == ["iam.csv"]
44
45
46def test_verify_detects_added_and_removed(pkg):
47 m = build_manifest(pkg)
48 (pkg / "extra.csv").write_text("new\n", encoding="utf-8")
49 (pkg / "summary.txt").unlink()
50 result = verify_manifest(pkg, m)
51 assert result.added == ["extra.csv"]
52 assert result.removed == ["summary.txt"]
53 assert not result.intact
54
55
56def test_verify_detects_manifest_tampering(pkg):
57 m = build_manifest(pkg)
58 # Rewrite a recorded hash but leave the (now stale) id in place.
59 m["files"][0]["sha256"] = "0" * 64
60 result = verify_manifest(pkg, m)
61 assert not result.id_ok
62 assert not result.root_ok
63 assert not result.intact
64
65
66def test_ignore_patterns_excluded_and_recorded(pkg):
67 (pkg / "notes.tmp").write_text("scratch\n", encoding="utf-8")
68 m = build_manifest(pkg, ignore=["*.tmp"])
69 assert m["ignore"] == ["*.tmp"]
70 assert all(not f["path"].endswith(".tmp") for f in m["files"])
71 # Verify with the same ignore keeps it intact despite the tmp file present.
72 assert verify_manifest(pkg, m, ignore=m["ignore"]).intact
73
74
75def test_chain_links(pkg):
76 m1 = build_manifest(pkg)
77 (pkg / "iam.csv").write_text("user,mfa\nalice,true\nbob,true\n", encoding="utf-8")
78 m2 = build_manifest(pkg, previous=m1["id"])
79 assert verify_chain([m1, m2]).ok
80
81
82def test_chain_detects_wrong_order(pkg):
83 m1 = build_manifest(pkg)
84 m2 = build_manifest(pkg, previous=m1["id"])
85 result = verify_chain([m2, m1])
86 assert not result.ok
87 assert result.broken_at == 0
88
89
90def test_chain_detects_spliced_entry(pkg):
91 m1 = build_manifest(pkg)
92 m2 = build_manifest(pkg, previous=m1["id"])
93 m3 = build_manifest(pkg, previous=m2["id"])
94 # Drop the middle manifest: m3.previous no longer matches m1.id.
95 result = verify_chain([m1, m3])
96 assert not result.ok
97 assert result.broken_at == 1
98
99
100def test_missing_directory_raises(tmp_path):
101 with pytest.raises(FileNotFoundError):
102 build_manifest(tmp_path / "nope")