#!/usr/bin/env bash # secret-scan.sh — Scans for private keys and high-entropy secrets # Usage: bash tools/secret-scan.sh [directory] # Uses .secret-scan-allowlist for false positives (one file path per line) set -e SCAN_DIR="${1:-.}" ALLOWLIST=".secret-scan-allowlist" FINDINGS=0 # Build find exclusions EXCLUDES=(-not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/coverage/*" -not -path "*/dist/*") # Load allowlist ALLOWLIST_PATHS=() if [ -f "$ALLOWLIST" ]; then while IFS= read -r line || [ -n "$line" ]; do [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue ALLOWLIST_PATHS+=("$line") done < "$ALLOWLIST" fi is_allowed() { local file="$1" for allowed in "${ALLOWLIST_PATHS[@]}"; do if [[ "$file" == *"$allowed"* ]]; then return 0 fi done return 1 } echo "Scanning $SCAN_DIR for secrets..." # Scan for private keys while IFS= read -r file; do [ -f "$file" ] || continue is_allowed "$file" && continue if grep -qE '-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----' "$file" 2>/dev/null; then echo "FINDING [private-key]: $file" FINDINGS=$((FINDINGS + 1)) fi done < <(find "$SCAN_DIR" "${EXCLUDES[@]}" -type f) # Scan for high-entropy hex strings (40+ chars) while IFS= read -r file; do [ -f "$file" ] || continue is_allowed "$file" && continue if grep -qE '[0-9a-f]{40,}' "$file" 2>/dev/null; then # Filter out common false positives (git SHAs in lock files, etc.) BASENAME=$(basename "$file") if [[ "$BASENAME" != "package-lock.json" && "$BASENAME" != "*.lock" ]]; then MATCHES=$(grep -oE '[0-9a-f]{40,}' "$file" 2>/dev/null || true) if [ -n "$MATCHES" ]; then echo "FINDING [high-entropy-hex]: $file" FINDINGS=$((FINDINGS + 1)) fi fi fi done < <(find "$SCAN_DIR" "${EXCLUDES[@]}" -type f -not -name "package-lock.json" -not -name "*.lock") if [ "$FINDINGS" -gt 0 ]; then echo "secret-scan: $FINDINGS finding(s) — FAIL" exit 1 else echo "secret-scan: clean — PASS" exit 0 fi