Add versioned ephemeral custody lifecycle
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
This commit is contained in:
parent
985cef2572
commit
30e6edc236
17 changed files with 2855 additions and 6 deletions
257
scripts/wp0025-broker-readiness.py
Executable file
257
scripts/wp0025-broker-readiness.py
Executable file
|
|
@ -0,0 +1,257 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue