diff --git a/.gitignore b/.gitignore index 8b17a3b..52a21ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env models/ *.tsbuildinfo diff --git a/config/gitleaks.toml b/config/gitleaks.toml new file mode 100644 index 0000000..95f0866 --- /dev/null +++ b/config/gitleaks.toml @@ -0,0 +1,38 @@ +# Gitleaks configuration file +# For more information, see https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml + +[allowlist] +description = "Global allowlist" +paths = [ + '''vendor/''', + '''node_modules/''', + '''\.env\.example$''', + '''\.env\.test$''', + '''go\.sum$''', + '''package-lock\.json$''', +] +stopwords = [ + "example", + "placeholder", + "test-password", +] + +[[rules]] +id = "generic-api-key" +description = "Generic API Key" +regex = '''(?i)(api_key|apikey|secret|password|token)[-|_| ]*[=|\:][-|_| ]*['|"]([0-9a-zA-Z]{16,128})['|"]''' +description_id = "potential_secret" +entropy = 3.5 +keywords = ["api_key", "apikey", "secret", "password", "token"] + +[[rules]] +id = "github-pat" +description = "GitHub Personal Access Token" +regex = '''ghp_[0-9a-zA-Z]{36}''' +keywords = ["ghp_"] + +[[rules]] +id = "google-oauth-client-secret" +description = "Google OAuth Client Secret" +regex = '''(?i)client_secret[-|_| ]*[=|\:][-|_| ]*['|"]([0-9a-zA-Z\-_]{24})['|"]''' +keywords = ["client_secret"] diff --git a/scripts/hooks/commit-msg b/scripts/hooks/commit-msg new file mode 100755 index 0000000..0d2f273 --- /dev/null +++ b/scripts/hooks/commit-msg @@ -0,0 +1,45 @@ +#!/bin/bash +# commit-msg hook to enforce Conventional Commits + +COMMIT_MSG_FILE=$1 +COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") + +# Regex for Conventional Commits (Simplified) +# Types: fix, feat, docs, chore, test, refactor, perf, style, ci, revert +# Scope: optional, inside () +# Subject: required, no ending dot (optional enforcement) +PATTERN="^(fix|feat|docs|chore|test|refactor|perf|style|ci|revert)(\([a-z0-9\._-]+\))?: .+$" + +if [[ ! "$COMMIT_MSG" =~ $PATTERN ]]; then + echo "❌ Error: Invalid commit message format." + echo "----------------------------------------------------------------" + echo "Your commit message must follow the Conventional Commits specification." + echo "" + echo "Format: (): " + echo "" + echo "Allowed types:" + echo " fix - Bug fix" + echo " feat - New feature" + echo " docs - Documentation only" + echo " chore - Build process or auxiliary tool changes" + echo " test - Adding or correcting tests" + echo " refactor - Code change that neither fixes a bug nor adds a feature" + echo " perf - A code change that improves performance" + echo " style - Changes that do not affect the meaning of the code" + echo " ci - CI configuration files and scripts changes" + echo " revert - Reverts a previous commit" + echo "" + echo "Example: feat(auth): add login support" + echo "----------------------------------------------------------------" + echo "See skills/git.conventional-commits.v1.md for more details." + exit 1 +fi + +# Check for length (Subject max 50 chars recommended, but let's say 72 hard limit for header) +HEADER=$(head -n 1 "$COMMIT_MSG_FILE") +LEN=${#HEADER} +if [ "$LEN" -gt 72 ]; then + echo "⚠️ Warning: Commit message header is $LEN characters long (max 72 recommended)." +fi + +exit 0 diff --git a/scripts/hooks/install-hooks.sh b/scripts/hooks/install-hooks.sh new file mode 100755 index 0000000..711b79f --- /dev/null +++ b/scripts/hooks/install-hooks.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# install-hooks.sh - Deploy Git hooks to the current repository + +HOOKS_DIR=$(git rev-parse --git-path hooks) +SOURCE_DIR="scripts/hooks" + +echo "Installing Git hooks to $HOOKS_DIR..." + +install_hook() { + local hook_name=$1 + if [ -f "$SOURCE_DIR/$hook_name" ]; then + cp "$SOURCE_DIR/$hook_name" "$HOOKS_DIR/$hook_name" + chmod +x "$HOOKS_DIR/$hook_name" + echo "✅ Installed $hook_name" + else + echo "❌ Hook $hook_name not found in $SOURCE_DIR" + fi +} + +install_hook "pre-commit" +install_hook "pre-push" +install_hook "commit-msg" + +echo "Done. Gitleaks integration is now active." diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 0000000..d89baba --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,3 @@ +#!/bin/bash +# Git pre-commit hook +./scripts/hooks/run-gitleaks.sh staged diff --git a/scripts/hooks/pre-push b/scripts/hooks/pre-push new file mode 100755 index 0000000..edc79bf --- /dev/null +++ b/scripts/hooks/pre-push @@ -0,0 +1,3 @@ +#!/bin/bash +# Git pre-push hook +./scripts/hooks/run-gitleaks.sh full diff --git a/scripts/hooks/run-gitleaks.sh b/scripts/hooks/run-gitleaks.sh new file mode 100755 index 0000000..6f77b91 --- /dev/null +++ b/scripts/hooks/run-gitleaks.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# run-gitleaks.sh - Wrapper for gitleaks detection modes +# Usage: ./run-gitleaks.sh [staged|full] + +MODE=${1:-staged} +GITLEAKS_BIN=${GITLEAKS_BIN:-gitleaks} +GITLEAKS_CONFIG=${GITLEAKS_CONFIG:-config/gitleaks.toml} +GITLEAKS_REPORT_DIR=${GITLEAKS_REPORT_DIR:-.git/gitleaks} +REPORT_PATH="$GITLEAKS_REPORT_DIR/report.json" + +# Check if gitleaks is installed +if ! command -v "$GITLEAKS_BIN" &> /dev/null; then + echo "Error: gitleaks is not installed or not in PATH." + echo "Install it from: https://github.com/gitleaks/gitleaks/releases" + exit 1 +fi + +# Create report directory +mkdir -p "$GITLEAKS_REPORT_DIR" + +if [ "$MODE" == "staged" ]; then + echo "Running Gitleaks on staged changes..." + # Scan staged changes via pipe + # -U0 for minimal context, --pipe reads from stdin + git diff --cached -U0 | "$GITLEAKS_BIN" detect -v --no-git --pipe --redact --config "$GITLEAKS_CONFIG" --report-path "$REPORT_PATH" + EXIT_CODE=$? +else + echo "Running full Gitleaks scan on repository..." + # Full repository scan + "$GITLEAKS_BIN" detect -v --redact --config "$GITLEAKS_CONFIG" --report-path "$REPORT_PATH" + EXIT_CODE=$? +fi + +if [ $EXIT_CODE -ne 0 ]; then + echo "" + echo "⚠️ Potential secrets detected!" + echo "Report generated at: $REPORT_PATH" + echo "" + echo "Please review the findings and refer to the security playbook:" + echo "skills/git.secret-incident-response.v1.md" + exit $EXIT_CODE +else + echo "✅ No secrets detected." + # Clean up empty report + [ -f "$REPORT_PATH" ] && [ ! -s "$REPORT_PATH" ] && rm "$REPORT_PATH" + exit 0 +fi diff --git a/scripts/security/secret-incident.sh b/scripts/security/secret-incident.sh new file mode 100755 index 0000000..146fccc --- /dev/null +++ b/scripts/security/secret-incident.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# secret-incident.sh - Helper script for handling gitleaks incidents +# Usage: ./scripts/security/secret-incident.sh + +REPORT_FILE=".git/gitleaks/report.json" +CONFIG_FILE="config/gitleaks.toml" + +echo "========================================" +echo "🛡️ Git Secret Incident Response Helper" +echo "========================================" + +if [ ! -f "$REPORT_FILE" ]; then + echo "✅ No active report found at $REPORT_FILE." + echo "Running gitleaks to check for issues..." + ./scripts/hooks/run-gitleaks.sh full + if [ $? -eq 0 ]; then + echo "No secrets detected. You are safe." + exit 0 + fi +fi + +echo "" +echo "⚠️ Secrets detected! parsing report..." +echo "" + +# Check dependencies +if ! command -v jq &> /dev/null; then + echo "Error: 'jq' is not installed. Please install it to parse JSON reports." + # Fallback to simple cat + cat "$REPORT_FILE" + exit 1 +fi + +# Summary +COUNT=$(jq length "$REPORT_FILE") +echo "Found $COUNT potential secret(s)." + +# List items +jq -r '.[] | "----------------------------------------\nType: \(.Description)\nFile: \(.File)\nLine: \(.StartLine)\nCommit: \(.Commit)\nSecret: \(.Secret | .[0:4])..."' "$REPORT_FILE" + +echo "----------------------------------------" +echo "" +echo "Select an action:" +echo "1) 🩹 Fix: I will remove the secret and rotate it (Standard Procedure)" +echo "2) 🧹 History: I pushed it and need to clean git history (Dangerous)" +echo "3) ✅ Allow: This is a false positive (Add to whitelist)" +echo "4) ❌ Exit" + +read -p "Enter choice [1-4]: " CHOICE + +case "$CHOICE" in + 1) + echo "" + echo ">>> ACTION: FIX & ROTATE" + echo "1. Revoke the exposed secret IMMEDIATELY." + echo "2. Remove the secret from the file." + echo "3. Run 'git restore --staged ' if it was just staged." + echo "4. Verify with './scripts/hooks/run-gitleaks.sh staged'" + ;; + 2) + echo "" + echo ">>> ACTION: CLEAN HISTORY" + echo "WARNING: This rewrites history. Notify your team!" + echo "" + echo "Recommended command (install git-filter-repo first):" + echo "git filter-repo --path --invert-paths --force" + echo "" + echo "Then force push:" + echo "git push --force --all" + ;; + 3) + echo "" + echo ">>> ACTION: ALLOWLIST" + echo "Editing $CONFIG_FILE..." + echo "Add the file path or regex to the [[allowlist]] section." + echo "" + echo "Example path:" + echo "paths = ['''path/to/false/positive.file''']" + echo "" + echo "Opening config file..." + # Try to open with default editor + ${EDITOR:-nano} "$CONFIG_FILE" + ;; + 4) + echo "Exiting." + exit 0 + ;; + *) + echo "Invalid choice." + ;; +esac + +echo "" +echo "For more details, read: skills/git.secret-incident-response.v1.md" diff --git a/skills/git.commit-check.v1.md b/skills/git.commit-check.v1.md new file mode 100644 index 0000000..780326e --- /dev/null +++ b/skills/git.commit-check.v1.md @@ -0,0 +1,73 @@ +# skill|git commit check (gitleaks) v1 +> 目标:在提交/推送前阻止 secrets 进入 git 历史。默认:本地 pre-commit 扫 staged;pre-push 全仓复检;CI 再复检一次。 + +## 适用范围 +- 所有代码仓库(尤其:Cloud Run / Vercel / infra / IaC) +- 所有会接触 token、证书、DB 账号、API Key 的变更 + +## 全局规则 +- 默认阻断提交:检测到疑似 secret => 退出码非 0 => commit/push 失败 +- 输出必须可操作:指出报告路径、下一步动作 +- 误报必须可控:只允许在 `gitleaks.toml` 做最小 allowlist(路径/regex) +- 误报放行必须由项目负责人审核,且永不把真实 secret 写进 allowlist(只 allow “确定无害”的模式) + +## 依赖 +- gitleaks v8+(必须支持 `detect --pipe`) +- bash / sh +- git + +## 安装/启用(仓库内) +1) 放置脚本(建议路径): +- `scripts/hooks/run-gitleaks.sh` +- `scripts/hooks/pre-commit` +- `scripts/hooks/pre-push` +- `scripts/hooks/install-hooks.sh` +- `config/gitleaks.toml` + +2) 一键安装 hooks: +```bash +bash scripts/hooks/install-hooks.sh +``` + +## Hook 行为设计 +### pre-commit(快) +只扫描 staged patch(避免扫全仓导致慢) +命令(参考): +```bash +git diff --cached -U0 | gitleaks detect -v --no-git --pipe --redact --config config/gitleaks.toml +``` + +### pre-push(稳) +推送前全仓扫描(兜底) +命令(参考): +```bash +gitleaks detect -v --redact --config config/gitleaks.toml +``` + +## 输出与报告 +- 报告目录:`.git/gitleaks/report.json` +- 发现泄露时输出: + - “Potential secrets detected” + - 报告路径 + - 处理动作(见 skills/git.secret-incident-response.v1.md) + +## 可配置项(环境变量) +- GITLEAKS_BIN:默认 gitleaks +- GITLEAKS_CONFIG:默认 config/gitleaks.toml +- GITLEAKS_REPORT_DIR:默认 .git/gitleaks +- GITLEAKS_MODE:staged|full + +## CI 复检(必须) +任何人都能绕过本地 hook,CI 必须复检: +```bash +gitleaks detect -v --redact --config config/gitleaks.toml +``` + +## 失败处理入口 +统一转交到:skills/git.secret-incident-response.v1.md + +## 验收标准 +✅ 提交包含假 key 时,commit 被阻止并生成 report.json +✅ 误报可通过 allowlist 精准放行 +✅ push 前全仓复检能挡住“漏网之鱼” +✅ CI 中同样会失败(防绕过) diff --git a/skills/git.conventional-commits.v1.md b/skills/git.conventional-commits.v1.md new file mode 100644 index 0000000..f43ef96 --- /dev/null +++ b/skills/git.conventional-commits.v1.md @@ -0,0 +1,54 @@ +# skill|conventional commits v1 +> 目标:统一 commit message 格式,提升可读性、自动化 changelog 生成、语义化版本管理。参考 Peter 的实践规范。 + +## 适用范围 +- 所有代码仓库 +- 所有团队成员的提交 + +## Commit Message 格式 +``` +(): + + + +