Build authorization-gated tenancy evidence harness
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0260c-4067-7052-9647-ad000d576e38
This commit is contained in:
parent
2c8e1d41ad
commit
beab2a04d1
32 changed files with 1816 additions and 11 deletions
123
src/whitehat_security/model.py
Normal file
123
src/whitehat_security/model.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue