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:
tegwick 2026-08-21 23:53:27 +02:00
parent 2c8e1d41ad
commit beab2a04d1
32 changed files with 1816 additions and 11 deletions

View file

@ -0,0 +1,4 @@
"""Adversarial evidence tools for the NetKingdom tenancy posture."""
__version__ = "0.1.0"

View file

@ -0,0 +1,73 @@
from __future__ import annotations
from dataclasses import dataclass
from .model import Outcome
@dataclass(frozen=True)
class CapacitySample:
consumer: str
latency_ms: float
error_rate: float
throughput_per_second: float
@dataclass(frozen=True)
class CapacityResult:
outcome: Outcome
governor_bound: bool
aggressor_peak: float
aggressor_ceiling: float
neighbour_degradation: dict[str, dict[str, float]]
reasons: list[str]
def characterize(*, baseline: list[CapacitySample], loaded: list[CapacitySample],
governor_bound: bool, aggressor_peak: float,
aggressor_ceiling: float) -> CapacityResult:
before = {sample.consumer: sample for sample in baseline}
after = {sample.consumer: sample for sample in loaded}
reasons: list[str] = []
if not governor_bound:
reasons.append("declared governor did not bind")
if aggressor_peak > aggressor_ceiling:
reasons.append("probe exceeded its authorized allowance")
missing = sorted(before.keys() - after.keys())
if missing:
reasons.append("loaded samples missing consumers: " + ", ".join(missing))
degradation: dict[str, dict[str, float]] = {}
for consumer in sorted(before.keys() & after.keys()):
old, new = before[consumer], after[consumer]
degradation[consumer] = {
"latency_increase_percent": _change(old.latency_ms, new.latency_ms),
"error_rate_increase_points": new.error_rate - old.error_rate,
"throughput_decrease_percent": _decrease(old.throughput_per_second,
new.throughput_per_second),
}
outcome: Outcome = "pass"
if aggressor_peak > aggressor_ceiling:
outcome = "aborted"
elif reasons:
outcome = "finding"
return CapacityResult(
outcome=outcome,
governor_bound=governor_bound,
aggressor_peak=aggressor_peak,
aggressor_ceiling=aggressor_ceiling,
neighbour_degradation=degradation,
reasons=reasons,
)
def _change(before: float, after: float) -> float:
if before == 0:
return 0.0 if after == 0 else float("inf")
return round((after - before) / before * 100, 3)
def _decrease(before: float, after: float) -> float:
if before == 0:
return 0.0
return round((before - after) / before * 100, 3)

View file

@ -0,0 +1,123 @@
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path
from .capacity import CapacitySample, characterize
from .differential import execute
from .e3 import CADENCE, PROBES
from .engagement import Engagement
from .fixtures import FixtureService, probe_suite
from .model import RunReport, utc_now
from .reporting import risk_nexus_message
def fixture_calibration() -> dict:
started = utc_now()
good = [execute(probe) for probe in probe_suite(FixtureService(enforce_tenant=True))]
bad = [execute(probe) for probe in probe_suite(FixtureService(enforce_tenant=False))]
detected = all(result.outcome == "finding" for result in bad)
rejected = all(result.outcome == "pass" for result in good)
return {
"schema_version": "whitehat-calibration/v1",
"evidence_class": "fixture",
"run_id": f"fixture-calibration-{started}",
"started_at": started,
"ended_at": utc_now(),
"outcome": "pass" if detected and rejected else "finding",
"expected": {"known_good": "pass", "known_bad": "finding"},
"known_good": [asdict(result) for result in good],
"known_bad": [asdict(result) for result in bad],
"limitations": [
"Offline fixture evidence calibrates the harness; it is not target assurance.",
"No network request, database connection, or live credential was used.",
],
}
def capacity_fixture() -> dict:
baseline = [
CapacitySample("aggressor", 10, 0, 100),
CapacitySample("neighbour", 12, 0, 80),
]
loaded = [
CapacitySample("aggressor", 25, 0.01, 120),
CapacitySample("neighbour", 18, 0.02, 60),
]
return asdict(characterize(
baseline=baseline, loaded=loaded, governor_bound=True,
aggressor_peak=10, aggressor_ceiling=10,
))
def validate_pack(path: Path) -> None:
data = json.loads(path.read_text(encoding="utf-8"))
required = {"schema_version", "target", "posture_claim", "attacker_model", "probes"}
missing = sorted(required - data.keys())
if missing:
raise ValueError(f"{path}: missing {', '.join(missing)}")
ids: set[str] = set()
for probe in data["probes"]:
for key in ("id", "operation", "route", "owner", "attacker", "absent"):
if key not in probe:
raise ValueError(f"{path}: probe missing {key}")
if probe["id"] in ids:
raise ValueError(f"{path}: duplicate probe id {probe['id']}")
ids.add(probe["id"])
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(prog="whitehat")
commands = parser.add_subparsers(dest="command", required=True)
fixtures = commands.add_parser("fixtures", help="calibrate E2 probes offline")
fixtures.add_argument("--output")
engagement = commands.add_parser("validate-engagement")
engagement.add_argument("path")
packs = commands.add_parser("validate-packs")
packs.add_argument("path")
commands.add_parser("e3-plan")
commands.add_parser("capacity-fixture")
message = commands.add_parser("risk-message")
message.add_argument("report")
args = parser.parse_args(argv)
if args.command == "fixtures":
result = fixture_calibration()
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
if args.output:
Path(args.output).write_text(rendered, encoding="utf-8")
else:
print(rendered, end="")
raise SystemExit(0 if result["outcome"] == "pass" else 1)
if args.command == "validate-engagement":
record = Engagement.load(args.path)
print(f"authorized: {record.raw['engagement_id']}")
return
if args.command == "validate-packs":
paths = sorted(Path(args.path).glob("*.json"))
if not paths:
raise SystemExit("no probe packs found")
for path in paths:
validate_pack(path)
print(f"validated {len(paths)} probe packs")
return
if args.command == "e3-plan":
print(json.dumps({"cadence": CADENCE, "probes": [asdict(probe) for probe in PROBES]},
indent=2, sort_keys=True))
return
if args.command == "capacity-fixture":
print(json.dumps(capacity_fixture(), indent=2, sort_keys=True))
return
if args.command == "risk-message":
report = RunReport(**json.loads(Path(args.report).read_text(encoding="utf-8")))
print(risk_nexus_message(report), end="")
return
raise SystemExit(2)
if __name__ == "__main__":
main(sys.argv[1:])

View 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
)

View file

@ -0,0 +1,61 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Literal
from .model import Outcome
Expectation = Literal["zero_rows", "statement_rejected", "false", "documented_limit"]
Query = Callable[[str], object]
@dataclass(frozen=True)
class E3Probe:
probe_id: str
sql_key: str
expectation: Expectation
@dataclass(frozen=True)
class E3Result:
probe_id: str
outcome: Outcome
reason: str
PROBES = (
E3Probe("conformance-view-empty", "conformance", "zero_rows"),
E3Probe("unset-guc-reads-none", "unset_guc", "zero_rows"),
E3Probe("tenant-a-cannot-read-b", "wrong_tenant_read", "zero_rows"),
E3Probe("tenant-a-cannot-insert-b", "wrong_tenant_insert", "statement_rejected"),
E3Probe("runtime-lacks-bypassrls", "runtime_bypassrls", "false"),
E3Probe("unsafe-definer-inventory-empty", "unsafe_definer", "zero_rows"),
E3Probe("sql-compromise-reset", "reset_to_b", "documented_limit"),
)
def evaluate(probe: E3Probe, *, rows: int = 0, rejected: bool = False,
boolean: bool | None = None) -> E3Result:
if probe.expectation == "documented_limit":
return E3Result(probe.probe_id, "inconclusive",
"E3-B observation records the documented SQL-compromise limit")
passed = {
"zero_rows": rows == 0,
"statement_rejected": rejected,
"false": boolean is False,
}[probe.expectation]
if passed:
return E3Result(probe.probe_id, "pass", f"expectation met: {probe.expectation}")
return E3Result(probe.probe_id, "finding", f"expectation failed: {probe.expectation}")
CADENCE = {
"interval": "24h",
"maximum_detection_window": "24h plus run and reporting latency",
"reset_triggers": [
"schema migration", "role or grant change", "RLS policy change",
"security-definer function change", "posture mechanism change",
],
"triggered_run_deadline": "before deployment promotion",
}

