Add value-safe verification and audit reporting
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
This commit is contained in:
parent
491e706a70
commit
c4504c6de9
19 changed files with 598 additions and 50 deletions
167
src/secrets_engine/audit.py
Normal file
167
src/secrets_engine/audit.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"""Allowlisted, non-secret summaries over local append-only evidence."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_LABEL = re.compile(r"^[a-z][a-z0-9-]{0,79}$")
|
||||
_DECISION_REF = re.compile(r"^[A-Z][A-Z0-9-]{2,80}$")
|
||||
_DELIVERY_RESULTS = {"delivered", "failed", "skipped-no-topic"}
|
||||
_VERIFY_RESULT = re.compile(r"^(positive|negative):(pass|fail)$")
|
||||
_ERROR_RESULT = re.compile(
|
||||
r"^failed-(Catalog|Decision|PolicyGuard|Backend|Provisioning|Verification|Delivery)Error$"
|
||||
)
|
||||
|
||||
|
||||
def _safe_label(value: object) -> str:
|
||||
text = value if isinstance(value, str) else ""
|
||||
return text if _LABEL.fullmatch(text) else "invalid-label"
|
||||
|
||||
|
||||
def _safe_result(value: object) -> str:
|
||||
text = value if isinstance(value, str) else ""
|
||||
if (
|
||||
_LABEL.fullmatch(text)
|
||||
or _VERIFY_RESULT.fullmatch(text)
|
||||
or _ERROR_RESULT.fullmatch(text)
|
||||
):
|
||||
return text
|
||||
return "invalid-label"
|
||||
|
||||
|
||||
def _safe_decision_ref(value: object) -> str:
|
||||
text = value if isinstance(value, str) else ""
|
||||
if not text:
|
||||
return ""
|
||||
try:
|
||||
return str(uuid.UUID(text))
|
||||
except ValueError:
|
||||
return text if _DECISION_REF.fullmatch(text) else ""
|
||||
|
||||
|
||||
def _safe_timestamp(value: object) -> str:
|
||||
if not isinstance(value, str):
|
||||
return ""
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
if parsed.tzinfo is None:
|
||||
return ""
|
||||
return parsed.astimezone(timezone.utc).isoformat()
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LaneAuditSummary:
|
||||
catalog_id: str
|
||||
operation_records: int
|
||||
malformed_records: int
|
||||
first_ts: str
|
||||
last_ts: str
|
||||
actions: dict[str, int]
|
||||
results: dict[str, int]
|
||||
decision_refs: list[str]
|
||||
session_cleanup: dict[str, int]
|
||||
hub_delivery: dict[str, int]
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
def render(self) -> str:
|
||||
lines = [
|
||||
f"Lane audit summary for '{self.catalog_id}'",
|
||||
f" operation records: {self.operation_records}",
|
||||
f" malformed records: {self.malformed_records}",
|
||||
f" first: {self.first_ts or 'n/a'}",
|
||||
f" last: {self.last_ts or 'n/a'}",
|
||||
" actions: " + _render_counts(self.actions),
|
||||
" results: " + _render_counts(self.results),
|
||||
" decisions: " + (", ".join(self.decision_refs) or "none"),
|
||||
" session cleanup: " + _render_counts(self.session_cleanup),
|
||||
" hub delivery: " + _render_counts(self.hub_delivery),
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _render_counts(counts: dict[str, int]) -> str:
|
||||
return ", ".join(f"{key}={value}" for key, value in sorted(counts.items())) or "none"
|
||||
|
||||
|
||||
def summarize_lane_evidence(evidence_dir: Path, catalog_id: str) -> LaneAuditSummary:
|
||||
"""Summarize one lane without returning arbitrary record fields or detail."""
|
||||
actions: Counter[str] = Counter()
|
||||
results: Counter[str] = Counter()
|
||||
cleanup: Counter[str] = Counter()
|
||||
hub_delivery: Counter[str] = Counter()
|
||||
decisions: set[str] = set()
|
||||
timestamps: list[str] = []
|
||||
malformed = 0
|
||||
operation_records = 0
|
||||
|
||||
for path in sorted(Path(evidence_dir).glob("evidence-*.jsonl")):
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except OSError:
|
||||
malformed += 1
|
||||
continue
|
||||
for line in lines:
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
malformed += 1
|
||||
continue
|
||||
if not isinstance(record, dict):
|
||||
malformed += 1
|
||||
continue
|
||||
if record.get("catalog_id") != catalog_id:
|
||||
continue
|
||||
action = _safe_label(record.get("action"))
|
||||
result = _safe_result(record.get("result"))
|
||||
if action == "evidence-delivery":
|
||||
if result in _DELIVERY_RESULTS:
|
||||
hub_delivery[result] += 1
|
||||
else:
|
||||
hub_delivery["invalid"] += 1
|
||||
continue
|
||||
|
||||
operation_records += 1
|
||||
actions[action] += 1
|
||||
results[result] += 1
|
||||
timestamp = _safe_timestamp(record.get("ts"))
|
||||
if timestamp:
|
||||
timestamps.append(timestamp)
|
||||
decision = _safe_decision_ref(record.get("decision_id"))
|
||||
if decision:
|
||||
decisions.add(decision)
|
||||
|
||||
detail = record.get("detail")
|
||||
session = detail.get("session") if isinstance(detail, dict) else None
|
||||
if isinstance(session, dict):
|
||||
attempted = session.get("revocation_attempted") is True
|
||||
succeeded = session.get("revocation_succeeded") is True
|
||||
if succeeded:
|
||||
cleanup["succeeded"] += 1
|
||||
elif attempted:
|
||||
cleanup["failed"] += 1
|
||||
else:
|
||||
cleanup["not-attempted"] += 1
|
||||
|
||||
timestamps.sort()
|
||||
return LaneAuditSummary(
|
||||
catalog_id=catalog_id,
|
||||
operation_records=operation_records,
|
||||
malformed_records=malformed,
|
||||
first_ts=timestamps[0] if timestamps else "",
|
||||
last_ts=timestamps[-1] if timestamps else "",
|
||||
actions=dict(sorted(actions.items())),
|
||||
results=dict(sorted(results.items())),
|
||||
decision_refs=sorted(decisions),
|
||||
session_cleanup=dict(sorted(cleanup.items())),
|
||||
hub_delivery=dict(sorted(hub_delivery.items())),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue