Prepare receipt-bound audit E2 third attempt

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02991-be07-7bb3-8b6d-e9701b5621de
This commit is contained in:
tegwick 2026-08-22 23:23:48 +02:00
parent 0525e632d7
commit 96d6781036
12 changed files with 440 additions and 27 deletions

View file

@ -27,3 +27,4 @@
| task | WHITEHAT-WP-0003-T01 | done | — | workplans/WHITEHAT-WP-0003-platform-custody-adapter.md | | task | WHITEHAT-WP-0003-T01 | done | — | workplans/WHITEHAT-WP-0003-platform-custody-adapter.md |
| task | WHITEHAT-WP-0004-T01 | done | — | workplans/WHITEHAT-WP-0004-canonical-custody-binding.md | | task | WHITEHAT-WP-0004-T01 | done | — | workplans/WHITEHAT-WP-0004-canonical-custody-binding.md |
| task | WHITEHAT-WP-0005-T01 | done | — | workplans/WHITEHAT-WP-0005-broker-receipt-admission.md | | task | WHITEHAT-WP-0005-T01 | done | — | workplans/WHITEHAT-WP-0005-broker-receipt-admission.md |
| task | WHITEHAT-WP-0005-T02 | done | — | workplans/WHITEHAT-WP-0005-broker-receipt-admission.md |

View file

@ -0,0 +1,69 @@
{
"engagement_id": "WH-ENG-20260822-AUDIT-E2-03",
"status": "proposed",
"proposal_at": "2026-08-22T21:15:00Z",
"proposal_reason": "Fresh attended attempt after -02 proved exact cleanup and exposed the now-implemented WP-0025 custody receipt handoff. No terminal identifier, identity, tenant, object, or Kubernetes Secret is reused.",
"authorization_id": "operator-session-2026-08-22-e2-03-approval",
"authorizer": "pending explicit repository operator approval",
"approved_at": "",
"expires_at": "2026-08-22T22:15:00Z",
"target": "http://audit-core.audit-core.svc.cluster.local:8080",
"target_id": "audit-core",
"target_owner": "audit-core / infrastructure operator",
"target_owner_acknowledged_at": "",
"target_owner_acknowledgement_status": "pending direct review after operator approval",
"environment": "production",
"production_approval": "pending explicit approval of this exact -03 package",
"approval_class": "live-e2",
"plane_namespace": "whitehat",
"runner_image_digest": "sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6",
"namespace": "whitehat",
"source": "dedicated pod whitehat/whitehat-e2-audit-03, labels whitehat.security/plane=true, whitehat.security/target=audit-core, whitehat.security/engagement=WH-ENG-20260822-AUDIT-E2-03, serviceAccount whitehat-runner, automountServiceAccountToken=false",
"source_image": "forgejo.coulomb.social/coulomb/audit-core@sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6",
"control_plane_scope": [
"GET apps/v1 deployment audit-core/audit-core for target revision only",
"GET v1 service audit-core/audit-core for target identity only",
"CREATE/GET/EXEC/DELETE v1 pod whitehat/whitehat-e2-audit-03 only; EXEC may stream runners/audit_core_e2.py to python3 stdin and invoke no other program",
"mount but never GET v1 secret whitehat/whitehat-e2-audit-credentials-03 provisioned by the custody owner",
"GET pod log whitehat/whitehat-e2-audit-03 only"
],
"routes": [
"GET /readyz",
"POST /v1/events",
"GET /v1/events/{event_id}",
"GET /v1/events?correlation_id={correlation_id}"
],
"fixture_ids": [
"tenant:trial:whitehat-a-20260822-03", "tenant:trial:whitehat-b-20260822-03",
"whitehat-e2-event-a-20260822-03", "whitehat-e2-event-b-20260822-03",
"whitehat-e2-absent-20260822-03", "whitehat-e2-forged-b-20260822-03",
"whitehat-e2-correlation-20260822-03"
],
"credential_lane": "RAILIANCE-WP-0025 canonical value-safe custody receipts; no credential value retained",
"credential_role": "two temporary ordinary tenant-scoped may_read/may_write fixture senders bound to source=whitehat-security",
"credential_max_ttl_seconds": 900,
"techniques": ["e2-differential"],
"prohibited_techniques": [
"discovery", "direct-database-access", "saturation", "large-payload",
"real-data-collection", "adjacent-service-follow-up",
"any control-plane operation outside control_plane_scope"
],
"rate_limit_per_minute": 10,
"max_concurrency": 1,
"maximum_requests": 30,
"window_start": "2026-08-22T22:00:00Z",
"window_end": "2026-08-22T22:15:00Z",
"operator_contact": "repository operator in coordinating session",
"abort_contact": "repository operator in coordinating session",
"abort_thresholds": {
"p95_latency_ms": 500,
"probe_attributable_5xx": 1,
"unexpected_or_non_fixture_data": 1,
"missing_attribution": 1
},
"posture_claim": "implemented E2; currently evidenced E1",
"attacker_model": "E2-authenticated-tenant-a",
"finding_destination": "risk-nexus",
"plane_bundle_digest": "9636f48f0b994118ff60a8c014e0099486945d66a2b3d3582dc57a09862b2035",
"target_ingress": "audit-core-whitehat-ingress at audit-core 5b5196e; namespace=whitehat AND whitehat.security/plane=true AND whitehat.security/target=audit-core"
}