View file

@ -0,0 +1,83 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
class AuthorizationError(ValueError):
pass
REQUIRED = {
"engagement_id", "authorization_id", "authorizer", "approved_at", "expires_at",
"target", "target_owner", "environment", "source", "routes", "fixture_ids",
"credential_lane", "credential_role", "credential_max_ttl_seconds", "techniques",
"prohibited_techniques", "rate_limit_per_minute", "max_concurrency", "window_start",
"window_end", "operator_contact", "abort_contact", "posture_claim", "attacker_model",
"finding_destination", "target_owner_acknowledged_at",
}
@dataclass(frozen=True)
class Engagement:
raw: dict[str, Any]
@classmethod
def load(cls, path: str | Path, *, now: datetime | None = None) -> "Engagement":
data = json.loads(Path(path).read_text(encoding="utf-8"))
missing = sorted(REQUIRED - data.keys())
if missing:
raise AuthorizationError(f"incomplete engagement; missing: {', '.join(missing)}")
engagement = cls(data)
engagement.validate(now=now)
return engagement
def validate(self, *, now: datetime | None = None) -> None:
current = now or datetime.now(UTC)
start = _timestamp(self.raw["window_start"])
end = _timestamp(self.raw["window_end"])
expiry = _timestamp(self.raw["expires_at"])
approved = _timestamp(self.raw["approved_at"])
acknowledged = _timestamp(self.raw["target_owner_acknowledged_at"])
if not approved <= current <= min(end, expiry):
raise AuthorizationError("engagement is outside its approved time/expiry window")
if current < start:
raise AuthorizationError("engagement window has not started")
if acknowledged < approved:
raise AuthorizationError("target-owner acknowledgement predates approval")
if acknowledged > current:
raise AuthorizationError("target-owner acknowledgement is in the future")
if start < approved:
raise AuthorizationError("engagement window starts before approval")
if self.raw["max_concurrency"] != 1:
raise AuthorizationError("v0.1 permits exactly one in-flight API/database operation")
if not 1 <= self.raw["rate_limit_per_minute"] <= 60:
raise AuthorizationError("API rate ceiling must be between 1 and 60 per minute")
if self.raw["environment"] == "production" and not self.raw.get("production_approval"):
raise AuthorizationError("production requires production-specific approval")
if self.raw["finding_destination"] != "risk-nexus":
raise AuthorizationError("finding destination must be risk-nexus")
parsed = urlparse(self.raw["target"])
if parsed.scheme and parsed.scheme not in {"http", "https", "postgresql"}:
raise AuthorizationError("unsupported target scheme")
if not self.raw["fixture_ids"]:
raise AuthorizationError("at least one disposable fixture id is required")
if not 1 <= self.raw["credential_max_ttl_seconds"] <= 3600:
raise AuthorizationError("credential TTL must be between 1 and 3600 seconds")
def permits(self, *, technique: str, route: str) -> None:
if technique not in self.raw["techniques"]:
raise AuthorizationError(f"technique not authorized: {technique}")
if route not in self.raw["routes"]:
raise AuthorizationError(f"route not authorized: {route}")
def _timestamp(value: str) -> datetime:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise AuthorizationError("engagement timestamps must include a timezone")
return parsed.astimezone(UTC)

View file

