railiance-platform/scripts/wp0025-broker-readiness.py
codex c9d02147d3
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Adopt canonical flex-auth credential checks
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02e56-e4ad-71a2-b3e2-b6193e0d8093
2026-08-23 14:03:40 +02:00

294 lines
10 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 os
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,
*,
now: datetime | None = None,
) -> dict[str, Any]:
created_at = now or datetime.now(UTC)
return {
"interface": BROKER_INTERFACE,
"version": 1,
"workplan_id": WORKPLAN_ID,
"owner": BROKER_OWNER,
"reviewer": validate_reviewer(reviewer),
"decision": decision,
"created_at": created_at.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],
*,
now: datetime | None = None,
) -> 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", now=now),
"adapter": verification["adapter"],
"cleanup_request_supported": True,
}
validate_broker_receipt(receipt, contract, now=now)
return receipt
def build_change_request(
contract: dict[str, Any],
reviewer: str,
note: str,
*,
now: datetime | None = None,
) -> dict[str, Any]:
return {
**receipt_base(contract, reviewer, "request-changes", now=now),
"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 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,
"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("--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":
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:
receipt_out = receipt
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)
receipt_out = receipt
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}
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:
print(f"broker readiness failed: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())