View file

@ -0,0 +1,13 @@
# WH-ENG-20260822-AUDIT-E2-03
Status: **proposed** for `2026-08-22T22:00:00Z``22:15:00Z`.
This is a fresh, terminal-unique attempt. It does not reuse the `-01` or `-02`
identifier, senders, tenants, objects, mounted Secret, or runner pod. The
projection gate closes at `22:03:00Z`; all custody resources expire and must be
cleaned by `22:15:00Z`.
The run uses the canonical RAILIANCE-WP-0025 projection contract, Whitehat
broker-readiness receipt, projection receipt, contract-bound `admit-plane`,
cleanup receipt, and finalized report. No live action is authorized while this
record remains proposed.

View file

@ -1,16 +1,20 @@
# Projection receipts # Projection receipts
Copy `example-projection-receipt.json` at window start and fill in the live Legacy value-safe receipts remain supported for historical fixtures. New live
engagement id, `projected_at`, `expires_at`, and identity *names* from the engagements use the canonical RAILIANCE-WP-0025 contract, broker-readiness,
custody procedure. Never put token values, passwords, or registry payloads projection, and cleanup receipts. Never hand-edit a live receipt or put token
here. values, passwords, or registry payloads here.
Live admission: Live admission:
```sh ```sh
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane \ PYTHONPATH=src python3 -m whitehat_security.cli admit-plane \
engagements/<record>.json targets/audit-core-e2.json \ engagements/<record>.json targets/audit-core-e2.json \
--receipt engagements/receipts/<engagement>.json --receipt /approved/path/projection.json \
--contract /approved/path/projection-contract.json \
--broker-receipt /approved/path/broker.json
``` ```
Without `--receipt`, live admission fails closed and requests no credential. Without all three bound documents, WP-0025 admission fails closed and requests
no credential. After platform cleanup, use `finalize-report` with the cleanup
receipt before delivery.

View file