@ -0,0 +1,91 @@
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from .differential import DifferentialProbe
from .model import Response
@dataclass
class FixtureService:
enforce_tenant: bool
def __post_init__(self) -> None:
self.objects = {
"object-a": {"id": "object-a", "tenant": "tenant-a", "label": "fixture-a"},
"object-b": {"id": "object-b", "tenant": "tenant-b", "label": "fixture-b"},
}
def snapshot_b(self) -> object:
return deepcopy(self.objects.get("object-b"))
def read(self, actor_tenant: str, object_id: str) -> Response:
item = self.objects.get(object_id)
if item is None or (self.enforce_tenant and item["tenant"] != actor_tenant):
return Response(404, {"error": "not_found"})
return Response(200, deepcopy(item))
def list_for(self, actor_tenant: str, requested_tenant: str) -> Response:
tenant = actor_tenant if self.enforce_tenant else requested_tenant
items = [deepcopy(item) for item in self.objects.values() if item["tenant"] == tenant]
return Response(200, {"items": items})
def create_for(self, actor_tenant: str, requested_tenant: str, object_id: str) -> Response:
if self.enforce_tenant and actor_tenant != requested_tenant:
return Response(403, {"error": "forbidden"})
self.objects[object_id] = {
"id": object_id,
"tenant": requested_tenant,
"label": "fixture-created",
}
return Response(201, {"id": object_id, "tenant": requested_tenant})
def update(self, actor_tenant: str, object_id: str) -> Response:
item = self.objects.get(object_id)
if item is None or (self.enforce_tenant and item["tenant"] != actor_tenant):
return Response(404, {"error": "not_found"})
item["label"] = "fixture-mutated"
return Response(200, deepcopy(item))
def delete(self, actor_tenant: str, object_id: str) -> Response:
item = self.objects.get(object_id)
if item is None or (self.enforce_tenant and item["tenant"] != actor_tenant):
return Response(404, {"error": "not_found"})
del self.objects[object_id]
return Response(200, {"deleted": object_id})
def probe_suite(service: FixtureService) -> list[DifferentialProbe]:
markers = ("object-b", "tenant-b", "fixture-b", "fixture-mutated")
return [
DifferentialProbe(
"read-by-id", "read", lambda: service.read("tenant-b", "object-b"),
lambda: service.read("tenant-a", "object-b"),
lambda: service.read("tenant-a", "absent"), markers,
),
DifferentialProbe(
"list-filter", "read", lambda: service.list_for("tenant-b", "tenant-b"),
lambda: service.list_for("tenant-a", "tenant-b"),
lambda: Response(200, {"items": []}), markers, hide_existence=False,
),
DifferentialProbe(
"create-attributed-to-b", "create",
lambda: Response(201, {"id": "owner-control"}),
lambda: service.create_for("tenant-a", "tenant-b", "object-created"),
lambda: Response(403, {"error": "forbidden"}), markers + ("object-created",),
state=lambda: deepcopy(service.objects), hide_existence=False,
),
DifferentialProbe(
"update-b", "update", lambda: Response(200, service.snapshot_b()),
lambda: service.update("tenant-a", "object-b"),
lambda: service.read("tenant-a", "absent"), markers,
state=service.snapshot_b,
),
DifferentialProbe(
"delete-b", "delete", lambda: Response(200, service.snapshot_b()),
lambda: service.delete("tenant-a", "object-b"),
lambda: service.read("tenant-a", "absent"), markers,
state=service.snapshot_b,
),
]

View 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)

View file

@ -0,0 +1,31 @@
from __future__ import annotations
import json
from pathlib import Path
from .model import RunReport
def write_report(report: RunReport, output: str | Path) -> None:
path = Path(output)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(report.as_dict(), indent=2, sort_keys=True) + "\n", encoding="utf-8")
def risk_nexus_message(report: RunReport) -> str:
lines = [
f"Whitehat run `{report.run_id}`: **{report.outcome}**",
"",
f"- evidence class: `{report.evidence_class}`",
f"- target: `{report.target}` at `{report.target_revision}`",
f"- posture/model: `{report.posture_claim}` / `{report.attacker_model}`",
f"- engagement/authorization: `{report.engagement_id}` / `{report.authorization_id}`",
f"- attempted operations: {report.attempted_operations}",
f"- interval: {report.started_at} to {report.ended_at}",
"",
report.assurance_statement,
]
if report.outcome == "finding":
lines.extend(["", "Reporter supplies facts only; risk-nexus owns severity and disclosure."])
return "\n".join(lines) + "\n"