From 96d6781036f214e4e8dac57822a66f31db0ca8f2 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 22 Aug 2026 23:23:48 +0200 Subject: [PATCH] Prepare receipt-bound audit E2 third attempt Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02991-be07-7bb3-8b6d-e9701b5621de --- WORK-RECORDS.md | 1 + engagements/2026-08-22-audit-core-e2-03.json | 69 +++++++++++ engagements/2026-08-22-audit-core-e2-03.md | 13 ++ engagements/receipts/README.md | 16 ++- .../WH-ENG-20260822-AUDIT-E2-03-invocation.md | 113 ++++++++++++++++++ .../whitehat-e2-audit-20260822-03.yaml | 51 ++++++++ runners/audit_core_e2.py | 54 +++++---- src/whitehat_security/cli.py | 29 ++++- src/whitehat_security/platform_custody.py | 26 ++++ tests/test_audit_core_runner.py | 41 +++++++ tests/test_platform_custody_adapter.py | 36 ++++++ ...ITEHAT-WP-0005-broker-receipt-admission.md | 18 +++ 12 files changed, 440 insertions(+), 27 deletions(-) create mode 100644 engagements/2026-08-22-audit-core-e2-03.json create mode 100644 engagements/2026-08-22-audit-core-e2-03.md create mode 100644 engagements/runtime/WH-ENG-20260822-AUDIT-E2-03-invocation.md create mode 100644 engagements/runtime/whitehat-e2-audit-20260822-03.yaml diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 69cd0e4..d81d812 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -27,3 +27,4 @@ | 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-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 | diff --git a/engagements/2026-08-22-audit-core-e2-03.json b/engagements/2026-08-22-audit-core-e2-03.json new file mode 100644 index 0000000..bc9fde5 --- /dev/null +++ b/engagements/2026-08-22-audit-core-e2-03.json @@ -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" +} diff --git a/engagements/2026-08-22-audit-core-e2-03.md b/engagements/2026-08-22-audit-core-e2-03.md new file mode 100644 index 0000000..9d6e6af --- /dev/null +++ b/engagements/2026-08-22-audit-core-e2-03.md @@ -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. diff --git a/engagements/receipts/README.md b/engagements/receipts/README.md index 3879c39..826db2b 100644 --- a/engagements/receipts/README.md +++ b/engagements/receipts/README.md @@ -1,16 +1,20 @@ # Projection receipts -Copy `example-projection-receipt.json` at window start and fill in the live -engagement id, `projected_at`, `expires_at`, and identity *names* from the -custody procedure. Never put token values, passwords, or registry payloads -here. +Legacy value-safe receipts remain supported for historical fixtures. New live +engagements use the canonical RAILIANCE-WP-0025 contract, broker-readiness, +projection, and cleanup receipts. Never hand-edit a live receipt or put token +values, passwords, or registry payloads here. Live admission: ```sh PYTHONPATH=src python3 -m whitehat_security.cli admit-plane \ engagements/.json targets/audit-core-e2.json \ - --receipt engagements/receipts/.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. diff --git a/engagements/runtime/WH-ENG-20260822-AUDIT-E2-03-invocation.md b/engagements/runtime/WH-ENG-20260822-AUDIT-E2-03-invocation.md new file mode 100644 index 0000000..b43f5ab --- /dev/null +++ b/engagements/runtime/WH-ENG-20260822-AUDIT-E2-03-invocation.md @@ -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 +``` diff --git a/engagements/runtime/whitehat-e2-audit-20260822-03.yaml b/engagements/runtime/whitehat-e2-audit-20260822-03.yaml new file mode 100644 index 0000000..892e81f --- /dev/null +++ b/engagements/runtime/whitehat-e2-audit-20260822-03.yaml @@ -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 diff --git a/runners/audit_core_e2.py b/runners/audit_core_e2.py index 1890e10..275f311 100644 --- a/runners/audit_core_e2.py +++ b/runners/audit_core_e2.py @@ -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: parser = argparse.ArgumentParser() parser.add_argument("--base-url", 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("--token-a-file", required=True) parser.add_argument("--token-b-file", required=True) @@ -278,26 +304,14 @@ def main() -> None: except Abort as error: outcome = "aborted" limitations.append(str(error)) - report = { - "schema_version": "whitehat-run/v1", - "run_id": f"{args.engagement_id}-{started}", - "evidence_class": "target", - "engagement_id": args.engagement_id, - "authorization_id": "operator-session-2026-08-21-e2-approval", - "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": client.requests, - "cleanup": "named immutable audit fixtures retained by target contract", - "credential_revocation": "pending orchestrator cleanup", - "probes": results, - "limitations": limitations, - "assurance_statement": ASSURANCE, - } + report = build_report( + args, + started=started, + outcome=outcome, + attempted_operations=client.requests, + results=results, + limitations=limitations, + ) print(json.dumps(report, sort_keys=True)) raise SystemExit(0 if outcome == "pass" else 1) diff --git a/src/whitehat_security/cli.py b/src/whitehat_security/cli.py index 87563f1..84fcb83 100644 --- a/src/whitehat_security/cli.py +++ b/src/whitehat_security/cli.py @@ -14,7 +14,7 @@ from .engagement import AuthorizationError, Engagement from .fixtures import FixtureService, probe_suite from .model import RunReport, utc_now from .plane import KillSwitch, admit, default_broker, retired_ids -from .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 .targets import load_catalog, load_registration @@ -100,6 +100,12 @@ def main(argv: list[str] | None = None) -> None: "--broker-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") deliver = commands.add_parser("deliver") deliver.add_argument("report") @@ -171,6 +177,27 @@ def main(argv: list[str] | None = None) -> None: raise SystemExit(1) print("clear") 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": try: report = RunReport(**json.loads(Path(args.report).read_text(encoding="utf-8"))) diff --git a/src/whitehat_security/platform_custody.py b/src/whitehat_security/platform_custody.py index 0d9097a..bbf8d0a 100644 --- a/src/whitehat_security/platform_custody.py +++ b/src/whitehat_security/platform_custody.py @@ -331,6 +331,32 @@ def validate_cleanup_receipt( 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( document: dict[str, Any], *, contract: dict[str, Any], now: datetime | None = None ) -> dict[str, Any]: diff --git a/tests/test_audit_core_runner.py b/tests/test_audit_core_runner.py index 81eefc1..8a907fe 100644 --- a/tests/test_audit_core_runner.py +++ b/tests/test_audit_core_runner.py @@ -84,3 +84,44 @@ def test_retry_invocation_fixture_set_is_declared_and_distinct(): assert fixtures <= set(engagement["fixture_ids"]) assert fixtures.isdisjoint(previous["fixture_ids"]) 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"] diff --git a/tests/test_platform_custody_adapter.py b/tests/test_platform_custody_adapter.py index 2adf98a..519aeec 100644 --- a/tests/test_platform_custody_adapter.py +++ b/tests/test_platform_custody_adapter.py @@ -15,6 +15,7 @@ from whitehat_security.platform_custody import ( broker_from_receipt, contract_digest, digest, + finalize_run_report, interface_artifacts, load_schemas, resource_names, @@ -381,3 +382,38 @@ def test_cleanup_receipt_must_match_lease(): bad = cleanup_receipt(bound, projection) bad["lease_id"] = "custody:" + "0" * 32 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, + ) diff --git a/workplans/WHITEHAT-WP-0005-broker-receipt-admission.md b/workplans/WHITEHAT-WP-0005-broker-receipt-admission.md index 77b6638..648719a 100644 --- a/workplans/WHITEHAT-WP-0005-broker-receipt-admission.md +++ b/workplans/WHITEHAT-WP-0005-broker-receipt-admission.md @@ -48,3 +48,21 @@ state_hub_task_id: "a6209f8a-b767-5ce8-8842-c9e454977f83" Add required `--broker-receipt` for WP-0025 admission, pass it through `broker_from_receipt`, and require exact digest equality. CLI tests cover 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.