- 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>
100 lines
4.6 KiB
Bash
100 lines
4.6 KiB
Bash
#!/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 ]]
|