| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- from __future__ import annotations
- from copy import deepcopy
- from datetime import UTC, datetime, timedelta
- import pytest
- class FakeAuditRepository:
- def __init__(self, events=None):
- self.events = list(events or [])
- self.seals = {}
- def fetch_events(self, *, categories, period_start, period_end):
- return [
- deepcopy(event)
- for event in self.events
- if event["category"] in categories
- and period_start <= event["occurred_at"] <= period_end
- ]
- def save_seal(self, seal):
- self.seals[seal["uid"]] = deepcopy(seal)
- return deepcopy(seal)
- def get_seal(self, seal_uid):
- seal = self.seals.get(seal_uid)
- return deepcopy(seal) if seal else None
- def list_seals(self, *, limit):
- return list(self.seals.values())[:limit]
- def _event(
- uid,
- category,
- *,
- occurred_at,
- action="completed",
- status="success",
- detail=None,
- ):
- return {
- "event_uid": uid,
- "category": category,
- "action": action,
- "status": status,
- "actor_uid": "actor-1",
- "resource_type": "device_asset",
- "resource_uid": "asset-1",
- "occurred_at": occurred_at,
- "safe_detail": detail or {"attempt": 1},
- }
- def _service(events):
- from app.core.system.governance_audit import GovernanceAuditService
- return GovernanceAuditService(
- FakeAuditRepository(events),
- evidence_secret="wp12-test-secret-with-at-least-32-bytes",
- key_version="test-v1",
- uid_factory=lambda: "seal-1",
- now_factory=lambda: datetime(2026, 7, 31, tzinfo=UTC),
- )
- def test_list_events_is_deterministic_bounded_and_secret_safe():
- from app.core.system.governance_audit import GovernanceAuditInvalid
- now = datetime(2026, 7, 30, 8, 0, tzinfo=UTC)
- events = [
- _event(
- "event-b",
- "ingestion",
- occurred_at=now,
- detail={
- "attempt": 2,
- "password": "must-not-leak",
- "nested": {"authorization": "Bearer secret"},
- },
- ),
- _event("event-a", "authentication", occurred_at=now),
- ]
- service = _service(events)
- result = service.list_events(
- period_start=now - timedelta(days=1),
- period_end=now + timedelta(days=1),
- page=1,
- page_size=1,
- )
- assert result["total"] == 2
- assert result["records"][0]["event_uid"] == "event-b"
- assert result["records"][0]["safe_detail"] == {
- "attempt": 2,
- "password": "[redacted]",
- "nested": {"authorization": "[redacted]"},
- }
- assert "must-not-leak" not in repr(result)
- assert "Bearer secret" not in repr(result)
- with pytest.raises(GovernanceAuditInvalid, match="page_size"):
- service.list_events(
- period_start=now - timedelta(days=1),
- period_end=now,
- page_size=101,
- )
- with pytest.raises(GovernanceAuditInvalid, match="category"):
- service.list_events(
- period_start=now - timedelta(days=1),
- period_end=now,
- categories=["arbitrary"],
- )
- def test_coverage_always_returns_the_six_fixed_categories():
- from app.core.system.governance_audit import AUDIT_CATEGORIES
- now = datetime(2026, 7, 30, 8, 0, tzinfo=UTC)
- service = _service(
- [_event("event-1", "authentication", occurred_at=now)]
- )
- coverage = service.coverage(
- period_start=now - timedelta(days=1),
- period_end=now + timedelta(seconds=1),
- )
- assert [item["category"] for item in coverage["categories"]] == list(
- AUDIT_CATEGORIES
- )
- assert coverage["categories"][0]["count"] == 1
- assert all(
- item["count"] == 0 for item in coverage["categories"][1:]
- )
- def test_signed_seal_is_stable_and_detects_changed_or_missing_events():
- now = datetime(2026, 7, 30, 8, 0, tzinfo=UTC)
- events = [
- _event("event-1", "authentication", occurred_at=now),
- _event("event-2", "ingestion", occurred_at=now + timedelta(seconds=1)),
- ]
- repository = FakeAuditRepository(events)
- from app.core.system.governance_audit import GovernanceAuditService
- service = GovernanceAuditService(
- repository,
- evidence_secret="wp12-test-secret-with-at-least-32-bytes",
- key_version="test-v1",
- uid_factory=lambda: "seal-1",
- now_factory=lambda: now + timedelta(days=1),
- )
- seal = service.create_seal(
- period_start=now - timedelta(seconds=1),
- period_end=now + timedelta(seconds=2),
- categories=["authentication", "ingestion"],
- actor_uid="admin-1",
- )
- assert seal["event_count"] == 2
- assert len(seal["root_hash"]) == 64
- assert len(seal["signature"]) == 64
- assert service.verify_seal("seal-1")["integrity_status"] == "intact"
- repository.events[1]["status"] = "failed"
- changed = service.verify_seal("seal-1")
- assert changed["integrity_status"] == "tampered"
- assert changed["actual_event_count"] == 2
- assert changed["actual_root_hash"] != seal["root_hash"]
- repository.events.pop()
- missing = service.verify_seal("seal-1")
- assert missing["integrity_status"] == "tampered"
- assert missing["actual_event_count"] == 1
- def test_seal_signature_rejects_modified_metadata_and_weak_secret():
- from app.core.system.governance_audit import (
- GovernanceAuditInvalid,
- GovernanceAuditService,
- )
- now = datetime(2026, 7, 30, 8, 0, tzinfo=UTC)
- repository = FakeAuditRepository(
- [_event("event-1", "authentication", occurred_at=now)]
- )
- service = GovernanceAuditService(
- repository,
- evidence_secret="wp12-test-secret-with-at-least-32-bytes",
- key_version="test-v1",
- uid_factory=lambda: "seal-1",
- now_factory=lambda: now + timedelta(days=1),
- )
- service.create_seal(
- period_start=now - timedelta(seconds=1),
- period_end=now + timedelta(seconds=1),
- actor_uid="admin-1",
- )
- repository.seals["seal-1"]["event_count"] = 99
- assert (
- service.verify_seal("seal-1")["integrity_status"]
- == "invalid_signature"
- )
- with pytest.raises(GovernanceAuditInvalid, match="32"):
- GovernanceAuditService(
- repository,
- evidence_secret="short",
- key_version="test-v1",
- )
- def test_seal_rejects_invalid_windows_and_unbounded_event_sets():
- from app.core.system.governance_audit import (
- GovernanceAuditInvalid,
- GovernanceAuditService,
- )
- now = datetime(2026, 7, 30, 8, 0, tzinfo=UTC)
- service = _service([])
- with pytest.raises(GovernanceAuditInvalid, match="period_end"):
- service.create_seal(
- period_start=now,
- period_end=now,
- actor_uid="admin-1",
- )
- class OversizedRepository(FakeAuditRepository):
- def fetch_events(self, **_kwargs):
- event = _event(
- "event-1", "authentication", occurred_at=now
- )
- return [event] * 50_001
- oversized = GovernanceAuditService(
- OversizedRepository(),
- evidence_secret="wp12-test-secret-with-at-least-32-bytes",
- key_version="test-v1",
- now_factory=lambda: now + timedelta(days=1),
- )
- with pytest.raises(GovernanceAuditInvalid, match="50,000"):
- oversized.create_seal(
- period_start=now - timedelta(seconds=1),
- period_end=now + timedelta(seconds=1),
- actor_uid="admin-1",
- )
- def test_seal_rejects_a_window_that_ends_in_the_future():
- from app.core.system.governance_audit import (
- GovernanceAuditInvalid,
- GovernanceAuditService,
- )
- now = datetime(2026, 7, 30, 8, 0, tzinfo=UTC)
- service = GovernanceAuditService(
- FakeAuditRepository(),
- evidence_secret="wp12-test-secret-with-at-least-32-bytes",
- key_version="test-v1",
- now_factory=lambda: now,
- )
- with pytest.raises(GovernanceAuditInvalid, match="future"):
- service.create_seal(
- period_start=now - timedelta(days=1),
- period_end=now + timedelta(seconds=1),
- actor_uid="admin-1",
- )
|