# 审计独立介质锚定黄金测试(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