nginx 로그 보고서 스크립트 추가
nginx 접속 로그(회전된 .gz 포함)를 모두 읽어 통계와 IP 목록을 파일 하나로 정리하는 서버 운영용 스크립트. 외부 IP 전체, 스캐너, 오류 응답이 많은 IP, /sharing 업로드와 다운로드와 로그인, 봇, nginx 오류 로그, SSH 실패, 방화벽 로그 기반 포트 스캔 의심 IP를 담는다. 읽기만 하며 보고서는 chmod 600. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d9425fff55
commit
3af258cf4d
309
scripts/nginx-log-report.sh
Normal file
309
scripts/nginx-log-report.sh
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# nginx-log-report.sh: one report file from all nginx logs
|
||||||
|
#
|
||||||
|
# Reads every access log (the current one and the rotated ones, plain or .gz) and,
|
||||||
|
# if they exist and are readable, the nginx error logs, the SSH log and the firewall
|
||||||
|
# log, and writes ONE text file with statistics and lists of IP addresses:
|
||||||
|
#
|
||||||
|
# * requests per day, status codes, requests per hour
|
||||||
|
# * every external IP with first/last visit and status counts
|
||||||
|
# * scanners: requests for typical attack paths, broken (non-HTTP) requests,
|
||||||
|
# IPs with many 4xx answers, blocked (403) requests
|
||||||
|
# * the /sharing app: uploads, downloads, logins, admin access
|
||||||
|
# * user agents (bots, empty), top paths, top 404 paths
|
||||||
|
# * nginx error log, failed SSH logins, port scan suspects (firewall log)
|
||||||
|
#
|
||||||
|
# It only reads, nothing is changed. The report contains IP addresses: it is
|
||||||
|
# created readable for the owner only (chmod 600).
|
||||||
|
#
|
||||||
|
# Usage: sudo bash nginx-log-report.sh [options]
|
||||||
|
# -d DIR directory of the nginx logs (default /var/log/nginx)
|
||||||
|
# -o FILE report file (default ./nginx-report-DATE-TIME.txt)
|
||||||
|
# -n N length of the "top" lists (default 30)
|
||||||
|
# -t N 4xx answers from one IP to be listed (default 20)
|
||||||
|
# -S DATE only entries since DATE (YYYY-MM-DD)
|
||||||
|
# -i REGEX which IPs are internal (extended regex) (default: private ranges, VPN 100.64/10)
|
||||||
|
# -s REGEX paths which look like a scan (default: .env, wp-login, phpmyadmin, ...)
|
||||||
|
# -a GLOB SSH log(s) (default '/var/log/auth.log*')
|
||||||
|
# -f GLOB firewall log(s), lines with SRC= and DPT= (default '/var/log/ufw.log*')
|
||||||
|
# -h this help
|
||||||
|
#
|
||||||
|
# Needs: bash 4, awk (mawk and gawk both work), gzip, sort, find. "column" is used when present.
|
||||||
|
# Log format: nginx "combined" (an extra field at the end, like X-Forwarded-For, is fine).
|
||||||
|
|
||||||
|
set -u
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
LOG_DIR="/var/log/nginx"
|
||||||
|
OUT=""
|
||||||
|
TOP=30
|
||||||
|
THRESH=20
|
||||||
|
SINCE=""
|
||||||
|
AUTH_GLOB="/var/log/auth.log*"
|
||||||
|
FW_GLOB="/var/log/ufw.log*"
|
||||||
|
# environment variables, so that awk reads the regular expressions unchanged
|
||||||
|
export INTERNAL_RE='^(10\.|192\.168\.|127\.|172\.(1[6-9]|2[0-9]|3[01])\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|::1$|[fF][cCdD][0-9a-fA-F]*:|[fF][eE]80:)'
|
||||||
|
export SCAN_RE='(\.env|\.git(/|$)|wp-(login|admin|content|includes)|xmlrpc|phpmyadmin|phpunit|eval-stdin|cgi-bin|actuator|boaform|HNAP1|\.php|\.asp|\.jsp|/etc/passwd|\.\./|manager/html|jndi|\.aws|\.DS_Store|autodiscover|/shell|\.sql$|\.bak$)'
|
||||||
|
export BOT_RE='(bot|crawl|spider|slurp|scrap|facebookexternalhit|fetcher|gpt|claude|perplexity|ahrefs|semrush|petal|yandex|baidu|python-requests|curl/|wget|go-http-client|java/|libwww|okhttp|httpclient|node-fetch|axios|masscan|zgrab|nmap|nikto|sqlmap|censys|shodan|expanse)'
|
||||||
|
|
||||||
|
usage() { sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||||
|
|
||||||
|
while getopts "d:o:n:t:S:i:s:a:f:h" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
d) LOG_DIR="$OPTARG" ;;
|
||||||
|
o) OUT="$OPTARG" ;;
|
||||||
|
n) TOP="$OPTARG" ;;
|
||||||
|
t) THRESH="$OPTARG" ;;
|
||||||
|
S) SINCE="$OPTARG" ;;
|
||||||
|
i) INTERNAL_RE="$OPTARG" ;;
|
||||||
|
s) SCAN_RE="$OPTARG" ;;
|
||||||
|
a) AUTH_GLOB="$OPTARG" ;;
|
||||||
|
f) FW_GLOB="$OPTARG" ;;
|
||||||
|
h) usage; exit 0 ;;
|
||||||
|
*) usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
TAB=$'\t'
|
||||||
|
[ -z "$OUT" ] && OUT="./nginx-report-$(date +%Y%m%d-%H%M).txt"
|
||||||
|
|
||||||
|
for n in "$TOP" "$THRESH"; do
|
||||||
|
case "$n" in ''|*[!0-9]*) echo "n and t need a number" >&2; exit 2 ;; esac
|
||||||
|
done
|
||||||
|
if [ -n "$SINCE" ] && ! [[ "$SINCE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
||||||
|
echo "-S needs a date like 2026-11-01" >&2; exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
TSV="$TMP/access.tsv"
|
||||||
|
|
||||||
|
# files that can be read from a glob or a name pattern in LOG_DIR
|
||||||
|
list_files() { # $1 = glob
|
||||||
|
local f
|
||||||
|
for f in $1; do [ -f "$f" ] && [ -r "$f" ] && printf '%s\n' "$f"; done
|
||||||
|
}
|
||||||
|
read_logs() { gzip -dcf -- "$@" 2>/dev/null; }
|
||||||
|
fmt_cols() { if command -v column >/dev/null 2>&1; then column -t -s "$TAB"; else cat; fi; }
|
||||||
|
# table HEADER < rows (tab separated)
|
||||||
|
table() { { printf '%s\n' "$1"; cat; } | fmt_cols; }
|
||||||
|
section() { printf '\n\n==== %s ====\n\n' "$1"; }
|
||||||
|
|
||||||
|
mapfile -t ACCESS_FILES < <(find "$LOG_DIR" -maxdepth 1 -type f -name 'access.log*' 2>/dev/null | sort)
|
||||||
|
READABLE=()
|
||||||
|
for f in "${ACCESS_FILES[@]+"${ACCESS_FILES[@]}"}"; do
|
||||||
|
if [ -r "$f" ]; then READABLE+=("$f"); else echo "cannot read $f (run with sudo?)" >&2; fi
|
||||||
|
done
|
||||||
|
if [ "${#READABLE[@]}" -eq 0 ]; then
|
||||||
|
echo "no readable access.log* in $LOG_DIR (use -d, or run with sudo)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- 1. normalize: ip, date, time, method, path (no query), status, bytes, user agent
|
||||||
|
read_logs "${READABLE[@]}" | awk -v since="$SINCE" '
|
||||||
|
BEGIN { OFS = "\t"; months = "JanFebMarAprMayJunJulAugSepOctNovDec" }
|
||||||
|
{
|
||||||
|
n = split($0, f, "\"")
|
||||||
|
if (n < 6) { bad++; next }
|
||||||
|
split(f[1], h, " ")
|
||||||
|
ip = h[1]
|
||||||
|
t = substr(h[4], 2)
|
||||||
|
if (split(t, d, "[/:]") < 6) { bad++; next }
|
||||||
|
m = (index(months, d[2]) + 2) / 3
|
||||||
|
date = sprintf("%s-%02d-%s", d[3], m, d[1])
|
||||||
|
time = d[4] ":" d[5] ":" d[6]
|
||||||
|
if (since != "" && date < since) next
|
||||||
|
split(f[2], r, " ")
|
||||||
|
method = r[1]; path = r[2]
|
||||||
|
if (path == "") path = "-"
|
||||||
|
q = index(path, "?"); if (q) path = substr(path, 1, q - 1)
|
||||||
|
split(f[3], s, " ")
|
||||||
|
ua = f[6]
|
||||||
|
gsub(/\t/, " ", method); gsub(/\t/, " ", path); gsub(/\t/, " ", ua)
|
||||||
|
print ip, date, time, method, path, s[1], s[2], ua
|
||||||
|
}
|
||||||
|
END { print bad + 0 > "/dev/stderr" }' > "$TSV" 2> "$TMP/bad.txt"
|
||||||
|
BAD_LINES="$(cat "$TMP/bad.txt")"
|
||||||
|
TOTAL="$(wc -l < "$TSV" | tr -d ' ')"
|
||||||
|
if [ "$TOTAL" -eq 0 ]; then echo "no entries found (check the log format and -S)" >&2; exit 1; fi
|
||||||
|
|
||||||
|
# one row per IP: class ip requests first last 2xx 3xx 4xx 5xx top-path
|
||||||
|
awk -F'\t' '
|
||||||
|
BEGIN { re = ENVIRON["INTERNAL_RE"] }
|
||||||
|
{
|
||||||
|
ip = $1; c[ip]++
|
||||||
|
dt = $2 " " $3
|
||||||
|
if (!(ip in first) || dt < first[ip]) first[ip] = dt
|
||||||
|
if (dt > last[ip]) last[ip] = dt
|
||||||
|
k = substr($6, 1, 1)
|
||||||
|
if (k == "2") s2[ip]++; else if (k == "3") s3[ip]++; else if (k == "4") s4[ip]++; else if (k == "5") s5[ip]++
|
||||||
|
pc[ip SUBSEP $5]++
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
for (key in pc) { split(key, a, SUBSEP); if (pc[key] > best[a[1]]) { best[a[1]] = pc[key]; top[a[1]] = a[2] } }
|
||||||
|
for (ip in c) printf "%s\t%s\t%d\t%s\t%s\t%d\t%d\t%d\t%d\t%s\n", (ip ~ re ? "internal" : "external"), ip, c[ip], first[ip], last[ip], s2[ip], s3[ip], s4[ip], s5[ip], top[ip]
|
||||||
|
}' "$TSV" | sort -t"$TAB" -k3,3nr > "$TMP/ips.tsv"
|
||||||
|
|
||||||
|
# by_ip 'awk condition': per IP count and status codes of the rows that match
|
||||||
|
by_ip() {
|
||||||
|
awk -F'\t' 'BEGIN { re = ENVIRON["INTERNAL_RE"] } '"$1"' {
|
||||||
|
n[$1]++
|
||||||
|
if (index(c[$1], $6) == 0) c[$1] = c[$1] $6 " "
|
||||||
|
} END { for (ip in n) printf "%s\t%s\t%d\t%s\n", (ip ~ re ? "internal" : "external"), ip, n[ip], c[ip] }' "$TSV" \
|
||||||
|
| sort -t"$TAB" -k3,3nr | head -n "$TOP"
|
||||||
|
}
|
||||||
|
|
||||||
|
report() {
|
||||||
|
echo "NGINX LOG REPORT"
|
||||||
|
echo "created: $(date '+%Y-%m-%d %H:%M:%S') on $(hostname 2>/dev/null)"
|
||||||
|
[ -n "$SINCE" ] && echo "since: $SINCE"
|
||||||
|
|
||||||
|
section "1. Summary"
|
||||||
|
awk -F'\t' '
|
||||||
|
{ d = $2 " " $3; if (min == "" || d < min) min = d; if (d > max) max = d }
|
||||||
|
END { printf "period: %s to %s\n", min, max }' "$TSV"
|
||||||
|
echo "requests: $TOTAL"
|
||||||
|
echo "IPs: $(wc -l < "$TMP/ips.tsv" | tr -d ' ') ($(awk -F'\t' '$1=="external"' "$TMP/ips.tsv" | wc -l | tr -d ' ') external)"
|
||||||
|
echo "skipped: $BAD_LINES lines which are not in the expected log format"
|
||||||
|
echo "files read:"
|
||||||
|
printf ' %s\n' "${READABLE[@]}"
|
||||||
|
|
||||||
|
section "2. Requests per day (errors = status 400 and above)"
|
||||||
|
awk -F'\t' '{ c[$2]++; if ($6 >= 400) e[$2]++ } END { for (k in c) printf "%s\t%d\t%d\n", k, c[k], e[k] }' "$TSV" | sort | table $'date\trequests\terrors'
|
||||||
|
|
||||||
|
section "3. Status codes"
|
||||||
|
awk -F'\t' '{ c[$6]++ } END { for (k in c) printf "%s\t%d\n", k, c[k] }' "$TSV" | sort -t"$TAB" -k2,2nr | table $'status\trequests'
|
||||||
|
|
||||||
|
section "4. Requests per hour of the day"
|
||||||
|
awk -F'\t' '{ c[substr($3, 1, 2)]++ } END {
|
||||||
|
for (i = 0; i < 24; i++) { k = sprintf("%02d", i); if (c[k] > mx) mx = c[k] }
|
||||||
|
for (i = 0; i < 24; i++) { k = sprintf("%02d", i); bar = ""; w = (mx ? int(40 * c[k] / mx) : 0); for (j = 0; j < w; j++) bar = bar "#"; printf "%s:00\t%d\t%s\n", k, c[k] + 0, bar }
|
||||||
|
}' "$TSV" | table $'hour\trequests\t'
|
||||||
|
|
||||||
|
section "5. EVERY external IP (most requests first, at most 5000)"
|
||||||
|
awk -F'\t' '$1 == "external" { print $2 "\t" $3 "\t" $4 "\t" $5 "\t" $6 "\t" $7 "\t" $8 "\t" $9 "\t" $10 }' "$TMP/ips.tsv" | head -n 5000 \
|
||||||
|
| table $'ip\trequests\tfirst\tlast\t2xx\t3xx\t4xx\t5xx\tmost requested path'
|
||||||
|
|
||||||
|
section "6. Internal IPs (top $TOP)"
|
||||||
|
awk -F'\t' '$1 == "internal" { print $2 "\t" $3 "\t" $4 "\t" $5 "\t" $6 "\t" $7 "\t" $8 "\t" $9 "\t" $10 }' "$TMP/ips.tsv" | head -n "$TOP" \
|
||||||
|
| table $'ip\trequests\tfirst\tlast\t2xx\t3xx\t4xx\t5xx\tmost requested path'
|
||||||
|
|
||||||
|
section "7. Scanners: requests for typical attack paths (not /sharing/, at most 500 IPs)"
|
||||||
|
awk -F'\t' '
|
||||||
|
BEGIN { scan = ENVIRON["SCAN_RE"]; re = ENVIRON["INTERNAL_RE"] }
|
||||||
|
$5 ~ /^\/sharing\// { next }
|
||||||
|
$5 ~ scan {
|
||||||
|
ip = $1; n[ip]++
|
||||||
|
if (index(codes[ip], $6) == 0) codes[ip] = codes[ip] $6 " "
|
||||||
|
if (ns[ip] < 3 && index(sample[ip], $5) == 0) { sample[ip] = sample[ip] (sample[ip] == "" ? "" : " ") $5; ns[ip]++ }
|
||||||
|
}
|
||||||
|
END { for (ip in n) printf "%s\t%s\t%d\t%s\t%s\n", (ip ~ re ? "internal" : "external"), ip, n[ip], codes[ip], sample[ip] }' "$TSV" \
|
||||||
|
| sort -t"$TAB" -k3,3nr | head -n 500 | table $'class\tip\trequests\tstatus\texample paths'
|
||||||
|
|
||||||
|
section "8. Broken requests (not HTTP, e.g. TLS or binary data sent to the HTTP port)"
|
||||||
|
by_ip '$5 !~ /^\// || $4 !~ /^[A-Z]+$/' | table $'class\tip\trequests\tstatus'
|
||||||
|
|
||||||
|
section "9. IPs with $THRESH or more error answers (400, 401, 403, 404, 405, 408, 444)"
|
||||||
|
awk -F'\t' -v th="$THRESH" '
|
||||||
|
BEGIN { re = ENVIRON["INTERNAL_RE"] }
|
||||||
|
{ t[$1]++; if ($6 == 400 || $6 == 401 || $6 == 403 || $6 == 404 || $6 == 405 || $6 == 408 || $6 == 444) e[$1]++ }
|
||||||
|
END { for (ip in e) if (e[ip] >= th) printf "%s\t%s\t%d\t%d\t%d%%\n", (ip ~ re ? "internal" : "external"), ip, e[ip], t[ip], 100 * e[ip] / t[ip] }' "$TSV" \
|
||||||
|
| sort -t"$TAB" -k3,3nr | head -n 500 | table $'class\tip\terrors\tall requests\tshare'
|
||||||
|
|
||||||
|
section "10. Blocked requests (403) per IP (top $TOP)"
|
||||||
|
by_ip '$6 == 403' | table $'class\tip\tblocked\tstatus'
|
||||||
|
echo
|
||||||
|
echo "most blocked paths:"
|
||||||
|
awk -F'\t' '$6 == 403 { c[$5]++ } END { for (p in c) printf "%d\t%s\n", c[p], p }' "$TSV" | sort -t"$TAB" -k1,1nr | head -n "$TOP" | table $'blocked\tpath'
|
||||||
|
|
||||||
|
section "11. /sharing"
|
||||||
|
awk -F'\t' '
|
||||||
|
BEGIN { re = ENVIRON["INTERNAL_RE"] }
|
||||||
|
$5 ~ /^\/sharing/ { t++; if ($1 ~ re) ti++; else te++ }
|
||||||
|
END { printf "requests: %d (internal %d, external %d)\n", t, ti, te }' "$TSV"
|
||||||
|
echo
|
||||||
|
echo "-- external IPs which reached /sharing (status 403 = blocked by nginx, 401 = the app asked for a login):"
|
||||||
|
by_ip '$5 ~ /^\/sharing/ && $1 !~ ENVIRON["INTERNAL_RE"]' | table $'class\tip\trequests\tstatus'
|
||||||
|
echo
|
||||||
|
echo "-- uploads (a file was created: POST /sharing/files/ answered 201):"
|
||||||
|
by_ip '$4 == "POST" && $5 ~ /^\/sharing\/files\/?$/ && $6 == 201' | table $'class\tip\tfiles\tstatus'
|
||||||
|
echo
|
||||||
|
echo "-- downloads (GET /sharing/files/<file> answered 200 or 206, previews included, thumbnails not):"
|
||||||
|
by_ip '$4 == "GET" && $5 ~ /^\/sharing\/files\// && $5 !~ /\.thumb$/ && ($6 == 200 || $6 == 206)' | table $'class\tip\tdownloads\tstatus'
|
||||||
|
echo
|
||||||
|
echo "-- logins (POST /sharing/login: 200 = right password, 403 = wrong password):"
|
||||||
|
by_ip '$4 == "POST" && $5 == "/sharing/login"' | table $'class\tip\tattempts\tstatus'
|
||||||
|
echo
|
||||||
|
echo "-- admin pages (/sharing/admin...):"
|
||||||
|
by_ip '$5 ~ /^\/sharing\/admin/' | table $'class\tip\trequests\tstatus'
|
||||||
|
|
||||||
|
section "12. User agents (top $TOP)"
|
||||||
|
awk -F'\t' '{ c[$8]++ } END { for (u in c) printf "%d\t%s\n", c[u], u }' "$TSV" | sort -t"$TAB" -k1,1nr | head -n "$TOP" | table $'requests\tuser agent'
|
||||||
|
echo
|
||||||
|
echo "-- automated clients and bots (by name) and empty user agents:"
|
||||||
|
awk -F'\t' '
|
||||||
|
BEGIN { bot = ENVIRON["BOT_RE"] }
|
||||||
|
{ u = tolower($8); if (u == "-" || u == "") u = "(empty)"
|
||||||
|
if (u == "(empty)" || u ~ bot) { c[$8 == "" || $8 == "-" ? "(empty)" : $8]++ } }
|
||||||
|
END { for (u in c) printf "%d\t%s\n", c[u], u }' "$TSV" | sort -t"$TAB" -k1,1nr | head -n "$TOP" | table $'requests\tuser agent'
|
||||||
|
|
||||||
|
section "13. Most requested paths (top $TOP)"
|
||||||
|
awk -F'\t' '{ c[$5]++ } END { for (p in c) printf "%d\t%s\n", c[p], p }' "$TSV" | sort -t"$TAB" -k1,1nr | head -n "$TOP" | table $'requests\tpath'
|
||||||
|
echo
|
||||||
|
echo "-- most requested paths which do not exist (404):"
|
||||||
|
awk -F'\t' '$6 == 404 { c[$5]++ } END { for (p in c) printf "%d\t%s\n", c[p], p }' "$TSV" | sort -t"$TAB" -k1,1nr | head -n "$TOP" | table $'requests\tpath'
|
||||||
|
|
||||||
|
section "14. nginx error log"
|
||||||
|
mapfile -t ERR_FILES < <(find "$LOG_DIR" -maxdepth 1 -type f -name 'error.log*' -readable 2>/dev/null | sort)
|
||||||
|
if [ "${#ERR_FILES[@]}" -eq 0 ]; then
|
||||||
|
echo "(no readable error.log* in $LOG_DIR)"
|
||||||
|
else
|
||||||
|
read_logs "${ERR_FILES[@]}" > "$TMP/error.txt"
|
||||||
|
echo "files: ${ERR_FILES[*]}"
|
||||||
|
echo "lines: $(wc -l < "$TMP/error.txt" | tr -d ' ')"
|
||||||
|
echo "TLS handshake failures: $(grep -c 'SSL_do_handshake() failed' "$TMP/error.txt")"
|
||||||
|
echo
|
||||||
|
echo "-- levels:"
|
||||||
|
awk '{ print $3 }' "$TMP/error.txt" | sort | uniq -c | sort -rn | head -n 10
|
||||||
|
echo
|
||||||
|
echo "-- kinds of messages (top $TOP):"
|
||||||
|
sed -E 's/^[0-9]{4}\/[0-9]{2}\/[0-9]{2} [0-9:]{8} \[[a-z]+\] [0-9]+#[0-9]+: (\*[0-9]+ )?//; s/,? client: .*$//' "$TMP/error.txt" \
|
||||||
|
| cut -c1-90 | sort | uniq -c | sort -rn | head -n "$TOP"
|
||||||
|
echo
|
||||||
|
echo "-- client IPs in the error log (top $TOP):"
|
||||||
|
grep -o 'client: [0-9a-fA-F.:]*' "$TMP/error.txt" | awk '{ print $2 }' | sort | uniq -c | sort -rn | head -n "$TOP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "15. Failed SSH logins (auth log)"
|
||||||
|
mapfile -t AUTH_FILES < <(list_files "$AUTH_GLOB")
|
||||||
|
if [ "${#AUTH_FILES[@]}" -eq 0 ]; then
|
||||||
|
echo "(no readable $AUTH_GLOB, use -a for another file)"
|
||||||
|
else
|
||||||
|
read_logs "${AUTH_FILES[@]}" | grep -E 'Failed password|Invalid user|authentication failure|Connection closed by (invalid|authenticating) user' \
|
||||||
|
| grep -oE '(from|rhost=)[ =]?[0-9a-fA-F.:]+' | sed -E 's/^(from|rhost=)[ =]?//' | sort | uniq -c | sort -rn | head -n "$TOP" \
|
||||||
|
| awk '{ printf "%s\t%s\n", $1, $2 }' | table $'failed\tip'
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "16. Port scan suspects (firewall log: one IP, many different destination ports)"
|
||||||
|
mapfile -t FW_FILES < <(list_files "$FW_GLOB")
|
||||||
|
if [ "${#FW_FILES[@]}" -eq 0 ]; then
|
||||||
|
echo "(no readable $FW_GLOB. nginx does not see port scans: switch the firewall log on, e.g. 'sudo ufw logging medium', and use -f)"
|
||||||
|
else
|
||||||
|
read_logs "${FW_FILES[@]}" | grep -E 'SRC=[0-9a-fA-F:.]+ .*DPT=' | awk '
|
||||||
|
{ src = ""; dpt = ""
|
||||||
|
for (i = 1; i <= NF; i++) { if ($i ~ /^SRC=/) src = substr($i, 5); else if ($i ~ /^DPT=/) dpt = substr($i, 5) }
|
||||||
|
if (src == "") next
|
||||||
|
n[src]++
|
||||||
|
if (!((src SUBSEP dpt) in seen)) { seen[src SUBSEP dpt] = 1; ports[src]++; if (np[src] < 8) { plist[src] = plist[src] dpt " "; np[src]++ } }
|
||||||
|
}
|
||||||
|
END { for (s in n) printf "%s\t%d\t%d\t%s\t%s\n", s, n[s], ports[s], (ports[s] >= 10 ? "SCAN SUSPECT" : "-"), plist[s] }' \
|
||||||
|
| sort -t"$TAB" -k3,3nr -k2,2nr | head -n "$TOP" | table $'ip\tpackets\tdifferent ports\tverdict\tfirst ports'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n\n---- end of report ----\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
report > "$OUT"
|
||||||
|
chmod 600 "$OUT" 2>/dev/null
|
||||||
|
echo "report written: $OUT ($TOTAL requests, $(wc -l < "$OUT" | tr -d ' ') lines)"
|
||||||
Loading…
x
Reference in New Issue
Block a user