Add governed test plane and close T04/T08

Encode fail-closed admission, target registrations, and a credential
broker that never returns secret values. Calibrate audit-core shaped
probes in-process. Send no packets and request no live credentials.

Assistant: grok
Assistant-Session: 01a02670-3345-76f2-a014-70fde8e2a2bb
This commit is contained in:
tegwick 2026-08-22 00:44:21 +02:00
parent 0aab0cb4c6
commit 95129d7a35
35 changed files with 1599 additions and 103 deletions

View file

@ -0,0 +1,83 @@
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from .differential import DifferentialProbe
from .model import Response
@dataclass
class AuditFixture:
"""In-process audit-core shape used to calibrate the E2 pack. No sockets."""
enforce_tenant: bool
def __post_init__(self) -> None:
self.events = {
"event-a": {
"id": "event-a", "tenant": "tenant-a",
"correlation_id": "corr-shared", "label": "fixture-a",
},
"event-b": {
"id": "event-b", "tenant": "tenant-b",
"correlation_id": "corr-shared", "label": "fixture-b",
},
}
def snapshot_events(self) -> object:
return deepcopy(self.events)
def _visible(self, actor_tenant: str, item: dict) -> bool:
return (not self.enforce_tenant) or item["tenant"] == actor_tenant
def get(self, actor_tenant: str, event_id: str) -> Response:
item = self.events.get(event_id)
if item is None or not self._visible(actor_tenant, item):
return Response(404, {"error": "not_found"})
return Response(200, deepcopy(item))
def by_correlation(self, actor_tenant: str, correlation_id: str) -> Response:
items = [
deepcopy(item) for item in self.events.values()
if item["correlation_id"] == correlation_id and self._visible(actor_tenant, item)
]
return Response(200, {"events": items})
def append(self, actor_tenant: str, event: dict) -> Response:
if self.enforce_tenant and event["tenant"] != actor_tenant:
return Response(403, {"error": "forbidden"})
self.events[event["id"]] = deepcopy(event)
return Response(202, {"id": event["id"], "tenant": event["tenant"]})
def audit_probe_suite(service: AuditFixture) -> list[DifferentialProbe]:
markers = ("event-b", "tenant-b", "fixture-b")
forged = {
"id": "event-forged-b", "tenant": "tenant-b",
"correlation_id": "corr-shared", "label": "fixture-forged",
}
return [
DifferentialProbe(
"audit-event-by-id", "read",
lambda: service.get("tenant-b", "event-b"),
lambda: service.get("tenant-a", "event-b"),
lambda: service.get("tenant-a", "absent"),
markers,
),
DifferentialProbe(
"audit-correlation-slice", "read",
lambda: service.by_correlation("tenant-b", "corr-shared"),
lambda: service.by_correlation("tenant-a", "corr-shared"),
lambda: service.by_correlation("tenant-a", "corr-absent"),
markers, hide_existence=False,
),
DifferentialProbe(
"audit-append-as-b", "create",
lambda: Response(202, {"id": "owner-control"}),
lambda: service.append("tenant-a", forged),
lambda: Response(403, {"error": "forbidden"}),
markers + ("event-forged-b",),
state=service.snapshot_events, hide_existence=False,
),
]

View file

