from __future__ import annotations import hashlib import json from dataclasses import asdict, dataclass, field from datetime import UTC, datetime from typing import Any, Literal Outcome = Literal["pass", "finding", "inconclusive", "aborted"] PASS_LIMIT = ( "Pass means only that the attacks attempted in this run did not work; " "it is not proof that the tenant boundary always holds." ) def utc_now() -> str: return datetime.now(UTC).isoformat().replace("+00:00", "Z") def canonical(value: Any) -> bytes: return json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode() def shape(value: Any, prefix: str = "$") -> list[str]: """Return schema paths only; never return scalar values.""" if isinstance(value, dict): paths = [prefix] for key in sorted(value): paths.extend(shape(value[key], f"{prefix}.{key}")) return paths if isinstance(value, list): paths = [f"{prefix}[]"] for item in value[:1]: paths.extend(shape(item, f"{prefix}[]")) return paths return [f"{prefix}:{type(value).__name__}"] def scalar_values(value: Any) -> list[str]: if isinstance(value, dict): return [item for child in value.values() for item in scalar_values(child)] if isinstance(value, list): return [item for child in value for item in scalar_values(child)] if value is None: return [] return [str(value)] def item_count(value: Any) -> int: if isinstance(value, list): return len(value) if isinstance(value, dict): for key in ("items", "events", "results", "rows"): if isinstance(value.get(key), list): return len(value[key]) return 1 if value else 0 return 1 if value is not None else 0 @dataclass(frozen=True) class Response: status: int body: Any content_type: str = "application/json" @dataclass(frozen=True) class Observation: status: int content_type: str count: int schema: list[str] run_digest: str fixture_match_count: int def observe(response: Response, *, salt: bytes, fixture_markers: tuple[str, ...]) -> Observation: values = set(scalar_values(response.body)) return Observation( status=response.status, content_type=response.content_type.split(";", 1)[0].strip().lower(), count=item_count(response.body), schema=shape(response.body), run_digest=hashlib.sha256(salt + canonical(response.body)).hexdigest(), fixture_match_count=sum( any(marker in value for value in values) for marker in fixture_markers ), ) @dataclass class ProbeResult: probe_id: str operation: str outcome: Outcome reasons: list[str] observations: dict[str, Observation] @dataclass class RunReport: schema_version: str run_id: str evidence_class: Literal["fixture", "target"] engagement_id: str authorization_id: str target: str target_revision: str posture_claim: str attacker_model: str started_at: str ended_at: str outcome: Outcome attempted_operations: int cleanup: str credential_revocation: str probes: list[ProbeResult] = field(default_factory=list) limitations: list[str] = field(default_factory=list) assurance_statement: str = PASS_LIMIT def as_dict(self) -> dict[str, Any]: return asdict(self)