Prepare canonical custody for audit E2 third attempt
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02991-be07-7bb3-8b6d-e9701b5621de
This commit is contained in:
codex 2026-08-22 23:23:41 +02:00
parent fc198b896f
commit 2a5c002aa5
5 changed files with 123 additions and 0 deletions

View file

@ -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 |

View file

@ -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
}
]
}

View file

@ -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:

View file

@ -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()

View file

@ -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.