diff --git a/Makefile b/Makefile index 9f872cb..ceee73e 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,9 @@ test: fixture-evidence: PYTHONPATH=src python3 -m whitehat_security.cli fixtures --output evidence/offline-calibration.json PYTHONPATH=src python3 -m whitehat_security.cli e3-fixtures --output evidence/offline-e3-calibration.json + PYTHONPATH=src python3 -m whitehat_security.cli e4-fixtures --output evidence/offline-e4-calibration.json + PYTHONPATH=src python3 -m whitehat_security.cli r4-fixtures --output evidence/offline-r4-calibration.json + PYTHONPATH=src python3 -m whitehat_security.cli cadence > evidence/probe-cadence.json PYTHONPATH=src python3 -m whitehat_security.cli capacity-fixture --output evidence/offline-capacity-calibration.json PYTHONPATH=src python3 -m whitehat_security.cli asm-fixtures --test-id T-01 --output evidence/offline-asm-t01-calibration.json PYTHONPATH=src python3 -m whitehat_security.cli asm-fixtures --test-id T-02 --output evidence/offline-asm-t02-calibration.json diff --git a/README.md b/README.md index 7003d58..cb9d0ec 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # whitehat-security -NetKingdom's offensive security facility. Automated white hat IT-security, -pen-testing and isolation-probing — pointed at infrastructure we choose, -including our own. +NetKingdom's offensive security facility. It calibrates adversarial probes +offline and admits a live run only under a complete engagement record. No +live target is authorized by the repository itself. -The estate's **adversarial evidence facility**: it attacks our own systems, on -a schedule, to find out whether the security properties they claim are actually -true. +The estate's **adversarial evidence facility**: it can test whether a claimed +boundary holds, and today that capability is the offline harness plus one +terminal historical audit-core E2 pass. It does not run on a schedule. It exists because a repo testing its own boundary grades its own homework. The probes most worth having are the ones an author would not think to write. diff --git a/SCOPE.md b/SCOPE.md index 932e3cd..5cf01ad 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -2,36 +2,70 @@ ## One-liner -`whitehat-security` is NetKingdom's authorization-bound offensive-security -tooling for producing adversarial evidence about security claims. +`whitehat-security` is NetKingdom's authorization-bound harness for adversarial +evidence. It can calibrate probes offline. It can admit a live run only when +a complete engagement record already exists. No live target is authorized. -## Core idea +## What it can do -The repository turns stated security properties into dated, reproducible attack -attempts. It remains separate from the systems it tests, reports findings -without grading their severity, and never treats a passing probe as proof that a -boundary always holds. +- Calibrate E2 differential probes in process, including an audit-core-shaped + pack. `whitehat fixtures` fails a known-bad fixture before it trusts a + known-good pass. It sends no packet. +- Calibrate E3 row-level-security outcomes in process, including the + SQL-compromise case that stays `inconclusive`. `whitehat e3-fixtures` opens + no database. +- Calibrate E4 credential confinement and R4 erasure recovery in process. + `whitehat e4-fixtures` and `whitehat r4-fixtures` project no credential, + open no store, and are not target assurance. No live E4 or R4 target is + registered. +- Calibrate P1/P2 capacity judgments from supplied samples. + `whitehat capacity-fixture` generates no load. +- Calibrate Active Secrets Management Canon T-01 through T-10 in process. + `whitehat asm-fixtures` sends no packet. The T-06 fixture counts simulated + custody calls; it is not an OpenBao client. +- Validate engagement records, target registrations, and probe packs, and + refuse plane admission unless the target is applicable, the approval class + matches, the kill switch is clear, and the engagement is inside its window. +- Render a value-safe run message and queue it as a local outbox file. + `whitehat deliver` does not file the message in `risk-nexus` and does not + assign severity. +- Render a Gate House conformance envelope. `whitehat conformance-message` + does not send it. +- Review a sealed Emergent knowledge package offline with + `tools/offline_emergent_knowledge_review.py`. That tool does not call a + provider and does not re-pin `informed-decision`. +- Keep an `audit-core` E2 runner that can send HTTP only from an admitted + plane pod. The dated engagements that used it are terminal. -## In scope +## What it cannot do -- Attacker models for tenant isolation, credential confinement, noisy-neighbour - behavior and erasure verification. -- Differential probes that compare behavior across controlled tenant contexts. -- Known-bad and known-good fixtures that demonstrate every probe can fail. -- Rules of engagement, target authorization, engagement records, abort - controls and evidence minimization. -- Probe cadence and the resulting assurance/exposure window. -- Delivery of findings and passing-run evidence to `risk-nexus`. +- Run on a schedule. `whitehat cadence` lists every family with + `scheduled: false`. The E3 interval of 24 hours is a declared detection + window, not an operating timer. +- Send a packet, open a database connection, generate load, or lease a live + credential under the current records. `WHITEHAT-WP-0006` and + `WHITEHAT-WP-0008` stay blocked on owner returns. +- Treat a fixture pass, or the terminal audit-core E2 pass, as proof that a + boundary holds. +- File findings, grade severity, disclose them, or fix a target. +- Hold an OpenBao client, a database driver, or a cluster-mutation API. +- Produce E4 or R4 evidence about a real substrate. Those families stop at + the in-process harness. +- Produce identity, authorization, or availability evidence. The attacker + model does not define those axes. +- Admit `tenant-engine` for E2 or `platform-pg` for E3. Both are + `not_applicable`. ## Out of scope - Fixing defects in target repositories. -- Assigning severity, disclosure policy or remediation deadlines. +- Assigning severity, disclosure policy, or remediation deadlines. - Replacing mechanical checks owned by a target repository's CI. - Defining the estate's security model or publishing permanent policy. - Probing any target without the authorization and engagement records required by [the rules of engagement](docs/rules-of-engagement.md). - Blocking build-mode delivery without a separately recorded decision. +- Turning the local outbox into a `risk-nexus` writer. ## Safety invariants @@ -40,56 +74,62 @@ boundary always holds. and non-networked fixture design may proceed. - No live target is authorized by this scope document. - Every live run names its authorization, target, owner, window, technique, - credential lane, rate ceiling, abort contact and finding destination. + credential lane, rate ceiling, abort contact, and finding destination. - A probe stops at the recorded boundary and never follows an adjacent system. - Evidence records response shape and counts, never real tenant row values or credentials. - Findings leave this repository; repairs do not enter it. +- A probe is trusted only after it has failed a known-bad fixture. ## Relevant when - A service claims a Tenancy Posture evidence level that requires adversarial - rather than mechanical evidence. -- A boundary must be tested using a leaked runtime credential or hostile tenant - context. -- The estate needs to know when a probe last ran, what attacker it modeled, and - whether it was proven against a known-bad fixture. + rather than mechanical evidence, and an engagement record for that service + already exists or the work is an offline fixture. +- The estate needs the attacker model, the last dated run, and whether the + harness has been shown to fail when the control is absent. ## Not relevant when -- A repository needs unit, schema or provisioning tests for its own code. -- A finding needs triage, severity or disclosure handling; use `risk-nexus`. +- A repository needs unit, schema, or provisioning tests for its own code. +- A finding needs triage, severity, or disclosure handling; use `risk-nexus`. - Permanent policy needs publication; use `policy-nexus`. -- The desired activity falls outside an approved engagement boundary. +- The desired activity is a live probe and no complete engagement exists. ## Current state - Repository status: active. -- Layer: Staff (declared in `INTENT.md`, 2026-09-01). `WHITEHAT-IN-0001` is - closed. -- Finished: `WHITEHAT-WP-0001` through `WHITEHAT-WP-0005` and - `WHITEHAT-WP-0007`. Residual live Tenancy Posture evidence is - `WHITEHAT-WP-0006`. Residual live ASM is `WHITEHAT-WP-0008`. Both stay - `blocked` after the 2026-10-04 recheck and the owner returns the same day. - T01 still waits on `rapp-postgres` for an ordinary runtime identity. T02 - still waits on an operator substrate window. T03 still waits on - `access-engine` or a new `audit-core` engagement ID. Live ASM still waits - on a surface owner, with T-06 on `approval-engine` and a consuming PEP. - No packet is authorized. +- Layer: Staff (declared in `INTENT.md`, 2026-09-01). `conformance_state` + stays `blocked-clean`. `WHITEHAT-IN-0001` is closed. +- The capability statement above is the 2026-10-04 scope assessment. The + comparison against `INTENT.md` is + [`history/2026-10-04T0649Z-scope-against-intent.md`](history/2026-10-04T0649Z-scope-against-intent.md). + `WHITEHAT-WP-0011` added the E4 and R4 calibrations and the cadence + register. It authorizes no packet. +- Finished: `WHITEHAT-WP-0001` through `WHITEHAT-WP-0005`, + `WHITEHAT-WP-0007`, `WHITEHAT-WP-0009`, `WHITEHAT-WP-0010`, and + `WHITEHAT-WP-0011`. +- Residual live Tenancy Posture evidence is `WHITEHAT-WP-0006`. Residual + live ASM is `WHITEHAT-WP-0008`. Both stay `blocked` after the 2026-10-04 + owner returns. T01 still waits on `rapp-postgres` for an ordinary runtime + identity. T02 still waits on an operator substrate window. T03 still waits + on `access-engine` or a new `audit-core` engagement ID. Live ASM still + waits on a surface owner, with T-06 on `approval-engine` and a consuming + PEP. No packet is authorized. - `WHITEHAT-WP-0007` triaged Gate House ASM T-01…T-10. Live `asm-tNN` registrations remain `pending`. In-process `fixture-asm-t01`…`t10` calibrations exist. That work authorized no live probe. - `WHITEHAT-WP-0001` T01–T08 are complete for every applicable target. `audit-core` has dated E2 pass `WH-ENG-20260822-AUDIT-E2-03`. `tenant-engine` E2 and `platform-pg` E3 stay `not_applicable`. `flex-auth` - E2 and `shared-substrate` P1/P2 stay `pending`. Offline E2, E3 and P1/P2 - harnesses fail known-bad and pass known-good in-process. -- `-01`, `-02` and `-03` are terminal. A later live run needs a new + E2 and `shared-substrate` P1/P2 stay `pending`. Offline E2, E3, E4, R4, and + P1/P2 harnesses fail known-bad and pass known-good in process. +- `-01`, `-02`, and `-03` are terminal. A later live run needs a new engagement ID, plane admission, and the approvals required by the rules of engagement. `WHITEHAT-WP-0006` owns that work and authorizes no packet. - `WHITEHAT-WP-0009` is finished. The 0.2.1 record shows WH-KNOW-001 and WH-KNOW-002. A local 0.2.3 retest did not reproduce them. `risk-nexus` - holds open RISK-F-0013, RISK-F-0014 and RISK-F-0015. `emergent-security` + holds open RISK-F-0013, RISK-F-0014, and RISK-F-0015. `emergent-security` commit `19e7feb` accepts the scoped review and the two library remediations, and leaves provider evidence and the `informed-decision` pin reconciliation in blocked EMERGENT-WP-0002. This plan authorizes no @@ -106,9 +146,9 @@ boundary always holds. ## Relationships - Owner and security canon: `net-kingdom`. -- Finding intake, severity and disclosure: `risk-nexus`. +- Finding intake, severity, and disclosure: `risk-nexus`. - Permanent publication: `policy-nexus`. -- Initial proposed target owners: `tenant-engine`, `audit-core` and +- Initial proposed target owners: `tenant-engine`, `audit-core`, and `flex-auth`. ## Getting oriented @@ -116,11 +156,15 @@ boundary always holds. 1. Read [`INTENT.md`](INTENT.md) for the durable purpose and ownership model. 2. Read [the rules of engagement](docs/rules-of-engagement.md) before any probe design or execution. -3. Read [`WHITEHAT-WP-0001`](workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md) +3. Read [`history/2026-10-04T0649Z-scope-against-intent.md`](history/2026-10-04T0649Z-scope-against-intent.md) + for the capability gaps against that intent. +4. Read [`WHITEHAT-WP-0001`](workplans/WHITEHAT-WP-0001-cross-tenant-evidence.md) for the finished applicable-target artifacts, [`WHITEHAT-WP-0006`](workplans/WHITEHAT-WP-0006-authorized-live-residuals.md) - for unauthorized live Tenancy Posture residuals, and + for unauthorized live Tenancy Posture residuals, [`WHITEHAT-WP-0007`](workplans/WHITEHAT-WP-0007-asm-t01-t10-triage.md) - for finished ASM T-01…T-10 triage, and + for finished ASM T-01…T-10 triage, [`WHITEHAT-WP-0008`](workplans/WHITEHAT-WP-0008-live-asm-residuals.md) - for unauthorized live ASM residuals. + for unauthorized live ASM residuals, and + [`WHITEHAT-WP-0011`](workplans/WHITEHAT-WP-0011-scope-capability-gaps.md) + for the offline E4, R4, and cadence work. diff --git a/docs/attacker-model.md b/docs/attacker-model.md index 0f25bbb..a63953e 100644 --- a/docs/attacker-model.md +++ b/docs/attacker-model.md @@ -165,6 +165,9 @@ credential and substrate confinement. A platform, migration, break-glass or tenant B credential; control of the credential issuer; or permission to alter routing/provisioning state. +Executable calibration is `whitehat e4-fixtures`. It is in-process, projects +no credential, and is not target assurance. No live E4 target is registered. + ## 6. Shared P1/P2 capacity — bounded co-resident This model tests capacity assurance, not the truth of the P1/P2 topology. @@ -232,6 +235,10 @@ outcome is inconclusive rather than pass. targets R2. The facility does not erase the evidence service's retained facts to satisfy a fleet-wide aspiration. +Executable calibration is `whitehat r4-fixtures`. It is in-process, opens no +store, and is not target assurance. An incomplete copy inventory stays +inconclusive. No live R4 target is registered. + ## 8. Axes outside this first model Identity (`I`), authorization (`A`) and availability (`V`) have evidence diff --git a/docs/e3-cadence.md b/docs/e3-cadence.md index a1c023a..5378875 100644 --- a/docs/e3-cadence.md +++ b/docs/e3-cadence.md @@ -4,7 +4,9 @@ The E3 detection interval is **24 hours**, so the declared maximum exposure window is **24 hours plus run and reporting latency**. A run is also required before deployment promotion following any schema migration, RLS policy change, role/grant change, security-definer function change, or posture-mechanism -change. The event-triggered run does not reset the daily schedule. +change. The event-triggered run does not reset the daily schedule. Nothing in this +repository schedules that run. `whitehat cadence` is the register, and every +family in it has `scheduled: false`. This is a detection control, not prevention. A migration can introduce an unprotected table between runs; the interval states how long that condition diff --git a/evidence/README.md b/evidence/README.md index 39b800b..29e83e6 100644 --- a/evidence/README.md +++ b/evidence/README.md @@ -1,11 +1,12 @@ # Evidence This directory stores sanitized run artifacts. `offline-calibration.json`, -`offline-e3-calibration.json`, `offline-capacity-calibration.json`, and +`offline-e3-calibration.json`, `offline-e4-calibration.json`, +`offline-r4-calibration.json`, `offline-capacity-calibration.json`, and `offline-asm-t01-calibration.json` … `offline-asm-t10-calibration.json` are generated from repository-created fixtures and prove only that the harness distinguishes known-good from known-bad behavior. They are not target -assurance. `WH-ENG-20260822-AUDIT-E2-02-abort.json` +assurance. `probe-cadence.json` is the unscheduled cadence register. `WH-ENG-20260822-AUDIT-E2-02-abort.json` is an abort record (`evidence_class: abort`), not an E2 pass or finding. `WH-ENG-20260822-AUDIT-E2-03.json` is the first authorized target pass; SHA-256 `2d5a21141b78024a5334881e2b7fd62a69c46931057f77515a6c6f18ec497593`. A pass diff --git a/evidence/offline-e4-calibration.json b/evidence/offline-e4-calibration.json new file mode 100644 index 0000000..9e3ba87 --- /dev/null +++ b/evidence/offline-e4-calibration.json @@ -0,0 +1,58 @@ +{ + "ended_at": "2026-10-04T06:54:09.471305Z", + "evidence_class": "fixture", + "known_bad": [ + { + "outcome": "finding", + "probe_id": "present-to-other-substrate", + "reason": "tenant A credential addressed tenant B substrate" + }, + { + "outcome": "finding", + "probe_id": "alternate-database", + "reason": "tenant A credential addressed tenant B substrate" + }, + { + "outcome": "finding", + "probe_id": "alternate-schema", + "reason": "tenant A credential addressed tenant B substrate" + }, + { + "outcome": "aborted", + "probe_id": "refresh-outside-authority", + "reason": "credential refresh outside recorded authority is a probe-control incident" + } + ], + "known_bad_failed_first": true, + "known_good": [ + { + "outcome": "pass", + "probe_id": "present-to-other-substrate", + "reason": "credential did not authenticate to or address tenant B" + }, + { + "outcome": "pass", + "probe_id": "alternate-database", + "reason": "credential did not authenticate to or address tenant B" + }, + { + "outcome": "pass", + "probe_id": "alternate-schema", + "reason": "credential did not authenticate to or address tenant B" + }, + { + "outcome": "pass", + "probe_id": "reachability-only", + "reason": "reachability alone is not an E4 failure" + } + ], + "limitations": [ + "Offline E4 calibration evaluates the harness; it is not target assurance.", + "No credential was leased, and no network or database call was made.", + "No live E4 target is registered. Reachability alone is not a finding." + ], + "outcome": "pass", + "run_id": "e4-calibration-2026-10-04T06:54:09.471214Z", + "schema_version": "whitehat-e4-calibration/v1", + "started_at": "2026-10-04T06:54:09.471214Z" +} diff --git a/evidence/offline-r4-calibration.json b/evidence/offline-r4-calibration.json new file mode 100644 index 0000000..606d707 --- /dev/null +++ b/evidence/offline-r4-calibration.json @@ -0,0 +1,75 @@ +{ + "ended_at": "2026-10-04T06:54:09.672396Z", + "evidence_class": "fixture", + "inconclusive": [ + { + "outcome": "inconclusive", + "probe_id": "inventory-incomplete", + "reason": "declared copies were not all exercised", + "route": "R4-A" + }, + { + "outcome": "inconclusive", + "probe_id": "before-horizon", + "reason": "recovery before the horizon is not an R4-A result", + "route": "R4-A" + }, + { + "outcome": "inconclusive", + "probe_id": "key-not-destroyed", + "reason": "key destruction alone is not a pass, and destruction was not recorded", + "route": "R4-B" + } + ], + "known_bad": [ + { + "outcome": "finding", + "probe_id": "horizon-recovered", + "reason": "fixture content recovered after the erasure horizon", + "route": "R4-A" + }, + { + "outcome": "finding", + "probe_id": "ciphertext-readable", + "reason": "erased fixture remains readable or confirmable", + "route": "R4-B" + }, + { + "outcome": "finding", + "probe_id": "usable-key-survives", + "reason": "erased fixture remains readable or confirmable", + "route": "R4-B" + }, + { + "outcome": "finding", + "probe_id": "low-entropy-oracle", + "reason": "erased fixture remains readable or confirmable", + "route": "R4-B" + } + ], + "known_bad_failed_first": true, + "known_good": [ + { + "outcome": "pass", + "probe_id": "horizon-clear", + "reason": "no fixture content recovered from the declared copies", + "route": "R4-A" + }, + { + "outcome": "pass", + "probe_id": "key-destroyed-clear", + "reason": "retained ciphertext is unreadable and no confirmation oracle remains", + "route": "R4-B" + } + ], + "limitations": [ + "Offline R4 calibration evaluates the harness; it is not target assurance.", + "No backup, replica, key, or ciphertext was accessed.", + "audit-core is not an R4 target. No live R4 target is registered.", + "An incomplete copy inventory stays inconclusive and is not a pass." + ], + "outcome": "pass", + "run_id": "r4-calibration-2026-10-04T06:54:09.672295Z", + "schema_version": "whitehat-r4-calibration/v1", + "started_at": "2026-10-04T06:54:09.672295Z" +} diff --git a/evidence/probe-cadence.json b/evidence/probe-cadence.json new file mode 100644 index 0000000..6219dbf --- /dev/null +++ b/evidence/probe-cadence.json @@ -0,0 +1,54 @@ +{ + "families": [ + { + "declared_interval": null, + "executable": "whitehat fixtures", + "exposure_window": "Per approved engagement window only. The dated audit-core pass is terminal. No standing schedule replaces it.", + "family": "E2", + "live_owner": "WHITEHAT-WP-0006-T03", + "scheduled": false + }, + { + "declared_interval": "24h", + "executable": "whitehat e3-fixtures", + "exposure_window": "24h plus run and reporting latency, once a runtime-safe surface exists. The interval is not operating.", + "family": "E3", + "live_owner": "WHITEHAT-WP-0006-T01", + "scheduled": false + }, + { + "declared_interval": null, + "executable": "whitehat e4-fixtures", + "exposure_window": "No live target and no schedule, so no detection window.", + "family": "E4", + "live_owner": null, + "scheduled": false + }, + { + "declared_interval": null, + "executable": "whitehat capacity-fixture", + "exposure_window": "An operator window only. The harness generates no load.", + "family": "P1/P2", + "live_owner": "WHITEHAT-WP-0006-T02", + "scheduled": false + }, + { + "declared_interval": null, + "executable": "whitehat r4-fixtures", + "exposure_window": "No live target and no schedule, so no detection window.", + "family": "R4", + "live_owner": null, + "scheduled": false + }, + { + "declared_interval": null, + "executable": "whitehat asm-fixtures", + "exposure_window": "Per approved engagement for one named Canon test_id.", + "family": "ASM T-01..T-10", + "live_owner": "WHITEHAT-WP-0008-T01", + "scheduled": false + } + ], + "scheduled_live_runs": false, + "schema_version": "whitehat-cadence/v1" +} diff --git a/history/2026-10-04T0649Z-scope-against-intent.md b/history/2026-10-04T0649Z-scope-against-intent.md new file mode 100644 index 0000000..825f055 --- /dev/null +++ b/history/2026-10-04T0649Z-scope-against-intent.md @@ -0,0 +1,131 @@ +# Scope against intent — 2026-10-04T0649Z + +Assessment of `SCOPE.md` against `INTENT.md` after reading the harness, the +target catalog, and the rules of engagement. This file records the gaps. It +does not authorize a packet, a database connection, or a load run. +`conformance_state` stays `blocked-clean`. + +## Method + +Compared each item under "What it owns" in `INTENT.md` with the code that can +run today: `src/whitehat_security/`, `runners/audit_core_e2.py`, `targets/`, +`whitehat` commands, and the blocked workplans `WHITEHAT-WP-0006` and +`WHITEHAT-WP-0008`. A capability counts only when a command or runner exists +and its limit is explicit. A prose attacker-model section is not a probe. + +## What the repository can actually do + +| Facility | Present | Limit | +| --- | --- | --- | +| E2 differential harness | `whitehat fixtures`, generic and audit-shaped fixtures | In process. Not a live target. | +| E2 live runner | `runners/audit_core_e2.py` | Can send HTTP from an admitted pod. Engagements `-01`, `-02`, and `-03` are terminal. | +| E3 evaluator and 24h window | `whitehat e3-fixtures`, `whitehat e3-plan` | No database driver. Nothing schedules the 24h window. | +| E4 confinement | `whitehat e4-fixtures`, `targets/fixture-e4.json` | In process. No live target. Projects no credential. | +| P1/P2 capacity judgment | `whitehat capacity-fixture` | Judges supplied samples. Generates no load. | +| R4 erasure | `whitehat r4-fixtures`, `targets/fixture-r4.json` | In process. No store and no key. Incomplete inventory stays inconclusive. | +| ASM T-01…T-10 | `whitehat asm-fixtures` | In process. Live registrations stay `pending`. | +| Plane admission | `whitehat admit-plane` | Fail-closed. Fixture classes cannot use a live environment. | +| Finding delivery | `whitehat deliver` | Writes `outbox/`. Does not file in `risk-nexus`. | +| Cadence | `whitehat cadence` | Every family has `scheduled: false`. | +| Knowledge review | `tools/offline_emergent_knowledge_review.py` | Sealed-package offline review. Not a tenancy probe and not a provider call. | +| Custody | default broker raises; receipt broker projects handles | No OpenBao client and no credential value in evidence. | + +The previous `SCOPE.md` listed attacker models, cadence, and finding delivery +as in-scope capabilities without saying which of them run. The README said the +facility attacks systems on a schedule. Both overclaimed. `SCOPE.md` now +states the table above. + +## Intent item by item + +### Adversarial evidence artifacts + +Intent owns E2, E3, E4, P1/P2, and R4. + +- E2 is partial. The harness and one terminal audit-core pass exist. + `flex-auth` is pending. `tenant-engine` is `not_applicable`. Further live + E2 waits on `WHITEHAT-WP-0006-T03`. +- E3 is partial. The evaluator and the declared 24h window exist. No + runtime-safe database exists. `platform-pg` is `not_applicable`. Live E3 + waits on `WHITEHAT-WP-0006-T01`. `rapp-postgres` confirmed on 2026-10-04 + that the ordinary runtime identity still lacks SELECT on the conformance + view. +- E4 was a gap. The attacker model described it and no probe existed, so it + could not fail a known-bad fixture. `WHITEHAT-WP-0011` adds that fixture. + There is still no live E4 target, which is an honest stop rather than a + missing harness. +- P1/P2 is partial. The sample evaluator exists and generates no load. A live + window waits on an operator record, `WHITEHAT-WP-0006-T02`. The custodian + confirmed that record does not exist. +- R4 was a gap of the same kind as E4. The fixture now exists. `audit-core` + remains not an R4 target. No live R4 target is registered. + +### Attacker model + +Met for E2, E3, E4, P1/P2, and R4 in `docs/attacker-model.md`. Identity, +authorization, and availability are explicitly outside that model. The model +is not itself evidence. + +### Cadence + +Partial, and this was the sharpest overclaim. Intent says the interval +between runs is the exposure window for a detection control. Only E3 names an +interval, and no process runs it. The cadence register now says so for every +family. Building a scheduler would either send packets or pretend to. Neither +belongs in this repository while the live plans are blocked. + +### Differential testing + +Met for the offline E2 harness. It is not running against a live pair of +tenant contexts. + +### Target selection and authorization + +Met as a control. The plane refuses `not_applicable` and `pending` targets, +retired engagement IDs, a missing owner acknowledgement, and an engaged kill +switch. The control is doing its job by refusing the live residuals. + +### Rules of engagement + +Met. Version 0.2 is accepted and authorizes no live target by itself. + +### Findings delivery + +Partial by design. The repository can render a factual message and queue it. +`risk-nexus` files, grades, and discloses. A direct writer here would cross +that boundary. The gap is documentary: delivery must not be described as +filing. + +### Staff boundary + +Met. There is no database driver, no OpenBao client, and no cluster-mutation +API. The ASM fixture's custody counter is local. The default custody broker +raises before a live custody call. + +## Most relevant gaps + +1. **Live evidence is still absent for the claims that matter.** E3, the + remaining E2 target, P1/P2, and ASM T-01…T-10 have harnesses and no + authorized run. Owner replies on 2026-10-04 confirmed the missing + identity, substrate window, engagement, and consuming PEP. Those gaps + stay on `WHITEHAT-WP-0006` and `WHITEHAT-WP-0008`. This assessment does + not reopen them and cannot close them from this repository. +2. **E4 and R4 had no probe that could fail.** Intent lists them as owned + evidence, and the repository's own rule is that a probe which has never + failed a known-bad fixture is not evidence. That gap is local and is the + one this repository can close without a packet. `WHITEHAT-WP-0011` does + that. The calibrations are harness evidence only. +3. **The scope document described a scheduled attacking facility.** The + runnable surface is an offline harness, a fail-closed plane, a local + outbox, and one terminal historical pass. `SCOPE.md`, the README lead, + and `whitehat cadence` now say that. No family is scheduled. + +Gaps that are real and are not addressed here: identity, authorization, and +availability axes; a `risk-nexus` filing client; a live E4 or R4 target; and +any change to `conformance_state`. + +## Resulting work + +`WHITEHAT-WP-0011` records this assessment, rewrites the scope to the +capability table, and adds the E4 fixture, the R4 fixture, and the unscheduled +cadence register. A pass in those fixtures means the harness noticed the +known-bad cases. It does not mean a tenant boundary held. diff --git a/schemas/target-registration.schema.json b/schemas/target-registration.schema.json index 9a674c8..06460f9 100644 --- a/schemas/target-registration.schema.json +++ b/schemas/target-registration.schema.json @@ -16,7 +16,8 @@ "minItems": 1, "items": {"enum": [ "fixture-e2", "live-e2", "fixture-e3", "e3", - "fixture-capacity", "capacity", "fixture-asm", "asm" + "fixture-capacity", "capacity", "fixture-asm", "asm", + "fixture-e4", "fixture-r4" ]} }, "routes": {"type": "array", "items": {"type": "string"}}, diff --git a/src/whitehat_security/cadence.py b/src/whitehat_security/cadence.py new file mode 100644 index 0000000..b55c971 --- /dev/null +++ b/src/whitehat_security/cadence.py @@ -0,0 +1,72 @@ +"""Declared detection windows. Nothing in this register starts a run.""" + +from __future__ import annotations + + +def register() -> dict: + """Return the probe cadence this repository can actually stand behind. + + ``scheduled`` is false for every family. A declared interval is the + exposure window that would apply once a live run exists. It is not a + claim that a scheduler is running. + """ + return { + "schema_version": "whitehat-cadence/v1", + "scheduled_live_runs": False, + "families": [ + { + "family": "E2", + "executable": "whitehat fixtures", + "declared_interval": None, + "scheduled": False, + "exposure_window": ( + "Per approved engagement window only. The dated audit-core " + "pass is terminal. No standing schedule replaces it." + ), + "live_owner": "WHITEHAT-WP-0006-T03", + }, + { + "family": "E3", + "executable": "whitehat e3-fixtures", + "declared_interval": "24h", + "scheduled": False, + "exposure_window": ( + "24h plus run and reporting latency, once a runtime-safe " + "surface exists. The interval is not operating." + ), + "live_owner": "WHITEHAT-WP-0006-T01", + }, + { + "family": "E4", + "executable": "whitehat e4-fixtures", + "declared_interval": None, + "scheduled": False, + "exposure_window": "No live target and no schedule, so no detection window.", + "live_owner": None, + }, + { + "family": "P1/P2", + "executable": "whitehat capacity-fixture", + "declared_interval": None, + "scheduled": False, + "exposure_window": "An operator window only. The harness generates no load.", + "live_owner": "WHITEHAT-WP-0006-T02", + }, + { + "family": "R4", + "executable": "whitehat r4-fixtures", + "declared_interval": None, + "scheduled": False, + "exposure_window": "No live target and no schedule, so no detection window.", + "live_owner": None, + }, + { + "family": "ASM T-01..T-10", + "executable": "whitehat asm-fixtures", + "declared_interval": None, + "scheduled": False, + "exposure_window": "Per approved engagement for one named Canon test_id.", + "live_owner": "WHITEHAT-WP-0008-T01", + }, + ], + } diff --git a/src/whitehat_security/cli.py b/src/whitehat_security/cli.py index a436530..41ae52b 100644 --- a/src/whitehat_security/cli.py +++ b/src/whitehat_security/cli.py @@ -10,7 +10,10 @@ from .audit_fixtures import AuditFixture, audit_probe_suite from .capacity import capacity_calibration from .differential import execute from .asm import asm_calibration +from .cadence import register as cadence_register from .e3 import CADENCE, PROBES, e3_calibration +from .e4 import e4_calibration +from .r4 import r4_calibration from .engagement import AuthorizationError, Engagement from .fixtures import FixtureService, probe_suite from .model import RunReport, utc_now @@ -97,8 +100,13 @@ def main(argv: list[str] | None = None) -> None: deliver.add_argument("report") deliver.add_argument("--outbox", default="outbox") commands.add_parser("e3-plan") + commands.add_parser("cadence", help="print the unscheduled cadence register") e3_fix = commands.add_parser("e3-fixtures", help="calibrate E3 probes offline") e3_fix.add_argument("--output") + e4_fix = commands.add_parser("e4-fixtures", help="calibrate E4 probes offline") + e4_fix.add_argument("--output") + r4_fix = commands.add_parser("r4-fixtures", help="calibrate R4 probes offline") + r4_fix.add_argument("--output") capacity_fix = commands.add_parser( "capacity-fixture", help="calibrate P1/P2 evaluator offline" ) @@ -213,6 +221,25 @@ def main(argv: list[str] | None = None) -> None: print(json.dumps({"cadence": CADENCE, "probes": [asdict(probe) for probe in PROBES]}, indent=2, sort_keys=True)) return + if args.command == "cadence": + print(json.dumps(cadence_register(), indent=2, sort_keys=True)) + return + if args.command == "e4-fixtures": + result = e4_calibration() + rendered = json.dumps(result, indent=2, sort_keys=True) + "\n" + if args.output: + Path(args.output).write_text(rendered, encoding="utf-8") + else: + print(rendered, end="") + raise SystemExit(0 if result["outcome"] == "pass" else 1) + if args.command == "r4-fixtures": + result = r4_calibration() + rendered = json.dumps(result, indent=2, sort_keys=True) + "\n" + if args.output: + Path(args.output).write_text(rendered, encoding="utf-8") + else: + print(rendered, end="") + raise SystemExit(0 if result["outcome"] == "pass" else 1) if args.command == "e3-fixtures": result = e3_calibration() rendered = json.dumps(result, indent=2, sort_keys=True) + "\n" diff --git a/src/whitehat_security/e4.py b/src/whitehat_security/e4.py new file mode 100644 index 0000000..7529dd0 --- /dev/null +++ b/src/whitehat_security/e4.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from .model import Outcome, utc_now + + +@dataclass(frozen=True) +class E4Observation: + """In-process observation. No credential value and no network call.""" + + probe_id: str + authenticated: bool = False + read_other: bool = False + wrote_other: bool = False + reachable_only: bool = False + refreshed_outside_authority: bool = False + + +@dataclass(frozen=True) +class E4Result: + probe_id: str + outcome: Outcome + reason: str + + +def evaluate(observation: E4Observation) -> E4Result: + """Judge one E4 observation. + + Reachability without authentication is not a finding. Refreshing the + credential outside its recorded authority aborts the probe; it is not + target evidence. + """ + if observation.refreshed_outside_authority: + return E4Result( + observation.probe_id, "aborted", + "credential refresh outside recorded authority is a probe-control incident", + ) + if observation.authenticated or observation.read_other or observation.wrote_other: + return E4Result( + observation.probe_id, "finding", + "tenant A credential addressed tenant B substrate", + ) + if observation.reachable_only: + return E4Result( + observation.probe_id, "pass", + "reachability alone is not an E4 failure", + ) + return E4Result( + observation.probe_id, "pass", + "credential did not authenticate to or address tenant B", + ) + + +def e4_calibration() -> dict: + """Known-bad must fail before a known-good pass is trusted. No network.""" + started = utc_now() + known_bad = [ + evaluate(E4Observation("present-to-other-substrate", authenticated=True)), + evaluate(E4Observation("alternate-database", read_other=True)), + evaluate(E4Observation("alternate-schema", wrote_other=True)), + evaluate(E4Observation("refresh-outside-authority", refreshed_outside_authority=True)), + ] + bad_held = ( + known_bad[0].outcome == "finding" + and known_bad[1].outcome == "finding" + and known_bad[2].outcome == "finding" + and known_bad[3].outcome == "aborted" + ) + known_good = [ + evaluate(E4Observation("present-to-other-substrate")), + evaluate(E4Observation("alternate-database")), + evaluate(E4Observation("alternate-schema")), + evaluate(E4Observation("reachability-only", reachable_only=True)), + ] if bad_held else [] + good_held = all(result.outcome == "pass" for result in known_good) + return { + "schema_version": "whitehat-e4-calibration/v1", + "evidence_class": "fixture", + "run_id": f"e4-calibration-{started}", + "started_at": started, + "ended_at": utc_now(), + "outcome": "pass" if bad_held and good_held else "finding", + "known_bad_failed_first": bad_held, + "known_good": [result.__dict__ for result in known_good], + "known_bad": [result.__dict__ for result in known_bad], + "limitations": [ + "Offline E4 calibration evaluates the harness; it is not target assurance.", + "No credential was leased, and no network or database call was made.", + "No live E4 target is registered. Reachability alone is not a finding.", + ], + } diff --git a/src/whitehat_security/plane.py b/src/whitehat_security/plane.py index 10e3805..5bd541b 100644 --- a/src/whitehat_security/plane.py +++ b/src/whitehat_security/plane.py @@ -28,9 +28,12 @@ APPROVAL_CLASSES = { "capacity": frozenset({"p1-noisy-neighbour", "p2-noisy-neighbour"}), "fixture-asm": frozenset({"asm-assurance"}), "asm": frozenset({"asm-assurance"}), + "fixture-e4": frozenset({"e4-confinement"}), + "fixture-r4": frozenset({"r4-erasure"}), } FIXTURE_CLASSES = frozenset({ "fixture-e2", "fixture-e3", "fixture-capacity", "fixture-asm", + "fixture-e4", "fixture-r4", }) diff --git a/src/whitehat_security/r4.py b/src/whitehat_security/r4.py new file mode 100644 index 0000000..652387e --- /dev/null +++ b/src/whitehat_security/r4.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +from .model import Outcome, utc_now + +Route = Literal["R4-A", "R4-B"] + + +@dataclass(frozen=True) +class R4Case: + """Synthetic erasure observation. No storage client and no key material.""" + + probe_id: str + route: Route + horizon_elapsed: bool = False + key_destroyed: bool = False + inventory_complete: bool = False + recovered: bool = False + readable: bool = False + usable_key_survives: bool = False + low_entropy_oracle: bool = False + + +@dataclass(frozen=True) +class R4Result: + probe_id: str + route: Route + outcome: Outcome + reason: str + + +def evaluate(case: R4Case) -> R4Result: + """Judge one R4 case. An incomplete inventory is inconclusive, not a pass.""" + if not case.inventory_complete: + return R4Result( + case.probe_id, case.route, "inconclusive", + "declared copies were not all exercised", + ) + if case.route == "R4-A": + if not case.horizon_elapsed: + return R4Result( + case.probe_id, case.route, "inconclusive", + "recovery before the horizon is not an R4-A result", + ) + if case.recovered: + return R4Result( + case.probe_id, case.route, "finding", + "fixture content recovered after the erasure horizon", + ) + return R4Result( + case.probe_id, case.route, "pass", + "no fixture content recovered from the declared copies", + ) + if not case.key_destroyed: + return R4Result( + case.probe_id, case.route, "inconclusive", + "key destruction alone is not a pass, and destruction was not recorded", + ) + if case.readable or case.usable_key_survives or case.low_entropy_oracle: + return R4Result( + case.probe_id, case.route, "finding", + "erased fixture remains readable or confirmable", + ) + return R4Result( + case.probe_id, case.route, "pass", + "retained ciphertext is unreadable and no confirmation oracle remains", + ) + + +def r4_calibration() -> dict: + """Known-bad must fail before a known-good pass is trusted. No storage.""" + started = utc_now() + known_bad = [ + evaluate(R4Case( + "horizon-recovered", "R4-A", + horizon_elapsed=True, inventory_complete=True, recovered=True, + )), + evaluate(R4Case( + "ciphertext-readable", "R4-B", + key_destroyed=True, inventory_complete=True, readable=True, + )), + evaluate(R4Case( + "usable-key-survives", "R4-B", + key_destroyed=True, inventory_complete=True, usable_key_survives=True, + )), + evaluate(R4Case( + "low-entropy-oracle", "R4-B", + key_destroyed=True, inventory_complete=True, low_entropy_oracle=True, + )), + ] + inconclusive = [ + evaluate(R4Case("inventory-incomplete", "R4-A", horizon_elapsed=True)), + evaluate(R4Case("before-horizon", "R4-A", inventory_complete=True)), + evaluate(R4Case("key-not-destroyed", "R4-B", inventory_complete=True)), + ] + bad_held = all(result.outcome == "finding" for result in known_bad) + limits_held = all(result.outcome == "inconclusive" for result in inconclusive) + known_good = [ + evaluate(R4Case( + "horizon-clear", "R4-A", + horizon_elapsed=True, inventory_complete=True, + )), + evaluate(R4Case( + "key-destroyed-clear", "R4-B", + key_destroyed=True, inventory_complete=True, + )), + ] if bad_held and limits_held else [] + good_held = all(result.outcome == "pass" for result in known_good) + return { + "schema_version": "whitehat-r4-calibration/v1", + "evidence_class": "fixture", + "run_id": f"r4-calibration-{started}", + "started_at": started, + "ended_at": utc_now(), + "outcome": "pass" if bad_held and limits_held and good_held else "finding", + "known_bad_failed_first": bad_held, + "known_good": [result.__dict__ for result in known_good], + "known_bad": [result.__dict__ for result in known_bad], + "inconclusive": [result.__dict__ for result in inconclusive], + "limitations": [ + "Offline R4 calibration evaluates the harness; it is not target assurance.", + "No backup, replica, key, or ciphertext was accessed.", + "audit-core is not an R4 target. No live R4 target is registered.", + "An incomplete copy inventory stays inconclusive and is not a pass.", + ], + } diff --git a/src/whitehat_security/targets.py b/src/whitehat_security/targets.py index 423d3e8..66e47a0 100644 --- a/src/whitehat_security/targets.py +++ b/src/whitehat_security/targets.py @@ -45,6 +45,12 @@ def load_registration(path: str | Path) -> dict[str, Any]: raise AuthorizationError(f"{path}: E3 registration must project one runtime identity") if classes & {"fixture-capacity", "capacity"} and count not in {0, 1}: raise AuthorizationError(f"{path}: capacity registration projects at most one aggressor identity") + if classes & {"fixture-e4", "fixture-r4"} and count != 0: + raise AuthorizationError(f"{path}: E4 and R4 fixtures project no credential") + if (classes & {"fixture-e4", "fixture-r4"}) and ( + classes & {"live-e2", "e3", "capacity", "asm"} + ): + raise AuthorizationError(f"{path}: E4 and R4 fixtures cannot carry a live approval class") if classes & {"fixture-asm", "asm"} and count not in {0, 1, 2}: raise AuthorizationError(f"{path}: ASM registration projects at most two identities") if classes & {"fixture-asm", "asm"} and not data.get("test_id"): diff --git a/targets/README.md b/targets/README.md index d655596..73a431d 100644 --- a/targets/README.md +++ b/targets/README.md @@ -12,6 +12,8 @@ honest applicability record the test plane admits against. | `fixture-e3` | applicable | In-process E3 evaluator. One runtime identity, no `BYPASSRLS`. | | `platform-pg` | not_applicable | No ordinary runtime identity can read the conformance view. | | `fixture-capacity` | applicable | In-process P1/P2 evaluator. Generates no load. | +| `fixture-e4` | applicable | In-process E4 evaluator. Projects no credential. No live E4 target. | +| `fixture-r4` | applicable | In-process R4 evaluator. Opens no store. No live R4 target. | | `shared-substrate` | pending | Live capacity needs an operator window and aggressor ceiling. | | `asm-t01` … `asm-t10` | pending | Gate House ASM T-01…T-10 live registrations. Separate `asm` class; not E2/E3/capacity. See `WHITEHAT-WP-0007`. | | `fixture-asm-t01` … `fixture-asm-t10` | applicable | In-process ASM known-bad/known-good harnesses. Offline only. | diff --git a/targets/fixture-e4.json b/targets/fixture-e4.json new file mode 100644 index 0000000..1c18e41 --- /dev/null +++ b/targets/fixture-e4.json @@ -0,0 +1,35 @@ +{ + "schema_version": "whitehat-target/v1", + "target_id": "fixture-e4", + "posture_claim": "E4", + "attacker_model": "leaked tenant-A credential presented to tenant-B substrate", + "applicability": "applicable", + "applicability_reason": "In-process evaluator created by this repository. It projects no credential and makes no network call. No live E4 target is registered.", + "approval_classes": ["fixture-e4"], + "adapter": "src/whitehat_security/e4.py", + "probe_pack": "src/whitehat_security/e4.py", + "known_bad_calibration": "src/whitehat_security/e4.py", + "fixture_lifecycle": { + "create": "in-process observations", + "delete": "drop process state", + "ids_are_synthetic": true + }, + "egress": "in-process", + "routes": [ + "other-substrate", + "alternate-database", + "alternate-schema", + "reachability", + "credential-refresh" + ], + "identities": { + "count": 0, + "role": "none; fixture projects no credential", + "broker_audience": "whitehat-e4/fixture" + }, + "abort_telemetry": [ + "credential_refresh_outside_authority", + "unexpected_credential_value", + "network_attempt" + ] +} diff --git a/targets/fixture-r4.json b/targets/fixture-r4.json new file mode 100644 index 0000000..db9a6c1 --- /dev/null +++ b/targets/fixture-r4.json @@ -0,0 +1,29 @@ +{ + "schema_version": "whitehat-target/v1", + "target_id": "fixture-r4", + "posture_claim": "R4", + "attacker_model": "holder of a pre-erasure copy of synthetic fixture data", + "applicability": "applicable", + "applicability_reason": "In-process evaluator created by this repository. It opens no storage client and holds no key. No live R4 target is registered. audit-core is not an R4 target.", + "approval_classes": ["fixture-r4"], + "adapter": "src/whitehat_security/r4.py", + "probe_pack": "src/whitehat_security/r4.py", + "known_bad_calibration": "src/whitehat_security/r4.py", + "fixture_lifecycle": { + "create": "in-process cases", + "delete": "drop process state", + "ids_are_synthetic": true + }, + "egress": "in-process", + "routes": ["horizon", "key-destroyed"], + "identities": { + "count": 0, + "role": "none; fixture holds no key", + "broker_audience": "whitehat-r4/fixture" + }, + "abort_telemetry": [ + "incomplete_copy_inventory", + "recovered_content_retained", + "key_material_present" + ] +} diff --git a/tests/test_cli.py b/tests/test_cli.py index 64799ea..d685c4e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -18,7 +18,7 @@ def test_validate_engagement_reports_clean_denial(tmp_path, capsys): def test_validate_targets_accepts_catalog(capsys): main(["validate-targets", "targets"]) - assert capsys.readouterr().out.startswith("validated 28 target registrations") + assert capsys.readouterr().out.startswith("validated 30 target registrations") def test_kill_switch_is_clear_by_default(capsys): diff --git a/tests/test_e4_r4_cadence.py b/tests/test_e4_r4_cadence.py new file mode 100644 index 0000000..ab7ad74 --- /dev/null +++ b/tests/test_e4_r4_cadence.py @@ -0,0 +1,47 @@ +from whitehat_security.cadence import register +from whitehat_security.e4 import E4Observation, e4_calibration, evaluate as evaluate_e4 +from whitehat_security.r4 import R4Case, evaluate as evaluate_r4, r4_calibration + + +def test_e4_known_bad_is_a_finding_and_reachability_is_not(): + assert evaluate_e4(E4Observation("auth", authenticated=True)).outcome == "finding" + assert evaluate_e4(E4Observation("reach", reachable_only=True)).outcome == "pass" + assert evaluate_e4( + E4Observation("refresh", refreshed_outside_authority=True) + ).outcome == "aborted" + + +def test_e4_calibration_fails_known_bad_before_trusting_a_pass(): + report = e4_calibration() + assert report["known_bad_failed_first"] is True + assert report["outcome"] == "pass" + assert report["evidence_class"] == "fixture" + assert {item["outcome"] for item in report["known_bad"]} == {"finding", "aborted"} + assert {item["outcome"] for item in report["known_good"]} == {"pass"} + + +def test_r4_incomplete_inventory_is_not_a_pass(): + result = evaluate_r4(R4Case("partial", "R4-A", horizon_elapsed=True, recovered=False)) + assert result.outcome == "inconclusive" + destroyed = evaluate_r4(R4Case("key-only", "R4-B", key_destroyed=True)) + assert destroyed.outcome == "inconclusive" + + +def test_r4_calibration_fails_known_bad_before_trusting_a_pass(): + report = r4_calibration() + assert report["known_bad_failed_first"] is True + assert report["outcome"] == "pass" + assert {item["outcome"] for item in report["known_bad"]} == {"finding"} + assert {item["outcome"] for item in report["known_good"]} == {"pass"} + assert {item["outcome"] for item in report["inconclusive"]} == {"inconclusive"} + + +def test_cadence_register_schedules_nothing(): + record = register() + assert record["scheduled_live_runs"] is False + families = {item["family"]: item for item in record["families"]} + assert set(families) == {"E2", "E3", "E4", "P1/P2", "R4", "ASM T-01..T-10"} + assert all(item["scheduled"] is False for item in families.values()) + assert families["E3"]["declared_interval"] == "24h" + assert families["E4"]["declared_interval"] is None + assert families["R4"]["live_owner"] is None diff --git a/tests/test_plane.py b/tests/test_plane.py index 6ffa3c5..5a16f0a 100644 --- a/tests/test_plane.py +++ b/tests/test_plane.py @@ -263,6 +263,40 @@ def test_fixture_e3_projects_one_runtime_identity(tmp_path): assert cleanup(lease, broker)["credential_revocation"] == "revoked" +def test_fixture_e4_and_r4_project_no_credential_and_refuse_a_live_environment(tmp_path): + cases = ( + ("fixture-e4", "fixture-e4", "e4-confinement", "other-substrate"), + ("fixture-r4", "fixture-r4", "r4-erasure", "horizon"), + ) + for target_id, approval_class, technique, route in cases: + broker = LocalBroker() + engagement = load_engagement(tmp_path, fixture_record( + engagement_id=f"WH-ENG-FIXTURE-{target_id}", + target_id=target_id, approval_class=approval_class, + techniques=[technique], routes=[route], + )) + lease = admit( + engagement=engagement, + registration=load_registration(f"targets/{target_id}.json"), + broker=broker, kill_switch=KillSwitch(tmp_path / "KILL"), + now=NOW, retired=set(), + ) + assert lease.identities == () + live = load_engagement(tmp_path, fixture_record( + engagement_id=f"WH-ENG-LIVE-{target_id}", + target_id=target_id, approval_class=approval_class, + techniques=[technique], routes=[route], environment="build", + plane_namespace="whitehat", runner_image_digest="sha256:abc", + )) + with pytest.raises(AuthorizationError, match="environment=fixture"): + admit( + engagement=live, + registration=load_registration(f"targets/{target_id}.json"), + broker=LocalBroker(), kill_switch=KillSwitch(tmp_path / "KILL"), + now=NOW, retired=set(), + ) + + def test_fixture_capacity_projects_no_identities(tmp_path): broker = LocalBroker() engagement = load_engagement(tmp_path, fixture_record( diff --git a/workplans/WHITEHAT-WP-0011-scope-capability-gaps.md b/workplans/WHITEHAT-WP-0011-scope-capability-gaps.md new file mode 100644 index 0000000..b3d5e22 --- /dev/null +++ b/workplans/WHITEHAT-WP-0011-scope-capability-gaps.md @@ -0,0 +1,80 @@ +--- +id: WHITEHAT-WP-0011 +type: workplan +title: "State actual scope and calibrate the missing E4 and R4 probes" +domain: infotech +repo: whitehat-security +status: finished +owner: whitehat-security +topic_slug: whitehat-security +created: "2026-10-04" +updated: "2026-10-04" +related: + - WHITEHAT-WP-0006 + - WHITEHAT-WP-0008 +--- + +# State actual scope and calibrate the missing E4 and R4 probes + +`INTENT.md` owns E2, E3, E4, P1/P2, and R4 evidence, plus the cadence that +sets the exposure window. On 2026-10-04 the repository could calibrate E2, +E3, P1/P2, and ASM offline, and it had prose for E4 and R4 with no probe. +`SCOPE.md` described those prose items as capabilities. This plan records +the comparison, makes the scope match the harness, and adds the two missing +offline calibrations. It does not reopen `WHITEHAT-WP-0006` or +`WHITEHAT-WP-0008`. It authorizes no packet, database connection, or load. + +The live gaps remain the ones those blocked plans already name. They are +not implementable here until the owner returns change. + +## T01 — Record scope against intent + +```task +id: WHITEHAT-WP-0011-T01 +status: done +priority: high +``` + +`history/2026-10-04T0649Z-scope-against-intent.md` compares each owned intent +item with the runnable surface. `SCOPE.md` now lists what the commands do +and what they refuse. The README no longer says the facility attacks systems +on a schedule. + +## T02 — Calibrate E4 offline + +```task +id: WHITEHAT-WP-0011-T02 +status: done +priority: high +``` + +`whitehat e4-fixtures` fails known-bad confinement cases before it trusts a +known-good pass. Reachability alone stays a pass. A credential refresh +outside recorded authority aborts. `targets/fixture-e4.json` is applicable +only as `fixture-e4` and projects no credential. The plane rejects that +class in a live environment. No live E4 class exists. + +## T03 — Calibrate R4 offline + +```task +id: WHITEHAT-WP-0011-T03 +status: done +priority: high +``` + +`whitehat r4-fixtures` fails recovered content, readable ciphertext, a +surviving usable key, and a low-entropy oracle before it trusts a clear +result. An incomplete copy inventory stays inconclusive. `targets/fixture-r4.json` +projects no key. `audit-core` is not registered as an R4 target. + +## T04 — Publish the unscheduled cadence register + +```task +id: WHITEHAT-WP-0011-T04 +status: done +priority: medium +``` + +`whitehat cadence` states E2, E3, E4, P1/P2, R4, and ASM T-01…T-10. E3 keeps +its declared 24h detection window. Every family has `scheduled: false`, and +`scheduled_live_runs` is false. The register does not start a run.