@ -6,19 +6,26 @@ import sys
from dataclasses import asdict
from pathlib import Path
from .audit_fixtures import AuditFixture, audit_probe_suite
from .capacity import CapacitySample, characterize
from .differential import execute
from .e3 import CADENCE, PROBES
from .engagement import AuthorizationError, Engagement
from .fixtures import FixtureService, probe_suite
from .model import RunReport, utc_now
from .reporting import risk_nexus_message
from .plane import KillSwitch, admit, default_broker, retired_ids
from .reporting import queue_risk_nexus, risk_nexus_message
from .targets import load_catalog, load_registration
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))]
good_generic = probe_suite(FixtureService(enforce_tenant=True))
bad_generic = probe_suite(FixtureService(enforce_tenant=False))
good_audit = audit_probe_suite(AuditFixture(enforce_tenant=True))
bad_audit = audit_probe_suite(AuditFixture(enforce_tenant=False))
good = [execute(probe) for probe in (*good_generic, *good_audit)]
bad = [execute(probe) for probe in (*bad_generic, *bad_audit)]
detected = all(result.outcome == "finding" for result in bad)
rejected = all(result.outcome == "pass" for result in good)
return {
@ -78,6 +85,15 @@ def main(argv: list[str] | None = None) -> None:
engagement.add_argument("path")
packs = commands.add_parser("validate-packs")
packs.add_argument("path")
targets = commands.add_parser("validate-targets")
targets.add_argument("path")
admit_plane = commands.add_parser("admit-plane")
admit_plane.add_argument("engagement")
admit_plane.add_argument("registration")
commands.add_parser("kill-switch")
deliver = commands.add_parser("deliver")
deliver.add_argument("report")
deliver.add_argument("--outbox", default="outbox")
commands.add_parser("e3-plan")
commands.add_parser("capacity-fixture")
message = commands.add_parser("risk-message")
@ -108,6 +124,42 @@ def main(argv: list[str] | None = None) -> None:
validate_pack(path)
print(f"validated {len(paths)} probe packs")
return
if args.command == "validate-targets":
try:
catalog = load_catalog(args.path)
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError) as error:
print(f"not authorized: {error}", file=sys.stderr)
raise SystemExit(2) from None
print(f"validated {len(catalog)} target registrations")
return
if args.command == "admit-plane":
try:
record = Engagement.load(args.engagement)
registration = load_registration(args.registration)
lease = admit(engagement=record, registration=registration,
broker=default_broker(record), kill_switch=KillSwitch(),
retired=retired_ids())
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError) as error:
print(f"not authorized: {error}", file=sys.stderr)
raise SystemExit(2) from None
print(f"admitted: {lease.engagement.raw['engagement_id']} lease={lease.lease_id}")
return
if args.command == "kill-switch":
switch = KillSwitch()
if switch.engaged():
print(f"engaged: {switch.path}")
raise SystemExit(1)
print("clear")
return
if args.command == "deliver":
try:
report = RunReport(**json.loads(Path(args.report).read_text(encoding="utf-8")))
path = queue_risk_nexus(report, args.outbox)
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError, TypeError) as error:
print(f"not authorized: {error}", file=sys.stderr)
raise SystemExit(2) from None
print(f"queued: {path}")
return
if args.command == "e3-plan":
print(json.dumps({"cadence": CADENCE, "probes": [asdict(probe) for probe in PROBES]},
indent=2, sort_keys=True))

View file

@ -2,10 +2,11 @@ from __future__ import annotations
import os
from dataclasses import dataclass
from datetime import datetime
from typing import Callable
from .engagement import Engagement
from .model import Observation, ProbeResult, Response, observe
from .plane import PlaneLease
Request = Callable[[], Response]
State = Callable[[], object]
@ -49,12 +50,17 @@ def execute(probe: DifferentialProbe, *, salt: bytes | None = None) -> ProbeResu
)
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 execute_authorized(probe: DifferentialProbe, *, lease: PlaneLease,
route: str, salt: bytes | None = None,
now: datetime | None = None) -> ProbeResult:
"""Live entry point: plane admission is rechecked immediately before calls."""
lease.engagement.validate(now=now)
lease.permits(technique="e2-differential", route=route)
lease.watcher.acquire()
try:
return execute(probe, salt=salt)
finally:
lease.watcher.release()
def _equivalent_denial(left: Observation, right: Observation) -> bool:

View file

