#!/usr/bin/env bash # # THE REMOTE HALF OF THE OUT OF REPO SWEEP. Staged onto a host and run there. # # IT NEVER PRINTS A MATCHED VALUE. Every finding is reported as KINDPATHLINEPATTERN_NAME and # nothing else. grep is invoked with -c and -n only, never in a mode that echoes the line, because a sweep that # prints what it finds converts a stored secret into a logged one. That is not a hypothetical: this build has # already disclosed a live credential into a log this week through flag usage output, finding D14. # # Usage: remote-scan.sh # kind script the file must REFERENCE credentials, never embed them # kind unit same, plus a systemd specific check for inline Environment= secrets # kind mode MODE ONLY. These files are the credential store and are supposed to contain values. set -uo pipefail KIND="${1:-}" GLOB="${2:-}" [ -z "$KIND" ] && { echo "usage: remote-scan.sh " >&2; exit 2; } # The same shapes the repository sweep looks for, kept deliberately in step with it. A DSN carrying a password, # an assignment whose right hand side looks like a secret, an inline sshpass, and a bearer or api key literal. PATTERNS=( "dsn_with_password|://[A-Za-z0-9_]+:[^@\"'[:space:]]{8,}@" "password_assignment|(PASS|PASSWD|PASSWORD|SECRET|TOKEN|PEPPER)[A-Za-z_]*[=:][[:space:]]*[\"']?[A-Za-z0-9!@#%^&*_+.-]{10,}" "inline_sshpass|sshpass[[:space:]]+-p[[:space:]]+[^[:space:]$]{6,}" "api_key_literal|(sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{12,}|ghp_[A-Za-z0-9]{20,})" ) # Placeholders are not secrets, and the list is short on purpose: every entry is a hole. ALLOW='REDACTED|EXAMPLE|example\.(com|invalid|test)|PLACEHOLDER|CHANGE_?ME|your-|\$\{|\$\(|xxxxx|://user:password@|GOMEMLIMIT|MM_[A-Z_]*URL"?$|=\$' findings=0 expand() { compgen -G "$GLOB" 2>/dev/null || true; } case "$KIND" in mode) for f in $(expand); do [ -f "$f" ] || continue m=$(stat -c '%a' "$f" 2>/dev/null || stat -f '%Lp' "$f" 2>/dev/null) o=$(stat -c '%U' "$f" 2>/dev/null || stat -f '%Su' "$f" 2>/dev/null) # World or group readable is a finding on a file that holds credentials, whatever it holds. case "$m" in 600|400|0600|0400) : ;; *) echo -e "mode\t$f\t0\tcredential_file_mode_$m"; findings=$((findings+1)) ;; esac [ "$o" = "root" ] || { echo -e "mode\t$f\t0\tcredential_file_owner_$o"; findings=$((findings+1)); } done ;; script|unit) for f in $(expand); do [ -f "$f" ] || continue for entry in "${PATTERNS[@]}"; do name="${entry%%|*}"; rx="${entry#*|}" # -n gives line numbers. The matched text is DISCARDED by cut before anything is printed. while IFS= read -r ln; do [ -z "$ln" ] && continue num="${ln%%:*}" body="${ln#*:}" printf '%s' "$body" | grep -qE "$ALLOW" && continue echo -e "$KIND\t$f\t$num\t$name" findings=$((findings+1)) done < <(grep -nIE "$rx" "$f" 2>/dev/null | head -20) done if [ "$KIND" = "unit" ]; then # A systemd unit should point at an EnvironmentFile, never inline a secret with Environment=. while IFS= read -r ln; do [ -z "$ln" ] && continue num="${ln%%:*}" echo -e "unit\t$f\t$num\tinline_environment_secret" findings=$((findings+1)) done < <(grep -nIE '^[[:space:]]*Environment=[A-Z_]*(PASS|SECRET|TOKEN|KEY|PEPPER|DSN|URL)[A-Z_]*=.+' "$f" 2>/dev/null | grep -viE 'GOMEMLIMIT|GODEBUG' | head -10) fi done ;; *) echo "unknown kind $KIND" >&2; exit 2 ;; esac echo -e "SUMMARY\t$KIND\t$GLOB\t$findings" exit 0