generated from coulomb/repo-seed
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.
152 lines
4.7 KiB
Python
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" |