@ -0,0 +1,263 @@
from __future__ import annotations
import json
import os
import time
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any, Protocol
from .engagement import AuthorizationError, Engagement
PLANE_NAMESPACE = "whitehat"
STANDING_E2_TTL = 900
TERMINAL_IDS = frozenset({
"WH-ENG-20260821-AUDIT-E2",
"WH-ENG-20260821-TENANT-E2",
})
APPROVAL_CLASSES = {
"fixture-e2": frozenset({"e2-differential"}),
"live-e2": frozenset({"e2-differential"}),
"e3": frozenset({"e3-rls"}),
"capacity": frozenset({"p1-noisy-neighbour", "p2-noisy-neighbour"}),
}
def repo_root() -> Path:
return Path(__file__).resolve().parents[2]
def retired_ids(engagements_dir: str | Path | None = None) -> set[str]:
retired = set(TERMINAL_IDS)
root = Path(engagements_dir) if engagements_dir else repo_root() / "engagements"
if not root.exists():
return retired
for path in root.glob("*.json"):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError, ValueError):
continue
if data.get("status") == "cancelled" and data.get("engagement_id"):
retired.add(data["engagement_id"])
return retired
class KillSwitch:
def __init__(self, path: str | Path | None = None) -> None:
default = os.environ.get("WHITEHAT_KILL_SWITCH_PATH", str(repo_root() / "plane" / "KILL"))
self.path = Path(path) if path is not None else Path(default)
def engaged(self) -> bool:
flag = os.environ.get("WHITEHAT_KILL_SWITCH", "").strip().lower()
if flag in {"1", "true", "yes", "on"}:
return True
return self.path.exists()
class RateWatcher:
def __init__(self, *, per_minute: int, max_concurrency: int, max_requests: int) -> None:
if max_concurrency != 1:
raise AuthorizationError("v0.1 permits exactly one in-flight API/database operation")
if not 1 <= per_minute <= 60:
raise AuthorizationError("API rate ceiling must be between 1 and 60 per minute")
if max_requests < 1:
raise AuthorizationError("request ceiling must be at least 1")
self.per_minute = per_minute
self.max_requests = max_requests
self._times: list[float] = []
self._in_flight = 0
self._count = 0
def acquire(self) -> None:
if self._in_flight:
raise AuthorizationError("concurrency ceiling reached")
now = time.monotonic()
self._times = [stamp for stamp in self._times if now - stamp < 60]
if len(self._times) >= self.per_minute:
raise AuthorizationError("rate ceiling reached")
if self._count >= self.max_requests:
raise AuthorizationError("request ceiling reached")
self._times.append(now)
self._count += 1
self._in_flight = 1
def release(self) -> None:
self._in_flight = 0
@dataclass(frozen=True)
class IdentityHandle:
role: str
mount_path: str
lease_id: str
expires_at: str
def __repr__(self) -> str:
return (
f"IdentityHandle(role={self.role!r}, mount_path={self.mount_path!r}, "
f"lease_id={self.lease_id!r}, expires_at={self.expires_at!r})"
)
class CredentialBroker(Protocol):
def project(self, engagement: Engagement, registration: dict[str, Any]
) -> tuple[IdentityHandle, IdentityHandle]:
...
def revoke(self, lease_id: str) -> None:
...
class UnconnectedCustodyBroker:
"""Fail-closed live broker. It never requests or returns credential values."""
def project(self, engagement: Engagement, registration: dict[str, Any]
) -> tuple[IdentityHandle, IdentityHandle]:
raise AuthorizationError(
"live custody broker is not connected; no credential was requested"
)
def revoke(self, lease_id: str) -> None:
raise AuthorizationError(
f"live custody broker is not connected; lease {lease_id} was not created"
)
class LocalBroker:
"""In-process projection for fixture-e2. Secret bytes never leave this object."""
def __init__(self) -> None:
self._secrets: dict[str, bytes] = {}
self._leases: dict[str, tuple[str, str]] = {}
def project(self, engagement: Engagement, registration: dict[str, Any]
) -> tuple[IdentityHandle, IdentityHandle]:
if registration.get("applicability") != "applicable":
raise AuthorizationError("broker will not project identities for a non-applicable target")
lease_id = os.urandom(8).hex()
ttl = min(int(engagement.raw["credential_max_ttl_seconds"]), STANDING_E2_TTL)
expires_at = (datetime.now(UTC) + timedelta(seconds=ttl)).isoformat().replace("+00:00", "Z")
owner = IdentityHandle("owner", f"/var/run/secrets/whitehat/{lease_id}/token-b",
lease_id, expires_at)
attacker = IdentityHandle("attacker", f"/var/run/secrets/whitehat/{lease_id}/token-a",
lease_id, expires_at)
self._secrets[owner.mount_path] = os.urandom(32)
self._secrets[attacker.mount_path] = os.urandom(32)
self._leases[lease_id] = (owner.mount_path, attacker.mount_path)
return owner, attacker
def revoke(self, lease_id: str) -> None:
paths = self._leases.pop(lease_id, None)
if paths is None:
raise AuthorizationError("lease is unknown; cleanup cannot be proven")
for path in paths:
self._secrets.pop(path, None)
@dataclass
class PlaneLease:
engagement: Engagement
registration: dict[str, Any]
identities: tuple[IdentityHandle, IdentityHandle]
watcher: RateWatcher
lease_id: str
broker_name: str
def permits(self, *, technique: str, route: str) -> None:
self.engagement.permits(technique=technique, route=route)
if route not in self.registration["routes"]:
raise AuthorizationError(f"route not registered: {route}")
def default_broker(engagement: Engagement) -> CredentialBroker:
if (engagement.raw.get("environment") == "fixture"
and engagement.raw.get("approval_class") == "fixture-e2"):
return LocalBroker()
return UnconnectedCustodyBroker()
def admit(*, engagement: Engagement, registration: dict[str, Any],
broker: CredentialBroker | None = None,
kill_switch: KillSwitch | None = None,
now: datetime | None = None,
retired: set[str] | None = None) -> PlaneLease:
engagement.validate(now=now)
switch = kill_switch or KillSwitch()
if switch.engaged():
raise AuthorizationError("kill switch is engaged")
engagement_id = engagement.raw["engagement_id"]
if engagement_id in (retired if retired is not None else retired_ids()):
raise AuthorizationError("engagement id is retired and must not be reused")
if registration.get("applicability") != "applicable":
raise AuthorizationError(
f"target is {registration.get('applicability', 'unregistered')}"
)
target_id = engagement.raw.get("target_id")
if not target_id:
raise AuthorizationError("engagement missing target_id for plane admission")
if target_id != registration["target_id"]:
raise AuthorizationError("engagement target_id does not match registration")
approval_class = engagement.raw.get("approval_class")
allowed = APPROVAL_CLASSES.get(approval_class)
if allowed is None:
raise AuthorizationError("engagement missing or unknown approval_class")
if approval_class not in registration["approval_classes"]:
raise AuthorizationError("approval_class is not registered for this target")
for technique in engagement.raw["techniques"]:
if technique not in allowed:
raise AuthorizationError(
f"technique {technique} not in approval class {approval_class}"
)
for route in engagement.raw["routes"]:
if route not in registration["routes"]:
raise AuthorizationError(f"route not registered: {route}")
if approval_class == "live-e2":
if engagement.raw.get("plane_namespace") != PLANE_NAMESPACE:
raise AuthorizationError("live E2 requires the whitehat plane namespace")
if not engagement.raw.get("runner_image_digest"):
raise AuthorizationError("live E2 requires a pinned runner image digest")
if engagement.raw["credential_max_ttl_seconds"] > STANDING_E2_TTL:
raise AuthorizationError("E2 credential TTL must be at most 900 seconds")
if engagement.raw["environment"] == "fixture":
raise AuthorizationError("live E2 cannot use the fixture environment")
elif approval_class == "fixture-e2":
if engagement.raw["environment"] != "fixture":
raise AuthorizationError("fixture-e2 requires environment=fixture")
elif approval_class in {"e3", "capacity"}:
raise AuthorizationError(
f"{approval_class} requires a separate explicit operator window; "
"the E2 plane will not admit it"
)
selected = broker or default_broker(engagement)
identities = selected.project(engagement, registration)
if len(identities) != 2:
raise AuthorizationError("broker must project exactly two identities")
roles = {handle.role for handle in identities}
if roles != {"owner", "attacker"}:
raise AuthorizationError("broker must project owner and attacker identities")
if any(hasattr(handle, "secret") and getattr(handle, "secret") for handle in identities):
raise AuthorizationError("broker exposed a credential value")
watcher = RateWatcher(
per_minute=engagement.raw["rate_limit_per_minute"],
max_concurrency=engagement.raw["max_concurrency"],
max_requests=int(engagement.raw.get("maximum_requests")
or engagement.raw["rate_limit_per_minute"]),
)
return PlaneLease(
engagement=engagement,
registration=registration,
identities=identities,
watcher=watcher,
lease_id=identities[0].lease_id,
broker_name=type(selected).__name__,
)
def cleanup(lease: PlaneLease, broker: CredentialBroker) -> dict[str, str]:
broker.revoke(lease.lease_id)
return {
"fixtures": "delete only recorded fixture_ids",
"credential_revocation": "revoked",
"lease_id": lease.lease_id,
}

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import json
from pathlib import Path
from .engagement import AuthorizationError
from .model import RunReport
@ -29,3 +30,14 @@ def risk_nexus_message(report: RunReport) -> str:
lines.extend(["", "Reporter supplies facts only; risk-nexus owns severity and disclosure."])
return "\n".join(lines) + "\n"
def queue_risk_nexus(report: RunReport, outbox: str | Path) -> Path:
"""Persist a delivery artifact. Fixture calibration is not target assurance."""
if report.evidence_class != "target":
raise AuthorizationError("fixture evidence is not delivered as target assurance")
directory = Path(outbox)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{report.run_id}.md"
path.write_text(risk_nexus_message(report), encoding="utf-8")
return path

