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:
commit
d7dcd9d6b1
29 files changed
+1163
No files matched your search
@@ -0,0 +1,16 @@
|
||||
# Testbed settings – copy to .env
|
||||
TZ=Europe/Vienna
|
||||
F2B_LOG_LEVEL=INFO
|
||||
|
||||
# 192.0.2.10 is used by check.sh as "whitelisted" address.
|
||||
# Do NOT add the workstation here if you want to run the manual block test.
|
||||
WHITELIST=192.0.2.10
|
||||
|
||||
F2B_CHAIN=DOCKER-USER
|
||||
ENABLE_WPLOGIN=true
|
||||
ENABLE_RECIDIVE=false
|
||||
ENABLE_NEWLOG_WATCH=true
|
||||
|
||||
# Published port of the target container
|
||||
TARGET_BIND=0.0.0.0
|
||||
TARGET_PORT=8081
|
||||
@@ -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 ]]
|
||||
@@ -0,0 +1,51 @@
|
||||
# npm-scanner – testbed
|
||||
#
|
||||
# npm-scanner from the local source plus a small web server as target.
|
||||
# NPM itself is simulated: fake-npm-logs.sh writes log lines in NPM format into
|
||||
# ./npm-logs/log and ./npm-logs/logs (layout of jlesage/nginx-proxy-manager).
|
||||
#
|
||||
# cp .env.example .env && nano .env
|
||||
# docker compose up -d --build
|
||||
# ./check.sh
|
||||
|
||||
name: npm-scanner-testbed
|
||||
|
||||
services:
|
||||
npm-scanner:
|
||||
build: ../..
|
||||
image: npm-scanner:testbed
|
||||
container_name: npm-scanner-testbed
|
||||
restart: "no"
|
||||
init: true
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
env_file: .env
|
||||
environment:
|
||||
NEWLOG_SETTLE_SECONDS: "2"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./npm-logs/log:/logs/npm1:ro
|
||||
- ./npm-logs/logs:/logs/npm2:ro
|
||||
|
||||
# Target behind a published port – traffic to it passes DOCKER-USER.
|
||||
target:
|
||||
image: traefik/whoami:v1.12.0
|
||||
container_name: npm-scanner-target
|
||||
restart: "no"
|
||||
ports:
|
||||
- "${TARGET_BIND:-0.0.0.0}:${TARGET_PORT:-8081}:80"
|
||||
networks:
|
||||
npm-scanner-target.dockernetwork.local:
|
||||
ipv4_address: 172.16.52.10
|
||||
|
||||
networks:
|
||||
npm-scanner-target.dockernetwork.local:
|
||||
name: npm-scanner-target.dockernetwork.local
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.16.52.0/24
|
||||
gateway: 172.16.52.1
|
||||
ip_range: 172.16.52.128/25
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# Writes log lines in Nginx Proxy Manager format, like NPM would.
|
||||
#
|
||||
# ./fake-npm-logs.sh scan <ip> [count] [host-id] [log|logs] 4xx on scanner paths
|
||||
# ./fake-npm-logs.sh wplogin <ip> [count] [host-id] [log|logs] POST /wp-login.php
|
||||
# ./fake-npm-logs.sh harmless <ip> [count] [host-id] [log|logs] 404 /favicon.ico, 200 /
|
||||
# ./fake-npm-logs.sh fallback <ip> [count] 444 on the default host
|
||||
# ./fake-npm-logs.sh newhost <host-id> [log|logs] empty log of a new proxy host
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
cmd=${1:?command missing}
|
||||
now() { date -u '+%d/%b/%Y:%H:%M:%S +0000'; }
|
||||
proxy_line() { # status method path ip
|
||||
printf '[%s] - %s %s - %s https test.example.com "%s" [Client %s] [Length 150] [Gzip -] [Sent-to 172.16.52.10] "curl/8.5.0" "-"\n' \
|
||||
"$(now)" "$1" "$1" "$2" "$3" "$4"
|
||||
}
|
||||
|
||||
case "$cmd" in
|
||||
scan|wplogin|harmless)
|
||||
ip=${2:?ip missing}; count=${3:-3}; host=${4:-1}; dir=npm-logs/${5:-log}
|
||||
file="$dir/proxy-host-${host}_access.log"
|
||||
mkdir -p "$dir"
|
||||
paths=(/.env /.git/config /phpinfo.php /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php /actuator/env)
|
||||
for ((i = 0; i < count; i++)); do
|
||||
case "$cmd" in
|
||||
scan) proxy_line 404 GET "${paths[i % ${#paths[@]}]}" "$ip" ;;
|
||||
wplogin) proxy_line 200 POST /wp-login.php "$ip" ;;
|
||||
harmless) proxy_line 404 GET /favicon.ico "$ip"; proxy_line 200 GET / "$ip" ;;
|
||||
esac >> "$file"
|
||||
sleep 0.2
|
||||
done
|
||||
echo "$cmd: $count × $ip -> $file"
|
||||
;;
|
||||
fallback)
|
||||
ip=${2:?ip missing}; count=${3:-3}
|
||||
mkdir -p npm-logs/log
|
||||
for ((i = 0; i < count; i++)); do
|
||||
printf '%s - - [%s] "GET / HTTP/1.1" 444 0 "-" "curl/8.5.0"\n' "$ip" "$(now)" >> npm-logs/log/default-host_access.log
|
||||
sleep 0.2
|
||||
done
|
||||
echo "fallback: $count × $ip -> npm-logs/log/default-host_access.log"
|
||||
;;
|
||||
newhost)
|
||||
host=${2:?host-id missing}; dir=npm-logs/${3:-log}
|
||||
mkdir -p "$dir"
|
||||
touch "$dir/proxy-host-${host}_access.log"
|
||||
echo "newhost: $dir/proxy-host-${host}_access.log"
|
||||
;;
|
||||
*)
|
||||
sed -n '2,9p' "$0"; exit 1 ;;
|
||||
esac
|
||||
Reference in new issue
Block a user