Admit E3 and capacity on the test plane

Register in-process E3 and capacity fixtures, keep live database and
substrate targets pending, and ask ops-mason for namespace-only provision.
No packets, no credentials, no cancelled engagement IDs.

Assistant: grok
Assistant-Session: 01a02670-3345-76f2-a014-70fde8e2a2bb
This commit is contained in:
tegwick 2026-08-22 09:40:27 +02:00
parent 4882c2d47a
commit 7e83a66573
22 changed files with 501 additions and 74 deletions

View file

@ -10,3 +10,4 @@ test:
fixture-evidence: fixture-evidence:
PYTHONPATH=src python3 -m whitehat_security.cli fixtures --output evidence/offline-calibration.json PYTHONPATH=src python3 -m whitehat_security.cli fixtures --output evidence/offline-calibration.json
PYTHONPATH=src python3 -m whitehat_security.cli e3-fixtures --output evidence/offline-e3-calibration.json

View file

@ -74,9 +74,10 @@ boundary always holds.
E2. Live audit-core still needs a new engagement and a provisioned plane. E2. Live audit-core still needs a new engagement and a provisioned plane.
- `T04` is complete for every applicable E2 probe: generic and audit-core - `T04` is complete for every applicable E2 probe: generic and audit-core
shaped fixtures fail known-bad and pass known-good in-process. shaped fixtures fail known-bad and pass known-good in-process.
- `T05` is in progress with a 24-hour E3 cadence and offline evaluator. - `T05` is in progress: 24-hour cadence, offline evaluator and in-process
- `T06` is in progress with a bounded characterization evaluator; no shared calibration exist; `platform-pg` is pending a live window.
substrate window is approved. - `T06` is in progress: the characterization evaluator is calibrated
in-process; `shared-substrate` is pending a live window and ceiling.
- `T07` is in progress: reports can be queued to `outbox/`; the first target - `T07` is in progress: reports can be queued to `outbox/`; the first target
report has not been produced. report has not been produced.
- `T08` is the governed test plane contract. Cluster provisioning is outside - `T08` is the governed test plane contract. Cluster provisioning is outside

View file

@ -14,8 +14,13 @@ The executable plan is available with:
```sh ```sh
PYTHONPATH=src python3 -m whitehat_security.cli e3-plan PYTHONPATH=src python3 -m whitehat_security.cli e3-plan
PYTHONPATH=src python3 -m whitehat_security.cli e3-fixtures
``` ```
`e3-fixtures` calibrates the seven expected outcomes in-process, including
keeping the SQL-compromise reset as `inconclusive`. It opens no database
connection and is not target assurance.
The run checks the conformance view, unset-GUC reads, wrong-tenant reads and The run checks the conformance view, unset-GUC reads, wrong-tenant reads and
writes, `BYPASSRLS` on the runtime identity, and unsafe `SECURITY DEFINER` writes, `BYPASSRLS` on the runtime identity, and unsafe `SECURITY DEFINER`
paths. The SQL-compromise reset probe is recorded as `inconclusive` under E3: paths. The SQL-compromise reset probe is recorded as `inconclusive` under E3:

View file

@ -15,8 +15,8 @@ make fixture-evidence
Success requires every read/write probe to pass against the enforcing fixture Success requires every read/write probe to pass against the enforcing fixture
and to produce a finding against the fixture with its tenant predicate removed. and to produce a finding against the fixture with its tenant predicate removed.
`evidence/offline-calibration.json` is calibration evidence, not target `make fixture-evidence` also writes `evidence/offline-e3-calibration.json`.
assurance. Both files are calibration evidence, not target assurance.
## Test plane ## Test plane

View file

@ -144,11 +144,13 @@ Live runs are admitted only through the test plane documented in
projected short-lived identities, registered target, kill switch, rate watcher projected short-lived identities, registered target, kill switch, rate watcher
and lease cleanup. The plane is a control, not authorization. and lease cleanup. The plane is a control, not authorization.
`fixture-e2` is the in-process class already permitted by §1. `live-e2` still `fixture-e2`, `fixture-e3` and `fixture-capacity` are in-process classes
requires the dated engagement record, target-owner acknowledgement and already permitted by §1. `live-e2` still requires the dated engagement
technique-specific operator approval required by §1, plus plane admission. record, target-owner acknowledgement and technique-specific operator approval
`e3`, recovery, saturation and destructive techniques keep separate explicit required by §1, plus plane admission. Live `e3` additionally names the
approval classes and are not admitted by the E2 plane. database and uses one ordinary runtime role with no `BYPASSRLS`. Live
`capacity` additionally names an aggressor ceiling and a substrate window.
Recovery and destructive techniques remain outside this plane.
Cancelled engagement identifiers are terminal and must not be reused. Cancelled engagement identifiers are terminal and must not be reused.

View file

