From 2a5c002aa515fb200fc8b876b69c7fe75b6e4011 Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 22 Aug 2026 23:23:41 +0200 Subject: [PATCH] Prepare canonical custody for audit E2 third attempt Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02991-be07-7bb3-8b6d-e9701b5621de --- WORK-RECORDS.md | 1 + ...20260822-AUDIT-E2-03-custody-contract.json | 59 +++++++++++++++++++ scripts/wp0025-broker-readiness.py | 20 +++++++ tests/test_wp0025_broker_readiness.py | 25 ++++++++ ...5-versioned-ephemeral-custody-lifecycle.md | 18 ++++++ 5 files changed, 123 insertions(+) create mode 100644 docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 5da992f..e31767e 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -107,6 +107,7 @@ | task | RAILIANCE-WP-0025-T03 | done | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md | | task | RAILIANCE-WP-0025-T04 | done | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md | | task | RAILIANCE-WP-0025-T05 | done | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md | +| task | RAILIANCE-WP-0025-T06 | done | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md | | task | RPF-WP-0018-T01 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | | task | RPF-WP-0018-T02 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | | task | RPF-WP-0018-T03 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | diff --git a/docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json b/docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json new file mode 100644 index 0000000..400199c --- /dev/null +++ b/docs/WH-ENG-20260822-AUDIT-E2-03-custody-contract.json @@ -0,0 +1,59 @@ +{ + "interface": "railiance.custody-projection-contract", + "version": 1, + "workplan_id": "RAILIANCE-WP-0025", + "engagement_id": "WH-ENG-20260822-AUDIT-E2-03", + "status": "proposed", + "engagement_contract_sha256": "3e70e4581ddcfc8dea35ec21e72d3e2cdd7699ee9e9932d8eee97b64b648d5cd", + "target": { + "id": "audit-core", + "namespace": "audit-core", + "deployment": "audit-core", + "container": "audit-core", + "sender_external_secret": "audit-core-senders", + "revision": "abd22fa0a64faca4c77752bb500c892d4f075ac5", + "image_digest": "sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6", + "contract_sha256": "5bf926e9da394da28e1d2cb11ead33d6d27a20b40914d8097b1041b35a18696e" + }, + "runner": { + "namespace": "whitehat", + "service_account": "whitehat-runner", + "secret_name": "whitehat-e2-audit-credentials-03", + "mount_root": "/var/run/secrets/whitehat", + "manifest_sha256": "41201af08df484659f1eb89e19acb8042fc44cd3eeac1369784e8ce9e2166c4a" + }, + "window": { + "starts_at": "2026-08-22T22:00:00Z", + "projection_cutoff": "2026-08-22T22:03:00Z", + "expires_at": "2026-08-22T22:15:00Z" + }, + "authority": { + "remote": "railiance01", + "registry_path": "platform/workloads/audit-core/senders", + "registry_field": "senders.json", + "kv_mount": "platform", + "kv_prefix": "engagements/WH-ENG-20260822-AUDIT-E2-03/audit-core", + "eso_service_account": "external-secrets", + "eso_namespace": "external-secrets" + }, + "identities": [ + { + "handle": "token-a", + "role": "attacker", + "sender_name": "whitehat-e2-a-20260822-03", + "tenant": "tenant:trial:whitehat-a-20260822-03", + "mount_path": "/var/run/secrets/whitehat/token-a", + "may_read": true, + "may_write": true + }, + { + "handle": "token-b", + "role": "owner", + "sender_name": "whitehat-e2-b-20260822-03", + "tenant": "tenant:trial:whitehat-b-20260822-03", + "mount_path": "/var/run/secrets/whitehat/token-b", + "may_read": true, + "may_write": true + } + ] +} diff --git a/scripts/wp0025-broker-readiness.py b/scripts/wp0025-broker-readiness.py index 184b6ad..8ae1e17 100755 --- a/scripts/wp0025-broker-readiness.py +++ b/scripts/wp0025-broker-readiness.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import json +import os import re import subprocess import sys @@ -177,6 +178,17 @@ def post_receipt(receipt: dict[str, Any], api_base: str) -> dict[str, Any]: return response +def write_receipt(path: Path, receipt: dict[str, Any]) -> None: + """Persist only the canonical broker receipt, never the CLI wrapper.""" + path.parent.mkdir(parents=True, exist_ok=True) + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + try: + with os.fdopen(descriptor, "w", encoding="utf-8") as handle: + handle.write(json.dumps(receipt, indent=2, sort_keys=True) + "\n") + finally: + os.chmod(path, 0o600) + + def show(contract: dict[str, Any]) -> dict[str, Any]: return { "interface": BROKER_INTERFACE, @@ -206,10 +218,12 @@ def main() -> int: parser.add_argument("--consumer-root", type=Path) parser.add_argument("--reviewer") parser.add_argument("--note") + parser.add_argument("--receipt-out", type=Path) parser.add_argument("--state-hub", default="http://127.0.0.1:8000") args = parser.parse_args() try: contract = validate_projection_contract(load_json(args.contract)) + receipt_out = None if args.command == "show": result = show(contract) elif args.command == "status": @@ -223,6 +237,7 @@ def main() -> int: "secret_values_observed": False, } else: + receipt_out = receipt result = { "ready": True, "engagement_id": contract["engagement_id"], @@ -239,6 +254,7 @@ def main() -> int: verification = verify_adapter(args.consumer_root) receipt = build_approval(contract, args.reviewer, verification) posted = post_receipt(receipt, args.state_hub) + receipt_out = receipt result = {"submitted": True, "message_id": posted["id"], "receipt": receipt} else: if not args.reviewer or not args.note: @@ -246,6 +262,10 @@ def main() -> int: receipt = build_change_request(contract, args.reviewer, args.note) posted = post_receipt(receipt, args.state_hub) result = {"submitted": True, "message_id": posted["id"], "receipt": receipt} + if args.receipt_out: + if receipt_out is None: + raise ReadinessError("--receipt-out requires approve or a ready status") + write_receipt(args.receipt_out, receipt_out) print(json.dumps(result, indent=2, sort_keys=True)) return 0 except (ContractError, OSError, ReadinessError) as exc: diff --git a/tests/test_wp0025_broker_readiness.py b/tests/test_wp0025_broker_readiness.py index 60b9e35..18eeeae 100644 --- a/tests/test_wp0025_broker_readiness.py +++ b/tests/test_wp0025_broker_readiness.py @@ -1,7 +1,9 @@ from __future__ import annotations import importlib.util +import json import subprocess +import stat import sys import tempfile import unittest @@ -84,6 +86,29 @@ class BrokerReadinessTests(unittest.TestCase): with self.assertRaises(module.ReadinessError): module.build_change_request(contract, "whitehat-owner", "") + def test_receipt_export_is_raw_canonical_document_with_private_mode(self) -> None: + contract = projection_contract() + verification = { + "passed": True, + "adapter": { + "repo": "whitehat-security", + "revision": "a" * 40, + "path": str(module.DEFAULT_ADAPTER), + "sha256": "b" * 64, + "tests_passed": True, + }, + "secret_values_observed": False, + } + receipt = module.build_approval(contract, "whitehat-owner", verification) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "nested" / "broker.json" + module.write_receipt(path, receipt) + observed = json.loads(path.read_text(encoding="utf-8")) + mode = stat.S_IMODE(path.stat().st_mode) + self.assertEqual(receipt, observed) + self.assertNotIn("submitted", observed) + self.assertEqual(0o600, mode) + if __name__ == "__main__": unittest.main() diff --git a/workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md b/workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md index 9289201..a2115fd 100644 --- a/workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md +++ b/workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md @@ -162,3 +162,21 @@ was deployed; those remain a separate owner decision. - [x] Remote command arguments survive shell-hostile templates unchanged. - [x] Every modeled partial mutation cleans only its receipt-bound scope. - [x] Expired cleanup is executable but no unattended scheduler is deployed. + +## T06 — Export the broker receipt consumed by admission + +```task +id: RAILIANCE-WP-0025-T06 +status: done +priority: high +``` + +The first fresh-run setup review found that `approve` posted the canonical +broker-readiness receipt to State Hub but exposed no safe way to persist the +raw receipt document required by Whitehat's `--broker-receipt` input. Redirecting +the command output would save a CLI wrapper rather than the canonical document. + +`approve` and a ready `status` now accept `--receipt-out`, create parent +directories, write only the canonical value-safe receipt, and force mode +`0600`. Other commands fail closed when that option is supplied. Regression +coverage proves the wrapper is excluded and the file mode is private.