Admit live E2 from a value-safe custody receipt
WH-ENG-20260822-AUDIT-E2-02 projected and then aborted: admit-plane had no receipt adapter, so the runner sent zero packets. Consume custody receipts as handles only, keep unconnected admission fail-closed, and retire -02. Assistant: grok Assistant-Session: 01a02670-3345-76f2-a014-70fde8e2a2bb
This commit is contained in:
parent
b6c1806680
commit
45548e44a2
14 changed files with 221 additions and 42 deletions
6
SCOPE.md
6
SCOPE.md
|
|
@ -69,8 +69,10 @@ boundary always holds.
|
|||
- `T01` is complete: the rules of engagement were accepted on 2026-08-21.
|
||||
- `T02` is complete: the per-axis attacker model is recorded in
|
||||
`docs/attacker-model.md`.
|
||||
- `T03` is in progress: `WH-ENG-20260822-AUDIT-E2-01` expired at 18:15Z with
|
||||
no live run. A new engagement ID is required for a retry.
|
||||
- `T03` is in progress: `WH-ENG-20260822-AUDIT-E2-01` expired unused;
|
||||
`WH-ENG-20260822-AUDIT-E2-02` aborted after projection because admission
|
||||
had no receipt adapter. Zero packets. A later live run needs a new ID and
|
||||
`admit-plane --receipt`.
|
||||
- `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: 24-hour cadence, offline evaluator and in-process
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ retired.
|
|||
|
||||
```sh
|
||||
PYTHONPATH=src python3 -m whitehat_security.cli validate-engagement engagements/<record>.json
|
||||
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane engagements/<record>.json targets/<target>.json
|
||||
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane engagements/<record>.json targets/<target>.json --receipt <custody-receipt.json>
|
||||
```
|
||||
|
||||
7. Confirm target identity/revision and probe attribution before the first
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ A live E2 run is admitted only when every item below is true:
|
|||
5. The runner namespace is `whitehat`, the image digest is pinned, and
|
||||
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.
|
||||
6. A value-safe custody **receipt** is supplied to `admit-plane --receipt`.
|
||||
The receipt names handles, mount keys, expiry and `secret_values_observed:
|
||||
false`. It must not contain token values. Without a receipt, live admission
|
||||
fails closed and requests no credential.
|
||||
7. The kill switch is clear.
|
||||
8. The rate watcher is attached. Cleanup revokes the lease.
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ requested.
|
|||
|
||||
```sh
|
||||
PYTHONPATH=src python3 -m whitehat_security.cli validate-targets targets
|
||||
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane engagements/<record>.json targets/<target>.json
|
||||
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane engagements/<record>.json targets/<target>.json --receipt <custody-receipt.json>
|
||||
PYTHONPATH=src python3 -m whitehat_security.cli kill-switch
|
||||
PYTHONPATH=src python3 -m whitehat_security.cli deliver evidence/<run>.json --outbox outbox
|
||||
```
|
||||
|
|
@ -47,7 +47,8 @@ Create `plane/KILL` to abort independently of the runner process.
|
|||
|
||||
## Retired identifiers
|
||||
|
||||
`WH-ENG-20260821-AUDIT-E2`, `WH-ENG-20260821-TENANT-E2`, and
|
||||
`WH-ENG-20260822-AUDIT-E2-01` are terminal. The last of those expired at
|
||||
18:15Z with no projection-ready notice and zero packets. A later live E2 run
|
||||
needs a new ID, a new window, and a fresh attended projection.
|
||||
`WH-ENG-20260821-AUDIT-E2`, `WH-ENG-20260821-TENANT-E2`,
|
||||
`WH-ENG-20260822-AUDIT-E2-01`, and `WH-ENG-20260822-AUDIT-E2-02` are terminal.
|
||||
`-02` projected and then aborted: admission had no receipt adapter, so the
|
||||
runner sent zero packets. A later live E2 run needs a new ID, a new window,
|
||||
a custody receipt, and `admit-plane --receipt`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
"engagement_id": "WH-ENG-20260822-AUDIT-E2-02",
|
||||
"status": "approved",
|
||||
"status": "aborted",
|
||||
"aborted_at": "2026-08-22T19:21:39Z",
|
||||
"abort_reason": "Attended projection succeeded at 19:17:54Z and the runner became Ready, but admit-plane failed closed: no live adapter consumed the value-safe custody receipt. Zero packets. Runner deleted 19:21:07Z; custody cleanup 19:21:39Z. Not E2 evidence. Identifier is terminal.",
|
||||
"proposal_at": "2026-08-22T18:36:50Z",
|
||||
"proposal_reason": "Fresh attended retry after WH-ENG-20260822-AUDIT-E2-01 expired with zero packets. The terminal identifier, identities and fixtures are not reused.",
|
||||
"authorization_id": "operator-session-2026-08-22-e2-02-approval",
|
||||
|
|
|
|||
|
|
@ -1,21 +1,13 @@
|
|||
# WH-ENG-20260822-AUDIT-E2-02
|
||||
|
||||
Status: **approved and waiting**. The engagement is fail-closed until
|
||||
`2026-08-22T19:15:00Z` and until custody reports projection ready.
|
||||
Status: **aborted**. Not E2 evidence.
|
||||
|
||||
| Gate | State |
|
||||
| --- | --- |
|
||||
| Operator approval | `2026-08-22T18:42:50Z` in the coordinating session |
|
||||
| Owner acknowledgement | `2026-08-22T18:45:39Z` after direct artifact review |
|
||||
| Foundational plane | applied by ops-mason `c26a6e5` |
|
||||
| Window | `2026-08-22T19:15:00Z`–`19:30:00Z` |
|
||||
| Image | `sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6` |
|
||||
| Custody projection | pending; do not mint before 19:15Z |
|
||||
| Runner pod | not applied |
|
||||
| Packets | none |
|
||||
Attended projection succeeded at `2026-08-22T19:17:54Z` and the runner pod
|
||||
became Ready. Target validation, engagement validation and the kill switch
|
||||
passed. `admit-plane` then failed closed because this repository had no
|
||||
adapter for a value-safe custody receipt. The runner was never invoked.
|
||||
Zero packets. Runner deleted `19:21:07Z`; custody cleanup `19:21:39Z`.
|
||||
Identifier is terminal.
|
||||
|
||||
Offline preflight must fail closed before the window and until the live custody
|
||||
broker confirms the two exact mounted identities.
|
||||
|
||||
Invocation, including required `--forged-event whitehat-e2-forged-b-20260822-02`:
|
||||
`engagements/runtime/WH-ENG-20260822-AUDIT-E2-02-invocation.md`.
|
||||
The fix is `admit-plane --receipt <projection-receipt.json>`, which issues a
|
||||
plane lease from handles only.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ from `whitehat admit-plane`. `WH-ENG-20260821-AUDIT-E2` and
|
|||
`WH-ENG-20260821-TENANT-E2` are cancelled and must not be reused. The pod
|
||||
manifest under `runtime/` is bound to a cancelled ID and must not be applied.
|
||||
|
||||
`WH-ENG-20260822-AUDIT-E2-01` expired at `2026-08-22T18:15:00Z` with no live
|
||||
run. Do not reuse it. A later attempt needs a new identifier and window.
|
||||
`WH-ENG-20260822-AUDIT-E2-01` expired unused. `WH-ENG-20260822-AUDIT-E2-02`
|
||||
aborted after projection because `admit-plane` had no receipt adapter; zero
|
||||
packets. Neither identifier may be reused. Live admission requires
|
||||
`--receipt` of a value-safe custody projection.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
This repository does not apply cluster objects, request credentials, or send
|
||||
packets. `ops-mason` provisions the foundational plane from these files
|
||||
**before** a fresh engagement is proposed. Cancelled or expired IDs
|
||||
`WH-ENG-20260821-AUDIT-E2`, `WH-ENG-20260821-TENANT-E2`, and
|
||||
`WH-ENG-20260822-AUDIT-E2-01` are terminal.
|
||||
`WH-ENG-20260821-AUDIT-E2`, `WH-ENG-20260821-TENANT-E2`,
|
||||
`WH-ENG-20260822-AUDIT-E2-01`, and `WH-ENG-20260822-AUDIT-E2-02` are terminal.
|
||||
|
||||
## Ordering
|
||||
|
||||
|
|
|
|||
21
schemas/projection-receipt.schema.json
Normal file
21
schemas/projection-receipt.schema.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "whitehat-projection-receipt/v1",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"engagement_id", "projected_at", "expires_at", "identities",
|
||||
"mounted_secret", "mounted_keys", "target_ready", "secret_values_observed"
|
||||
],
|
||||
"properties": {
|
||||
"engagement_id": {"type": "string", "minLength": 1},
|
||||
"projected_at": {"type": "string", "format": "date-time"},
|
||||
"expires_at": {"type": "string", "format": "date-time"},
|
||||
"identities": {"type": "array", "minItems": 1, "items": {"type": "string"}},
|
||||
"mounted_secret": {"type": "string", "pattern": "^whitehat/"},
|
||||
"mounted_keys": {"type": "array", "items": {"type": "string"}},
|
||||
"target_image_matches": {"type": "boolean"},
|
||||
"target_ready": {"const": true},
|
||||
"secret_values_observed": {"const": false}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ from .e3 import CADENCE, PROBES, e3_calibration
|
|||
from .engagement import AuthorizationError, Engagement
|
||||
from .fixtures import FixtureService, probe_suite
|
||||
from .model import RunReport, utc_now
|
||||
from .plane import KillSwitch, admit, default_broker, retired_ids
|
||||
from .plane import KillSwitch, ReceiptBroker, admit, default_broker, retired_ids
|
||||
from .reporting import queue_risk_nexus, risk_nexus_message
|
||||
from .targets import load_catalog, load_registration
|
||||
|
||||
|
|
@ -90,6 +90,7 @@ def main(argv: list[str] | None = None) -> None:
|
|||
admit_plane = commands.add_parser("admit-plane")
|
||||
admit_plane.add_argument("engagement")
|
||||
admit_plane.add_argument("registration")
|
||||
admit_plane.add_argument("--receipt", help="value-safe custody projection receipt")
|
||||
commands.add_parser("kill-switch")
|
||||
deliver = commands.add_parser("deliver")
|
||||
deliver.add_argument("report")
|
||||
|
|
@ -138,8 +139,11 @@ def main(argv: list[str] | None = None) -> None:
|
|||
try:
|
||||
record = Engagement.load(args.engagement)
|
||||
registration = load_registration(args.registration)
|
||||
broker = (
|
||||
ReceiptBroker.load(args.receipt) if args.receipt else default_broker(record)
|
||||
)
|
||||
lease = admit(engagement=record, registration=registration,
|
||||
broker=default_broker(record), kill_switch=KillSwitch(),
|
||||
broker=broker, kill_switch=KillSwitch(),
|
||||
retired=retired_ids())
|
||||
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError) as error:
|
||||
print(f"not authorized: {error}", file=sys.stderr)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ class Engagement:
|
|||
raise AuthorizationError(
|
||||
"engagement window elapsed with no live run; identifier must not be reused"
|
||||
)
|
||||
if self.raw.get("status") == "aborted":
|
||||
raise AuthorizationError(
|
||||
"engagement aborted without target evidence; identifier must not be reused"
|
||||
)
|
||||
if self.raw.get("status") == "proposed":
|
||||
raise AuthorizationError(
|
||||
"engagement is proposed; operator approval and owner acknowledgement are pending"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ TERMINAL_IDS = frozenset({
|
|||
"WH-ENG-20260821-AUDIT-E2",
|
||||
"WH-ENG-20260821-TENANT-E2",
|
||||
"WH-ENG-20260822-AUDIT-E2-01",
|
||||
"WH-ENG-20260822-AUDIT-E2-02",
|
||||
})
|
||||
APPROVAL_CLASSES = {
|
||||
"fixture-e2": frozenset({"e2-differential"}),
|
||||
|
|
@ -42,7 +43,7 @@ def retired_ids(engagements_dir: str | Path | None = None) -> set[str]:
|
|||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError, ValueError):
|
||||
continue
|
||||
if data.get("status") in {"cancelled", "expired"} and data.get("engagement_id"):
|
||||
if data.get("status") in {"cancelled", "expired", "aborted"} and data.get("engagement_id"):
|
||||
retired.add(data["engagement_id"])
|
||||
return retired
|
||||
|
||||
|
|
@ -113,6 +114,82 @@ class CredentialBroker(Protocol):
|
|||
...
|
||||
|
||||
|
||||
FORBIDDEN_RECEIPT_KEYS = {
|
||||
"token", "token_a", "token_b", "password", "secret", "secret_value",
|
||||
"bearer", "credential", "value", "senders.json",
|
||||
}
|
||||
|
||||
|
||||
def _receipt_contains_secret_material(value: Any) -> bool:
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
lowered = str(key).lower().replace("-", "_")
|
||||
if lowered in FORBIDDEN_RECEIPT_KEYS:
|
||||
return True
|
||||
if _receipt_contains_secret_material(child):
|
||||
return True
|
||||
return False
|
||||
if isinstance(value, list):
|
||||
return any(_receipt_contains_secret_material(item) for item in value)
|
||||
return False
|
||||
|
||||
|
||||
class ReceiptBroker:
|
||||
"""Consume a value-safe custody receipt. Never reads or stores secret bytes."""
|
||||
|
||||
def __init__(self, receipt: dict[str, Any]) -> None:
|
||||
if _receipt_contains_secret_material(receipt):
|
||||
raise AuthorizationError("projection receipt contains secret material")
|
||||
if receipt.get("secret_values_observed") is not False:
|
||||
raise AuthorizationError("projection receipt did not prove secret values were unobserved")
|
||||
self.receipt = receipt
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path) -> "ReceiptBroker":
|
||||
return cls(json.loads(Path(path).read_text(encoding="utf-8")))
|
||||
|
||||
def project(self, engagement: Engagement, registration: dict[str, Any]
|
||||
) -> tuple[IdentityHandle, ...]:
|
||||
if self.receipt.get("engagement_id") != engagement.raw["engagement_id"]:
|
||||
raise AuthorizationError("projection receipt engagement_id does not match")
|
||||
if not self.receipt.get("target_ready"):
|
||||
raise AuthorizationError("projection receipt does not show a ready target")
|
||||
expires_at = self.receipt.get("expires_at")
|
||||
if not expires_at:
|
||||
raise AuthorizationError("projection receipt missing expires_at")
|
||||
expiry = datetime.fromisoformat(str(expires_at).replace("Z", "+00:00"))
|
||||
if expiry.tzinfo is None:
|
||||
raise AuthorizationError("projection receipt expiry must include a timezone")
|
||||
if datetime.now(UTC) > expiry.astimezone(UTC):
|
||||
raise AuthorizationError("projection receipt has expired")
|
||||
keys = list(self.receipt.get("mounted_keys") or [])
|
||||
count = int(registration["identities"]["count"])
|
||||
if count == 2 and keys != ["token-a", "token-b"]:
|
||||
raise AuthorizationError("E2 receipt must mount exactly token-a and token-b")
|
||||
names = list(self.receipt.get("identities") or [])
|
||||
if len(names) != count:
|
||||
raise AuthorizationError("projection receipt identity count does not match registration")
|
||||
secret = str(self.receipt.get("mounted_secret") or "")
|
||||
if not secret.startswith("whitehat/"):
|
||||
raise AuthorizationError("projection receipt must mount in namespace whitehat")
|
||||
lease_id = f"{engagement.raw['engagement_id']}:{self.receipt.get('projected_at', '')}"
|
||||
if count == 2:
|
||||
return (
|
||||
IdentityHandle("owner", "/var/run/secrets/whitehat/token-b", lease_id, expires_at),
|
||||
IdentityHandle("attacker", "/var/run/secrets/whitehat/token-a", lease_id, expires_at),
|
||||
)
|
||||
if count == 1:
|
||||
return (
|
||||
IdentityHandle("runtime", "/var/run/secrets/whitehat/runtime", lease_id, expires_at),
|
||||
)
|
||||
return ()
|
||||
|
||||
def revoke(self, lease_id: str) -> None:
|
||||
raise AuthorizationError(
|
||||
"receipt broker does not hold credentials; custody must revoke the projection"
|
||||
)
|
||||
|
||||
|
||||
class UnconnectedCustodyBroker:
|
||||
"""Fail-closed live broker. It never requests or returns credential values."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -63,8 +64,13 @@ def test_pending_owner_acknowledgement_fails_closed(tmp_path):
|
|||
load(tmp_path, data)
|
||||
|
||||
|
||||
def test_retry_record_is_authorized_only_inside_its_window():
|
||||
path = "engagements/2026-08-22-audit-core-e2-02.json"
|
||||
def test_retry_record_is_authorized_only_inside_its_window(tmp_path):
|
||||
data = json.loads(Path("engagements/2026-08-22-audit-core-e2-02.json").read_text())
|
||||
data["status"] = "approved"
|
||||
data.pop("aborted_at", None)
|
||||
data.pop("abort_reason", None)
|
||||
path = tmp_path / "retry.json"
|
||||
path.write_text(json.dumps(data), encoding="utf-8")
|
||||
with pytest.raises(AuthorizationError, match="has not started"):
|
||||
Engagement.load(path, now=datetime(2026, 8, 22, 19, 14, tzinfo=UTC))
|
||||
engagement = Engagement.load(path, now=datetime(2026, 8, 22, 19, 15, tzinfo=UTC))
|
||||
|
|
@ -73,6 +79,14 @@ def test_retry_record_is_authorized_only_inside_its_window():
|
|||
Engagement.load(path, now=datetime(2026, 8, 22, 19, 31, tzinfo=UTC))
|
||||
|
||||
|
||||
def test_aborted_record_fails_closed():
|
||||
with pytest.raises(AuthorizationError, match="aborted"):
|
||||
Engagement.load(
|
||||
"engagements/2026-08-22-audit-core-e2-02.json",
|
||||
now=datetime(2026, 8, 22, 19, 20, tzinfo=UTC),
|
||||
)
|
||||
|
||||
|
||||
def test_elapsed_record_fails_closed_even_inside_old_window(tmp_path):
|
||||
data = record()
|
||||
data["status"] = "expired"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ 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,
|
||||
KillSwitch, LocalBroker, RateWatcher, ReceiptBroker, UnconnectedCustodyBroker,
|
||||
admit, cleanup, retired_ids,
|
||||
)
|
||||
from whitehat_security.reporting import queue_risk_nexus
|
||||
|
|
@ -71,6 +71,7 @@ def test_retired_ids_include_cancelled_records():
|
|||
assert "WH-ENG-20260821-AUDIT-E2" in ids
|
||||
assert "WH-ENG-20260821-TENANT-E2" in ids
|
||||
assert "WH-ENG-20260822-AUDIT-E2-01" in ids
|
||||
assert "WH-ENG-20260822-AUDIT-E2-02" in ids
|
||||
|
||||
|
||||
def test_fixture_plane_admits_and_projects_handles_without_secrets(tmp_path):
|
||||
|
|
@ -165,6 +166,63 @@ def test_unconnected_broker_requests_no_credential(tmp_path):
|
|||
)
|
||||
|
||||
|
||||
def test_receipt_broker_issues_lease_without_secret_values(tmp_path):
|
||||
engagement = load_engagement(tmp_path, fixture_record(
|
||||
approval_class="live-e2", environment="build",
|
||||
target_id="audit-core",
|
||||
routes=["POST /v1/events"],
|
||||
plane_namespace="whitehat",
|
||||
runner_image_digest="sha256:abc",
|
||||
))
|
||||
receipt = {
|
||||
"engagement_id": "WH-ENG-FIXTURE-1",
|
||||
"projected_at": "2026-08-22T12:00:00Z",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"identities": ["whitehat-e2-a", "whitehat-e2-b"],
|
||||
"mounted_secret": "whitehat/whitehat-e2-audit-credentials",
|
||||
"mounted_keys": ["token-a", "token-b"],
|
||||
"target_ready": True,
|
||||
"secret_values_observed": False,
|
||||
}
|
||||
lease = admit(
|
||||
engagement=engagement, registration=load_registration("targets/audit-core-e2.json"),
|
||||
broker=ReceiptBroker(receipt), kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||
now=NOW, retired=set(),
|
||||
)
|
||||
assert {handle.role for handle in lease.identities} == {"owner", "attacker"}
|
||||
rendered = repr(lease) + repr(lease.identities) + json.dumps(receipt)
|
||||
assert "token_urlsafe" not in rendered
|
||||
with pytest.raises(AuthorizationError, match="custody must revoke"):
|
||||
cleanup(lease, ReceiptBroker(receipt))
|
||||
|
||||
|
||||
def test_receipt_broker_rejects_secret_material(tmp_path):
|
||||
engagement = load_engagement(tmp_path, fixture_record(
|
||||
approval_class="live-e2", environment="build",
|
||||
target_id="audit-core",
|
||||
routes=["POST /v1/events"],
|
||||
plane_namespace="whitehat",
|
||||
runner_image_digest="sha256:abc",
|
||||
))
|
||||
receipt = {
|
||||
"engagement_id": "WH-ENG-FIXTURE-1",
|
||||
"projected_at": "2026-08-22T12:00:00Z",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"identities": ["whitehat-e2-a", "whitehat-e2-b"],
|
||||
"mounted_secret": "whitehat/whitehat-e2-audit-credentials",
|
||||
"mounted_keys": ["token-a", "token-b"],
|
||||
"target_ready": True,
|
||||
"secret_values_observed": False,
|
||||
"token": "must-not-appear",
|
||||
}
|
||||
with pytest.raises(AuthorizationError, match="secret material"):
|
||||
admit(
|
||||
engagement=engagement, registration=load_registration("targets/audit-core-e2.json"),
|
||||
broker=ReceiptBroker(receipt), kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||
now=NOW, retired=set(),
|
||||
)
|
||||
|
||||
|
||||
def test_e3_is_not_admitted_by_the_e2_plane(tmp_path):
|
||||
engagement = load_engagement(tmp_path, fixture_record(
|
||||
approval_class="e3", techniques=["e3-rls"],
|
||||
|
|
|
|||
|
|
@ -163,8 +163,10 @@ target request and are retired. ops-mason applied the foundational plane on
|
|||
2026-08-22T09:52:55Z; audit-core acknowledged at 2026-08-22T10:04:56Z
|
||||
(`32afceb`) after the fixture/digest correction `1418dac`. Offline preflight
|
||||
recorded a window that elapsed at 18:15Z with no projection-ready notice and
|
||||
zero packets. That identifier is expired and must not be reused. A later live
|
||||
E2 run needs a new ID, a new window, and a fresh attended projection.
|
||||
zero packets. `WH-ENG-20260822-AUDIT-E2-02` projected at 19:17:54Z and the
|
||||
runner became Ready, then `admit-plane` failed closed because no receipt
|
||||
adapter existed; zero packets, cleanup complete 19:21:39Z. Both identifiers
|
||||
are terminal. Live admission now consumes a value-safe custody receipt.
|
||||
Whitehat will not relabel tenant-engine to finish this task.
|
||||
|
||||
### T04 — Prove the probes fail
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue