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

main: 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")