@ -11,11 +11,15 @@ A live E2 run is admitted only when every item below is true:
2. Known-bad calibration for that target has already failed in-process. 2. Known-bad calibration for that target has already failed in-process.
3. The engagement is complete, in window, owner-acknowledged, and not a 3. The engagement is complete, in window, owner-acknowledged, and not a
retired ID. retired ID.
4. `approval_class` is `live-e2` (or `fixture-e2` for in-process only). 4. `approval_class` matches the registration (`fixture-e2` / `live-e2` /
`fixture-e3` / `e3` / `fixture-capacity` / `capacity`).
5. The runner namespace is `whitehat`, the image digest is pinned, and 5. The runner namespace is `whitehat`, the image digest is pinned, and
engagement labels are present. engagement labels are present. Live E3 also names a `database`. Live
6. The custody broker projected exactly two identity *handles* (owner and capacity also names an `aggressor_ceiling`.
attacker). Credential values are not returned to the operator or agent. 6. The custody broker projected the registered number of identity *handles*
(two for E2, one runtime for E3, none for capacity fixtures). Credential
values are not returned to the operator or agent. Live classes fail closed
while the custody broker is unconnected.
7. The kill switch is clear. 7. The kill switch is clear.
8. The rate watcher is attached. Cleanup revokes the lease. 8. The rate watcher is attached. Cleanup revokes the lease.

View file

@ -0,0 +1,31 @@
# Plane provision request — not an engagement
Status: **coordination only; apply nothing from this repository**
`WHITEHAT-WP-0001-T08` encoded the governed test plane as a contract. Live E2
still needs the cluster objects. This note asks `ops-mason` to provision the
**namespace, default-deny network policy, and runner service account** from
`plane/`. It does **not** authorize:
- applying the runner pod
- projecting credentials
- sending packets
- reusing `WH-ENG-20260821-AUDIT-E2` or `WH-ENG-20260821-TENANT-E2`
## Requested objects
| Object | File | Notes |
| --- | --- | --- |
| Namespace `whitehat` | `plane/namespace.yaml` | restricted PSS |
| Default-deny NetworkPolicy | `plane/network-policy.yaml` | audit-core egress is documented, not a standing allow for other targets |
| ServiceAccount `whitehat-runner` | `plane/service-account.yaml` | `automountServiceAccountToken: false` |
Do not create a credential secret. The live custody broker is still
unconnected; whitehat will fail closed until a later engagement ID exists.
## Next engagement (not this request)
After the namespace exists, a **new** audit-core E2 ID can be drafted. It will
need two ordinary tenant-scoped `may_read`/`may_write` fixture senders, TTL
≤ 900s, projected into the runner mount without exposing values to the agent.
That is a separate request and uses a new ID.

View file

@ -1,8 +1,9 @@
# Evidence # Evidence
This directory stores sanitized run artifacts. `offline-calibration.json` is This directory stores sanitized run artifacts. `offline-calibration.json` and
generated from repository-created fixtures and proves only that the harness `offline-e3-calibration.json` are generated from repository-created fixtures
distinguishes known-good from known-bad behavior. and prove only that the harness distinguishes known-good from known-bad
behavior. They are not target assurance.
Before committing target evidence, verify that it contains no response body, Before committing target evidence, verify that it contains no response body,
credential, database URL, real tenant identifier, or real tenant value. A credential, database URL, real tenant identifier, or real tenant value. A

View file

@ -0,0 +1,99 @@
{
"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"
},
"ended_at": "2026-08-22T07:39:54.791280Z",
"evidence_class": "fixture",
"known_bad": [
{
"outcome": "finding",
"probe_id": "conformance-view-empty",
"reason": "expectation failed: zero_rows"
},
{
"outcome": "finding",
"probe_id": "unset-guc-reads-none",
"reason": "expectation failed: zero_rows"
},
{
"outcome": "finding",
"probe_id": "tenant-a-cannot-read-b",
"reason": "expectation failed: zero_rows"
},
{
"outcome": "finding",
"probe_id": "tenant-a-cannot-insert-b",
"reason": "expectation failed: statement_rejected"
},
{
"outcome": "finding",
"probe_id": "runtime-lacks-bypassrls",
"reason": "expectation failed: false"
},
{
"outcome": "finding",
"probe_id": "unsafe-definer-inventory-empty",
"reason": "expectation failed: zero_rows"
},
{
"outcome": "inconclusive",
"probe_id": "sql-compromise-reset",
"reason": "E3-B observation records the documented SQL-compromise limit"
}
],
"known_good": [
{
"outcome": "pass",
"probe_id": "conformance-view-empty",
"reason": "expectation met: zero_rows"
},
{
"outcome": "pass",
"probe_id": "unset-guc-reads-none",
"reason": "expectation met: zero_rows"
},
{
"outcome": "pass",
"probe_id": "tenant-a-cannot-read-b",
"reason": "expectation met: zero_rows"
},
{
"outcome": "pass",
"probe_id": "tenant-a-cannot-insert-b",
"reason": "expectation met: statement_rejected"
},
{
"outcome": "pass",
"probe_id": "runtime-lacks-bypassrls",
"reason": "expectation met: false"
},
{
"outcome": "pass",
"probe_id": "unsafe-definer-inventory-empty",
"reason": "expectation met: zero_rows"
},
{
"outcome": "inconclusive",
"probe_id": "sql-compromise-reset",
"reason": "E3-B observation records the documented SQL-compromise limit"
}
],
"limitations": [
"Offline E3 calibration evaluates the harness; it is not target assurance.",
"No database connection or live credential was used.",
"sql-compromise-reset is E3's documented limit and stays inconclusive."
],
"outcome": "pass",
"run_id": "e3-calibration-2026-08-22T07:39:54.791131Z",
"schema_version": "whitehat-e3-calibration/v1",
"started_at": "2026-08-22T07:39:54.791131Z"
}

