Files
ops-warden/tests/test_audit.py
tegwick d6088e4e16 Implement WP-0022 audit trail and WP-0023 INTENT–SCOPE closeout
Add unified metadata-only audit.jsonl with secret-material guard, instrument
sign/access/worker paths, and expose warden activity CLI. Surface broker hint
when VAULT_TOKEN is unset, refresh INTENT/SCOPE docs, and add production
integration checklists plus catalog lane promotion playbook.
2026-07-01 23:32:38 +02:00

152 lines
4.7 KiB
Python

"""Tests for unified audit trail (WARDEN-WP-0022)."""
from __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch
import pytest
from typer.testing import CliRunner
from warden.audit import (
AuditError,
collect_activity,
fetch_hub_notes,
read_events,
record_event,
)
from warden.cli import app
runner = CliRunner()
def test_record_and_read_event(tmp_path: Path) -> None:
record_event(
tmp_path,
kind="sign",
action="issue",
subject="agt-test",
target="agt-test",
decision_id="dec-1",
backend="local",
)
events = read_events(tmp_path)
assert len(events) == 1
assert events[0]["kind"] == "sign"
assert events[0]["subject"] == "agt-test"
assert events[0]["decision_id"] == "dec-1"
def test_read_events_filters_by_kind_and_since(tmp_path: Path) -> None:
record_event(tmp_path, kind="sign", action="issue", subject="a", target="a")
record_event(tmp_path, kind="access", action="fetch", subject="op", target="need-1")
since = datetime.now(timezone.utc) - timedelta(hours=1)
sign_only = read_events(tmp_path, since=since, kinds={"sign"})
assert len(sign_only) == 1
assert sign_only[0]["kind"] == "sign"
def test_secret_guard_rejects_token_prefix(tmp_path: Path) -> None:
with pytest.raises(AuditError, match="secret"):
record_event(
tmp_path,
kind="access",
action="fetch",
subject="ghp_abc123456789012345678901234567890",
target="need",
)
def test_secret_guard_rejects_high_entropy(tmp_path: Path) -> None:
with pytest.raises(AuditError, match="high-entropy"):
record_event(
tmp_path,
kind="access",
action="fetch",
subject="operator",
target="need",
note="9f3a8c2d1b0e7f6a5c4d3b2a1f0e9d8c7b6a5948372615049382716059483",
)
def test_rotation_when_log_exceeds_limit(tmp_path: Path, monkeypatch) -> None:
import warden.audit as audit_mod
monkeypatch.setattr(audit_mod, "_MAX_BYTES", 50)
for i in range(5):
record_event(tmp_path, kind="worker", action="tick", subject="worker", target=str(i))
assert (tmp_path / "audit.jsonl").exists()
assert (tmp_path / "audit.jsonl.1").exists()
def test_collect_activity_merges_legacy_logs(tmp_path: Path) -> None:
ts = datetime.now(timezone.utc).isoformat()
(tmp_path / "signatures.log").write_text(
json.dumps(
{
"timestamp": ts,
"actor": "agt-legacy",
"actor_type": "agt",
"backend": "vault",
}
)
+ "\n"
)
(tmp_path / "access-audit.log").write_text(
json.dumps(
{
"timestamp": ts,
"action": "fetch",
"need_id": "openbao-api-key",
"owner_repo": "railiance-platform",
"subject": "operator",
"exit_code": 0,
}
)
+ "\n"
)
events = collect_activity(tmp_path, days=7)
kinds = {e["kind"] for e in events}
assert "sign" in kinds
assert "access" in kinds
assert any(e.get("source") == "signatures.log" for e in events)
def test_fetch_hub_notes_filters_ops_warden(tmp_path: Path) -> None:
payload = [
{
"created_at": datetime.now(timezone.utc).isoformat(),
"summary": "ops-warden: worker tick complete",
"author": "codex",
"event_type": "note",
},
{
"created_at": datetime.now(timezone.utc).isoformat(),
"summary": "unrelated repo change",
"author": "codex",
"event_type": "note",
},
]
with patch("httpx.get") as mock_get:
mock_get.return_value.raise_for_status = lambda: None
mock_get.return_value.json.return_value = payload
notes = fetch_hub_notes(days=7, hub_url="http://127.0.0.1:8000")
assert len(notes) == 1
assert notes[0]["kind"] == "hub"
def test_activity_cli_json(tmp_path: Path, monkeypatch) -> None:
state_dir = tmp_path / "state"
state_dir.mkdir()
cfg = tmp_path / "warden.yaml"
cfg.write_text(f"backend: local\nca_key: {tmp_path / 'ca'}\nstate_dir: {state_dir}\n")
(tmp_path / "ca").write_text("fake")
monkeypatch.setenv("WARDEN_CONFIG", str(cfg))
record_event(state_dir, kind="sign", action="issue", subject="agt-cli", target="agt-cli")
result = runner.invoke(app, ["activity", "--days", "1", "--json"])
assert result.exit_code == 0
data = json.loads(result.stdout)
assert isinstance(data, list)
assert data[0]["kind"] == "sign"