From 8ee4676a683748a878f95993829c3064d458f24b Mon Sep 17 00:00:00 2001 From: ahmifte Date: Wed, 10 Jun 2026 17:33:53 -0700 Subject: [PATCH] chore: add seal pre-commit hook for secret scanning --- .githooks/gitleaks.toml | 83 +++++++++++++++++++++++++++++ .githooks/pre-commit | 115 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 .githooks/gitleaks.toml create mode 100755 .githooks/pre-commit diff --git a/.githooks/gitleaks.toml b/.githooks/gitleaks.toml new file mode 100644 index 0000000..75e4f95 --- /dev/null +++ b/.githooks/gitleaks.toml @@ -0,0 +1,83 @@ +title = "seal gitleaks config" + +[extend] +useDefault = true + +# Extra patterns beyond gitleaks defaults — tuned for common dev mistakes. +[[rules]] +id = "generic-env-assignment" +description = "Likely secret assigned in an env file (not .env.example)" +regex = '''(?i)(api[_-]?key|secret|token|password|passwd|credential)\s*=\s*['"]?[a-zA-Z0-9_\-./]{8,}['"]?''' +tags = ["key", "env"] + +[[rules]] +id = "stripe-restricted-key" +description = "Stripe restricted API key" +regex = '''rk_(live|test)_[0-9a-zA-Z]{10,}''' +tags = ["stripe", "key"] + +[[rules]] +id = "stripe-secret-key" +description = "Stripe secret API key" +regex = '''sk_(live|test)_[0-9a-zA-Z]{10,}''' +tags = ["stripe", "key"] + +[[rules]] +id = "stripe-webhook-secret" +description = "Stripe webhook signing secret" +regex = '''whsec_[0-9a-zA-Z]{10,}''' +tags = ["stripe", "key"] + +[[rules]] +id = "openai-api-key" +description = "OpenAI API key" +regex = '''sk-[a-zA-Z0-9]{20,}''' +tags = ["openai", "key"] + +[[rules]] +id = "github-pat" +description = "GitHub personal access token" +regex = '''ghp_[0-9a-zA-Z]{20,}''' +tags = ["github", "key"] + +[[rules]] +id = "github-oauth" +description = "GitHub OAuth token" +regex = '''gho_[0-9a-zA-Z]{20,}''' +tags = ["github", "key"] + +[[rules]] +id = "github-fine-grained-pat" +description = "GitHub fine-grained PAT" +regex = '''github_pat_[0-9a-zA-Z_]{20,}''' +tags = ["github", "key"] + +[[rules]] +id = "aws-access-key" +description = "AWS access key ID" +regex = '''AKIA[0-9A-Z]{16}''' +tags = ["aws", "key"] + +[[rules]] +id = "slack-token" +description = "Slack token" +regex = '''xox[baprs]-[0-9A-Za-z\-]{10,}''' +tags = ["slack", "key"] + +[allowlist] +description = "Safe placeholders and example files" +paths = [ + '''\.env\.example$''', + '''\.env\.sample$''', + '''README\.md$''', + '''SECURITY\.md$''', + '''gitleaks\.toml$''', + '''hooks/pre-commit$''', +] +regexes = [ + '''your-[a-z-]+-key''', + '''replace-with''', + '''example\.com''', + '''xxxxxxxx''', + '''<[A-Z_]+>''', +] diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..1e3112d --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# seal — pre-commit secret scanner +# Blocks commits that stage secrets, private env files, or high-risk key patterns. +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' + +fail() { + echo -e "${RED}seal: commit blocked — $1${NC}" >&2 + exit 1 +} + +warn() { + echo -e "${YELLOW}seal: warning — $1${NC}" >&2 +} + +# --- 1. Block staging of files that should never be committed --- +BLOCKED_FILES=( + ".env" + ".env.local" + ".env.production" + ".env.development" + "credentials.json" + "service-account.json" + "id_rsa" + "id_ed25519" + "*.pem" + "*.p12" + "*.pfx" +) + +STAGED=$(git diff --cached --name-only --diff-filter=ACMR) + +for file in $STAGED; do + base=$(basename "$file") + for blocked in "${BLOCKED_FILES[@]}"; do + if [[ "$blocked" == *"*"* ]]; then + ext="${blocked#\*}" + if [[ "$base" == *"$ext" ]]; then + fail "refusing to commit '$file' ($ext files must stay local)" + fi + elif [[ "$base" == "$blocked" ]]; then + fail "refusing to commit '$file' (add it to .gitignore; only .env.example belongs in git)" + fi + done +done + +# --- 2. Prefer gitleaks when installed (best coverage) --- +if command -v gitleaks >/dev/null 2>&1; then + CONFIG="$HOOK_DIR/gitleaks.toml" + if [[ ! -f "$CONFIG" ]]; then + CONFIG="$ROOT/gitleaks.toml" + fi + if [[ ! -f "$CONFIG" ]]; then + CONFIG="" + fi + if [[ -n "$CONFIG" ]]; then + gitleaks protect --staged --redact --verbose --config "$CONFIG" --source "$ROOT" + else + gitleaks protect --staged --redact --verbose --source "$ROOT" + fi + echo "seal: gitleaks scan passed" + exit 0 +fi + +# --- 3. Fallback: pattern scan on staged content (no gitleaks required) --- +PATTERNS=( + 'sk_live_[0-9a-zA-Z]{10,}' + 'sk_test_[0-9a-zA-Z]{10,}' + 'rk_live_[0-9a-zA-Z]{10,}' + 'rk_test_[0-9a-zA-Z]{10,}' + 'whsec_[0-9a-zA-Z]{10,}' + 'sk-[a-zA-Z0-9]{20,}' + 'ghp_[0-9a-zA-Z]{20,}' + 'gho_[0-9a-zA-Z]{20,}' + 'github_pat_[0-9a-zA-Z_]{20,}' + 'AKIA[0-9A-Z]{16}' + 'xox[baprs]-[0-9A-Za-z\-]{10,}' +) + +FOUND=0 +for file in $STAGED; do + # Skip allowlisted paths + case "$file" in + *.env.example|*.env.sample|README.md|SECURITY.md|gitleaks.toml) continue ;; + esac + + if [[ ! -f "$ROOT/$file" ]]; then + continue + fi + + # Skip binary files + if file -b --mime-type "$ROOT/$file" 2>/dev/null | grep -qE '^(image|video|audio|application/(zip|gzip|pdf|octet-stream))'; then + continue + fi + + for pattern in "${PATTERNS[@]}"; do + if grep -qE "$pattern" "$ROOT/$file" 2>/dev/null; then + echo -e "${RED}seal: possible secret in $file (matched: $pattern)${NC}" >&2 + FOUND=1 + fi + done +done + +if [[ "$FOUND" -eq 1 ]]; then + fail "secret-like value detected in staged files. Install gitleaks for deeper scanning: brew install gitleaks" +fi + +warn "gitleaks not installed — using basic pattern scan only. Run: brew install gitleaks" +echo "seal: basic scan passed"