@ -0,0 +1,113 @@
# WH-ENG-20260822-AUDIT-E2-03 — attended invocation
This package is proposed for `22:00Z``22:15Z`; projection closes at `22:03Z`.
Do nothing live until the engagement and the exact WP-0025 contract are
approved and audit-core has acknowledged them.
Runtime value-safe artifacts are written beneath
`/tmp/WH-ENG-20260822-AUDIT-E2-03/`:
- `broker.json`
- `projection.json`
- `runner-report.json`
- `cleanup.json`
- `final-report.json`
The platform contract is
`/home/worsch/railiance-platform/docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json`.
## Broker gate, projection, and admission
From `railiance-platform`, after approval and before the window:
```bash
python3 scripts/wp0025-broker-readiness.py approve \
--contract docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json \
--consumer-root /home/worsch/whitehat-security \
--reviewer whitehat-owner \
--receipt-out /tmp/WH-ENG-20260822-AUDIT-E2-03/broker.json
python3 scripts/custody-projection.py preflight \
--contract docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json
```
At or after `22:00Z`, no later than `22:03Z`:
```bash
python3 scripts/custody-projection.py project \
--contract docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json \
--confirm WH-ENG-20260822-AUDIT-E2-03:attended \
--receipt-out /tmp/WH-ENG-20260822-AUDIT-E2-03/projection.json
```
From `whitehat-security`, admit before applying the runner:
```bash
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane \
engagements/2026-08-22-audit-core-e2-03.json \
targets/audit-core-e2.json \
--receipt /tmp/WH-ENG-20260822-AUDIT-E2-03/projection.json \
--contract /home/worsch/railiance-platform/docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json \
--broker-receipt /tmp/WH-ENG-20260822-AUDIT-E2-03/broker.json
```
## Runner
Only after admission succeeds:
```bash
ssh -o BatchMode=yes railiance01 kubectl apply -f - \
< engagements/runtime/whitehat-e2-audit-20260822-03.yaml
ssh -o BatchMode=yes railiance01 kubectl -n whitehat wait \
--for=condition=Ready pod/whitehat-e2-audit-03 --timeout=60s
```
Stream `runners/audit_core_e2.py` to `python3 -` inside that pod with these
exact arguments:
```text
--base-url http://audit-core.audit-core.svc.cluster.local:8080
--engagement-id WH-ENG-20260822-AUDIT-E2-03
--authorization-id operator-session-2026-08-22-e2-03-approval
--target-revision sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6
--token-a-file /var/run/secrets/whitehat/token-a
--token-b-file /var/run/secrets/whitehat/token-b
--tenant-a tenant:trial:whitehat-a-20260822-03
--tenant-b tenant:trial:whitehat-b-20260822-03
--event-a whitehat-e2-event-a-20260822-03
--event-b whitehat-e2-event-b-20260822-03
--absent-event whitehat-e2-absent-20260822-03
--forged-event whitehat-e2-forged-b-20260822-03
--correlation whitehat-e2-correlation-20260822-03
--occurred-at 2026-08-22T22:00:00Z
--rate 10 --max-requests 30 --abort-p95-ms 500
```
The runner output is the sanitized `runner-report.json`. Delete the runner
immediately after execution, whether it passes, finds, or aborts.
## Cleanup and report finalization
From `railiance-platform`, before `22:15Z`:
```bash
python3 scripts/custody-projection.py cleanup \
--contract docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json \
--receipt /tmp/WH-ENG-20260822-AUDIT-E2-03/projection.json \
--confirm WH-ENG-20260822-AUDIT-E2-03:cleanup \
--receipt-out /tmp/WH-ENG-20260822-AUDIT-E2-03/cleanup.json
```
From `whitehat-security`, bind the report to the cleanup receipt and deliver it:
```bash
PYTHONPATH=src python3 -m whitehat_security.cli finalize-report \
/tmp/WH-ENG-20260822-AUDIT-E2-03/runner-report.json \
--contract /home/worsch/railiance-platform/docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json \
--receipt /tmp/WH-ENG-20260822-AUDIT-E2-03/projection.json \
--cleanup-receipt /tmp/WH-ENG-20260822-AUDIT-E2-03/cleanup.json \
--output /tmp/WH-ENG-20260822-AUDIT-E2-03/final-report.json
PYTHONPATH=src python3 -m whitehat_security.cli deliver \
/tmp/WH-ENG-20260822-AUDIT-E2-03/final-report.json --outbox outbox
```

View file

