aps-agent/tests/golden/test_audit_ledger.py

123 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 审计独立介质锚定黄金测试(plan.md §3.6 / 矩阵「全审计」)
# ============================================================
# 覆盖:根确定性、锚定往返、篡改检测(改事件/删账本行/改根)、租户/项目隔离。
from __future__ import annotations
import copy
import json
from pathlib import Path
from server.agent_core.audit_ledger import AnchorLedger, audit_root, GENESIS_ROOT
def _events(n: int = 3) -> list[dict]:
out: list[dict] = []
prev = "GENESIS"
import hashlib
for i in range(n):
ev = {"id": i + 1, "action": f"action-{i}", "hash": prev}
prev = hashlib.sha256((prev + json.dumps(ev, sort_keys=True)).encode()).hexdigest()
ev["hash"] = prev
out.append(ev)
return out
def test_audit_root_is_deterministic_and_order_sensitive():
"""锚定根确定性:相同事件同根;顺序/数量变化则不同(root 聚合链哈希字段)。"""
events = _events()
assert audit_root(events) == audit_root(events)
assert audit_root([]) == GENESIS_ROOT
assert audit_root(events) != audit_root(events[:-1]) # 数量变化
assert audit_root(events) != audit_root(list(reversed(events))) # 顺序变化
# root 聚合 hash 字段:哈希序列变化(篡改 hash 本身)必然改变 root
tampered_hash = copy.deepcopy(events)
tampered_hash[1]["hash"] = "0" * 64
assert audit_root(events) != audit_root(tampered_hash)
def test_audit_root_plus_chain_catches_body_tampering():
"""事件体篡改由链校验捕捉,root 保持不变(root 信任链哈希,二者互补)。"""
from server.agent_core.registry import verify_audit_chain
events = _events()
tampered = copy.deepcopy(events)
tampered[1]["action"] = "hacked" # 改事件体但不动 hash
assert audit_root(events) == audit_root(tampered) # root 不变
assert verify_audit_chain(tampered)["ok"] is False # 链校验失败
def test_ledger_append_and_status_match(tmp_path: Path):
"""锚定往返:append 后 status 反映 ok=true 与最新根。"""
events = _events()
ledger = AnchorLedger("tenant-a", "world-1", ledger_dir=str(tmp_path))
assert ledger.status(events)["anchored"] is False # 未锚定
record = ledger.append(events, reason="test")
assert record["count"] == len(events)
status = ledger.status(events)
assert status["anchored"] is True
assert status["ok"] is True
assert status["currentRoot"] == record["root"]
def test_ledger_detects_event_tampering(tmp_path: Path):
"""篡改检测:锚定后改 hash 序列 → ok=false(根不匹配)。"""
events = _events()
ledger = AnchorLedger("tenant-a", "world-1", ledger_dir=str(tmp_path))
ledger.append(events)
tampered = copy.deepcopy(events)
tampered[2]["hash"] = "f" * 64 # 哈希被改 → root 变化
status = ledger.status(tampered)
assert status["ok"] is False
assert status["reason"] == "mismatch"
def test_ledger_detects_deleted_ledger_line(tmp_path: Path):
"""不可改删:删除账本行后 status 退化(记录数减少/无记录)。"""
events = _events()
ledger = AnchorLedger("tenant-a", "world-1", ledger_dir=str(tmp_path))
ledger.append(events)
ledger.append(events, reason="second")
assert ledger.status(events)["records"] == 2
# 删除最后一行(模拟介质被删改)
lines = ledger.path.read_text(encoding="utf-8").splitlines()
ledger.path.write_text("\n".join(lines[:-1]) + "\n", encoding="utf-8")
status = ledger.status(events)
assert status["records"] == 1
# 仍锚定但可能 mismatch(若删除的是最新匹配行)
assert status["anchored"] is True
def test_ledger_isolation_per_tenant_world(tmp_path: Path):
"""租户/项目隔离:不同 scope 各自独立账本文件。"""
events = _events()
a = AnchorLedger("tenant-a", "world-1", ledger_dir=str(tmp_path))
b = AnchorLedger("tenant-b", "world-2", ledger_dir=str(tmp_path))
a.append(events)
assert b.status(events)["anchored"] is False
assert a.path != b.path
assert a.path.exists() and not b.path.exists()
def test_ledger_archive_generates_worm_manifest(tmp_path: Path):
"""WORM 模拟:archive_old 归档超窗记录并生成 .worm-manifest.json(sha256 + merkle)。"""
import json as _json
from server.agent_core.audit_ledger import WORM_MANIFEST
from server.agent_core.audit_merkle import merkle_root
ledger = AnchorLedger("tenant-a", "world-1", ledger_dir=str(tmp_path))
evs = []
for i in range(5):
evs.append({"id": i + 1, "action": f"a{i}", "hash": "0" * 64})
ledger.append(evs, reason="test")
r = ledger.archive_old(keep_last=2)
assert r["archived"] == 3 and r["kept"] == 2
assert r["worm"]["sealed"] is True
archive = Path(r["archive"])
manifest_path = archive.parent / WORM_MANIFEST
assert manifest_path.exists()
manifest = _json.loads(manifest_path.read_text(encoding="utf-8"))
entry = manifest["files"][archive.name]
assert entry["sha256"] and entry["merkleRoot"]
records = [_json.loads(ln) for ln in archive.read_text(encoding="utf-8").splitlines()
if ln.strip()]
assert entry["merkleRoot"] == merkle_root(records)
assert ledger.verify_archive()["ok"] is True