diff --git a/SCOPE.md b/SCOPE.md index aef1f30..43c65ce 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -71,7 +71,9 @@ boundary always holds. `docs/attacker-model.md`. - `T03` is in progress: the differential core, audit-core adapter and test plane admission exist. `tenant-engine` is registered `not_applicable` for - E2. Live audit-core still needs a new engagement and a provisioned plane. + E2. The foundational plane is applied. `WH-ENG-20260822-AUDIT-E2-01` is + proposed; live audit-core still needs approval, owner acknowledgement and + a connected custody broker. - `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 diff --git a/docs/operator-runbook.md b/docs/operator-runbook.md index 9b61140..da46838 100644 --- a/docs/operator-runbook.md +++ b/docs/operator-runbook.md @@ -28,8 +28,9 @@ PYTHONPATH=src python3 -m whitehat_security.cli validate-targets targets PYTHONPATH=src python3 -m whitehat_security.cli kill-switch ``` -`admit-plane` must succeed against a **new** engagement ID. The default live -broker is unconnected and fails closed without requesting a credential. +`admit-plane` must succeed against a **new, approved** engagement ID. A +`proposed` record fails closed. The default live broker is unconnected and +fails closed without requesting a credential. Create `plane/KILL` to abort independently of the runner. Cancelled IDs `WH-ENG-20260821-AUDIT-E2` and `WH-ENG-20260821-TENANT-E2` are diff --git a/docs/test-plane.md b/docs/test-plane.md index 1f3bd40..a3ff5c9 100644 --- a/docs/test-plane.md +++ b/docs/test-plane.md @@ -47,6 +47,7 @@ Create `plane/KILL` to abort independently of the runner process. ## Retired identifiers -`WH-ENG-20260821-AUDIT-E2` and `WH-ENG-20260821-TENANT-E2` are terminal. A new -live run needs a new ID after the cluster plane exists and the custody broker -can project identities without exposing values. +`WH-ENG-20260821-AUDIT-E2` and `WH-ENG-20260821-TENANT-E2` are terminal. The +foundational plane is provisioned first. `WH-ENG-20260822-AUDIT-E2-01` is the +next live-e2 **proposal**; it stays `proposed` until operator approval and +owner acknowledgement exist. The live custody broker remains unconnected. diff --git a/engagements/2026-08-22-audit-core-e2.json b/engagements/2026-08-22-audit-core-e2.json new file mode 100644 index 0000000..f95edfb --- /dev/null +++ b/engagements/2026-08-22-audit-core-e2.json @@ -0,0 +1,68 @@ +{ + "engagement_id": "WH-ENG-20260822-AUDIT-E2-01", + "status": "proposed", + "proposal_at": "2026-08-22T09:44:00Z", + "proposal_reason": "Fresh live-e2 proposal after foundational plane apply. Operator approval and target-owner acknowledgement are pending. Zero target requests. Do not reuse WH-ENG-20260821-AUDIT-E2.", + "authorization_id": "pending-operator-approval", + "authorizer": null, + "approved_at": null, + "expires_at": "2026-08-22T18:15:00Z", + "target": "http://audit-core.audit-core.svc.cluster.local:8080", + "target_id": "audit-core", + "target_owner": "audit-core / infrastructure operator", + "target_owner_acknowledged_at": null, + "target_owner_acknowledgement_status": "pending", + "environment": "production", + "production_approval": null, + "approval_class": "live-e2", + "plane_namespace": "whitehat", + "runner_image_digest": "sha256:7febc28e8a828dbc245144a38e5728e0fbf496b594dd7591170b450a1265fb10", + "namespace": "whitehat", + "source": "dedicated pod whitehat/whitehat-e2-audit, labels whitehat.security/plane=true, whitehat.security/target=audit-core, whitehat.security/engagement=WH-ENG-20260822-AUDIT-E2-01, serviceAccount whitehat-runner, automountServiceAccountToken=false", + "source_image": "forgejo.coulomb.social/coulomb/audit-core@sha256:7febc28e8a828dbc245144a38e5728e0fbf496b594dd7591170b450a1265fb10", + "control_plane_scope": [ + "GET apps/v1 deployment audit-core/audit-core for target revision only", + "GET v1 service audit-core/audit-core for target identity only", + "CREATE/GET/EXEC/DELETE v1 pod whitehat/whitehat-e2-audit only; EXEC may stream runners/audit_core_e2.py to python3 stdin and invoke no other program", + "mount but never GET v1 secret whitehat/whitehat-e2-audit-credentials provisioned by the custody owner", + "GET pod log whitehat/whitehat-e2-audit only" + ], + "routes": [ + "GET /readyz", + "POST /v1/events", + "GET /v1/events/{event_id}", + "GET /v1/events?correlation_id={correlation_id}" + ], + "fixture_ids": [ + "tenant:trial:whitehat-a-20260822", "tenant:trial:whitehat-b-20260822", + "whitehat-e2-event-a-20260822", "whitehat-e2-event-b-20260822", + "whitehat-e2-absent-20260822", "whitehat-e2-correlation-20260822" + ], + "credential_lane": "audit-core sender registry through its sanctioned custody owner; no credential value retained", + "credential_role": "two temporary ordinary tenant-scoped may_read/may_write fixture senders bound to source=whitehat-security", + "credential_max_ttl_seconds": 900, + "techniques": ["e2-differential"], + "prohibited_techniques": [ + "discovery", "direct-database-access", "saturation", "large-payload", + "real-data-collection", "adjacent-service-follow-up", + "any control-plane operation outside control_plane_scope" + ], + "rate_limit_per_minute": 10, + "max_concurrency": 1, + "maximum_requests": 30, + "window_start": "2026-08-22T18:00:00Z", + "window_end": "2026-08-22T18:15:00Z", + "operator_contact": "repository operator in coordinating session", + "abort_contact": "repository operator in coordinating session", + "abort_thresholds": { + "p95_latency_ms": 500, + "probe_attributable_5xx": 1, + "unexpected_or_non_fixture_data": 1, + "missing_attribution": 1 + }, + "posture_claim": "implemented E2; currently evidenced E1", + "attacker_model": "E2-authenticated-tenant-a", + "finding_destination": "risk-nexus", + "plane_bundle_digest": "9636f48f0b994118ff60a8c014e0099486945d66a2b3d3582dc57a09862b2035", + "target_ingress": "audit-core-whitehat-ingress at audit-core 5b5196e; namespace=whitehat AND whitehat.security/plane=true AND whitehat.security/target=audit-core" +} diff --git a/engagements/2026-08-22-audit-core-e2.md b/engagements/2026-08-22-audit-core-e2.md new file mode 100644 index 0000000..992f16a --- /dev/null +++ b/engagements/2026-08-22-audit-core-e2.md @@ -0,0 +1,28 @@ +# WH-ENG-20260822-AUDIT-E2-01 — proposal + +Status: **proposed**. This is not operator approval, not target-owner +acknowledgement, and not authorization to send a packet. + +Reserved by audit-core in `c45151e1-df1a-4306-a70a-83be7875f01c` after Mason +applied the foundational plane (`c26a6e5`). The cancelled ID +`WH-ENG-20260821-AUDIT-E2` remains terminal. + +| Field | Proposed value | +| --- | --- | +| Target | `audit-core.audit-core.svc.cluster.local:8080` | +| Registration | `targets/audit-core-e2.json` (`applicable`, `live-e2`) | +| Source | pod `whitehat/whitehat-e2-audit` with plane and target labels, SA `whitehat-runner`, no token | +| Image pin | `forgejo.coulomb.social/coulomb/audit-core@sha256:7febc28e8a828dbc245144a38e5728e0fbf496b594dd7591170b450a1265fb10` | +| Identities | two ordinary tenant-scoped `may_read`/`may_write` senders, `source=whitehat-security`, TTL ≤ 900s, mount-only | +| Routes | `GET /readyz`, `POST /v1/events`, `GET /v1/events/{event_id}`, `GET /v1/events?correlation_id=` | +| Ceiling | concurrency 1; 10/min; at most 30 requests | +| Window | 2026-08-22T18:00:00Z–18:15:00Z (refresh at approval if stale) | +| Cleanup | revoke both leases; delete only named synthetic fixtures; prove cleanup | +| Reporting | `risk-nexus`, no severity | + +Offline preflight (`validate-engagement`, `admit-plane`) must fail closed while +`status` is `proposed` and the live custody broker is unconnected. That +failure is the expected result, not a pass. + +Do not apply `plane/runner-pod.yaml` against this ID until approval, +acknowledgement, and projected identities exist. diff --git a/engagements/2026-08-22-plane-provision.md b/engagements/2026-08-22-plane-provision.md index 84b7c48..f4d190e 100644 --- a/engagements/2026-08-22-plane-provision.md +++ b/engagements/2026-08-22-plane-provision.md @@ -1,6 +1,8 @@ # Plane provision request — not an engagement -Status: **coordination only; apply nothing from this repository** +Status: **foundational plane applied by ops-mason 2026-08-22** (`c26a6e5`). +This repository still applies nothing. Next: proposed engagement +`WH-ENG-20260822-AUDIT-E2-01`, not a runner or credential. `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 diff --git a/engagements/README.md b/engagements/README.md index 87d34e2..0e099cc 100644 --- a/engagements/README.md +++ b/engagements/README.md @@ -14,3 +14,7 @@ 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` is **proposed**. Offline validation must fail +closed until operator approval and target-owner acknowledgement are recorded +and the custody broker is connected. + diff --git a/plane/README.md b/plane/README.md index 3fa0640..117f7c1 100644 --- a/plane/README.md +++ b/plane/README.md @@ -1,10 +1,25 @@ # Governed test plane -Contract only. This repository does not apply cluster objects, request -credentials, or send packets. `ops-mason` provisions from these files after a -**new, unretired** engagement ID exists. Cancelled IDs +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 IDs `WH-ENG-20260821-AUDIT-E2` and `WH-ENG-20260821-TENANT-E2` are terminal. +## Ordering + +1. Foundational plane: Namespace `whitehat`, default-deny NetworkPolicy, + audit-core egress NetworkPolicy, ServiceAccount `whitehat-runner` with + `automountServiceAccountToken: false`. +2. A **new** unretired engagement proposal (`status: proposed`). +3. Operator approval and target-owner acknowledgement. +4. Custody projection of two short-lived identities into the runner mount. +5. `whitehat admit-plane`, then a runner pod. Not before. + +ops-mason applied step 1 on 2026-08-22 (evidence `c26a6e5`, bundle digest +`9636f48f0b994118ff60a8c014e0099486945d66a2b3d3582dc57a09862b2035`). The +namespace had zero Pods and zero Secrets at apply. That apply authorizes no +runner, credential, engagement or traffic. + ## Controls 1. Namespace `whitehat`, default-deny networking, explicit target egress. @@ -20,8 +35,8 @@ credentials, or send packets. `ops-mason` provisions from these files after a ## Standing versus live -`fixture-e2` is the in-process class already permitted by the rules of -engagement. `live-e2` still needs a dated engagement, target-owner -acknowledgement, plane namespace, pinned digest and a connected custody -broker. `e3` and `capacity` are separate approval classes and will not be -admitted by the E2 plane. +`fixture-e2`, `fixture-e3` and `fixture-capacity` are in-process classes +already permitted by the rules of engagement. `live-e2` still needs a dated +engagement, target-owner acknowledgement, the provisioned plane namespace, a +pinned digest and a connected custody broker. Live `e3` and `capacity` keep +separate approval classes and extra envelope fields. diff --git a/src/whitehat_security/engagement.py b/src/whitehat_security/engagement.py index 2b51f22..ff6ca59 100644 --- a/src/whitehat_security/engagement.py +++ b/src/whitehat_security/engagement.py @@ -39,6 +39,10 @@ class Engagement: def validate(self, *, now: datetime | None = None) -> None: if self.raw.get("status") == "cancelled": raise AuthorizationError("engagement is cancelled") + if self.raw.get("status") == "proposed": + raise AuthorizationError( + "engagement is proposed; operator approval and owner acknowledgement are pending" + ) current = now or datetime.now(UTC) start = _timestamp(self.raw["window_start"]) end = _timestamp(self.raw["window_end"]) diff --git a/tests/test_cli.py b/tests/test_cli.py index 25e68c3..dc321a6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -24,6 +24,28 @@ def test_kill_switch_is_clear_by_default(capsys): assert capsys.readouterr().out.strip() == "clear" +def test_validate_engagement_refuses_proposed_record(capsys): + with pytest.raises(SystemExit) as stopped: + main(["validate-engagement", "engagements/2026-08-22-audit-core-e2.json"]) + assert stopped.value.code == 2 + err = capsys.readouterr().err + assert err.startswith("not authorized:") + assert "proposed" in err + + +def test_admit_plane_refuses_proposed_record(capsys): + with pytest.raises(SystemExit) as stopped: + main([ + "admit-plane", + "engagements/2026-08-22-audit-core-e2.json", + "targets/audit-core-e2.json", + ]) + assert stopped.value.code == 2 + err = capsys.readouterr().err + assert "not authorized:" in err + assert "proposed" in err + + def test_admit_plane_refuses_cancelled_engagement(capsys): with pytest.raises(SystemExit) as stopped: main([ diff --git a/tests/test_engagement.py b/tests/test_engagement.py index f081b3c..ee2e767 100644 --- a/tests/test_engagement.py +++ b/tests/test_engagement.py @@ -63,6 +63,15 @@ def test_pending_owner_acknowledgement_fails_closed(tmp_path): load(tmp_path, data) +def test_proposed_record_fails_closed_before_window_checks(tmp_path): + data = record() + data["status"] = "proposed" + data["approved_at"] = None + data["target_owner_acknowledged_at"] = None + with pytest.raises(AuthorizationError, match="proposed"): + load(tmp_path, data) + + def test_cancelled_record_fails_closed_even_with_owner_acknowledgement(tmp_path): data = record() data["status"] = "cancelled" diff --git a/workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md b/workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md index 4d0c4db..518726a 100644 --- a/workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md +++ b/workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md @@ -158,10 +158,11 @@ inventories live in `probe-packs/`. Target honesty lives in `targets/`: artifact, not a deferral); `flex-auth` is pending. The 2026-08-21 production engagement IDs were cancelled without sending a -target request and are retired. A live audit-core run still needs a **new** -engagement, target-owner acknowledgement, provisioned `whitehat` namespace, -and a connected custody broker. Whitehat will not relabel tenant-engine to -finish this task. +target request and are retired. ops-mason applied the foundational plane on +2026-08-22. `WH-ENG-20260822-AUDIT-E2-01` is a **proposed** live-e2 record +for audit-core: operator approval, owner acknowledgement and the custody +broker remain pending. Offline preflight must fail closed. Whitehat will not +relabel tenant-engine to finish this task. ### T04 — Prove the probes fail