omnixient/modules/dev-env/scripts/git-hooks/pre-commit
2026-06-28 09:59:05 +02:00

138 lines
5 KiB
Bash

#!/usr/bin/env bash
# dev-env shared pre-commit hook.
#
# Wired into every dev-env-managed repo via core.hooksPath. Refuses to
# commit obvious secrets and unencrypted sops files. Body is identical
# to the one in ~/omarchy-dev-env/setup/install-git-hooks.sh — that
# script's per-repo install loop is replaced by core.hooksPath.
#
# False positives: `git commit --no-verify` to bypass.
set -u
# Patterns are assembled from fragments so the literal text in this
# file doesn't match the patterns themselves — otherwise this hook
# would always block commits that touch it.
_PVT='PRI''VATE'
_AWS='AWS_SEC''RET_ACCESS_KEY'
FORBIDDEN_PATTERNS=(
"${_PVT} KEY"
"BEGIN RSA ${_PVT}"
"BEGIN EC ${_PVT}"
"BEGIN OPENSSH ${_PVT}"
'password\s*=\s*["\x27][^"\x27]+'
'secret\s*=\s*["\x27][^"\x27]+'
'api_key\s*=\s*["\x27][^"\x27]+'
'admin_key\s*=\s*["\x27][^"\x27]+'
"${_AWS}"
'POSTGRES_PASSWORD=(?!.*(example|changeme|placeholder))'
)
SKIP_FILES=(
'*.md'
'*.txt'
'secrets-management.md'
# The hook scripts themselves contain the FORBIDDEN_PATTERNS as
# literals — scanning them would always self-trigger.
'modules/dev-env/scripts/git-hooks/*'
# Auth test fixtures: dozens of password="secret1234" placeholders
# across the file — line-level markers would be ridiculous; this is
# exactly the kind of file SKIP_FILES exists for.
'tests/api/test_auth.py'
)
# Line-level allowlist. A line that matches a FORBIDDEN_PATTERN is treated
# as a false positive in any of these cases:
# 1. The line itself contains the marker.
# 2. The line *immediately above* contains the marker (handy for a
# single-line trigger where the marker doesn't fit on the line).
# 3. The line falls inside a "<marker> start" ... "<marker> end" block
# (handy for multi-line clumps — same convention as gitleaks).
# Use sparingly — only on lines that genuinely don't hold a secret (prose
# comments, test fixtures with placeholder values, constant strings).
ALLOWLIST_MARKER='pragma: allowlist secret'
errors=0
for file in $(git diff --cached --name-only --diff-filter=ACM); do
skip=false
for pat in "${SKIP_FILES[@]}"; do
# shellcheck disable=SC2053
if [[ "$file" == $pat ]]; then
skip=true
break
fi
done
[[ "$skip" == true ]] && continue
blob=$(git show ":$file" 2>/dev/null) || continue
# Walk the file once to compute the set of allowlisted line numbers.
# Tracks both "single-line marker" and "<marker> start/end" block state.
declare -A allowlisted=()
in_block=false
prev_was_marker=false
line_num=0
while IFS= read -r line_content; do
line_num=$((line_num + 1))
if [[ "$line_content" == *"$ALLOWLIST_MARKER start"* ]]; then
in_block=true
prev_was_marker=false
continue
fi
if [[ "$line_content" == *"$ALLOWLIST_MARKER end"* ]]; then
in_block=false
prev_was_marker=false
continue
fi
is_marker_line=false
if [[ "$line_content" == *"$ALLOWLIST_MARKER"* ]]; then
is_marker_line=true
fi
if [[ "$in_block" == true \
|| "$is_marker_line" == true \
|| "$prev_was_marker" == true ]]; then
allowlisted[$line_num]=1
fi
prev_was_marker=$is_marker_line
done <<<"$blob"
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
while IFS= read -r match; do
[[ -z "$match" ]] && continue
line_num="${match%%:*}"
[[ -n "${allowlisted[$line_num]:-}" ]] && continue
echo "ERROR: potential secret in $file:$line_num (pattern: $pattern)"
errors=$((errors + 1))
done < <(grep -niE "$pattern" <<<"$blob" || true)
done
unset allowlisted
done
# Unencrypted or malformed sops files.
# Structural check: a real sops YAML always contains both a top-level `sops:`
# block AND a `mac:` field whose value is `ENC[...]`. Either signal alone is
# trivially forgeable; together they're specific to actual sops output.
for file in $(git diff --cached --name-only --diff-filter=ACM | grep -E 'secrets.*\.yaml$' || true); do
blob=$(git show ":$file" 2>/dev/null)
if ! grep -q '^[[:space:]]*sops:' <<<"$blob"; then
echo "ERROR: unencrypted secrets file: $file (no sops metadata block)"
echo " run: sops -e -i $file"
errors=$((errors + 1))
elif ! grep -q '^[[:space:]]*mac: ENC\[' <<<"$blob"; then
echo "ERROR: secrets file has sops block but mac is not encrypted: $file"
echo " file may be tampered or partially decrypted; re-encrypt with sops"
errors=$((errors + 1))
fi
done
if (( errors > 0 )); then
echo ""
echo "commit blocked: $errors potential secret(s) detected"
echo "false positive? add '# $ALLOWLIST_MARKER' on/above the line,"
echo "or wrap a block with '# $ALLOWLIST_MARKER start' ... '# $ALLOWLIST_MARKER end',"
echo "or bypass with: git commit --no-verify"
exit 1
fi
exit 0