kings-guard/src/kings_guard/acceptance.py
tegwick 96623fd46f Prepare independent deployed cadence acceptance verifier
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
2026-10-15 21:18:13 +02:00

242 lines
10 KiB
Python

"""Offline assessment of owner-delivered deployed Qonto evidence (no clients)."""
from __future__ import annotations
import argparse
import hashlib
import json
from collections import Counter
from pathlib import Path
from typing import Any
from kings_guard.cadence import parse_timestamp as _parse_timestamp
from kings_guard.live import _confirm_stream, _read_jsonl
def parse_timestamp(value: str):
if not isinstance(value, str) or not value.strip():
raise ValueError("timestamp must be a timezone-qualified string")
from datetime import datetime
if datetime.fromisoformat(value.replace("Z", "+00:00")).tzinfo is None:
raise ValueError("timestamp must include a timezone")
return _parse_timestamp(value)
def assess_capture(
events: list[dict[str, Any]],
snapshot: dict[str, Any],
receiver: dict[str, Any],
receipts: list[dict[str, Any]],
*,
source: str,
tenant: str,
frame_seconds: int,
now: str,
) -> dict[str, Any]:
"""Assess actual supplied evidence; caller provenance remains an owner claim.
Receiver time bounds are acceptance time, while source sequence bounds run
from startup to snapshot. Requiring one receipt per sequence links them.
"""
if type(frame_seconds) is not int or frame_seconds <= 0:
raise ValueError("frame_seconds must be a positive integer")
if not source or not tenant:
raise ValueError("exact source and tenant are required")
_, _, notes, corrections = _confirm_stream(events, snapshot)
findings = list(corrections)
now_dt = parse_timestamp(now)
instance = snapshot["stream_instance_id"]
times = [parse_timestamp(event["timestamp"]) for event in events]
snapshot_at = parse_timestamp(snapshot["snapshot_at"])
if times != sorted(times) or times[-1] > snapshot_at or snapshot_at > now_dt:
findings.append("source timestamps are unordered or outside snapshot/evaluation bounds")
beats = [event for event in events if event["event_class"] == "audit.heartbeat"]
if not beats or beats[0].get("reason") != "startup" or events[0] != beats[0]:
findings.append("capture does not start with the source startup heartbeat")
periodic = [event for event in beats if event.get("reason") == "periodic"]
if (
not periodic
or not beats
or not any(
(parse_timestamp(event["timestamp"]) - times[0]).total_seconds() >= frame_seconds
for event in periodic
)
):
findings.append("a real periodic heartbeat after one full frame is missing")
beat_times = [parse_timestamp(event["timestamp"]) for event in beats]
if any(
(b - a).total_seconds() > 2 * frame_seconds
for a, b in zip(beat_times, beat_times[1:], strict=False)
):
findings.append("source heartbeat gap exceeds registered two-frame wait")
if not any(event["event_class"] in {"audit.allow", "audit.deny"} for event in events):
findings.append("capture has no request transition")
if not any(event["event_class"] == "audit.deny" for event in events):
findings.append("capture has no denial transition")
stale = not beat_times or (now_dt - beat_times[-1]).total_seconds() > 3 * frame_seconds
if stale:
findings.append("process heartbeat is stale beyond effective three-frame deadline")
if any(
receiver.get(key) != value
for key, value in (("source", source), ("tenant", tenant), ("stream_instance_id", instance))
):
findings.append("receiver source/tenant/instance binding does not match")
since, until = parse_timestamp(receiver["since"]), parse_timestamp(receiver["until"])
if since > until or until > now_dt:
findings.append("receiver acceptance window is invalid or in the future")
rows = receiver.get("counts", [])
received: dict[str, int] = {}
for row in rows:
key, value = row["class"], row["count"]
if key in received or type(value) is not int or value < 0:
raise ValueError("receiver counts must be unique nonnegative integers")
received[key] = value
expected = Counter(
"audit-core.heartbeat" if e["event_class"] == "audit.heartbeat" else e["event_class"]
for e in events
)
if {k: v for k, v in received.items() if v} != dict(expected):
findings.append("receiver accepted counts diverge from captured instance")
by_sequence: dict[int, dict[str, Any]] = {}
ids: set[str] = set()
lags: list[float] = []
for receipt in receipts:
sequence = receipt["stream_sequence"]
if type(sequence) is not int or sequence in by_sequence or not 1 <= sequence <= len(events):
raise ValueError("receipt sequences must be unique and within the source capture")
by_sequence[sequence] = receipt
event = events[sequence - 1]
expected_type = (
"audit-core.heartbeat"
if event["event_class"] == "audit.heartbeat"
else event["event_class"]
)
accepted_at = parse_timestamp(receipt["accepted_at"])
event_id = receipt.get("event_id")
if not event_id or event_id in ids:
findings.append("receipt event ID is missing or duplicated")
ids.add(event_id)
if (
receipt.get("stream_instance_id") != instance
or receipt.get("source") != source
or receipt.get("tenant") != tenant
or receipt.get("type") != expected_type
or receipt.get("status_code") not in (200, 202)
or not since <= accepted_at <= until
):
findings.append("receipt binding/status/acceptance window is invalid")
if event["event_class"] == "audit.heartbeat" and receipt.get("covered_event_classes") != [
"audit.deny"
]:
findings.append("heartbeat receipt must retain exact audit.deny coverage")
lag = (accepted_at - times[sequence - 1]).total_seconds()
if receipt.get("status_code") != 202:
findings.append("first-acceptance 202 receipt is required to assess delivery lag")
else:
lags.append(lag)
if lag < 0 or lag > frame_seconds:
findings.append("delivery lag exceeds one frame or clocks are inconsistent")
if set(by_sequence) != set(range(1, len(events) + 1)):
findings.append("durable acceptance receipt missing for source sequence")
findings = list(dict.fromkeys(findings))
return {
"schema_version": "kings-guard-capture-assessment/v1",
"result": "pass" if not findings else "fail",
"findings": findings,
"source": source,
"tenant": tenant,
"stream_instance_id": instance,
"frame_seconds": frame_seconds,
"effective_deadline_seconds": 3 * frame_seconds,
"evaluated_at": now,
"max_delivery_lag_upper_bound_seconds": max(lags, default=None),
"source_event_count": len(events),
"notes": list(notes),
"scope": (
"supplied deployed capture only; provenance and missing-heartbeat "
"injection are separate owner receipts"
),
"means": (
"Receipt time bounds acceptance from above; count agreement does not "
"prove completeness or omitted event absence."
),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
for name in ("events", "snapshot", "receiver", "receipts"):
parser.add_argument(f"--{name}", required=True, type=Path)
parser.add_argument("--source", required=True)
parser.add_argument("--tenant", required=True)
parser.add_argument("--frame-seconds", required=True, type=int)
parser.add_argument("--now", required=True, help="Explicit timezone-qualified observation time")
args = parser.parse_args()
try:
result = assess_capture(
_read_jsonl(args.events),
json.loads(args.snapshot.read_text()),
json.loads(args.receiver.read_text()),
_read_jsonl(args.receipts),
source=args.source,
tenant=args.tenant,
frame_seconds=args.frame_seconds,
now=args.now,
)
result["input_sha256"] = {
name: hashlib.sha256(getattr(args, name).read_bytes()).hexdigest()
for name in ("events", "snapshot", "receiver", "receipts")
}
except (ValueError, KeyError, TypeError, IndexError, OSError) as exc:
result = {"result": "invalid", "error": str(exc)}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result["result"] == "pass" else 1
if __name__ == "__main__":
raise SystemExit(main())
def assess_class_silence(
*,
source: str,
sender: str,
registered_wait_seconds: dict[str, int],
last_seen: dict[str, str | None],
findings: list[dict[str, Any]],
now: str,
) -> dict[str, Any]:
"""Compare owner-provided receiver observations with independent class clocks.
This cannot attest that supplied observations came from production. Each
class needs an explicit last-seen entry (None is an observed never-seen
value). Healthy unrelated classes cannot cover a missing one.
"""
now_dt = parse_timestamp(now)
expected: set[str] = set()
for event_class, wait in registered_wait_seconds.items():
if type(wait) is not int or wait <= 0 or event_class not in last_seen:
raise ValueError("each registered class needs a positive wait and explicit last_seen")
value = last_seen[event_class]
stamp = parse_timestamp(value) if value is not None else None
if stamp is not None and stamp > now_dt:
raise ValueError("heartbeat last_seen cannot be in the future")
if stamp is None or (now_dt - stamp).total_seconds() > wait * 1.5:
expected.add(event_class)
actual = {
item["class"]
for item in findings
if item.get("source") == source
and item.get("sender") == sender
and item.get("kind") in {"missing_heartbeat", "no_heartbeat_since_registration"}
}
return {
"result": "pass" if actual == expected else "fail",
"source": source,
"sender": sender,
"evaluated_at": now,
"expected_missing_classes": sorted(expected),
"actual_missing_classes": sorted(actual),
"means": "Independent evaluation of supplied class clocks; no suppression inference.",
}