generated from coulomb/repo-seed
feat(warden): implement WARDEN-WP-0002 correctness and operational completeness
T1 — TTL max enforcement: - models.py: MAX_TTL_HOURS policy constant - ca.py: _enforce_ttl() raises CAError when spec.ttl_hours > type max - Called at top of LocalCA.sign() and VaultCA.sign() - scorecard.py: check_ttl_policy() — flags certs with issued TTL > type max - run_scorecard() now returns 5 checks T2 — Stale cert cleanup: - ca.py: _evict_cert() removes existing cert before writing new one (no accumulation) - cli.py: warden cleanup [actor] [--dry-run] command - check_no_stale_certs detail suggests 'warden cleanup' when stale certs found T3 — Outgoing signatures log: - ca.py: _append_signature_log() writes JSONL to state_dir/signatures.log - Called after every successful sign() in LocalCA and VaultCA - cli.py: warden log [actor] [--last N] [--json] command - parse_cert_metadata now also returns valid_from (needed for TTL policy check) 61 tests passing, ruff clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Annotated, List, Optional
|
||||
|
||||
@@ -394,3 +394,105 @@ def inventory_remove(
|
||||
raise typer.Exit(1)
|
||||
|
||||
console.print(f"[green]Removed[/green] {actor_name}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# warden cleanup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.command()
|
||||
def cleanup(
|
||||
actor_name: Annotated[Optional[str], typer.Argument(help="Actor name (omit for all)")] = None,
|
||||
dry_run: Annotated[bool, typer.Option("--dry-run", help="Preview without deleting")] = False,
|
||||
) -> None:
|
||||
"""Remove stale (expired > 5 min) certificates from state dir."""
|
||||
cfg = _load_cfg()
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=5)
|
||||
|
||||
if actor_name:
|
||||
cert_path = cfg.state_dir / f"{actor_name}-cert.pub"
|
||||
paths = [cert_path] if cert_path.exists() else []
|
||||
else:
|
||||
paths = sorted(cfg.state_dir.glob("*-cert.pub")) if cfg.state_dir.exists() else []
|
||||
|
||||
removed = []
|
||||
for cert_path in paths:
|
||||
try:
|
||||
meta = parse_cert_metadata(cert_path)
|
||||
except Exception:
|
||||
continue
|
||||
if meta["valid_before"] < cutoff:
|
||||
if dry_run:
|
||||
console.print(f"would remove: {cert_path.name}")
|
||||
else:
|
||||
cert_path.unlink()
|
||||
console.print(f"removed: {cert_path.name}")
|
||||
removed.append(cert_path.name)
|
||||
|
||||
if not removed:
|
||||
console.print("No stale certificates found.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# warden log
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.command()
|
||||
def log(
|
||||
actor_name: Annotated[Optional[str], typer.Argument(help="Filter by actor name")] = None,
|
||||
last: Annotated[int, typer.Option("--last", help="Show last N entries")] = 20,
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""Show outgoing certificate signing history."""
|
||||
cfg = _load_cfg()
|
||||
log_path = cfg.state_dir / "signatures.log"
|
||||
|
||||
if not log_path.exists():
|
||||
if output_json:
|
||||
print("[]")
|
||||
else:
|
||||
console.print("No signatures log found.")
|
||||
return
|
||||
|
||||
entries = []
|
||||
for line in log_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if actor_name and entry.get("actor") != actor_name:
|
||||
continue
|
||||
entries.append(entry)
|
||||
|
||||
entries = entries[-last:]
|
||||
|
||||
if output_json:
|
||||
print(json.dumps(entries, indent=2))
|
||||
return
|
||||
|
||||
if not entries:
|
||||
console.print("No matching log entries.")
|
||||
return
|
||||
|
||||
table = Table(title="Signatures Log")
|
||||
table.add_column("Timestamp")
|
||||
table.add_column("Actor")
|
||||
table.add_column("Type")
|
||||
table.add_column("Identity")
|
||||
table.add_column("TTL (h)")
|
||||
table.add_column("Valid Before (UTC)")
|
||||
table.add_column("Backend")
|
||||
for e in entries:
|
||||
table.add_row(
|
||||
e.get("timestamp", "")[:19],
|
||||
e.get("actor", ""),
|
||||
e.get("actor_type", ""),
|
||||
e.get("identity", ""),
|
||||
str(e.get("ttl_hours", "")),
|
||||
e.get("valid_before", "")[:19],
|
||||
e.get("backend", ""),
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
Reference in New Issue
Block a user