#!/bin/sh # check-secrets.sh —— 凭证防泄漏检查(ADR-021,规范见 patbond-doc docs/development/git-workflow.md) # # 规则单一来源:本地 pre-commit 与 CI 兜底跑的是同一个脚本、同一张规则表。 # 三仓(patbond-api / patbond-flutter / patbond-doc)各存一份同构副本,改规则时三仓同步。 # # 用法: # sh scripts/check-secrets.sh --staged # pre-commit:扫暂存区内容(经 scripts/hooks/pre-commit 调用) # sh scripts/check-secrets.sh --all # CI 兜底 / 手动自查:扫全部已跟踪文件(缺省模式) # sh scripts/check-secrets.sh <文件...> # 扫指定文件 # # 拦下真实凭证时的第一动作:去云控制台轮换/禁用该密钥,然后才是清理提交。 set -u mode="${1:---all}" # 允许清单:行内出现任一形态即放行(${} 注入、占位值、明显示例值) ALLOW='\$\{[^}]*\}|\{\{[^}]*\}\}|changeme|change[-_]me|your[-_][a-zA-Z0-9_-]+|<[a-zA-Z0-9 ,_.-]+>|placeholder|example|sample|dummy|fake|redacted|\*\*\*' # 内容扫描跳过:本脚本与 hook 自身(含规则文本,非凭证) SKIP_PATHS='(^|/)scripts/(check-secrets\.sh|hooks/pre-commit)$' # 文件名黑名单:凭证载体文件本体禁止入库(.sample/.example 除外) DENY_NAME='(^|/)\.env(\.[^/]+)?$|(^|/)credentials[^/]*$|[Aa]ccess[Kk]eys?[^/]*\.csv$|(^|/)rootkey\.csv$' DENY_NAME_OK='\.(sample|example)$' # 规则表:ID大小写旗标(i=忽略大小写,-=敏感)文件范围ERE(-=全部文件)行模式ERE RULES=$(cat <<'EOF' AK-AWS - - AKIA[0-9A-Z]{16} AK-QCLOUD - - AKID[0-9A-Za-z]{16,} AK-ALIYUN - - LTAI[0-9A-Za-z]{12,} MINIO-DEFAULT i - minio[-_.]?admin PRIVATE-KEY - - ^[[:space:]]*-----BEGIN [A-Z ]*PRIVATE KEY-----[[:space:]]*$ KEY-ASSIGN i - (access[-_]?key(_?id)?|secret[-_]?(access[-_]?)?key)["']?[[:space:]]*[:=][[:space:]]*["']?[A-Za-z0-9+/=_-]{8,} JWT-SECRET i - (jwt[-_.]?secret|signing[-_]?key|token[-_]?secret|hmac[-_]?(key|secret))["']?[[:space:]]*[:=][[:space:]]*["']?[A-Za-z0-9+/=_-]{8,} DB-PASSWORD i \.(ya?ml|properties|toml|conf|ini)(\.sample|\.example)?$ (password|passwd|pwd)["']?[[:space:]]*[:=][[:space:]]*["']?[^[:space:]"'$]{6,} EOF ) case "$mode" in --staged) files=$(git diff --cached --name-only --diff-filter=ACM) src=index ;; --all) files=$(git ls-files) src=worktree ;; -*) echo "用法: $0 [--staged|--all|<文件...>]" >&2 exit 2 ;; *) files=$(printf '%s\n' "$@") src=worktree ;; esac [ -n "$files" ] || exit 0 tmp=$(mktemp) || exit 2 viol=$(mktemp) || exit 2 trap 'rm -f "$tmp" "$viol"' EXIT # 第一道:文件名黑名单 printf '%s\n' "$files" | grep -E "$DENY_NAME" | grep -vE "$DENY_NAME_OK" | sed 's/^/[NAME-DENY] /' >>"$viol" || true # 第二道:逐文件逐规则内容扫描(二进制文件经 grep -I 自然跳过) IFS=' ' for f in $files; do printf '%s' "$f" | grep -qE "$SKIP_PATHS" && continue if [ "$src" = index ]; then git show ":$f" >"$tmp" 2>/dev/null || continue else [ -f "$f" ] || continue cat -- "$f" >"$tmp" fi printf '%s\n' "$RULES" | while IFS="$(printf '\t')" read -r id flag scope pat; do [ -n "$id" ] || continue if [ "$scope" != "-" ]; then printf '%s' "$f" | grep -qE "$scope" || continue fi ci="" [ "$flag" = "i" ] && ci="-i" grep -InE $ci -e "$pat" "$tmp" 2>/dev/null | grep -viE "$ALLOW" | sed "s|^|[$id] $f:|" >>"$viol" || true done done if [ -s "$viol" ]; then echo "凭证防泄漏检查未通过(ADR-021)——以下内容疑似真实凭证:" >&2 cat "$viol" >&2 cat >&2 <<'MSG' 处置: 1. 若是真实凭证:先去云控制台轮换/禁用该密钥,再从提交中移除; 2. 若是误报:改用 ${} 注入或占位值(changeme / your-xxx / <占位>), 或与团队确认后调整三仓同构的 scripts/check-secrets.sh 规则表。 敏感信息只允许存在于被 gitignore 的文件或 .sample 占位中(git-workflow.md)。 MSG exit 1 fi exit 0