website-scripts/check-compliance.sh

288 lines
12 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
# check-compliance.sh — 部署前合规检查(对照 CLAUDE.md + LESSONS-LEARNED.md
# 用法: bash scripts/check-compliance.sh [--strict]
# 退出码: 0=通过, 1=违规
set -o pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
PASS=0; WARN=0; FAIL=0
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ok() { echo -e " ${GREEN}[✓]${NC} $1"; ((PASS++)); }
warn() { echo -e " ${YELLOW}[!]${NC} $1"; ((WARN++)); }
err() { echo -e " ${RED}[✗]${NC} $1"; ((FAIL++)); }
echo "========================================="
echo " 部署前合规检查"
echo "========================================="
echo ""
# ============================================================
# D1: healthcheck 禁止用 curl
# ============================================================
echo "--- D1: healthcheck 禁止 curl ---"
for f in "$PROJECT_ROOT"/*/docker-compose.yml; do
site=$(basename "$(dirname "$f")")
if grep -A2 'healthcheck' "$f" 2>/dev/null | grep -q 'curl'; then
err "$site: healthcheck 使用了 curl必须用 wget --spider 或 node fetch"
else
ok "$site"
fi
done
echo ""
# ============================================================
# D2: 含数据库的站点容器必须添加 healthcheck
# ============================================================
echo "--- D2: 含数据库容器 healthcheck ---"
DB_SITES="issue-ai assets-ai oa-ai monitor-ai gitea-ai"
for site in $DB_SITES; do
f="$PROJECT_ROOT/$site/docker-compose.yml"
[ ! -f "$f" ] && continue
if grep -q 'healthcheck' "$f"; then
ok "$site"
else
err "$site: 含数据库但缺少 healthcheck"
fi
done
echo ""
# ============================================================
# D3: 禁止 entrypoint 延迟构建
# ============================================================
echo "--- D3: 禁止 entrypoint 延迟构建 ---"
# 仅检查 entrypoint.shDockerfile 中 builder 阶段的 npm run build 是合法的多阶段构建)
for f in "$PROJECT_ROOT"/*/entrypoint.sh; do
[ ! -f "$f" ] && continue
site=$(echo "$f" | sed 's|.*/\([^/]*\)/.*|\1|')
if grep -q 'npm run build\|npm run dev\|next build\|next dev' "$f" 2>/dev/null; then
err "$site: entrypoint.sh 中包含构建命令"
fi
done
ok "所有 entrypoint.sh 不含构建命令"
echo ""
# ============================================================
# D4: 必须使用 docker compose V2禁止 docker-compose
# ============================================================
echo "--- D4: docker compose V2 ---"
if grep -rn 'docker-compose ' "$PROJECT_ROOT/scripts/" --include='*.sh' 2>/dev/null | grep -v '^[[:space:]]*#' | grep -v 'docker compose' | grep -q .; then
err "脚本中存在 docker-composeV1应使用 docker composeV2"
else
ok "所有脚本使用 docker compose V2"
fi
echo ""
# ============================================================
# 时区规范: 禁止 Date.toISOString() 格式化本地日期
# ============================================================
echo "--- 时区: 禁止 Date.toISOString() ---"
# 仅标记直接 toISOString()(无 UTC+8 偏移的用法),允许 +8h 偏移模式
VIOLATIONS=$(grep -rn 'new Date()\.toISOString()\|new Date\.toISOString()' "$PROJECT_ROOT"/*/src --include='*.ts' --include='*.tsx' 2>/dev/null | grep -v node_modules | grep -v 'shared/')
if [ -n "$VIOLATIONS" ]; then
echo "$VIOLATIONS" | while IFS= read -r line; do
err "$line"
done
else
ok "所有站点未使用裸 Date.toISOString()"
fi
echo ""
# ============================================================
# 时区规范: SQLite datetime('now') 必须带 +8 hours
# ============================================================
echo "--- 时区: SQLite datetime('now') → +8 hours ---"
VIOLATIONS=$(grep -rn "datetime('now')" "$PROJECT_ROOT"/*/src --include='*.ts' --include='*.tsx' 2>/dev/null | grep -v "+8 hours" | grep -v node_modules | grep -v shared)
if [ -n "$VIOLATIONS" ]; then
echo "$VIOLATIONS" | while IFS= read -r line; do
err "$line"
done
else
ok "所有 SQLite datetime('now') 均带 +8 hours"
fi
echo ""
# ============================================================
# JWT_SECRET + COOKIE_DOMAIN 在所有站点一致
# ============================================================
echo "--- JWT_SECRET + COOKIE_DOMAIN 一致性 ---"
JWT_VALUES=""
COOKIE_VALUES=""
for f in "$PROJECT_ROOT"/*/docker-compose.yml; do
site=$(basename "$(dirname "$f")")
jwt=$(grep -o "JWT_SECRET=\${JWT_SECRET:-[^}]*}\|JWT_SECRET=[^ ]*" "$f" 2>/dev/null | head -1)
cookie=$(grep -o "COOKIE_DOMAIN=[^ ]*" "$f" 2>/dev/null | head -1)
[ -n "$jwt" ] && JWT_VALUES="$JWT_VALUES\n$site: $jwt"
[ -n "$cookie" ] && COOKIE_VALUES="$COOKIE_VALUES\n$site: $cookie"
done
echo -e "$JWT_VALUES" | sed '/^$/d'
echo -e "$COOKIE_VALUES" | sed '/^$/d'
# Check consistency
jwt_count=$(echo -e "$JWT_VALUES" | grep -o 'JWT_SECRET=[^ ]*' | sort -u | wc -l)
cookie_count=$(echo -e "$COOKIE_VALUES" | grep -o 'COOKIE_DOMAIN=[^ ]*' | sort -u | wc -l)
[ "$jwt_count" -le 1 ] && ok "JWT_SECRET 一致" || err "JWT_SECRET 不一致($jwt_count 个不同值)"
[ "$cookie_count" -le 1 ] && ok "COOKIE_DOMAIN 一致" || err "COOKIE_DOMAIN 不一致($cookie_count 个不同值)"
echo ""
# ============================================================
# LESSONS-LEARNED #24: cookie domain 必须设置
# ============================================================
echo "--- LESSONS-LEARNED #24: cookie domain ---"
for f in "$PROJECT_ROOT"/*/docker-compose.yml; do
site=$(basename "$(dirname "$f")")
if grep -q 'COOKIE_DOMAIN' "$f"; then
ok "$site"
else
warn "$site: docker-compose.yml 未设置 COOKIE_DOMAIN"
fi
done
echo ""
# ============================================================
# LESSONS-LEARNED #30: globals.css 含 @source "../../shared"
# ============================================================
echo "--- LESSONS-LEARNED #30: globals.css @source ---"
for f in "$PROJECT_ROOT"/issue-ai/src/app/globals.css "$PROJECT_ROOT"/assets-ai/src/app/globals.css "$PROJECT_ROOT"/oa-ai/src/app/globals.css "$PROJECT_ROOT"/monitor-ai/src/app/globals.css; do
[ ! -f "$f" ] && continue
site=$(echo "$f" | sed 's|.*/\([^/]*\)/src/.*|\1|')
if grep -q '@source.*shared' "$f" 2>/dev/null; then
ok "$site"
else
warn "$site: globals.css 缺少 @source \"../../shared\""
fi
done
echo ""
# ============================================================
# LESSONS-LEARNED #35: outputFileTracingIncludes
# ============================================================
echo "--- LESSONS-LEARNED #35: outputFileTracingIncludes ---"
for f in "$PROJECT_ROOT"/issue-ai/next.config.ts "$PROJECT_ROOT"/assets-ai/next.config.ts "$PROJECT_ROOT"/oa-ai/next.config.ts "$PROJECT_ROOT"/monitor-ai/next.config.ts; do
[ ! -f "$f" ] && continue
site=$(echo "$f" | sed 's|.*/\([^/]*\)/next.*|\1|')
if grep -q 'outputFileTracingIncludes' "$f" 2>/dev/null; then
ok "$site"
else
warn "$site: next.config.ts 缺少 outputFileTracingIncludes"
fi
done
echo ""
# ============================================================
# LESSONS-LEARNED #40: sso cert 使用 Let's Encrypt
# ============================================================
echo "--- LESSONS-LEARNED #40: sso nginx cert ---"
SSO_CONF="$PROJECT_ROOT/nginx-proxy-ai/conf.d/sso-ai.conf"
if [ -f "$SSO_CONF" ]; then
if grep -q 'www.tlyq.ai-0001\|letsencrypt' "$SSO_CONF" 2>/dev/null; then
ok "sso-ai.conf 使用 Let's Encrypt 证书"
else
err "sso-ai.conf 未使用 Let's Encrypt 证书LESSONS-LEARNED #40"
fi
else
warn "sso-ai.conf 不存在"
fi
echo ""
# ============================================================
# LESSONS-LEARNED #37: 禁用 request.url
# ============================================================
echo "--- LESSONS-LEARNED #37: 禁用 request.url ---"
# 仅检查 V2 middleware旧 middleware.ts 保持向后兼容,迁移后删除)
VIOLATIONS=$(grep -rn 'request\.url' "$PROJECT_ROOT/shared/lib/auth/middleware-v2.ts" 2>/dev/null | grep -v '\.origin\|node_modules')
if [ -n "$VIOLATIONS" ]; then
echo "$VIOLATIONS" | while IFS= read -r line; do
err "$line"
done
else
ok "middleware-v2 未直接使用 request.url"
fi
echo ""
# ============================================================
# LESSONS-LEARNED #41: 容器内禁止 docker exec 获取凭据
# ============================================================
echo "--- LESSONS-LEARNED #41: docker exec in container ---"
VIOLATIONS=$(grep -rn "execFileSync.*docker.*exec\|execSync.*docker.*exec\|exec('docker'" "$PROJECT_ROOT"/*/src --include='*.ts' --include='*.tsx' 2>/dev/null | grep -v node_modules)
if [ -n "$VIOLATIONS" ]; then
echo "$VIOLATIONS" | while IFS= read -r line; do
warn "$line (容器内可能无 docker CLI见 LESSONS-LEARNED #41"
done
else
ok "无 docker exec 依赖"
fi
echo ""
# ============================================================
# OA: LLDAP_ADMIN_PASSWORD 环境变量注入
# ============================================================
echo "--- OA: LLDAP_ADMIN_PASSWORD ---"
if grep -q 'LLDAP_ADMIN_PASSWORD' "$PROJECT_ROOT/oa-ai/docker-compose.yml" 2>/dev/null; then
ok "oa-ai docker-compose.yml 含 LLDAP_ADMIN_PASSWORD"
else
err "oa-ai docker-compose.yml 缺少 LLDAP_ADMIN_PASSWORDLESSONS-LEARNED #41"
fi
echo ""
# ============================================================
# D8 + LESSONS-LEARNED #48: OIDC token_endpoint_auth_method 一致性
# ============================================================
echo "--- D8: OIDC token_endpoint_auth_method 一致性 ---"
SSO_CONFIG="$PROJECT_ROOT/sso-ai/config/authelia/configuration.yml"
if [ -f "$SSO_CONFIG" ]; then
# 列出每个 OIDC client 的 token_endpoint_auth_method
while IFS= read -r line; do
if echo "$line" | grep -q 'client_id:'; then
current_client=$(echo "$line" | sed "s/.*client_id: *'\(.*\)'/\1/" | sed 's/.*client_id: *"\(.*\)"/\1/' | sed 's/.*client_id: *//' | tr -d "'\"")
fi
if echo "$line" | grep -q 'token_endpoint_auth_method:'; then
method=$(echo "$line" | grep -o "client_secret_[a-z]*")
if [ "$method" = "client_secret_post" ]; then
ok "$current_client: $method"
elif [ "$method" = "client_secret_basic" ]; then
err "$current_client: $method(应为 client_secret_post见 LESSONS-LEARNED #48"
fi
current_client=""
fi
done < "$SSO_CONFIG"
# 检查是否有 OIDC client 完全没有 token_endpoint_auth_methodAuthelia 默认 client_secret_basic
# 注意:在 YAML 中 token_endpoint_auth_method 可能出现在任意位置,需读完整个 client 块
unset_client=""
prev_client=""
while IFS= read -r line; do
# 遇到新 client_id → 结算上一个
if echo "$line" | grep -q '^[[:space:]]*-[[:space:]]*client_id:'; then
if [ -n "$prev_client" ] && [ "$prev_has_method" -eq 0 ]; then
err "$prev_client: 未设置 token_endpoint_auth_method默认 client_secret_basic → 与代码 client_secret_post 不匹配)"
fi
prev_client=$(echo "$line" | sed "s/.*client_id: *'\(.*\)'/\1/" | sed 's/.*client_id: *"\(.*\)"/\1/' | sed 's/.*client_id: *//' | tr -d "'\"")
prev_has_method=0
fi
if echo "$line" | grep -q 'token_endpoint_auth_method:'; then
prev_has_method=1
fi
done < "$SSO_CONFIG"
# 结算最后一个 client
if [ -n "$prev_client" ] && [ "$prev_has_method" -eq 0 ]; then
err "$prev_client: 未设置 token_endpoint_auth_method默认 client_secret_basic → 与代码 client_secret_post 不匹配)"
fi
else
warn "sso-ai/config/authelia/configuration.yml 不存在"
fi
echo ""
# ============================================================
# 结果
# ============================================================
echo "========================================="
echo -e " 通过: ${GREEN}${PASS}${NC} 警告: ${YELLOW}${WARN}${NC} 违规: ${RED}${FAIL}${NC}"
echo "========================================="
if [ "$FAIL" -gt 0 ]; then
echo -e "${RED}存在 ${FAIL} 项违规,部署前必须修复${NC}"
exit 1
elif [ "$WARN" -gt 0 ]; then
echo -e "${YELLOW}存在 ${WARN} 项警告,建议修复后部署${NC}"
fi
exit 0