games-development-ai/deploy/tests/test-staging-rc.sh

497 lines
27 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# staging RC 脚本回归:只用假 SSH 与本地假 HTTP 服务,不连接远端、不启动 systemd。
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ACTIVATE="$ROOT_DIR/deploy/activate-staging-rc.sh"
PREPARE="$ROOT_DIR/deploy/prepare-staging-rc.sh"
SMOKE="$ROOT_DIR/deploy/smoke-test.sh"
STORAGE_AUDIT="$ROOT_DIR/deploy/generate-staging-storage-audit.sh"
RESTORE_VERIFY="$ROOT_DIR/deploy/verify-mysql-dump-restore.sh"
GENERATION_GATE_REF="sha256:$(printf '0%.0s' $(seq 1 64)):r1/$(printf '0%.0s' $(seq 1 64)).attestation"
TMP_DIR="$(mktemp -d)"
SERVER_PID=""
cleanup() {
if [ -n "$SERVER_PID" ]; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
fail() {
printf 'TEST_FAIL %s\n' "$*" >&2
exit 1
}
# 执行 prepare 内嵌的真实 Surefire XML 校验器,覆盖全跳过拒绝与部分跳过放行边界。
test_surefire_report_guard() {
local surefire_verifier="$TMP_DIR/verify-security-java-reports.py"
local surefire_fixture="$TMP_DIR/surefire-fixture"
local surefire_report_dir="$surefire_fixture/game-cloud/security/target/surefire-reports"
awk '
/^import pathlib,sys,xml.etree.ElementTree as ET$/ { capture=1 }
capture && /^PY$/ { exit }
capture { print }
' "$PREPARE" >"$surefire_verifier"
rg -q 'surefire-reports/TEST-\*\.xml' "$surefire_verifier" || fail "无法提取 prepare 的 Surefire XML 校验器"
mkdir -p "$surefire_report_dir" "$surefire_fixture/evidence"
cat >"$surefire_report_dir/TEST-com.example.AllSkippedSecurityTest.xml" <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="com.example.AllSkippedSecurityTest" tests="1" failures="0" errors="0" skipped="1"/>
XML
if (cd "$surefire_fixture" && python3 "$surefire_verifier" \
"$surefire_fixture/evidence/security-java-tests-summary.tsv" AllSkippedSecurityTest) \
>"$TMP_DIR/surefire-all-skipped.log" 2>&1; then
fail "Surefire 校验器错误放行 tests=1、skipped=1 的全跳过测试类"
fi
rg -q 'executed=0' "$TMP_DIR/surefire-all-skipped.log" || fail "全跳过拒绝原因未记录实际执行数"
# 边界正例:同类存在一个 skipped但仍至少执行一个测试时必须允许并产出摘要。
cat >"$surefire_report_dir/TEST-com.example.AllSkippedSecurityTest.xml" <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="com.example.AllSkippedSecurityTest" tests="2" failures="0" errors="0" skipped="1"/>
XML
(cd "$surefire_fixture" && python3 "$surefire_verifier" \
"$surefire_fixture/evidence/security-java-tests-summary.tsv" AllSkippedSecurityTest)
rg -q $'^AllSkippedSecurityTest\\t2\\t0\\t0\\t1$' \
"$surefire_fixture/evidence/security-java-tests-summary.tsv" || fail "部分跳过但有执行的测试类未生成正确摘要"
}
if [ "${STAGING_RC_TEST_CASE:-}" = "surefire-report-guard" ]; then
test_surefire_report_guard
printf 'TEST_PASS Surefire XML 全跳过拒绝与部分跳过边界通过\n'
exit 0
fi
bash -n "$ACTIVATE" "$PREPARE" "$SMOKE" "$STORAGE_AUDIT" "$RESTORE_VERIFY"
rg -q 'docker exec -i .*MYSQL_CONTAINER' "$STORAGE_AUDIT" || fail "存储审计未把 SQL stdin 接入 MySQL 容器"
# 假 SSH 只捕获参数和 stdin 中的远端脚本;不执行任何远端命令。
cat >"$TMP_DIR/fake-ssh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$@" >"$RC_TEST_TMP/ssh-args.txt"
cat >"$RC_TEST_TMP/remote-script.sh"
SH
chmod +x "$TMP_DIR/fake-ssh"
export RC_TEST_TMP="$TMP_DIR"
RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" \
BOOTSTRAP_ADMIN_TOKEN_FILE=/root/game-staging/credentials/bootstrap-admin.token \
SSH_BIN="$TMP_DIR/fake-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/activate-wrapper.log"
rg -q '^ProxyCommand=none$' "$TMP_DIR/ssh-args.txt" || fail "SSH 未显式禁用代理"
rg -q '^root@100\.64\.0\.7$' "$TMP_DIR/ssh-args.txt" || fail "SSH 默认目标不是 mini-desktop 直连地址"
bash -n "$TMP_DIR/remote-script.sh"
rg -q 'pre-migration-ruoyi-vue-pro\.sql\.gz' "$TMP_DIR/remote-script.sh" || fail "缺迁移前数据库快照"
rg -q 'generate-staging-storage-audit\.sh' "$TMP_DIR/remote-script.sh" || fail "缺实时存储审计"
rg -q 'verify-mysql-dump-restore\.sh' "$TMP_DIR/remote-script.sh" || fail "缺隔离恢复验证"
rg -q 'failure_handler' "$TMP_DIR/remote-script.sh" || fail "缺失败回滚处理"
rg -q 'start_stack.*48080 4173 4174 8300 9501 true' "$TMP_DIR/remote-script.sh" || fail "缺正式五端口启动"
rg -q 'mv -Tf.*active_pointer' "$TMP_DIR/remote-script.sh" || fail "active 指针不是原子替换"
rg -Fq 'spring.cloud.nacos.discovery.enabled=${discovery_enabled}' "$TMP_DIR/remote-script.sh" || \
fail "backend launcher 没有显式控制 Nacos discovery"
rg -q 'start_candidate_probe.*14173 14174 18300 19501' "$TMP_DIR/remote-script.sh" || \
fail "候选非数据面探针未启动"
rg -q 'verify_generation_gate_evidence' "$TMP_DIR/remote-script.sh" || fail "缺生成 R1 证据校验"
! rg -q 'git .*cat-file.*expected_commit' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 仍要求属于被证明 commit存在自引用"
rg -q 'attestation_root' "$TMP_DIR/remote-script.sh" || fail "缺独立 R1 attestation 根目录"
rg -q 'flock -n' "$TMP_DIR/remote-script.sh" || fail "activation 缺单实例部署锁"
rg -q 'candidate_backend=SKIPPED_NO_ISOLATED_DATA_PLANE' "$TMP_DIR/remote-script.sh" || \
fail "候选探活没有明确跳过共享数据面 backend"
rg -q 'run_flyway_migrations' "$TMP_DIR/remote-script.sh" || fail "迁移未拆成独立受控步骤"
rg -q 'restore_database_snapshot' "$TMP_DIR/remote-script.sh" || fail "迁移后失败缺数据库快照补偿"
rg -q 'FIRST_RC_FULL_ROLLBACK=UNAVAILABLE' "$TMP_DIR/remote-script.sh" || fail "首个 RC 未明确禁止宣称完整回滚"
rg -q 'manage-dogfood-reviewers\.py' "$TMP_DIR/remote-script.sh" || fail "激活未调用审核账号轮换工具"
rg -q 'provision-dogfood-with-nacos-transaction\.sh' "$TMP_DIR/remote-script.sh" || \
fail "激活未在 admin 禁用前执行账号预置事务"
rg -q 'ADMIN_TOKEN_FILE="\$bootstrap_admin_token_file"' "$TMP_DIR/remote-script.sh" || \
fail "激活账号预置未使用 bootstrap token 文件"
rg -q 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' \
"$TMP_DIR/remote-script.sh" || fail "激活未核对 40 人账号终验"
rg -q '请先运行受控 new-api import' "$TMP_DIR/remote-script.sh" || \
fail "额度池不足没有给出受控 new-api import 指引"
rg -q -- '--ack ROTATE_DOGFOOD_REVIEWERS' "$TMP_DIR/remote-script.sh" || fail "审核账号轮换 ACK 不精确"
rg -q 'reviewer-runtime-verification\.json' "$TMP_DIR/remote-script.sh" || fail "缺 reviewer/readonly 登录与旧 token 失效运行态证据"
rg -Fq 'if status != 401 or body.get("code") != 401:' "$TMP_DIR/remote-script.sh" || \
fail "bootstrap token 失效验收未严格要求 HTTP 401 且业务 code=401非 401 可能被误判为失效"
rg -q 'rm -f.*bootstrap_admin_token_file' "$TMP_DIR/remote-script.sh" || fail "轮换成功后未删除 bootstrap token 文件"
! rg -q 'admin123' "$ACTIVATE" || fail "激活脚本写入了默认公开密码"
for required_marker in actor_prompt_eval judge_prompt_eval game_content_gold live_prompt full_gate fresh_25 historical_11 production_shadow_20 runtime_eligible; do
rg -q "$required_marker" "$TMP_DIR/remote-script.sh" || fail "生成门未核对 R1 字段:$required_marker"
done
gate_line="$(rg -n 'verify_generation_gate_evidence' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
snapshot_line="$(rg -n '迁移前数据库快照' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$gate_line" -lt "$snapshot_line" ] || fail "生成门校验没有发生在数据库快照/迁移前"
candidate_probe_line="$(rg -n '启动不接共享数据面的候选探针' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
stop_old_line="$(rg -n '候选非数据面探针全绿,停止旧栈' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
restore_verify_line="$(rg -n '在隔离临时 MySQL 容器中验证快照可恢复' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
migration_line="$(rg -n '^run_flyway_migrations$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$candidate_probe_line" -lt "$stop_old_line" ] || fail "候选非数据面探针没有在停止旧栈前完成"
[ "$stop_old_line" -lt "$snapshot_line" ] || fail "迁移快照前旧栈仍可能写入,失败补偿会丢数据"
[ "$snapshot_line" -lt "$restore_verify_line" ] || fail "隔离恢复验证没有使用停写后的最终快照"
[ "$restore_verify_line" -lt "$migration_line" ] || fail "未先完成隔离恢复验证就执行正式迁移"
reviewer_line="$(rg -n 'manage-dogfood-reviewers\.py' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
provision_line="$(rg -n 'provision-dogfood-with-nacos-transaction\.sh' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
provision_pass_line="$(rg -n 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
runtime_verify_line="$(rg -n '狗粮审核账号轮换与运行态验证 PASS' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
stable_smoke_line="$(rg -n 'stable-smoke\.log' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
active_switch_line="$(rg -n '正式栈全绿,原子切换 active 指针' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$provision_line" -lt "$provision_pass_line" ] || fail "账号预置后未核对终验"
[ "$provision_pass_line" -lt "$reviewer_line" ] || fail "admin 禁用发生在账号预置恢复前"
[ "$reviewer_line" -lt "$runtime_verify_line" ] || fail "审核账号轮换后未做运行态验证"
[ "$runtime_verify_line" -lt "$stable_smoke_line" ] || fail "审核账号运行态验证没有位于写 smoke 前"
[ "$stable_smoke_line" -lt "$active_switch_line" ] || fail "active 指针在 smoke 前切换"
token_delete_line="$(rg -n '^delete_bootstrap_token$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
[ "$active_switch_line" -lt "$token_delete_line" ] || fail "bootstrap token 未在 smoke+active 后最后删除"
failure_handler_body="$(sed -n '/^failure_handler()/,/^}/p' "$TMP_DIR/remote-script.sh")"
! printf '%s\n' "$failure_handler_body" | rg -q '^ delete_bootstrap_token' || \
fail "激活失败路径删除了补偿后重试所需的 bootstrap token"
printf '%s\n' "$failure_handler_body" | rg -q 'cleanup_dogfood_provision_runtime' || \
fail "激活失败补偿未定向清理本次限流桶与 OAuth token 缓存"
! printf '%s\n' "$failure_handler_body" | rg -q 'exit 72' || \
fail "运行态 cleanup 失败仍直接 exit 72跳过最终证据落盘"
cleanup_rc_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'rc=72' | cut -d: -f1)"
failure_status_line="$(printf '%s\n' "$failure_handler_body" | rg -n "printf 'failed commit=" | cut -d: -f1)"
failure_chmod_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'chmod -R go-rwx' | cut -d: -f1)"
failure_exit_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'exit "\$rc"' | cut -d: -f1)"
[ "$cleanup_rc_line" -lt "$failure_status_line" ] \
&& [ "$failure_status_line" -lt "$failure_chmod_line" ] \
&& [ "$failure_chmod_line" -lt "$failure_exit_line" ] || \
fail "cleanup rc=72 后未按 RC_STATUS→chmod→统一 exit 顺序完整落证"
cleanup_function_body="$(sed -n '/^cleanup_dogfood_provision_runtime()/,/^}/p' "$TMP_DIR/remote-script.sh")"
printf '%s\n' "$cleanup_function_body" | rg -q -- '--cleanup' || fail "激活补偿未进入定向清理模式"
printf '%s\n' "$cleanup_function_body" | rg -q 'account_provision_cleanup_manifest.*account_provision_run_id' || \
fail "激活补偿未用本次 run-id 约束清理清单"
if rg -q '/root/games-development-ai' "$TMP_DIR/remote-script.sh"; then
fail "激活脚本引用了远端脏旧仓"
fi
# 生成门引用是激活必填项,且必须在 SSH 前拒绝缺失或不安全路径。
: >"$TMP_DIR/ssh-called"
cat >"$TMP_DIR/reject-ssh" <<'SH'
#!/usr/bin/env bash
printf 'called\n' >>"$RC_TEST_TMP/ssh-called"
exit 99
SH
chmod +x "$TMP_DIR/reject-ssh"
if RC_COMMIT="0000000000000000000000000000000000000000" SSH_BIN="$TMP_DIR/reject-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/missing-generation-gate.log" 2>&1; then
fail "缺生成门引用仍允许激活"
fi
[ ! -s "$TMP_DIR/ssh-called" ] || fail "缺生成门引用后仍发生 SSH"
if RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="sha256:$(printf '1%.0s' $(seq 1 64)):../gate.txt" \
SSH_BIN="$TMP_DIR/reject-ssh" bash "$ACTIVATE" >"$TMP_DIR/unsafe-generation-gate.log" 2>&1; then
fail "不安全生成门相对路径仍允许激活"
fi
[ ! -s "$TMP_DIR/ssh-called" ] || fail "非法生成门路径后仍发生 SSH"
# 真激活缺 bootstrap token 文件路径时必须在 SSH 前 fail-closed不能回退仓库默认密码。
if RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" SSH_BIN="$TMP_DIR/reject-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/missing-bootstrap-token.log" 2>&1; then
fail "缺 BOOTSTRAP_ADMIN_TOKEN_FILE 仍允许激活"
fi
[ ! -s "$TMP_DIR/ssh-called" ] || fail "缺 bootstrap token 路径后仍发生 SSH"
# 旧变量仅允许受控路径和 hash任意 URL 必须在 SSH 前失败。
if RC_COMMIT="0000000000000000000000000000000000000000" \
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" \
OBJECT_SNAPSHOT_REF="fake://object-snapshot/rc-test" SSH_BIN="$TMP_DIR/fake-ssh" \
bash "$ACTIVATE" >"$TMP_DIR/invalid-snapshot.log" 2>&1; then
fail "任意 OBJECT_SNAPSHOT_REF 仍允许激活"
fi
# prepare 必须在 ready 前运行发布工具回归与安全关键 Java 测试,并把日志哈希绑定 RC commit。
for test_name in test-staging-rc.sh test-provision-dogfood-users.sh \
test-provision-dogfood-nacos-transaction.sh test-import-newapi-pool.sh \
test-dogfood-smoke.sh; do
rg -q "$test_name" "$PREPARE" || fail "prepare 未运行发布工具回归:$test_name"
done
rg -q 'release-tool-tests\.log' "$PREPARE" || fail "prepare 未保存发布工具测试日志"
rg -q 'security-java-tests\.log' "$PREPARE" || fail "prepare 未保存安全关键 Java 测试日志"
rg -q 'DogfoodStartupValidatorTest' "$PREPARE" || fail "prepare 未运行 Dogfood 启动安全测试"
rg -q 'DogfoodSecurityStartupValidatorTest' "$PREPARE" || fail "prepare 未运行 Dogfood 配置安全测试"
rg -q 'TokenAuthenticationFilterTest' "$PREPARE" || fail "prepare 未运行令牌认证过滤器测试"
rg -q 'AuthenticationEntryPointImplTest' "$PREPARE" || fail "prepare 未运行未认证响应测试"
rg -q 'RuntimePackageServiceImplTest' "$PREPARE" || fail "prepare 未运行 artifact CAS 测试"
rg -q 'RuntimePackageApiImplDogfoodTest' "$PREPARE" || fail "prepare 未运行 runtime RPC 安全测试"
rg -q 'AdminNewapiQuotaControllerTest' "$PREPARE" || fail "prepare 未运行配额修复入口测试"
rg -q 'NewapiQuotaClaimConsumerTest' "$PREPARE" || fail "prepare 未运行配额 MQ 消费测试"
rg -q 'NewapiQuotaServiceImplTest' "$PREPARE" || fail "prepare 未运行配额服务测试"
rg -q 'test-dogfood-reviewer-security\.py' "$PREPARE" || fail "prepare 未运行审核账号安全测试"
rg -q 'test_dogfood_ops\.py' "$PREPARE" || fail "prepare 未运行波次证据账本测试"
rg -q 'backend_security_tests=PASS' "$PREPARE" || fail "prepare 未把安全 Java 测试结果绑定 RC"
rg -q 'security_java_tests_sha256=' "$PREPARE" || fail "prepare 摘要未绑定安全测试日志哈希"
rg -q 'verify_security_java_reports' "$PREPARE" || fail "prepare 未校验安全测试报告实际包含全部目标类"
rg -q 'security-java-tests-summary\.tsv' "$PREPARE" || fail "prepare 未保存可审计的 Java 测试计数摘要"
rg -q 'security_java_tests_summary_sha256=' "$PREPARE" || fail "prepare 摘要未绑定 Java 测试计数证据"
test_surefire_report_guard
rg -q 'FRONTEND_SECRET_SCAN_PASS' "$PREPARE" || fail "prepare 未扫描前端制品中的默认凭据"
rg -q 'frontend_secret_scan_sha256=' "$PREPARE" || fail "prepare 摘要未绑定前端凭据扫描证据"
rg -Fq "frontend_forbidden_patterns='admin123|test1|mock-token|mock-studio-token|bootstrap-admin\\.token|DOGFOOD_PASSWORD_SEED'" \
"$PREPARE" || fail "prepare 前端扫描模式未完整覆盖 test1、mock-studio-token 与既有模式"
rg -q "printf 'patterns=%s" "$PREPARE" || fail "prepare 扫描证据未记录实际模式集"
rg -q 'frontend_secret_scan_patterns_sha256=' "$PREPARE" || fail "prepare 摘要未绑定扫描模式集证据"
frontend_forbidden_patterns="$(sed -n "s/^frontend_forbidden_patterns='\\(.*\\)'$/\\1/p" "$PREPARE")"
[ -n "$frontend_forbidden_patterns" ] || fail "无法从 prepare 解析前端禁止模式集"
for forbidden_sample in admin123 test1 mock-token mock-studio-token bootstrap-admin.token DOGFOOD_PASSWORD_SEED; do
printf '%s\n' "$forbidden_sample" | rg -q -i "$frontend_forbidden_patterns" || \
fail "前端扫描正则未命中禁止样本:$forbidden_sample"
done
if printf '%s\n' 'production-studio-token' | rg -q -i "$frontend_forbidden_patterns"; then
fail "前端扫描正则误伤安全样本"
fi
! rg -q 'backend_tests=NOT_RUN' "$PREPARE" || fail "prepare 仍宣称后端测试未运行"
venv_line="$(rg -n '准备 commit 内独立 Python 虚拟环境' "$PREPARE" | cut -d: -f1)"
tool_test_line="$(rg -n '运行发布工具回归测试' "$PREPARE" | cut -d: -f1)"
[ "$venv_line" -lt "$tool_test_line" ] || fail "发布工具回归未使用 commit 内已建 venv"
rg -q 'cheap-worker/\.venv/bin.*PATH' "$PREPARE" || fail "发布工具回归未显式使用 commit 内 Python"
rg -q '原子切换 active' "$PREPARE" || fail "prepare 完成提示仍未使用 active 指针"
# 独立 attestation 不属于被证明 commit内容寻址文件应通过宽权限、symlink、commit 漂移必须拒绝。
cat >"$TMP_DIR/exec-ssh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
while [ "$#" -gt 0 ] && [ "$1" != bash ]; do shift; done
[ "${1:-}" = bash ] || exit 97
exec "$@"
SH
chmod +x "$TMP_DIR/exec-ssh"
attestation_root="$(realpath "$TMP_DIR")/attestations"
mkdir -p "$attestation_root/r1"
attestation_file="$attestation_root/r1/r1.attestation"
cat >"$attestation_file" <<EOF
schema=generation-r1-gate/1
rc_commit=0000000000000000000000000000000000000000
r1_status=PASS
actor_prompt_eval=PASS
judge_prompt_eval=PASS
game_content_gold=PASS
live_prompt=PASS
full_gate=PASS
fresh_25=PASS
historical_11=PASS
production_shadow_20=PASS
runtime_eligible=true
EOF
chmod 600 "$attestation_file"
attestation_hash="$(sha256sum "$attestation_file" | awk '{print $1}')"
mv "$attestation_file" "$attestation_root/r1/$attestation_hash.attestation"
attestation_file="$attestation_root/r1/$attestation_hash.attestation"
attestation_ref="sha256:$attestation_hash:r1/$attestation_hash.attestation"
RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >"$TMP_DIR/attestation-pass.log"
rg -q 'GENERATION_ATTESTATION_PASS' "$TMP_DIR/attestation-pass.log" || fail "独立 attestation 未通过"
chmod 644 "$attestation_file"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "权限过宽 attestation 未拒绝"
fi
chmod 600 "$attestation_file"
ln -s "$attestation_file" "$attestation_root/r1/symlink.attestation"
if RC_COMMIT=0000000000000000000000000000000000000000 \
GENERATION_GATE_EVIDENCE_REF="sha256:$attestation_hash:r1/symlink.attestation" \
GENERATION_ATTESTATION_ROOT="$attestation_root" SSH_BIN="$TMP_DIR/exec-ssh" \
bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "symlink attestation 未拒绝"
fi
if RC_COMMIT=1111111111111111111111111111111111111111 \
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
fail "rc_commit 漂移 attestation 未拒绝"
fi
# 直接验证 DB-only 审计:生成、外部引用 fail-closed、旧 hash/路径校验。
FAKE_RC="$TMP_DIR/fake-rc"
mkdir -p "$FAKE_RC/evidence"
git -C "$FAKE_RC" init -q
git -C "$FAKE_RC" config user.email test@example.invalid
git -C "$FAKE_RC" config user.name staging-test
printf 'rc\n' >"$FAKE_RC/README"
git -C "$FAKE_RC" add README
git -C "$FAKE_RC" commit -qm init
cat >"$TMP_DIR/fake-audit-mysql" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
cat >/dev/null
cat <<'OUT'
game_version_rows=53
game_version_package_url_nonempty=0
game_runtime_package_rows=54
game_runtime_package_url_nonempty=0
game_runtime_package_payload_rows=48
game_source_project_rows=41
game_source_project_url_nonempty=0
game_source_project_payload_rows=41
game_aigc_task_rows=78
game_aigc_result_package_url_nonempty=0
infra_file_rows=0
OUT
SH
chmod +x "$TMP_DIR/fake-audit-mysql"
audit_output="$(RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
bash "$STORAGE_AUDIT")"
audit_ref="$(printf '%s\n' "$audit_output" | sed -n 's/^STORAGE_AUDIT_PASS ref=//p')"
[[ "$audit_ref" =~ ^sha256:[0-9a-f]{64}:evidence/storage-audit\.tsv$ ]] || fail "审计引用格式非法"
rg -q '^object_storage_status=DB_ONLY_NA$' "$FAKE_RC/evidence/storage-audit.tsv" || fail "缺 DB_ONLY_NA"
rg -q '^minio_involved_prefix_count=0$' "$FAKE_RC/evidence/storage-audit.tsv" || fail "缺 MinIO 前缀事实"
RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
EXPECTED_STORAGE_AUDIT_REF="$audit_ref" bash "$STORAGE_AUDIT" >/dev/null
bad_ref="sha256:$(printf '0%.0s' $(seq 1 64)):evidence/storage-audit.tsv"
if RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
EXPECTED_STORAGE_AUDIT_REF="$bad_ref" bash "$STORAGE_AUDIT" >/dev/null 2>&1; then
fail "错误 storage audit hash 未被拒绝"
fi
cat >"$TMP_DIR/fake-audit-external" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
cat >/dev/null
sed 's/game_runtime_package_url_nonempty=0/game_runtime_package_url_nonempty=1/' "$RC_TEST_TMP/audit-values"
SH
cp "$TMP_DIR/fake-audit-mysql" "$TMP_DIR/audit-values-script"
"$TMP_DIR/fake-audit-mysql" </dev/null >"$TMP_DIR/audit-values"
chmod +x "$TMP_DIR/fake-audit-external"
if RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-external" \
bash "$STORAGE_AUDIT" >/dev/null 2>&1; then
fail "发现外部 URL 时仍签发 DB_ONLY_NA"
fi
# 隔离恢复验证使用假 Docker 覆盖 run/ready/import/query/stop不触碰真实容器。
printf 'CREATE DATABASE test;\n' | gzip -9 >"$TMP_DIR/dump.sql.gz"
cat >"$TMP_DIR/fake-docker" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"$RC_TEST_TMP/docker-calls.log"
case "$1" in
run) printf 'fake-container-id\n' ;;
stop) ;;
exec)
if printf '%s\n' "$*" | grep -q 'SELECT 1'; then
exit 0
fi
if printf '%s\n' "$*" | grep -q -- '-N -B'; then
cat >/dev/null
cat <<'OUT'
flyway_failed_rows=0
game_version_rows=53
game_version_package_url_nonempty=0
game_runtime_package_rows=54
game_runtime_package_url_nonempty=0
game_runtime_package_payload_rows=48
game_source_project_rows=41
game_source_project_url_nonempty=0
game_source_project_payload_rows=41
game_aigc_task_rows=78
game_aigc_result_package_url_nonempty=0
infra_file_rows=0
OUT
else
cat >/dev/null
fi
;;
*) exit 2 ;;
esac
SH
chmod +x "$TMP_DIR/fake-docker"
DUMP_FILE="$TMP_DIR/dump.sql.gz" EXPECTED_AUDIT_FILE="$FAKE_RC/evidence/storage-audit.tsv" \
DOCKER_BIN="$TMP_DIR/fake-docker" RESTORE_VERIFY_TIMEOUT_SEC=10 \
bash "$RESTORE_VERIFY" >"$TMP_DIR/restore.log"
rg -q '^RESTORE_VERIFY_PASS ' "$TMP_DIR/restore.log" || fail "假容器恢复未通过"
rg -q '^run .*--network none ' "$TMP_DIR/docker-calls.log" || fail "恢复容器未隔离网络"
rg -q '^stop ' "$TMP_DIR/docker-calls.log" || fail "恢复容器未销毁"
DUMP_FILE="$TMP_DIR/dump.sql.gz" EXPECTED_AUDIT_FILE="$FAKE_RC/evidence/storage-audit.tsv" \
RESTORE_VERIFY_DRY_RUN=1 bash "$RESTORE_VERIFY" >"$TMP_DIR/restore-dry.log"
rg -q '^RESTORE_VERIFY_DRY_RUN_PASS' "$TMP_DIR/restore-dry.log" || fail "恢复 dry-run 输入校验失败"
# 本地假服务验证 readiness smoke 全程只有 GET。
cat >"$TMP_DIR/fake-readiness.py" <<'PY'
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from socketserver import TCPServer
port_file = os.environ["RC_TEST_PORT_FILE"]
method_file = os.environ["RC_TEST_METHOD_FILE"]
class Handler(BaseHTTPRequestHandler):
def log_message(self, _format, *_args):
return
def send_json(self, payload):
body = json.dumps(payload).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def record(self):
with open(method_file, "a", encoding="utf-8") as handle:
handle.write(self.command + " " + self.path + "\n")
def do_GET(self):
self.record()
if self.path == "/actuator/health":
self.send_json({"status": "UP"})
elif self.path.startswith("/app-api/feed/zones"):
self.send_json({"code": 0, "data": [{"id": 2}]})
elif self.path.startswith("/app-api/feed/stream"):
self.send_json({"code": 0, "data": {"list": [{"gameId": 1, "versionId": 2}], "hasMore": False}})
elif self.path.startswith("/app-api/runtime/package/2"):
self.send_json({"code": 0, "data": {"versionId": 2}})
else:
self.send_json({"code": 404, "data": None})
def do_POST(self):
self.record()
self.send_json({"code": 500})
class LocalThreadingHTTPServer(ThreadingHTTPServer):
def server_bind(self):
# 跳过 HTTPServer.server_bind 的反向 DNS 查询,离线构建机不能依赖主机名解析。
TCPServer.server_bind(self)
self.server_name, self.server_port = self.server_address[:2]
server = LocalThreadingHTTPServer(("127.0.0.1", 0), Handler)
with open(port_file, "w", encoding="utf-8") as handle:
handle.write(str(server.server_port))
server.serve_forever()
PY
export RC_TEST_PORT_FILE="$TMP_DIR/port"
export RC_TEST_METHOD_FILE="$TMP_DIR/methods"
python3 "$TMP_DIR/fake-readiness.py" &
SERVER_PID=$!
for _ in $(seq 1 300); do
[ -s "$TMP_DIR/port" ] && break
sleep 0.1
done
[ -s "$TMP_DIR/port" ] || fail "假 readiness 服务未启动"
port="$(<"$TMP_DIR/port")"
BASE="http://127.0.0.1:$port" STUDIO_BASE="http://127.0.0.1:4173" \
bash "$SMOKE" >"$TMP_DIR/smoke.log"
if rg -n -v '^GET ' "$TMP_DIR/methods"; then
fail "默认 smoke 出现写请求"
fi
printf 'TEST_PASS staging RC 存储审计、隔离恢复与只读 smoke 均通过\n'