#!/usr/bin/env bash
# lnbits-sensei shared pre-commit hook.
#
# Wired into every repo under your `core.hooksPath` (set by
# modules/dev-env/config.nix when `devEnv.gitHooks.enable = true`).
# Refuses to commit obvious secrets and unencrypted sops files.
#
# False positives:
#   - Add `# pragma: allowlist secret` on/above the offending line, or
#   - Wrap a block with `# pragma: allowlist secret start` ... end, or
#   - Bypass with `git commit --no-verify` (last resort).

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'
    # The hook script itself contains the FORBIDDEN_PATTERNS as
    # literals — scanning it would always self-trigger.
    'modules/dev-env/scripts/git-hooks/*'
)

# 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
#      (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
