500 lines
27 KiB
Bash
Executable File
500 lines
27 KiB
Bash
Executable File
#!/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 摘要未绑定扫描模式集证据"
|
||
rg -q 'runtime-tree-manifest\.py capture' "$PREPARE" || fail "prepare 未签发 Python 运行树清单"
|
||
rg -q 'runtime_tree_manifest_sha256=' "$PREPARE" || fail "prepare 摘要未绑定 Python 运行树清单"
|
||
rg -q 'runtime-tree-manifest\.py verify' "$ACTIVATE" || fail "activate 未校验 Python 运行树漂移"
|
||
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'
|