@ -0,0 +1,51 @@
# CONTRACT ONLY. Do not apply until WP-0025 projection and admit-plane succeed.
apiVersion: v1
kind: Pod
metadata:
name: whitehat-e2-audit-03
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-20260822-AUDIT-E2-03
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: forgejo.coulomb.social/coulomb/audit-core@sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6
imagePullPolicy: IfNotPresent
command: ["sleep", "900"]
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-03
defaultMode: 0440
items:
- key: token-a
path: token-a
- key: token-b
path: token-b

View file

@ -183,10 +183,36 @@ def invocation_fixture_ids(args: argparse.Namespace) -> set[str]:
} }
def build_report(args: argparse.Namespace, *, started: str, outcome: str,
attempted_operations: int, results: list[dict[str, Any]],
limitations: list[str]) -> dict[str, Any]:
return {
"schema_version": "whitehat-run/v1",
"run_id": f"{args.engagement_id}-{started}",
"evidence_class": "target",
"engagement_id": args.engagement_id,
"authorization_id": args.authorization_id,
"target": "audit-core",
"target_revision": args.target_revision,
"posture_claim": "implemented E2; currently evidenced E1",
"attacker_model": "E2-authenticated-tenant-a",
"started_at": started,
"ended_at": now(),
"outcome": outcome,
"attempted_operations": attempted_operations,
"cleanup": "named immutable audit fixtures retained by target contract",
"credential_revocation": "pending orchestrator cleanup",
"probes": results,
"limitations": limitations,
"assurance_statement": ASSURANCE,
}
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True) parser.add_argument("--base-url", required=True)
parser.add_argument("--engagement-id", required=True) parser.add_argument("--engagement-id", required=True)
parser.add_argument("--authorization-id", required=True)
parser.add_argument("--target-revision", required=True) parser.add_argument("--target-revision", required=True)
parser.add_argument("--token-a-file", required=True) parser.add_argument("--token-a-file", required=True)
parser.add_argument("--token-b-file", required=True) parser.add_argument("--token-b-file", required=True)
@ -278,26 +304,14 @@ def main() -> None:
except Abort as error: except Abort as error:
outcome = "aborted" outcome = "aborted"
limitations.append(str(error)) limitations.append(str(error))
report = { report = build_report(
"schema_version": "whitehat-run/v1", args,
"run_id": f"{args.engagement_id}-{started}", started=started,
"evidence_class": "target", outcome=outcome,
"engagement_id": args.engagement_id, attempted_operations=client.requests,
"authorization_id": "operator-session-2026-08-21-e2-approval", results=results,
"target": "audit-core", limitations=limitations,
"target_revision": args.target_revision, )
"posture_claim": "implemented E2; currently evidenced E1",
"attacker_model": "E2-authenticated-tenant-a",
"started_at": started,
"ended_at": now(),
"outcome": outcome,
"attempted_operations": client.requests,
"cleanup": "named immutable audit fixtures retained by target contract",
"credential_revocation": "pending orchestrator cleanup",
"probes": results,
"limitations": limitations,
"assurance_statement": ASSURANCE,
}
print(json.dumps(report, sort_keys=True)) print(json.dumps(report, sort_keys=True))
raise SystemExit(0 if outcome == "pass" else 1) raise SystemExit(0 if outcome == "pass" else 1)

View file

