Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0260c-4067-7052-9647-ad000d576e38
123 lines
3.4 KiB
Python
123 lines
3.4 KiB
Python
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)
|