whitehat-security/src/whitehat_security/model.py
tegwick beab2a04d1 Build authorization-gated tenancy evidence harness
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0260c-4067-7052-9647-ad000d576e38
2026-08-21 23:53:27 +02:00

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)