View file

@ -14,13 +14,13 @@
"approval_classes": { "approval_classes": {
"type": "array", "type": "array",
"minItems": 1, "minItems": 1,
"items": {"enum": ["fixture-e2", "live-e2", "e3", "capacity"]} "items": {"enum": ["fixture-e2", "live-e2", "fixture-e3", "e3", "fixture-capacity", "capacity"]}
}, },
"routes": {"type": "array", "items": {"type": "string"}}, "routes": {"type": "array", "items": {"type": "string"}},
"identities": { "identities": {
"type": "object", "type": "object",
"properties": { "properties": {
"count": {"const": 2}, "count": {"type": "integer", "minimum": 0, "maximum": 2},
"ttl_seconds": {"type": "integer", "minimum": 1, "maximum": 900} "ttl_seconds": {"type": "integer", "minimum": 1, "maximum": 900}
} }
} }

View file

@ -9,7 +9,7 @@ from pathlib import Path
from .audit_fixtures import AuditFixture, audit_probe_suite from .audit_fixtures import AuditFixture, audit_probe_suite
from .capacity import CapacitySample, characterize from .capacity import CapacitySample, characterize
from .differential import execute from .differential import execute
from .e3 import CADENCE, PROBES from .e3 import CADENCE, PROBES, e3_calibration
from .engagement import AuthorizationError, Engagement from .engagement import AuthorizationError, Engagement
from .fixtures import FixtureService, probe_suite from .fixtures import FixtureService, probe_suite
from .model import RunReport, utc_now from .model import RunReport, utc_now
@ -95,6 +95,8 @@ def main(argv: list[str] | None = None) -> None:
deliver.add_argument("report") deliver.add_argument("report")
deliver.add_argument("--outbox", default="outbox") deliver.add_argument("--outbox", default="outbox")
commands.add_parser("e3-plan") commands.add_parser("e3-plan")
e3_fix = commands.add_parser("e3-fixtures", help="calibrate E3 probes offline")
e3_fix.add_argument("--output")
commands.add_parser("capacity-fixture") commands.add_parser("capacity-fixture")
message = commands.add_parser("risk-message") message = commands.add_parser("risk-message")
message.add_argument("report") message.add_argument("report")
@ -164,6 +166,14 @@ def main(argv: list[str] | None = None) -> None:
print(json.dumps({"cadence": CADENCE, "probes": [asdict(probe) for probe in PROBES]}, print(json.dumps({"cadence": CADENCE, "probes": [asdict(probe) for probe in PROBES]},
indent=2, sort_keys=True)) indent=2, sort_keys=True))
return return
if args.command == "e3-fixtures":
result = e3_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 == "capacity-fixture": if args.command == "capacity-fixture":
print(json.dumps(capacity_fixture(), indent=2, sort_keys=True)) print(json.dumps(capacity_fixture(), indent=2, sort_keys=True))
return return

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable, Literal from typing import Callable, Literal
from .model import Outcome from .model import Outcome, utc_now
Expectation = Literal["zero_rows", "statement_rejected", "false", "documented_limit"] Expectation = Literal["zero_rows", "statement_rejected", "false", "documented_limit"]
Query = Callable[[str], object] Query = Callable[[str], object]
@ -49,6 +49,52 @@ def evaluate(probe: E3Probe, *, rows: int = 0, rejected: bool = False,
return E3Result(probe.probe_id, "finding", f"expectation failed: {probe.expectation}") return E3Result(probe.probe_id, "finding", f"expectation failed: {probe.expectation}")
def fixture_results(*, enforce: bool) -> list[E3Result]:
"""In-process known-good/known-bad outcomes. No database connection."""
results: list[E3Result] = []
for probe in PROBES:
if probe.expectation == "documented_limit":
results.append(evaluate(probe))
continue
if enforce:
results.append(evaluate(probe, rows=0, rejected=True, boolean=False))
else:
results.append(evaluate(probe, rows=1, rejected=False, boolean=True))
return results
def e3_calibration() -> dict:
started = utc_now()
good = fixture_results(enforce=True)
bad = fixture_results(enforce=False)
def expected(result: E3Result, *, enforce: bool) -> bool:
probe = next(item for item in PROBES if item.probe_id == result.probe_id)
if probe.expectation == "documented_limit":
return result.outcome == "inconclusive"
return result.outcome == ("pass" if enforce else "finding")
ok = all(expected(item, enforce=True) for item in good) and all(
expected(item, enforce=False) for item in bad
)
return {
"schema_version": "whitehat-e3-calibration/v1",
"evidence_class": "fixture",
"run_id": f"e3-calibration-{started}",
"started_at": started,
"ended_at": utc_now(),
"outcome": "pass" if ok else "finding",
"cadence": CADENCE,
"known_good": [result.__dict__ for result in good],
"known_bad": [result.__dict__ for result in bad],
"limitations": [
"Offline E3 calibration evaluates the harness; it is not target assurance.",
"No database connection or live credential was used.",
"sql-compromise-reset is E3's documented limit and stays inconclusive.",
],
}
CADENCE = { CADENCE = {
"interval": "24h", "interval": "24h",
"maximum_detection_window": "24h plus run and reporting latency", "maximum_detection_window": "24h plus run and reporting latency",

View file

@ -19,9 +19,12 @@ TERMINAL_IDS = frozenset({
APPROVAL_CLASSES = { APPROVAL_CLASSES = {
"fixture-e2": frozenset({"e2-differential"}), "fixture-e2": frozenset({"e2-differential"}),
"live-e2": frozenset({"e2-differential"}), "live-e2": frozenset({"e2-differential"}),
"fixture-e3": frozenset({"e3-rls"}),
"e3": frozenset({"e3-rls"}), "e3": frozenset({"e3-rls"}),
"fixture-capacity": frozenset({"p1-noisy-neighbour", "p2-noisy-neighbour"}),
"capacity": frozenset({"p1-noisy-neighbour", "p2-noisy-neighbour"}), "capacity": frozenset({"p1-noisy-neighbour", "p2-noisy-neighbour"}),
} }
FIXTURE_CLASSES = frozenset({"fixture-e2", "fixture-e3", "fixture-capacity"})
def repo_root() -> Path: def repo_root() -> Path:
@ -102,7 +105,7 @@ class IdentityHandle:
class CredentialBroker(Protocol): class CredentialBroker(Protocol):
def project(self, engagement: Engagement, registration: dict[str, Any] def project(self, engagement: Engagement, registration: dict[str, Any]
) -> tuple[IdentityHandle, IdentityHandle]: ) -> tuple[IdentityHandle, ...]:
... ...
def revoke(self, lease_id: str) -> None: def revoke(self, lease_id: str) -> None:
@ -113,7 +116,7 @@ class UnconnectedCustodyBroker:
"""Fail-closed live broker. It never requests or returns credential values.""" """Fail-closed live broker. It never requests or returns credential values."""
def project(self, engagement: Engagement, registration: dict[str, Any] def project(self, engagement: Engagement, registration: dict[str, Any]
) -> tuple[IdentityHandle, IdentityHandle]: ) -> tuple[IdentityHandle, ...]:
raise AuthorizationError( raise AuthorizationError(
"live custody broker is not connected; no credential was requested" "live custody broker is not connected; no credential was requested"
) )
@ -132,20 +135,25 @@ class LocalBroker:
self._leases: dict[str, tuple[str, str]] = {} self._leases: dict[str, tuple[str, str]] = {}
def project(self, engagement: Engagement, registration: dict[str, Any] def project(self, engagement: Engagement, registration: dict[str, Any]
) -> tuple[IdentityHandle, IdentityHandle]: ) -> tuple[IdentityHandle, ...]:
if registration.get("applicability") != "applicable": if registration.get("applicability") != "applicable":
raise AuthorizationError("broker will not project identities for a non-applicable target") raise AuthorizationError("broker will not project identities for a non-applicable target")
count = int(registration["identities"]["count"])
lease_id = os.urandom(8).hex() lease_id = os.urandom(8).hex()
ttl = min(int(engagement.raw["credential_max_ttl_seconds"]), STANDING_E2_TTL) 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") 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", handles: list[IdentityHandle] = []
lease_id, expires_at) if count >= 1:
attacker = IdentityHandle("attacker", f"/var/run/secrets/whitehat/{lease_id}/token-a", role = "owner" if count == 2 else "runtime"
lease_id, expires_at) path = f"/var/run/secrets/whitehat/{lease_id}/{'token-b' if count == 2 else 'runtime'}"
self._secrets[owner.mount_path] = os.urandom(32) handles.append(IdentityHandle(role, path, lease_id, expires_at))
self._secrets[attacker.mount_path] = os.urandom(32) self._secrets[path] = os.urandom(32)
self._leases[lease_id] = (owner.mount_path, attacker.mount_path) if count == 2:
return owner, attacker path = f"/var/run/secrets/whitehat/{lease_id}/token-a"
handles.append(IdentityHandle("attacker", path, lease_id, expires_at))
self._secrets[path] = os.urandom(32)
self._leases[lease_id] = tuple(handle.mount_path for handle in handles)
return tuple(handles)
def revoke(self, lease_id: str) -> None: def revoke(self, lease_id: str) -> None:
paths = self._leases.pop(lease_id, None) paths = self._leases.pop(lease_id, None)
@ -159,7 +167,7 @@ class LocalBroker:
class PlaneLease: class PlaneLease:
engagement: Engagement engagement: Engagement
registration: dict[str, Any] registration: dict[str, Any]
identities: tuple[IdentityHandle, IdentityHandle] identities: tuple[IdentityHandle, ...]
watcher: RateWatcher watcher: RateWatcher
lease_id: str lease_id: str
broker_name: str broker_name: str
@ -172,7 +180,7 @@ class PlaneLease:
def default_broker(engagement: Engagement) -> CredentialBroker: def default_broker(engagement: Engagement) -> CredentialBroker:
if (engagement.raw.get("environment") == "fixture" if (engagement.raw.get("environment") == "fixture"
and engagement.raw.get("approval_class") == "fixture-e2"): and engagement.raw.get("approval_class") in FIXTURE_CLASSES):
return LocalBroker() return LocalBroker()
return UnconnectedCustodyBroker() return UnconnectedCustodyBroker()
@ -212,32 +220,26 @@ def admit(*, engagement: Engagement, registration: dict[str, Any],
for route in engagement.raw["routes"]: for route in engagement.raw["routes"]:
if route not in registration["routes"]: if route not in registration["routes"]:
raise AuthorizationError(f"route not registered: {route}") raise AuthorizationError(f"route not registered: {route}")
if approval_class == "live-e2": _enforce_class_envelope(approval_class, engagement)
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) selected = broker or default_broker(engagement)
identities = selected.project(engagement, registration) count = int(registration["identities"]["count"])
if len(identities) != 2: if count:
raise AuthorizationError("broker must project exactly two identities") identities = selected.project(engagement, registration)
roles = {handle.role for handle in identities} if len(identities) != count:
if roles != {"owner", "attacker"}: raise AuthorizationError(f"broker must project exactly {count} identities")
raise AuthorizationError("broker must project owner and attacker identities") expected = {2: {"owner", "attacker"}, 1: {"runtime"}}[count]
if any(hasattr(handle, "secret") and getattr(handle, "secret") for handle in identities): if {handle.role for handle in identities} != expected:
raise AuthorizationError("broker exposed a credential value") raise AuthorizationError(f"broker must project {sorted(expected)} identities")
if any(hasattr(handle, "secret") and getattr(handle, "secret") for handle in identities):
raise AuthorizationError("broker exposed a credential value")
lease_id = identities[0].lease_id
else:
if approval_class not in FIXTURE_CLASSES:
selected.project(engagement, registration)
identities = ()
lease_id = os.urandom(8).hex()
if hasattr(selected, "_leases"):
selected._leases[lease_id] = ()
watcher = RateWatcher( watcher = RateWatcher(
per_minute=engagement.raw["rate_limit_per_minute"], per_minute=engagement.raw["rate_limit_per_minute"],
max_concurrency=engagement.raw["max_concurrency"], max_concurrency=engagement.raw["max_concurrency"],
@ -249,11 +251,31 @@ def admit(*, engagement: Engagement, registration: dict[str, Any],
registration=registration, registration=registration,
identities=identities, identities=identities,
watcher=watcher, watcher=watcher,
lease_id=identities[0].lease_id, lease_id=lease_id,
broker_name=type(selected).__name__, broker_name=type(selected).__name__,
) )
def _enforce_class_envelope(approval_class: str, engagement: Engagement) -> None:
environment = engagement.raw["environment"]
if approval_class in FIXTURE_CLASSES:
if environment != "fixture":
raise AuthorizationError(f"{approval_class} requires environment=fixture")
return
if environment == "fixture":
raise AuthorizationError(f"{approval_class} cannot use the fixture environment")
if engagement.raw.get("plane_namespace") != PLANE_NAMESPACE:
raise AuthorizationError(f"{approval_class} requires the whitehat plane namespace")
if not engagement.raw.get("runner_image_digest"):
raise AuthorizationError(f"{approval_class} requires a pinned runner image digest")
if engagement.raw["credential_max_ttl_seconds"] > STANDING_E2_TTL:
raise AuthorizationError("credential TTL must be at most 900 seconds")
if approval_class == "e3" and not engagement.raw.get("database"):
raise AuthorizationError("live E3 requires a named database")
if approval_class == "capacity" and engagement.raw.get("aggressor_ceiling") is None:
raise AuthorizationError("live capacity requires an aggressor_ceiling")
def cleanup(lease: PlaneLease, broker: CredentialBroker) -> dict[str, str]: def cleanup(lease: PlaneLease, broker: CredentialBroker) -> dict[str, str]:
broker.revoke(lease.lease_id) broker.revoke(lease.lease_id)
return { return {

View file

@ -37,9 +37,15 @@ def load_registration(path: str | Path) -> dict[str, Any]:
if not data["routes"]: if not data["routes"]:
raise AuthorizationError(f"{path}: applicable target must register routes") raise AuthorizationError(f"{path}: applicable target must register routes")
identities = data["identities"] identities = data["identities"]
if identities.get("count") != 2: count = identities.get("count")
classes = set(data["approval_classes"])
if classes & {"fixture-e2", "live-e2"} and count != 2:
raise AuthorizationError(f"{path}: E2 registration must project two identities") raise AuthorizationError(f"{path}: E2 registration must project two identities")
if identities.get("ttl_seconds", 0) > 900: if classes & {"fixture-e3", "e3"} and count != 1:
raise AuthorizationError(f"{path}: E3 registration must project one runtime identity")
if classes & {"fixture-capacity", "capacity"} and count not in {0, 1}:
raise AuthorizationError(f"{path}: capacity registration projects at most one aggressor identity")
if count and identities.get("ttl_seconds", 0) > 900:
raise AuthorizationError(f"{path}: identity TTL must be at most 900 seconds") raise AuthorizationError(f"{path}: identity TTL must be at most 900 seconds")
if data.get("known_bad_calibration") in {None, "", "pending"}: if data.get("known_bad_calibration") in {None, "", "pending"}:
raise AuthorizationError(f"{path}: applicable target needs completed known-bad calibration") raise AuthorizationError(f"{path}: applicable target needs completed known-bad calibration")

View file

@ -9,6 +9,10 @@ honest applicability record the test plane admits against.
| `audit-core` | applicable | Two ordinary tenant-scoped senders instantiate E2. Live run still needs a new engagement and plane lease. | | `audit-core` | applicable | Two ordinary tenant-scoped senders instantiate E2. Live run still needs a new engagement and plane lease. |
| `tenant-engine` | not_applicable | No tenant-A identity without tenant-B authority. | | `tenant-engine` | not_applicable | No tenant-A identity without tenant-B authority. |
| `flex-auth` | pending | Initial envelope; owner has not confirmed the attacker identity. | | `flex-auth` | pending | Initial envelope; owner has not confirmed the attacker identity. |
| `fixture-e3` | applicable | In-process E3 evaluator. One runtime identity, no `BYPASSRLS`. |
| `platform-pg` | pending | Live E3 needs a named database, ordinary runtime role, and dated window. |
| `fixture-capacity` | applicable | In-process P1/P2 evaluator. Generates no load. |
| `shared-substrate` | pending | Live capacity needs an operator window and aggressor ceiling. |
`not_applicable` is a completed artifact, not a deferral. Do not relabel it to `not_applicable` is a completed artifact, not a deferral. Do not relabel it to
close a workplan. Do not reuse cancelled engagement IDs from `engagements/`. close a workplan. Do not reuse cancelled engagement IDs from `engagements/`.

View file

@ -0,0 +1,29 @@
{
"schema_version": "whitehat-target/v1",
"target_id": "fixture-capacity",
"posture_claim": "P1/P2",
"attacker_model": "bounded co-resident saturating its own allowance",
"applicability": "applicable",
"applicability_reason": "In-process evaluator created by this repository. It generates no load and records governor binding plus neighbour degradation.",
"approval_classes": ["fixture-capacity"],
"adapter": "src/whitehat_security/capacity.py",
"probe_pack": "src/whitehat_security/capacity.py",
"known_bad_calibration": "src/whitehat_security/capacity.py",
"fixture_lifecycle": {
"create": "in-process samples",
"delete": "drop process state",
"ids_are_synthetic": true
},
"egress": "in-process",
"routes": ["baseline", "loaded"],
"identities": {
"count": 0,
"role": "none; fixture generates no network load",
"broker_audience": "whitehat-capacity/fixture"
},
"abort_telemetry": [
"governor_unbound",
"aggressor_exceeded_ceiling",
"missing_neighbour_sample"
]
}

39
targets/fixture-e3.json Normal file
View file

@ -0,0 +1,39 @@
{
"schema_version": "whitehat-target/v1",
"target_id": "fixture-e3",
"posture_claim": "E3",
"attacker_model": "E3-A omitted-predicate accident; E3-B documented SQL-compromise limit",
"applicability": "applicable",
"applicability_reason": "In-process evaluator created by this repository. It uses one ordinary runtime identity and never grants BYPASSRLS or superuser.",
"approval_classes": ["fixture-e3"],
"adapter": "src/whitehat_security/e3.py",
"probe_pack": "src/whitehat_security/e3.py",
"known_bad_calibration": "src/whitehat_security/e3.py",
"fixture_lifecycle": {
"create": "in-process constructor",
"delete": "drop process state",
"ids_are_synthetic": true
},
"egress": "in-process",
"routes": [
"conformance",
"unset_guc",
"wrong_tenant_read",
"wrong_tenant_insert",
"runtime_bypassrls",
"unsafe_definer",
"reset_to_b"
],
"identities": {
"count": 1,
"role": "ordinary runtime database role without BYPASSRLS",
"ttl_seconds": 900,
"broker_audience": "whitehat-e3/fixture"
},
"abort_telemetry": [
"statement_timeout",
"unexpected_or_non_fixture_data",
"missing_attribution",
"privilege_escalation"
]
}

View file

@ -0,0 +1,22 @@
{
"schema_version": "whitehat-target/v1",
"target_id": "platform-pg",
"posture_claim": "E3",
"attacker_model": "E3-A omitted-predicate accident; E3-B documented SQL-compromise limit",
"applicability": "pending",
"applicability_reason": "Named in the rules of engagement as outside the initial API-probe envelope. A live run needs the database owner to confirm an ordinary runtime role without BYPASSRLS, the conformance view, and a dated window. This registration does not authorize a connection.",
"approval_classes": ["e3"],
"routes": [],
"identities": {
"count": 1,
"role": "ordinary runtime database role without BYPASSRLS or superuser",
"ttl_seconds": 900,
"broker_audience": "whitehat-e3/platform-pg"
},
"abort_telemetry": [
"statement_timeout",
"unexpected_or_non_fixture_data",
"missing_attribution",
"privilege_escalation"
]
}

View file

@ -0,0 +1,21 @@
{
"schema_version": "whitehat-target/v1",
"target_id": "shared-substrate",
"posture_claim": "P1/P2",
"attacker_model": "bounded co-resident saturating its own allowance",
"applicability": "pending",
"applicability_reason": "No operator-approved substrate window or aggressor ceiling exists. Saturation against a shared rail is an outage if run without those bounds. This registration does not authorize load.",
"approval_classes": ["capacity"],
"routes": [],
"identities": {
"count": 0,
"role": "none until a window names the aggressor consumer",
"broker_audience": "whitehat-capacity/shared-substrate"
},
"abort_telemetry": [
"governor_unbound",
"aggressor_exceeded_ceiling",
"missing_neighbour_sample",
"shared_substrate_abort_threshold"
]
}

View file

@ -16,7 +16,7 @@ def test_validate_engagement_reports_clean_denial(tmp_path, capsys):
def test_validate_targets_accepts_catalog(capsys): def test_validate_targets_accepts_catalog(capsys):
main(["validate-targets", "targets"]) main(["validate-targets", "targets"])
assert capsys.readouterr().out.startswith("validated 4 target registrations") assert capsys.readouterr().out.startswith("validated 8 target registrations")
def test_kill_switch_is_clear_by_default(capsys): def test_kill_switch_is_clear_by_default(capsys):

View file

@ -8,6 +8,7 @@ from whitehat_security.differential import execute, execute_authorized
from whitehat_security.engagement import AuthorizationError, Engagement from whitehat_security.engagement import AuthorizationError, Engagement
from whitehat_security.fixtures import FixtureService, probe_suite from whitehat_security.fixtures import FixtureService, probe_suite
from whitehat_security.model import RunReport from whitehat_security.model import RunReport
from whitehat_security.e3 import e3_calibration
from whitehat_security.plane import ( from whitehat_security.plane import (
KillSwitch, LocalBroker, RateWatcher, UnconnectedCustodyBroker, KillSwitch, LocalBroker, RateWatcher, UnconnectedCustodyBroker,
admit, cleanup, retired_ids, admit, cleanup, retired_ids,
@ -59,6 +60,10 @@ def test_catalog_loads_honest_applicability():
assert catalog["audit-core"]["applicability"] == "applicable" assert catalog["audit-core"]["applicability"] == "applicable"
assert catalog["tenant-engine"]["applicability"] == "not_applicable" assert catalog["tenant-engine"]["applicability"] == "not_applicable"
assert catalog["flex-auth"]["applicability"] == "pending" assert catalog["flex-auth"]["applicability"] == "pending"
assert catalog["fixture-e3"]["applicability"] == "applicable"
assert catalog["fixture-capacity"]["applicability"] == "applicable"
assert catalog["platform-pg"]["applicability"] == "pending"
assert catalog["shared-substrate"]["applicability"] == "pending"
def test_retired_ids_include_cancelled_records(): def test_retired_ids_include_cancelled_records():
@ -171,6 +176,82 @@ def test_e3_is_not_admitted_by_the_e2_plane(tmp_path):
) )
def test_fixture_e3_projects_one_runtime_identity(tmp_path):
broker = LocalBroker()
engagement = load_engagement(tmp_path, fixture_record(
target_id="fixture-e3", approval_class="fixture-e3",
techniques=["e3-rls"], routes=["conformance"],
))
lease = admit(
engagement=engagement, registration=load_registration("targets/fixture-e3.json"),
broker=broker, kill_switch=KillSwitch(tmp_path / "KILL"),
now=NOW, retired=set(),
)
assert len(lease.identities) == 1
assert lease.identities[0].role == "runtime"
assert cleanup(lease, broker)["credential_revocation"] == "revoked"
def test_fixture_capacity_projects_no_identities(tmp_path):
broker = LocalBroker()
engagement = load_engagement(tmp_path, fixture_record(
target_id="fixture-capacity", approval_class="fixture-capacity",
techniques=["p1-noisy-neighbour"], routes=["baseline"],
))
lease = admit(
engagement=engagement, registration=load_registration("targets/fixture-capacity.json"),
broker=broker, kill_switch=KillSwitch(tmp_path / "KILL"),
now=NOW, retired=set(),
)
assert lease.identities == ()
assert cleanup(lease, broker)["credential_revocation"] == "revoked"
def test_pending_e3_target_is_refused(tmp_path):
engagement = load_engagement(tmp_path, fixture_record(
target_id="platform-pg", approval_class="e3",
techniques=["e3-rls"], environment="build",
plane_namespace="whitehat", runner_image_digest="sha256:abc",
database="platform-pg", routes=["conformance"],
))
with pytest.raises(AuthorizationError, match="pending"):
admit(
engagement=engagement,
registration=load_registration("targets/platform-pg-e3.json"),
broker=LocalBroker(), kill_switch=KillSwitch(tmp_path / "KILL"),
now=NOW, retired=set(),
)
def test_live_e3_without_database_fails_closed(tmp_path):
registration = load_registration("targets/fixture-e3.json")
registration["target_id"] = "fixture-e3-live"
registration["approval_classes"] = ["e3"]
engagement = load_engagement(tmp_path, fixture_record(
target_id="fixture-e3-live", approval_class="e3",
techniques=["e3-rls"], environment="build",
plane_namespace="whitehat", runner_image_digest="sha256:abc",
routes=["conformance"],
))
with pytest.raises(AuthorizationError, match="named database"):
admit(
engagement=engagement, registration=registration,
broker=LocalBroker(), kill_switch=KillSwitch(tmp_path / "KILL"),
now=NOW, retired=set(),
)
def test_e3_calibration_keeps_documented_limit_inconclusive():
report = e3_calibration()
assert report["outcome"] == "pass"
by_id = {item["probe_id"]: item for item in report["known_good"]}
assert by_id["sql-compromise-reset"]["outcome"] == "inconclusive"
assert by_id["conformance-view-empty"]["outcome"] == "pass"
bad = {item["probe_id"]: item for item in report["known_bad"]}
assert bad["conformance-view-empty"]["outcome"] == "finding"
assert bad["sql-compromise-reset"]["outcome"] == "inconclusive"
def test_rate_and_concurrency_ceilings(tmp_path): def test_rate_and_concurrency_ceilings(tmp_path):
watcher = RateWatcher(per_minute=10, max_concurrency=1, max_requests=1) watcher = RateWatcher(per_minute=10, max_concurrency=1, max_requests=1)
watcher.acquire() watcher.acquire()

View file

@ -215,12 +215,13 @@ notices. This repo is that something.
control the interval between runs *is* the exposure window, and ADR-0003 leaves control the interval between runs *is* the exposure window, and ADR-0003 leaves
the number to this repo. Set it, and state the resulting window in the record. the number to this repo. Set it, and state the resulting window in the record.
Implementation progress, 2026-08-21: the cadence is fixed at 24 hours plus run Implementation progress, 2026-08-22: the cadence is fixed at 24 hours plus run
and reporting latency, with event-triggered pre-promotion runs after schema, and reporting latency, with event-triggered pre-promotion runs after schema,
role, RLS or security-definer changes. `src/whitehat_security/e3.py` encodes the role, RLS or security-definer changes. `src/whitehat_security/e3.py` encodes the
seven expected outcomes and keeps the SQL-compromise GUC reset labeled as E3's seven expected outcomes, keeps the SQL-compromise GUC reset labeled as E3's
documented limit. A live database run still requires its separate authorized documented limit, and calibrates known-good/known-bad in-process. `platform-pg`
target and window. is registered `pending`. A live database run still requires its separate
authorized target, named database, ordinary runtime role, and window.
### T06 — Noisy-neighbour characterisation (the P1/P2 artifact) ### T06 — Noisy-neighbour characterisation (the P1/P2 artifact)
@ -244,11 +245,12 @@ acceptable.
Runs inside a declared window per T01 — on a single-node rail with a six- Runs inside a declared window per T01 — on a single-node rail with a six-
consumer connection ceiling, a saturation probe is an outage if run carelessly. consumer connection ceiling, a saturation probe is an outage if run carelessly.
Implementation progress, 2026-08-21: `src/whitehat_security/capacity.py` Implementation progress, 2026-08-22: `src/whitehat_security/capacity.py`
records baseline/loaded latency, errors and throughput per consumer, governor records baseline/loaded latency, errors and throughput per consumer, governor
binding, aggressor peak/ceiling and neighbour degradation. No live load has binding, aggressor peak/ceiling and neighbour degradation. The in-process
been generated; the required operator-approved substrate window and ceilings fixture is registered as `fixture-capacity`. `shared-substrate` is `pending`.
do not yet exist. No live load has been generated; the required operator-approved substrate
window and aggressor ceiling do not yet exist.
### T07 — Reporting into risk-nexus ### T07 — Reporting into risk-nexus
@ -297,9 +299,10 @@ Completed 2026-08-22 as a repository contract, not a cluster provision:
- Rate watcher, lease cleanup, default-deny plane manifests, runner identity. - Rate watcher, lease cleanup, default-deny plane manifests, runner identity.
- Automatic outbox delivery of target reports only. - Automatic outbox delivery of target reports only.
`ops-mason` still has to provision namespace, network policy and a real `ops-mason` was asked on 2026-08-22 to provision only the namespace, default
custody projection after a new engagement ID exists. That provision is not deny policy and runner service account from `plane/`. That message does not
this task, and this task does not authorize it. authorize a pod, a credential, or traffic. A real custody projection still
waits on a new engagement ID.
## Sequencing ## Sequencing