Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
257 lines
9 KiB
Python
Executable file
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())
|