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:
parent
4882c2d47a
commit
7e83a66573
22 changed files with 501 additions and 74 deletions
1
Makefile
1
Makefile
|
|
@ -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
|
||||||
|
|
|
||||||
7
SCOPE.md
7
SCOPE.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
31
engagements/2026-08-22-plane-provision.md
Normal file
31
engagements/2026-08-22-plane-provision.md
Normal 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.
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
99
evidence/offline-e3-calibration.json
Normal file
99
evidence/offline-e3-calibration.json
Normal 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"
|
||||||
|
}
|
||||||
|
|
@ -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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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/`.
|
||||||
|
|
|
||||||
29
targets/fixture-capacity.json
Normal file
29
targets/fixture-capacity.json
Normal 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
39
targets/fixture-e3.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
22
targets/platform-pg-e3.json
Normal file
22
targets/platform-pg-e3.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
21
targets/shared-substrate-capacity.json
Normal file
21
targets/shared-substrate-capacity.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue