From e888560175174045da487ae0551014936107247c Mon Sep 17 00:00:00 2001 From: Adyrem Date: Fri, 15 May 2026 20:03:43 +0200 Subject: [PATCH] Add pre-commit hook to detect secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/hooks/pre-commit | 103 +++++++++++++++++++++++++++++++++++++++ scripts/install-hooks.sh | 14 ++++++ 2 files changed, 117 insertions(+) create mode 100755 scripts/hooks/pre-commit create mode 100755 scripts/install-hooks.sh diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 0000000..88da490 --- /dev/null +++ b/scripts/hooks/pre-commit @@ -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 diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100755 index 0000000..4e2b4b2 --- /dev/null +++ b/scripts/install-hooks.sh @@ -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