2026-06-28 12:28:45 +02:00
|
|
|
"""Non-secret evidence writer.
|
|
|
|
|
|
|
|
|
|
Every privileged or noteworthy action emits an evidence record to a local
|
|
|
|
|
append-only JSONL log and, best-effort, to the State Hub progress API. Records
|
|
|
|
|
are scrubbed of anything that looks like a secret value before they are written.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import urllib.error
|
|
|
|
|
import urllib.request
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from secrets_engine.redact import looks_secret, redact_text
|
|
|
|
|
|
|
|
|
|
# Keys that must never carry a value into evidence regardless of nesting.
|
|
|
|
|
_FORBIDDEN_VALUE_KEYS = {"value", "secret", "token", "password", "raw"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _scrub(obj: Any) -> Any:
|
|
|
|
|
"""Recursively drop secret-looking keys and redact token shapes in strings."""
|
|
|
|
|
if isinstance(obj, dict):
|
|
|
|
|
out = {}
|
|
|
|
|
for k, v in obj.items():
|
|
|
|
|
if k.lower() in _FORBIDDEN_VALUE_KEYS or looks_secret(k):
|
|
|
|
|
out[k] = "<omitted: non-secret evidence only>"
|
|
|
|
|
else:
|
|
|
|
|
out[k] = _scrub(v)
|
|
|
|
|
return out
|
|
|
|
|
if isinstance(obj, list):
|
|
|
|
|
return [_scrub(v) for v in obj]
|
|
|
|
|
if isinstance(obj, str):
|
|
|
|
|
return redact_text(obj)
|
|
|
|
|
return obj
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class EvidenceWriter:
|
|
|
|
|
evidence_dir: Path
|
|
|
|
|
hub_url: str = ""
|
|
|
|
|
topic_id: str = ""
|
|
|
|
|
workstream_id: str = ""
|
|
|
|
|
author: str = "secrets-engine"
|
|
|
|
|
actor: str = field(default_factory=lambda: os.environ.get("USER", "unknown"))
|
|
|
|
|
|
|
|
|
|
def __post_init__(self) -> None:
|
|
|
|
|
self.evidence_dir = Path(self.evidence_dir)
|
|
|
|
|
|
|
|
|
|
def _log_path(self) -> Path:
|
|
|
|
|
self.evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
day = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
|
|
|
return self.evidence_dir / f"evidence-{day}.jsonl"
|
|
|
|
|
|
|
|
|
|
def record(
|
|
|
|
|
self,
|
|
|
|
|
action: str,
|
|
|
|
|
*,
|
|
|
|
|
result: str,
|
|
|
|
|
catalog_id: str = "",
|
|
|
|
|
stage: str = "",
|
|
|
|
|
decision_id: str = "",
|
|
|
|
|
detail: dict[str, Any] | None = None,
|
|
|
|
|
hub: bool = True,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Append one non-secret evidence record. Returns the stored record."""
|
|
|
|
|
record = {
|
|
|
|
|
"ts": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
"action": action,
|
|
|
|
|
"result": result,
|
|
|
|
|
"actor": self.actor,
|
|
|
|
|
"catalog_id": catalog_id,
|
|
|
|
|
"stage": stage,
|
|
|
|
|
"decision_id": decision_id,
|
|
|
|
|
"detail": _scrub(detail or {}),
|
|
|
|
|
}
|
|
|
|
|
path = self._log_path()
|
|
|
|
|
with path.open("a", encoding="utf-8") as fh:
|
|
|
|
|
fh.write(json.dumps(record, sort_keys=True) + "\n")
|
|
|
|
|
if hub and self.hub_url:
|
|
|
|
|
self._post_hub(action, result, catalog_id, stage, decision_id)
|
|
|
|
|
return record
|
|
|
|
|
|
|
|
|
|
def _post_hub(
|
|
|
|
|
self, action: str, result: str, catalog_id: str, stage: str, decision_id: str
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Best-effort progress note to State Hub. Never raises; never sends values."""
|
|
|
|
|
if not self.topic_id:
|
|
|
|
|
return
|
|
|
|
|
summary = f"secrets-engine {action}: {result}"
|
|
|
|
|
if catalog_id:
|
|
|
|
|
summary += f" [{catalog_id}{'/' + stage if stage else ''}]"
|
|
|
|
|
payload: dict[str, Any] = {
|
|
|
|
|
"topic_id": self.topic_id,
|
|
|
|
|
"event_type": "note",
|
|
|
|
|
"summary": summary,
|
|
|
|
|
"author": self.author,
|
|
|
|
|
}
|
|
|
|
|
if self.workstream_id:
|
2026-07-09 00:32:54 +02:00
|
|
|
payload["workplan_id"] = self.workstream_id
|
2026-06-28 12:28:45 +02:00
|
|
|
if decision_id:
|
|
|
|
|
payload["detail"] = {"decision_id": decision_id, "catalog_id": catalog_id}
|
|
|
|
|
try:
|
|
|
|
|
req = urllib.request.Request(
|
|
|
|
|
self.hub_url.rstrip("/") + "/progress/",
|
|
|
|
|
data=json.dumps(payload).encode(),
|
|
|
|
|
headers={"Content-Type": "application/json"},
|
|
|
|
|
method="POST",
|
|
|
|
|
)
|
|
|
|
|
urllib.request.urlopen(req, timeout=3).read()
|
|
|
|
|
except (urllib.error.URLError, OSError, ValueError):
|
|
|
|
|
# Hub being offline must never block secret work or leak anything.
|
|
|
|
|
pass
|