#!/bin/bash # ============================================================================= # workbuddy / Claude 推送前 4 件套预检脚本 # ============================================================================= # 用途: 推送前自检 4 件套,发现 P0 漏洞立即拦截 # 1. 鉴权: 新增/修改端点是否带 Depends(get_current_*) 鉴权 # 2. 依赖: 新增 import 是否同步 requirements.txt / package.json # 3. alembic: model schema 变化是否生成迁移脚本 # 4. 配置: nginx / docker / conf 改动是否完整 # # 用法: # bash scripts/pre-commit-check.sh # 检查 staged 变更 # bash scripts/pre-commit-check.sh --staged # 同上(默认) # bash scripts/pre-commit-check.sh --branch # 检查当前分支相对 main 的全部变更 # bash scripts/pre-commit-check.sh --strict # 严格模式(任何 warn 也失败) # bash scripts/pre-commit-check.sh --json # 输出 JSON 格式(给 workbuddy 解析) # # 退出码: # 0 = 全过 / 仅 INFO # 1 = 有 WARN(--strict 下) # 2 = 有 ERROR(必须修) # ============================================================================= set -e # 颜色 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' PASS_COUNT=0 WARN_COUNT=0 FAIL_COUNT=0 WARN_LIST=() FAIL_LIST=() pass() { PASS_COUNT=$((PASS_COUNT+1)); echo -e "${GREEN}[PASS]${NC} $1"; } warn() { WARN_COUNT=$((WARN_COUNT+1)); WARN_LIST+=("$1"); echo -e "${YELLOW}[WARN]${NC} $1"; } fail() { FAIL_COUNT=$((FAIL_COUNT+1)); FAIL_LIST+=("$1"); echo -e "${RED}[FAIL]${NC} $1"; } info() { echo -e "${BLUE}[INFO]${NC} $1"; } # 参数 MODE="staged" STRICT=false JSON_OUT=false for arg in "$@"; do case $arg in --staged) MODE="staged" ;; --branch) MODE="branch" ;; --strict) STRICT=true ;; --json) JSON_OUT=true ;; *) ;; esac done PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$PROJECT_ROOT" # 收集变更文件 if [ "$MODE" = "staged" ]; then info "检查 staged 变更..." CHANGED=$(git diff --cached --name-only) BASE="staged" elif [ "$MODE" = "branch" ]; then info "检查当前分支 vs main 的变更..." BASE_BRANCH="main" git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1 || BASE_BRANCH="origin/main" git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1 || { fail "找不到 main 分支,请先 git fetch" exit 2 } CHANGED=$(git diff --name-only "$BASE_BRANCH"...HEAD) fi if [ -z "$CHANGED" ]; then info "无变更文件,跳过" exit 0 fi info "变更文件 ($([ "$MODE" = "staged" ] && echo "staged" || echo "branch")):" echo "$CHANGED" | sed 's/^/ /' # ============================================================================= # 检查 1: 鉴权 # ============================================================================= info "" info "── 检查 1/4: 鉴权 (Depends(get_current_*))" check_auth() { local file="$1" # 只检查后端 api 路由文件 case "$file" in backend/app/api/*.py|backend/app/api/**/*.py) ;; *) return 0 ;; esac # 跳过非路由文件 case "$file" in *schemas*) return 0 ;; *_test*) return 0 ;; esac # 看 diff 是否新增/修改了路由(@router.*) local diff if [ "$MODE" = "staged" ]; then diff=$(git diff --cached "$file") else diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") fi # 有 router 装饰器改动? if ! echo "$diff" | grep -qE '^\+.*@router\.(get|post|put|delete|patch)'; then return 0 # 无路由变化,跳过 fi # 看新增/修改的函数是否带 Depends local func_diff func_diff=$(echo "$diff" | grep -E '^\+.*(async )?def [a-z_]+\(') if echo "$func_diff" | grep -qE 'Depends\(.*get_current'; then pass " $file: 新路由有 Depends 鉴权" elif echo "$func_diff" | grep -qE '@router\.(get|post|put|delete|patch)'; then # 新增路由但没 Depends → 极可能是 P0 漏洞 local new_routes new_routes=$(echo "$func_diff" | grep -B 5 'def [a-z_]' | grep -E '^\+.*@router\.' | head -5) if [ -n "$new_routes" ]; then fail " $file: 新增路由可能无鉴权: $(echo "$new_routes" | wc -l) 处" fi fi } for f in $CHANGED; do [ -f "$f" ] && check_auth "$f" done # ============================================================================= # 检查 2: 依赖 # ============================================================================= info "" info "── 检查 2/4: 依赖 (requirements.txt / package.json)" check_deps() { local file="$1" # 只检查 Python / JS 文件 case "$file" in *.py) # 看 diff 是否新增 import local diff if [ "$MODE" = "staged" ]; then diff=$(git diff --cached "$file") else diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") fi local new_imports new_imports=$(echo "$diff" | grep -E '^\+.*^(from|import) ' | grep -vE '^\+\s*#' | head -10) if [ -z "$new_imports" ]; then return 0; fi # 提取第三方包(标准库除外) local third_party third_party=$(echo "$new_imports" | grep -E '^\+.*^(from|import) [a-z_]+' | \ sed -E 's/^\+ *(from|import) ([a-z_][a-z0-9_]*).*/\2/' | \ grep -vE '^(os|sys|re|json|time|datetime|typing|asyncio|pathlib|hashlib|hmac|secrets|base64|urllib|http|logging|functools|collections|itertools|contextlib|io|copy|enum|dataclasses|abc|math|random|string|subprocess|threading|multiprocessing|signal|socket|ssl|tempfile|shutil|glob|fnmatch|stat|argparse|getopt|unittest|traceback|warnings|pickle|csv|xml|html|email|zoneinfo|decimal|fractions|gcd)' | \ sort -u) if [ -n "$third_party" ]; then # 检查 requirements.txt 是否已有 local missing="" for pkg in $third_party; do if ! grep -qiE "^${pkg}([=<>!~]|$)" backend/requirements.txt 2>/dev/null; then missing+="$pkg " fi done if [ -n "$missing" ]; then fail " $file: 新增第三方 import 但 requirements.txt 缺: $missing" else pass " $file: 新增 import 已在 requirements.txt" fi fi ;; *.ts|*.tsx|*.vue|*.js|*.jsx) # 看 diff 是否新增 import / require local diff if [ "$MODE" = "staged" ]; then diff=$(git diff --cached "$file") else diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") fi local new_imports new_imports=$(echo "$diff" | grep -E '^\+.*(import .* from |require\()' | head -10) if [ -z "$new_imports" ]; then return 0; fi # 找对应 package.json local pkg_json="package.json" case "$file" in frontend-admin/*) pkg_json="frontend-admin/package.json" ;; frontend-agent/*) pkg_json="frontend-agent/package.json" ;; frontend-h5/*) pkg_json="frontend-h5/package.json" ;; frontend-portal/*) pkg_json="frontend-portal/package.json" ;; esac if [ -f "$pkg_json" ]; then # 提取包名(简单粗暴,workbuddy 改的常规 npm 包) local new_pkgs new_pkgs=$(echo "$new_imports" | grep -oE "from ['\"](@?[a-z][a-z0-9_/.-]+)" | sed -E "s/from ['\"]//" | sort -u) if [ -n "$new_pkgs" ]; then local missing="" for pkg in $new_pkgs; do if ! grep -qE "\"${pkg#@*/?}\"" "$pkg_json" 2>/dev/null; then missing+="$pkg " fi done if [ -n "$missing" ]; then warn " $file: 新增 import,需确认 $pkg_json 有: $missing" fi fi fi ;; esac } for f in $CHANGED; do [ -f "$f" ] && check_deps "$f" done # ============================================================================= # 检查 3: alembic # ============================================================================= info "" info "── 检查 3/4: alembic 迁移" check_alembic() { local file="$1" # model 改了 → 必须有 alembic 迁移 case "$file" in backend/app/models/*.py) # 看 diff 是否改 schema(Column / type / nullable / default) local diff if [ "$MODE" = "staged" ]; then diff=$(git diff --cached "$file") else diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") fi if echo "$diff" | grep -qE '^\+.*Column\(|^\+.*Mapped\['; then # 找本次 commit/branch 是否新增 alembic 迁移 local migrations if [ "$MODE" = "staged" ]; then migrations=$(git diff --cached --name-only | grep "alembic/versions/.*\.py" || true) else migrations=$(git diff --name-only "$BASE_BRANCH"...HEAD | grep "alembic/versions/.*\.py" || true) fi if [ -z "$migrations" ]; then fail " $file: model schema 变化但无 alembic 迁移" else pass " $file: model 改了,有 alembic 迁移: $migrations" fi fi ;; backend/alembic/versions/*.py) info " $file: alembic 迁移新增" ;; esac } for f in $CHANGED; do [ -f "$f" ] && check_alembic "$f" done # ============================================================================= # 检查 4: 配置 # ============================================================================= info "" info "── 检查 4/4: 配置 (nginx / docker / conf)" check_config() { local file="$1" case "$file" in nginx.conf|deploy-server/nginx.conf|docker-compose.yml|docker-compose*.yml|.env.example) warn " $file: 配置文件改动,确认 deploy.sh / docs/DEPLOY_NAS.md 同步更新" ;; backend/app/config.py) # 配置改了 → 看 .env.example 是否同步 local diff if [ "$MODE" = "staged" ]; then diff=$(git diff --cached "$file") else diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") fi local new_settings new_settings=$(echo "$diff" | grep -E '^\+.*(os\.getenv|Field\(.*env=)' | head -5) if [ -n "$new_settings" ]; then warn " $file: 新增配置项,确认 .env.example 同步" fi ;; esac } for f in $CHANGED; do [ -f "$f" ] && check_config "$f" done # ============================================================================= # 总结 # ============================================================================= info "" info "── 总结" echo " PASS: $PASS_COUNT / WARN: $WARN_COUNT / FAIL: $FAIL_COUNT" if [ $FAIL_COUNT -gt 0 ]; then echo "" echo "🛑 [FAIL] 列表:" for msg in "${FAIL_LIST[@]}"; do echo " - $msg"; done echo "" echo "🛑 预检失败,请修 FAIL 项后再推送" exit 2 fi if [ $WARN_COUNT -gt 0 ]; then echo "" echo "⚠️ [WARN] 列表:" for msg in "${WARN_LIST[@]}"; do echo " - $msg"; done if [ "$STRICT" = true ]; then echo "" echo "🛑 严格模式下 WARN 也算失败" exit 1 fi echo "" echo "⚠️ 有 WARN 项,但允许推送(评审员关注)" fi echo "" echo "✅ 预检通过,可以推送" exit 0