| 1 | |
| 2 | set -euo pipefail |
| 3 | |
| 4 | MODE="tree" |
| 5 | if [[ "${1:-}" == "--staged" ]]; then |
| 6 | MODE="staged" |
| 7 | fi |
| 8 | |
| 9 | PATTERNS=( |
| 10 | "rfc1918-192-168:\\b192\\.168\\.[0-9]{1,3}\\.[0-9]{1,3}\\b" |
| 11 | "rfc1918-10-dot:\\b10\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\b" |
| 12 | "rfc1918-172-16:\\b172\\.(1[6-9]|2[0-9]|3[0-1])\\.[0-9]{1,3}\\.[0-9]{1,3}\\b" |
| 13 | "workspace-path:/shared/projects(/|\\b)" |
| 14 | "container-id:\\bCT [0-9]{3}\\b" |
| 15 | "homelab-node:\\bpve-gpu\\b" |
| 16 | "windows-desktop-path:\\bP:[/\\\\]" |
| 17 | "github-pat:\\bghp_[A-Za-z0-9]{30,}\\b" |
| 18 | "openai-key:\\bsk-[A-Za-z0-9]{20,}\\b" |
| 19 | "slack-bot-token:\\bxoxb-[A-Za-z0-9-]{20,}\\b" |
| 20 | "private-ssh-pem:-----BEGIN (OPENSSH|RSA|EC|DSA|PGP) PRIVATE KEY-----" |
| 21 | ) |
| 22 | |
| 23 | EXEMPT_PATHS=( |
| 24 | "scripts/opsec-scan.sh" |
| 25 | ".github/workflows/opsec.yml" |
| 26 | "CHANGELOG.md" |
| 27 | "docs/SIEM.md" |
| 28 | ) |
| 29 | |
| 30 | is_exempt() { |
| 31 | local p="$1" |
| 32 | for e in "${EXEMPT_PATHS[@]}"; do |
| 33 | [[ "$p" == "$e" ]] && return 0 |
| 34 | done |
| 35 | return 1 |
| 36 | } |
| 37 | |
| 38 | scan_blob() { |
| 39 | local path="$1" |
| 40 | local content="$2" |
| 41 | local fail=0 |
| 42 | for entry in "${PATTERNS[@]}"; do |
| 43 | local label="${entry%%:*}" |
| 44 | local rx="${entry#*:}" |
| 45 | local hits |
| 46 | hits=$(printf '%s' "$content" | grep -nE -e "$rx" 2>/dev/null || true) |
| 47 | if [[ -n "$hits" ]]; then |
| 48 | echo "[opsec] $path: $label" |
| 49 | echo "$hits" | sed 's/^/ /' |
| 50 | fail=1 |
| 51 | fi |
| 52 | done |
| 53 | return $fail |
| 54 | } |
| 55 | |
| 56 | overall=0 |
| 57 | |
| 58 | if [[ "$MODE" == "staged" ]]; then |
| 59 | while IFS= read -r -d '' path; do |
| 60 | is_exempt "$path" && continue |
| 61 | [[ -f "$path" ]] || continue |
| 62 | added=$(git diff --cached -U0 -- "$path" | grep -E '^\+' | grep -vE '^\+\+\+' || true) |
| 63 | [[ -z "$added" ]] && continue |
| 64 | if ! scan_blob "$path" "$added"; then overall=1; fi |
| 65 | done < <(git diff --cached --name-only -z --diff-filter=AM) |
| 66 | else |
| 67 | while IFS= read -r -d '' path; do |
| 68 | is_exempt "$path" && continue |
| 69 | if ! grep -Iq . "$path" 2>/dev/null; then continue; fi |
| 70 | if ! scan_blob "$path" "$(cat "$path")"; then overall=1; fi |
| 71 | done < <(git ls-files -z) |
| 72 | fi |
| 73 | |
| 74 | if [[ $overall -ne 0 ]]; then |
| 75 | echo "" |
| 76 | echo "[opsec] one or more patterns matched. Redact and re-run." |
| 77 | exit 1 |
| 78 | fi |
| 79 | |
| 80 | echo "[opsec] clean." |