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:
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.
- `T04` is complete for every applicable E2 probe: generic and audit-core
shaped fixtures fail known-bad and pass known-good in-process.
- `T05` is in progress with a 24-hour E3 cadence and offline evaluator.
- `T06` is in progress with a bounded characterization evaluator; no shared
substrate window is approved.
- `T05` is in progress: 24-hour cadence, offline evaluator and in-process
calibration exist; `platform-pg` is pending a live window.
- `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
report has not been produced.
- `T08` is the governed test plane contract. Cluster provisioning is outside

View file

@ -14,8 +14,13 @@ The executable plan is available with:
```sh
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
writes, `BYPASSRLS` on the runtime identity, and unsafe `SECURITY DEFINER`
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
and to produce a finding against the fixture with its tenant predicate removed.
`evidence/offline-calibration.json` is calibration evidence, not target
assurance.
`make fixture-evidence` also writes `evidence/offline-e3-calibration.json`.
Both files are calibration evidence, not target assurance.
## 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
and lease cleanup. The plane is a control, not authorization.
`fixture-e2` is the in-process class already permitted by §1. `live-e2` still
requires the dated engagement record, target-owner acknowledgement and
technique-specific operator approval required by §1, plus plane admission.
`e3`, recovery, saturation and destructive techniques keep separate explicit
approval classes and are not admitted by the E2 plane.
`fixture-e2`, `fixture-e3` and `fixture-capacity` are in-process classes
already permitted by §1. `live-e2` still requires the dated engagement
record, target-owner acknowledgement and technique-specific operator approval
required by §1, plus plane admission. Live `e3` additionally names the
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.

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.
3. The engagement is complete, in window, owner-acknowledged, and not a
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
engagement labels are present.
6. The custody broker projected exactly two identity *handles* (owner and
attacker). Credential values are not returned to the operator or agent.
engagement labels are present. Live E3 also names a `database`. Live
capacity also names an `aggressor_ceiling`.
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.
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
This directory stores sanitized run artifacts. `offline-calibration.json` is
generated from repository-created fixtures and proves only that the harness
distinguishes known-good from known-bad behavior.
This directory stores sanitized run artifacts. `offline-calibration.json` and
`offline-e3-calibration.json` are generated from repository-created fixtures
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,
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": {
"type": "array",
"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"}},
"identities": {
"type": "object",
"properties": {
"count": {"const": 2},
"count": {"type": "integer", "minimum": 0, "maximum": 2},
"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 .capacity import CapacitySample, characterize
from .differential import execute
from .e3 import CADENCE, PROBES
from .e3 import CADENCE, PROBES, e3_calibration
from .engagement import AuthorizationError, Engagement
from .fixtures import FixtureService, probe_suite
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("--outbox", default="outbox")
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")
message = commands.add_parser("risk-message")
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]},
indent=2, sort_keys=True))
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":
print(json.dumps(capacity_fixture(), indent=2, sort_keys=True))
return

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Literal
from .model import Outcome
from .model import Outcome, utc_now
Expectation = Literal["zero_rows", "statement_rejected", "false", "documented_limit"]
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}")
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 = {
"interval": "24h",
"maximum_detection_window": "24h plus run and reporting latency",

View file

@ -19,9 +19,12 @@ TERMINAL_IDS = frozenset({
APPROVAL_CLASSES = {
"fixture-e2": frozenset({"e2-differential"}),
"live-e2": frozenset({"e2-differential"}),
"fixture-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"}),
}
FIXTURE_CLASSES = frozenset({"fixture-e2", "fixture-e3", "fixture-capacity"})
def repo_root() -> Path:
@ -102,7 +105,7 @@ class IdentityHandle:
class CredentialBroker(Protocol):
def project(self, engagement: Engagement, registration: dict[str, Any]
) -> tuple[IdentityHandle, IdentityHandle]:
) -> tuple[IdentityHandle, ...]:
...
def revoke(self, lease_id: str) -> None:
@ -113,7 +116,7 @@ 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]:
) -> tuple[IdentityHandle, ...]:
raise AuthorizationError(
"live custody broker is not connected; no credential was requested"
)
@ -132,20 +135,25 @@ class LocalBroker:
self._leases: dict[str, tuple[str, str]] = {}
def project(self, engagement: Engagement, registration: dict[str, Any]
) -> tuple[IdentityHandle, IdentityHandle]:
) -> tuple[IdentityHandle, ...]:
if registration.get("applicability") != "applicable":
raise AuthorizationError("broker will not project identities for a non-applicable target")
count = int(registration["identities"]["count"])
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
handles: list[IdentityHandle] = []
if count >= 1:
role = "owner" if count == 2 else "runtime"
path = f"/var/run/secrets/whitehat/{lease_id}/{'token-b' if count == 2 else 'runtime'}"
handles.append(IdentityHandle(role, path, lease_id, expires_at))
self._secrets[path] = os.urandom(32)
if count == 2:
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:
paths = self._leases.pop(lease_id, None)
@ -159,7 +167,7 @@ class LocalBroker:
class PlaneLease:
engagement: Engagement
registration: dict[str, Any]
identities: tuple[IdentityHandle, IdentityHandle]
identities: tuple[IdentityHandle, ...]
watcher: RateWatcher
lease_id: str
broker_name: str
@ -172,7 +180,7 @@ class PlaneLease:
def default_broker(engagement: Engagement) -> CredentialBroker:
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 UnconnectedCustodyBroker()
@ -212,32 +220,26 @@ def admit(*, engagement: Engagement, registration: dict[str, Any],
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"
)
_enforce_class_envelope(approval_class, engagement)
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")
count = int(registration["identities"]["count"])
if count:
identities = selected.project(engagement, registration)
if len(identities) != count:
raise AuthorizationError(f"broker must project exactly {count} identities")
expected = {2: {"owner", "attacker"}, 1: {"runtime"}}[count]
if {handle.role for handle in identities} != expected:
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(
per_minute=engagement.raw["rate_limit_per_minute"],
max_concurrency=engagement.raw["max_concurrency"],
@ -249,11 +251,31 @@ def admit(*, engagement: Engagement, registration: dict[str, Any],
registration=registration,
identities=identities,
watcher=watcher,
lease_id=identities[0].lease_id,
lease_id=lease_id,
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]:
broker.revoke(lease.lease_id)
return {

View file

@ -37,9 +37,15 @@ def load_registration(path: str | Path) -> dict[str, Any]:
if not data["routes"]:
raise AuthorizationError(f"{path}: applicable target must register routes")
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")
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")
if data.get("known_bad_calibration") in {None, "", "pending"}:
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. |
| `tenant-engine` | not_applicable | No tenant-A identity without tenant-B authority. |
| `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
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):
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):

View file

@ -8,6 +8,7 @@ from whitehat_security.differential import execute, execute_authorized
from whitehat_security.engagement import AuthorizationError, Engagement
from whitehat_security.fixtures import FixtureService, probe_suite
from whitehat_security.model import RunReport
from whitehat_security.e3 import e3_calibration
from whitehat_security.plane import (
KillSwitch, LocalBroker, RateWatcher, UnconnectedCustodyBroker,
admit, cleanup, retired_ids,
@ -59,6 +60,10 @@ def test_catalog_loads_honest_applicability():
assert catalog["audit-core"]["applicability"] == "applicable"
assert catalog["tenant-engine"]["applicability"] == "not_applicable"
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():
@ -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):
watcher = RateWatcher(per_minute=10, max_concurrency=1, max_requests=1)
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
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,
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
documented limit. A live database run still requires its separate authorized
target and window.
seven expected outcomes, keeps the SQL-compromise GUC reset labeled as E3's
documented limit, and calibrates known-good/known-bad in-process. `platform-pg`
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)
@ -244,11 +245,12 @@ acceptable.
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.
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
binding, aggressor peak/ceiling and neighbour degradation. No live load has
been generated; the required operator-approved substrate window and ceilings
do not yet exist.
binding, aggressor peak/ceiling and neighbour degradation. The in-process
fixture is registered as `fixture-capacity`. `shared-substrate` is `pending`.
No live load has been generated; the required operator-approved substrate
window and aggressor ceiling do not yet exist.
### 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.
- Automatic outbox delivery of target reports only.
`ops-mason` still has to provision namespace, network policy and a real
custody projection after a new engagement ID exists. That provision is not
this task, and this task does not authorize it.
`ops-mason` was asked on 2026-08-22 to provision only the namespace, default
deny policy and runner service account from `plane/`. That message does not
authorize a pod, a credential, or traffic. A real custody projection still
waits on a new engagement ID.
## Sequencing