aps-agent/tests/golden/test_audit_mirror.py

117 lines
4.6 KiB
Python
Raw Permalink Normal View History

# 审计事件独立介质镜像黄金测试(plan.md §3.6 / 矩阵「审计 append-only」)
# ============================================================
# 覆盖:镜像追加/读取、改 world 不影响镜像、租户隔离、启用/禁用开关、
# 链校验仍可作用于镜像事件、gov_audit 镜像优先。
from __future__ import annotations
import copy
from pathlib import Path
from server.agent_core import audit
from server.agent_core.audit_mirror import AuditMirror, mirror_enabled
def _event(i: int = 1) -> dict:
return {"id": i, "at": "2026-08-01T00:00:00+08:00", "ts": "2026-08-01T00:00:00+08:00",
"actor": "tester", "category": "TEST", "action": f"test.{i}",
"target": {"type": "TEST"}, "power": "P0", "rationale": {},
"evidenceRefs": [], "beforeSnapshot": None, "result": "SUCCESS",
"prevHash": "GENESIS", "hash": "0" * 64}
def test_mirror_append_and_read(tmp_path: Path):
"""镜像追加/读取:追加事件可读回,计数正确。"""
m = AuditMirror("tenant-a", "world-1", mirror_dir=str(tmp_path))
assert m.read_events() == []
m.append_event(_event(1))
m.append_event(_event(2))
events = m.read_events()
assert len(events) == 2
assert [e["id"] for e in events] == [1, 2]
assert m.count() == 2
def test_mirror_isolated_per_tenant_world(tmp_path: Path):
"""租户/项目隔离:不同 scope 各自独立文件。"""
a = AuditMirror("tenant-a", "world-1", mirror_dir=str(tmp_path))
b = AuditMirror("tenant-b", "world-2", mirror_dir=str(tmp_path))
a.append_event(_event(1))
assert b.read_events() == []
assert a.path != b.path
assert a.path.exists() and not b.path.exists()
def test_mirror_unaffected_by_world_mutation(tmp_path: Path, monkeypatch):
"""改 world 不影响镜像:镜像保留原始事件(独立介质证据)。"""
m = AuditMirror("tenant-a", "world-1", mirror_dir=str(tmp_path))
m.append_event(_event(1))
# 模拟业务进程篡改 world 副本
forged = copy.deepcopy(_event(1))
forged["actor"] = "hacker"
world = {"auditEvents": [forged]}
del world # world 修改不影响镜像
events = m.read_events()
assert events[0]["actor"] == "tester" # 镜像保留原始值
def test_mirror_enabled_switch(tmp_path: Path, monkeypatch):
"""启用/禁用开关:APS_AUDIT_MIRROR=1 启用,缺省禁用。"""
monkeypatch.delenv("APS_AUDIT_MIRROR", raising=False)
monkeypatch.delenv("APS_AUDIT_MIRROR_DIR", raising=False)
assert mirror_enabled() is False
monkeypatch.setenv("APS_AUDIT_MIRROR", "1")
assert mirror_enabled() is True
def test_write_audit_mirrors_when_enabled(tmp_path: Path, monkeypatch):
"""write_audit 启用镜像时把事件写入独立介质。"""
monkeypatch.setenv("APS_AUDIT_MIRROR", "1")
monkeypatch.setenv("APS_AUDIT_MIRROR_DIR", str(tmp_path / "mirror"))
monkeypatch.setattr("server.auth.context.get_identity", lambda: _FakeIdentity())
import server.state.projects as projects_module
monkeypatch.setattr(projects_module, "get_project_store", lambda: _FakeProjectStore())
world: dict = {}
audit.write_audit(world, lambda _k: 1, actor="tester", category="TEST",
action="test.mirror", target={"type": "T"},
power="P0", rationale={})
assert world["auditEvents"][0]["id"] == 1
m = AuditMirror("tenant-fake", "world-fake", mirror_dir=str(tmp_path / "mirror"))
events = m.read_events()
assert len(events) == 1
assert events[0]["action"] == "test.mirror"
class _FakeIdentity:
tenant_uuid = "tenant-fake"
user_id = None
username = None
fullname = None
roles = ()
class _FakeProjectStore:
def active_world_key(self) -> str:
return "world-fake"
def test_mirror_events_pass_chain_verify(tmp_path: Path):
"""链校验仍可作用于镜像事件(镜像保留完整哈希链)。"""
import hashlib
import json
from server.agent_core.registry import verify_audit_chain
prev = "GENESIS"
events = []
m = AuditMirror("tenant-a", "world-1", mirror_dir=str(tmp_path))
for i in range(3):
ev = _event(i + 1)
ev["prevHash"] = prev # 链式前驱(第二事件起指向前一事件 hash)
body = {k: v for k, v in ev.items() if k != "hash"} # 与 verify_audit_chain 剥离方式一致
payload = prev + json.dumps(body, ensure_ascii=False, sort_keys=True)
ev["hash"] = hashlib.sha256(payload.encode()).hexdigest()
prev = ev["hash"]
events.append(ev)
m.append_event(ev)
assert verify_audit_chain(m.read_events())["ok"] is True