View file

@ -0,0 +1,61 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from .engagement import AuthorizationError
REQUIRED = {
"schema_version", "target_id", "posture_claim", "attacker_model",
"applicability", "applicability_reason", "approval_classes", "routes",
"identities", "abort_telemetry",
}
APPLICABLE_REQUIRED = {
"adapter", "probe_pack", "known_bad_calibration", "fixture_lifecycle", "egress",
}
APPLICABILITY = {"applicable", "not_applicable", "pending"}
def load_registration(path: str | Path) -> dict[str, Any]:
data = json.loads(Path(path).read_text(encoding="utf-8"))
missing = sorted(REQUIRED - data.keys())
if missing:
raise AuthorizationError(f"{path}: missing {', '.join(missing)}")
if data["schema_version"] != "whitehat-target/v1":
raise AuthorizationError(f"{path}: unsupported schema_version")
if data["applicability"] not in APPLICABILITY:
raise AuthorizationError(f"{path}: invalid applicability")
if not data["applicability_reason"]:
raise AuthorizationError(f"{path}: applicability_reason is required")
if not data["approval_classes"]:
raise AuthorizationError(f"{path}: at least one approval class is required")
if data["applicability"] == "applicable":
missing_live = sorted(APPLICABLE_REQUIRED - data.keys())
if missing_live:
raise AuthorizationError(f"{path}: applicable target missing {', '.join(missing_live)}")
if not data["routes"]:
raise AuthorizationError(f"{path}: applicable target must register routes")
identities = data["identities"]
if identities.get("count") != 2:
raise AuthorizationError(f"{path}: E2 registration must project two identities")
if identities.get("ttl_seconds", 0) > 900:
raise AuthorizationError(f"{path}: identity TTL must be at most 900 seconds")
if data.get("known_bad_calibration") in {None, "", "pending"}:
raise AuthorizationError(f"{path}: applicable target needs completed known-bad calibration")
return data
def load_catalog(directory: str | Path) -> dict[str, dict[str, Any]]:
root = Path(directory)
paths = sorted(root.glob("*.json"))
if not paths:
raise AuthorizationError(f"{root}: no target registrations found")
catalog: dict[str, dict[str, Any]] = {}
for path in paths:
registration = load_registration(path)
target_id = registration["target_id"]
if target_id in catalog:
raise AuthorizationError(f"duplicate target_id: {target_id}")
catalog[target_id] = registration
return catalog