Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
Executable
+569
@@ -0,0 +1,569 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#########################################################################################
|
||||
#### server-attack-report.sh — Analyze server logs for attack patterns ####
|
||||
#### Scans nginx access logs and SSH auth logs, produces a formatted report. ####
|
||||
#### Requires: bash, awk, grep, sort, uniq ####
|
||||
#### ####
|
||||
#### Author: Phil Connor ####
|
||||
#### Contact: contact@mylinux.work ####
|
||||
#### License: MIT ####
|
||||
#### Version 1.00 ####
|
||||
#### ####
|
||||
#### Usage: ####
|
||||
#### ./server-attack-report.sh ####
|
||||
#### ./server-attack-report.sh --access /var/log/nginx/access.log ####
|
||||
#### ./server-attack-report.sh --auth /var/log/auth.log ####
|
||||
#### ./server-attack-report.sh --top 30 --geo ####
|
||||
#### ./server-attack-report.sh --markdown --output report.md ####
|
||||
#### ####
|
||||
#### See --help for all options. ####
|
||||
#########################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors ────────────────────────────────────────────────────────────
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
RESET='\033[0m'
|
||||
else
|
||||
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
|
||||
fi
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────
|
||||
ACCESS_LOG=""
|
||||
AUTH_LOG=""
|
||||
TOP_N=20
|
||||
GEO_LOOKUP=false
|
||||
MARKDOWN=false
|
||||
OUTPUT_FILE=""
|
||||
THRESHOLD=50
|
||||
|
||||
# ── Auto-detect log paths ────────────────────────────────────────────
|
||||
detect_access_log() {
|
||||
local candidates=(
|
||||
/var/log/nginx/access.log
|
||||
/var/log/apache2/access.log
|
||||
/var/log/httpd/access_log
|
||||
)
|
||||
for f in "${candidates[@]}"; do
|
||||
if [[ -r "$f" && -s "$f" ]]; then
|
||||
echo "$f"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
detect_auth_log() {
|
||||
local candidates=(
|
||||
/var/log/auth.log
|
||||
/var/log/secure
|
||||
)
|
||||
for f in "${candidates[@]}"; do
|
||||
if [[ -r "$f" && -s "$f" ]]; then
|
||||
echo "$f"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Usage ─────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Analyze server logs for attack patterns and produce a security report.
|
||||
|
||||
Options:
|
||||
--access PATH Path to nginx/Apache access log
|
||||
(auto-detects if not specified)
|
||||
--auth PATH Path to SSH auth log
|
||||
(auto-detects if not specified)
|
||||
--top N Number of results per section (default: 20)
|
||||
--threshold N Flag IPs with more than N requests (default: 50)
|
||||
--geo Enable geographic IP lookups (requires curl + internet)
|
||||
--markdown Output in markdown format
|
||||
--output FILE Write report to file instead of stdout
|
||||
--no-color Disable colored output
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") --access /var/log/nginx/access.log --top 30
|
||||
$(basename "$0") --geo --markdown --output report.md
|
||||
$(basename "$0") --auth /var/log/secure --threshold 100
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Parse arguments ───────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--access) ACCESS_LOG="${2:?--access requires a path}"; shift 2 ;;
|
||||
--auth) AUTH_LOG="${2:?--auth requires a path}"; shift 2 ;;
|
||||
--top) TOP_N="${2:?--top requires a number}"; shift 2 ;;
|
||||
--threshold) THRESHOLD="${2:?--threshold requires a number}"; shift 2 ;;
|
||||
--geo) GEO_LOOKUP=true; shift ;;
|
||||
--markdown) MARKDOWN=true; shift ;;
|
||||
--output) OUTPUT_FILE="${2:?--output requires a filename}"; shift 2 ;;
|
||||
--no-color) RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "Unknown option: $1" >&2; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Output helpers ────────────────────────────────────────────────────
|
||||
OUT_FD=1
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
exec 3>"$OUTPUT_FILE"
|
||||
OUT_FD=3
|
||||
# disable colors for file output
|
||||
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
|
||||
fi
|
||||
|
||||
out() { echo -e "$*" >&$OUT_FD; }
|
||||
|
||||
heading() {
|
||||
if $MARKDOWN; then
|
||||
out ""
|
||||
out "## $1"
|
||||
out ""
|
||||
else
|
||||
out ""
|
||||
out "${BOLD}${CYAN}═══ $1 ═══${RESET}"
|
||||
out ""
|
||||
fi
|
||||
}
|
||||
|
||||
subheading() {
|
||||
if $MARKDOWN; then
|
||||
out "### $1"
|
||||
out ""
|
||||
else
|
||||
out "${BOLD}$1${RESET}"
|
||||
fi
|
||||
}
|
||||
|
||||
separator() {
|
||||
if $MARKDOWN; then
|
||||
out "---"
|
||||
out ""
|
||||
else
|
||||
out "${DIM}$(printf '─%.0s' {1..70})${RESET}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Auto-detect logs ─────────────────────────────────────────────────
|
||||
if [[ -z "$ACCESS_LOG" ]]; then
|
||||
ACCESS_LOG=$(detect_access_log)
|
||||
fi
|
||||
|
||||
if [[ -z "$AUTH_LOG" ]]; then
|
||||
AUTH_LOG=$(detect_auth_log)
|
||||
fi
|
||||
|
||||
HAS_ACCESS=false
|
||||
HAS_AUTH=false
|
||||
|
||||
if [[ -n "$ACCESS_LOG" && -r "$ACCESS_LOG" && -s "$ACCESS_LOG" ]]; then
|
||||
HAS_ACCESS=true
|
||||
fi
|
||||
|
||||
if [[ -n "$AUTH_LOG" && -r "$AUTH_LOG" && -s "$AUTH_LOG" ]]; then
|
||||
HAS_AUTH=true
|
||||
fi
|
||||
|
||||
if ! $HAS_ACCESS && ! $HAS_AUTH; then
|
||||
echo "ERROR: No readable log files found." >&2
|
||||
echo "Specify paths with --access and/or --auth" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Report header ─────────────────────────────────────────────────────
|
||||
REPORT_TIME=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
|
||||
|
||||
if $MARKDOWN; then
|
||||
out "# Server Attack Report"
|
||||
out ""
|
||||
out "- **Host:** ${HOSTNAME}"
|
||||
out "- **Generated:** ${REPORT_TIME}"
|
||||
[[ $HAS_ACCESS == true ]] && out "- **Access log:** ${ACCESS_LOG}"
|
||||
[[ $HAS_AUTH == true ]] && out "- **Auth log:** ${AUTH_LOG}"
|
||||
out ""
|
||||
else
|
||||
out "${BOLD}Server Attack Report${RESET}"
|
||||
out "Host: ${HOSTNAME}"
|
||||
out "Generated: ${REPORT_TIME}"
|
||||
[[ $HAS_ACCESS == true ]] && out "Access log: ${ACCESS_LOG}"
|
||||
[[ $HAS_AUTH == true ]] && out "Auth log: ${AUTH_LOG}"
|
||||
fi
|
||||
|
||||
# ── Helper: format count table ────────────────────────────────────────
|
||||
# Reads lines of "count value" from stdin and formats them
|
||||
format_table() {
|
||||
local label_col="${1:-Item}"
|
||||
local count_col="${2:-Count}"
|
||||
|
||||
if $MARKDOWN; then
|
||||
out "| ${count_col} | ${label_col} |"
|
||||
out "|------|------|"
|
||||
while IFS= read -r line; do
|
||||
local count value
|
||||
count=$(echo "$line" | awk '{print $1}')
|
||||
value=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //')
|
||||
out "| ${count} | ${value} |"
|
||||
done
|
||||
out ""
|
||||
else
|
||||
printf "${BOLD}%8s %-s${RESET}\n" "$count_col" "$label_col" >&$OUT_FD
|
||||
while IFS= read -r line; do
|
||||
local count value
|
||||
count=$(echo "$line" | awk '{print $1}')
|
||||
value=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //')
|
||||
printf "%8s %s\n" "$count" "$value" >&$OUT_FD
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: format multi-column table ─────────────────────────────────
|
||||
format_table_multi() {
|
||||
local header="$1"
|
||||
|
||||
if $MARKDOWN; then
|
||||
out "$header"
|
||||
# auto-generate separator from header pipe count
|
||||
local cols
|
||||
cols=$(echo "$header" | awk -F'|' '{print NF-2}')
|
||||
local sep="|"
|
||||
for ((i=0; i<cols; i++)); do sep+="------|"; done
|
||||
out "$sep"
|
||||
while IFS= read -r line; do
|
||||
out "$line"
|
||||
done
|
||||
out ""
|
||||
else
|
||||
out "$header"
|
||||
while IFS= read -r line; do
|
||||
out "$line"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# ACCESS LOG ANALYSIS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
if $HAS_ACCESS; then
|
||||
|
||||
TOTAL_REQUESTS=$(wc -l < "$ACCESS_LOG" | tr -dc '0-9')
|
||||
TOTAL_IPS=$(awk '{print $1}' "$ACCESS_LOG" | sort -u | wc -l | tr -dc '0-9')
|
||||
TOTAL_404=$(awk '$9 == 404' "$ACCESS_LOG" | wc -l | tr -dc '0-9')
|
||||
TOTAL_POST=$({ grep -c '"POST ' "$ACCESS_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
|
||||
: "${TOTAL_REQUESTS:=0}" "${TOTAL_IPS:=0}" "${TOTAL_404:=0}" "${TOTAL_POST:=0}"
|
||||
|
||||
heading "Summary"
|
||||
|
||||
if $MARKDOWN; then
|
||||
out "| Metric | Value |"
|
||||
out "|--------|-------|"
|
||||
out "| Total requests | ${TOTAL_REQUESTS} |"
|
||||
out "| Unique IPs | ${TOTAL_IPS} |"
|
||||
out "| 404 responses | ${TOTAL_404} |"
|
||||
out "| POST requests | ${TOTAL_POST} |"
|
||||
out ""
|
||||
else
|
||||
out "Total requests: ${BOLD}${TOTAL_REQUESTS}${RESET}"
|
||||
out "Unique IPs: ${BOLD}${TOTAL_IPS}${RESET}"
|
||||
out "404 responses: ${BOLD}${TOTAL_404}${RESET}"
|
||||
out "POST requests: ${BOLD}${TOTAL_POST}${RESET}"
|
||||
fi
|
||||
|
||||
# ── Top IPs ───────────────────────────────────────────────────────
|
||||
heading "Top IPs by Request Count"
|
||||
|
||||
awk '{print $1}' "$ACCESS_LOG" \
|
||||
| sort | uniq -c | sort -rn | head -"$TOP_N" \
|
||||
| format_table "IP Address" "Requests"
|
||||
|
||||
# ── High-volume IPs ───────────────────────────────────────────────
|
||||
HIGH_VOLUME=$(awk '{print $1}' "$ACCESS_LOG" \
|
||||
| sort | uniq -c | awk -v t="$THRESHOLD" '$1 > t' | sort -rn | wc -l)
|
||||
|
||||
if [[ "$HIGH_VOLUME" -gt 0 ]]; then
|
||||
heading "High-Volume IPs (>${THRESHOLD} requests)"
|
||||
|
||||
if $GEO_LOOKUP; then
|
||||
subheading "With geographic lookup"
|
||||
awk '{print $1}' "$ACCESS_LOG" \
|
||||
| sort | uniq -c | awk -v t="$THRESHOLD" '$1 > t' | sort -rn \
|
||||
| head -"$TOP_N" | while read -r count ip; do
|
||||
country=$(curl -s --max-time 3 "https://ipinfo.io/${ip}/country" 2>/dev/null || echo "??")
|
||||
org=$(curl -s --max-time 3 "https://ipinfo.io/${ip}/org" 2>/dev/null || echo "unknown")
|
||||
if $MARKDOWN; then
|
||||
out "| ${count} | ${ip} | ${country} | ${org} |"
|
||||
else
|
||||
printf "%8s %-16s %-4s %s\n" "$count" "$ip" "$country" "$org" >&$OUT_FD
|
||||
fi
|
||||
done
|
||||
out ""
|
||||
else
|
||||
awk '{print $1}' "$ACCESS_LOG" \
|
||||
| sort | uniq -c | awk -v t="$THRESHOLD" '$1 > t' | sort -rn \
|
||||
| head -"$TOP_N" \
|
||||
| format_table "IP Address" "Requests"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── POST Requests ─────────────────────────────────────────────────
|
||||
if [[ "$TOTAL_POST" -gt 0 ]]; then
|
||||
heading "POST Requests"
|
||||
subheading "POST requests by IP and path"
|
||||
|
||||
grep '"POST ' "$ACCESS_LOG" \
|
||||
| awk '{print $1, $7, $9}' | sort | uniq -c | sort -rn \
|
||||
| head -"$TOP_N" \
|
||||
| format_table "IP / Path / Status" "Count"
|
||||
fi
|
||||
|
||||
# ── 404 Paths ─────────────────────────────────────────────────────
|
||||
if [[ "$TOTAL_404" -gt 0 ]]; then
|
||||
heading "Top 404 Paths (Scanner Targets)"
|
||||
|
||||
awk '$9 == 404 {print $7}' "$ACCESS_LOG" \
|
||||
| sort | uniq -c | sort -rn | head -"$TOP_N" \
|
||||
| format_table "Path" "Hits"
|
||||
fi
|
||||
|
||||
# ── Suspicious Paths ──────────────────────────────────────────────
|
||||
SUSPICIOUS=$({ grep -ciE '\.(env|git|php|asp|jsp|bak|sql|tar\.gz|zip)|wp-login|wp-admin|xmlrpc|phpmyadmin|pma|cgi-bin' "$ACCESS_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
|
||||
: "${SUSPICIOUS:=0}"
|
||||
|
||||
if [[ "$SUSPICIOUS" -gt 0 ]]; then
|
||||
heading "Suspicious Path Probes"
|
||||
|
||||
subheading "Secret/config file hunting"
|
||||
grep -iE '\.(env|git|aws|ssh)' "$ACCESS_LOG" 2>/dev/null \
|
||||
| awk '{print $1, $7}' | sort | uniq -c | sort -rn \
|
||||
| head -"$TOP_N" \
|
||||
| format_table "IP / Path" "Hits"
|
||||
|
||||
separator
|
||||
|
||||
subheading "WordPress / PHP scanning"
|
||||
grep -iE 'wp-login|wp-admin|xmlrpc|\.php|phpmyadmin|pma|cgi-bin' "$ACCESS_LOG" 2>/dev/null \
|
||||
| awk '{print $1, $7}' | sort | uniq -c | sort -rn \
|
||||
| head -"$TOP_N" \
|
||||
| format_table "IP / Path" "Hits"
|
||||
fi
|
||||
|
||||
# ── API Enumeration ───────────────────────────────────────────────
|
||||
API_PROBES=$({ grep -ciE '/api/|/v[0-9]+/|/swagger|/graphql|/actuator|/openapi|/api-docs|/debug/' "$ACCESS_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
|
||||
: "${API_PROBES:=0}"
|
||||
|
||||
if [[ "$API_PROBES" -gt 0 ]]; then
|
||||
heading "API Enumeration Probes"
|
||||
|
||||
grep -iE '/api/|/v[0-9]+/|/swagger|/graphql|/actuator|/openapi|/api-docs|/debug/' "$ACCESS_LOG" \
|
||||
| awk '{print $1, $7}' | sort | uniq -c | sort -rn \
|
||||
| head -"$TOP_N" \
|
||||
| format_table "IP / Path" "Hits"
|
||||
fi
|
||||
|
||||
# ── User Agents ───────────────────────────────────────────────────
|
||||
heading "Top User Agents"
|
||||
|
||||
awk -F'"' '{print $6}' "$ACCESS_LOG" \
|
||||
| sort | uniq -c | sort -rn | head -"$TOP_N" \
|
||||
| format_table "User Agent" "Requests"
|
||||
|
||||
# ── Known Bot User Agents ─────────────────────────────────────────
|
||||
BOT_HITS=$({ grep -ciE 'GPTBot|ClaudeBot|CCBot|Bytespider|PerplexityBot|meta-external|meta-webindexer|SemrushBot|AhrefsBot|MJ12bot|DotBot|Scrapy|python-requests|Go-http-client|curl/' "$ACCESS_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
|
||||
: "${BOT_HITS:=0}"
|
||||
|
||||
if [[ "$BOT_HITS" -gt 0 ]]; then
|
||||
heading "Known Bot / Scraper Traffic"
|
||||
|
||||
grep -iE 'GPTBot|ClaudeBot|CCBot|Bytespider|PerplexityBot|meta-external|meta-webindexer|SemrushBot|AhrefsBot|MJ12bot|DotBot|Scrapy|python-requests|Go-http-client|curl/' "$ACCESS_LOG" \
|
||||
| awk -F'"' '{print $6}' | sort | uniq -c | sort -rn \
|
||||
| head -"$TOP_N" \
|
||||
| format_table "Bot User Agent" "Requests"
|
||||
fi
|
||||
|
||||
# ── Response Code Distribution ────────────────────────────────────
|
||||
heading "Response Code Distribution"
|
||||
|
||||
awk '$9 ~ /^[0-9]{3}$/ {print $9}' "$ACCESS_LOG" \
|
||||
| sort | uniq -c | sort -rn \
|
||||
| format_table "Status Code" "Count"
|
||||
|
||||
# ── Requests Per Hour ─────────────────────────────────────────────
|
||||
heading "Requests Per Hour (Recent 24)"
|
||||
|
||||
awk '{gsub(/\[/,"",$4); print $4}' "$ACCESS_LOG" \
|
||||
| cut -d: -f1-2 | sort | uniq -c \
|
||||
| tail -24 \
|
||||
| format_table "Date/Hour" "Requests"
|
||||
|
||||
# ── Bandwidth by IP ───────────────────────────────────────────────
|
||||
heading "Top IPs by Bandwidth"
|
||||
|
||||
awk '$10 ~ /^[0-9]+$/ {sum[$1] += $10} END {for (ip in sum) printf "%d %s\n", sum[ip], ip}' "$ACCESS_LOG" \
|
||||
| sort -rn | head -"$TOP_N" \
|
||||
| while read -r bytes ip; do
|
||||
bytes="${bytes//[^0-9]/}"
|
||||
[[ -z "$bytes" ]] && bytes=0
|
||||
if [[ "$bytes" -ge 1073741824 ]]; then
|
||||
human=$(awk "BEGIN {printf \"%.1f GB\", $bytes/1073741824}")
|
||||
elif [[ "$bytes" -ge 1048576 ]]; then
|
||||
human=$(awk "BEGIN {printf \"%.1f MB\", $bytes/1048576}")
|
||||
elif [[ "$bytes" -ge 1024 ]]; then
|
||||
human=$(awk "BEGIN {printf \"%.1f KB\", $bytes/1024}")
|
||||
else
|
||||
human="${bytes} B"
|
||||
fi
|
||||
echo "${human} ${ip}"
|
||||
done \
|
||||
| format_table "IP Address" "Bandwidth"
|
||||
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# SSH AUTH LOG ANALYSIS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
if $HAS_AUTH; then
|
||||
|
||||
TOTAL_FAILED=$({ grep -c "Failed password" "$AUTH_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
|
||||
TOTAL_INVALID=$({ grep -c "Invalid user" "$AUTH_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
|
||||
TOTAL_ACCEPTED=$({ grep -c "Accepted" "$AUTH_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
|
||||
: "${TOTAL_FAILED:=0}" "${TOTAL_INVALID:=0}" "${TOTAL_ACCEPTED:=0}"
|
||||
|
||||
heading "SSH Authentication Summary"
|
||||
|
||||
if $MARKDOWN; then
|
||||
out "| Metric | Value |"
|
||||
out "|--------|-------|"
|
||||
out "| Failed password attempts | ${TOTAL_FAILED} |"
|
||||
out "| Invalid user attempts | ${TOTAL_INVALID} |"
|
||||
out "| Successful logins | ${TOTAL_ACCEPTED} |"
|
||||
out ""
|
||||
else
|
||||
out "Failed password attempts: ${BOLD}${RED}${TOTAL_FAILED}${RESET}"
|
||||
out "Invalid user attempts: ${BOLD}${RED}${TOTAL_INVALID}${RESET}"
|
||||
out "Successful logins: ${BOLD}${GREEN}${TOTAL_ACCEPTED}${RESET}"
|
||||
fi
|
||||
|
||||
# ── SSH Brute Force IPs ───────────────────────────────────────────
|
||||
if [[ "$TOTAL_FAILED" -gt 0 ]]; then
|
||||
heading "Top SSH Brute Force IPs"
|
||||
|
||||
grep "Failed password" "$AUTH_LOG" \
|
||||
| sed -n 's/.*from \([0-9.]*\) port.*/\1/p' | sort | uniq -c | sort -rn \
|
||||
| head -"$TOP_N" \
|
||||
| if $GEO_LOOKUP; then
|
||||
while read -r count ip; do
|
||||
country=$(curl -s --max-time 3 "https://ipinfo.io/${ip}/country" 2>/dev/null || echo "??")
|
||||
if $MARKDOWN; then
|
||||
echo "| ${count} | ${ip} | ${country} |"
|
||||
else
|
||||
printf "%8s %-16s %s\n" "$count" "$ip" "$country"
|
||||
fi
|
||||
done
|
||||
else
|
||||
format_table "IP Address" "Attempts"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Targeted Usernames ────────────────────────────────────────────
|
||||
if [[ "$TOTAL_FAILED" -gt 0 ]]; then
|
||||
heading "Most Targeted SSH Usernames"
|
||||
|
||||
grep "Failed password" "$AUTH_LOG" \
|
||||
| sed -n 's/.*Failed password for invalid user \([^ ]*\) from.*/\1/p; s/.*Failed password for \([^ ]*\) from.*/\1/p' \
|
||||
| sort | uniq -c | sort -rn | head -"$TOP_N" \
|
||||
| format_table "Username" "Attempts"
|
||||
fi
|
||||
|
||||
# ── Invalid Users ─────────────────────────────────────────────────
|
||||
if [[ "$TOTAL_INVALID" -gt 0 ]]; then
|
||||
heading "Invalid Usernames Attempted"
|
||||
|
||||
grep "Invalid user" "$AUTH_LOG" \
|
||||
| sed -n 's/.*Invalid user \([^ ]*\) from.*/\1/p' \
|
||||
| sort | uniq -c | sort -rn \
|
||||
| head -"$TOP_N" \
|
||||
| format_table "Username" "Attempts"
|
||||
fi
|
||||
|
||||
# ── Successful Logins ─────────────────────────────────────────────
|
||||
if [[ "$TOTAL_ACCEPTED" -gt 0 ]]; then
|
||||
heading "Successful SSH Logins (Verify These)"
|
||||
|
||||
if $MARKDOWN; then
|
||||
out "| Date | User | IP | Method |"
|
||||
out "|------|------|----|--------|"
|
||||
fi
|
||||
|
||||
grep "Accepted" "$AUTH_LOG" | tail -"$TOP_N" \
|
||||
| while IFS= read -r line; do
|
||||
local_date=$(echo "$line" | awk '{print $1, $2, $3}')
|
||||
user=$(echo "$line" | awk '{print $9}')
|
||||
ip=$(echo "$line" | awk '{print $11}')
|
||||
method=$(echo "$line" | awk '{print $7}')
|
||||
if $MARKDOWN; then
|
||||
out "| ${local_date} | ${user} | ${ip} | ${method} |"
|
||||
else
|
||||
printf " %-15s %-12s %-16s %s\n" "$local_date" "$user" "$ip" "$method" >&$OUT_FD
|
||||
fi
|
||||
done
|
||||
out ""
|
||||
fi
|
||||
|
||||
# ── SSH Failures Per Hour ─────────────────────────────────────────
|
||||
if [[ "$TOTAL_FAILED" -gt 0 ]]; then
|
||||
heading "SSH Failures Per Hour (Recent)"
|
||||
|
||||
grep "Failed password" "$AUTH_LOG" \
|
||||
| awk '{
|
||||
if ($1 ~ /^[0-9]{4}-/) {
|
||||
# ISO 8601: 2026-04-25T21:11:13.346843+02:00
|
||||
split($1, a, "T")
|
||||
split(a[2], b, ":")
|
||||
print a[1] " " b[1]":00"
|
||||
} else {
|
||||
# BSD syslog: Apr 24 14:30:01
|
||||
print $1, $2, substr($3,1,2)":00"
|
||||
}
|
||||
}' | sort | uniq -c \
|
||||
| tail -24 \
|
||||
| format_table "Time" "Failures"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# REPORT FOOTER
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
separator
|
||||
|
||||
if $MARKDOWN; then
|
||||
out ""
|
||||
out "*Report generated by [server-attack-report.sh](https://mylinux.work/projects/server-attack-report/) on ${REPORT_TIME}*"
|
||||
else
|
||||
out "${DIM}Report generated by server-attack-report.sh on ${REPORT_TIME}${RESET}"
|
||||
fi
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
exec 3>&-
|
||||
echo "Report written to: ${OUTPUT_FILE}"
|
||||
fi
|
||||
Reference in New Issue
Block a user