railiance-platform/scripts/wp0025-broker-readiness.py
codex 30e6edc236
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Add versioned ephemeral custody lifecycle
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
2026-08-22 21:56:42 +02:00

257 lines
9 KiB
Python
Executable file

#!/usr/bin/env python3
"""Direct Whitehat owner interface for WP-0025 broker readiness."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Callable
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from custody_contract import ( # noqa: E402
BROKER_INTERFACE,
BROKER_OWNER,
PROJECTION_INTERFACE,
WORKPLAN_ID,
ContractError,
broker_subject,
canonical_json,
contract_digest,
current_broker_receipt,
file_digest,
http_json,
interface_artifacts,
load_json,
validate_broker_receipt,
validate_projection_contract,
)
DEFAULT_ADAPTER = Path("src/whitehat_security/platform_custody.py")
DEFAULT_TEST = Path("tests/test_platform_custody_adapter.py")
class ReadinessError(RuntimeError):
pass
def validate_reviewer(value: str) -> str:
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:@/+\-]{1,127}", value):
raise ReadinessError("reviewer must be a stable 2-128 character identifier")
return value
def validate_note(value: str) -> str:
note = value.strip()
if not note or len(note) > 2000:
raise ReadinessError("request-changes requires a note of 1-2000 characters")
forbidden = ("BEGIN PRIVATE KEY", "BEGIN OPENSSH PRIVATE KEY", "AGE-SECRET-KEY-1", "hvs.")
if any(marker in note for marker in forbidden):
raise ReadinessError("note appears to contain credential material")
return note
def safe_child(root: Path, relative: Path) -> Path:
if relative.is_absolute():
raise ReadinessError("adapter and test paths must be repository-relative")
target = (root / relative).resolve()
try:
target.relative_to(root.resolve())
except ValueError as exc:
raise ReadinessError("consumer artifact escapes repository root") from exc
return target
def verify_adapter(
consumer_root: Path,
*,
adapter_path: Path = DEFAULT_ADAPTER,
test_path: Path = DEFAULT_TEST,
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
) -> dict[str, Any]:
root = consumer_root.resolve()
adapter = safe_child(root, adapter_path)
test = safe_child(root, test_path)
if not adapter.is_file() or not test.is_file():
return {
"passed": False,
"adapter_present": adapter.is_file(),
"test_present": test.is_file(),
"secret_values_observed": False,
}
revision_result = runner(
["git", "rev-parse", "HEAD"],
cwd=root,
text=True,
capture_output=True,
check=False,
)
revision = revision_result.stdout.strip() if revision_result.returncode == 0 else ""
tests = runner(
[sys.executable, "-m", "pytest", "-q", str(test_path)],
cwd=root,
text=True,
capture_output=True,
check=False,
)
passed = bool(re.fullmatch(r"[0-9a-f]{40}", revision)) and tests.returncode == 0
return {
"passed": passed,
"adapter_present": True,
"test_present": True,
"adapter": {
"repo": "whitehat-security",
"revision": revision,
"path": str(adapter_path),
"sha256": file_digest(adapter),
"tests_passed": tests.returncode == 0,
},
"test_exit_code": tests.returncode,
"secret_values_observed": False,
}
def receipt_base(contract: dict[str, Any], reviewer: str, decision: str) -> dict[str, Any]:
return {
"interface": BROKER_INTERFACE,
"version": 1,
"workplan_id": WORKPLAN_ID,
"owner": BROKER_OWNER,
"reviewer": validate_reviewer(reviewer),
"decision": decision,
"created_at": datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
"engagement_id": contract["engagement_id"],
"target_id": contract["target"]["id"],
"projection_contract_digest": contract_digest(contract),
"projection_receipt_interface": PROJECTION_INTERFACE,
"interface_artifacts": interface_artifacts(),
"required_roles": ["attacker", "owner"],
"mount_paths": sorted(item["mount_path"] for item in contract["identities"]),
"secret_values_observed": False,
}
def build_approval(
contract: dict[str, Any], reviewer: str, verification: dict[str, Any]
) -> dict[str, Any]:
if verification.get("passed") is not True or verification.get("secret_values_observed") is not False:
raise ReadinessError("consumer adapter verification did not pass")
receipt = {
**receipt_base(contract, reviewer, "approve"),
"adapter": verification["adapter"],
"cleanup_request_supported": True,
}
validate_broker_receipt(receipt, contract)
return receipt
def build_change_request(contract: dict[str, Any], reviewer: str, note: str) -> dict[str, Any]:
return {
**receipt_base(contract, reviewer, "request-changes"),
"note": validate_note(note),
}
def post_receipt(receipt: dict[str, Any], api_base: str) -> dict[str, Any]:
response = http_json(
"POST",
f"{api_base.rstrip('/')}/messages/",
{
"from_agent": BROKER_OWNER,
"to_agent": "railiance-platform",
"subject": broker_subject(receipt),
"body": canonical_json(receipt),
},
)
if not isinstance(response, dict) or not response.get("id"):
raise ReadinessError("State Hub returned an invalid message receipt")
return response
def show(contract: dict[str, Any]) -> dict[str, Any]:
return {
"interface": BROKER_INTERFACE,
"version": 1,
"owner": BROKER_OWNER,
"engagement_id": contract["engagement_id"],
"target_id": contract["target"]["id"],
"projection_contract_digest": contract_digest(contract),
"projection_receipt_interface": PROJECTION_INTERFACE,
"required_roles": ["attacker", "owner"],
"mount_paths": sorted(item["mount_path"] for item in contract["identities"]),
"adapter_path": str(DEFAULT_ADAPTER),
"test_path": str(DEFAULT_TEST),
"approve_command": (
"python3 scripts/wp0025-broker-readiness.py approve --contract <contract.json> "
"--consumer-root <whitehat-security> --reviewer <stable-id>"
),
"live_mutation_authorized": False,
"secret_values_observed": False,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=["show", "verify", "approve", "request-changes", "status"])
parser.add_argument("--contract", type=Path, required=True)
parser.add_argument("--consumer-root", type=Path)
parser.add_argument("--reviewer")
parser.add_argument("--note")
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))
if args.command == "show":
result = show(contract)
elif args.command == "status":
try:
receipt = current_broker_receipt(contract, args.state_hub)
except ContractError as exc:
result = {
"ready": False,
"engagement_id": contract["engagement_id"],
"reason": str(exc),
"secret_values_observed": False,
}
else:
result = {
"ready": True,
"engagement_id": contract["engagement_id"],
"receipt": receipt,
"secret_values_observed": False,
}
elif args.command == "verify":
if not args.consumer_root:
raise ReadinessError("verify requires --consumer-root")
result = verify_adapter(args.consumer_root)
elif args.command == "approve":
if not args.consumer_root or not args.reviewer:
raise ReadinessError("approve requires --consumer-root and --reviewer")
verification = verify_adapter(args.consumer_root)
receipt = build_approval(contract, args.reviewer, verification)
posted = post_receipt(receipt, args.state_hub)
result = {"submitted": True, "message_id": posted["id"], "receipt": receipt}
else:
if not args.reviewer or not args.note:
raise ReadinessError("request-changes requires --reviewer and --note")
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}
print(json.dumps(result, indent=2, sort_keys=True))
return 0
except (ContractError, OSError, ReadinessError) as exc:
print(f"broker readiness failed: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())