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
No EOL
4.7 KiB
Python
152 lines
No EOL
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" |