@ -14,7 +14,7 @@ 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 .plane import KillSwitch, admit, default_broker, retired_ids from .plane import KillSwitch, admit, default_broker, retired_ids
from .platform_custody import broker_from_receipt from .platform_custody import broker_from_receipt, finalize_run_report
from .reporting import queue_risk_nexus, risk_nexus_message from .reporting import queue_risk_nexus, risk_nexus_message
from .targets import load_catalog, load_registration from .targets import load_catalog, load_registration
@ -100,6 +100,12 @@ def main(argv: list[str] | None = None) -> None:
"--broker-receipt", "--broker-receipt",
help="WP-0025 broker-readiness receipt; required for railiance.custody-projection-receipt", help="WP-0025 broker-readiness receipt; required for railiance.custody-projection-receipt",
) )
finalize = commands.add_parser("finalize-report")
finalize.add_argument("report")
finalize.add_argument("--contract", required=True)
finalize.add_argument("--receipt", required=True)
finalize.add_argument("--cleanup-receipt", required=True)
finalize.add_argument("--output", required=True)
commands.add_parser("kill-switch") commands.add_parser("kill-switch")
deliver = commands.add_parser("deliver") deliver = commands.add_parser("deliver")
deliver.add_argument("report") deliver.add_argument("report")
@ -171,6 +177,27 @@ def main(argv: list[str] | None = None) -> None:
raise SystemExit(1) raise SystemExit(1)
print("clear") print("clear")
return return
if args.command == "finalize-report":
try:
report = json.loads(Path(args.report).read_text(encoding="utf-8"))
contract = json.loads(Path(args.contract).read_text(encoding="utf-8"))
projection = json.loads(Path(args.receipt).read_text(encoding="utf-8"))
cleanup = json.loads(Path(args.cleanup_receipt).read_text(encoding="utf-8"))
finalized = finalize_run_report(
report, projection=projection, cleanup=cleanup, contract=contract
)
RunReport(**finalized)
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(
json.dumps(finalized, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError, TypeError) as error:
print(f"not authorized: {error}", file=sys.stderr)
raise SystemExit(2) from None
print(f"finalized: {output}")
return
if args.command == "deliver": if args.command == "deliver":
try: try:
report = RunReport(**json.loads(Path(args.report).read_text(encoding="utf-8"))) report = RunReport(**json.loads(Path(args.report).read_text(encoding="utf-8")))

View file

@ -331,6 +331,32 @@ def validate_cleanup_receipt(
return document return document
def finalize_run_report(
report: dict[str, Any], *, projection: dict[str, Any],
cleanup: dict[str, Any], contract: dict[str, Any]
) -> dict[str, Any]:
"""Bind a sanitized runner report to the canonical custody cleanup."""
validate_projection_contract(contract)
validate_projection_receipt(projection, contract=contract)
validate_cleanup_receipt(cleanup, projection=projection, contract=contract)
if report.get("engagement_id") != contract["engagement_id"]:
raise AuthorizationError("run report engagement does not match custody contract")
if report.get("target") != contract["target"]["id"]:
raise AuthorizationError("run report target does not match custody contract")
if report.get("target_revision") != contract["target"]["image_digest"]:
raise AuthorizationError("run report revision does not match custody contract")
result = dict(report)
result["cleanup"] = (
"WP-0025 receipt-bound cleanup completed at "
f"{cleanup['cleaned_at']} for projection {projection['receipt_id']}"
)
result["credential_revocation"] = (
"revoked by railiance-platform custody cleanup for lease "
f"{projection['lease_id']}"
)
return result
def validate_broker_readiness( def validate_broker_readiness(
document: dict[str, Any], *, contract: dict[str, Any], now: datetime | None = None document: dict[str, Any], *, contract: dict[str, Any], now: datetime | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:

View file

@ -84,3 +84,44 @@ def test_retry_invocation_fixture_set_is_declared_and_distinct():
assert fixtures <= set(engagement["fixture_ids"]) assert fixtures <= set(engagement["fixture_ids"])
assert fixtures.isdisjoint(previous["fixture_ids"]) assert fixtures.isdisjoint(previous["fixture_ids"])
assert engagement["engagement_id"] != previous["engagement_id"] assert engagement["engagement_id"] != previous["engagement_id"]
def test_report_uses_supplied_authorization_id():
args = Namespace(
engagement_id="WH-ENG-EXAMPLE",
authorization_id="operator-session-example",
target_revision="sha256:" + "a" * 64,
)
report = runner.build_report(
args,
started="2026-08-22T22:00:00Z",
outcome="pass",
attempted_operations=9,
results=[],
limitations=[],
)
assert report["authorization_id"] == "operator-session-example"
assert report["engagement_id"] == "WH-ENG-EXAMPLE"
assert report["attempted_operations"] == 9
def test_third_attempt_fixtures_are_new_and_complete():
previous = json.loads(
Path("engagements/2026-08-22-audit-core-e2-02.json").read_text()
)
engagement = json.loads(
Path("engagements/2026-08-22-audit-core-e2-03.json").read_text()
)
args = Namespace(
tenant_a="tenant:trial:whitehat-a-20260822-03",
tenant_b="tenant:trial:whitehat-b-20260822-03",
event_a="whitehat-e2-event-a-20260822-03",
event_b="whitehat-e2-event-b-20260822-03",
absent_event="whitehat-e2-absent-20260822-03",
forged_event="whitehat-e2-forged-b-20260822-03",
correlation="whitehat-e2-correlation-20260822-03",
)
fixtures = runner.invocation_fixture_ids(args)
assert fixtures <= set(engagement["fixture_ids"])
assert fixtures.isdisjoint(previous["fixture_ids"])
assert engagement["engagement_id"] != previous["engagement_id"]

View file

@ -15,6 +15,7 @@ from whitehat_security.platform_custody import (
broker_from_receipt, broker_from_receipt,
contract_digest, contract_digest,
digest, digest,
finalize_run_report,
interface_artifacts, interface_artifacts,
load_schemas, load_schemas,
resource_names, resource_names,
@ -381,3 +382,38 @@ def test_cleanup_receipt_must_match_lease():
bad = cleanup_receipt(bound, projection) bad = cleanup_receipt(bound, projection)
bad["lease_id"] = "custody:" + "0" * 32 bad["lease_id"] = "custody:" + "0" * 32
validate_cleanup_receipt(bad, projection=projection, contract=bound) validate_cleanup_receipt(bad, projection=projection, contract=bound)
def test_finalized_report_is_bound_to_cleanup_receipt():
bound = contract()
projection = projection_receipt(bound)
cleanup = cleanup_receipt(bound, projection)
report = {
"engagement_id": bound["engagement_id"],
"target": bound["target"]["id"],
"target_revision": bound["target"]["image_digest"],
"credential_revocation": "pending orchestrator cleanup",
"cleanup": "pending",
}
finalized = finalize_run_report(
report, projection=projection, cleanup=cleanup, contract=bound
)
assert finalized["credential_revocation"].startswith("revoked by railiance-platform")
assert projection["receipt_id"] in finalized["cleanup"]
def test_finalized_report_refuses_wrong_engagement():
bound = contract()
projection = projection_receipt(bound)
cleanup = cleanup_receipt(bound, projection)
with pytest.raises(AuthorizationError, match="report engagement"):
finalize_run_report(
{
"engagement_id": "WH-ENG-WRONG",
"target": "audit-core",
"target_revision": bound["target"]["image_digest"],
},
projection=projection,
cleanup=cleanup,
contract=bound,
)

View file

@ -48,3 +48,21 @@ state_hub_task_id: "a6209f8a-b767-5ce8-8842-c9e454977f83"
Add required `--broker-receipt` for WP-0025 admission, pass it through Add required `--broker-receipt` for WP-0025 admission, pass it through
`broker_from_receipt`, and require exact digest equality. CLI tests cover `broker_from_receipt`, and require exact digest equality. CLI tests cover
a missing broker receipt and a mismatched recanonicalized digest. a missing broker receipt and a mismatched recanonicalized digest.
### T02 — Bind runner authorization and final report to cleanup
```task
id: WHITEHAT-WP-0005-T02
status: done
priority: high
```
Fresh-run setup found two remaining operational misbindings. The audit-core
runner hard-coded the authorization id from the original 2026-08-21 attempt,
and its sanitized report could only say credential cleanup was pending.
The runner now requires `--authorization-id` and carries that exact value into
the report. `finalize-report` validates the WP-0025 projection contract,
projection receipt, and cleanup receipt, binds their engagement, target, image,
lease, and canonical receipt id to the runner report, and only then records
custody revocation as complete. Mismatched reports fail closed.