Files
ai-contract-platform/yuqei-infra/scripts/run-p1-07-6-isolated-uat.py
T

669 lines
35 KiB
Python

#!/usr/bin/env python3
"""Preflight and execute the P1-07.6 two-region evidence-trust UAT.
The runner intentionally refuses placeholders, mutable images, shared volumes,
one-person approvals, and execution without a real evidence package. It never
persists bearer tokens in the generated report.
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.request
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
IMAGE_REFERENCE_RE = re.compile(r"^[^\s@]+@sha256:[0-9a-f]{64}$")
UAT_SERVICE_IMAGE_ENV = {
"postgres": "POSTGRES_IMAGE",
"postgres-init-databases": "POSTGRES_CLIENT_IMAGE",
"redis": "REDIS_IMAGE",
"ai-redis": "REDIS_IMAGE",
"minio": "MINIO_IMAGE",
"contract-api-migrate": "CONTRACT_API_IMAGE",
"contract-api": "CONTRACT_API_IMAGE",
"contract-review-worker": "CONTRACT_API_IMAGE",
"contract-playbook-governance-worker": "CONTRACT_API_IMAGE",
"ai-platform-api-migrate": "AI_PLATFORM_API_IMAGE",
"ai-platform-prompt-assets-import": "AI_PLATFORM_API_IMAGE",
"ai-platform-api": "AI_PLATFORM_API_IMAGE",
"worker": "AI_WORKER_IMAGE",
"contract-web": "CONTRACT_WEB_IMAGE",
"kms-gateway-aliyun": "KMS_GATEWAY_IMAGE",
"kms-gateway-tencent": "KMS_GATEWAY_IMAGE",
"evidence-witness-aliyun": "EVIDENCE_WITNESS_GATEWAY_IMAGE",
"evidence-witness-tencent": "EVIDENCE_WITNESS_GATEWAY_IMAGE",
}
POLICY_UPDATE_FIELDS = (
"timezone",
"business_calendar_enabled",
"business_days",
"workday_start_minute",
"workday_end_minute",
"holiday_dates",
"first_response_minutes",
"scan_enabled",
"scan_interval_minutes",
"scan_lookback_days",
"external_notification_enabled",
"notification_channel_id",
"scan_slo_target_rate",
"notification_slo_target_rate",
"slo_alerts_enabled",
"slo_warning_burn_rate",
"slo_critical_burn_rate",
"slo_minimum_sample_size",
"slo_fast_window_minutes",
"slo_mid_window_minutes",
"slo_fast_burn_rate",
"slo_slow_burn_rate",
"draft_evidence_valid_days",
"draft_evidence_reminder_days",
"draft_evidence_reminders_enabled",
"audit_verification_enabled",
"audit_verification_per_minute",
"audit_verification_per_day",
"audit_verification_per_export_per_day",
"audit_invalid_attempt_threshold",
"audit_lockout_minutes",
"evidence_probe_enabled",
"evidence_probe_interval_minutes",
"evidence_probe_slo_target_rate",
"transparency_anchor_enabled",
"transparency_anchor_interval_minutes",
"transparency_minimum_witnesses",
)
REQUIRED_VALUES = (
"COMPOSE_PROJECT_NAME",
"POSTGRES_VOLUME_NAME",
"MINIO_VOLUME_NAME",
"CONTRACT_FILES_VOLUME_NAME",
"MINIO_ROOT_USER",
"MINIO_ROOT_PASSWORD",
"POSTGRES_IMAGE",
"POSTGRES_CLIENT_IMAGE",
"REDIS_IMAGE",
"MINIO_IMAGE",
"CONTRACT_API_IMAGE",
"CONTRACT_WEB_IMAGE",
"AI_PLATFORM_API_IMAGE",
"AI_WORKER_IMAGE",
"POSTGRES_BIND_ADDRESS",
"REDIS_BIND_ADDRESS",
"MINIO_API_BIND_ADDRESS",
"MINIO_CONSOLE_BIND_ADDRESS",
"CONTRACT_API_BIND_ADDRESS",
"AI_PLATFORM_API_BIND_ADDRESS",
"CONTRACT_WEB_BIND_ADDRESS",
"KMS_GATEWAY_IMAGE",
"EVIDENCE_WITNESS_GATEWAY_IMAGE",
"UAT_ALIYUN_KMS_REGION",
"UAT_ALIYUN_KMS_ENDPOINT",
"UAT_ALIYUN_ENCRYPTION_KEY_ID",
"UAT_ALIYUN_EVIDENCE_KEY_ID",
"UAT_ALIYUN_EVIDENCE_KEY_VERSION",
"UAT_ALIYUN_WITNESS_KEY_ID",
"UAT_ALIYUN_WITNESS_KEY_VERSION",
"UAT_ALIYUN_WITNESS_BUCKET",
"UAT_ALIYUN_WITNESS_REGION",
"UAT_ALIYUN_WITNESS_ENDPOINT",
"UAT_TENCENT_KMS_REGION",
"UAT_TENCENT_ENCRYPTION_KEY_ID",
"UAT_TENCENT_EVIDENCE_KEY_ID",
"UAT_TENCENT_EVIDENCE_KEY_VERSION",
"UAT_TENCENT_WITNESS_KEY_ID",
"UAT_TENCENT_WITNESS_KEY_VERSION",
"UAT_TENCENT_WITNESS_BUCKET",
"UAT_TENCENT_WITNESS_REGION",
"UAT_TENCENT_WITNESS_ENDPOINT",
)
SECRET_FILE_KEYS = (
"UAT_ALIYUN_KMS_TOKEN_FILE",
"UAT_ALIYUN_KMS_CLIENT_KEY_FILE",
"UAT_ALIYUN_KMS_CLIENT_KEY_PASSWORD_FILE",
"UAT_ALIYUN_KMS_PROVIDER_CA_FILE",
"UAT_ALIYUN_GATEWAY_TLS_CERT_FILE",
"UAT_ALIYUN_GATEWAY_TLS_KEY_FILE",
"UAT_ALIYUN_GATEWAY_CA_FILE",
"UAT_ALIYUN_OSS_CREDENTIAL_FILE",
"UAT_ALIYUN_WITNESS_TOKEN_FILE",
"UAT_ALIYUN_WITNESS_TLS_CERT_FILE",
"UAT_ALIYUN_WITNESS_TLS_KEY_FILE",
"UAT_ALIYUN_WITNESS_CA_FILE",
"UAT_ALIYUN_WITNESS_PUBLIC_KEY_FILE",
"UAT_TENCENT_KMS_TOKEN_FILE",
"UAT_TENCENT_KMS_CREDENTIAL_FILE",
"UAT_TENCENT_GATEWAY_TLS_CERT_FILE",
"UAT_TENCENT_GATEWAY_TLS_KEY_FILE",
"UAT_TENCENT_GATEWAY_CA_FILE",
"UAT_TENCENT_COS_CREDENTIAL_FILE",
"UAT_TENCENT_WITNESS_TOKEN_FILE",
"UAT_TENCENT_WITNESS_TLS_CERT_FILE",
"UAT_TENCENT_WITNESS_TLS_KEY_FILE",
"UAT_TENCENT_WITNESS_CA_FILE",
"UAT_TENCENT_WITNESS_PUBLIC_KEY_FILE",
)
class UatFailure(RuntimeError):
pass
def now_iso() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def load_env(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
for line_number, raw in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1):
line = raw.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line[7:].strip()
if "=" not in line:
raise UatFailure(f"Invalid env entry at line {line_number}.")
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
value = value[1:-1]
values[key] = value
return values
def check(checks: list[dict[str, str]], name: str, passed: bool, detail: str) -> None:
checks.append({"name": name, "status": "passed" if passed else "failed", "detail": detail})
def preflight(env: dict[str, str], *, admin_a: str, admin_b: str, static_only: bool) -> list[dict[str, str]]:
checks: list[dict[str, str]] = []
missing = [key for key in REQUIRED_VALUES if not env.get(key, "").strip()]
check(checks, "required_configuration", not missing, "all required values are present" if not missing else "missing: " + ", ".join(missing))
placeholders = sorted(key for key, value in env.items() if "CHANGE_ME" in value or "example.com" in value)
check(checks, "no_placeholders", not placeholders, "no placeholders found" if not placeholders else "placeholder keys: " + ", ".join(placeholders))
image_keys = (
"POSTGRES_IMAGE",
"POSTGRES_CLIENT_IMAGE",
"REDIS_IMAGE",
"MINIO_IMAGE",
"CONTRACT_API_IMAGE",
"CONTRACT_WEB_IMAGE",
"AI_PLATFORM_API_IMAGE",
"AI_WORKER_IMAGE",
"KMS_GATEWAY_IMAGE",
"EVIDENCE_WITNESS_GATEWAY_IMAGE",
)
invalid_images = [key for key in image_keys if not IMAGE_REFERENCE_RE.fullmatch(env.get(key, ""))]
check(checks, "immutable_images", not invalid_images, "all UAT images use immutable SHA-256 digests" if not invalid_images else "mutable or invalid image keys: " + ", ".join(invalid_images))
project = env.get("COMPOSE_PROJECT_NAME", "").lower()
isolated_project = "uat" in project and not any(marker in project for marker in ("prod", "production"))
check(checks, "isolated_project", isolated_project, "compose project is explicitly UAT-scoped" if isolated_project else "COMPOSE_PROJECT_NAME must contain uat and must not contain prod")
volume_keys = ("POSTGRES_VOLUME_NAME", "MINIO_VOLUME_NAME", "CONTRACT_FILES_VOLUME_NAME")
bad_volumes = [key for key in volume_keys if "uat" not in env.get(key, "").lower()]
object_storage_volume = env.get("MINIO_VOLUME_NAME", "").lower()
if (
"MINIO_VOLUME_NAME" not in bad_volumes
and ("rustfs" not in object_storage_volume or "minio" in object_storage_volume)
):
bad_volumes.append("MINIO_VOLUME_NAME")
check(checks, "isolated_volumes", not bad_volumes, "all persistent volumes are UAT-scoped" if not bad_volumes else "non-UAT volume keys: " + ", ".join(bad_volumes))
default_ports = {"5432", "6379", "9000", "9001", "8100", "8101", "3000"}
configured_ports = {env.get(key, "") for key in (
"POSTGRES_HOST_PORT", "REDIS_HOST_PORT", "MINIO_API_HOST_PORT", "MINIO_CONSOLE_HOST_PORT",
"CONTRACT_API_HOST_PORT", "AI_PLATFORM_API_HOST_PORT", "CONTRACT_WEB_HOST_PORT",
)}
port_isolation = "" not in configured_ports and configured_ports.isdisjoint(default_ports) and len(configured_ports) == 7
check(checks, "isolated_ports", port_isolation, "all seven host ports are distinct from development defaults" if port_isolation else "host ports must be present, unique, and distinct from default ports")
bind_keys = (
"POSTGRES_BIND_ADDRESS", "REDIS_BIND_ADDRESS", "MINIO_API_BIND_ADDRESS",
"MINIO_CONSOLE_BIND_ADDRESS", "CONTRACT_API_BIND_ADDRESS",
"AI_PLATFORM_API_BIND_ADDRESS", "CONTRACT_WEB_BIND_ADDRESS",
)
loopback_only = all(env.get(key) in {"127.0.0.1", "::1", "localhost"} for key in bind_keys)
check(checks, "loopback_bindings", loopback_only, "all host bindings are loopback-only" if loopback_only else "all UAT host bindings must use loopback addresses")
key_sets = {
"aliyun": [env.get("UAT_ALIYUN_ENCRYPTION_KEY_ID"), env.get("UAT_ALIYUN_EVIDENCE_KEY_ID"), env.get("UAT_ALIYUN_WITNESS_KEY_ID")],
"tencent": [env.get("UAT_TENCENT_ENCRYPTION_KEY_ID"), env.get("UAT_TENCENT_EVIDENCE_KEY_ID"), env.get("UAT_TENCENT_WITNESS_KEY_ID")],
}
key_isolation = all(None not in values and "" not in values and len(set(values)) == 3 for values in key_sets.values())
key_isolation = key_isolation and env.get("UAT_ALIYUN_WITNESS_KEY_ID") != env.get("UAT_TENCENT_WITNESS_KEY_ID")
check(checks, "separate_kms_keys", key_isolation, "encryption, evidence, and witness keys are separated per provider" if key_isolation else "each provider requires three distinct keys and witness keys must differ across providers")
independent_targets = (
env.get("UAT_ALIYUN_KMS_REGION") != env.get("UAT_TENCENT_KMS_REGION")
and env.get("UAT_ALIYUN_WITNESS_BUCKET") != env.get("UAT_TENCENT_WITNESS_BUCKET")
and env.get("UAT_ALIYUN_WITNESS_ENDPOINT", "").startswith("https://")
and env.get("UAT_TENCENT_WITNESS_ENDPOINT", "").startswith("https://")
)
check(checks, "independent_regions_and_witnesses", independent_targets, "regions, buckets, and HTTPS witness endpoints are independent" if independent_targets else "two distinct regions, buckets, and HTTPS endpoints are required")
same_admin = not admin_a.strip() or not admin_b.strip() or admin_a.strip() == admin_b.strip()
check(checks, "two_platform_administrators", not same_admin, "administrator identities are distinct" if not same_admin else "admin A and admin B must be non-empty and different")
missing_files: list[str] = []
empty_files: list[str] = []
file_paths: list[str] = []
for key in SECRET_FILE_KEYS:
raw = env.get(key, "").strip()
if not raw:
missing_files.append(key)
continue
path = Path(raw).expanduser()
file_paths.append(str(path.resolve(strict=False)).lower())
if not path.is_file():
missing_files.append(key)
elif path.stat().st_size == 0:
empty_files.append(key)
duplicate_files = len(file_paths) != len(set(file_paths))
file_ok = not missing_files and not empty_files and not duplicate_files
detail = "all secret, CA, certificate, and public-key files are separate and non-empty"
if not file_ok:
parts = []
if missing_files:
parts.append("missing: " + ", ".join(missing_files))
if empty_files:
parts.append("empty: " + ", ".join(empty_files))
if duplicate_files:
parts.append("at least two file settings resolve to the same path")
detail = "; ".join(parts)
check(checks, "file_backed_secrets_and_trust", file_ok, detail)
docker_ready = static_only or shutil.which("docker") is not None
check(checks, "docker_runtime", docker_ready, "runtime check skipped" if static_only else ("docker executable is available" if docker_ready else "docker executable is not installed or not on PATH"))
return checks
class Api:
def __init__(self, base_url: str, tenant_id: str) -> None:
self.base_url = base_url.rstrip("/")
self.tenant_id = tenant_id
def request(
self,
method: str,
path: str,
*,
admin: str | None = None,
payload: dict[str, Any] | None = None,
bearer: str | None = None,
expected: tuple[int, ...] = (200,),
timeout: int = 90,
) -> tuple[int, Any]:
headers = {"Accept": "application/json", "User-Agent": "yuqei-p1-07-6-uat/1.0"}
if admin:
headers.update({"X-Tenant-Id": self.tenant_id, "X-User-Id": admin})
if bearer:
headers["Authorization"] = f"Bearer {bearer}"
data = None
if payload is not None:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json"
request = urllib.request.Request(f"{self.base_url}{path}", data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
status = response.status
raw = response.read()
except urllib.error.HTTPError as error:
status = error.code
raw = error.read()
except (urllib.error.URLError, TimeoutError) as error:
raise UatFailure(f"API request failed: {method} {path}: {error}") from error
try:
body = json.loads(raw) if raw else None
except json.JSONDecodeError:
body = {"unparseable_response": True}
if status not in expected:
code = None
if isinstance(body, dict):
detail = body.get("detail")
code = detail.get("code") if isinstance(detail, dict) else None
raise UatFailure(f"Unexpected API status {status} for {method} {path}" + (f" ({code})" if code else ""))
return status, body
def run_command(command: list[str], *, cwd: Path, timeout: int = 600) -> str:
completed = subprocess.run(command, cwd=cwd, text=True, capture_output=True, timeout=timeout, check=False)
if completed.returncode != 0:
message = (completed.stderr or completed.stdout or "command failed").strip().splitlines()[-1]
raise UatFailure(f"Command failed ({completed.returncode}): {message}")
return completed.stdout
def validate_rendered_compose(raw_json: str, env: dict[str, str]) -> None:
try:
payload = json.loads(raw_json)
except json.JSONDecodeError as exc:
raise UatFailure("Compose did not emit valid JSON.") from exc
services = payload.get("services") if isinstance(payload, dict) else None
volumes = payload.get("volumes") if isinstance(payload, dict) else None
if not isinstance(services, dict) or not isinstance(volumes, dict):
raise UatFailure("Rendered Compose model is incomplete.")
for service_name, env_key in UAT_SERVICE_IMAGE_ENV.items():
service = services.get(service_name)
if not isinstance(service, dict):
raise UatFailure(f"Rendered Compose service is missing: {service_name}.")
expected = env.get(env_key, "")
if not IMAGE_REFERENCE_RE.fullmatch(expected) or service.get("image") != expected:
raise UatFailure(
f"Rendered Compose service {service_name} is not bound to {env_key}."
)
if service.get("build") is not None:
raise UatFailure(
f"Rendered Compose service {service_name} retains a build definition."
)
storage = services["minio"]
mounts = storage.get("volumes")
valid_mount = (
isinstance(mounts, list)
and len(mounts) == 1
and isinstance(mounts[0], dict)
and mounts[0].get("type") == "volume"
and mounts[0].get("source") == "rustfs-data"
and mounts[0].get("target") == "/data"
)
volume = volumes.get("rustfs-data")
valid_volume = (
isinstance(volume, dict)
and volume.get("name") == env.get("MINIO_VOLUME_NAME")
and "rustfs" in str(volume.get("name", "")).lower()
and "minio" not in str(volume.get("name", "")).lower()
)
if not valid_mount or not valid_volume:
raise UatFailure("Rendered Compose does not use the isolated RustFS volume contract.")
environment = storage.get("environment")
if (
not isinstance(environment, dict)
or environment.get("RUSTFS_ACCESS_KEY") != env.get("MINIO_ROOT_USER")
or environment.get("RUSTFS_SECRET_KEY") != env.get("MINIO_ROOT_PASSWORD")
or environment.get("RUSTFS_VOLUMES") != "/data"
):
raise UatFailure("Rendered Compose RustFS credentials or data path are not bound.")
command = storage.get("command")
command_text = "\n".join(command) if isinstance(command, list) else str(command or "")
healthcheck = storage.get("healthcheck")
health_test = healthcheck.get("test") if isinstance(healthcheck, dict) else None
if (
storage.get("entrypoint") != ["/bin/sh", "-ec"]
or "set -eu" not in command_text
or "/data/.minio.sys" not in command_text
or ".yuqei-rustfs-volume-v1" not in command_text
or "exec /entrypoint.sh rustfs" not in command_text
or not isinstance(health_test, list)
or not any(
isinstance(token, str) and "/health/ready" in token
for token in health_test
)
or storage.get("user") != "10001:10001"
or storage.get("read_only") is not True
or "ALL" not in storage.get("cap_drop", [])
or "no-new-privileges:true" not in storage.get("security_opt", [])
):
raise UatFailure("Rendered Compose RustFS startup or hardening contract is invalid.")
def require(condition: bool, message: str) -> None:
if not condition:
raise UatFailure(message)
def package_payload(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text(encoding="utf-8"))
if isinstance(value, dict) and isinstance(value.get("package"), dict):
return value["package"]
if not isinstance(value, dict):
raise UatFailure("Evidence package must be a JSON object.")
return value
def summarize_probes(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [{
"target_name": item.get("target_name"),
"region": item.get("region"),
"status": item.get("status"),
"signature_valid": item.get("signature_valid"),
"public_key_fingerprint": item.get("public_key_fingerprint"),
"error_code": item.get("error_code"),
} for item in items]
def summarize_anchors(batch: dict[str, Any]) -> dict[str, Any]:
return {
"configured_targets": batch.get("configured_targets"),
"required_witnesses": batch.get("required_witnesses"),
"verified_witnesses": batch.get("verified_witnesses"),
"quorum_met": batch.get("quorum_met"),
"anchors": [{
"target_name": item.get("target_name"),
"region": item.get("region"),
"status": item.get("status"),
"witness_status": item.get("witness_status"),
"witness_key_fingerprint": item.get("witness_key_fingerprint"),
"error_code": item.get("error_code"),
"external_reference": item.get("external_reference"),
} for item in batch.get("anchors", [])],
}
def execute_uat(args: argparse.Namespace, env: dict[str, str], report: dict[str, Any]) -> None:
infra_root = Path(__file__).resolve().parents[1]
base_compose = infra_root / "compose" / "docker-compose.yml"
overlay_compose = infra_root / "compose" / "docker-compose.uat.evidence-trust.yml"
compose = [
"docker", "compose", "--env-file", str(args.env_file.resolve()),
"-f", str(base_compose), "-f", str(overlay_compose),
]
steps: list[dict[str, Any]] = report["steps"]
rendered = run_command(compose + ["config", "--format", "json"], cwd=infra_root)
validate_rendered_compose(rendered, env)
steps.append({"name": "compose_config", "status": "passed", "at": now_iso()})
run_command(compose + ["up", "-d", "--wait", "--wait-timeout", str(args.startup_timeout)], cwd=infra_root, timeout=args.startup_timeout + 120)
steps.append({"name": "isolated_stack_started", "status": "passed", "at": now_iso()})
api_base = args.api_base or f"http://127.0.0.1:{env['CONTRACT_API_HOST_PORT']}"
api = Api(api_base, args.tenant_id)
deadline = time.monotonic() + args.startup_timeout
while True:
try:
api.request("GET", "/health", expected=(200,), timeout=5)
break
except UatFailure:
if time.monotonic() >= deadline:
raise UatFailure("Contract API did not become healthy before the UAT timeout.")
time.sleep(3)
prefix = "/api/v1/admin/pilot-operations"
policies: dict[str, dict[str, Any]] = {}
for label, admin in (("admin_a", args.admin_a), ("admin_b", args.admin_b)):
_status, policy = api.request("GET", f"{prefix}/policy", admin=admin)
require(isinstance(policy, dict), f"{label} did not receive a policy response.")
policies[label] = policy
require(policies["admin_a"].get("owner_tenant_id") == policies["admin_b"].get("owner_tenant_id"), "The two administrators are not operating the same platform tenant.")
steps.append({"name": "two_platform_admins_verified", "status": "passed", "at": now_iso(), "evidence": {"admin_a": args.admin_a, "admin_b": args.admin_b, "tenant_id": args.tenant_id}})
missing_policy = [field for field in POLICY_UPDATE_FIELDS if field not in policies["admin_a"]]
require(not missing_policy, "Policy response is missing update fields: " + ", ".join(missing_policy))
policy_update = {field: policies["admin_a"][field] for field in POLICY_UPDATE_FIELDS}
policy_update.update({
"evidence_probe_enabled": True,
"transparency_anchor_enabled": True,
"transparency_minimum_witnesses": 2,
"slo_alerts_enabled": True,
"slo_minimum_sample_size": 1,
"evidence_probe_slo_target_rate": 0.99,
})
_status, updated_policy = api.request("PUT", f"{prefix}/policy", admin=args.admin_a, payload=policy_update)
require(updated_policy.get("transparency_minimum_witnesses") == 2, "Two-witness quorum policy was not applied.")
steps.append({"name": "two_witness_policy_enabled", "status": "passed", "at": now_iso(), "evidence": {"policy_version": updated_policy.get("version"), "minimum_witnesses": 2, "slo_minimum_sample_size": 1}})
_status, probes = api.request("POST", f"{prefix}/evidence-probes/regions/run", admin=args.admin_a)
require(isinstance(probes, list) and len(probes) == 2, "Baseline must return exactly two regional probes.")
require(all(item.get("status") == "succeeded" and item.get("signature_valid") is True for item in probes), "Both baseline regional KMS probes must succeed.")
require(len({item.get("region") for item in probes}) == 2 and len({item.get("public_key_fingerprint") for item in probes}) == 2, "Regional probes must have distinct regions and key fingerprints.")
steps.append({"name": "two_region_kms_baseline", "status": "passed", "at": now_iso(), "evidence": summarize_probes(probes)})
_status, anchors = api.request("POST", f"{prefix}/evidence-transparency/anchors/run", admin=args.admin_a)
require(anchors.get("configured_targets") == 2 and anchors.get("verified_witnesses") == 2 and anchors.get("quorum_met") is True, "Both immutable witnesses must verify and meet quorum.")
steps.append({"name": "two_immutable_witness_baseline", "status": "passed", "at": now_iso(), "evidence": summarize_anchors(anchors)})
evidence = package_payload(args.evidence_package)
changes_path = f"{prefix}/evidence-audit/change-requests"
issue_payload = {
"action": "issue",
"name": f"P1-07.6 isolated UAT {datetime.now(UTC).strftime('%Y%m%d%H%M%S')}",
"scopes": ["evidence:verify", "trust-bundle:read", "transparency:read"],
"per_minute_quota": 60,
"per_day_quota": 1000,
"per_export_per_day_quota": 100,
"reason": "P1-07.6 isolated dual-control UAT credential issuance",
}
_status, issue = api.request("POST", changes_path, admin=args.admin_a, payload=issue_payload)
issue_id = issue.get("id")
require(isinstance(issue_id, str), "Credential issue request ID is missing.")
same_status, same_body = api.request("POST", f"{changes_path}/{issue_id}/review", admin=args.admin_a, payload={"decision": "approve", "note": "self approval must fail"}, expected=(409,))
same_code = same_body.get("detail", {}).get("code") if isinstance(same_body, dict) and isinstance(same_body.get("detail"), dict) else None
require(same_status == 409 and same_code == "audit_client_change_separation_of_duties", "Self-approval was not rejected by separation-of-duties control.")
_status, issued = api.request("POST", f"{changes_path}/{issue_id}/review", admin=args.admin_b, payload={"decision": "approve", "note": "second administrator approved UAT issuance"})
token_v1 = issued.get("bearer_token")
client = issued.get("client") or {}
client_id = client.get("id")
require(isinstance(token_v1, str) and isinstance(client_id, str) and client.get("credential_version") == 1, "Issue approval did not return a version-1 credential.")
api.request("POST", "/api/v1/public/reliable-decision/verify", payload={"package": evidence}, bearer=token_v1)
rotate_payload = {"action": "rotate", "audit_client_id": client_id, "reason": "P1-07.6 isolated UAT credential rotation"}
_status, rotate = api.request("POST", changes_path, admin=args.admin_a, payload=rotate_payload)
_status, rotated = api.request("POST", f"{changes_path}/{rotate['id']}/review", admin=args.admin_b, payload={"decision": "approve", "note": "second administrator approved UAT rotation"})
token_v2 = rotated.get("bearer_token")
require(isinstance(token_v2, str) and rotated.get("client", {}).get("credential_version") == 2, "Rotation approval did not return a version-2 credential.")
api.request("POST", "/api/v1/public/reliable-decision/verify", payload={"package": evidence}, bearer=token_v1, expected=(401,))
api.request("POST", "/api/v1/public/reliable-decision/verify", payload={"package": evidence}, bearer=token_v2)
revoke_payload = {"action": "revoke", "audit_client_id": client_id, "reason": "P1-07.6 isolated UAT credential revocation"}
_status, revoke = api.request("POST", changes_path, admin=args.admin_a, payload=revoke_payload)
_status, revoked = api.request("POST", f"{changes_path}/{revoke['id']}/review", admin=args.admin_b, payload={"decision": "approve", "note": "second administrator approved UAT revocation"})
require(revoked.get("client", {}).get("status") == "revoked", "Credential was not revoked.")
api.request("POST", "/api/v1/public/reliable-decision/verify", payload={"package": evidence}, bearer=token_v2, expected=(401,))
steps.append({"name": "credential_dual_control_lifecycle", "status": "passed", "at": now_iso(), "evidence": {"issue_request_id": issue_id, "rotate_request_id": rotate.get("id"), "revoke_request_id": revoke.get("id"), "audit_client_id": client_id, "versions_tested": [1, 2], "self_approval_rejected": True, "old_and_revoked_tokens_rejected": True}})
outage_started = False
try:
run_command(compose + ["stop", "kms-gateway-tencent"], cwd=infra_root)
outage_started = True
steps.append({"name": "tencent_kms_outage_injected", "status": "passed", "at": now_iso(), "evidence": {"service": "kms-gateway-tencent"}})
_status, failed_probes = api.request("POST", f"{prefix}/evidence-probes/regions/run", admin=args.admin_a)
require(isinstance(failed_probes, list) and len(failed_probes) == 2, "Outage probe run did not return two targets.")
failed = [item for item in failed_probes if item.get("status") == "failed"]
succeeded = [item for item in failed_probes if item.get("status") == "succeeded"]
require(len(failed) == 1 and len(succeeded) == 1 and "tencent" in str(failed[0].get("target_name", "")).lower(), "Outage must isolate Tencent while Alibaba remains healthy.")
steps.append({"name": "regional_failure_isolation", "status": "passed", "at": now_iso(), "evidence": summarize_probes(failed_probes)})
_status, failed_anchors = api.request("POST", f"{prefix}/evidence-transparency/anchors/run", admin=args.admin_a)
require(failed_anchors.get("verified_witnesses") == 1 and failed_anchors.get("quorum_met") is False, "Witness outage must fail the two-witness quorum.")
steps.append({"name": "witness_quorum_failure_detected", "status": "passed", "at": now_iso(), "evidence": summarize_anchors(failed_anchors)})
api.request("POST", f"{prefix}/run-scheduled-scan", admin=args.admin_a)
_status, insights = api.request("GET", f"{prefix}/insights?period_days=30", admin=args.admin_a)
alerts = [item for item in insights.get("slo_alerts", []) if item.get("metric_key") == "evidence_verification_probe" and item.get("status") == "open"]
require(alerts and alerts[0].get("handoff_case_id"), "Probe failure did not create an open SLO alert with a handoff case.")
alert = alerts[0]
case_id = alert["handoff_case_id"]
steps.append({"name": "slo_alert_and_handoff_created", "status": "passed", "at": now_iso(), "evidence": {"alert_id": alert.get("id"), "severity": alert.get("severity"), "burn_rate": alert.get("burn_rate"), "handoff_case_id": case_id}})
finally:
if outage_started:
run_command(compose + ["up", "-d", "--wait", "--wait-timeout", str(args.startup_timeout), "kms-gateway-tencent", "evidence-witness-tencent"], cwd=infra_root, timeout=args.startup_timeout + 120)
_status, recovered_probes = api.request("POST", f"{prefix}/evidence-probes/regions/run", admin=args.admin_b)
require(all(item.get("status") == "succeeded" and item.get("signature_valid") is True for item in recovered_probes), "Both KMS probes must recover after service restoration.")
_status, recovered_anchors = api.request("POST", f"{prefix}/evidence-transparency/anchors/run", admin=args.admin_b)
require(recovered_anchors.get("verified_witnesses") == 2 and recovered_anchors.get("quorum_met") is True, "Both witnesses must recover and restore quorum.")
steps.append({"name": "kms_and_witness_recovered", "status": "passed", "at": now_iso(), "evidence": {"probes": summarize_probes(recovered_probes), "anchors": summarize_anchors(recovered_anchors)}})
api.request("POST", f"{prefix}/cases/{case_id}/actions", admin=args.admin_a, payload={"action": "acknowledge", "note": "UAT alert acknowledged by first administrator"})
api.request("POST", f"{prefix}/cases/{case_id}/actions", admin=args.admin_b, payload={"action": "assign", "assignee": args.admin_b, "note": "UAT recovery assigned to second administrator"})
_status, resolved_case = api.request("POST", f"{prefix}/cases/{case_id}/actions", admin=args.admin_b, payload={"action": "resolve", "note": "Regional KMS and immutable witness probes recovered; manual takeover closed."})
require(resolved_case.get("status") == "resolved" and resolved_case.get("assigned_to") == args.admin_b, "Manual handoff case was not resolved by the second administrator.")
_status, events = api.request("GET", f"{prefix}/cases/{case_id}/events", admin=args.admin_a)
actions = [item.get("action") for item in events.get("items", [])]
require(all(action in actions for action in ("acknowledge", "assign", "resolve")), "Manual handoff audit trail is incomplete.")
steps.append({"name": "manual_takeover_closed_loop", "status": "passed", "at": now_iso(), "evidence": {"case_id": case_id, "status": "resolved", "assigned_to": args.admin_b, "actions": actions}})
def write_report(path: Path, report: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the isolated P1-07.6 evidence trust UAT")
parser.add_argument("--env-file", type=Path, required=True)
parser.add_argument("--admin-a", required=True)
parser.add_argument("--admin-b", required=True)
parser.add_argument("--tenant-id", default="default")
parser.add_argument("--api-base")
parser.add_argument("--evidence-package", type=Path)
parser.add_argument("--execute", action="store_true", help="start the isolated stack and run the destructive outage drill")
parser.add_argument("--static-only", action="store_true", help="skip checking for a local Docker executable during preflight")
parser.add_argument("--startup-timeout", type=int, default=300)
parser.add_argument("--report", type=Path, default=Path("p1-07-6-isolated-uat-report.json"))
args = parser.parse_args()
report: dict[str, Any] = {
"format_version": "yuqei-p1-07-6-isolated-uat/v1",
"started_at": now_iso(),
"mode": "execute" if args.execute else "preflight",
"environment_file": str(args.env_file.resolve(strict=False)),
"tenant_id": args.tenant_id,
"administrators": {"admin_a": args.admin_a, "admin_b": args.admin_b},
"preflight": [],
"steps": [],
"status": "running",
}
try:
require(args.env_file.is_file(), "The UAT env file does not exist.")
env = load_env(args.env_file)
report["preflight"] = preflight(env, admin_a=args.admin_a, admin_b=args.admin_b, static_only=args.static_only)
failed = [item for item in report["preflight"] if item["status"] != "passed"]
require(not failed, "UAT preflight failed: " + ", ".join(item["name"] for item in failed))
if args.execute:
require(not args.static_only, "--static-only cannot be combined with --execute.")
require(args.evidence_package is not None and args.evidence_package.is_file(), "--execute requires a real --evidence-package JSON file.")
execute_uat(args, env, report)
report["status"] = "passed"
return_code = 0
except (UatFailure, OSError, ValueError, json.JSONDecodeError, subprocess.TimeoutExpired) as error:
report["status"] = "failed"
report["failure"] = {"message": str(error), "at": now_iso()}
return_code = 1
finally:
report["completed_at"] = now_iso()
write_report(args.report, report)
print(json.dumps({"status": report["status"], "report": str(args.report.resolve()), "failed_checks": [item["name"] for item in report["preflight"] if item["status"] == "failed"]}, ensure_ascii=False))
return return_code
if __name__ == "__main__":
sys.exit(main())