Add pre-commit hook to detect secrets
Blocks commits containing:
- Private key file headers (RSA/EC/DSA/OPENSSH)
- Plaintext passwords/tokens ≥16 chars
- Gitea/GitHub-style hex tokens
- Files with private key path suffixes (_ed25519, _rsa, etc.)
Allowlisted: Ansible vault variable references ({{ vault_* }}),
encrypted vault blocks ($ANSIBLE_VAULT), and comment lines.
Run scripts/install-hooks.sh after cloning to activate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
FAIL=0
|
||||
errors=()
|
||||
|
||||
# Files that must never be staged (matched against path)
|
||||
FORBIDDEN_PATHS=(
|
||||
'vault_pass$'
|
||||
'\.secret$'
|
||||
'\.vault$'
|
||||
'_ed25519$'
|
||||
'_rsa$'
|
||||
'_dsa$'
|
||||
'_ecdsa$'
|
||||
'user_credentials\.json$'
|
||||
'\.env$'
|
||||
)
|
||||
|
||||
# Regex patterns that indicate secrets in file content.
|
||||
# Each is matched with: grep -qiE -e "$pattern"
|
||||
SECRET_PATTERNS=(
|
||||
'BEGIN (RSA|EC|DSA|OPENSSH) PRIVATE KEY'
|
||||
'password[[:space:]]*[:=][[:space:]]*["\x27]?[A-Za-z0-9+/!@#$%^&*]{16,}'
|
||||
'secret_key[[:space:]]*[:=][[:space:]]*["\x27][A-Za-z0-9+/]{24,}'
|
||||
'internal_token[[:space:]]*[:=][[:space:]]*["\x27][A-Za-z0-9+/]{24,}'
|
||||
'token[[:space:]]*[:=][[:space:]]*["\x27][a-f0-9]{40}'
|
||||
'api[_-]?key[[:space:]]*[:=][[:space:]]*["\x27][A-Za-z0-9]{24,}'
|
||||
)
|
||||
|
||||
# Lines matching these are skipped before pattern matching (false-positive suppression)
|
||||
SAFE_PATTERNS=(
|
||||
'\$ANSIBLE_VAULT'
|
||||
'\{\{.*\}\}'
|
||||
'^[[:space:]]*#'
|
||||
'vault_password_file'
|
||||
'^vault_[a-z_]*:'
|
||||
'grafana_admin_password: admin'
|
||||
'_password:.*vault_'
|
||||
'Example\|example\|placeholder\|CHANGEME'
|
||||
)
|
||||
|
||||
staged=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null)
|
||||
[[ -z "$staged" ]] && exit 0
|
||||
|
||||
# --- Check forbidden file paths ---
|
||||
for f in $staged; do
|
||||
for pattern in "${FORBIDDEN_PATHS[@]}"; do
|
||||
if echo "$f" | grep -qE -e "$pattern"; then
|
||||
errors+=("FORBIDDEN FILE: $f (matches: $pattern)")
|
||||
FAIL=1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# --- Check staged content for secret patterns ---
|
||||
for f in $staged; do
|
||||
# Skip binary files
|
||||
if git diff --cached -- "$f" | grep -qP '^\+.*[\x00-\x08\x0b-\x1f\x7f-\xff]' 2>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Strip leading '+' from diff hunk line
|
||||
content="${line:1}"
|
||||
|
||||
# Skip safe/allowlisted lines
|
||||
skip=0
|
||||
for safe in "${SAFE_PATTERNS[@]}"; do
|
||||
if echo "$content" | grep -qE -e "$safe"; then
|
||||
skip=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
[[ $skip -eq 1 ]] && continue
|
||||
|
||||
# Check against secret patterns
|
||||
for pattern in "${SECRET_PATTERNS[@]}"; do
|
||||
if echo "$content" | grep -qiE -e "$pattern"; then
|
||||
# Redact the actual value in the error message
|
||||
redacted=$(echo "$content" | sed -E 's/([=:][[:space:]]*["\x27]?)[A-Za-z0-9+\/!@#$%^&*]{8,}/\1**REDACTED**/g')
|
||||
errors+=("SECRET in $f: ${redacted:0:120}")
|
||||
FAIL=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
done < <(git diff --cached -- "$f" | grep -E '^\+[^+]')
|
||||
done
|
||||
|
||||
# --- Report ---
|
||||
if [[ ${FAIL} -eq 1 ]]; then
|
||||
echo "" >&2
|
||||
echo "pre-commit: potential secrets detected — commit blocked" >&2
|
||||
echo "" >&2
|
||||
for e in "${errors[@]}"; do
|
||||
echo " ✗ $e" >&2
|
||||
done
|
||||
echo "" >&2
|
||||
echo " To bypass (only if certain this is a false positive):" >&2
|
||||
echo " git commit --no-verify" >&2
|
||||
echo "" >&2
|
||||
exit 1
|
||||
fi
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run once after cloning: ./scripts/install-hooks.sh
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
HOOKS_SRC="$REPO_ROOT/scripts/hooks"
|
||||
HOOKS_DST="$REPO_ROOT/.git/hooks"
|
||||
|
||||
for hook in "$HOOKS_SRC"/*; do
|
||||
name=$(basename "$hook")
|
||||
cp "$hook" "$HOOKS_DST/$name"
|
||||
chmod +x "$HOOKS_DST/$name"
|
||||
echo "Installed: .git/hooks/$name"
|
||||
done
|
||||
Reference in New Issue
Block a user