Add governed test plane and close T04/T08
Encode fail-closed admission, target registrations, and a credential broker that never returns secret values. Calibrate audit-core shaped probes in-process. Send no packets and request no live credentials. Assistant: grok Assistant-Session: 01a02670-3345-76f2-a014-70fde8e2a2bb
This commit is contained in:
parent
0aab0cb4c6
commit
95129d7a35
35 changed files with 1599 additions and 103 deletions
2
Makefile
2
Makefile
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
check: test
|
check: test
|
||||||
PYTHONPATH=src python3 -m whitehat_security.cli validate-packs probe-packs
|
PYTHONPATH=src python3 -m whitehat_security.cli validate-packs probe-packs
|
||||||
|
PYTHONPATH=src python3 -m whitehat_security.cli validate-targets targets
|
||||||
|
PYTHONPATH=src python3 -m whitehat_security.cli kill-switch
|
||||||
|
|
||||||
test:
|
test:
|
||||||
PYTHONPATH=src python3 -m pytest -q
|
PYTHONPATH=src python3 -m pytest -q
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -28,6 +28,8 @@ probes most worth having are the ones an author would not think to write.
|
||||||
- Operator runbook: [`docs/operator-runbook.md`](docs/operator-runbook.md)
|
- Operator runbook: [`docs/operator-runbook.md`](docs/operator-runbook.md)
|
||||||
- Session cutoff and redesign handoff:
|
- Session cutoff and redesign handoff:
|
||||||
[`docs/session-cutoff-2026-08-22.md`](docs/session-cutoff-2026-08-22.md)
|
[`docs/session-cutoff-2026-08-22.md`](docs/session-cutoff-2026-08-22.md)
|
||||||
|
- Governed test plane: [`docs/test-plane.md`](docs/test-plane.md)
|
||||||
|
- Target registrations: [`targets/`](targets/)
|
||||||
- Target packs: [`probe-packs/`](probe-packs/)
|
- Target packs: [`probe-packs/`](probe-packs/)
|
||||||
|
|
||||||
## Offline calibration
|
## Offline calibration
|
||||||
|
|
@ -37,8 +39,9 @@ make check
|
||||||
make fixture-evidence
|
make fixture-evidence
|
||||||
```
|
```
|
||||||
|
|
||||||
This proves that every current differential probe passes an enforcing fixture
|
This proves that every current differential probe, including the audit-core
|
||||||
and detects the corresponding missing-tenant-predicate fixture. It sends no
|
shaped pack, passes an enforcing fixture and detects the corresponding
|
||||||
network traffic and is not evidence about a target service. Live execution
|
missing-tenant-predicate fixture. It sends no network traffic and is not
|
||||||
requires a current target-specific record based on
|
evidence about a target service. Live execution requires a current
|
||||||
[`engagements/template.json`](engagements/template.json).
|
target-specific record, a registered applicable target, and plane admission.
|
||||||
|
See [`docs/test-plane.md`](docs/test-plane.md).
|
||||||
|
|
|
||||||
17
SCOPE.md
17
SCOPE.md
|
|
@ -69,17 +69,20 @@ boundary always holds.
|
||||||
- `T01` is complete: the rules of engagement were accepted on 2026-08-21.
|
- `T01` is complete: the rules of engagement were accepted on 2026-08-21.
|
||||||
- `T02` is complete: the per-axis attacker model is recorded in
|
- `T02` is complete: the per-axis attacker model is recorded in
|
||||||
`docs/attacker-model.md`.
|
`docs/attacker-model.md`.
|
||||||
- `T03` is in progress: the differential core and audit-core/tenant-engine
|
- `T03` is in progress: the differential core, audit-core adapter and test
|
||||||
target packs exist, but target run records are still gated.
|
plane admission exist. `tenant-engine` is registered `not_applicable` for
|
||||||
- `T04` is in progress: all generic read/write operations detect the
|
E2. Live audit-core still needs a new engagement and a provisioned plane.
|
||||||
known-bad fixture; target-specific calibration follows executable adapters.
|
- `T04` is complete for every applicable E2 probe: generic and audit-core
|
||||||
|
shaped fixtures fail known-bad and pass known-good in-process.
|
||||||
- `T05` is in progress with a 24-hour E3 cadence and offline evaluator.
|
- `T05` is in progress with a 24-hour E3 cadence and offline evaluator.
|
||||||
- `T06` is in progress with a bounded characterization evaluator; no shared
|
- `T06` is in progress with a bounded characterization evaluator; no shared
|
||||||
substrate window is approved.
|
substrate window is approved.
|
||||||
- `T07` is in progress with a report schema and risk-nexus message formatter;
|
- `T07` is in progress: reports can be queued to `outbox/`; the first target
|
||||||
the first target report has not yet been produced.
|
report has not been produced.
|
||||||
|
- `T08` is the governed test plane contract. Cluster provisioning is outside
|
||||||
|
this repository.
|
||||||
- No live probe traffic is authorized; each target still requires its own
|
- No live probe traffic is authorized; each target still requires its own
|
||||||
engagement record and approvals.
|
engagement record, plane admission and approvals.
|
||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
| task | WHITEHAT-WP-0001-T01 | done | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
| task | WHITEHAT-WP-0001-T01 | done | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
||||||
| task | WHITEHAT-WP-0001-T02 | done | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
| task | WHITEHAT-WP-0001-T02 | done | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
||||||
| task | WHITEHAT-WP-0001-T03 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
| task | WHITEHAT-WP-0001-T03 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
||||||
| task | WHITEHAT-WP-0001-T04 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
| task | WHITEHAT-WP-0001-T04 | done | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
||||||
| task | WHITEHAT-WP-0001-T05 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
| task | WHITEHAT-WP-0001-T05 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
||||||
| task | WHITEHAT-WP-0001-T06 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
| task | WHITEHAT-WP-0001-T06 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
||||||
| task | WHITEHAT-WP-0001-T07 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
| task | WHITEHAT-WP-0001-T07 | progress | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
||||||
|
| task | WHITEHAT-WP-0001-T08 | done | — | workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md |
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,23 @@ and to produce a finding against the fixture with its tenant predicate removed.
|
||||||
`evidence/offline-calibration.json` is calibration evidence, not target
|
`evidence/offline-calibration.json` is calibration evidence, not target
|
||||||
assurance.
|
assurance.
|
||||||
|
|
||||||
|
## Test plane
|
||||||
|
|
||||||
|
Live execution is gated by [`docs/test-plane.md`](test-plane.md). Before any
|
||||||
|
target run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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.
|
||||||
|
Create `plane/KILL` to abort independently of the runner.
|
||||||
|
|
||||||
|
Cancelled IDs `WH-ENG-20260821-AUDIT-E2` and `WH-ENG-20260821-TENANT-E2` are
|
||||||
|
retired.
|
||||||
|
|
||||||
## Prepare a target run
|
## Prepare a target run
|
||||||
|
|
||||||
1. Select the target's probe pack and review every route with its owner.
|
1. Select the target's probe pack and review every route with its owner.
|
||||||
|
|
@ -27,10 +44,11 @@ assurance.
|
||||||
4. Provision only the two disposable tenants/objects named in the record.
|
4. Provision only the two disposable tenants/objects named in the record.
|
||||||
5. Obtain the ordinary modeled credential through its custody lane. Do not put
|
5. Obtain the ordinary modeled credential through its custody lane. Do not put
|
||||||
its value in the record or shell history.
|
its value in the record or shell history.
|
||||||
6. Validate within the approved window:
|
6. Validate within the approved window, then admit the plane:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
PYTHONPATH=src python3 -m whitehat_security.cli validate-engagement engagements/<record>.json
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Confirm target identity/revision and probe attribution before the first
|
7. Confirm target identity/revision and probe attribution before the first
|
||||||
|
|
@ -61,7 +79,9 @@ unexpected response by broadening routes, identities, discovery, or privilege.
|
||||||
proven; otherwise `pass`.
|
proven; otherwise `pass`.
|
||||||
- Validate the record against `schemas/run-report.schema.json`.
|
- Validate the record against `schemas/run-report.schema.json`.
|
||||||
- Route both passing and finding target reports to `risk-nexus`; never assign
|
- Route both passing and finding target reports to `risk-nexus`; never assign
|
||||||
severity. The message formatter is `whitehat risk-message <report.json>`.
|
severity. Queue the delivery with `whitehat deliver <report.json>` (fixture
|
||||||
|
calibration is refused). The message formatter is
|
||||||
|
`whitehat risk-message <report.json>`.
|
||||||
- Schedule the next run from the target's declared cadence.
|
- Schedule the next run from the target's declared cadence.
|
||||||
|
|
||||||
No target run is complete until its dated report and risk-nexus delivery are
|
No target run is complete until its dated report and risk-nexus delivery are
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ The reporter does not edit risk-nexus grading fields. `risk-nexus` decides
|
||||||
whether an observation is a finding or note, and owns severity, disclosure,
|
whether an observation is a finding or note, and owns severity, disclosure,
|
||||||
review cadence and escalation.
|
review cadence and escalation.
|
||||||
|
|
||||||
Offline calibration stays in this repository and is plainly labeled
|
Queue a target report without assigning severity:
|
||||||
`evidence_class: fixture`; it is not sent as if it were a target result.
|
|
||||||
|
```sh
|
||||||
|
PYTHONPATH=src python3 -m whitehat_security.cli deliver evidence/<run>.json --outbox outbox
|
||||||
|
```
|
||||||
|
|
||||||
|
Offline calibration stays in this repository and is plainly labeled
|
||||||
|
`evidence_class: fixture`; it is not sent as if it were a target result. The
|
||||||
|
deliver command refuses fixture evidence.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Status: **accepted**
|
Status: **accepted**
|
||||||
|
|
||||||
Version: 0.1
|
Version: 0.2
|
||||||
|
|
||||||
Date: 2026-08-18
|
Date: 2026-08-18
|
||||||
|
|
||||||
|
|
@ -137,7 +137,24 @@ known-bad fixture pass is a finding until independently explained. Every live
|
||||||
probe must first fail against its known-bad fixture and pass against the
|
probe must first fail against its known-bad fixture and pass against the
|
||||||
known-good fixture in the same revision.
|
known-good fixture in the same revision.
|
||||||
|
|
||||||
## 10. Approval
|
## 10. Governed test plane
|
||||||
|
|
||||||
|
Live runs are admitted only through the test plane documented in
|
||||||
|
[`test-plane.md`](test-plane.md): dedicated namespace, default-deny networking,
|
||||||
|
projected short-lived identities, registered target, kill switch, rate watcher
|
||||||
|
and lease cleanup. The plane is a control, not authorization.
|
||||||
|
|
||||||
|
`fixture-e2` is the in-process class already permitted by §1. `live-e2` still
|
||||||
|
requires the dated engagement record, target-owner acknowledgement and
|
||||||
|
technique-specific operator approval required by §1, plus plane admission.
|
||||||
|
`e3`, recovery, saturation and destructive techniques keep separate explicit
|
||||||
|
approval classes and are not admitted by the E2 plane.
|
||||||
|
|
||||||
|
Cancelled engagement identifiers are terminal and must not be reused.
|
||||||
|
|
||||||
|
This section authorizes no live target and no packet.
|
||||||
|
|
||||||
|
## 11. Approval
|
||||||
|
|
||||||
Approved by: **repository operator**, by explicit approval in the coordinating
|
Approved by: **repository operator**, by explicit approval in the coordinating
|
||||||
session
|
session
|
||||||
|
|
@ -149,3 +166,6 @@ Approved scope: these rules are accepted as the operating boundary for
|
||||||
fixture work. It does **not** authorize a live target or a packet to any target.
|
fixture work. It does **not** authorize a live target or a packet to any target.
|
||||||
Every live run still requires the engagement record, target-owner
|
Every live run still requires the engagement record, target-owner
|
||||||
acknowledgement and any technique-specific operator approval required by §1.
|
acknowledgement and any technique-specific operator approval required by §1.
|
||||||
|
|
||||||
|
v0.2 (2026-08-22) adds §10. It tightens admission; it does not expand
|
||||||
|
authorization or replace the 2026-08-21 acceptance.
|
||||||
|
|
|
||||||
|
|
@ -76,3 +76,10 @@ each run:
|
||||||
Resume only with new engagement IDs after that plane can issue the correct test
|
Resume only with new engagement IDs after that plane can issue the correct test
|
||||||
identities and enforce the complete envelope. The existing workplan remains the
|
identities and enforce the complete envelope. The existing workplan remains the
|
||||||
backlog: T01 and T02 are done; T03, T04, T05, T06, and T07 remain in progress.
|
backlog: T01 and T02 are done; T03, T04, T05, T06, and T07 remain in progress.
|
||||||
|
|
||||||
|
## Progress after cutoff
|
||||||
|
|
||||||
|
On 2026-08-22 the plane contract was encoded in this repository:
|
||||||
|
[`docs/test-plane.md`](test-plane.md), `targets/`, `plane/`, and
|
||||||
|
`whitehat_security.plane`. No live packet was sent. No credential was
|
||||||
|
requested. Cancelled engagement IDs remain terminal.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
# Target applicability for WHITEHAT-WP-0001
|
# Target applicability for WHITEHAT-WP-0001
|
||||||
|
|
||||||
Status: active review, 2026-08-22
|
Status: encoded in `targets/`, 2026-08-22
|
||||||
|
|
||||||
The E2 attacker model requires an authenticated identity that is legitimately
|
The E2 attacker model requires an authenticated identity that is legitimately
|
||||||
authorized for tenant A and not tenant B. A target name alone is insufficient;
|
authorized for tenant A and not tenant B. A target name alone is insufficient;
|
||||||
the target must expose a boundary where that identity exists.
|
the target must expose a boundary where that identity exists. Machine-readable
|
||||||
|
records live in [`targets/`](../targets/README.md). The test plane will not
|
||||||
|
admit a `not_applicable` or `pending` target.
|
||||||
|
|
||||||
## audit-core — applicable
|
## audit-core — applicable
|
||||||
|
|
||||||
|
|
|
||||||
48
docs/test-plane.md
Normal file
48
docs/test-plane.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Governed test plane
|
||||||
|
|
||||||
|
This is the missing control from the 2026-08-22 cutoff. It is a fail-closed
|
||||||
|
admission path, not a standing authorization to probe.
|
||||||
|
|
||||||
|
## What the plane is
|
||||||
|
|
||||||
|
A live E2 run is admitted only when every item below is true:
|
||||||
|
|
||||||
|
1. The target is registered in `targets/` with `applicability: applicable`.
|
||||||
|
2. Known-bad calibration for that target has already failed in-process.
|
||||||
|
3. The engagement is complete, in window, owner-acknowledged, and not a
|
||||||
|
retired ID.
|
||||||
|
4. `approval_class` is `live-e2` (or `fixture-e2` for in-process only).
|
||||||
|
5. The runner namespace is `whitehat`, the image digest is pinned, and
|
||||||
|
engagement labels are present.
|
||||||
|
6. The custody broker projected exactly two identity *handles* (owner and
|
||||||
|
attacker). Credential values are not returned to the operator or agent.
|
||||||
|
7. The kill switch is clear.
|
||||||
|
8. The rate watcher is attached. Cleanup revokes the lease.
|
||||||
|
|
||||||
|
If any item is false, `whitehat admit-plane` exits 2 and no identity is
|
||||||
|
requested.
|
||||||
|
|
||||||
|
## What the plane is not
|
||||||
|
|
||||||
|
- Not authorization. Rules of engagement §1 still apply.
|
||||||
|
- Not a packet. Manifests in `plane/` are a contract for `ops-mason`.
|
||||||
|
- Not a secret broker. `UnconnectedCustodyBroker` is the default for every
|
||||||
|
non-fixture environment and raises before any custody call.
|
||||||
|
- Not a way to finish tenant-engine E2. That target is `not_applicable`.
|
||||||
|
|
||||||
|
## Operator commands
|
||||||
|
|
||||||
|
```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 kill-switch
|
||||||
|
PYTHONPATH=src python3 -m whitehat_security.cli deliver evidence/<run>.json --outbox outbox
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -9,3 +9,8 @@ or route.
|
||||||
An engagement record contains authorization metadata only. Never put a token,
|
An engagement record contains authorization metadata only. Never put a token,
|
||||||
password, database URL, secret path value, or real tenant identifier here.
|
password, database URL, secret path value, or real tenant identifier here.
|
||||||
|
|
||||||
|
Live admission also requires a matching file in `targets/` and a plane lease
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
4
engagements/runtime/README.md
Normal file
4
engagements/runtime/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
The `audit-core-e2-pod.yaml` manifest is bound to cancelled engagement
|
||||||
|
`WH-ENG-20260821-AUDIT-E2`. Do not apply it. New live runs use
|
||||||
|
`plane/runner-pod.yaml` with a new unretired engagement ID after the plane
|
||||||
|
exists.
|
||||||
|
|
@ -5,10 +5,14 @@
|
||||||
"approved_at": "YYYY-MM-DDTHH:MM:SSZ",
|
"approved_at": "YYYY-MM-DDTHH:MM:SSZ",
|
||||||
"expires_at": "YYYY-MM-DDTHH:MM:SSZ",
|
"expires_at": "YYYY-MM-DDTHH:MM:SSZ",
|
||||||
"target": "https://target.example.invalid",
|
"target": "https://target.example.invalid",
|
||||||
|
"target_id": "registered-target-id",
|
||||||
"target_owner": "responsible repo or party",
|
"target_owner": "responsible repo or party",
|
||||||
"target_owner_acknowledged_at": "YYYY-MM-DDTHH:MM:SSZ",
|
"target_owner_acknowledged_at": "YYYY-MM-DDTHH:MM:SSZ",
|
||||||
"environment": "build",
|
"environment": "build",
|
||||||
"namespace": "namespace if applicable",
|
"approval_class": "live-e2",
|
||||||
|
"plane_namespace": "whitehat",
|
||||||
|
"runner_image_digest": "sha256:pin-me",
|
||||||
|
"namespace": "whitehat",
|
||||||
"source": "probe source and network position",
|
"source": "probe source and network position",
|
||||||
"routes": ["GET /exact/in-scope/route"],
|
"routes": ["GET /exact/in-scope/route"],
|
||||||
"fixture_ids": ["synthetic-tenant-a", "synthetic-tenant-b"],
|
"fixture_ids": ["synthetic-tenant-a", "synthetic-tenant-b"],
|
||||||
|
|
@ -19,6 +23,7 @@
|
||||||
"prohibited_techniques": ["discovery", "saturation", "real-data collection"],
|
"prohibited_techniques": ["discovery", "saturation", "real-data collection"],
|
||||||
"rate_limit_per_minute": 10,
|
"rate_limit_per_minute": 10,
|
||||||
"max_concurrency": 1,
|
"max_concurrency": 1,
|
||||||
|
"maximum_requests": 30,
|
||||||
"window_start": "YYYY-MM-DDTHH:MM:SSZ",
|
"window_start": "YYYY-MM-DDTHH:MM:SSZ",
|
||||||
"window_end": "YYYY-MM-DDTHH:MM:SSZ",
|
"window_end": "YYYY-MM-DDTHH:MM:SSZ",
|
||||||
"operator_contact": "operator contact",
|
"operator_contact": "operator contact",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"ended_at": "2026-08-21T21:51:31.697322Z",
|
"ended_at": "2026-08-21T22:43:03.740339Z",
|
||||||
"evidence_class": "fixture",
|
"evidence_class": "fixture",
|
||||||
"expected": {
|
"expected": {
|
||||||
"known_bad": "finding",
|
"known_bad": "finding",
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "eaad30a4dc5b105af70231b5a4e72b9fc61e64a73a24074f919efd6fb5f9e9ae",
|
"run_digest": "2804e68bca6d4652529b13014b55efa296eb81101726ff8d6634dff4b97c5f32",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "5b9c2a46c8b84818280ce239e6bb01157bbcdf93165b8bfa97dd2c8c9121280f",
|
"run_digest": "6095953dfd3d463aa844f1c51d20745bb5c0c5a79ce223c7445e57b6c323cd31",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "5b9c2a46c8b84818280ce239e6bb01157bbcdf93165b8bfa97dd2c8c9121280f",
|
"run_digest": "6095953dfd3d463aa844f1c51d20745bb5c0c5a79ce223c7445e57b6c323cd31",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 0,
|
"count": 0,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "de0385fce9a44845bc70a21d8561a6b3211538b2c32d9b35789042dfe594f223",
|
"run_digest": "4a8cefe3ea0fa3fb888d8c578dd227c6a12216868da197f82b1e4de23aedd0f0",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.items[]"
|
"$.items[]"
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "3985d8408fbce3de8f6cd6dbf9b2d70257bbaee903cc5bc5a3da61610f6689c0",
|
"run_digest": "29e86fd84a1c6aa038d8d1278d52c4470135bcc81db39b800940132e2761bc7b",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.items[]",
|
"$.items[]",
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "3985d8408fbce3de8f6cd6dbf9b2d70257bbaee903cc5bc5a3da61610f6689c0",
|
"run_digest": "29e86fd84a1c6aa038d8d1278d52c4470135bcc81db39b800940132e2761bc7b",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.items[]",
|
"$.items[]",
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "857d9822bdfb939e4f126efa577be8d4a9e64ab023f5d611ca581498a963d048",
|
"run_digest": "4a2f56562162e6bb6745ca18b8e7325c9565235506d3870bb907ba588ac4fadc",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 2,
|
"fixture_match_count": 2,
|
||||||
"run_digest": "06e7ab37f09fcbcb142cf1d09927885685ef9f9d273f2ec49186c40a16c04c18",
|
"run_digest": "5e5ea8b63082ebc7bcce23fd5b66098a2e4ee092dba8a1c4c2a562e70e7c9550",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -134,7 +134,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "bf9bfc96947e5da52b13b9e027ad6f5c115da06c05dc7c79b82ea6b3a6514965",
|
"run_digest": "3a686f08bad5214b56d3e0aa05c141998519809e5cb16499c3f3784a0becbe4a",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str"
|
"$.id:str"
|
||||||
|
|
@ -156,7 +156,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "65bb2998672433c35d92bdb9b15965b2eb72776280b5cecc95c9d94e9149af12",
|
"run_digest": "44a1c9a896f575f82c40a744d41519a1f84469cfe22fc889f6f68756da57d77f",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -167,7 +167,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "7e3e05697523dde79079b1a5cf6ed50e12177962742b8b7ddcf62d025daad6ad",
|
"run_digest": "f48030bb08aa0d07380f0cec2ff0f919c5312fa780e3388984e8d4873440189f",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -180,7 +180,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "49d1b1ff789c3cf3709f0e7fe0041dc6cdbbfc7106ee736bb9d8acfcbb13568b",
|
"run_digest": "5baf8016840cabb74202e381f5f163a463c1279382d8f74332f3b602f1c9704e",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -205,7 +205,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "15604312df72ed94d0b08f047fbe1e185e3bd6f49e41cb6cb6b779ba0c06be0a",
|
"run_digest": "6d9c1f7d5d5c20afb8d8337ba4214e04be05e1b2336edc6263720c2f349a04cc",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 1,
|
"fixture_match_count": 1,
|
||||||
"run_digest": "ec66dc06c3b3202bc3e2f233c0eed22848106a5dbb2d39ca90233bfbb853e852",
|
"run_digest": "32d054c129bb639b1beb7b8ca83a9556b88558facc84132e9fec63950cb4e66d",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.deleted:str"
|
"$.deleted:str"
|
||||||
|
|
@ -227,7 +227,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "961561666050ffa3e74a3ccf98a27859fd657a962767ff7a507cb6cca7a1cd5b",
|
"run_digest": "3853f8bf9f473716ce4fced2d44f3a58b411c078afc6f131b4a47eda4929f798",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -245,6 +245,154 @@
|
||||||
"tenant-B fixture state changed",
|
"tenant-B fixture state changed",
|
||||||
"attacker response differs from the absent-object reference"
|
"attacker response differs from the absent-object reference"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"observations": {
|
||||||
|
"absent": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "b6638dcf2a3cf24a99ea503a0661f8ec3011b22689bdd6777c98693e4d49f80c",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.error:str"
|
||||||
|
],
|
||||||
|
"status": 404
|
||||||
|
},
|
||||||
|
"attacker": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 3,
|
||||||
|
"run_digest": "099b50b271a61251f76de35b36f832e9d52711cdd3d2e086464e52f8ab075ec7",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.correlation_id:str",
|
||||||
|
"$.id:str",
|
||||||
|
"$.label:str",
|
||||||
|
"$.tenant:str"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 3,
|
||||||
|
"run_digest": "099b50b271a61251f76de35b36f832e9d52711cdd3d2e086464e52f8ab075ec7",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.correlation_id:str",
|
||||||
|
"$.id:str",
|
||||||
|
"$.label:str",
|
||||||
|
"$.tenant:str"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operation": "read",
|
||||||
|
"outcome": "finding",
|
||||||
|
"probe_id": "audit-event-by-id",
|
||||||
|
"reasons": [
|
||||||
|
"attacker response contains a tenant-B fixture marker",
|
||||||
|
"attacker response differs from the absent-object reference"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"observations": {
|
||||||
|
"absent": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 0,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "738fa714a179543df7c4cdf663dcd27d2c636e73a5c6aa006c07b083d513dd0d",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.events[]"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"attacker": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 2,
|
||||||
|
"fixture_match_count": 3,
|
||||||
|
"run_digest": "02a06a47c2f9de17df2d857b945d5629196e74d40314e0a1842e1f728a440c8d",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.events[]",
|
||||||
|
"$.events[]",
|
||||||
|
"$.events[].correlation_id:str",
|
||||||
|
"$.events[].id:str",
|
||||||
|
"$.events[].label:str",
|
||||||
|
"$.events[].tenant:str"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 2,
|
||||||
|
"fixture_match_count": 3,
|
||||||
|
"run_digest": "02a06a47c2f9de17df2d857b945d5629196e74d40314e0a1842e1f728a440c8d",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.events[]",
|
||||||
|
"$.events[]",
|
||||||
|
"$.events[].correlation_id:str",
|
||||||
|
"$.events[].id:str",
|
||||||
|
"$.events[].label:str",
|
||||||
|
"$.events[].tenant:str"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operation": "read",
|
||||||
|
"outcome": "finding",
|
||||||
|
"probe_id": "audit-correlation-slice",
|
||||||
|
"reasons": [
|
||||||
|
"attacker response contains a tenant-B fixture marker"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"observations": {
|
||||||
|
"absent": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "a8ee76d5c9e51366ae25603241b71d3377f08f3aaded0ff1537b8f1f9640b851",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.error:str"
|
||||||
|
],
|
||||||
|
"status": 403
|
||||||
|
},
|
||||||
|
"attacker": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 2,
|
||||||
|
"run_digest": "f57a7a61abe2d3e7b3af734900863d295a4b6eed8579729b86ad75d73dcb85bb",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.id:str",
|
||||||
|
"$.tenant:str"
|
||||||
|
],
|
||||||
|
"status": 202
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "2801a3c1b5f46865ac24d24a748dc7124ac3982b85dd20bdb596c033c008708f",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.id:str"
|
||||||
|
],
|
||||||
|
"status": 202
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operation": "create",
|
||||||
|
"outcome": "finding",
|
||||||
|
"probe_id": "audit-append-as-b",
|
||||||
|
"reasons": [
|
||||||
|
"attacker response contains a tenant-B fixture marker",
|
||||||
|
"tenant-B fixture state changed"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"known_good": [
|
"known_good": [
|
||||||
|
|
@ -254,7 +402,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "eddeeb2191de71535b4d2c06a88807ba797dff4b2c13481f06c522e0315bb7ea",
|
"run_digest": "056e35eda0bacdda3e2a862a5e325f5fe090b8090f83037b5172200ff7b0e6dc",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -265,7 +413,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "eddeeb2191de71535b4d2c06a88807ba797dff4b2c13481f06c522e0315bb7ea",
|
"run_digest": "056e35eda0bacdda3e2a862a5e325f5fe090b8090f83037b5172200ff7b0e6dc",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -276,7 +424,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "13c9649399717d67dec6441a3e6ee046a389a84549fc39538548bb7e2496df37",
|
"run_digest": "49ea6432de7c9a88d2b32a4601d4fa811436909127084edaed522c28bc627235",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -297,7 +445,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 0,
|
"count": 0,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "db750d043a8e87c08f86b35735cbde33060236fbf673934edd16a2ef8077b314",
|
"run_digest": "61f8492a8388851bdfe14592a200635255cbe24d068f524ae6de9d6b68260c3d",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.items[]"
|
"$.items[]"
|
||||||
|
|
@ -308,7 +456,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "a1cbcf47d0c2f17a97abc59e2a559729fc027337f4360be44287764a6b061e7e",
|
"run_digest": "3add37393c96826ead72b129dbf8ac0efd07c254231a7a5a40640d953e82320e",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.items[]",
|
"$.items[]",
|
||||||
|
|
@ -323,7 +471,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "1ce9addbaf53fb9dd8ced38fd0bbd237e7d338ac3ad31a2d8e06e168620d91c6",
|
"run_digest": "479d2a3ab215dd0a629ca719163e1454439b782193e5202bf34e65c0c3a7ccf9",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.items[]",
|
"$.items[]",
|
||||||
|
|
@ -346,7 +494,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "2375ef32a41c0e05c19b2e362c0b2d1e490b0ecf863b7db9a3fb2c7824650f88",
|
"run_digest": "e33ae1117bfbf3ea0f2beee3ada7a4b2e7105e3f8ff9ea0f32e1a458e97c8277",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -357,7 +505,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "2375ef32a41c0e05c19b2e362c0b2d1e490b0ecf863b7db9a3fb2c7824650f88",
|
"run_digest": "e33ae1117bfbf3ea0f2beee3ada7a4b2e7105e3f8ff9ea0f32e1a458e97c8277",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -368,7 +516,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "93a604c1def0003e5ba2ac6bb71607adfaa5ad27e6f314c9e135d22ed91f4d50",
|
"run_digest": "11a2bc634aa1f2cda1bb7f137339bc47f69c21f025b47fa6fd5795c0840b4adf",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str"
|
"$.id:str"
|
||||||
|
|
@ -387,7 +535,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "128ec07ad319e1bd03582118fcb672edee8bdb90cf04259fd1c70565a49dfb24",
|
"run_digest": "31c30991423c8f2d3dd4757e0fdd188b37f6470a6babad1d9700aef0f451da32",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -398,7 +546,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "128ec07ad319e1bd03582118fcb672edee8bdb90cf04259fd1c70565a49dfb24",
|
"run_digest": "31c30991423c8f2d3dd4757e0fdd188b37f6470a6babad1d9700aef0f451da32",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -409,7 +557,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "39041e5495f98db586db32437188e52a4d1afe349f9b29f1fcbb8c2ed163f750",
|
"run_digest": "fb069aca7ea87059083bee9517d11d411a68e8b1ba88a1bf59394f11c6ab83c2",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -430,7 +578,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "d494c9a631fb3af8ad9a36c369d6633d5baa727b078f1740c1e08fc811834959",
|
"run_digest": "9205aee8ba243fc42f8ccf89fad98523c85c39adaa2311e9263512b2cc6faa9e",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -441,7 +589,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 0,
|
"fixture_match_count": 0,
|
||||||
"run_digest": "d494c9a631fb3af8ad9a36c369d6633d5baa727b078f1740c1e08fc811834959",
|
"run_digest": "9205aee8ba243fc42f8ccf89fad98523c85c39adaa2311e9263512b2cc6faa9e",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.error:str"
|
"$.error:str"
|
||||||
|
|
@ -452,7 +600,7 @@
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixture_match_count": 3,
|
"fixture_match_count": 3,
|
||||||
"run_digest": "a5ea7cb1c5b72a7b06f989181b4ff39debe887134fcc69c0053602dbc82b98da",
|
"run_digest": "681e463a7d41ce5ff05da5bc75241a0a176e8a6c1fdb19e1e88cc8f9490e023b",
|
||||||
"schema": [
|
"schema": [
|
||||||
"$",
|
"$",
|
||||||
"$.id:str",
|
"$.id:str",
|
||||||
|
|
@ -466,6 +614,142 @@
|
||||||
"outcome": "pass",
|
"outcome": "pass",
|
||||||
"probe_id": "delete-b",
|
"probe_id": "delete-b",
|
||||||
"reasons": []
|
"reasons": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"observations": {
|
||||||
|
"absent": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "e41d24b31b123d4a33e4d94a1015005694eeb0d00cf243b6238ac76ce28020e7",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.error:str"
|
||||||
|
],
|
||||||
|
"status": 404
|
||||||
|
},
|
||||||
|
"attacker": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "e41d24b31b123d4a33e4d94a1015005694eeb0d00cf243b6238ac76ce28020e7",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.error:str"
|
||||||
|
],
|
||||||
|
"status": 404
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 3,
|
||||||
|
"run_digest": "4308d43196348f1b1e55796f2e27b89c90371261e7d0472706778ad60562ab70",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.correlation_id:str",
|
||||||
|
"$.id:str",
|
||||||
|
"$.label:str",
|
||||||
|
"$.tenant:str"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operation": "read",
|
||||||
|
"outcome": "pass",
|
||||||
|
"probe_id": "audit-event-by-id",
|
||||||
|
"reasons": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"observations": {
|
||||||
|
"absent": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 0,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "014cf19c288b1c2a1411f08523dac6664bd2f62418148b9ff7f6875ab3f30d53",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.events[]"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"attacker": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "603bc2c5c8f8f643d2bc9c1822e5a8c3414b181aab311410c6ae753ec50d56c3",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.events[]",
|
||||||
|
"$.events[]",
|
||||||
|
"$.events[].correlation_id:str",
|
||||||
|
"$.events[].id:str",
|
||||||
|
"$.events[].label:str",
|
||||||
|
"$.events[].tenant:str"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 3,
|
||||||
|
"run_digest": "e2715b7b38e5f4141f6017e362301cd27219f0e591bab2250679815eb6a3b937",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.events[]",
|
||||||
|
"$.events[]",
|
||||||
|
"$.events[].correlation_id:str",
|
||||||
|
"$.events[].id:str",
|
||||||
|
"$.events[].label:str",
|
||||||
|
"$.events[].tenant:str"
|
||||||
|
],
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operation": "read",
|
||||||
|
"outcome": "pass",
|
||||||
|
"probe_id": "audit-correlation-slice",
|
||||||
|
"reasons": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"observations": {
|
||||||
|
"absent": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "ead0052d3e827ac7333f98a802459109362838131fd81e6a14c74809b320195d",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.error:str"
|
||||||
|
],
|
||||||
|
"status": 403
|
||||||
|
},
|
||||||
|
"attacker": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "ead0052d3e827ac7333f98a802459109362838131fd81e6a14c74809b320195d",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.error:str"
|
||||||
|
],
|
||||||
|
"status": 403
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"content_type": "application/json",
|
||||||
|
"count": 1,
|
||||||
|
"fixture_match_count": 0,
|
||||||
|
"run_digest": "f84797611f1a1c39fbfd93d06e7007352ea47fa87250613c56df52a64a6322c4",
|
||||||
|
"schema": [
|
||||||
|
"$",
|
||||||
|
"$.id:str"
|
||||||
|
],
|
||||||
|
"status": 202
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operation": "create",
|
||||||
|
"outcome": "pass",
|
||||||
|
"probe_id": "audit-append-as-b",
|
||||||
|
"reasons": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"limitations": [
|
"limitations": [
|
||||||
|
|
@ -473,7 +757,7 @@
|
||||||
"No network request, database connection, or live credential was used."
|
"No network request, database connection, or live credential was used."
|
||||||
],
|
],
|
||||||
"outcome": "pass",
|
"outcome": "pass",
|
||||||
"run_id": "fixture-calibration-2026-08-21T21:51:31.696239Z",
|
"run_id": "fixture-calibration-2026-08-21T22:43:03.737244Z",
|
||||||
"schema_version": "whitehat-calibration/v1",
|
"schema_version": "whitehat-calibration/v1",
|
||||||
"started_at": "2026-08-21T21:51:31.696239Z"
|
"started_at": "2026-08-21T22:43:03.737244Z"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
outbox/README.md
Normal file
5
outbox/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Outbox
|
||||||
|
|
||||||
|
Queued `risk-nexus` delivery artifacts. Only `evidence_class: target` reports
|
||||||
|
are written here. This directory is not a network client; it stores the
|
||||||
|
message that still has to be handed to `risk-nexus`.
|
||||||
27
plane/README.md
Normal file
27
plane/README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# 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
|
||||||
|
`WH-ENG-20260821-AUDIT-E2` and `WH-ENG-20260821-TENANT-E2` are terminal.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
1. Namespace `whitehat`, default-deny networking, explicit target egress.
|
||||||
|
2. Immutable runner identity with required engagement labels and no standing
|
||||||
|
service-account token.
|
||||||
|
3. Custody broker projects two short-lived tenant-scoped identities into the
|
||||||
|
runner mount and revokes them. Values never enter Git, chat, evidence or
|
||||||
|
State Hub.
|
||||||
|
4. Pinned runner image digest on the engagement.
|
||||||
|
5. Independently usable kill switch: create `plane/KILL` or set
|
||||||
|
`WHITEHAT_KILL_SWITCH=1`. Admission fails closed while it is engaged.
|
||||||
|
6. Rate watcher and automatic lease cleanup in `whitehat_security.plane`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
9
plane/namespace.yaml
Normal file
9
plane/namespace.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# CONTRACT ONLY. Do not apply from this repository.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: whitehat
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: whitehat-security
|
||||||
|
whitehat.security/plane: "true"
|
||||||
|
pod-security.kubernetes.io/enforce: restricted
|
||||||
40
plane/network-policy.yaml
Normal file
40
plane/network-policy.yaml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# CONTRACT ONLY. Do not apply from this repository.
|
||||||
|
# Default deny in namespace whitehat; live E2 adds a named egress rule per
|
||||||
|
# registered target. This example names audit-core only as the currently
|
||||||
|
# applicable live E2 target.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny
|
||||||
|
namespace: whitehat
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes: ["Ingress", "Egress"]
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-audit-core-e2
|
||||||
|
namespace: whitehat
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
whitehat.security/target: audit-core
|
||||||
|
policyTypes: ["Egress"]
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: audit-core
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- protocol: TCP
|
||||||
|
port: 53
|
||||||
53
plane/runner-pod.yaml
Normal file
53
plane/runner-pod.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# CONTRACT ONLY. Do not apply from this repository.
|
||||||
|
# Replace PLACEHOLDERS after a new unretired engagement is admitted.
|
||||||
|
# Never reuse WH-ENG-20260821-AUDIT-E2 or WH-ENG-20260821-TENANT-E2.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: whitehat-e2-audit
|
||||||
|
namespace: whitehat
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: whitehat-security
|
||||||
|
app.kubernetes.io/component: whitehat-probe
|
||||||
|
whitehat.security/plane: "true"
|
||||||
|
whitehat.security/target: audit-core
|
||||||
|
whitehat.security/engagement: WH-ENG-PLACEHOLDER
|
||||||
|
spec:
|
||||||
|
serviceAccountName: whitehat-runner
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
activeDeadlineSeconds: 900
|
||||||
|
restartPolicy: Never
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 10001
|
||||||
|
runAsGroup: 10001
|
||||||
|
fsGroup: 10001
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: probe
|
||||||
|
image: PINNED_RUNNER_IMAGE_DIGEST
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command: ["python3", "-"]
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 5m, memory: 16Mi}
|
||||||
|
limits: {cpu: 50m, memory: 64Mi}
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
volumeMounts:
|
||||||
|
- name: credentials
|
||||||
|
mountPath: /var/run/secrets/whitehat
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: credentials
|
||||||
|
secret:
|
||||||
|
secretName: whitehat-e2-audit-credentials
|
||||||
|
defaultMode: 0440
|
||||||
|
items:
|
||||||
|
- key: token-a
|
||||||
|
path: token-a
|
||||||
|
- key: token-b
|
||||||
|
path: token-b
|
||||||
9
plane/service-account.yaml
Normal file
9
plane/service-account.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# CONTRACT ONLY. Do not apply from this repository.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: whitehat-runner
|
||||||
|
namespace: whitehat
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: whitehat-security
|
||||||
|
automountServiceAccountToken: false
|
||||||
28
schemas/target-registration.schema.json
Normal file
28
schemas/target-registration.schema.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "whitehat-target/v1",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": [
|
||||||
|
"schema_version", "target_id", "posture_claim", "attacker_model",
|
||||||
|
"applicability", "applicability_reason", "approval_classes", "routes",
|
||||||
|
"identities", "abort_telemetry"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {"const": "whitehat-target/v1"},
|
||||||
|
"applicability": {"enum": ["applicable", "not_applicable", "pending"]},
|
||||||
|
"approval_classes": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {"enum": ["fixture-e2", "live-e2", "e3", "capacity"]}
|
||||||
|
},
|
||||||
|
"routes": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"identities": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"count": {"const": 2},
|
||||||
|
"ttl_seconds": {"type": "integer", "minimum": 1, "maximum": 900}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/whitehat_security/audit_fixtures.py
Normal file
83
src/whitehat_security/audit_fixtures.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .differential import DifferentialProbe
|
||||||
|
from .model import Response
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuditFixture:
|
||||||
|
"""In-process audit-core shape used to calibrate the E2 pack. No sockets."""
|
||||||
|
|
||||||
|
enforce_tenant: bool
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.events = {
|
||||||
|
"event-a": {
|
||||||
|
"id": "event-a", "tenant": "tenant-a",
|
||||||
|
"correlation_id": "corr-shared", "label": "fixture-a",
|
||||||
|
},
|
||||||
|
"event-b": {
|
||||||
|
"id": "event-b", "tenant": "tenant-b",
|
||||||
|
"correlation_id": "corr-shared", "label": "fixture-b",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def snapshot_events(self) -> object:
|
||||||
|
return deepcopy(self.events)
|
||||||
|
|
||||||
|
def _visible(self, actor_tenant: str, item: dict) -> bool:
|
||||||
|
return (not self.enforce_tenant) or item["tenant"] == actor_tenant
|
||||||
|
|
||||||
|
def get(self, actor_tenant: str, event_id: str) -> Response:
|
||||||
|
item = self.events.get(event_id)
|
||||||
|
if item is None or not self._visible(actor_tenant, item):
|
||||||
|
return Response(404, {"error": "not_found"})
|
||||||
|
return Response(200, deepcopy(item))
|
||||||
|
|
||||||
|
def by_correlation(self, actor_tenant: str, correlation_id: str) -> Response:
|
||||||
|
items = [
|
||||||
|
deepcopy(item) for item in self.events.values()
|
||||||
|
if item["correlation_id"] == correlation_id and self._visible(actor_tenant, item)
|
||||||
|
]
|
||||||
|
return Response(200, {"events": items})
|
||||||
|
|
||||||
|
def append(self, actor_tenant: str, event: dict) -> Response:
|
||||||
|
if self.enforce_tenant and event["tenant"] != actor_tenant:
|
||||||
|
return Response(403, {"error": "forbidden"})
|
||||||
|
self.events[event["id"]] = deepcopy(event)
|
||||||
|
return Response(202, {"id": event["id"], "tenant": event["tenant"]})
|
||||||
|
|
||||||
|
|
||||||
|
def audit_probe_suite(service: AuditFixture) -> list[DifferentialProbe]:
|
||||||
|
markers = ("event-b", "tenant-b", "fixture-b")
|
||||||
|
forged = {
|
||||||
|
"id": "event-forged-b", "tenant": "tenant-b",
|
||||||
|
"correlation_id": "corr-shared", "label": "fixture-forged",
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
DifferentialProbe(
|
||||||
|
"audit-event-by-id", "read",
|
||||||
|
lambda: service.get("tenant-b", "event-b"),
|
||||||
|
lambda: service.get("tenant-a", "event-b"),
|
||||||
|
lambda: service.get("tenant-a", "absent"),
|
||||||
|
markers,
|
||||||
|
),
|
||||||
|
DifferentialProbe(
|
||||||
|
"audit-correlation-slice", "read",
|
||||||
|
lambda: service.by_correlation("tenant-b", "corr-shared"),
|
||||||
|
lambda: service.by_correlation("tenant-a", "corr-shared"),
|
||||||
|
lambda: service.by_correlation("tenant-a", "corr-absent"),
|
||||||
|
markers, hide_existence=False,
|
||||||
|
),
|
||||||
|
DifferentialProbe(
|
||||||
|
"audit-append-as-b", "create",
|
||||||
|
lambda: Response(202, {"id": "owner-control"}),
|
||||||
|
lambda: service.append("tenant-a", forged),
|
||||||
|
lambda: Response(403, {"error": "forbidden"}),
|
||||||
|
markers + ("event-forged-b",),
|
||||||
|
state=service.snapshot_events, hide_existence=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -6,19 +6,26 @@ import sys
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
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
|
||||||
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
|
||||||
from .reporting import risk_nexus_message
|
from .plane import KillSwitch, admit, default_broker, retired_ids
|
||||||
|
from .reporting import queue_risk_nexus, risk_nexus_message
|
||||||
|
from .targets import load_catalog, load_registration
|
||||||
|
|
||||||
|
|
||||||
def fixture_calibration() -> dict:
|
def fixture_calibration() -> dict:
|
||||||
started = utc_now()
|
started = utc_now()
|
||||||
good = [execute(probe) for probe in probe_suite(FixtureService(enforce_tenant=True))]
|
good_generic = probe_suite(FixtureService(enforce_tenant=True))
|
||||||
bad = [execute(probe) for probe in probe_suite(FixtureService(enforce_tenant=False))]
|
bad_generic = probe_suite(FixtureService(enforce_tenant=False))
|
||||||
|
good_audit = audit_probe_suite(AuditFixture(enforce_tenant=True))
|
||||||
|
bad_audit = audit_probe_suite(AuditFixture(enforce_tenant=False))
|
||||||
|
good = [execute(probe) for probe in (*good_generic, *good_audit)]
|
||||||
|
bad = [execute(probe) for probe in (*bad_generic, *bad_audit)]
|
||||||
detected = all(result.outcome == "finding" for result in bad)
|
detected = all(result.outcome == "finding" for result in bad)
|
||||||
rejected = all(result.outcome == "pass" for result in good)
|
rejected = all(result.outcome == "pass" for result in good)
|
||||||
return {
|
return {
|
||||||
|
|
@ -78,6 +85,15 @@ def main(argv: list[str] | None = None) -> None:
|
||||||
engagement.add_argument("path")
|
engagement.add_argument("path")
|
||||||
packs = commands.add_parser("validate-packs")
|
packs = commands.add_parser("validate-packs")
|
||||||
packs.add_argument("path")
|
packs.add_argument("path")
|
||||||
|
targets = commands.add_parser("validate-targets")
|
||||||
|
targets.add_argument("path")
|
||||||
|
admit_plane = commands.add_parser("admit-plane")
|
||||||
|
admit_plane.add_argument("engagement")
|
||||||
|
admit_plane.add_argument("registration")
|
||||||
|
commands.add_parser("kill-switch")
|
||||||
|
deliver = commands.add_parser("deliver")
|
||||||
|
deliver.add_argument("report")
|
||||||
|
deliver.add_argument("--outbox", default="outbox")
|
||||||
commands.add_parser("e3-plan")
|
commands.add_parser("e3-plan")
|
||||||
commands.add_parser("capacity-fixture")
|
commands.add_parser("capacity-fixture")
|
||||||
message = commands.add_parser("risk-message")
|
message = commands.add_parser("risk-message")
|
||||||
|
|
@ -108,6 +124,42 @@ def main(argv: list[str] | None = None) -> None:
|
||||||
validate_pack(path)
|
validate_pack(path)
|
||||||
print(f"validated {len(paths)} probe packs")
|
print(f"validated {len(paths)} probe packs")
|
||||||
return
|
return
|
||||||
|
if args.command == "validate-targets":
|
||||||
|
try:
|
||||||
|
catalog = load_catalog(args.path)
|
||||||
|
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError) as error:
|
||||||
|
print(f"not authorized: {error}", file=sys.stderr)
|
||||||
|
raise SystemExit(2) from None
|
||||||
|
print(f"validated {len(catalog)} target registrations")
|
||||||
|
return
|
||||||
|
if args.command == "admit-plane":
|
||||||
|
try:
|
||||||
|
record = Engagement.load(args.engagement)
|
||||||
|
registration = load_registration(args.registration)
|
||||||
|
lease = admit(engagement=record, registration=registration,
|
||||||
|
broker=default_broker(record), kill_switch=KillSwitch(),
|
||||||
|
retired=retired_ids())
|
||||||
|
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError) as error:
|
||||||
|
print(f"not authorized: {error}", file=sys.stderr)
|
||||||
|
raise SystemExit(2) from None
|
||||||
|
print(f"admitted: {lease.engagement.raw['engagement_id']} lease={lease.lease_id}")
|
||||||
|
return
|
||||||
|
if args.command == "kill-switch":
|
||||||
|
switch = KillSwitch()
|
||||||
|
if switch.engaged():
|
||||||
|
print(f"engaged: {switch.path}")
|
||||||
|
raise SystemExit(1)
|
||||||
|
print("clear")
|
||||||
|
return
|
||||||
|
if args.command == "deliver":
|
||||||
|
try:
|
||||||
|
report = RunReport(**json.loads(Path(args.report).read_text(encoding="utf-8")))
|
||||||
|
path = queue_risk_nexus(report, args.outbox)
|
||||||
|
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError, TypeError) as error:
|
||||||
|
print(f"not authorized: {error}", file=sys.stderr)
|
||||||
|
raise SystemExit(2) from None
|
||||||
|
print(f"queued: {path}")
|
||||||
|
return
|
||||||
if args.command == "e3-plan":
|
if args.command == "e3-plan":
|
||||||
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))
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from .engagement import Engagement
|
|
||||||
from .model import Observation, ProbeResult, Response, observe
|
from .model import Observation, ProbeResult, Response, observe
|
||||||
|
from .plane import PlaneLease
|
||||||
|
|
||||||
Request = Callable[[], Response]
|
Request = Callable[[], Response]
|
||||||
State = Callable[[], object]
|
State = Callable[[], object]
|
||||||
|
|
@ -49,12 +50,17 @@ def execute(probe: DifferentialProbe, *, salt: bytes | None = None) -> ProbeResu
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def execute_authorized(probe: DifferentialProbe, *, engagement: Engagement,
|
def execute_authorized(probe: DifferentialProbe, *, lease: PlaneLease,
|
||||||
route: str, salt: bytes | None = None) -> ProbeResult:
|
route: str, salt: bytes | None = None,
|
||||||
"""Live entry point: authorization is rechecked immediately before calls."""
|
now: datetime | None = None) -> ProbeResult:
|
||||||
engagement.validate()
|
"""Live entry point: plane admission is rechecked immediately before calls."""
|
||||||
engagement.permits(technique="e2-differential", route=route)
|
lease.engagement.validate(now=now)
|
||||||
|
lease.permits(technique="e2-differential", route=route)
|
||||||
|
lease.watcher.acquire()
|
||||||
|
try:
|
||||||
return execute(probe, salt=salt)
|
return execute(probe, salt=salt)
|
||||||
|
finally:
|
||||||
|
lease.watcher.release()
|
||||||
|
|
||||||
|
|
||||||
def _equivalent_denial(left: Observation, right: Observation) -> bool:
|
def _equivalent_denial(left: Observation, right: Observation) -> bool:
|
||||||
|
|
|
||||||
263
src/whitehat_security/plane.py
Normal file
263
src/whitehat_security/plane.py
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
from .engagement import AuthorizationError, Engagement
|
||||||
|
|
||||||
|
PLANE_NAMESPACE = "whitehat"
|
||||||
|
STANDING_E2_TTL = 900
|
||||||
|
TERMINAL_IDS = frozenset({
|
||||||
|
"WH-ENG-20260821-AUDIT-E2",
|
||||||
|
"WH-ENG-20260821-TENANT-E2",
|
||||||
|
})
|
||||||
|
APPROVAL_CLASSES = {
|
||||||
|
"fixture-e2": frozenset({"e2-differential"}),
|
||||||
|
"live-e2": frozenset({"e2-differential"}),
|
||||||
|
"e3": frozenset({"e3-rls"}),
|
||||||
|
"capacity": frozenset({"p1-noisy-neighbour", "p2-noisy-neighbour"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def retired_ids(engagements_dir: str | Path | None = None) -> set[str]:
|
||||||
|
retired = set(TERMINAL_IDS)
|
||||||
|
root = Path(engagements_dir) if engagements_dir else repo_root() / "engagements"
|
||||||
|
if not root.exists():
|
||||||
|
return retired
|
||||||
|
for path in root.glob("*.json"):
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError, ValueError):
|
||||||
|
continue
|
||||||
|
if data.get("status") == "cancelled" and data.get("engagement_id"):
|
||||||
|
retired.add(data["engagement_id"])
|
||||||
|
return retired
|
||||||
|
|
||||||
|
|
||||||
|
class KillSwitch:
|
||||||
|
def __init__(self, path: str | Path | None = None) -> None:
|
||||||
|
default = os.environ.get("WHITEHAT_KILL_SWITCH_PATH", str(repo_root() / "plane" / "KILL"))
|
||||||
|
self.path = Path(path) if path is not None else Path(default)
|
||||||
|
|
||||||
|
def engaged(self) -> bool:
|
||||||
|
flag = os.environ.get("WHITEHAT_KILL_SWITCH", "").strip().lower()
|
||||||
|
if flag in {"1", "true", "yes", "on"}:
|
||||||
|
return True
|
||||||
|
return self.path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
class RateWatcher:
|
||||||
|
def __init__(self, *, per_minute: int, max_concurrency: int, max_requests: int) -> None:
|
||||||
|
if max_concurrency != 1:
|
||||||
|
raise AuthorizationError("v0.1 permits exactly one in-flight API/database operation")
|
||||||
|
if not 1 <= per_minute <= 60:
|
||||||
|
raise AuthorizationError("API rate ceiling must be between 1 and 60 per minute")
|
||||||
|
if max_requests < 1:
|
||||||
|
raise AuthorizationError("request ceiling must be at least 1")
|
||||||
|
self.per_minute = per_minute
|
||||||
|
self.max_requests = max_requests
|
||||||
|
self._times: list[float] = []
|
||||||
|
self._in_flight = 0
|
||||||
|
self._count = 0
|
||||||
|
|
||||||
|
def acquire(self) -> None:
|
||||||
|
if self._in_flight:
|
||||||
|
raise AuthorizationError("concurrency ceiling reached")
|
||||||
|
now = time.monotonic()
|
||||||
|
self._times = [stamp for stamp in self._times if now - stamp < 60]
|
||||||
|
if len(self._times) >= self.per_minute:
|
||||||
|
raise AuthorizationError("rate ceiling reached")
|
||||||
|
if self._count >= self.max_requests:
|
||||||
|
raise AuthorizationError("request ceiling reached")
|
||||||
|
self._times.append(now)
|
||||||
|
self._count += 1
|
||||||
|
self._in_flight = 1
|
||||||
|
|
||||||
|
def release(self) -> None:
|
||||||
|
self._in_flight = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IdentityHandle:
|
||||||
|
role: str
|
||||||
|
mount_path: str
|
||||||
|
lease_id: str
|
||||||
|
expires_at: str
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"IdentityHandle(role={self.role!r}, mount_path={self.mount_path!r}, "
|
||||||
|
f"lease_id={self.lease_id!r}, expires_at={self.expires_at!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CredentialBroker(Protocol):
|
||||||
|
def project(self, engagement: Engagement, registration: dict[str, Any]
|
||||||
|
) -> tuple[IdentityHandle, IdentityHandle]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def revoke(self, lease_id: str) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
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]:
|
||||||
|
raise AuthorizationError(
|
||||||
|
"live custody broker is not connected; no credential was requested"
|
||||||
|
)
|
||||||
|
|
||||||
|
def revoke(self, lease_id: str) -> None:
|
||||||
|
raise AuthorizationError(
|
||||||
|
f"live custody broker is not connected; lease {lease_id} was not created"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalBroker:
|
||||||
|
"""In-process projection for fixture-e2. Secret bytes never leave this object."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._secrets: dict[str, bytes] = {}
|
||||||
|
self._leases: dict[str, tuple[str, str]] = {}
|
||||||
|
|
||||||
|
def project(self, engagement: Engagement, registration: dict[str, Any]
|
||||||
|
) -> tuple[IdentityHandle, IdentityHandle]:
|
||||||
|
if registration.get("applicability") != "applicable":
|
||||||
|
raise AuthorizationError("broker will not project identities for a non-applicable target")
|
||||||
|
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
|
||||||
|
|
||||||
|
def revoke(self, lease_id: str) -> None:
|
||||||
|
paths = self._leases.pop(lease_id, None)
|
||||||
|
if paths is None:
|
||||||
|
raise AuthorizationError("lease is unknown; cleanup cannot be proven")
|
||||||
|
for path in paths:
|
||||||
|
self._secrets.pop(path, None)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlaneLease:
|
||||||
|
engagement: Engagement
|
||||||
|
registration: dict[str, Any]
|
||||||
|
identities: tuple[IdentityHandle, IdentityHandle]
|
||||||
|
watcher: RateWatcher
|
||||||
|
lease_id: str
|
||||||
|
broker_name: str
|
||||||
|
|
||||||
|
def permits(self, *, technique: str, route: str) -> None:
|
||||||
|
self.engagement.permits(technique=technique, route=route)
|
||||||
|
if route not in self.registration["routes"]:
|
||||||
|
raise AuthorizationError(f"route not registered: {route}")
|
||||||
|
|
||||||
|
|
||||||
|
def default_broker(engagement: Engagement) -> CredentialBroker:
|
||||||
|
if (engagement.raw.get("environment") == "fixture"
|
||||||
|
and engagement.raw.get("approval_class") == "fixture-e2"):
|
||||||
|
return LocalBroker()
|
||||||
|
return UnconnectedCustodyBroker()
|
||||||
|
|
||||||
|
|
||||||
|
def admit(*, engagement: Engagement, registration: dict[str, Any],
|
||||||
|
broker: CredentialBroker | None = None,
|
||||||
|
kill_switch: KillSwitch | None = None,
|
||||||
|
now: datetime | None = None,
|
||||||
|
retired: set[str] | None = None) -> PlaneLease:
|
||||||
|
engagement.validate(now=now)
|
||||||
|
switch = kill_switch or KillSwitch()
|
||||||
|
if switch.engaged():
|
||||||
|
raise AuthorizationError("kill switch is engaged")
|
||||||
|
engagement_id = engagement.raw["engagement_id"]
|
||||||
|
if engagement_id in (retired if retired is not None else retired_ids()):
|
||||||
|
raise AuthorizationError("engagement id is retired and must not be reused")
|
||||||
|
if registration.get("applicability") != "applicable":
|
||||||
|
raise AuthorizationError(
|
||||||
|
f"target is {registration.get('applicability', 'unregistered')}"
|
||||||
|
)
|
||||||
|
target_id = engagement.raw.get("target_id")
|
||||||
|
if not target_id:
|
||||||
|
raise AuthorizationError("engagement missing target_id for plane admission")
|
||||||
|
if target_id != registration["target_id"]:
|
||||||
|
raise AuthorizationError("engagement target_id does not match registration")
|
||||||
|
approval_class = engagement.raw.get("approval_class")
|
||||||
|
allowed = APPROVAL_CLASSES.get(approval_class)
|
||||||
|
if allowed is None:
|
||||||
|
raise AuthorizationError("engagement missing or unknown approval_class")
|
||||||
|
if approval_class not in registration["approval_classes"]:
|
||||||
|
raise AuthorizationError("approval_class is not registered for this target")
|
||||||
|
for technique in engagement.raw["techniques"]:
|
||||||
|
if technique not in allowed:
|
||||||
|
raise AuthorizationError(
|
||||||
|
f"technique {technique} not in approval class {approval_class}"
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
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")
|
||||||
|
watcher = RateWatcher(
|
||||||
|
per_minute=engagement.raw["rate_limit_per_minute"],
|
||||||
|
max_concurrency=engagement.raw["max_concurrency"],
|
||||||
|
max_requests=int(engagement.raw.get("maximum_requests")
|
||||||
|
or engagement.raw["rate_limit_per_minute"]),
|
||||||
|
)
|
||||||
|
return PlaneLease(
|
||||||
|
engagement=engagement,
|
||||||
|
registration=registration,
|
||||||
|
identities=identities,
|
||||||
|
watcher=watcher,
|
||||||
|
lease_id=identities[0].lease_id,
|
||||||
|
broker_name=type(selected).__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup(lease: PlaneLease, broker: CredentialBroker) -> dict[str, str]:
|
||||||
|
broker.revoke(lease.lease_id)
|
||||||
|
return {
|
||||||
|
"fixtures": "delete only recorded fixture_ids",
|
||||||
|
"credential_revocation": "revoked",
|
||||||
|
"lease_id": lease.lease_id,
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .engagement import AuthorizationError
|
||||||
from .model import RunReport
|
from .model import RunReport
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,3 +30,14 @@ def risk_nexus_message(report: RunReport) -> str:
|
||||||
lines.extend(["", "Reporter supplies facts only; risk-nexus owns severity and disclosure."])
|
lines.extend(["", "Reporter supplies facts only; risk-nexus owns severity and disclosure."])
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def queue_risk_nexus(report: RunReport, outbox: str | Path) -> Path:
|
||||||
|
"""Persist a delivery artifact. Fixture calibration is not target assurance."""
|
||||||
|
if report.evidence_class != "target":
|
||||||
|
raise AuthorizationError("fixture evidence is not delivered as target assurance")
|
||||||
|
directory = Path(outbox)
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = directory / f"{report.run_id}.md"
|
||||||
|
path.write_text(risk_nexus_message(report), encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
|
||||||
61
src/whitehat_security/targets.py
Normal file
61
src/whitehat_security/targets.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .engagement import AuthorizationError
|
||||||
|
|
||||||
|
REQUIRED = {
|
||||||
|
"schema_version", "target_id", "posture_claim", "attacker_model",
|
||||||
|
"applicability", "applicability_reason", "approval_classes", "routes",
|
||||||
|
"identities", "abort_telemetry",
|
||||||
|
}
|
||||||
|
APPLICABLE_REQUIRED = {
|
||||||
|
"adapter", "probe_pack", "known_bad_calibration", "fixture_lifecycle", "egress",
|
||||||
|
}
|
||||||
|
APPLICABILITY = {"applicable", "not_applicable", "pending"}
|
||||||
|
|
||||||
|
|
||||||
|
def load_registration(path: str | Path) -> dict[str, Any]:
|
||||||
|
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||||
|
missing = sorted(REQUIRED - data.keys())
|
||||||
|
if missing:
|
||||||
|
raise AuthorizationError(f"{path}: missing {', '.join(missing)}")
|
||||||
|
if data["schema_version"] != "whitehat-target/v1":
|
||||||
|
raise AuthorizationError(f"{path}: unsupported schema_version")
|
||||||
|
if data["applicability"] not in APPLICABILITY:
|
||||||
|
raise AuthorizationError(f"{path}: invalid applicability")
|
||||||
|
if not data["applicability_reason"]:
|
||||||
|
raise AuthorizationError(f"{path}: applicability_reason is required")
|
||||||
|
if not data["approval_classes"]:
|
||||||
|
raise AuthorizationError(f"{path}: at least one approval class is required")
|
||||||
|
if data["applicability"] == "applicable":
|
||||||
|
missing_live = sorted(APPLICABLE_REQUIRED - data.keys())
|
||||||
|
if missing_live:
|
||||||
|
raise AuthorizationError(f"{path}: applicable target missing {', '.join(missing_live)}")
|
||||||
|
if not data["routes"]:
|
||||||
|
raise AuthorizationError(f"{path}: applicable target must register routes")
|
||||||
|
identities = data["identities"]
|
||||||
|
if identities.get("count") != 2:
|
||||||
|
raise AuthorizationError(f"{path}: E2 registration must project two identities")
|
||||||
|
if 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")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def load_catalog(directory: str | Path) -> dict[str, dict[str, Any]]:
|
||||||
|
root = Path(directory)
|
||||||
|
paths = sorted(root.glob("*.json"))
|
||||||
|
if not paths:
|
||||||
|
raise AuthorizationError(f"{root}: no target registrations found")
|
||||||
|
catalog: dict[str, dict[str, Any]] = {}
|
||||||
|
for path in paths:
|
||||||
|
registration = load_registration(path)
|
||||||
|
target_id = registration["target_id"]
|
||||||
|
if target_id in catalog:
|
||||||
|
raise AuthorizationError(f"duplicate target_id: {target_id}")
|
||||||
|
catalog[target_id] = registration
|
||||||
|
return catalog
|
||||||
14
targets/README.md
Normal file
14
targets/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Target registrations
|
||||||
|
|
||||||
|
A target name is not an E2 boundary. Each file in this directory is the
|
||||||
|
honest applicability record the test plane admits against.
|
||||||
|
|
||||||
|
| `target_id` | Applicability | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `fixture-e2` | applicable | In-process known-good/known-bad harness. Offline only. |
|
||||||
|
| `audit-core` | applicable | Two ordinary tenant-scoped senders instantiate E2. Live run still needs a new engagement and plane lease. |
|
||||||
|
| `tenant-engine` | not_applicable | No tenant-A identity without tenant-B authority. |
|
||||||
|
| `flex-auth` | pending | Initial envelope; owner has not confirmed the attacker identity. |
|
||||||
|
|
||||||
|
`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/`.
|
||||||
36
targets/audit-core-e2.json
Normal file
36
targets/audit-core-e2.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"schema_version": "whitehat-target/v1",
|
||||||
|
"target_id": "audit-core",
|
||||||
|
"posture_claim": "implemented E2; currently evidenced E1",
|
||||||
|
"attacker_model": "E2-authenticated-tenant-a",
|
||||||
|
"applicability": "applicable",
|
||||||
|
"applicability_reason": "audit-core binds each sender token to a tenant set at one choke point. Two ordinary tenant-scoped senders instantiate the E2 model without operator or database privilege.",
|
||||||
|
"approval_classes": ["live-e2"],
|
||||||
|
"adapter": "runners/audit_core_e2.py",
|
||||||
|
"probe_pack": "probe-packs/audit-core-e2.json",
|
||||||
|
"known_bad_calibration": "src/whitehat_security/audit_fixtures.py",
|
||||||
|
"fixture_lifecycle": {
|
||||||
|
"create": "append two synthetic events and one reserved absent id",
|
||||||
|
"delete": "named immutable audit fixtures retained by target contract unless the owner provides a delete",
|
||||||
|
"ids_are_synthetic": true
|
||||||
|
},
|
||||||
|
"egress": "http://audit-core.audit-core.svc.cluster.local:8080",
|
||||||
|
"routes": [
|
||||||
|
"GET /readyz",
|
||||||
|
"POST /v1/events",
|
||||||
|
"GET /v1/events/{event_id}",
|
||||||
|
"GET /v1/events?correlation_id={correlation_id}"
|
||||||
|
],
|
||||||
|
"identities": {
|
||||||
|
"count": 2,
|
||||||
|
"role": "ordinary tenant-scoped may_read/may_write fixture senders",
|
||||||
|
"ttl_seconds": 900,
|
||||||
|
"broker_audience": "whitehat-e2/audit-core"
|
||||||
|
},
|
||||||
|
"abort_telemetry": [
|
||||||
|
"p95_latency_ms",
|
||||||
|
"probe_attributable_5xx",
|
||||||
|
"unexpected_or_non_fixture_data",
|
||||||
|
"missing_attribution"
|
||||||
|
]
|
||||||
|
}
|
||||||
37
targets/fixture-e2.json
Normal file
37
targets/fixture-e2.json
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"schema_version": "whitehat-target/v1",
|
||||||
|
"target_id": "fixture-e2",
|
||||||
|
"posture_claim": "E2",
|
||||||
|
"attacker_model": "E2-authenticated-tenant-a",
|
||||||
|
"applicability": "applicable",
|
||||||
|
"applicability_reason": "In-process known-good/known-bad service created by this repository. It instantiates tenant A with no tenant B authority.",
|
||||||
|
"approval_classes": ["fixture-e2"],
|
||||||
|
"adapter": "src/whitehat_security/fixtures.py",
|
||||||
|
"probe_pack": "src/whitehat_security/fixtures.py",
|
||||||
|
"known_bad_calibration": "src/whitehat_security/fixtures.py",
|
||||||
|
"fixture_lifecycle": {
|
||||||
|
"create": "in-process constructor",
|
||||||
|
"delete": "drop process state",
|
||||||
|
"ids_are_synthetic": true
|
||||||
|
},
|
||||||
|
"egress": "in-process",
|
||||||
|
"routes": [
|
||||||
|
"GET /objects/{id}",
|
||||||
|
"GET /objects?tenant={tenant_id}",
|
||||||
|
"POST /objects",
|
||||||
|
"PATCH /objects/{id}",
|
||||||
|
"DELETE /objects/{id}"
|
||||||
|
],
|
||||||
|
"identities": {
|
||||||
|
"count": 2,
|
||||||
|
"role": "ordinary tenant-scoped fixture actor",
|
||||||
|
"ttl_seconds": 900,
|
||||||
|
"broker_audience": "whitehat-e2/fixture"
|
||||||
|
},
|
||||||
|
"abort_telemetry": [
|
||||||
|
"p95_latency_ms",
|
||||||
|
"probe_attributable_5xx",
|
||||||
|
"unexpected_or_non_fixture_data",
|
||||||
|
"missing_attribution"
|
||||||
|
]
|
||||||
|
}
|
||||||
22
targets/flex-auth-e2.json
Normal file
22
targets/flex-auth-e2.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"schema_version": "whitehat-target/v1",
|
||||||
|
"target_id": "flex-auth",
|
||||||
|
"posture_claim": "E2",
|
||||||
|
"attacker_model": "E2-authenticated-tenant-a",
|
||||||
|
"applicability": "pending",
|
||||||
|
"applicability_reason": "Named in the initial envelope. No target-owner confirmation yet that a relationship-inheritance decision can be exercised as tenant A without tenant B authority.",
|
||||||
|
"approval_classes": ["live-e2"],
|
||||||
|
"routes": [],
|
||||||
|
"identities": {
|
||||||
|
"count": 2,
|
||||||
|
"role": "ordinary tenant-scoped decision subject; unconfirmed",
|
||||||
|
"ttl_seconds": 900,
|
||||||
|
"broker_audience": "whitehat-e2/flex-auth"
|
||||||
|
},
|
||||||
|
"abort_telemetry": [
|
||||||
|
"p95_latency_ms",
|
||||||
|
"probe_attributable_5xx",
|
||||||
|
"unexpected_or_non_fixture_data",
|
||||||
|
"missing_attribution"
|
||||||
|
]
|
||||||
|
}
|
||||||
22
targets/tenant-engine-e2.json
Normal file
22
targets/tenant-engine-e2.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"schema_version": "whitehat-target/v1",
|
||||||
|
"target_id": "tenant-engine",
|
||||||
|
"posture_claim": "implemented E2; currently evidenced E1",
|
||||||
|
"attacker_model": "E2-authenticated-tenant-a",
|
||||||
|
"applicability": "not_applicable",
|
||||||
|
"applicability_reason": "No authenticated tenant-A identity with no tenant-B authority exists. The platform operator is allowed to address every tenant; an unregistered actor tests caller authorization, not E2. Whitehat will not substitute either identity to finish the workplan.",
|
||||||
|
"approval_classes": ["live-e2"],
|
||||||
|
"routes": [],
|
||||||
|
"identities": {
|
||||||
|
"count": 2,
|
||||||
|
"role": "ordinary tenant-scoped caller; none currently exposed",
|
||||||
|
"ttl_seconds": 900,
|
||||||
|
"broker_audience": "whitehat-e2/tenant-engine"
|
||||||
|
},
|
||||||
|
"abort_telemetry": [
|
||||||
|
"p95_latency_ms",
|
||||||
|
"probe_attributable_5xx",
|
||||||
|
"unexpected_or_non_fixture_data",
|
||||||
|
"missing_attribution"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -12,3 +12,43 @@ def test_validate_engagement_reports_clean_denial(tmp_path, capsys):
|
||||||
main(["validate-engagement", str(path)])
|
main(["validate-engagement", str(path)])
|
||||||
assert stopped.value.code == 2
|
assert stopped.value.code == 2
|
||||||
assert capsys.readouterr().err.startswith("not authorized:")
|
assert capsys.readouterr().err.startswith("not authorized:")
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_targets_accepts_catalog(capsys):
|
||||||
|
main(["validate-targets", "targets"])
|
||||||
|
assert capsys.readouterr().out.startswith("validated 4 target registrations")
|
||||||
|
|
||||||
|
|
||||||
|
def test_kill_switch_is_clear_by_default(capsys):
|
||||||
|
main(["kill-switch"])
|
||||||
|
assert capsys.readouterr().out.strip() == "clear"
|
||||||
|
|
||||||
|
|
||||||
|
def test_admit_plane_refuses_cancelled_engagement(capsys):
|
||||||
|
with pytest.raises(SystemExit) as stopped:
|
||||||
|
main([
|
||||||
|
"admit-plane",
|
||||||
|
"engagements/2026-08-21-audit-core-e2.json",
|
||||||
|
"targets/audit-core-e2.json",
|
||||||
|
])
|
||||||
|
assert stopped.value.code == 2
|
||||||
|
assert "not authorized:" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_deliver_refuses_fixture_calibration(tmp_path, capsys):
|
||||||
|
report = tmp_path / "fixture.json"
|
||||||
|
report.write_text(json.dumps({
|
||||||
|
"schema_version": "whitehat-run/v1", "run_id": "run-1",
|
||||||
|
"evidence_class": "fixture", "engagement_id": "eng",
|
||||||
|
"authorization_id": "auth", "target": "fixture-e2",
|
||||||
|
"target_revision": "local", "posture_claim": "E2",
|
||||||
|
"attacker_model": "E2", "started_at": "2026-08-22T00:00:00Z",
|
||||||
|
"ended_at": "2026-08-22T00:01:00Z", "outcome": "pass",
|
||||||
|
"attempted_operations": 1, "cleanup": "n/a",
|
||||||
|
"credential_revocation": "n/a", "probes": [], "limitations": [],
|
||||||
|
"assurance_statement": "not proof",
|
||||||
|
}), encoding="utf-8")
|
||||||
|
with pytest.raises(SystemExit) as stopped:
|
||||||
|
main(["deliver", str(report), "--outbox", str(tmp_path / "outbox")])
|
||||||
|
assert stopped.value.code == 2
|
||||||
|
assert "fixture evidence" in capsys.readouterr().err
|
||||||
|
|
|
||||||
234
tests/test_plane.py
Normal file
234
tests/test_plane.py
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
import json
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from whitehat_security.audit_fixtures import AuditFixture, audit_probe_suite
|
||||||
|
from whitehat_security.differential import execute, execute_authorized
|
||||||
|
from whitehat_security.engagement import AuthorizationError, Engagement
|
||||||
|
from whitehat_security.fixtures import FixtureService, probe_suite
|
||||||
|
from whitehat_security.model import RunReport
|
||||||
|
from whitehat_security.plane import (
|
||||||
|
KillSwitch, LocalBroker, RateWatcher, UnconnectedCustodyBroker,
|
||||||
|
admit, cleanup, retired_ids,
|
||||||
|
)
|
||||||
|
from whitehat_security.reporting import queue_risk_nexus
|
||||||
|
from whitehat_security.targets import load_catalog, load_registration
|
||||||
|
|
||||||
|
|
||||||
|
NOW = datetime(2026, 8, 22, 12, tzinfo=UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def fixture_record(**overrides):
|
||||||
|
data = {
|
||||||
|
"engagement_id": "WH-ENG-FIXTURE-1", "authorization_id": "auth-1",
|
||||||
|
"authorizer": "operator", "approved_at": "2026-08-22T11:00:00Z",
|
||||||
|
"expires_at": "2026-08-22T18:00:00Z", "target": "in-process",
|
||||||
|
"target_id": "fixture-e2", "target_owner": "whitehat-security",
|
||||||
|
"environment": "fixture", "source": "in-process",
|
||||||
|
"approval_class": "fixture-e2",
|
||||||
|
"routes": ["GET /objects/{id}"], "fixture_ids": ["object-a", "object-b"],
|
||||||
|
"credential_lane": "local-broker", "credential_role": "runtime",
|
||||||
|
"credential_max_ttl_seconds": 900, "techniques": ["e2-differential"],
|
||||||
|
"prohibited_techniques": ["saturation"], "rate_limit_per_minute": 10,
|
||||||
|
"max_concurrency": 1, "maximum_requests": 8,
|
||||||
|
"window_start": "2026-08-22T11:00:00Z",
|
||||||
|
"window_end": "2026-08-22T18:00:00Z", "operator_contact": "operator",
|
||||||
|
"abort_contact": "operator", "posture_claim": "E2",
|
||||||
|
"attacker_model": "E2-authenticated-tenant-a",
|
||||||
|
"finding_destination": "risk-nexus",
|
||||||
|
"target_owner_acknowledged_at": "2026-08-22T11:01:00Z",
|
||||||
|
}
|
||||||
|
data.update(overrides)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def load_engagement(tmp_path, data):
|
||||||
|
path = tmp_path / "engagement.json"
|
||||||
|
path.write_text(json.dumps(data), encoding="utf-8")
|
||||||
|
return Engagement.load(path, now=NOW)
|
||||||
|
|
||||||
|
|
||||||
|
def fixture_registration():
|
||||||
|
return load_registration("targets/fixture-e2.json")
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalog_loads_honest_applicability():
|
||||||
|
catalog = load_catalog("targets")
|
||||||
|
assert catalog["fixture-e2"]["applicability"] == "applicable"
|
||||||
|
assert catalog["audit-core"]["applicability"] == "applicable"
|
||||||
|
assert catalog["tenant-engine"]["applicability"] == "not_applicable"
|
||||||
|
assert catalog["flex-auth"]["applicability"] == "pending"
|
||||||
|
|
||||||
|
|
||||||
|
def test_retired_ids_include_cancelled_records():
|
||||||
|
ids = retired_ids()
|
||||||
|
assert "WH-ENG-20260821-AUDIT-E2" in ids
|
||||||
|
assert "WH-ENG-20260821-TENANT-E2" in ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_plane_admits_and_projects_handles_without_secrets(tmp_path):
|
||||||
|
engagement = load_engagement(tmp_path, fixture_record())
|
||||||
|
broker = LocalBroker()
|
||||||
|
lease = admit(
|
||||||
|
engagement=engagement, registration=fixture_registration(),
|
||||||
|
broker=broker, kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||||
|
now=NOW, retired=retired_ids(),
|
||||||
|
)
|
||||||
|
roles = {handle.role for handle in lease.identities}
|
||||||
|
assert roles == {"owner", "attacker"}
|
||||||
|
assert not hasattr(lease.identities[0], "secret")
|
||||||
|
assert not hasattr(lease.identities[1], "secret")
|
||||||
|
rendered = repr(lease) + repr(lease.identities)
|
||||||
|
for value in broker._secrets.values():
|
||||||
|
assert value.hex() not in rendered
|
||||||
|
assert value not in rendered.encode()
|
||||||
|
report = cleanup(lease, broker)
|
||||||
|
assert report["credential_revocation"] == "revoked"
|
||||||
|
with pytest.raises(AuthorizationError, match="unknown"):
|
||||||
|
broker.revoke(lease.lease_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_applicable_target_is_refused(tmp_path):
|
||||||
|
engagement = load_engagement(tmp_path, fixture_record(target_id="tenant-engine"))
|
||||||
|
registration = load_registration("targets/tenant-engine-e2.json")
|
||||||
|
with pytest.raises(AuthorizationError, match="not_applicable"):
|
||||||
|
admit(
|
||||||
|
engagement=engagement, registration=registration,
|
||||||
|
broker=LocalBroker(), kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||||
|
now=NOW, retired=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_retired_id_cannot_be_reused_even_if_fields_are_complete(tmp_path):
|
||||||
|
engagement = load_engagement(
|
||||||
|
tmp_path, fixture_record(engagement_id="WH-ENG-20260821-AUDIT-E2")
|
||||||
|
)
|
||||||
|
with pytest.raises(AuthorizationError, match="retired"):
|
||||||
|
admit(
|
||||||
|
engagement=engagement, registration=fixture_registration(),
|
||||||
|
broker=LocalBroker(), kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||||
|
now=NOW, retired=retired_ids(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kill_switch_fails_closed(tmp_path):
|
||||||
|
path = tmp_path / "KILL"
|
||||||
|
path.write_text("", encoding="utf-8")
|
||||||
|
engagement = load_engagement(tmp_path, fixture_record())
|
||||||
|
with pytest.raises(AuthorizationError, match="kill switch"):
|
||||||
|
admit(
|
||||||
|
engagement=engagement, registration=fixture_registration(),
|
||||||
|
broker=LocalBroker(), kill_switch=KillSwitch(path),
|
||||||
|
now=NOW, retired=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_live_e2_without_plane_namespace_fails(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="user-engine",
|
||||||
|
runner_image_digest="sha256:abc",
|
||||||
|
))
|
||||||
|
registration = load_registration("targets/audit-core-e2.json")
|
||||||
|
with pytest.raises(AuthorizationError, match="whitehat plane namespace"):
|
||||||
|
admit(
|
||||||
|
engagement=engagement, registration=registration,
|
||||||
|
broker=LocalBroker(), kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||||
|
now=NOW, retired=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unconnected_broker_requests_no_credential(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",
|
||||||
|
))
|
||||||
|
registration = load_registration("targets/audit-core-e2.json")
|
||||||
|
with pytest.raises(AuthorizationError, match="no credential was requested"):
|
||||||
|
admit(
|
||||||
|
engagement=engagement, registration=registration,
|
||||||
|
broker=UnconnectedCustodyBroker(),
|
||||||
|
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"],
|
||||||
|
))
|
||||||
|
with pytest.raises(AuthorizationError):
|
||||||
|
admit(
|
||||||
|
engagement=engagement, registration=fixture_registration(),
|
||||||
|
broker=LocalBroker(), kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||||
|
now=NOW, retired=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rate_and_concurrency_ceilings(tmp_path):
|
||||||
|
watcher = RateWatcher(per_minute=10, max_concurrency=1, max_requests=1)
|
||||||
|
watcher.acquire()
|
||||||
|
with pytest.raises(AuthorizationError, match="concurrency"):
|
||||||
|
watcher.acquire()
|
||||||
|
watcher.release()
|
||||||
|
with pytest.raises(AuthorizationError, match="request ceiling"):
|
||||||
|
watcher.acquire()
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_authorized_requires_plane_lease(tmp_path):
|
||||||
|
engagement = load_engagement(tmp_path, fixture_record())
|
||||||
|
lease = admit(
|
||||||
|
engagement=engagement, registration=fixture_registration(),
|
||||||
|
broker=LocalBroker(), kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||||
|
now=NOW, retired=set(),
|
||||||
|
)
|
||||||
|
probe = probe_suite(FixtureService(True))[0]
|
||||||
|
result = execute_authorized(
|
||||||
|
probe, lease=lease, route="GET /objects/{id}", salt=b"test", now=NOW
|
||||||
|
)
|
||||||
|
assert result.outcome == "pass"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_report_is_not_delivered_as_target_assurance(tmp_path):
|
||||||
|
report = RunReport(
|
||||||
|
schema_version="whitehat-run/v1", run_id="run-1", evidence_class="fixture",
|
||||||
|
engagement_id="eng-1", authorization_id="auth-1", target="fixture-e2",
|
||||||
|
target_revision="local", posture_claim="E2", attacker_model="E2",
|
||||||
|
started_at="2026-08-22T00:00:00Z", ended_at="2026-08-22T00:01:00Z",
|
||||||
|
outcome="pass", attempted_operations=1, cleanup="complete",
|
||||||
|
credential_revocation="complete",
|
||||||
|
)
|
||||||
|
with pytest.raises(AuthorizationError, match="fixture evidence"):
|
||||||
|
queue_risk_nexus(report, tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_target_report_is_queued_without_severity(tmp_path):
|
||||||
|
report = RunReport(
|
||||||
|
schema_version="whitehat-run/v1", run_id="run-2", evidence_class="target",
|
||||||
|
engagement_id="eng-1", authorization_id="auth-1", target="audit-core",
|
||||||
|
target_revision="abc", posture_claim="E2", attacker_model="E2",
|
||||||
|
started_at="2026-08-22T00:00:00Z", ended_at="2026-08-22T00:01:00Z",
|
||||||
|
outcome="pass", attempted_operations=1, cleanup="complete",
|
||||||
|
credential_revocation="complete",
|
||||||
|
)
|
||||||
|
path = queue_risk_nexus(report, tmp_path)
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
assert "**pass**" in text
|
||||||
|
assert "Severity" not in text
|
||||||
|
assert "not proof" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_shaped_probes_calibrate():
|
||||||
|
good = [execute(probe, salt=b"test") for probe in audit_probe_suite(AuditFixture(True))]
|
||||||
|
bad = [execute(probe, salt=b"test") for probe in audit_probe_suite(AuditFixture(False))]
|
||||||
|
assert {item.probe_id for item in good} == {
|
||||||
|
"audit-event-by-id", "audit-correlation-slice", "audit-append-as-b",
|
||||||
|
}
|
||||||
|
assert {item.outcome for item in good} == {"pass"}
|
||||||
|
assert {item.outcome for item in bad} == {"finding"}
|
||||||
|
|
@ -89,8 +89,10 @@ Drafted in `docs/rules-of-engagement.md` on 2026-08-18 with authorization
|
||||||
classes, per-run records, initial target envelope, hard prohibitions,
|
classes, per-run records, initial target envelope, hard prohibitions,
|
||||||
credential/attribution rules, rate defaults, abort/cleanup and evidence
|
credential/attribution rules, rate defaults, abort/cleanup and evidence
|
||||||
schema. The operator accepted v0.1 on 2026-08-21 with the scope recorded in
|
schema. The operator accepted v0.1 on 2026-08-21 with the scope recorded in
|
||||||
§10. The acceptance approves the operating rules and offline fixture work; it
|
what is now §11. v0.2 (2026-08-22) adds §10, the governed test plane, as a
|
||||||
does not pre-authorize any live target.
|
stricter admission control. It does not expand authorization. The acceptance
|
||||||
|
approves the operating rules and offline fixture work; it does not
|
||||||
|
pre-authorize any live target.
|
||||||
|
|
||||||
### T02 — The attacker model per axis
|
### T02 — The attacker model per axis
|
||||||
|
|
||||||
|
|
@ -141,30 +143,31 @@ The core technique: run the same request as two tenants and compare.
|
||||||
produced ordinary-looking responses, a 403 and a 404, and nothing alerted.
|
produced ordinary-looking responses, a 403 and a 404, and nothing alerted.
|
||||||
- Capture evidence as a count and a schema shape, never as tenant data (T01).
|
- Capture evidence as a count and a schema shape, never as tenant data (T01).
|
||||||
|
|
||||||
**Acceptance:** run against `tenant-engine` and `audit-core`, both of which
|
**Acceptance:** a dated run record against every *applicable* E2 target. The
|
||||||
currently claim `E2`. The artifact is the run record, not a green tick.
|
artifact is the run record, not a green tick. `tenant-engine` is registered
|
||||||
|
`not_applicable` for E2; that record is the artifact for that target.
|
||||||
|
`audit-core` remains the live E2 target and still needs a new admitted
|
||||||
|
engagement.
|
||||||
|
|
||||||
Implementation progress, 2026-08-21: `src/whitehat_security/differential.py`
|
Implementation progress, 2026-08-22: `src/whitehat_security/differential.py`
|
||||||
now performs owner/attacker/absent comparisons, detects fixture disclosure and
|
performs owner/attacker/absent comparisons, detects fixture disclosure and
|
||||||
write-side state change, and persists only sanitized observations. Concrete
|
write-side state change, and persists only sanitized observations. Live entry
|
||||||
route inventories live in `probe-packs/tenant-engine-e2.json` and
|
is `execute_authorized`, which requires a plane lease. Concrete route
|
||||||
`probe-packs/audit-core-e2.json`. The two target runs remain gated on complete
|
inventories live in `probe-packs/`. Target honesty lives in `targets/`:
|
||||||
engagement records, owner acknowledgements and executable identity adapters.
|
`audit-core` is applicable; `tenant-engine` is `not_applicable` (that is the
|
||||||
|
artifact, not a deferral); `flex-auth` is pending.
|
||||||
|
|
||||||
Production engagements were operator-approved on 2026-08-21 and committed in
|
The 2026-08-21 production engagement IDs were cancelled without sending a
|
||||||
`engagements/`. Audit-core accepted the route/identity shape and reviewed the
|
target request and are retired. A live audit-core run still needs a **new**
|
||||||
adapter; its first review caught a missing idempotency header before traffic,
|
engagement, target-owner acknowledgement, provisioned `whitehat` namespace,
|
||||||
which is corrected and regression-tested. `docs/target-applicability.md`
|
and a connected custody broker. Whitehat will not relabel tenant-engine to
|
||||||
records a separate issue: tenant-engine currently exposes no authenticated
|
finish this task.
|
||||||
tenant-A identity with no tenant-B authority, so its E2 applicability is
|
|
||||||
pending owner confirmation rather than being faked with its all-tenant service
|
|
||||||
operator or an unauthenticated actor.
|
|
||||||
|
|
||||||
### T04 — Prove the probes fail
|
### T04 — Prove the probes fail
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: WHITEHAT-WP-0001-T04
|
id: WHITEHAT-WP-0001-T04
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "a2afc0f5-3ece-410c-864c-829a8aac30fc"
|
state_hub_task_id: "a2afc0f5-3ece-410c-864c-829a8aac30fc"
|
||||||
```
|
```
|
||||||
|
|
@ -179,12 +182,13 @@ the same discipline applies here and is not optional.
|
||||||
**Acceptance:** every probe in T03 demonstrated failing before any of them is
|
**Acceptance:** every probe in T03 demonstrated failing before any of them is
|
||||||
trusted passing.
|
trusted passing.
|
||||||
|
|
||||||
Implementation progress, 2026-08-21: five generic read/list/create/update/
|
Completed 2026-08-22. Five generic read/list/create/update/delete probes and
|
||||||
delete probes pass the enforcing in-process fixture and all five produce
|
the three audit-core shaped probes pass the enforcing in-process fixture and
|
||||||
findings when its tenant predicate is removed. The sanitized artifact is
|
all produce findings when the tenant predicate is removed. Tenant-engine has
|
||||||
`evidence/offline-calibration.json`; fourteen tests guard the calibration and
|
no applicable E2 identity, so its pack is not calibrated as if it were E2.
|
||||||
fail-closed authorization behavior. Target-specific probes are not trusted
|
`make fixture-evidence` refreshes `evidence/offline-calibration.json`.
|
||||||
passing until they receive the same calibration.
|
Target probes are still not trusted passing against a live service until a
|
||||||
|
new admitted engagement runs.
|
||||||
|
|
||||||
### T05 — RLS conformance under attack (the E3 artifact)
|
### T05 — RLS conformance under attack (the E3 artifact)
|
||||||
|
|
||||||
|
|
@ -263,28 +267,56 @@ A **passing** run is also reported. "The attacks we thought of did not work" is
|
||||||
the honest claim, and recording it dated is what lets anyone see how stale the
|
the honest claim, and recording it dated is what lets anyone see how stale the
|
||||||
assurance has become.
|
assurance has become.
|
||||||
|
|
||||||
Implementation progress, 2026-08-21: `schemas/run-report.schema.json` defines
|
Implementation progress, 2026-08-22: `schemas/run-report.schema.json` defines
|
||||||
the minimized evidence contract and `whitehat risk-message` renders both pass
|
the minimized evidence contract, `whitehat risk-message` renders both pass
|
||||||
and finding deliveries without severity. Offline calibration is deliberately
|
and finding deliveries without severity, and `whitehat deliver` queues a
|
||||||
not routed as target assurance. Completion awaits the first authorized target
|
target report to `outbox/`. Fixture calibration is refused as target
|
||||||
report being delivered to `risk-nexus`.
|
assurance. Completion awaits the first authorized target report being
|
||||||
|
delivered to `risk-nexus`.
|
||||||
|
|
||||||
|
### T08 — Governed test plane
|
||||||
|
|
||||||
|
The 2026-08-22 cutoff's missing infrastructure, encoded here so live work can
|
||||||
|
resume later without assembling authority during the run.
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: WHITEHAT-WP-0001-T08
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Completed 2026-08-22 as a repository contract, not a cluster provision:
|
||||||
|
|
||||||
|
- Target registration schema and catalog, including an honest
|
||||||
|
`not_applicable` state.
|
||||||
|
- Fail-closed admission: retired IDs, kill switch, approval class, namespace,
|
||||||
|
pinned digest, known-bad calibration, two identity handles.
|
||||||
|
- Credential broker interface that never returns secret values. The live
|
||||||
|
broker is unconnected and raises before any custody call.
|
||||||
|
- Rate watcher, lease cleanup, default-deny plane manifests, runner identity.
|
||||||
|
- Automatic outbox delivery of target reports only.
|
||||||
|
|
||||||
|
`ops-mason` still has to provision namespace, network policy and a real
|
||||||
|
custody projection after a new engagement ID exists. That provision is not
|
||||||
|
this task, and this task does not authorize it.
|
||||||
|
|
||||||
## Sequencing
|
## Sequencing
|
||||||
|
|
||||||
T01 gates all. T02 shapes T03/T05/T06. T04 gates trusting any of them. T07 can
|
T01 gates all. T02 shapes T03/T05/T06. T04 gates trusting any of them. T08
|
||||||
follow T03.
|
gates live T03. T07 can follow T03.
|
||||||
|
|
||||||
## Session cutoff — 2026-08-22
|
## Session cutoff — 2026-08-22
|
||||||
|
|
||||||
This session ends with the workplan deliberately **active**, not cosmetically
|
The coordinating session ended with the workplan deliberately **active**. T01,
|
||||||
complete. T01 and T02 are done. T03 through T07 remain in progress. The two
|
T02, T04 and T08 are done. T03, T05, T06 and T07 remain in progress. The two
|
||||||
dated production engagement records were cancelled without sending any target
|
dated production engagement records were cancelled without sending any target
|
||||||
request; cancelled engagement IDs are terminal and must never be reused.
|
request; cancelled engagement IDs are terminal and must never be reused.
|
||||||
|
|
||||||
The offline instrument is implemented and calibrated, but the estate does not
|
The offline instrument is implemented, calibrated, and plane-gated. The
|
||||||
yet provide the governed test plane needed to exercise it routinely and safely.
|
estate still has to provision the cluster plane and a value-safe credential
|
||||||
The exact completed scope, unresolved scope, and recommended restart boundary
|
broker before any live evidence exists. Resume only with new engagement IDs.
|
||||||
are recorded in `docs/session-cutoff-2026-08-22.md`.
|
The exact completed scope is recorded in `docs/session-cutoff-2026-08-22.md`
|
||||||
|
and `docs/test-plane.md`.
|
||||||
|
|
||||||
## Risks
|
## Risks
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue