Initial version: npm-scanner image, filters, entrypoint, watcher, testbed

- Image based on crazymax/fail2ban:1.1.1 with inotify-tools
- Filters npm-scanner and npm-wplogin, tested with fail2ban-regex in the build
- jail.local generated from .env, optional npm-wplogin and recidive
- Watcher reloads the jails when NPM creates a new proxy host log
- check command, docs, testbed for LXC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schiwagoaandClaude Opus 5.5 committed 2026-10-25 20:41:08 +01:00
commit d7dcd9d6b1
29 files changed
+1163

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# npm-scanner – automated testbed checks (run as root next to docker-compose.yaml)
#
# ./check.sh prepare create the simulated NPM log directories (before `docker compose up`)
# ./check.sh run all checks against the running testbed
# ./check.sh cleanup unban the test addresses
#
# Test addresses are from the documentation ranges (RFC 5737) and never route anywhere.
set -uo pipefail
cd "$(dirname "$0")"
C=npm-scanner-testbed
F=./fake-npm-logs.sh
pass=0 fail=0
ok() { echo " ok $*"; pass=$((pass + 1)); }
bad() { echo " FAIL $*"; fail=$((fail + 1)); }
f2b() { docker exec "$C" fail2ban-client "$@"; }
banned() { f2b get "$1" banip 2>/dev/null | tr ' ' '\n' | grep -qx "$2"; }
wait_ban() { # jail ip [seconds]
for _ in $(seq 1 "${3:-15}"); do banned "$1" "$2" && return 0; sleep 1; done
return 1
}
if [[ "${1:-}" == "prepare" ]]; then
mkdir -p npm-logs/log npm-logs/logs data
touch npm-logs/log/proxy-host-1_access.log npm-logs/log/default-host_access.log
echo "prepared: npm-logs/log, npm-logs/logs, data"
exit 0
fi
if [[ "${1:-}" == "cleanup" ]]; then
for ip in 198.51.100.77 198.51.100.78 198.51.100.79 198.51.100.80 192.0.2.10 198.51.100.81 198.51.100.82; do
f2b unban "$ip" >/dev/null 2>&1 || true
done
echo "test addresses unbanned"
exit 0
fi
echo "== 1. container"
state=$(docker inspect -f '{{.State.Health.Status}}' "$C" 2>/dev/null)
[[ "$state" == healthy ]] && ok "health: $state" || bad "health: ${state:-not running}"
jails=$(f2b status | sed -n 's/.*Jail list:\s*//p')
[[ "$jails" == *npm-scanner* ]] && ok "jails: $jails" || bad "npm-scanner jail missing ($jails)"
[[ "$jails" == *npm-wplogin* ]] && ok "npm-wplogin active" || bad "npm-wplogin missing (ENABLE_WPLOGIN=true?)"
docker exec "$C" test -f /usr/share/npm-scanner/filter-tests.passed && ok "filter tests passed in build" || bad "filter test marker missing"
echo "== 2. scanner ban (log/, host 1)"
$F scan 198.51.100.77 3 1 log >/dev/null
wait_ban npm-scanner 198.51.100.77 && ok "198.51.100.77 banned" || bad "198.51.100.77 not banned"
iptables -S DOCKER-USER | grep -q -- '-j f2b-npm-scan' && ok "DOCKER-USER jumps to f2b-npm-scan" || bad "no jump in DOCKER-USER"
iptables -S f2b-npm-scan 2>/dev/null | grep -q '198.51.100.77' && ok "rule for 198.51.100.77 on the host" || bad "no host rule for 198.51.100.77"
echo "== 3. new host in second directory (logs/, host 40) -> watcher"
$F newhost 40 logs >/dev/null
sleep 6
f2b get npm-scanner logpath | grep -q '/logs/npm2/proxy-host-40_access.log' && ok "host 40 watched after reload" || bad "host 40 not watched"
docker logs "$C" 2>&1 | grep -q 'proxy-host-40_access.log – npm-scanner reloaded' && ok "watcher logged the reload" || bad "no watcher message"
banned npm-scanner 198.51.100.77 && ok "ban survived the reload" || bad "ban lost after reload"
$F scan 198.51.100.78 3 40 logs >/dev/null
wait_ban npm-scanner 198.51.100.78 && ok "198.51.100.78 banned from host 40" || bad "198.51.100.78 not banned"
echo "== 4. default host 444"
$F fallback 198.51.100.79 3 >/dev/null
wait_ban npm-scanner 198.51.100.79 && ok "198.51.100.79 banned (444)" || bad "198.51.100.79 not banned"
echo "== 5. wp-login brute force"
$F wplogin 198.51.100.80 5 1 log >/dev/null
wait_ban npm-wplogin 198.51.100.80 && ok "198.51.100.80 banned by npm-wplogin" || bad "198.51.100.80 not banned"
echo "== 6. must NOT ban"
$F scan 192.0.2.10 5 1 log >/dev/null
$F harmless 198.51.100.81 10 1 log >/dev/null
$F scan 198.51.100.82 2 1 log >/dev/null
sleep 5
banned npm-scanner 192.0.2.10 && bad "whitelisted 192.0.2.10 banned" || ok "whitelisted 192.0.2.10 not banned"
banned npm-scanner 198.51.100.81 && bad "harmless 198.51.100.81 banned" || ok "harmless 404/200 not banned"
banned npm-scanner 198.51.100.82 && bad "198.51.100.82 banned after 2 hits" || ok "2 hits below maxretry"
echo "== 7. restart keeps bans"
docker restart "$C" >/dev/null
for _ in $(seq 1 30); do f2b ping >/dev/null 2>&1 && break; sleep 1; done
sleep 3
banned npm-scanner 198.51.100.77 && ok "198.51.100.77 restored after restart" || bad "bans lost after restart"
iptables -S f2b-npm-scan 2>/dev/null | grep -q '198.51.100.77' && ok "host rule restored" || bad "host rule missing after restart"
echo "== 8. stop removes the rules"
docker stop "$C" >/dev/null
if iptables -S 2>/dev/null | grep -q 'f2b-npm-'; then bad "f2b-npm-* rules left after stop"; else ok "no f2b-npm-* rules after stop"; fi
docker start "$C" >/dev/null
for _ in $(seq 1 30); do f2b ping >/dev/null 2>&1 && break; sleep 1; done
sleep 3
banned npm-scanner 198.51.100.77 && ok "bans back after start" || bad "bans missing after start"
echo
echo "result: $pass ok, $fail failed"
[[ $fail -eq 0 ]]