117 lines
4.6 KiB
Python
117 lines
4.6 KiB
Python
# 审计事件独立介质镜像黄金测试(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
|