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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue