Initial commit: consolidate security scripts

Bring in check-nextjs-rce.sh and README-scanner.md from existing Gitea repo,
plus npm-security-check.sh from local bin/security.
This commit is contained in:
pdmarf
2026-04-17 21:51:27 +01:00
commit 93b02d0124
3 changed files with 461 additions and 0 deletions

267
npm-security-check.sh Executable file
View File

@@ -0,0 +1,267 @@
#!/usr/bin/env bash
# npm-security-check.sh
# Scans for NPM/Node.js malware indicators on this VM.
set -euo pipefail
HOSTNAME=$(hostname)
DATE=$(date)
LOGFILE="${1:-npm_security_check_${HOSTNAME}_$(date +%Y%m%d_%H%M%S).log}"
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BOLD='\033[1m'
RESET='\033[0m'
ISSUES=0
WARNINGS=0
log() { echo "$*" | tee -a "$LOGFILE"; }
header() { log ""; log "=========================================="; log "$*"; log "=========================================="; }
ok() { log "$(printf "${GREEN}${RESET} %s" "$*")"; }
warn() { log "$(printf "${YELLOW}${RESET} %s" "$*")"; (( WARNINGS++ )) || true; }
fail() { log "$(printf "${RED}${RESET} %s" "$*")"; (( ISSUES++ )) || true; }
# ── Header ────────────────────────────────────────────────────────────────────
log "=========================================="
log " NPM / Node.js Security Check"
log "=========================================="
log "Hostname : $HOSTNAME"
log "Date : $DATE"
log "Log file : $LOGFILE"
# ── 1. Global npm packages ────────────────────────────────────────────────────
header "1. Global npm packages"
KNOWN_GOOD_GLOBALS="npm corepack"
if command -v npm &>/dev/null; then
GLOBALS=$(npm list -g --depth=0 2>/dev/null | tail -n +2 | sed 's/.*── //')
log "$GLOBALS"
# Flag anything that looks like a typosquat or known-bad package
SUSPICIOUS_PATTERNS="(plain-crypto-js|axios-[0-9]|node-fetch-[0-9]{3}|colors-js|event-stream|flatmap-stream|ua-parser-js|coa@|rc@[0-9]|nodemailer-[0-9]{3})"
HITS=$(echo "$GLOBALS" | grep -E "$SUSPICIOUS_PATTERNS" || true)
if [[ -n "$HITS" ]]; then
fail "Suspicious global package(s) found:"
log "$HITS"
else
ok "No suspicious global packages"
fi
else
warn "npm not found in PATH"
fi
# ── 2. Known malicious packages in lock files ─────────────────────────────────
header "2. Malicious package names in lock files"
BAD_PKGS=(
"plain-crypto-js"
"axios-proxy"
"node-colors"
"colors-js"
"event-stream"
"flatmap-stream"
"ua-parser-js"
"getcookies"
)
LOCKFILES=$(find / -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" \
2>/dev/null | grep -v node_modules | grep -v "\.vscode-server" | grep -v "\.cache") || true
if [[ -z "$LOCKFILES" ]]; then
warn "No lock files found to scan"
else
COUNT=$(echo "$LOCKFILES" | wc -l)
log "Scanning $COUNT lock file(s)..."
for pkg in "${BAD_PKGS[@]}"; do
MATCHES=$(echo "$LOCKFILES" | xargs grep -l "\"$pkg\"" 2>/dev/null || true)
if [[ -n "$MATCHES" ]]; then
fail "Found '$pkg' in: $MATCHES"
fi
done
ok "No known-malicious package names found"
fi
# ── 3. Node processes and their origin ────────────────────────────────────────
header "3. Running Node/Next.js processes"
NODE_PROCS=$(ps aux | grep -E "[n]ode|[n]ext-server|[n]pm|[n]px|[p]npm" | grep -v grep || true)
if [[ -z "$NODE_PROCS" ]]; then
ok "No Node.js processes running"
else
log "$NODE_PROCS"
# Check for processes running as root outside of Docker containers
ROOT_PROCS=$(echo "$NODE_PROCS" | awk '$1 == "root" {print}' || true)
if [[ -n "$ROOT_PROCS" ]]; then
# Check if each root process is inside a Docker cgroup (normal)
while IFS= read -r proc; do
PID=$(echo "$proc" | awk '{print $2}')
CGROUP=$(cat /proc/"$PID"/cgroup 2>/dev/null | grep -c "docker" || true)
if [[ "$CGROUP" -gt 0 ]]; then
ok "PID $PID runs as root but is inside a Docker container (normal)"
else
warn "PID $PID is a root Node process outside Docker — review manually"
log " Command: $(cat /proc/"$PID"/cmdline 2>/dev/null | tr '\0' ' ' || echo 'unreadable')"
fi
done <<< "$ROOT_PROCS"
else
ok "No root-owned Node processes outside Docker"
fi
fi
# ── 4. Outbound network connections from Node processes ───────────────────────
header "4. Node process network connections"
if command -v lsof &>/dev/null; then
NODE_CONNS=$(lsof -i TCP -a -c node -a -s TCP:ESTABLISHED 2>/dev/null || true)
if [[ -n "$NODE_CONNS" ]]; then
log "$NODE_CONNS"
# Flag connections to non-443/80 ports on public IPs
UNUSUAL=$(echo "$NODE_CONNS" | awk '!/localhost|127\.0\.0|192\.168|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|:443|:80|:22/ && /ESTABLISHED/' || true)
if [[ -n "$UNUSUAL" ]]; then
warn "Unusual outbound Node connections (non-standard ports or IPs):"
log "$UNUSUAL"
else
ok "Node connections look normal (443/80 or private IPs)"
fi
else
ok "No established TCP connections from node processes"
fi
else
# Fallback to ss
ALL_CONNS=$(ss -tnp 2>/dev/null | grep "node\|npm" || true)
if [[ -n "$ALL_CONNS" ]]; then
log "$ALL_CONNS"
else
ok "No Node network connections found"
fi
fi
# ── 5. Known C2 indicators ────────────────────────────────────────────────────
header "5. Known C2 / malware indicators"
# From previous axios supply-chain attack (Apr 2025 npm incident)
C2_IPS=("142.11.206.73" "185.220.101" "194.165.16")
C2_DOMAINS=("sfrclak.com" "discordapp.com/api/webhooks" "ngrok.io")
ACTIVE_CONNS=$(ss -tn 2>/dev/null || netstat -tn 2>/dev/null || true)
FOUND_C2=false
for ip in "${C2_IPS[@]}"; do
if echo "$ACTIVE_CONNS" | grep -q "$ip"; then
fail "Active connection to known C2 IP: $ip"
FOUND_C2=true
fi
done
for domain in "${C2_DOMAINS[@]}"; do
if echo "$ACTIVE_CONNS" | grep -q "$domain"; then
fail "Active connection to suspicious domain: $domain"
FOUND_C2=true
fi
done
if ! $FOUND_C2; then
ok "No connections to known C2 infrastructure"
fi
# ── 6. Suspicious processes (miners, RATs) ────────────────────────────────────
header "6. Suspicious process names"
SUSPICIOUS_PROCS="(xmrig|minerd|cpuminer|kdevtmpfsi|kinsing|/tmp/[a-z0-9]{8,}|/dev/shm/)"
# Match processes whose executable path (field 11) starts with ../ — not args containing ../
HITS=$(ps aux | grep -E "$SUSPICIOUS_PROCS" | grep -v grep || true)
DOTDOT=$(ps aux | grep -v grep | awk '$11 ~ /^\.\.\// {print}' || true)
[[ -n "$DOTDOT" ]] && HITS="$HITS
$DOTDOT"
if [[ -n "$HITS" ]]; then
fail "Suspicious processes detected:"
log "$HITS"
else
ok "No suspicious process names"
fi
# ── 7. Suspicious files in temp directories ───────────────────────────────────
header "7. Suspicious files in /tmp and /dev/shm"
for dir in /tmp /dev/shm /var/tmp; do
EXEC_FILES=$(find "$dir" -type f -executable 2>/dev/null | head -20 || true)
JS_FILES=$(find "$dir" -name "*.js" -o -name "*.mjs" 2>/dev/null | head -10 || true)
if [[ -n "$EXEC_FILES" ]]; then
warn "Executable files in $dir:"
log "$EXEC_FILES"
fi
if [[ -n "$JS_FILES" ]]; then
warn "JS files in $dir:"
log "$JS_FILES"
fi
done
ok "Temp directory scan complete"
# ── 8. npm configuration ──────────────────────────────────────────────────────
header "8. npm configuration"
if [[ -f "$HOME/.npmrc" ]]; then
log "$(cat "$HOME/.npmrc")"
# Check for non-official registry
ALT_REGISTRY=$(grep -v "^#" "$HOME/.npmrc" | grep "registry" | grep -v "registry.npmjs.org" || true)
if [[ -n "$ALT_REGISTRY" ]]; then
warn "Non-official npm registry configured: $ALT_REGISTRY"
else
ok ".npmrc uses official registry"
fi
else
warn "No .npmrc found (registry defaults to npmjs.org — acceptable)"
fi
# ── 9. Docker containers quick review ────────────────────────────────────────
header "9. Docker containers"
if command -v docker &>/dev/null && docker ps &>/dev/null 2>&1; then
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null | tee -a "$LOGFILE"
# Flag containers with no named image (just an image ID)
UNNAMED=$(docker ps --format "{{.Names}} {{.Image}}" 2>/dev/null | awk '$2 ~ /^[0-9a-f]{12}$/' || true)
if [[ -n "$UNNAMED" ]]; then
warn "Container(s) using unnamed image IDs (verify these are known):"
log "$UNNAMED"
else
ok "All containers use named images"
fi
else
warn "Docker not available or not accessible"
fi
# ── 10. Bash history spot-check ───────────────────────────────────────────────
header "10. Bash history — suspicious patterns"
HIST_FILE="${HISTFILE:-$HOME/.bash_history}"
if [[ -f "$HIST_FILE" ]]; then
# Look for obfuscated execution patterns (not internal curl to known Tailscale IPs)
SUSPICIOUS_HIST=$(grep -E "(eval\s*\$|base64\s*-d|python.*exec|perl.*eval|/dev/tcp/|bash.*<\(curl.*[^1][^0][^0]\.)" \
"$HIST_FILE" 2>/dev/null | grep -vE "100\.[0-9]+\.[0-9]+\.[0-9]+" | tail -20 || true)
if [[ -n "$SUSPICIOUS_HIST" ]]; then
warn "Potentially suspicious history entries:"
log "$SUSPICIOUS_HIST"
else
ok "No obviously suspicious history entries"
fi
else
warn "Bash history file not found at $HIST_FILE"
fi
# ── Summary ────────────────────────────────────────────────────────────────────
header "SUMMARY"
log "Scan completed at: $(date)"
log "Results saved to : $LOGFILE"
log ""
if [[ $ISSUES -gt 0 ]]; then
log "$(printf "${RED}✗ %d issue(s) found — review output above${RESET}" "$ISSUES")"
exit 1
elif [[ $WARNINGS -gt 0 ]]; then
log "$(printf "${YELLOW}⚠ Clean but %d warning(s) — review output above${RESET}" "$WARNINGS")"
exit 0
else
log "$(printf "${GREEN}✓ All checks passed — no indicators of compromise${RESET}")"
exit 0
fi