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
67
src/whitehat_security/differential.py
Normal file
67
src/whitehat_security/differential.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from .engagement import Engagement
|
||||
from .model import Observation, ProbeResult, Response, observe
|
||||
|
||||
Request = Callable[[], Response]
|
||||
State = Callable[[], object]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DifferentialProbe:
|
||||
probe_id: str
|
||||
operation: str
|
||||
owner_request: Request
|
||||
attacker_request: Request
|
||||
absent_request: Request
|
||||
fixture_markers: tuple[str, ...]
|
||||
state: State | None = None
|
||||
hide_existence: bool = True
|
||||
|
||||
|
||||
def execute(probe: DifferentialProbe, *, salt: bytes | None = None) -> ProbeResult:
|
||||
"""Run one cold probe, retaining response bodies only in this stack frame."""
|
||||
run_salt = salt or os.urandom(32)
|
||||
before = probe.state() if probe.state else None
|
||||
owner = observe(probe.owner_request(), salt=run_salt, fixture_markers=probe.fixture_markers)
|
||||
absent = observe(probe.absent_request(), salt=run_salt, fixture_markers=probe.fixture_markers)
|
||||
attacker = observe(probe.attacker_request(), salt=run_salt, fixture_markers=probe.fixture_markers)
|
||||
after = probe.state() if probe.state else None
|
||||
|
||||
reasons: list[str] = []
|
||||
if attacker.fixture_match_count:
|
||||
reasons.append("attacker response contains a tenant-B fixture marker")
|
||||
if before != after:
|
||||
reasons.append("tenant-B fixture state changed")
|
||||
if probe.hide_existence and not _equivalent_denial(attacker, absent):
|
||||
reasons.append("attacker response differs from the absent-object reference")
|
||||
|
||||
return ProbeResult(
|
||||
probe_id=probe.probe_id,
|
||||
operation=probe.operation,
|
||||
outcome="finding" if reasons else "pass",
|
||||
reasons=reasons,
|
||||
observations={"owner": owner, "attacker": attacker, "absent": absent},
|
||||
)
|
||||
|
||||
|
||||
def execute_authorized(probe: DifferentialProbe, *, engagement: Engagement,
|
||||
route: str, salt: bytes | None = None) -> ProbeResult:
|
||||
"""Live entry point: authorization is rechecked immediately before calls."""
|
||||
engagement.validate()
|
||||
engagement.permits(technique="e2-differential", route=route)
|
||||
return execute(probe, salt=salt)
|
||||
|
||||
|
||||
def _equivalent_denial(left: Observation, right: Observation) -> bool:
|
||||
return (
|
||||
left.status == right.status
|
||||
and left.content_type == right.content_type
|
||||
and left.count == right.count
|
||||
and left.schema == right.schema
|
||||
and left.run_digest == right.run_digest
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue