From 080073a7d7a2eb8c4b2a922f1905a5b78ec1f127 Mon Sep 17 00:00:00 2001 From: pdm Date: Sat, 18 Apr 2026 08:47:32 +0000 Subject: [PATCH] Add npm sudo config audit script Checks npm prefix ownership, PATH wiring, cache ownership, shell history for sudo npm usage, and n/nvm version manager config. Runs daily at 08:10 via cron and on initial setup.sh run. Co-Authored-By: Claude Sonnet 4.6 --- check-npm-sudo-config.sh | 197 +++++++++++++++++++++++++++++++++++++++ setup.sh | 14 ++- 2 files changed, 209 insertions(+), 2 deletions(-) create mode 100755 check-npm-sudo-config.sh diff --git a/check-npm-sudo-config.sh b/check-npm-sudo-config.sh new file mode 100755 index 0000000..ca5f8c8 --- /dev/null +++ b/check-npm-sudo-config.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# check-npm-sudo-config.sh +# Audits npm configuration on this VM for sudo-related issues and recommends fixes. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=config.sh +source "$SCRIPT_DIR/config.sh" + +send_telegram() { + curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -d chat_id="${TELEGRAM_CHAT_ID}" \ + -d text="$1" \ + -d parse_mode="HTML" > /dev/null || true +} + +HOSTNAME=$(hostname) +DATE=$(date) + +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +BOLD='\033[1m' +RESET='\033[0m' + +ISSUES=0 +WARNINGS=0 + +log() { echo "$*"; } +ok() { printf "${GREEN}✓${RESET} %s\n" "$*"; } +warn() { printf "${YELLOW}⚠${RESET} %s\n" "$*"; (( WARNINGS++ )) || true; } +fail() { printf "${RED}✗${RESET} %s\n" "$*"; (( ISSUES++ )) || true; } +rec() { printf " ${YELLOW}→${RESET} %s\n" "$*"; } +header() { echo ""; echo "=========================================="; echo "$*"; echo "=========================================="; } + +log "==========================================" +log " npm sudo config audit" +log "==========================================" +log "Hostname : $HOSTNAME" +log "Date : $DATE" + +# ── 1. npm present? ─────────────────────────────────────────────────────────── +header "1. npm availability" + +if ! command -v npm &>/dev/null; then + warn "npm not found in PATH — skipping remaining checks" + echo "" + echo "RESULT: 0 issue(s), 1 warning(s)" + exit 0 +fi + +NPM_PATH=$(command -v npm) +ok "npm found: $NPM_PATH" + +# ── 2. npm prefix ───────────────────────────────────────────────────────────── +header "2. npm prefix" + +PREFIX=$(npm config get prefix 2>/dev/null || echo "unknown") +log "Current prefix: $PREFIX" + +if [[ "$PREFIX" == "/usr" || "$PREFIX" == "/usr/local" ]]; then + fail "npm prefix is $PREFIX (system-wide) — global installs require sudo" + rec "npm config set prefix ~/.npm-global" + rec "Add to ~/.profile: export PATH=\"\$HOME/.npm-global/bin:\$PATH\"" +elif [[ "$PREFIX" == "unknown" ]]; then + warn "Could not determine npm prefix" +else + PREFIX_OWNER=$(stat -c "%U" "$PREFIX" 2>/dev/null || echo "unknown") + if [[ "$PREFIX_OWNER" == "root" ]]; then + fail "npm prefix $PREFIX is owned by root — global installs require sudo" + rec "sudo chown -R \$(whoami) $PREFIX" + rec "Or set a user-owned prefix: npm config set prefix ~/.npm-global" + else + ok "npm prefix is $PREFIX (owned by $PREFIX_OWNER)" + fi +fi + +# ── 3. .npmrc ──────────────────────────────────────────────────────────────── +header "3. ~/.npmrc" + +if [[ -f "$HOME/.npmrc" ]]; then + log "$(cat "$HOME/.npmrc")" + NPM_PREFIX_LINE=$(grep "^prefix=" "$HOME/.npmrc" 2>/dev/null || true) + if [[ -n "$NPM_PREFIX_LINE" ]]; then + ok ".npmrc explicitly sets: $NPM_PREFIX_LINE" + else + warn ".npmrc exists but does not pin the prefix" + rec "npm config set prefix ~/.npm-global" + fi +else + warn "No ~/.npmrc — prefix is not pinned to a user directory" + rec "npm config set prefix ~/.npm-global" +fi + +# ── 4. prefix/bin in PATH ───────────────────────────────────────────────────── +header "4. npm prefix bin in PATH" + +if [[ "$PREFIX" != "unknown" ]]; then + PREFIX_BIN="${PREFIX}/bin" + if echo "$PATH" | tr ':' '\n' | grep -qxF "$PREFIX_BIN"; then + ok "$PREFIX_BIN is in PATH" + else + warn "$PREFIX_BIN is NOT in PATH — globally installed binaries won't run" + PROFILE_FILE="$HOME/.profile" + [[ -f "$HOME/.zshrc" ]] && PROFILE_FILE="$HOME/.zshrc" + rec "Add to $PROFILE_FILE: export PATH=\"$PREFIX_BIN:\$PATH\"" + rec "Then reload: source $PROFILE_FILE" + fi +fi + +# ── 5. Root-owned files in npm prefix ──────────────────────────────────────── +header "5. Root-owned files in npm prefix" + +if [[ -d "$PREFIX" ]]; then + ROOT_FILES=$(find "$PREFIX" -maxdepth 3 -user root 2>/dev/null | head -5 || true) + if [[ -n "$ROOT_FILES" ]]; then + fail "Root-owned files found in npm prefix (past sudo npm usage):" + echo "$ROOT_FILES" + rec "sudo chown -R \$(whoami) $PREFIX" + else + ok "No root-owned files in $PREFIX" + fi +else + ok "npm prefix directory does not exist yet (no global installs made)" +fi + +# ── 6. sudo npm in shell history ───────────────────────────────────────────── +header "6. Shell history — sudo npm usage" + +SUDO_NPM_FOUND=false +for hfile in "$HOME/.bash_history" "$HOME/.zsh_history"; do + if [[ -f "$hfile" ]]; then + HITS=$(grep -c "sudo npm" "$hfile" 2>/dev/null || true) + if [[ "$HITS" -gt 0 ]]; then + warn "Found $HITS occurrence(s) of \"sudo npm\" in $hfile" + SUDO_NPM_FOUND=true + fi + fi +done +$SUDO_NPM_FOUND || ok "No \"sudo npm\" in shell history" + +# ── 7. npm cache ownership ─────────────────────────────────────────────────── +header "7. npm cache ownership" + +CACHE_DIR=$(npm config get cache 2>/dev/null || echo "$HOME/.npm") +if [[ -d "$CACHE_DIR" ]]; then + ROOT_CACHE=$(find "$CACHE_DIR" -maxdepth 2 -user root 2>/dev/null | head -3 || true) + if [[ -n "$ROOT_CACHE" ]]; then + fail "Root-owned files in npm cache ($CACHE_DIR) — will cause EACCES errors" + rec "sudo chown -R \$(whoami) $CACHE_DIR" + else + ok "npm cache ($CACHE_DIR) is user-owned" + fi +else + ok "npm cache directory does not exist yet" +fi + +# ── 8. Node version manager ─────────────────────────────────────────────────── +header "8. Node version manager" + +if command -v n &>/dev/null; then + N_PREFIX_VAL="${N_PREFIX:-}" + if [[ -z "$N_PREFIX_VAL" ]]; then + warn "n is installed but N_PREFIX is not set — n defaults to /usr/local (requires sudo)" + rec "Add to ~/.profile: export N_PREFIX=\$HOME/.n" + rec "Add to ~/.profile: export PATH=\$PATH:\$N_PREFIX/bin" + else + ok "n is installed, N_PREFIX=$N_PREFIX_VAL" + fi +elif [[ -s "$HOME/.nvm/nvm.sh" ]] || command -v nvm &>/dev/null 2>&1; then + ok "nvm is managing Node (sudo-free by design)" +elif command -v fnm &>/dev/null; then + ok "fnm is managing Node (sudo-free by design)" +else + ok "No Node version manager detected" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +header "SUMMARY" +log "Scan completed at: $(date)" +log "" + +if [[ $ISSUES -gt 0 ]]; then + printf "${RED}✗ %d issue(s) and %d warning(s) — see recommendations above${RESET}\n" "$ISSUES" "$WARNINGS" + send_telegram "⚠️ npm sudo config issues +Host: ${HOSTNAME} +Issues: ${ISSUES} | Warnings: ${WARNINGS} +Run manually: bash check-npm-sudo-config.sh" + exit 1 +elif [[ $WARNINGS -gt 0 ]]; then + printf "${YELLOW}⚠ Clean but %d warning(s) — see recommendations above${RESET}\n" "$WARNINGS" + exit 0 +else + printf "${GREEN}✓ npm is correctly configured on %s${RESET}\n" "$HOSTNAME" + exit 0 +fi diff --git a/setup.sh b/setup.sh index 2684c27..3540678 100755 --- a/setup.sh +++ b/setup.sh @@ -31,6 +31,7 @@ fi # ── Make scripts executable ──────────────────────────────────────────────────── chmod +x "$SCRIPT_DIR/npm-security-check.sh" chmod +x "$SCRIPT_DIR/check-nextjs-rce.sh" +chmod +x "$SCRIPT_DIR/check-npm-sudo-config.sh" # ── Create logs directory ────────────────────────────────────────────────────── mkdir -p "$SCRIPT_DIR/logs" @@ -38,7 +39,8 @@ mkdir -p "$SCRIPT_DIR/logs" # ── Cron jobs ────────────────────────────────────────────────────────────────── CRON_1="0 8 * * * $SCRIPT_DIR/npm-security-check.sh >> $SCRIPT_DIR/logs/npm-security-check-\$(date +\%Y\%m\%d).log 2>&1" CRON_2="5 8 * * * $SCRIPT_DIR/check-nextjs-rce.sh >> $SCRIPT_DIR/logs/check-nextjs-rce-\$(date +\%Y\%m\%d).log 2>&1" -CRON_3="0 9 * * * find $SCRIPT_DIR/logs -name '*.log' -mtime +60 -delete" +CRON_3="10 8 * * * $SCRIPT_DIR/check-npm-sudo-config.sh >> $SCRIPT_DIR/logs/check-npm-sudo-config-\$(date +\%Y\%m\%d).log 2>&1" +CRON_4="0 9 * * * find $SCRIPT_DIR/logs -name '*.log' -mtime +60 -delete" EXISTING=$(crontab -l 2>/dev/null || true) @@ -56,10 +58,17 @@ else echo "Cron job registered: check-nextjs-rce.sh daily at 08:05." fi +if echo "$EXISTING" | grep -qF "check-npm-sudo-config.sh"; then + echo "Cron job for check-npm-sudo-config.sh already registered — skipping." +else + (crontab -l 2>/dev/null; echo "$CRON_3") | crontab - + echo "Cron job registered: check-npm-sudo-config.sh daily at 08:10." +fi + if echo "$EXISTING" | grep -qF "logs -name '*.log'"; then echo "Log cleanup cron already registered — skipping." else - (crontab -l 2>/dev/null; echo "$CRON_3") | crontab - + (crontab -l 2>/dev/null; echo "$CRON_4") | crontab - echo "Cron job registered: log cleanup daily at 09:00 (60 day retention)." fi @@ -90,5 +99,6 @@ echo "" echo "Running initial security scan..." bash "$SCRIPT_DIR/npm-security-check.sh" >> "$SCRIPT_DIR/logs/npm-security-check-$(date +%Y%m%d).log" 2>&1 && echo "npm-security-check: done." || echo "npm-security-check: issues found — check Telegram." bash "$SCRIPT_DIR/check-nextjs-rce.sh" >> "$SCRIPT_DIR/logs/check-nextjs-rce-$(date +%Y%m%d).log" 2>&1 && echo "check-nextjs-rce: done." || echo "check-nextjs-rce: issues found — check Telegram." +bash "$SCRIPT_DIR/check-npm-sudo-config.sh" >> "$SCRIPT_DIR/logs/check-npm-sudo-config-$(date +%Y%m%d).log" 2>&1 && echo "check-npm-sudo-config: done." || echo "check-npm-sudo-config: issues found — check Telegram." echo "" echo "Initial scan complete. Check Telegram for any alerts."