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