b7417a6bfa
- live-attack-monitor.sh: * Remove snapshot loading (start fresh each session) * Fix Apache log monitoring to use tail -n 0 -F (only new entries) * Add IP file sync to main loop for auto-blocking to work * Fix IP_DATA consolidation for cross-process communication - bot-analyzer.sh: * Implement gzip compression for large temp files (10-20x space savings) * Update all read/write operations to use compressed files * Fix for servers with 200+ domains and millions of log entries - run.sh: * Add HISTFILE fallback to prevent crashes when sourced
3242 lines
124 KiB
Bash
Executable File
3242 lines
124 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
#############################################################################
|
|
# Apache/cPanel Domain Log Bot & Botnet Analyzer
|
|
# Version: 3.1 Enhanced (with Library Integration)
|
|
# Advanced log analysis for bot activity, security threats, and botnets
|
|
#
|
|
# Features:
|
|
# - Comprehensive bot classification (legitimate, AI, monitoring, suspicious)
|
|
# - Enhanced attack vector detection (SQL injection, XSS, path traversal,
|
|
# RCE/shell upload, info disclosure, login bruteforce)
|
|
# - Threat scoring system (0-100 risk scores for each IP)
|
|
# - Time-series analysis with hourly traffic visualization
|
|
# - Response code intelligence (what are bots finding?)
|
|
# - False positive detection for legitimate monitoring services
|
|
# - Bandwidth cost estimation for bot traffic
|
|
# - Botnet pattern analysis (coordinated attacks, DDoS detection)
|
|
# - Prioritized blocklists sorted by threat severity
|
|
# - Actionable reports with copy-paste ready configurations
|
|
# - Performance optimized for large log files (>500k entries)
|
|
# - User filtering (analyze all users or specific user)
|
|
# - Auto-detects log directory based on control panel
|
|
#############################################################################
|
|
|
|
# Load libraries
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
|
source "$SCRIPT_DIR/lib/system-detect.sh"
|
|
source "$SCRIPT_DIR/lib/user-manager.sh"
|
|
source "$SCRIPT_DIR/lib/ip-reputation.sh"
|
|
source "$SCRIPT_DIR/lib/bot-signatures.sh"
|
|
source "$SCRIPT_DIR/lib/attack-patterns.sh"
|
|
source "$SCRIPT_DIR/lib/threat-intelligence.sh"
|
|
|
|
# Default configuration (auto-detected from system)
|
|
LOG_DIR="${SYS_LOG_DIR:-/var/log/apache2/domlogs}"
|
|
TEMP_DIR="/tmp/bot_analysis_$$"
|
|
OUTPUT_FILE="/tmp/bot_analysis_report_$(date +%Y%m%d_%H%M%S).txt"
|
|
DAYS_BACK="" # Empty means all logs, otherwise filter by days
|
|
HOURS_BACK="" # Empty means all logs, otherwise filter by hours
|
|
FILTER_USER="" # Empty means all users, otherwise specific user
|
|
|
|
# Parse command line arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-d|--days)
|
|
DAYS_BACK="$2"
|
|
shift 2
|
|
;;
|
|
-H|--hours)
|
|
HOURS_BACK="$2"
|
|
shift 2
|
|
;;
|
|
-l|--log-dir)
|
|
LOG_DIR="$2"
|
|
shift 2
|
|
;;
|
|
-o|--output)
|
|
OUTPUT_FILE="$2"
|
|
shift 2
|
|
;;
|
|
-u|--user)
|
|
FILTER_USER="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
echo "Apache/cPanel Domain Log Bot & Botnet Analyzer v3.1"
|
|
echo ""
|
|
echo "Usage: $0 [-d DAYS | -H HOURS] [-u USER] [-l LOG_DIR] [-o OUTPUT_FILE]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " -d, --days DAYS Analyze only logs from last N days (24-hour periods)"
|
|
echo " -H, --hours HOURS Analyze only logs from last N hours"
|
|
echo " -u, --user USER Analyze only logs for specific cPanel user"
|
|
echo " -l, --log-dir DIR Custom log directory (auto-detected by default)"
|
|
echo " -o, --output FILE Custom output file path"
|
|
echo " -h, --help Show this help message"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 # Analyze all logs in default directory"
|
|
echo " $0 -d 7 # Analyze logs from last 7 days"
|
|
echo " $0 -H 6 # Analyze logs from last 6 hours"
|
|
echo " $0 -l /custom/path # Use custom log directory"
|
|
echo ""
|
|
echo "Note: If both -d and -H are specified, only -H (hours) will be used."
|
|
echo ""
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
echo "Use -h for help"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Interactive prompts for missing options
|
|
prompt_time_range() {
|
|
clear
|
|
print_banner "Bot Analyzer - Time Range Selection"
|
|
echo ""
|
|
echo -e " ${GREEN}1)${NC} All available logs"
|
|
echo -e " ${GREEN}2)${NC} Last 1 hour"
|
|
echo -e " ${GREEN}3)${NC} Last 6 hours"
|
|
echo -e " ${GREEN}4)${NC} Last 24 hours"
|
|
echo -e " ${GREEN}5)${NC} Last 7 days"
|
|
echo -e " ${GREEN}6)${NC} Last 30 days"
|
|
echo -e " ${GREEN}7)${NC} Custom hours"
|
|
echo -e " ${GREEN}8)${NC} Custom days"
|
|
echo ""
|
|
read -p "Select time range (1-8): " time_choice
|
|
|
|
case $time_choice in
|
|
1) ;; # All logs - no filter
|
|
2) HOURS_BACK=1 ;;
|
|
3) HOURS_BACK=6 ;;
|
|
4) HOURS_BACK=24 ;;
|
|
5) DAYS_BACK=7 ;;
|
|
6) DAYS_BACK=30 ;;
|
|
7)
|
|
read -p "Enter number of hours: " custom_hours
|
|
if [[ "$custom_hours" =~ ^[0-9]+$ ]]; then
|
|
HOURS_BACK=$custom_hours
|
|
else
|
|
print_error "Invalid input, using all logs"
|
|
fi
|
|
;;
|
|
8)
|
|
read -p "Enter number of days: " custom_days
|
|
if [[ "$custom_days" =~ ^[0-9]+$ ]]; then
|
|
DAYS_BACK=$custom_days
|
|
else
|
|
print_error "Invalid input, using all logs"
|
|
fi
|
|
;;
|
|
*)
|
|
print_warning "Invalid choice, using all logs"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
prompt_user_scope() {
|
|
clear
|
|
print_banner "Bot Analyzer - User Scope Selection"
|
|
echo ""
|
|
echo -e " ${GREEN}1)${NC} All users (system-wide analysis)"
|
|
echo -e " ${GREEN}2)${NC} Specific user"
|
|
echo ""
|
|
read -p "Select option (1-2): " user_choice
|
|
|
|
if [ "$user_choice" = "2" ]; then
|
|
echo ""
|
|
local selected=$(select_user_interactive "Select user to analyze")
|
|
if [ $? -eq 0 ] && [ "$selected" != "ALL" ]; then
|
|
FILTER_USER="$selected"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Interactive prompts for missing options
|
|
# Prompt for time range if not specified
|
|
if [ -z "$DAYS_BACK" ] && [ -z "$HOURS_BACK" ]; then
|
|
prompt_time_range
|
|
fi
|
|
|
|
# Prompt for user if not specified
|
|
if [ -z "$FILTER_USER" ]; then
|
|
prompt_user_scope
|
|
fi
|
|
|
|
# Validate time filter options
|
|
if [ -n "$DAYS_BACK" ] && [ -n "$HOURS_BACK" ]; then
|
|
echo -e "${YELLOW}Warning: Both days and hours specified. Using hours filter only.${NC}" >&2
|
|
DAYS_BACK=""
|
|
fi
|
|
|
|
# Color codes for terminal output
|
|
RED='\033[0;31m'
|
|
YELLOW='\033[1;33m'
|
|
GREEN='\033[0;32m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Check for required commands
|
|
check_dependencies() {
|
|
local missing_deps=()
|
|
for cmd in awk grep sort uniq find sed head tail cut; do
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
missing_deps+=("$cmd")
|
|
fi
|
|
done
|
|
|
|
if [ ${#missing_deps[@]} -gt 0 ]; then
|
|
echo -e "${RED}Error: Missing required commands: ${missing_deps[*]}${NC}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Check disk space
|
|
check_disk_space() {
|
|
local available_kb
|
|
available_kb=$(df /tmp 2>/dev/null | tail -1 | awk '{print $4}')
|
|
|
|
if [ -z "$available_kb" ]; then
|
|
echo -e "${YELLOW}Warning: Cannot determine available disk space in /tmp${NC}" >&2
|
|
return
|
|
fi
|
|
|
|
if [ "$available_kb" -lt 102400 ]; then # Less than 100MB
|
|
echo -e "${YELLOW}Warning: Low disk space in /tmp: $((available_kb/1024))MB available${NC}" >&2
|
|
read -p "Continue anyway? (y/N): " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Run dependency checks
|
|
check_dependencies
|
|
check_disk_space
|
|
|
|
# Create temp directory
|
|
mkdir -p "$TEMP_DIR" || {
|
|
echo -e "${RED}Error: Cannot create temp directory: $TEMP_DIR${NC}" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Cleanup on exit
|
|
trap "rm -rf $TEMP_DIR" EXIT
|
|
|
|
#############################################################################
|
|
# Bot Signature Database
|
|
#############################################################################
|
|
# NOTE: Bot signatures now loaded from lib/bot-signatures.sh
|
|
# Arrays available: LEGIT_BOTS, AI_BOTS, MONITOR_BOTS, SUSPICIOUS_BOTS
|
|
|
|
#############################################################################
|
|
# Helper Functions
|
|
#############################################################################
|
|
|
|
print_header() {
|
|
echo -e "\n${CYAN}===============================================================${NC}"
|
|
echo -e "${CYAN}$1${NC}"
|
|
echo -e "${CYAN}===============================================================${NC}\n"
|
|
}
|
|
|
|
print_alert() {
|
|
echo -e "${RED}$1${NC}"
|
|
}
|
|
|
|
print_warning() {
|
|
echo -e "${YELLOW}$1${NC}"
|
|
}
|
|
|
|
print_info() {
|
|
echo -e "${BLUE} $1${NC}"
|
|
}
|
|
|
|
print_success() {
|
|
echo -e "${GREEN}$1${NC}"
|
|
}
|
|
|
|
#############################################################################
|
|
# Log Parsing Functions
|
|
#############################################################################
|
|
|
|
parse_logs() {
|
|
print_info "Parsing logs from: $LOG_DIR"
|
|
|
|
local find_opts=()
|
|
|
|
# Add time filter if specified (hours takes precedence over days)
|
|
if [ -n "$HOURS_BACK" ]; then
|
|
local minutes=$((HOURS_BACK * 60))
|
|
find_opts+=(-mmin -"$minutes")
|
|
print_info "Filtering logs from last $HOURS_BACK hours"
|
|
elif [ -n "$DAYS_BACK" ]; then
|
|
find_opts+=(-mtime -"$DAYS_BACK")
|
|
print_info "Filtering logs from last $DAYS_BACK days"
|
|
fi
|
|
|
|
# Parse all domain logs (excluding -bytes_log, .offset, and error_log files)
|
|
# cPanel creates files like: domain.com, domain.com-ssl_log
|
|
local file_count=0
|
|
local progress_interval=50
|
|
echo ""
|
|
find "$LOG_DIR" -type f ! -name "*-bytes_log" ! -name "*.offset" ! -name "*error_log" "${find_opts[@]}" 2>/dev/null | while read -r logfile; do
|
|
# Skip empty files
|
|
[ -s "$logfile" ] || continue
|
|
|
|
domain=$(basename "$logfile" | sed 's/-ssl_log$//')
|
|
|
|
# User filtering: skip domains not belonging to the specified user
|
|
if [ -n "$FILTER_USER" ]; then
|
|
if ! echo "$user_domains" | grep -qFx "$domain"; then
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
# Show progress every N files
|
|
file_count=$((file_count + 1))
|
|
if [ $((file_count % progress_interval)) -eq 0 ]; then
|
|
echo -ne "\r Parsed $file_count log files... (current: $domain)"
|
|
fi
|
|
|
|
# Parse Apache Combined Log Format with error handling
|
|
# Format: IP - - [timestamp] "METHOD URL PROTOCOL" STATUS SIZE "REFERRER" "USER-AGENT"
|
|
awk -v domain="$domain" '
|
|
{
|
|
# Skip empty lines and malformed entries
|
|
if (NF < 10 || length($0) < 50) next
|
|
|
|
# Extract IP (first field - space separated)
|
|
ip = $1
|
|
|
|
# Extract timestamp (between square brackets)
|
|
if (match($0, /\[([^\]]+)\]/, ts)) {
|
|
timestamp = ts[1]
|
|
} else {
|
|
timestamp = "unknown"
|
|
}
|
|
|
|
# Extract HTTP method, URL, and status
|
|
if (match($0, /"([A-Z]+) ([^ ]+) [^"]*" ([0-9]+) ([0-9-]+)/, req)) {
|
|
http_method = req[1]
|
|
request_url = req[2]
|
|
status = req[3]
|
|
size = req[4]
|
|
} else {
|
|
# Fallback for malformed requests
|
|
http_method = "-"
|
|
request_url = "-"
|
|
status = "-"
|
|
size = "0"
|
|
}
|
|
|
|
# Extract User-Agent (last quoted string)
|
|
if (match($0, /"([^"]*)"[[:space:]]*$/, ua)) {
|
|
user_agent = ua[1]
|
|
if (user_agent == "") user_agent = "-"
|
|
} else {
|
|
user_agent = "-"
|
|
}
|
|
|
|
# Only output valid entries
|
|
if (ip != "" && ip !~ /^[[:space:]]*$/) {
|
|
print ip "|" domain "|" request_url "|" status "|" size "|" user_agent "|" http_method "|" timestamp
|
|
}
|
|
}' "$logfile" 2>/dev/null
|
|
done | gzip > "$TEMP_DIR/parsed_logs.txt.gz"
|
|
|
|
# Clear the progress line
|
|
echo -ne "\r\033[K"
|
|
|
|
if [ ! -s "$TEMP_DIR/parsed_logs.txt.gz" ]; then
|
|
print_alert "No log entries were parsed. Check log format or permissions."
|
|
return 1
|
|
fi
|
|
|
|
local line_count
|
|
line_count=$(zcat "$TEMP_DIR/parsed_logs.txt.gz" | wc -l)
|
|
local file_size_kb
|
|
file_size_kb=$(du -k "$TEMP_DIR/parsed_logs.txt.gz" | cut -f1)
|
|
print_success "Logs parsed successfully ($line_count entries, ${file_size_kb}KB compressed)"
|
|
return 0
|
|
}
|
|
|
|
#############################################################################
|
|
# Bot Detection & Classification
|
|
#############################################################################
|
|
|
|
classify_bots() {
|
|
print_info "Classifying bot traffic..."
|
|
|
|
# Build combined grep patterns for efficiency
|
|
local legit_pattern=$(printf "%s|" "${!LEGIT_BOTS[@]}" | sed 's/|$//')
|
|
local ai_pattern=$(printf "%s|" "${!AI_BOTS[@]}" | sed 's/|$//')
|
|
local monitor_pattern=$(printf "%s|" "${!MONITOR_BOTS[@]}" | sed 's/|$//')
|
|
local suspicious_pattern=$(printf "%s|" "${!SUSPICIOUS_BOTS[@]}" | sed 's/|$//')
|
|
|
|
# Process logs with AWK for better performance
|
|
awk -F'|' -v legit="$legit_pattern" -v ai="$ai_pattern" -v monitor="$monitor_pattern" -v suspicious="$suspicious_pattern" '
|
|
BEGIN {
|
|
# Convert patterns to lowercase for case-insensitive matching
|
|
legit_lower = tolower(legit)
|
|
ai_lower = tolower(ai)
|
|
monitor_lower = tolower(monitor)
|
|
suspicious_lower = tolower(suspicious)
|
|
}
|
|
{
|
|
ip = $1
|
|
domain = $2
|
|
url = $3
|
|
status = $4
|
|
size = $5
|
|
ua = $6
|
|
method = $7
|
|
timestamp = $8
|
|
ua_lower = tolower(ua)
|
|
|
|
bot_type = "unknown"
|
|
bot_name = "Unknown"
|
|
|
|
# Check each category in priority order
|
|
if (legit != "" && match(ua_lower, legit_lower)) {
|
|
bot_type = "legit"
|
|
# Extract actual bot name from UA
|
|
split(legit, bots, "|")
|
|
for (i in bots) {
|
|
if (match(ua_lower, tolower(bots[i]))) {
|
|
bot_name = bots[i]
|
|
break
|
|
}
|
|
}
|
|
} else if (ai != "" && match(ua_lower, ai_lower)) {
|
|
bot_type = "ai"
|
|
split(ai, bots, "|")
|
|
for (i in bots) {
|
|
if (match(ua_lower, tolower(bots[i]))) {
|
|
bot_name = bots[i]
|
|
break
|
|
}
|
|
}
|
|
} else if (monitor != "" && match(ua_lower, monitor_lower)) {
|
|
bot_type = "monitor"
|
|
split(monitor, bots, "|")
|
|
for (i in bots) {
|
|
if (match(ua_lower, tolower(bots[i]))) {
|
|
bot_name = bots[i]
|
|
break
|
|
}
|
|
}
|
|
} else if (suspicious != "" && match(ua_lower, suspicious_lower)) {
|
|
bot_type = "suspicious"
|
|
split(suspicious, bots, "|")
|
|
for (i in bots) {
|
|
if (match(ua_lower, tolower(bots[i]))) {
|
|
bot_name = bots[i]
|
|
break
|
|
}
|
|
}
|
|
} else if (match(ua_lower, /bot|crawler|spider|scraper|curl|wget|python-|java\/|scan/)) {
|
|
# FILTER OUT legitimate browsers that might contain "bot" in version strings
|
|
# Common browsers: Chrome, Firefox, Safari, Edge, Opera, Samsung Browser, etc.
|
|
if (match(ua_lower, /chrome\/|firefox\/|safari\/|edg\/|edge\/|opr\/|opera\//) ||
|
|
match(ua_lower, /mozilla\/5\.0/) && match(ua_lower, /applewebkit|gecko/) && !match(ua_lower, /bot|crawler|spider/) ||
|
|
match(ua_lower, /samsungbrowser|ucbrowser|yabrowser|vivaldi/) ||
|
|
match(ua_lower, /android.*mobile|iphone|ipad|windows nt|macintosh|linux x86/) && !match(ua_lower, /bot|crawler|spider/)) {
|
|
# This is a legitimate browser, skip it
|
|
next
|
|
}
|
|
|
|
bot_type = "unidentified_bot"
|
|
# Extract first word of UA as bot name
|
|
match(ua, /^[^ ]+/, name)
|
|
bot_name = substr(name[0], 1, 30)
|
|
}
|
|
|
|
# Only print if bot_type is not "unknown" (i.e., we identified it as something)
|
|
if (bot_type != "unknown") {
|
|
print ip "|" domain "|" url "|" status "|" size "|" ua "|" method "|" timestamp "|" bot_type "|" bot_name
|
|
}
|
|
}' < <(zcat "$TEMP_DIR/parsed_logs.txt.gz") | gzip > "$TEMP_DIR/classified_bots.txt.gz"
|
|
|
|
if [ ! -s "$TEMP_DIR/classified_bots.txt.gz" ]; then
|
|
print_alert "Bot classification failed"
|
|
return 1
|
|
fi
|
|
|
|
local classified_count
|
|
classified_count=$(zcat "$TEMP_DIR/classified_bots.txt.gz" | wc -l)
|
|
local file_size_kb
|
|
file_size_kb=$(du -k "$TEMP_DIR/classified_bots.txt.gz" | cut -f1)
|
|
print_success "Bot classification complete ($classified_count entries, ${file_size_kb}KB compressed)"
|
|
return 0
|
|
}
|
|
|
|
#############################################################################
|
|
# Threat Detection
|
|
#############################################################################
|
|
|
|
detect_threats() {
|
|
print_info "Detecting security threats..."
|
|
|
|
# Use a single AWK pass for multiple threat detections (more efficient)
|
|
awk -F'|' '
|
|
{
|
|
ip = $1
|
|
domain = $2
|
|
url = $3
|
|
status = $4
|
|
size = $5
|
|
ua = $6
|
|
method = $7
|
|
url_lower = tolower(url)
|
|
ua_lower = tolower(ua)
|
|
|
|
# SQL Injection patterns (enhanced)
|
|
if (match(url_lower, /union.*select|concat\(|benchmark\(|sleep\(|waitfor|cast\(|exec\(/) ||
|
|
match(url_lower, /information_schema|drop table|insert into|update.*set|delete from/) ||
|
|
match(url_lower, /%27|0x[0-9a-f]+|hex\(|unhex\(|load_file\(/)) {
|
|
print ip "|" domain "|" url "|" status "|sqli" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# XSS patterns
|
|
if (match(url_lower, /<script|javascript:|onerror=|onload=|<iframe|eval\(|alert\(/) ||
|
|
match(url_lower, /document\.cookie|document\.write|\.innerhtml/)) {
|
|
print ip "|" domain "|" url "|" status "|xss" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Path Traversal / LFI
|
|
if (match(url_lower, /\.\.\/|\.\.\\|etc\/passwd|etc\/shadow|boot\.ini|win\.ini/) ||
|
|
match(url_lower, /proc\/self|\/etc\/|c:\\|windows\/system32/)) {
|
|
print ip "|" domain "|" url "|" status "|path_traversal" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Shell upload / RCE attempts
|
|
if (match(url_lower, /cmd\.exe|\/bin\/bash|\/bin\/sh|phpinfo\(|system\(|exec\(|passthru\(/) ||
|
|
match(url_lower, /shell\.php|c99\.php|r57\.php|backdoor/) ||
|
|
(match(url_lower, /\.(php|jsp|asp|aspx)/) && method == "POST")) {
|
|
print ip "|" domain "|" url "|" status "|rce_upload" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Info Disclosure attempts
|
|
if (match(url_lower, /\.git\/|\.env|\.sql$|\.bak$|\.old$|config\.php|phpinfo|readme/) ||
|
|
match(url_lower, /web\.config|composer\.json|package\.json|\.htaccess|\.htpasswd/) ||
|
|
match(url_lower, /database\.sql|backup\.zip|dump\.sql/)) {
|
|
print ip "|" domain "|" url "|" status "|info_disclosure" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Login bruteforce
|
|
if (match(url_lower, /wp-login\.php|xmlrpc\.php/) && method == "POST") {
|
|
print ip "|" domain "|" url "|" status "|login_bruteforce" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Admin/sensitive endpoint probing
|
|
if (match(url_lower, /wp-admin|phpmyadmin|admin|administrator|login|wp-login|xmlrpc/) ||
|
|
match(url_lower, /\.env|\.git|\.sql|backup|config\./)) {
|
|
print ip "|" domain "|" url > "'"$TEMP_DIR"'/admin_probes_raw.txt"
|
|
}
|
|
|
|
# 404 scanning (reconnaissance)
|
|
if (status == "404" || status == "403") {
|
|
print ip "|" domain "|" url "|" status > "'"$TEMP_DIR"'/404_scans_raw.txt"
|
|
}
|
|
|
|
# Large data transfers (potential scraping)
|
|
if (size > 1000000) {
|
|
print ip "|" domain "|" url "|" size > "'"$TEMP_DIR"'/large_transfers_raw.txt"
|
|
}
|
|
|
|
# Suspicious user agents
|
|
if (match(ua_lower, /nikto|nmap|masscan|sqlmap|havij|acunetix|nessus|burp/) ||
|
|
match(ua_lower, /metasploit|<script|null|python-requests|go-http-client/)) {
|
|
print ip "|" ua > "'"$TEMP_DIR"'/suspicious_ua_raw.txt"
|
|
}
|
|
|
|
# Track response codes for intelligence
|
|
print status > "'"$TEMP_DIR"'/response_codes_raw.txt"
|
|
}
|
|
' < <(zcat "$TEMP_DIR/parsed_logs.txt.gz")
|
|
|
|
# Process attack vectors by type
|
|
if [ -f "$TEMP_DIR/attack_vectors_raw.txt" ]; then
|
|
# Overall attack vectors summary
|
|
awk -F'|' '{print $5}' "$TEMP_DIR/attack_vectors_raw.txt" | sort | uniq -c | sort -rn > "$TEMP_DIR/attack_types.txt"
|
|
|
|
# Breakdown by attack type
|
|
for attack_type in sqli xss path_traversal rce_upload info_disclosure login_bruteforce; do
|
|
grep "|$attack_type$" "$TEMP_DIR/attack_vectors_raw.txt" | \
|
|
awk -F'|' '{print $1"|"$2"|"$3"|"$4}' | \
|
|
sort | uniq -c | sort -rn > "$TEMP_DIR/${attack_type}_attempts.txt"
|
|
done
|
|
|
|
# Old sqli file for backwards compatibility
|
|
if [ -f "$TEMP_DIR/sqli_attempts.txt" ]; then
|
|
cp "$TEMP_DIR/sqli_attempts.txt" "$TEMP_DIR/sqli_attempts_legacy.txt"
|
|
fi
|
|
else
|
|
touch "$TEMP_DIR/attack_types.txt"
|
|
fi
|
|
|
|
# Process raw data into sorted/counted results
|
|
if [ -f "$TEMP_DIR/admin_probes_raw.txt" ]; then
|
|
sort "$TEMP_DIR/admin_probes_raw.txt" | uniq -c | sort -rn > "$TEMP_DIR/admin_probes.txt"
|
|
else
|
|
touch "$TEMP_DIR/admin_probes.txt"
|
|
fi
|
|
|
|
if [ -f "$TEMP_DIR/404_scans_raw.txt" ]; then
|
|
sort "$TEMP_DIR/404_scans_raw.txt" | uniq -c | sort -rn > "$TEMP_DIR/404_scans.txt"
|
|
else
|
|
touch "$TEMP_DIR/404_scans.txt"
|
|
fi
|
|
|
|
if [ -f "$TEMP_DIR/large_transfers_raw.txt" ]; then
|
|
sort "$TEMP_DIR/large_transfers_raw.txt" | uniq -c | sort -rn > "$TEMP_DIR/large_transfers.txt"
|
|
else
|
|
touch "$TEMP_DIR/large_transfers.txt"
|
|
fi
|
|
|
|
if [ -f "$TEMP_DIR/suspicious_ua_raw.txt" ]; then
|
|
sort "$TEMP_DIR/suspicious_ua_raw.txt" | uniq -c | sort -rn > "$TEMP_DIR/suspicious_ua.txt"
|
|
else
|
|
touch "$TEMP_DIR/suspicious_ua.txt"
|
|
fi
|
|
|
|
# Process response codes
|
|
if [ -f "$TEMP_DIR/response_codes_raw.txt" ]; then
|
|
sort "$TEMP_DIR/response_codes_raw.txt" | uniq -c | sort -rn > "$TEMP_DIR/response_codes.txt"
|
|
else
|
|
touch "$TEMP_DIR/response_codes.txt"
|
|
fi
|
|
|
|
print_success "Threat detection complete"
|
|
}
|
|
|
|
#############################################################################
|
|
# Botnet Detection
|
|
#############################################################################
|
|
|
|
detect_botnets() {
|
|
print_info "Analyzing for botnet patterns..."
|
|
|
|
# Group IPs by similar behavior patterns
|
|
# Pattern 1: Multiple IPs hitting same URLs in coordinated manner
|
|
zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $1"|"$3}' | \
|
|
sort | uniq -c | awk '$1 > 10 {print $2}' | \
|
|
cut -d'|' -f2 | sort | uniq -c | sort -rn | \
|
|
awk '$1 > 5 {print $2}' > "$TEMP_DIR/coordinated_urls.txt"
|
|
|
|
# Pattern 2: IPs with similar User-Agents hitting multiple domains
|
|
zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $1"|"$6}' | \
|
|
sort | uniq > "$TEMP_DIR/ip_ua_pairs.txt"
|
|
|
|
# Pattern 3: Detect IP ranges (Class C networks) with suspicious activity
|
|
zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $1}' | \
|
|
awk -F'.' '{print $1"."$2"."$3".0/24"}' | \
|
|
sort | uniq -c | sort -rn | awk '$1 > 20' > "$TEMP_DIR/suspicious_networks.txt"
|
|
|
|
# Pattern 4: Rapid fire requests (DDoS indicators)
|
|
# Extract timestamp and count requests per IP per minute
|
|
zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{
|
|
ip = $1
|
|
timestamp = $8
|
|
# Extract date/time components (handles format: DD/MMM/YYYY:HH:MM:SS)
|
|
if (match(timestamp, /([0-9]{2})\/([A-Za-z]{3})\/([0-9]{4}):([0-9]{2}):([0-9]{2})/, ts)) {
|
|
# Group by hour:minute for rapid-fire detection
|
|
time_key = ts[3] ts[2] ts[1] "_" ts[4] ts[5]
|
|
print ip "|" time_key
|
|
}
|
|
}' | \
|
|
sort | uniq -c | \
|
|
awk '$1 > 50 {print $1 " " $2}' | \
|
|
awk -F'|' '{print $1}' | \
|
|
awk '{ip=$2; count=$1; sum[ip]+=count; max[ip]=(count>max[ip]?count:max[ip])} END {for(ip in sum) print sum[ip], ip, max[ip]}' | \
|
|
sort -rn > "$TEMP_DIR/rapid_fire_ips.txt"
|
|
|
|
print_success "Botnet analysis complete"
|
|
}
|
|
|
|
#############################################################################
|
|
# Server IP Detection
|
|
#############################################################################
|
|
|
|
detect_server_ips() {
|
|
print_info "Detecting server's own IP addresses..."
|
|
|
|
> "$TEMP_DIR/server_ips.txt"
|
|
|
|
# Method 1: Get all IPs from network interfaces
|
|
if command -v hostname >/dev/null 2>&1; then
|
|
hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' >> "$TEMP_DIR/server_ips.txt"
|
|
fi
|
|
|
|
# Method 2: Parse ip addr output
|
|
if command -v ip >/dev/null 2>&1; then
|
|
ip addr show 2>/dev/null | grep -oP 'inet \K[\d.]+' >> "$TEMP_DIR/server_ips.txt"
|
|
fi
|
|
|
|
# Method 3: Try ifconfig as fallback
|
|
if command -v ifconfig >/dev/null 2>&1; then
|
|
ifconfig 2>/dev/null | grep -oP 'inet (addr:)?\K[\d.]+' >> "$TEMP_DIR/server_ips.txt"
|
|
fi
|
|
|
|
# Method 4: Get public IP from external services (with timeout)
|
|
# Try multiple services for reliability
|
|
for service in "ifconfig.me/ip" "icanhazip.com" "ipecho.net/plain" "api.ipify.org"; do
|
|
public_ip=$(curl -s --max-time 3 "$service" 2>/dev/null | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')
|
|
if [ -n "$public_ip" ]; then
|
|
echo "$public_ip" >> "$TEMP_DIR/server_ips.txt"
|
|
break
|
|
fi
|
|
done
|
|
|
|
# Method 5: Check cPanel server IP if available
|
|
if [ -f "/var/cpanel/mainip" ]; then
|
|
cat /var/cpanel/mainip >> "$TEMP_DIR/server_ips.txt"
|
|
fi
|
|
|
|
# Remove duplicates and empty lines
|
|
sort -u "$TEMP_DIR/server_ips.txt" | grep -v '^$' > "$TEMP_DIR/server_ips_final.txt"
|
|
mv "$TEMP_DIR/server_ips_final.txt" "$TEMP_DIR/server_ips.txt"
|
|
|
|
server_ip_count=$(wc -l < "$TEMP_DIR/server_ips.txt" 2>/dev/null || echo 0)
|
|
|
|
if [ "$server_ip_count" -gt 0 ]; then
|
|
print_success "Detected $server_ip_count server IP(s) - these will be excluded from threat analysis"
|
|
else
|
|
print_warning "Could not detect server IPs automatically - proceeding without server IP filtering"
|
|
fi
|
|
}
|
|
|
|
# Helper function to check if an IP should be excluded
|
|
is_excluded_ip() {
|
|
local ip="$1"
|
|
|
|
# Check if private/internal IP
|
|
if [[ "$ip" =~ ^127\. ]] || \
|
|
[[ "$ip" =~ ^10\. ]] || \
|
|
[[ "$ip" =~ ^192\.168\. ]] || \
|
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]] || \
|
|
[[ "$ip" =~ ^169\.254\. ]] || \
|
|
[[ "$ip" == "localhost" ]] || \
|
|
[[ "$ip" == "::1" ]]; then
|
|
return 0 # True - should be excluded
|
|
fi
|
|
|
|
# Check if it's the server's own IP
|
|
if [ -f "$TEMP_DIR/server_ips.txt" ]; then
|
|
if grep -qFx "$ip" "$TEMP_DIR/server_ips.txt" 2>/dev/null; then
|
|
return 0 # True - should be excluded
|
|
fi
|
|
fi
|
|
|
|
return 1 # False - should not be excluded
|
|
}
|
|
|
|
#############################################################################
|
|
# Time-Series Analysis
|
|
#############################################################################
|
|
|
|
analyze_time_series() {
|
|
print_info "Analyzing time-series patterns..."
|
|
|
|
# Extract hourly bot traffic
|
|
zcat "$TEMP_DIR/classified_bots.txt.gz" | awk -F'|' '$9 != "unknown" {
|
|
timestamp = $8
|
|
if (match(timestamp, /([0-9]{2})\/([A-Za-z]{3})\/([0-9]{4}):([0-9]{2}):([0-9]{2}):([0-9]{2})/, ts)) {
|
|
hour = ts[4]
|
|
print hour
|
|
}
|
|
}' | sort | uniq -c > "$TEMP_DIR/hourly_bot_traffic.txt"
|
|
|
|
# Extract hourly attack traffic
|
|
if [ -f "$TEMP_DIR/attack_vectors_raw.txt" ]; then
|
|
# Parse timestamps from original parsed logs for IPs in attack vectors
|
|
awk -F'|' 'NR==FNR {ips[$1]=1; next} $1 in ips {
|
|
timestamp = $8
|
|
if (match(timestamp, /([0-9]{2})\/([A-Za-z]{3})\/([0-9]{4}):([0-9]{2}):([0-9]{2}):([0-9]{2})/, ts)) {
|
|
hour = ts[4]
|
|
print hour
|
|
}
|
|
}' "$TEMP_DIR/attack_vectors_raw.txt" <(zcat "$TEMP_DIR/parsed_logs.txt.gz") | sort | uniq -c > "$TEMP_DIR/hourly_attack_traffic.txt"
|
|
fi
|
|
|
|
print_success "Time-series analysis complete"
|
|
}
|
|
|
|
#############################################################################
|
|
# Threat Scoring
|
|
#############################################################################
|
|
|
|
calculate_threat_scores() {
|
|
print_info "Calculating threat scores..."
|
|
|
|
# Pre-count requests per IP (MUCH faster than grepping for each IP)
|
|
declare -A ip_request_counts
|
|
while IFS='|' read -r ip rest; do
|
|
((ip_request_counts["$ip"]++))
|
|
done < <(zcat "$TEMP_DIR/parsed_logs.txt.gz")
|
|
|
|
# Build hash tables from threat files for O(1) lookups
|
|
declare -A threat_ips_sqli threat_ips_xss threat_ips_path threat_ips_rce threat_ips_login
|
|
declare -A threat_ips_suspicious threat_ips_ddos threat_admin_count threat_404_count
|
|
|
|
# Parse each threat file and build hash tables
|
|
[ -f "$TEMP_DIR/sqli_attempts.txt" ] && while read -r line; do
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
[ -n "$ip" ] && threat_ips_sqli["$ip"]=1
|
|
done < "$TEMP_DIR/sqli_attempts.txt"
|
|
|
|
[ -f "$TEMP_DIR/xss_attempts.txt" ] && while read -r line; do
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
[ -n "$ip" ] && threat_ips_xss["$ip"]=1
|
|
done < "$TEMP_DIR/xss_attempts.txt"
|
|
|
|
[ -f "$TEMP_DIR/path_traversal_attempts.txt" ] && while read -r line; do
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
[ -n "$ip" ] && threat_ips_path["$ip"]=1
|
|
done < "$TEMP_DIR/path_traversal_attempts.txt"
|
|
|
|
[ -f "$TEMP_DIR/rce_upload_attempts.txt" ] && while read -r line; do
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
[ -n "$ip" ] && threat_ips_rce["$ip"]=1
|
|
done < "$TEMP_DIR/rce_upload_attempts.txt"
|
|
|
|
[ -f "$TEMP_DIR/login_bruteforce_attempts.txt" ] && while read -r line; do
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
[ -n "$ip" ] && threat_ips_login["$ip"]=1
|
|
done < "$TEMP_DIR/login_bruteforce_attempts.txt"
|
|
|
|
[ -f "$TEMP_DIR/suspicious_ua.txt" ] && while read -r line; do
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
[ -n "$ip" ] && threat_ips_suspicious["$ip"]=1
|
|
done < "$TEMP_DIR/suspicious_ua.txt"
|
|
|
|
[ -f "$TEMP_DIR/rapid_fire_ips.txt" ] && while read -r line; do
|
|
ip=$(echo "$line" | awk '{print $2}')
|
|
[ -n "$ip" ] && threat_ips_ddos["$ip"]=1
|
|
done < "$TEMP_DIR/rapid_fire_ips.txt"
|
|
|
|
[ -f "$TEMP_DIR/admin_probes.txt" ] && while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
[ -n "$ip" ] && threat_admin_count["$ip"]=$count
|
|
done < "$TEMP_DIR/admin_probes.txt"
|
|
|
|
[ -f "$TEMP_DIR/404_scans.txt" ] && while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
[ -n "$ip" ] && threat_404_count["$ip"]=$count
|
|
done < "$TEMP_DIR/404_scans.txt"
|
|
|
|
# Now calculate scores for each IP (using pre-counted requests)
|
|
for ip in "${!ip_request_counts[@]}"; do
|
|
# Skip excluded IPs
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
|
|
score=0
|
|
req_count=${ip_request_counts[$ip]}
|
|
|
|
# Base request volume (max 10 points)
|
|
if [ "$req_count" -gt 10000 ]; then score=$((score + 10))
|
|
elif [ "$req_count" -gt 5000 ]; then score=$((score + 8))
|
|
elif [ "$req_count" -gt 1000 ]; then score=$((score + 5))
|
|
elif [ "$req_count" -gt 500 ]; then score=$((score + 3))
|
|
fi
|
|
|
|
# Attack patterns
|
|
[ -n "${threat_ips_sqli[$ip]}" ] && score=$((score + 15))
|
|
[ -n "${threat_ips_xss[$ip]}" ] && score=$((score + 12))
|
|
[ -n "${threat_ips_path[$ip]}" ] && score=$((score + 15))
|
|
[ -n "${threat_ips_rce[$ip]}" ] && score=$((score + 20))
|
|
[ -n "${threat_ips_login[$ip]}" ] && score=$((score + 10))
|
|
[ -n "${threat_ips_suspicious[$ip]}" ] && score=$((score + 10))
|
|
[ -n "${threat_ips_ddos[$ip]}" ] && score=$((score + 10))
|
|
|
|
# Admin probing
|
|
admin_count=${threat_admin_count[$ip]:-0}
|
|
[ "$admin_count" -gt 20 ] 2>/dev/null && score=$((score + 5))
|
|
|
|
# 404 scanning
|
|
scan_404=${threat_404_count[$ip]:-0}
|
|
[ "$scan_404" -gt 50 ] 2>/dev/null && score=$((score + 3))
|
|
|
|
# Threat Intelligence Enrichment (from external sources)
|
|
# Check AbuseIPDB reputation
|
|
local abuse_data=$(check_abuseipdb "$ip" 2>/dev/null || echo "0|0|Unknown|Unknown")
|
|
IFS='|' read -r abuse_confidence abuse_reports abuse_country abuse_isp <<< "$abuse_data"
|
|
|
|
# Add bonus for known malicious IPs
|
|
if [ "$abuse_confidence" -ge 75 ]; then
|
|
score=$((score + 15)) # High confidence malicious
|
|
elif [ "$abuse_confidence" -ge 50 ]; then
|
|
score=$((score + 8)) # Moderate confidence
|
|
elif [ "$abuse_confidence" -ge 25 ]; then
|
|
score=$((score + 3)) # Low confidence
|
|
fi
|
|
|
|
# Geographic risk assessment
|
|
local geo_country=$(get_country_code "$ip" 2>/dev/null || echo "XX")
|
|
if is_high_risk_country "$geo_country" 2>/dev/null; then
|
|
score=$((score + 5)) # High-risk country bonus
|
|
fi
|
|
|
|
# Cap at 100
|
|
[ $score -gt 100 ] && score=100
|
|
|
|
# Only output IPs with score > 0
|
|
[ $score -gt 0 ] && echo "$score|$ip|$req_count"
|
|
|
|
# Track in centralized IP reputation database (background process)
|
|
if [ $score -gt 0 ]; then
|
|
(
|
|
# Update IP with hit count
|
|
increment_ip_hits "$ip" "$req_count" >/dev/null 2>&1
|
|
|
|
# Tag with specific attack types found
|
|
[ -n "${threat_ips_sqli[$ip]}" ] && flag_ip_attack "$ip" "SQL_INJECTION" 0 "Bot analyzer: SQL injection attempts" >/dev/null 2>&1
|
|
[ -n "${threat_ips_xss[$ip]}" ] && flag_ip_attack "$ip" "XSS" 0 "Bot analyzer: XSS attempts" >/dev/null 2>&1
|
|
[ -n "${threat_ips_path[$ip]}" ] && flag_ip_attack "$ip" "PATH_TRAVERSAL" 0 "Bot analyzer: Path traversal" >/dev/null 2>&1
|
|
[ -n "${threat_ips_rce[$ip]}" ] && flag_ip_attack "$ip" "RCE" 0 "Bot analyzer: RCE/shell upload attempts" >/dev/null 2>&1
|
|
[ -n "${threat_ips_login[$ip]}" ] && flag_ip_attack "$ip" "BRUTEFORCE" 0 "Bot analyzer: Login bruteforce" >/dev/null 2>&1
|
|
[ -n "${threat_ips_ddos[$ip]}" ] && flag_ip_attack "$ip" "DDOS" 0 "Bot analyzer: Rapid-fire requests" >/dev/null 2>&1
|
|
[ -n "${threat_ips_suspicious[$ip]}" ] && flag_ip_attack "$ip" "SCANNER" 0 "Bot analyzer: Suspicious user-agent" >/dev/null 2>&1
|
|
) &
|
|
fi
|
|
done | sort -t'|' -k1 -rn > "$TEMP_DIR/threat_scores.txt"
|
|
|
|
# Wait for background IP reputation updates to complete
|
|
wait
|
|
|
|
print_success "Threat scores calculated and IP reputation updated"
|
|
}
|
|
|
|
#############################################################################
|
|
# False Positive Detection
|
|
#############################################################################
|
|
|
|
detect_false_positives() {
|
|
print_info "Detecting legitimate services (false positives)..."
|
|
|
|
# Known monitoring service patterns
|
|
zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{
|
|
ip = $1
|
|
domain = $2
|
|
url = $3
|
|
ua = tolower($6)
|
|
|
|
# Pingdom
|
|
if (match(ua, /pingdom/) || match(ua, /pingdom\.com_bot/)) {
|
|
print ip "|Pingdom Monitoring|" ua "|" domain
|
|
}
|
|
# UptimeRobot
|
|
else if (match(ua, /uptimerobot/)) {
|
|
print ip "|UptimeRobot Monitoring|" ua "|" domain
|
|
}
|
|
# StatusCake
|
|
else if (match(ua, /statuscake/)) {
|
|
print ip "|StatusCake Monitoring|" ua "|" domain
|
|
}
|
|
# WordPress cache preload (WP Rocket, Hummingbird)
|
|
else if (match(url, /admin-ajax\.php.*cache_preload/) || match(url, /admin-ajax\.php.*wphb/)) {
|
|
print ip "|WordPress Cache Preload|" ua "|" domain
|
|
}
|
|
# Legitimate backup services
|
|
else if (match(ua, /jetpack|vaultpress|updraftplus/)) {
|
|
print ip "|Backup Service|" ua "|" domain
|
|
}
|
|
}' | sort -u > "$TEMP_DIR/false_positives.txt"
|
|
|
|
print_success "False positive detection complete"
|
|
}
|
|
|
|
#############################################################################
|
|
# Statistical Analysis
|
|
#############################################################################
|
|
|
|
generate_statistics() {
|
|
print_info "Generating statistics..."
|
|
|
|
# Top 5 bots by request count
|
|
zcat "$TEMP_DIR/classified_bots.txt.gz" | awk -F'|' '$9 != "unknown" {print $10}' | \
|
|
sort | uniq -c | sort -rn | head -5 > "$TEMP_DIR/top_bots.txt"
|
|
|
|
# Top 5 most-hit sites
|
|
zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $2}' | \
|
|
sort | uniq -c | sort -rn | head -5 > "$TEMP_DIR/top_sites.txt"
|
|
|
|
# Top 5 most-hit URLs
|
|
zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $2"|"$3}' | \
|
|
sort | uniq -c | sort -rn | head -5 > "$TEMP_DIR/top_urls.txt"
|
|
|
|
# Top 5 IP addresses by request count
|
|
zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $1}' | \
|
|
sort | uniq -c | sort -rn | head -5 > "$TEMP_DIR/top_ips.txt"
|
|
|
|
# Traffic breakdown by bot type
|
|
zcat "$TEMP_DIR/classified_bots.txt.gz" | awk -F'|' '{print $9}' | \
|
|
sort | uniq -c | sort -rn > "$TEMP_DIR/traffic_breakdown.txt"
|
|
|
|
# Per-domain traffic sources
|
|
while read -r domain; do
|
|
echo "$domain" > "$TEMP_DIR/domain_${domain}_stats.txt"
|
|
zcat "$TEMP_DIR/classified_bots.txt.gz" | grep "|$domain|" | \
|
|
awk -F'|' '{print $9}' | sort | uniq -c | sort -rn >> "$TEMP_DIR/domain_${domain}_stats.txt"
|
|
done < <(zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $2}' | sort -u)
|
|
|
|
print_success "Statistics generated"
|
|
}
|
|
|
|
#############################################################################
|
|
# Report Generation
|
|
#############################################################################
|
|
|
|
generate_report() {
|
|
exec > >(tee "$OUTPUT_FILE")
|
|
|
|
echo "==============================================================="
|
|
echo " APACHE/CPANEL BOT & BOTNET ANALYSIS REPORT"
|
|
echo " Generated: $(date '+%Y-%m-%d %H:%M:%S')"
|
|
echo "==============================================================="
|
|
|
|
# CRITICAL ALERTS SECTION
|
|
print_header "CRITICAL ALERTS"
|
|
|
|
alert_count=0
|
|
|
|
# Check for attack vectors
|
|
if [ -s "$TEMP_DIR/attack_types.txt" ]; then
|
|
print_alert "Security Attack Vectors Detected:"
|
|
while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
attack_type=$(echo "$line" | awk '{print $2}')
|
|
|
|
case $attack_type in
|
|
sqli) echo " SQL Injection: $count attempts" ;;
|
|
xss) echo " XSS Attacks: $count attempts" ;;
|
|
path_traversal) echo " Path Traversal: $count attempts" ;;
|
|
rce_upload) echo " RCE/Shell Upload: $count attempts" ;;
|
|
info_disclosure) echo " Info Disclosure: $count attempts" ;;
|
|
login_bruteforce) echo " Login Bruteforce: $count attempts" ;;
|
|
esac
|
|
done < "$TEMP_DIR/attack_types.txt"
|
|
echo ""
|
|
alert_count=$((alert_count + 1))
|
|
fi
|
|
|
|
# Check for suspicious scanners
|
|
if [ -s "$TEMP_DIR/suspicious_ua.txt" ]; then
|
|
scanner_count=$(wc -l < "$TEMP_DIR/suspicious_ua.txt")
|
|
print_alert "Malicious scanners detected: $scanner_count IPs"
|
|
echo " Top scanners:"
|
|
head -3 "$TEMP_DIR/suspicious_ua.txt" | while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
ua=$(echo "$line" | cut -d'|' -f2)
|
|
printf " %s requests - IP: %s - UA: %s\n" "$count" "$ip" "$ua"
|
|
done
|
|
echo ""
|
|
alert_count=$((alert_count + 1))
|
|
fi
|
|
|
|
# Check for rapid-fire IPs (potential DDoS)
|
|
if [ -s "$TEMP_DIR/rapid_fire_ips.txt" ]; then
|
|
ddos_count=$(wc -l < "$TEMP_DIR/rapid_fire_ips.txt")
|
|
print_alert "Potential DDoS sources: $ddos_count IPs with >50 req/min"
|
|
echo " Top offenders:"
|
|
head -3 "$TEMP_DIR/rapid_fire_ips.txt" | awk '{print " "$2" - "$1" rapid requests"}'
|
|
echo ""
|
|
alert_count=$((alert_count + 1))
|
|
fi
|
|
|
|
# Check for suspicious networks
|
|
if [ -s "$TEMP_DIR/suspicious_networks.txt" ]; then
|
|
net_count=$(wc -l < "$TEMP_DIR/suspicious_networks.txt")
|
|
print_alert "Suspicious networks detected: $net_count Class C ranges"
|
|
echo " Top networks:"
|
|
head -3 "$TEMP_DIR/suspicious_networks.txt" | awk '{print " "$2" - "$1" requests"}'
|
|
echo ""
|
|
alert_count=$((alert_count + 1))
|
|
fi
|
|
|
|
if [ $alert_count -eq 0 ]; then
|
|
print_success "No critical threats detected"
|
|
fi
|
|
|
|
# QUICK STATS DASHBOARD
|
|
print_header "QUICK STATS DASHBOARD"
|
|
|
|
total_requests=$(zcat "$TEMP_DIR/parsed_logs.txt.gz" | wc -l)
|
|
unique_ips=$(zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $1}' | sort -u | wc -l)
|
|
unique_domains=$(zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $2}' | sort -u | wc -l)
|
|
bot_requests=$(zcat "$TEMP_DIR/classified_bots.txt.gz" | awk -F'|' '$9 != "unknown"' | wc -l)
|
|
|
|
# Count private/internal IPs (excluded from threat analysis)
|
|
private_ips=$(zcat "$TEMP_DIR/parsed_logs.txt.gz" | awk -F'|' '{print $1}' | sort -u | grep -E '^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.)' | wc -l)
|
|
|
|
# Count server's own IPs in the logs
|
|
server_ip_hits=0
|
|
if [ -f "$TEMP_DIR/server_ips.txt" ] && [ -s "$TEMP_DIR/server_ips.txt" ]; then
|
|
while read -r server_ip; do
|
|
if zcat "$TEMP_DIR/parsed_logs.txt.gz" | grep -q "^$server_ip|" 2>/dev/null; then
|
|
server_ip_hits=$((server_ip_hits + 1))
|
|
fi
|
|
done < "$TEMP_DIR/server_ips.txt"
|
|
fi
|
|
|
|
echo "Total Requests: $(printf "%'d" $total_requests)"
|
|
echo "Unique IPs: $(printf "%'d" $unique_ips)"
|
|
|
|
# Show breakdown if we have excluded IPs
|
|
if [ "$private_ips" -gt 0 ] || [ "$server_ip_hits" -gt 0 ]; then
|
|
excluded_total=$((private_ips + server_ip_hits))
|
|
echo " ├─ Excluded IPs: $(printf "%'d" $excluded_total)"
|
|
[ "$private_ips" -gt 0 ] && echo " │ ├─ Private/Internal: $private_ips"
|
|
[ "$server_ip_hits" -gt 0 ] && echo " │ └─ Server's own: $server_ip_hits"
|
|
echo " └─ External IPs: $(printf "%'d" $((unique_ips - excluded_total)))"
|
|
fi
|
|
|
|
echo "Domains Analyzed: $unique_domains"
|
|
echo "Bot Requests: $(printf "%'d" $bot_requests) ($(awk "BEGIN {printf \"%.1f\", ($bot_requests/$total_requests)*100}")%)"
|
|
|
|
# Show detected server IPs
|
|
if [ -f "$TEMP_DIR/server_ips.txt" ] && [ -s "$TEMP_DIR/server_ips.txt" ]; then
|
|
echo ""
|
|
echo " Server IPs Detected (excluded from threat analysis):"
|
|
while read -r server_ip; do
|
|
echo " • $server_ip"
|
|
done < "$TEMP_DIR/server_ips.txt"
|
|
fi
|
|
echo ""
|
|
|
|
# Traffic breakdown
|
|
echo "Traffic Breakdown:"
|
|
while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
type=$(echo "$line" | awk '{print $2}')
|
|
pct=$(awk "BEGIN {printf \"%.1f\", ($count/$total_requests)*100}")
|
|
|
|
case $type in
|
|
legit) echo " Legitimate Bots: $(printf "%'7d" $count) ($pct%)" ;;
|
|
ai) echo " AI Bots: $(printf "%'7d" $count) ($pct%)" ;;
|
|
monitor) echo " 📡 Monitoring/SEO: $(printf "%'7d" $count) ($pct%)" ;;
|
|
suspicious) echo " Suspicious Bots: $(printf "%'7d" $count) ($pct%)" ;;
|
|
unidentified_bot) echo " ❓ Unidentified Bots: $(printf "%'7d" $count) ($pct%)" ;;
|
|
unknown) echo " Regular Traffic: $(printf "%'7d" $count) ($pct%)" ;;
|
|
esac
|
|
done < "$TEMP_DIR/traffic_breakdown.txt"
|
|
|
|
# TIME-SERIES ANALYSIS
|
|
if [ -s "$TEMP_DIR/hourly_bot_traffic.txt" ]; then
|
|
echo ""
|
|
echo "Bot Traffic Timeline (hourly):"
|
|
max_bot_traffic=$(awk '{print $1}' "$TEMP_DIR/hourly_bot_traffic.txt" | sort -rn | head -1)
|
|
while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
hour=$(echo "$line" | awk '{print $2}')
|
|
# Create simple bar chart
|
|
bar_width=$((count * 10 / max_bot_traffic))
|
|
[ $bar_width -eq 0 ] && [ $count -gt 0 ] && bar_width=1
|
|
bar=$(printf '█%.0s' $(seq 1 $bar_width))
|
|
spaces=$(printf '░%.0s' $(seq 1 $((10 - bar_width))))
|
|
|
|
# Detect spikes (>2x average)
|
|
avg_traffic=$((total_requests / 24))
|
|
spike=""
|
|
[ $count -gt $((avg_traffic * 2)) ] && spike=" SPIKE"
|
|
|
|
# Strip leading zeros to avoid octal interpretation
|
|
hour_num=$((10#$hour))
|
|
next_hour=$((hour_num + 1))
|
|
printf " %02d:00-%02d:00: %s%s %'6d bot requests%s\n" "$hour_num" "$next_hour" "$bar" "$spaces" "$count" "$spike"
|
|
done < "$TEMP_DIR/hourly_bot_traffic.txt"
|
|
fi
|
|
|
|
# RESPONSE CODE INTELLIGENCE
|
|
if [ -s "$TEMP_DIR/response_codes.txt" ]; then
|
|
echo ""
|
|
echo "Response Code Analysis:"
|
|
while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
code=$(echo "$line" | awk '{print $2}')
|
|
pct=$(awk "BEGIN {printf \"%.1f\", ($count/$total_requests)*100}")
|
|
|
|
case $code in
|
|
200) echo " 200 (Success): $(printf "%'7d" $count) ($pct%) Bots are getting data" ;;
|
|
404) echo " 404 (Not Found): $(printf "%'7d" $count) ($pct%) Scanning for vulnerabilities" ;;
|
|
403) echo " 403 (Forbidden): $(printf "%'7d" $count) ($pct%) Blocked by existing rules" ;;
|
|
401) echo " 401 (Unauthorized):$(printf "%'7d" $count) ($pct%) Login attempts failing" ;;
|
|
500|502|503) echo " $code (Server Error):$(printf "%'7d" $count) ($pct%) Check if exploit triggered" ;;
|
|
301|302) echo " $code (Redirect): $(printf "%'7d" $count) ($pct%)" ;;
|
|
*) echo " $code: $(printf "%'7d" $count) ($pct%)" ;;
|
|
esac
|
|
done < "$TEMP_DIR/response_codes.txt" | head -7
|
|
fi
|
|
|
|
# FALSE POSITIVE WARNINGS
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ]; then
|
|
echo ""
|
|
echo "Whitelist Recommendations (Legitimate Services):"
|
|
while read -r line; do
|
|
ip=$(echo "$line" | cut -d'|' -f1)
|
|
service=$(echo "$line" | cut -d'|' -f2)
|
|
domain=$(echo "$line" | cut -d'|' -f4)
|
|
req_count=$(grep -c "^$ip|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null || echo 0)
|
|
echo " $ip - $req_count requests - Identified as: $service"
|
|
echo " → Domain: $domain"
|
|
echo " → Action: VERIFY OWNERSHIP then whitelist"
|
|
done < "$TEMP_DIR/false_positives.txt" | head -6
|
|
fi
|
|
|
|
# TOP 5 THREATS
|
|
print_header "TOP 5 THREATS (with recommended actions)"
|
|
|
|
echo "1. Highest Risk IPs (by threat score):"
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
counter=1
|
|
while read -r line && [ $counter -le 10 ]; do
|
|
score=$(echo "$line" | cut -d'|' -f1)
|
|
ip=$(echo "$line" | cut -d'|' -f2)
|
|
count=$(echo "$line" | cut -d'|' -f3)
|
|
|
|
# Determine threat level and action based on score
|
|
if [ "$score" -ge 80 ]; then
|
|
threat_level="CRITICAL"
|
|
threat_icon=""
|
|
action="BLOCK IMMEDIATELY + INVESTIGATE"
|
|
echo -e " ${RED}[$counter] $ip - RISK: $score/100 $threat_icon $threat_level${NC}"
|
|
elif [ "$score" -ge 60 ]; then
|
|
threat_level="HIGH"
|
|
threat_icon=""
|
|
action="BLOCK or AGGRESSIVE RATE LIMIT"
|
|
echo -e " ${YELLOW}[$counter] $ip - RISK: $score/100 $threat_icon $threat_level${NC}"
|
|
elif [ "$score" -ge 40 ]; then
|
|
threat_level="MODERATE"
|
|
threat_icon=""
|
|
action="RATE LIMIT RECOMMENDED"
|
|
echo " [$counter] $ip - RISK: $score/100 $threat_icon $threat_level"
|
|
else
|
|
threat_level="LOW"
|
|
threat_icon=""
|
|
action="MONITOR"
|
|
echo " [$counter] $ip - RISK: $score/100 $threat_icon $threat_level"
|
|
fi
|
|
|
|
echo " $count requests - Action: $action"
|
|
|
|
# Show which attack vectors this IP used
|
|
attack_types=""
|
|
grep -q "$ip" "$TEMP_DIR/sqli_attempts.txt" 2>/dev/null && attack_types="${attack_types}SQL-Injection "
|
|
grep -q "$ip" "$TEMP_DIR/xss_attempts.txt" 2>/dev/null && attack_types="${attack_types}XSS "
|
|
grep -q "$ip" "$TEMP_DIR/path_traversal_attempts.txt" 2>/dev/null && attack_types="${attack_types}Path-Traversal "
|
|
grep -q "$ip" "$TEMP_DIR/rce_upload_attempts.txt" 2>/dev/null && attack_types="${attack_types}RCE/Upload "
|
|
grep -q "$ip" "$TEMP_DIR/login_bruteforce_attempts.txt" 2>/dev/null && attack_types="${attack_types}Login-Bruteforce "
|
|
grep -q "$ip" "$TEMP_DIR/suspicious_ua.txt" 2>/dev/null && attack_types="${attack_types}Scanner-UA "
|
|
grep -q "$ip" "$TEMP_DIR/rapid_fire_ips.txt" 2>/dev/null && attack_types="${attack_types}DDoS-Pattern "
|
|
|
|
[ -n "$attack_types" ] && echo " Attack vectors: $attack_types"
|
|
|
|
counter=$((counter + 1))
|
|
done < "$TEMP_DIR/threat_scores.txt"
|
|
else
|
|
echo " No significant threats detected "
|
|
fi
|
|
echo ""
|
|
|
|
echo "2. Top Aggressive Bots:"
|
|
counter=1
|
|
while read -r line && [ $counter -le 5 ]; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
bot=$(echo "$line" | awk '{$1=""; print $0}' | xargs)
|
|
|
|
action="Allow"
|
|
if echo "$bot" | grep -qiE "ahrefs|semrush|dotbot|blex|megaindex"; then
|
|
action="Consider blocking (aggressive)"
|
|
fi
|
|
|
|
echo " [$counter] $bot - $count requests - Action: $action"
|
|
counter=$((counter + 1))
|
|
done < "$TEMP_DIR/top_bots.txt"
|
|
echo ""
|
|
|
|
echo "3. Admin Endpoint Probing:"
|
|
if [ -s "$TEMP_DIR/admin_probes.txt" ]; then
|
|
head -3 "$TEMP_DIR/admin_probes.txt" | while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
domain=$(echo "$line" | awk '{print $2}' | cut -d'|' -f2)
|
|
url=$(echo "$line" | awk '{print $2}' | cut -d'|' -f3)
|
|
printf " %s attempts - IP: %s - %s%s\n" "$count" "$ip" "$domain" "$url"
|
|
done
|
|
echo " Action: Verify legitimate admin access or block"
|
|
else
|
|
echo " None detected "
|
|
fi
|
|
echo ""
|
|
|
|
echo "4. 404 Scanners (Reconnaissance):"
|
|
if [ -s "$TEMP_DIR/404_scans.txt" ]; then
|
|
head -3 "$TEMP_DIR/404_scans.txt" | awk '$1 > 10 {
|
|
count = $1
|
|
$1 = ""
|
|
gsub(/^[[:space:]]+\|?/, "")
|
|
split($0, parts, "|")
|
|
printf " %s failed requests - IP: %s - %s%s\n", count, parts[1], parts[2], parts[3]
|
|
}'
|
|
else
|
|
echo " None detected "
|
|
fi
|
|
echo ""
|
|
|
|
echo "5. Large Data Transfers:"
|
|
if [ -s "$TEMP_DIR/large_transfers.txt" ]; then
|
|
# Calculate total bot bandwidth
|
|
total_bot_bandwidth=0
|
|
if [ -f "$TEMP_DIR/classified_bots.txt" ]; then
|
|
total_bot_bandwidth=$(awk -F'|' '$9 != "unknown" && $5 ~ /^[0-9]+$/ {sum += $5} END {print sum}' "$TEMP_DIR/classified_bots.txt")
|
|
fi
|
|
|
|
if [ -n "$total_bot_bandwidth" ] && [ "$total_bot_bandwidth" -gt 0 ]; then
|
|
bot_bandwidth_mb=$(awk "BEGIN {printf \"%.0f\", $total_bot_bandwidth/1048576}")
|
|
bot_bandwidth_gb=$(awk "BEGIN {printf \"%.2f\", $total_bot_bandwidth/1073741824}")
|
|
# Estimate cost at $0.09/GB (typical CDN pricing)
|
|
estimated_cost=$(awk "BEGIN {printf \"%.2f\", ($total_bot_bandwidth/1073741824) * 0.09}")
|
|
|
|
total_bandwidth=$(awk -F'|' '$5 ~ /^[0-9]+$/ {sum += $5} END {print sum}' "$TEMP_DIR/parsed_logs.txt")
|
|
bot_pct=$(awk "BEGIN {printf \"%.1f\", ($total_bot_bandwidth/$total_bandwidth)*100}")
|
|
|
|
echo ""
|
|
echo " 💰 Bandwidth Impact:"
|
|
echo " Total bot bandwidth: ${bot_bandwidth_mb} MB (${bot_bandwidth_gb} GB) - ${bot_pct}% of total"
|
|
echo " Estimated cost: \$$estimated_cost (at \$0.09/GB CDN pricing)"
|
|
fi
|
|
echo ""
|
|
echo " Top bandwidth consumers:"
|
|
|
|
head -3 "$TEMP_DIR/large_transfers.txt" | while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
domain=$(echo "$line" | awk '{print $2}' | cut -d'|' -f2)
|
|
url=$(echo "$line" | awk '{print $2}' | cut -d'|' -f3)
|
|
size=$(echo "$line" | awk '{print $2}' | cut -d'|' -f4)
|
|
size_mb=$(awk "BEGIN {printf \"%.1f\", $size/1048576}")
|
|
total_ip_mb=$(awk "BEGIN {printf \"%.0f\", $size * $count / 1048576}")
|
|
printf " %s transfers from %s - %.1f MB avg (%s MB total) - %s%s\n" "$count" "$ip" "$size_mb" "$total_ip_mb" "$domain" "$url"
|
|
done
|
|
echo " Action: Verify if scraping, consider serving WebP/optimized images"
|
|
else
|
|
echo " None detected "
|
|
fi
|
|
|
|
# TOP 5 TARGETED SITES
|
|
print_header "TOP 5 TARGETED SITES (with risk breakdown)"
|
|
|
|
counter=1
|
|
while read -r line && [ $counter -le 5 ]; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
domain=$(echo "$line" | awk '{print $2}')
|
|
|
|
echo "[$counter] $domain - $count requests"
|
|
|
|
# Show traffic breakdown for this domain
|
|
if [ -f "$TEMP_DIR/domain_${domain}_stats.txt" ]; then
|
|
tail -n +2 "$TEMP_DIR/domain_${domain}_stats.txt" | while read -r stat_line; do
|
|
stat_count=$(echo "$stat_line" | awk '{print $1}')
|
|
stat_type=$(echo "$stat_line" | awk '{print $2}')
|
|
pct=$(awk "BEGIN {printf \"%.1f\", ($stat_count/$count)*100}")
|
|
|
|
case $stat_type in
|
|
suspicious) echo -e " ${YELLOW}Suspicious: $stat_count ($pct%)${NC}" ;;
|
|
ai) echo " AI Bots: $stat_count ($pct%)" ;;
|
|
legit) echo " Legit Bots: $stat_count ($pct%)" ;;
|
|
unknown) echo " Regular: $stat_count ($pct%)" ;;
|
|
*) echo " $stat_type: $stat_count ($pct%)" ;;
|
|
esac
|
|
done
|
|
fi
|
|
echo ""
|
|
|
|
counter=$((counter + 1))
|
|
done < "$TEMP_DIR/top_sites.txt"
|
|
|
|
# BLOCKLIST
|
|
print_header "COPY-PASTE READY BLOCKLIST (Prioritized by Threat Score)"
|
|
|
|
echo "# Apache .htaccess format:"
|
|
echo "# Add to .htaccess in document root"
|
|
echo "# IPs sorted by risk score (highest first)"
|
|
echo ""
|
|
|
|
# Use threat scores to prioritize blocklist (exclude false positives and excluded IPs)
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
# Get IPs with score >= 60 (HIGH and CRITICAL)
|
|
awk -F'|' '$1 >= 60 {print $2 "|" $1}' "$TEMP_DIR/threat_scores.txt" | head -30 | while read -r entry; do
|
|
ip=$(echo "$entry" | cut -d'|' -f1)
|
|
score=$(echo "$entry" | cut -d'|' -f2)
|
|
|
|
# Skip excluded IPs (private, localhost, server's own)
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
|
|
# Skip if in false positives
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ] && grep -q "^$ip|" "$TEMP_DIR/false_positives.txt" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
|
|
echo "Deny from $ip # Risk score: $score/100"
|
|
done
|
|
else
|
|
# Fallback to old method
|
|
{
|
|
[ -s "$TEMP_DIR/suspicious_ua.txt" ] && awk '{print $2}' "$TEMP_DIR/suspicious_ua.txt" | cut -d'|' -f1
|
|
[ -s "$TEMP_DIR/attack_vectors_raw.txt" ] && awk -F'|' '{print $1}' "$TEMP_DIR/attack_vectors_raw.txt" | sort -u
|
|
[ -s "$TEMP_DIR/rapid_fire_ips.txt" ] && head -10 "$TEMP_DIR/rapid_fire_ips.txt" | awk '{print $2}'
|
|
} | sort -u | head -30 | while read -r ip; do
|
|
echo "Deny from $ip"
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
echo "# User-Agent blocking (add to .htaccess):"
|
|
echo "SetEnvIfNoCase User-Agent \"nikto|nmap|masscan|sqlmap|havij\" bad_bot"
|
|
echo "SetEnvIfNoCase User-Agent \"acunetix|nessus|burp|metasploit\" bad_bot"
|
|
echo ""
|
|
echo "# Optional: Block aggressive SEO bots"
|
|
echo "# SetEnvIfNoCase User-Agent \"AhrefsBot|SemrushBot|MJ12bot|DotBot\" bad_bot"
|
|
echo ""
|
|
echo "Order Allow,Deny"
|
|
echo "Allow from all"
|
|
echo "Deny from env=bad_bot"
|
|
|
|
echo ""
|
|
echo "# CSF/iptables format:"
|
|
echo "# Run these commands as root:"
|
|
echo ""
|
|
|
|
# Same prioritized list for CSF
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
awk -F'|' '$1 >= 60 {print $2 "|" $1}' "$TEMP_DIR/threat_scores.txt" | head -30 | while read -r entry; do
|
|
ip=$(echo "$entry" | cut -d'|' -f1)
|
|
score=$(echo "$entry" | cut -d'|' -f2)
|
|
|
|
# Skip excluded IPs (private, localhost, server's own)
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
|
|
# Skip if in false positives
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ] && grep -q "^$ip|" "$TEMP_DIR/false_positives.txt" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
|
|
echo "csf -d $ip \"Threat score: $score/100\""
|
|
done
|
|
else
|
|
# Fallback
|
|
{
|
|
[ -s "$TEMP_DIR/suspicious_ua.txt" ] && awk '{print $2}' "$TEMP_DIR/suspicious_ua.txt" | cut -d'|' -f1
|
|
[ -s "$TEMP_DIR/attack_vectors_raw.txt" ] && awk -F'|' '{print $1}' "$TEMP_DIR/attack_vectors_raw.txt" | sort -u
|
|
[ -s "$TEMP_DIR/rapid_fire_ips.txt" ] && head -10 "$TEMP_DIR/rapid_fire_ips.txt" | awk '{print $2}'
|
|
} | sort -u | head -30 | while read -r ip; do
|
|
echo "csf -d $ip \"Bot/Scanner threat\""
|
|
done
|
|
fi
|
|
|
|
# SUMMARY
|
|
print_header "📋 SUMMARY & RECOMMENDATIONS"
|
|
|
|
threat_score=0
|
|
|
|
# Calculate threat score from attack vectors
|
|
[ -s "$TEMP_DIR/sqli_attempts.txt" ] && threat_score=$((threat_score + 15))
|
|
[ -s "$TEMP_DIR/xss_attempts.txt" ] && threat_score=$((threat_score + 12))
|
|
[ -s "$TEMP_DIR/path_traversal_attempts.txt" ] && threat_score=$((threat_score + 15))
|
|
[ -s "$TEMP_DIR/rce_upload_attempts.txt" ] && threat_score=$((threat_score + 20))
|
|
[ -s "$TEMP_DIR/login_bruteforce_attempts.txt" ] && threat_score=$((threat_score + 10))
|
|
[ -s "$TEMP_DIR/suspicious_ua.txt" ] && threat_score=$((threat_score + 8))
|
|
[ -s "$TEMP_DIR/rapid_fire_ips.txt" ] && threat_score=$((threat_score + 5))
|
|
[ $(wc -l < "$TEMP_DIR/admin_probes.txt" 2>/dev/null || echo 0) -gt 10 ] && threat_score=$((threat_score + 3))
|
|
|
|
# Count high-risk IPs
|
|
high_risk_count=0
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
high_risk_count=$(awk -F'|' '$1 >= 60' "$TEMP_DIR/threat_scores.txt" | wc -l)
|
|
fi
|
|
|
|
if [ $threat_score -ge 25 ] || [ $high_risk_count -ge 5 ]; then
|
|
print_alert "THREAT LEVEL: CRITICAL - Immediate action required"
|
|
echo " Summary: Multiple attack vectors detected from $high_risk_count high-risk IPs"
|
|
echo ""
|
|
echo " Immediate Actions:"
|
|
echo " 1. ⚡ Apply the blocklist above IMMEDIATELY (prioritized by threat score)"
|
|
echo " 2. Review admin access logs for successful breaches"
|
|
echo " 3. 🛡 Enable ModSecurity WAF or Cloudflare if not already active"
|
|
echo " 4. 🔄 Update all CMS platforms and plugins urgently"
|
|
echo " 5. 🔐 Force password reset for admin accounts if login attempts detected"
|
|
echo " 6. Re-run this analysis in 1 hour to verify blocks are working"
|
|
elif [ $threat_score -ge 12 ] || [ $high_risk_count -ge 2 ]; then
|
|
print_warning "THREAT LEVEL: HIGH - Action recommended within 24 hours"
|
|
echo " Summary: Significant threat activity from $high_risk_count high-risk IPs"
|
|
echo ""
|
|
echo " Recommended Actions:"
|
|
echo " 1. Review and apply the blocklist above (focus on CRITICAL/HIGH scores)"
|
|
echo " 2. Enable rate limiting for admin endpoints"
|
|
echo " 3. Monitor logs closely for the next 24-48 hours"
|
|
echo " 4. Consider implementing fail2ban or similar IDS"
|
|
echo " 5. Review and update security plugins/modules"
|
|
elif [ $threat_score -ge 5 ]; then
|
|
print_warning "THREAT LEVEL: MODERATE - Routine security maintenance"
|
|
echo " Summary: Normal bot activity with some suspicious patterns"
|
|
echo ""
|
|
echo " Recommended Actions:"
|
|
echo " 1. Review suspicious IPs in the report"
|
|
echo " 2. Consider rate limiting aggressive bots"
|
|
echo " 3. Continue routine log monitoring"
|
|
echo " 4. Block aggressive SEO bots if impacting performance"
|
|
else
|
|
print_success "THREAT LEVEL: ✅ LOW - Normal operation"
|
|
echo " Summary: Minimal threat activity detected"
|
|
echo ""
|
|
echo " Recommended Actions:"
|
|
echo " 1. Continue routine log monitoring"
|
|
echo " 2. Review false positive warnings to whitelist legitimate services"
|
|
echo " 3. Consider blocking aggressive SEO bots if bandwidth is a concern"
|
|
fi
|
|
|
|
echo ""
|
|
echo "==============================================================="
|
|
echo "Report saved to: $OUTPUT_FILE"
|
|
echo "==============================================================="
|
|
}
|
|
|
|
################################################################################
|
|
# BASELINE HEALTH CHECK - Test domains before making changes
|
|
################################################################################
|
|
|
|
baseline_health_check() {
|
|
print_info "Performing baseline health check on all domains..."
|
|
echo ""
|
|
|
|
# Create baseline health file
|
|
> "$TEMP_DIR/baseline_health.txt"
|
|
> "$TEMP_DIR/domain_list.txt"
|
|
|
|
# Get all domains from logs (we'll test these)
|
|
find "$LOG_DIR" -type f -name "*.com" -o -name "*.net" -o -name "*.org" 2>/dev/null | \
|
|
xargs -r basename -a 2>/dev/null | \
|
|
sort -u > "$TEMP_DIR/domain_list.txt"
|
|
|
|
# If no domains found from log files, try reference database
|
|
if [ ! -s "$TEMP_DIR/domain_list.txt" ]; then
|
|
if [ -s "$TOOLKIT_BASE_DIR/.sysref" ]; then
|
|
grep "^DOMAIN|" "$TOOLKIT_BASE_DIR/.sysref" 2>/dev/null | \
|
|
cut -d'|' -f2 | sort -u > "$TEMP_DIR/domain_list.txt"
|
|
fi
|
|
fi
|
|
|
|
local domain_count=$(wc -l < "$TEMP_DIR/domain_list.txt" 2>/dev/null || echo "0")
|
|
|
|
if [ "$domain_count" -eq 0 ]; then
|
|
print_warning "No domains found for health check"
|
|
return 0
|
|
fi
|
|
|
|
print_info "Testing $domain_count domain(s)..."
|
|
echo ""
|
|
|
|
local tested=0
|
|
local working=0
|
|
local broken=0
|
|
|
|
while read -r domain; do
|
|
[ -z "$domain" ] && continue
|
|
|
|
tested=$((tested + 1))
|
|
|
|
# Test HTTP and HTTPS
|
|
local http_status=""
|
|
local https_status=""
|
|
local result=""
|
|
|
|
# Try HTTP first (timeout 5 seconds)
|
|
http_status=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "http://$domain" 2>/dev/null || echo "timeout")
|
|
|
|
# Try HTTPS (timeout 5 seconds)
|
|
https_status=$(curl -s -o /dev/null -w "%{http_code}" -m 5 -k "https://$domain" 2>/dev/null || echo "timeout")
|
|
|
|
# Determine overall status
|
|
if [ "$http_status" = "200" ] || [ "$https_status" = "200" ]; then
|
|
result="200_OK"
|
|
working=$((working + 1))
|
|
echo -e " ${GREEN}${NC} $domain - HTTP:$http_status HTTPS:$https_status"
|
|
elif [ "$http_status" = "301" ] || [ "$http_status" = "302" ] || [ "$https_status" = "301" ] || [ "$https_status" = "302" ]; then
|
|
result="REDIRECT"
|
|
working=$((working + 1))
|
|
echo -e " ${YELLOW}→${NC} $domain - Redirect (HTTP:$http_status HTTPS:$https_status)"
|
|
elif [ "$http_status" = "403" ] || [ "$https_status" = "403" ]; then
|
|
result="403_FORBIDDEN"
|
|
broken=$((broken + 1))
|
|
echo -e " ${RED}${NC} $domain - Forbidden (HTTP:$http_status HTTPS:$https_status)"
|
|
elif [ "$http_status" = "timeout" ] && [ "$https_status" = "timeout" ]; then
|
|
result="TIMEOUT"
|
|
broken=$((broken + 1))
|
|
echo -e " ${RED}⏱${NC} $domain - Timeout (unreachable)"
|
|
else
|
|
result="ERROR_${http_status}_${https_status}"
|
|
broken=$((broken + 1))
|
|
echo -e " ${YELLOW}?${NC} $domain - HTTP:$http_status HTTPS:$https_status"
|
|
fi
|
|
|
|
# Store baseline: domain|http_status|https_status|result
|
|
echo "$domain|$http_status|$https_status|$result" >> "$TEMP_DIR/baseline_health.txt"
|
|
|
|
done < "$TEMP_DIR/domain_list.txt"
|
|
|
|
echo ""
|
|
print_success "Baseline health check complete: $working working, $broken with issues"
|
|
echo ""
|
|
}
|
|
|
|
verify_domains_still_working() {
|
|
print_info "Verifying domains still work after changes..."
|
|
echo ""
|
|
|
|
if [ ! -s "$TEMP_DIR/baseline_health.txt" ]; then
|
|
print_warning "No baseline health data available"
|
|
return 0
|
|
fi
|
|
|
|
local changes_detected=0
|
|
local now_broken=0
|
|
|
|
while IFS='|' read -r domain baseline_http baseline_https baseline_result; do
|
|
[ -z "$domain" ] && continue
|
|
|
|
# Re-test domain
|
|
local http_status=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "http://$domain" 2>/dev/null || echo "timeout")
|
|
local https_status=$(curl -s -o /dev/null -w "%{http_code}" -m 5 -k "https://$domain" 2>/dev/null || echo "timeout")
|
|
|
|
# Determine new status
|
|
local new_result=""
|
|
if [ "$http_status" = "200" ] || [ "$https_status" = "200" ]; then
|
|
new_result="200_OK"
|
|
elif [ "$http_status" = "301" ] || [ "$http_status" = "302" ] || [ "$https_status" = "301" ] || [ "$https_status" = "302" ]; then
|
|
new_result="REDIRECT"
|
|
elif [ "$http_status" = "403" ] || [ "$https_status" = "403" ]; then
|
|
new_result="403_FORBIDDEN"
|
|
elif [ "$http_status" = "timeout" ] && [ "$https_status" = "timeout" ]; then
|
|
new_result="TIMEOUT"
|
|
else
|
|
new_result="ERROR"
|
|
fi
|
|
|
|
# Compare to baseline
|
|
if [ "$baseline_result" != "$new_result" ]; then
|
|
changes_detected=$((changes_detected + 1))
|
|
|
|
# Check if it got worse
|
|
if [ "$baseline_result" = "200_OK" ] || [ "$baseline_result" = "REDIRECT" ]; then
|
|
if [ "$new_result" = "403_FORBIDDEN" ] || [ "$new_result" = "TIMEOUT" ] || [ "$new_result" = "ERROR" ]; then
|
|
now_broken=$((now_broken + 1))
|
|
echo -e " ${RED}BROKEN:${NC} $domain"
|
|
echo -e " Before: $baseline_result (HTTP:$baseline_http HTTPS:$baseline_https)"
|
|
echo -e " After: $new_result (HTTP:$http_status HTTPS:$https_status)"
|
|
echo -e " ${RED}WARNING: This domain stopped working after your changes!${NC}"
|
|
echo ""
|
|
fi
|
|
# Check if it got better
|
|
elif [ "$baseline_result" = "403_FORBIDDEN" ] || [ "$baseline_result" = "TIMEOUT" ]; then
|
|
if [ "$new_result" = "200_OK" ] || [ "$new_result" = "REDIRECT" ]; then
|
|
echo -e " ${GREEN}✅ FIXED:${NC} $domain"
|
|
echo -e " Before: $baseline_result"
|
|
echo -e " After: $new_result"
|
|
echo ""
|
|
fi
|
|
fi
|
|
fi
|
|
done < "$TEMP_DIR/baseline_health.txt"
|
|
|
|
if [ $now_broken -gt 0 ]; then
|
|
echo ""
|
|
print_alert "WARNING: $now_broken domain(s) stopped working after your changes!"
|
|
echo ""
|
|
echo "Recommended actions:"
|
|
echo " 1. Review the firewall rules you just applied"
|
|
echo " 2. Check CSF temporary blocks: csf -t"
|
|
echo " 3. Check CSF deny list: csf -g"
|
|
echo " 4. Consider reverting changes if issues persist"
|
|
echo ""
|
|
elif [ $changes_detected -eq 0 ]; then
|
|
print_success "All domains still working normally"
|
|
else
|
|
print_success "Some status changes detected but no domains broken"
|
|
fi
|
|
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
}
|
|
|
|
#############################################################################
|
|
# Main Execution
|
|
#############################################################################
|
|
|
|
main() {
|
|
echo ""
|
|
print_header "Starting Apache/cPanel Bot Analysis"
|
|
|
|
# Check if log directory exists
|
|
if [ ! -d "$LOG_DIR" ]; then
|
|
print_alert "Error: Log directory not found: $LOG_DIR"
|
|
echo "Please specify the correct log directory with -l option"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if logs exist
|
|
local find_opts=()
|
|
if [ -n "$HOURS_BACK" ]; then
|
|
local minutes=$((HOURS_BACK * 60))
|
|
find_opts+=(-mmin -"$minutes")
|
|
elif [ -n "$DAYS_BACK" ]; then
|
|
find_opts+=(-mtime -"$DAYS_BACK")
|
|
fi
|
|
|
|
# User filtering
|
|
if [ -n "$FILTER_USER" ]; then
|
|
print_info "Filtering logs for user: $FILTER_USER"
|
|
user_domains=$(get_user_domains "$FILTER_USER")
|
|
if [ -z "$user_domains" ]; then
|
|
print_error "No domains found for user: $FILTER_USER"
|
|
exit 1
|
|
fi
|
|
print_info "User has $(echo "$user_domains" | wc -l) domain(s)"
|
|
fi
|
|
|
|
log_count=$(find "$LOG_DIR" -type f ! -name "*-bytes_log" ! -name "*.offset" ! -name "*error_log" "${find_opts[@]}" 2>/dev/null | wc -l)
|
|
if [ "$log_count" -eq 0 ]; then
|
|
print_alert "Error: No log files found in $LOG_DIR"
|
|
if [ -n "$HOURS_BACK" ]; then
|
|
echo "No logs found from the last $HOURS_BACK hours"
|
|
elif [ -n "$DAYS_BACK" ]; then
|
|
echo "No logs found from the last $DAYS_BACK days"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
print_info "Found $log_count log files to analyze"
|
|
if [ -n "$HOURS_BACK" ]; then
|
|
print_info "Analyzing logs from the last $HOURS_BACK hours"
|
|
elif [ -n "$DAYS_BACK" ]; then
|
|
print_info "Analyzing logs from the last $DAYS_BACK days"
|
|
fi
|
|
|
|
# Baseline health check - test all domains before analysis
|
|
baseline_health_check
|
|
|
|
# Execute analysis pipeline with error handling
|
|
parse_logs || {
|
|
print_alert "Log parsing failed"
|
|
exit 1
|
|
}
|
|
|
|
classify_bots || {
|
|
print_alert "Bot classification failed"
|
|
exit 1
|
|
}
|
|
|
|
detect_server_ips
|
|
detect_threats
|
|
detect_botnets
|
|
analyze_time_series
|
|
calculate_threat_scores
|
|
detect_false_positives
|
|
generate_statistics
|
|
generate_report
|
|
|
|
print_success "Analysis complete!"
|
|
echo ""
|
|
echo "Report location: $OUTPUT_FILE"
|
|
|
|
# Analyze threat patterns and generate recommendations
|
|
analyze_domain_threats
|
|
analyze_geographic_threats
|
|
generate_recommendations
|
|
|
|
# Ask user what to do next
|
|
show_post_analysis_menu
|
|
}
|
|
|
|
################################################################################
|
|
# DOMAIN-LEVEL THREAT ANALYSIS
|
|
################################################################################
|
|
|
|
analyze_domain_threats() {
|
|
print_info "Analyzing per-domain threat patterns..."
|
|
|
|
# Create domain threat analysis file
|
|
> "$TEMP_DIR/domain_threats.txt"
|
|
> "$TEMP_DIR/domain_high_risk_ips.txt"
|
|
|
|
# Get all unique domains from parsed logs
|
|
awk -F'|' '{print $2}' "$TEMP_DIR/parsed_logs.txt" 2>/dev/null | sort -u > "$TEMP_DIR/all_domains.txt"
|
|
|
|
# For each domain, calculate threat metrics
|
|
while read -r domain; do
|
|
[ -z "$domain" ] && continue
|
|
|
|
# Total requests to this domain
|
|
local total_requests=$(grep -c "^[^|]*|$domain|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null || echo "0")
|
|
|
|
# Bot requests to this domain
|
|
local bot_requests=$(grep "|$domain|" "$TEMP_DIR/classified_bots.txt" 2>/dev/null | wc -l || echo "0")
|
|
|
|
# High-risk IPs hitting this domain (score >= 70)
|
|
local high_risk_count=0
|
|
local high_risk_ips=""
|
|
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
while read -r score_line; do
|
|
local score=$(echo "$score_line" | cut -d'|' -f1)
|
|
local ip=$(echo "$score_line" | cut -d'|' -f2)
|
|
|
|
if [ "$score" -ge 70 ]; then
|
|
# Check if this IP hit this domain
|
|
if grep -q "^$ip|$domain|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null; then
|
|
local ip_requests=$(grep -c "^$ip|$domain|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null || echo "0")
|
|
high_risk_count=$((high_risk_count + 1))
|
|
high_risk_ips="${high_risk_ips}${ip}:${score}:${ip_requests} "
|
|
fi
|
|
fi
|
|
done < "$TEMP_DIR/threat_scores.txt"
|
|
fi
|
|
|
|
# Attack attempts targeting this domain
|
|
local attack_attempts=0
|
|
if [ -s "$TEMP_DIR/attack_vectors_raw.txt" ]; then
|
|
attack_attempts=$(grep "|$domain|" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | wc -l || echo "0")
|
|
fi
|
|
|
|
# Calculate bot percentage
|
|
local bot_percentage=0
|
|
if [ "$total_requests" -gt 0 ]; then
|
|
bot_percentage=$(awk "BEGIN {printf \"%.1f\", ($bot_requests / $total_requests) * 100}")
|
|
fi
|
|
|
|
# Store domain threat data
|
|
# Format: domain|total_requests|bot_requests|bot_percentage|high_risk_ip_count|attack_attempts|high_risk_ips_detail
|
|
echo "$domain|$total_requests|$bot_requests|$bot_percentage|$high_risk_count|$attack_attempts|$high_risk_ips" >> "$TEMP_DIR/domain_threats.txt"
|
|
|
|
# Track which high-risk IPs hit which domains
|
|
if [ $high_risk_count -gt 0 ]; then
|
|
echo "$domain|$high_risk_count|$high_risk_ips" >> "$TEMP_DIR/domain_high_risk_ips.txt"
|
|
fi
|
|
|
|
done < "$TEMP_DIR/all_domains.txt"
|
|
|
|
# Sort by high-risk IP count (descending)
|
|
sort -t'|' -k5 -rn "$TEMP_DIR/domain_threats.txt" > "$TEMP_DIR/domain_threats_sorted.txt"
|
|
|
|
print_success "Domain threat analysis complete"
|
|
}
|
|
|
|
################################################################################
|
|
# GEOGRAPHIC ANALYSIS (Country-based threat tracking)
|
|
################################################################################
|
|
|
|
analyze_geographic_threats() {
|
|
print_info "Analyzing geographic distribution of threats..."
|
|
|
|
# Create geographic analysis file
|
|
> "$TEMP_DIR/geo_analysis.txt"
|
|
> "$TEMP_DIR/geo_needs_maxmind.txt"
|
|
|
|
# Check if GeoIP/MaxMind is available
|
|
local has_geoip=false
|
|
if command -v geoiplookup >/dev/null 2>&1 || command -v mmdbinspect >/dev/null 2>&1; then
|
|
has_geoip=true
|
|
fi
|
|
|
|
if [ "$has_geoip" = false ]; then
|
|
# Can't do full geographic analysis without GeoIP
|
|
# But we can still detect if traffic looks suspicious by analyzing IP ranges
|
|
|
|
# Count high-risk IPs by /24 network
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
awk -F'|' '$1 >= 70 {
|
|
split($2, ip, ".")
|
|
network = ip[1]"."ip[2]"."ip[3]".0/24"
|
|
print network
|
|
}' "$TEMP_DIR/threat_scores.txt" | sort | uniq -c | sort -rn > "$TEMP_DIR/high_risk_networks.txt"
|
|
|
|
local network_count=$(wc -l < "$TEMP_DIR/high_risk_networks.txt" 2>/dev/null || echo "0")
|
|
local total_high_risk=$(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" | wc -l)
|
|
|
|
if [ "$network_count" -gt 10 ] || [ "$total_high_risk" -gt 50 ]; then
|
|
# Multiple networks or many IPs suggests distributed attack
|
|
# Recommend MaxMind for geographic blocking
|
|
echo "DISTRIBUTED|$network_count networks|$total_high_risk IPs|MaxMind recommended" > "$TEMP_DIR/geo_needs_maxmind.txt"
|
|
fi
|
|
fi
|
|
|
|
print_info "Geographic analysis limited (MaxMind GeoIP2 not installed)"
|
|
else
|
|
# Full geographic analysis with GeoIP
|
|
print_info "Performing full geographic analysis with GeoIP..."
|
|
|
|
# TODO: Implement full GeoIP lookups when available
|
|
# This would lookup each high-risk IP and count by country
|
|
fi
|
|
|
|
print_success "Geographic analysis complete"
|
|
}
|
|
|
|
################################################################################
|
|
# RECOMMENDATION ENGINE
|
|
################################################################################
|
|
|
|
generate_recommendations() {
|
|
print_info "Generating intelligent recommendations..."
|
|
|
|
# Initialize recommendation file
|
|
> "$TEMP_DIR/recommendations.txt"
|
|
local rec_count=0
|
|
|
|
# Get total unique high-risk IPs
|
|
local total_high_risk_ips=0
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
total_high_risk_ips=$(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | wc -l || echo "0")
|
|
fi
|
|
|
|
# Get total domains affected
|
|
local total_domains=$(wc -l < "$TEMP_DIR/all_domains.txt" 2>/dev/null || echo "0")
|
|
local affected_domains=0
|
|
if [ -s "$TEMP_DIR/domain_high_risk_ips.txt" ]; then
|
|
affected_domains=$(wc -l < "$TEMP_DIR/domain_high_risk_ips.txt" || echo "0")
|
|
fi
|
|
|
|
# Determine attack scope: single domain vs server-wide
|
|
local attack_scope="unknown"
|
|
local primary_target=""
|
|
local primary_target_percentage=0
|
|
|
|
if [ $affected_domains -eq 1 ] && [ $total_domains -gt 1 ]; then
|
|
attack_scope="single_domain"
|
|
primary_target=$(head -1 "$TEMP_DIR/domain_high_risk_ips.txt" 2>/dev/null | cut -d'|' -f1)
|
|
# Calculate what % of high-risk IPs are targeting this domain
|
|
local domain_risk_count=$(head -1 "$TEMP_DIR/domain_high_risk_ips.txt" 2>/dev/null | cut -d'|' -f2)
|
|
if [ $total_high_risk_ips -gt 0 ]; then
|
|
primary_target_percentage=$(awk "BEGIN {printf \"%.0f\", ($domain_risk_count / $total_high_risk_ips) * 100}")
|
|
fi
|
|
elif [ $affected_domains -gt 1 ] && [ $total_domains -gt 1 ]; then
|
|
# Check if one domain is getting most of the traffic
|
|
local top_domain_count=$(head -1 "$TEMP_DIR/domain_threats_sorted.txt" 2>/dev/null | cut -d'|' -f5)
|
|
if [ "$top_domain_count" -gt 0 ] && [ $total_high_risk_ips -gt 0 ]; then
|
|
local top_percentage=$(awk "BEGIN {printf \"%.0f\", ($top_domain_count / $total_high_risk_ips) * 100}")
|
|
if [ "$top_percentage" -ge 75 ]; then
|
|
attack_scope="primary_target"
|
|
primary_target=$(head -1 "$TEMP_DIR/domain_threats_sorted.txt" 2>/dev/null | cut -d'|' -f1)
|
|
primary_target_percentage=$top_percentage
|
|
else
|
|
attack_scope="server_wide"
|
|
fi
|
|
else
|
|
attack_scope="server_wide"
|
|
fi
|
|
elif [ $affected_domains -eq $total_domains ] && [ $total_domains -gt 1 ]; then
|
|
attack_scope="server_wide"
|
|
elif [ $total_domains -eq 1 ]; then
|
|
attack_scope="single_server"
|
|
primary_target=$(head -1 "$TEMP_DIR/all_domains.txt" 2>/dev/null)
|
|
fi
|
|
|
|
# RECOMMENDATION #1: IP Blocking Strategy
|
|
if [ $total_high_risk_ips -gt 0 ]; then
|
|
rec_count=$((rec_count + 1))
|
|
if [ $total_high_risk_ips -le 10 ]; then
|
|
echo "REC|$rec_count|ip_block_temp|Block $total_high_risk_ips high-risk IPs for 1 hour|HIGH|CSF temporary block recommended for ${total_high_risk_ips} IPs with threat score >= 70" >> "$TEMP_DIR/recommendations.txt"
|
|
elif [ $total_high_risk_ips -le 50 ]; then
|
|
echo "REC|$rec_count|ip_block_temp|Block $total_high_risk_ips high-risk IPs for 24 hours|HIGH|Large number of threats detected - 24hr block recommended" >> "$TEMP_DIR/recommendations.txt"
|
|
else
|
|
echo "REC|$rec_count|ip_block_perm|Permanently block $total_high_risk_ips high-risk IPs|CRITICAL|Severe bot attack detected - permanent blocking recommended" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
fi
|
|
|
|
# RECOMMENDATION #2: Connection Limit (CSF CT_LIMIT)
|
|
# Only recommend if CSF is installed and CT_LIMIT is enabled
|
|
if command -v csf >/dev/null 2>&1 && [ -f /etc/csf/csf.conf ]; then
|
|
# Check if CT_LIMIT is enabled (not set to 0)
|
|
local current_ct_limit=$(grep "^CT_LIMIT" /etc/csf/csf.conf 2>/dev/null | grep -oP '"\K[0-9]+' || echo "0")
|
|
|
|
if [ "$current_ct_limit" -gt 0 ]; then
|
|
# Check concurrent connections from top IPs
|
|
local max_connections=0
|
|
if [ -s "$TEMP_DIR/rapid_fire_ips.txt" ]; then
|
|
max_connections=$(head -1 "$TEMP_DIR/rapid_fire_ips.txt" 2>/dev/null | awk '{print $1}' || echo "0")
|
|
fi
|
|
|
|
if [ "$max_connections" -gt 100 ] && [ "$max_connections" -lt "$current_ct_limit" ]; then
|
|
rec_count=$((rec_count + 1))
|
|
local recommended_limit=$((max_connections - 20))
|
|
echo "REC|$rec_count|csf_ct_limit|Reduce CSF CT_LIMIT from $current_ct_limit to $recommended_limit|MEDIUM|High concurrent connections detected ($max_connections from single IP)" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# RECOMMENDATION #3: Domain-Specific .htaccess Protection
|
|
if [ "$attack_scope" = "single_domain" ] || [ "$attack_scope" = "primary_target" ]; then
|
|
rec_count=$((rec_count + 1))
|
|
echo "REC|$rec_count|htaccess_domain|Add bot blocking to $primary_target .htaccess|HIGH|${primary_target_percentage}% of attacks target this domain" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
|
|
# RECOMMENDATION #4: Server-wide Apache Protection
|
|
if [ "$attack_scope" = "server_wide" ]; then
|
|
rec_count=$((rec_count + 1))
|
|
echo "REC|$rec_count|apache_global|Add global bot blocking to Apache pre-virtualhost|HIGH|Attack affects $affected_domains of $total_domains domains" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
|
|
# RECOMMENDATION #5: WordPress-specific (if attack patterns show wp-admin/wp-login attempts)
|
|
local wp_attacks=0
|
|
if [ -s "$TEMP_DIR/attack_vectors_raw.txt" ]; then
|
|
wp_attacks=$(grep -i "wp-admin\|wp-login\|xmlrpc" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | wc -l || echo "0")
|
|
fi
|
|
|
|
if [ $wp_attacks -gt 50 ]; then
|
|
rec_count=$((rec_count + 1))
|
|
|
|
# Determine which domains have WordPress
|
|
local wp_domain_count=0
|
|
local wp_target_domain=""
|
|
|
|
if [ -s "$TEMP_DIR/attack_vectors_raw.txt" ]; then
|
|
# Get unique domains with WP attacks
|
|
wp_domain_count=$(grep -i "wp-admin\|wp-login\|xmlrpc" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | cut -d'|' -f2 | sort -u | wc -l || echo "0")
|
|
wp_target_domain=$(grep -i "wp-admin\|wp-login\|xmlrpc" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | cut -d'|' -f2 | sort -u | head -1)
|
|
fi
|
|
|
|
# Generate appropriate recommendation based on how many domains have WordPress attacks
|
|
if [ $wp_domain_count -eq 1 ] || [ "$attack_scope" = "single_domain" ] || [ "$attack_scope" = "single_server" ]; then
|
|
# Single domain being attacked
|
|
echo "REC|$rec_count|wp_hardening|Harden WordPress on $wp_target_domain|HIGH|$wp_attacks WordPress login/admin attempts detected" >> "$TEMP_DIR/recommendations.txt"
|
|
elif [ "$attack_scope" = "primary_target" ]; then
|
|
# One primary target but others also affected
|
|
echo "REC|$rec_count|wp_hardening|Harden WordPress on $primary_target|HIGH|$wp_attacks WordPress login/admin attempts detected" >> "$TEMP_DIR/recommendations.txt"
|
|
else
|
|
# Multiple domains with WordPress attacks
|
|
echo "REC|$rec_count|wp_hardening|Harden WordPress across $wp_domain_count domains|HIGH|$wp_attacks WordPress login/admin attempts detected" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
fi
|
|
|
|
# PORTFLOOD Protection removed - not appropriate for web servers with many sites
|
|
# Blocking ports 80/443 based on connection count breaks legitimate traffic
|
|
|
|
# RECOMMENDATION #7: CSF SYNFLOOD Protection (if DDoS patterns detected)
|
|
if [ -s "$TEMP_DIR/rapid_fire_ips.txt" ]; then
|
|
local ddos_count=$(wc -l < "$TEMP_DIR/rapid_fire_ips.txt" || echo "0")
|
|
if [ $ddos_count -gt 10 ]; then
|
|
rec_count=$((rec_count + 1))
|
|
echo "REC|$rec_count|csf_synflood|Enable CSF SYNFLOOD protection|HIGH|$ddos_count potential DDoS sources detected" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
fi
|
|
|
|
# RECOMMENDATION #8: MaxMind GeoIP for Country Blocking (if distributed attack)
|
|
if [ -s "$TEMP_DIR/geo_needs_maxmind.txt" ]; then
|
|
local geo_info=$(cat "$TEMP_DIR/geo_needs_maxmind.txt")
|
|
local network_count=$(echo "$geo_info" | cut -d'|' -f2 | grep -oP '\d+' || echo "0")
|
|
local ip_count=$(echo "$geo_info" | cut -d'|' -f3 | grep -oP '\d+' || echo "0")
|
|
|
|
rec_count=$((rec_count + 1))
|
|
echo "REC|$rec_count|install_maxmind|Install MaxMind GeoIP2 for country-based blocking|MEDIUM|Distributed attack from $network_count networks ($ip_count IPs) - geographic blocking recommended" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
|
|
# Store attack scope for menu system
|
|
echo "$attack_scope|$primary_target|$primary_target_percentage|$affected_domains|$total_domains" > "$TEMP_DIR/attack_scope.txt"
|
|
|
|
print_success "Generated $rec_count recommendations"
|
|
}
|
|
|
|
################################################################################
|
|
# POST-ANALYSIS MENU
|
|
################################################################################
|
|
|
|
show_post_analysis_menu() {
|
|
# Load attack scope information
|
|
local attack_scope="unknown"
|
|
local primary_target=""
|
|
local primary_target_percentage=0
|
|
local affected_domains=0
|
|
local total_domains=0
|
|
|
|
if [ -s "$TEMP_DIR/attack_scope.txt" ]; then
|
|
local scope_data=$(cat "$TEMP_DIR/attack_scope.txt")
|
|
attack_scope=$(echo "$scope_data" | cut -d'|' -f1)
|
|
primary_target=$(echo "$scope_data" | cut -d'|' -f2)
|
|
primary_target_percentage=$(echo "$scope_data" | cut -d'|' -f3)
|
|
affected_domains=$(echo "$scope_data" | cut -d'|' -f4)
|
|
total_domains=$(echo "$scope_data" | cut -d'|' -f5)
|
|
fi
|
|
|
|
# Check if there are any recommendations
|
|
local has_recommendations=false
|
|
local rec_count=0
|
|
if [ -s "$TEMP_DIR/recommendations.txt" ]; then
|
|
has_recommendations=true
|
|
rec_count=$(wc -l < "$TEMP_DIR/recommendations.txt")
|
|
fi
|
|
|
|
# Show menu
|
|
echo ""
|
|
echo "==============================================================="
|
|
print_header "THREAT ANALYSIS SUMMARY"
|
|
echo ""
|
|
|
|
# Display attack scope
|
|
case "$attack_scope" in
|
|
single_domain)
|
|
print_warning "ATTACK SCOPE: Single Domain Target"
|
|
echo " • Primary Target: $primary_target"
|
|
echo " • This domain is receiving 100% of high-risk traffic"
|
|
echo " • Recommendation: Domain-specific protection"
|
|
;;
|
|
primary_target)
|
|
print_warning "ATTACK SCOPE: Primarily Targeting One Domain"
|
|
echo " • Primary Target: $primary_target ($primary_target_percentage% of attacks)"
|
|
echo " • Other domains also affected: $affected_domains of $total_domains total"
|
|
echo " • Recommendation: Focus protection on primary target"
|
|
;;
|
|
server_wide)
|
|
print_alert "ATTACK SCOPE: Server-Wide Attack"
|
|
echo " • Multiple domains under attack: $affected_domains of $total_domains"
|
|
echo " • Attack is distributed across the server"
|
|
echo " • Recommendation: Server-wide protection needed"
|
|
;;
|
|
single_server)
|
|
print_info "ATTACK SCOPE: Single-Domain Server"
|
|
echo " • Target: $primary_target (only domain on server)"
|
|
echo " • Server-level protection will apply to this domain"
|
|
;;
|
|
*)
|
|
print_info "No significant threats detected"
|
|
;;
|
|
esac
|
|
|
|
echo ""
|
|
|
|
# Display recommendations
|
|
if [ "$has_recommendations" = true ]; then
|
|
echo "==============================================================="
|
|
print_header "RECOMMENDED ACTIONS ($rec_count recommendations)"
|
|
echo ""
|
|
|
|
local count=0
|
|
while IFS='|' read -r rec_type rec_num action_type action_title priority description; do
|
|
count=$((count + 1))
|
|
|
|
# Color code by priority
|
|
local priority_color=""
|
|
local priority_icon=""
|
|
case "$priority" in
|
|
CRITICAL)
|
|
priority_color="${RED}"
|
|
priority_icon=""
|
|
;;
|
|
HIGH)
|
|
priority_color="${YELLOW}"
|
|
priority_icon=""
|
|
;;
|
|
MEDIUM)
|
|
priority_color="${BLUE}"
|
|
priority_icon=""
|
|
;;
|
|
*)
|
|
priority_color="${NC}"
|
|
priority_icon=" "
|
|
;;
|
|
esac
|
|
|
|
echo -e " ${BOLD}[$count]${NC} $priority_icon $action_title"
|
|
echo -e " ${priority_color}Priority: $priority${NC} - $description"
|
|
echo ""
|
|
done < "$TEMP_DIR/recommendations.txt"
|
|
|
|
echo "==============================================================="
|
|
echo ""
|
|
echo "What would you like to do?"
|
|
echo ""
|
|
echo " 1) Go to Take Action Menu (implement recommended actions)"
|
|
echo " 2) Review Individual Recommendations (detailed view)"
|
|
echo " 3) Go Back (return to main menu)"
|
|
echo ""
|
|
read -p "Select option [1-3]: " menu_choice
|
|
|
|
case "$menu_choice" in
|
|
1)
|
|
show_action_menu
|
|
;;
|
|
2)
|
|
show_detailed_recommendations
|
|
;;
|
|
3)
|
|
print_info "Returning to main menu..."
|
|
return 0
|
|
;;
|
|
*)
|
|
print_warning "Invalid option - returning to main menu"
|
|
return 0
|
|
;;
|
|
esac
|
|
else
|
|
print_success "No recommendations - your server appears secure"
|
|
echo ""
|
|
echo "Press Enter to return to main menu..."
|
|
read
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# DETAILED RECOMMENDATIONS VIEWER
|
|
################################################################################
|
|
|
|
show_detailed_recommendations() {
|
|
clear
|
|
print_banner "Detailed Recommendations"
|
|
echo ""
|
|
|
|
if [ ! -s "$TEMP_DIR/recommendations.txt" ]; then
|
|
print_warning "No recommendations available"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_post_analysis_menu
|
|
return
|
|
fi
|
|
|
|
local count=0
|
|
while IFS='|' read -r rec_type rec_num action_type action_title priority description; do
|
|
count=$((count + 1))
|
|
|
|
echo "==============================================================="
|
|
echo -e "${BOLD}Recommendation #$count:${NC} $action_title"
|
|
echo "==============================================================="
|
|
echo ""
|
|
echo "Priority: $priority"
|
|
echo "Action Type: $action_type"
|
|
echo "Description: $description"
|
|
echo ""
|
|
|
|
# Show specific details based on action type
|
|
case "$action_type" in
|
|
ip_block_temp|ip_block_perm)
|
|
echo "Affected IPs:"
|
|
awk -F'|' '$1 >= 70 {printf " • %s (score: %s)\n", $2, $1}' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | head -10
|
|
;;
|
|
htaccess_domain)
|
|
local target_domain=$(echo "$action_title" | grep -oP 'to \K[^ ]+')
|
|
echo "Target Domain: $target_domain"
|
|
if [ -s "$TEMP_DIR/domain_threats_sorted.txt" ]; then
|
|
grep "^$target_domain|" "$TEMP_DIR/domain_threats_sorted.txt" | while IFS='|' read -r domain total_req bot_req bot_pct high_risk attacks ips; do
|
|
echo " • Total Requests: $total_req"
|
|
echo " • Bot Requests: $bot_req ($bot_pct%)"
|
|
echo " • High-Risk IPs: $high_risk"
|
|
echo " • Attack Attempts: $attacks"
|
|
done
|
|
fi
|
|
;;
|
|
apache_global)
|
|
echo "Affected Domains:"
|
|
if [ -s "$TEMP_DIR/domain_high_risk_ips.txt" ]; then
|
|
awk -F'|' '{printf " • %s (%s high-risk IPs)\n", $1, $2}' "$TEMP_DIR/domain_high_risk_ips.txt" | head -10
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
echo ""
|
|
done < "$TEMP_DIR/recommendations.txt"
|
|
|
|
echo "==============================================================="
|
|
echo ""
|
|
read -p "Press Enter to return to action menu..."
|
|
show_post_analysis_menu
|
|
}
|
|
|
|
################################################################################
|
|
# ACTION MENU (IMPLEMENT RECOMMENDATIONS)
|
|
################################################################################
|
|
|
|
show_action_menu() {
|
|
clear
|
|
print_banner "Take Action Menu"
|
|
echo ""
|
|
|
|
# Build hash table of recommended actions with their priorities
|
|
declare -A recommended_actions
|
|
declare -A action_priorities
|
|
declare -A action_descriptions
|
|
|
|
if [ -s "$TEMP_DIR/recommendations.txt" ]; then
|
|
while IFS='|' read -r rec_type rec_num action_type action_title priority description; do
|
|
recommended_actions["$action_type"]=1
|
|
action_priorities["$action_type"]="$priority"
|
|
action_descriptions["$action_type"]="$description"
|
|
done < "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
|
|
# Display all available actions (not just recommended ones)
|
|
echo "All Available Actions:"
|
|
echo ""
|
|
echo "Legend: = Recommended by analysis"
|
|
echo ""
|
|
|
|
local count=0
|
|
declare -a action_types
|
|
declare -a action_titles
|
|
declare -a action_descs
|
|
|
|
# Define all possible actions
|
|
# 1. IP Blocking Actions
|
|
count=$((count + 1))
|
|
action_types[$count]="ip_block_temp_1hr"
|
|
action_titles[$count]="Block high-risk IPs for 1 hour (CSF temporary)"
|
|
action_descs[$count]="Temporary firewall block, auto-expires after 1 hour"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[ip_block_temp]}" "${action_priorities[ip_block_temp]}"
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="ip_block_temp_24hr"
|
|
action_titles[$count]="Block high-risk IPs for 24 hours (CSF temporary)"
|
|
action_descs[$count]="Temporary firewall block, auto-expires after 24 hours"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[ip_block_temp]}" "${action_priorities[ip_block_temp]}"
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="ip_block_perm"
|
|
action_titles[$count]="Block high-risk IPs permanently (CSF permanent)"
|
|
action_descs[$count]="Permanent firewall block - requires manual removal"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[ip_block_perm]}" "${action_priorities[ip_block_perm]}"
|
|
|
|
echo ""
|
|
echo "------------------------------------------------------------─"
|
|
echo ""
|
|
|
|
# 2. Domain/Site Protection
|
|
count=$((count + 1))
|
|
action_types[$count]="htaccess_domain"
|
|
action_titles[$count]="Add bot blocking to specific domain .htaccess"
|
|
action_descs[$count]="Domain-level protection via Apache .htaccess rules"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[htaccess_domain]}" "${action_priorities[htaccess_domain]}"
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="apache_global"
|
|
action_titles[$count]="Add global bot blocking to Apache (all domains)"
|
|
action_descs[$count]="Server-wide Apache configuration, affects all sites"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[apache_global]}" "${action_priorities[apache_global]}"
|
|
|
|
echo ""
|
|
echo "------------------------------------------------------------─"
|
|
echo ""
|
|
|
|
# 3. CSF Firewall Configuration
|
|
count=$((count + 1))
|
|
action_types[$count]="csf_ct_limit"
|
|
action_titles[$count]="Adjust CSF connection tracking limit (CT_LIMIT)"
|
|
action_descs[$count]="Limit concurrent connections per IP address"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[csf_ct_limit]}" "${action_priorities[csf_ct_limit]}"
|
|
|
|
# PORTFLOOD action removed - not appropriate for web servers
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="csf_synflood"
|
|
action_titles[$count]="Enable CSF SYNFLOOD protection"
|
|
action_descs[$count]="Protect against SYN flood DDoS attacks"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[csf_synflood]}" "${action_priorities[csf_synflood]}"
|
|
|
|
echo ""
|
|
echo "------------------------------------------------------------─"
|
|
echo ""
|
|
|
|
# 4. Geographic & Application Hardening
|
|
count=$((count + 1))
|
|
action_types[$count]="install_maxmind"
|
|
action_titles[$count]="Install MaxMind GeoIP2 for country-based blocking"
|
|
action_descs[$count]="Enable geographic filtering with CSF CC_DENY (requires free MaxMind license)"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[install_maxmind]}" "${action_priorities[install_maxmind]}"
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="wp_hardening"
|
|
action_titles[$count]="WordPress security hardening"
|
|
action_descs[$count]="Protect WordPress login and admin areas"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[wp_hardening]}" "${action_priorities[wp_hardening]}"
|
|
|
|
echo ""
|
|
echo "============================================================═"
|
|
echo ""
|
|
echo " 0) Go Back"
|
|
echo ""
|
|
read -p "Select action [0-$count]: " action_choice
|
|
|
|
# Validate choice
|
|
if [ "$action_choice" = "0" ]; then
|
|
show_post_analysis_menu
|
|
return
|
|
elif [ "$action_choice" -lt 1 ] || [ "$action_choice" -gt "$count" ] 2>/dev/null; then
|
|
print_warning "Invalid selection"
|
|
sleep 2
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Execute selected action
|
|
local selected_type="${action_types[$action_choice]}"
|
|
execute_action "$selected_type" "$action_choice"
|
|
}
|
|
|
|
# Helper function to display action options
|
|
display_action_option() {
|
|
local num=$1
|
|
local action_type=$2
|
|
local title=$3
|
|
local desc=$4
|
|
local is_recommended=$5
|
|
local priority=$6
|
|
|
|
# Show recommendation marker and priority if recommended
|
|
if [ -n "$is_recommended" ]; then
|
|
case "$priority" in
|
|
CRITICAL)
|
|
echo -e " ${RED}$num)${NC} ${BOLD}$title${NC} ${RED} RECOMMENDED [CRITICAL]${NC}"
|
|
;;
|
|
HIGH)
|
|
echo -e " ${YELLOW}$num)${NC} ${BOLD}$title${NC} ${YELLOW} RECOMMENDED [HIGH]${NC}"
|
|
;;
|
|
MEDIUM)
|
|
echo -e " ${BLUE}$num)${NC} ${BOLD}$title${NC} ${BLUE} RECOMMENDED [MEDIUM]${NC}"
|
|
;;
|
|
*)
|
|
echo -e " ${GREEN}$num)${NC} ${BOLD}$title${NC} ${GREEN} RECOMMENDED${NC}"
|
|
;;
|
|
esac
|
|
else
|
|
echo -e " $num) $title"
|
|
fi
|
|
echo " $desc"
|
|
}
|
|
|
|
################################################################################
|
|
# ACTION EXECUTION ENGINE
|
|
################################################################################
|
|
|
|
execute_action() {
|
|
local action_type="$1"
|
|
local rec_number="$2"
|
|
|
|
case "$action_type" in
|
|
ip_block_temp_1hr)
|
|
execute_ip_blocking_specific "1hr"
|
|
;;
|
|
ip_block_temp_24hr)
|
|
execute_ip_blocking_specific "24hr"
|
|
;;
|
|
ip_block_temp)
|
|
execute_ip_blocking "temp"
|
|
;;
|
|
ip_block_perm)
|
|
execute_ip_blocking "perm"
|
|
;;
|
|
csf_ct_limit)
|
|
execute_csf_ct_limit
|
|
;;
|
|
csf_synflood)
|
|
execute_csf_synflood
|
|
;;
|
|
htaccess_domain)
|
|
execute_htaccess_domain_blocking
|
|
;;
|
|
apache_global)
|
|
execute_apache_global_blocking
|
|
;;
|
|
install_maxmind)
|
|
execute_install_maxmind
|
|
;;
|
|
wp_hardening)
|
|
execute_wp_hardening
|
|
;;
|
|
rate_limiting)
|
|
execute_rate_limiting
|
|
;;
|
|
*)
|
|
print_warning "Action type '$action_type' not yet implemented"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
;;
|
|
esac
|
|
}
|
|
|
|
execute_ip_blocking_specific() {
|
|
local duration_type="$1" # "1hr" or "24hr"
|
|
|
|
clear
|
|
print_banner "IP Blocking - CSF Temporary Block"
|
|
echo ""
|
|
|
|
# Check if CSF is installed
|
|
if ! command -v csf >/dev/null 2>&1; then
|
|
print_warning "CSF (ConfigServer Security & Firewall) is not installed"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Get high-risk IPs
|
|
if [ ! -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
print_warning "No threat scores available"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
local high_risk_count=$(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | wc -l || echo "0")
|
|
|
|
if [ "$high_risk_count" -eq 0 ]; then
|
|
print_info "No high-risk IPs detected (score >= 70)"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Set duration based on type
|
|
local duration
|
|
local duration_text
|
|
if [ "$duration_type" = "1hr" ]; then
|
|
duration=3600
|
|
duration_text="1 hour"
|
|
else
|
|
duration=86400
|
|
duration_text="24 hours"
|
|
fi
|
|
|
|
echo "This will block $high_risk_count high-risk IPs for $duration_text"
|
|
echo ""
|
|
echo "High-risk IPs (top 10):"
|
|
awk -F'|' '$1 >= 70 {printf " • %s (score: %s, %s requests)\n", $2, $1, $3}' "$TEMP_DIR/threat_scores.txt" | head -10
|
|
echo ""
|
|
|
|
if [ "$high_risk_count" -gt 10 ]; then
|
|
echo " ... and $((high_risk_count - 10)) more"
|
|
echo ""
|
|
fi
|
|
|
|
read -p "Proceed with blocking for $duration_text? (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Collect IPs to block
|
|
local -a ips_to_block
|
|
while IFS='|' read -r score ip requests; do
|
|
if [ "$score" -ge 70 ]; then
|
|
# Skip excluded IPs
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
# Skip false positives
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ] && grep -q "^$ip|" "$TEMP_DIR/false_positives.txt" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
ips_to_block+=("$ip")
|
|
fi
|
|
done < "$TEMP_DIR/threat_scores.txt"
|
|
|
|
# Apply blocks
|
|
echo ""
|
|
print_info "Applying CSF blocks for $duration_text..."
|
|
echo ""
|
|
|
|
local success_count=0
|
|
local fail_count=0
|
|
|
|
for ip in "${ips_to_block[@]}"; do
|
|
local score=$(grep "|$ip|" "$TEMP_DIR/threat_scores.txt" 2>/dev/null | cut -d'|' -f1 || echo "unknown")
|
|
|
|
if csf -td "$ip" "$duration" "Bot threat score: $score/100 - Auto-blocked by toolkit" >/dev/null 2>&1; then
|
|
echo -e " ${GREEN}${NC} Blocked $ip for $duration_text (score: $score/100)"
|
|
success_count=$((success_count + 1))
|
|
else
|
|
echo -e " ${RED}${NC} Failed to block $ip"
|
|
fail_count=$((fail_count + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [ $success_count -gt 0 ]; then
|
|
print_success "Successfully blocked $success_count IP(s) for $duration_text"
|
|
echo ""
|
|
echo "These blocks will automatically expire after $duration_text"
|
|
echo "To view temporary blocks: csf -t"
|
|
echo "To remove a block early: csf -tr IP"
|
|
fi
|
|
|
|
if [ $fail_count -gt 0 ]; then
|
|
print_warning "$fail_count IP(s) failed to block - check CSF configuration"
|
|
fi
|
|
|
|
# Restart CSF
|
|
print_info "Restarting CSF to apply changes..."
|
|
if csf -r >/dev/null 2>&1; then
|
|
print_success "CSF restarted successfully"
|
|
else
|
|
print_warning "CSF restart may have failed - check manually with: csf -r"
|
|
fi
|
|
|
|
echo ""
|
|
# Verify domains still work after blocking
|
|
verify_domains_still_working
|
|
|
|
show_action_menu
|
|
}
|
|
|
|
execute_ip_blocking() {
|
|
local block_mode="$1" # "temp" or "perm"
|
|
|
|
if [ "$block_mode" = "temp" ]; then
|
|
# Call the existing CSF blocking function
|
|
offer_csf_blocking
|
|
else
|
|
# Permanent blocking
|
|
clear
|
|
print_banner "Permanent IP Blocking"
|
|
echo ""
|
|
print_alert "WARNING: Permanent blocks must be manually removed later"
|
|
echo ""
|
|
echo "This will permanently block all high-risk IPs (score >= 70)"
|
|
echo ""
|
|
read -p "Are you sure you want to proceed? (yes/no): " confirm
|
|
|
|
if [ "$confirm" = "yes" ]; then
|
|
offer_csf_blocking
|
|
else
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
fi
|
|
fi
|
|
}
|
|
|
|
execute_csf_ct_limit() {
|
|
clear
|
|
print_banner "Update CSF Connection Tracking Limit"
|
|
echo ""
|
|
|
|
# Check if CSF is installed
|
|
if ! command -v csf >/dev/null 2>&1; then
|
|
print_warning "CSF is not installed on this server"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Get recommended limit from recommendation
|
|
local recommended_limit=$(grep "|csf_ct_limit|" "$TEMP_DIR/recommendations.txt" 2>/dev/null | grep -oP 'to \K[0-9]+' || echo "100")
|
|
|
|
# Get current CT_LIMIT
|
|
local current_limit=$(grep "^CT_LIMIT" /etc/csf/csf.conf 2>/dev/null | grep -oP '"\K[0-9]+' || echo "unknown")
|
|
|
|
echo "Current CT_LIMIT: $current_limit"
|
|
echo "Recommended CT_LIMIT: $recommended_limit"
|
|
echo ""
|
|
echo "This will modify /etc/csf/csf.conf and restart CSF"
|
|
echo ""
|
|
read -p "Enter new CT_LIMIT value [$recommended_limit]: " new_limit
|
|
|
|
# Use recommended if nothing entered
|
|
[ -z "$new_limit" ] && new_limit=$recommended_limit
|
|
|
|
# Validate it's a number
|
|
if ! [[ "$new_limit" =~ ^[0-9]+$ ]]; then
|
|
print_warning "Invalid number"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Update CSF config
|
|
print_info "Updating CT_LIMIT to $new_limit..."
|
|
|
|
if [ -f /etc/csf/csf.conf ]; then
|
|
sed -i "s/^CT_LIMIT = .*/CT_LIMIT = \"$new_limit\"/" /etc/csf/csf.conf
|
|
|
|
# Restart CSF
|
|
print_info "Restarting CSF..."
|
|
csf -r >/dev/null 2>&1
|
|
|
|
print_success "CT_LIMIT updated successfully to $new_limit"
|
|
else
|
|
print_warning "Could not find /etc/csf/csf.conf"
|
|
fi
|
|
|
|
echo ""
|
|
# Verify domains still work after CT_LIMIT change
|
|
verify_domains_still_working
|
|
|
|
show_action_menu
|
|
}
|
|
|
|
execute_htaccess_domain_blocking() {
|
|
clear
|
|
print_banner "Add Bot Blocking to Domain .htaccess"
|
|
echo ""
|
|
|
|
# Get target domain from recommendation
|
|
local target_domain=$(grep "|htaccess_domain|" "$TEMP_DIR/recommendations.txt" 2>/dev/null | head -1 | grep -oP 'to \K[^ ]+' || echo "")
|
|
|
|
if [ -z "$target_domain" ]; then
|
|
print_warning "Could not determine target domain"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
echo "Target Domain: $target_domain"
|
|
echo ""
|
|
|
|
# Find document root for this domain using reference database
|
|
local doc_root=""
|
|
if [ -s "$TOOLKIT_BASE_DIR/.sysref" ]; then
|
|
doc_root=$(grep "^DOMAIN|$target_domain|" "$TOOLKIT_BASE_DIR/.sysref" 2>/dev/null | head -1 | cut -d'|' -f4)
|
|
fi
|
|
|
|
if [ -z "$doc_root" ]; then
|
|
print_warning "Document root not found in reference database"
|
|
echo "Please enter the document root manually:"
|
|
read -p "Document root: " doc_root
|
|
else
|
|
echo "Document root: $doc_root"
|
|
fi
|
|
|
|
if [ ! -d "$doc_root" ]; then
|
|
print_warning "Document root does not exist: $doc_root"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
local htaccess_file="$doc_root/.htaccess"
|
|
|
|
echo ""
|
|
echo "This will add bot blocking rules to: $htaccess_file"
|
|
echo ""
|
|
read -p "Proceed? (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Create backup
|
|
if [ -f "$htaccess_file" ]; then
|
|
cp "$htaccess_file" "$htaccess_file.backup.$(date +%Y%m%d_%H%M%S)"
|
|
print_info "Backed up existing .htaccess"
|
|
fi
|
|
|
|
# Generate bot blocking rules
|
|
print_info "Adding bot blocking rules..."
|
|
|
|
# Get high-risk IPs for this domain
|
|
local block_ips=$(grep "^[^|]*|$target_domain|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null | cut -d'|' -f1 | sort -u | while read ip; do
|
|
# Check if this IP has high threat score
|
|
if grep -q "|$ip$" "$TEMP_DIR/threat_scores.txt" 2>/dev/null; then
|
|
local score=$(grep "|$ip$" "$TEMP_DIR/threat_scores.txt" | cut -d'|' -f1)
|
|
if [ "$score" -ge 70 ]; then
|
|
echo "$ip"
|
|
fi
|
|
fi
|
|
done)
|
|
|
|
# Add rules to .htaccess
|
|
{
|
|
echo ""
|
|
echo "# Bot blocking rules added by toolkit on $(date)"
|
|
echo "# High-risk IPs (threat score >= 70)"
|
|
echo "<IfModule mod_authz_core.c>"
|
|
for ip in $block_ips; do
|
|
echo " Require not ip $ip"
|
|
done
|
|
echo "</IfModule>"
|
|
echo ""
|
|
} >> "$htaccess_file"
|
|
|
|
local block_count=$(echo "$block_ips" | wc -w)
|
|
print_success "Added blocking rules for $block_count IPs to $htaccess_file"
|
|
echo ""
|
|
echo "Backup saved to: $htaccess_file.backup.$(date +%Y%m%d_%H%M%S)"
|
|
echo ""
|
|
|
|
# Verify domains still work after .htaccess changes
|
|
verify_domains_still_working
|
|
|
|
show_action_menu
|
|
}
|
|
|
|
execute_apache_global_blocking() {
|
|
clear
|
|
print_banner "Add Global Bot Blocking to Apache"
|
|
echo ""
|
|
|
|
print_warning "This feature will add blocking rules to Apache pre-virtualhost configuration"
|
|
echo "This affects ALL domains on the server"
|
|
echo ""
|
|
|
|
# Determine Apache config location
|
|
local apache_conf=""
|
|
if [ -d "/etc/apache2/conf.d" ]; then
|
|
apache_conf="/etc/apache2/conf.d/bot_blocking.conf"
|
|
elif [ -d "/etc/httpd/conf.d" ]; then
|
|
apache_conf="/etc/httpd/conf.d/bot_blocking.conf"
|
|
else
|
|
print_warning "Could not determine Apache config directory"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
echo "Configuration will be written to: $apache_conf"
|
|
echo ""
|
|
read -p "Proceed? (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Create backup if file exists
|
|
if [ -f "$apache_conf" ]; then
|
|
cp "$apache_conf" "$apache_conf.backup.$(date +%Y%m%d_%H%M%S)"
|
|
print_info "Backed up existing configuration"
|
|
fi
|
|
|
|
# Generate global blocking rules
|
|
print_info "Generating global bot blocking configuration..."
|
|
|
|
{
|
|
echo "# Global bot blocking rules"
|
|
echo "# Generated by toolkit on $(date)"
|
|
echo ""
|
|
echo "<IfModule mod_authz_core.c>"
|
|
echo " # Block high-risk IPs (threat score >= 70)"
|
|
|
|
awk -F'|' '$1 >= 70 {print " Require not ip " $2}' "$TEMP_DIR/threat_scores.txt" 2>/dev/null
|
|
|
|
echo "</IfModule>"
|
|
echo ""
|
|
} > "$apache_conf"
|
|
|
|
local block_count=$(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | wc -l)
|
|
print_success "Created global blocking configuration with $block_count IPs"
|
|
|
|
echo ""
|
|
echo "Restarting Apache to apply changes..."
|
|
|
|
if systemctl restart httpd 2>/dev/null || systemctl restart apache2 2>/dev/null; then
|
|
print_success "Apache restarted successfully"
|
|
else
|
|
print_warning "Could not restart Apache - please restart manually"
|
|
fi
|
|
|
|
echo ""
|
|
# Verify domains still work after Apache global blocking
|
|
verify_domains_still_working
|
|
|
|
show_action_menu
|
|
}
|
|
|
|
execute_wp_hardening() {
|
|
clear
|
|
print_banner "WordPress Hardening"
|
|
echo ""
|
|
print_info "WordPress hardening feature coming soon..."
|
|
echo ""
|
|
echo "Recommended manual actions:"
|
|
echo " • Install Wordfence or similar security plugin"
|
|
echo " • Enable two-factor authentication"
|
|
echo " • Limit login attempts"
|
|
echo " • Disable XML-RPC if not needed"
|
|
echo " • Use strong passwords"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
}
|
|
|
|
execute_rate_limiting() {
|
|
clear
|
|
print_banner "Enable Rate Limiting"
|
|
echo ""
|
|
print_info "Rate limiting modules like mod_evasive/mod_security can help with application-level DoS"
|
|
echo ""
|
|
echo "For better bot protection, consider:"
|
|
echo " - IP blocking (options 1-3) - Block specific attacking IPs"
|
|
echo " - CSF CT_LIMIT adjustment (option 4) - Limit connections per IP"
|
|
echo " - .htaccess rules (option 5) - Domain-specific blocking"
|
|
echo ""
|
|
echo "This option (rate limiting) is currently a placeholder for future implementation."
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
}
|
|
|
|
# execute_csf_portflood() removed - not appropriate for web servers with 400+ sites
|
|
# Blocking ports 80/443 based on connection count would break legitimate traffic
|
|
|
|
execute_csf_synflood() {
|
|
clear
|
|
print_banner "Enable CSF SYNFLOOD Protection"
|
|
echo ""
|
|
|
|
if ! command -v csf >/dev/null 2>&1; then
|
|
print_warning "CSF is not installed on this server"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Get current SYNFLOOD setting
|
|
local current_synflood=$(grep "^SYNFLOOD = " /etc/csf/csf.conf 2>/dev/null | grep -oP '"\K[^"]+' || echo "0")
|
|
|
|
echo "Current SYNFLOOD protection: ${current_synflood}"
|
|
echo ""
|
|
echo "SYNFLOOD protects against SYN flood DDoS attacks by limiting"
|
|
echo "the rate of new TCP connections."
|
|
echo ""
|
|
echo "Recommended settings:"
|
|
echo " SYNFLOOD = \"1\" (enable protection)"
|
|
echo " SYNFLOOD_RATE = \"100/s\" (100 connections per second)"
|
|
echo " SYNFLOOD_BURST = \"150\" (allow burst of 150)"
|
|
echo ""
|
|
read -p "Enable SYNFLOOD protection? (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Update CSF config
|
|
print_info "Enabling SYNFLOOD protection..."
|
|
if [ -f /etc/csf/csf.conf ]; then
|
|
sed -i 's/^SYNFLOOD = .*/SYNFLOOD = "1"/' /etc/csf/csf.conf
|
|
sed -i 's/^SYNFLOOD_RATE = .*/SYNFLOOD_RATE = "100\/s"/' /etc/csf/csf.conf
|
|
sed -i 's/^SYNFLOOD_BURST = .*/SYNFLOOD_BURST = "150"/' /etc/csf/csf.conf
|
|
|
|
# Restart CSF
|
|
print_info "Restarting CSF..."
|
|
csf -r >/dev/null 2>&1
|
|
|
|
print_success "SYNFLOOD protection enabled"
|
|
else
|
|
print_warning "Could not find /etc/csf/csf.conf"
|
|
fi
|
|
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
}
|
|
|
|
execute_install_maxmind() {
|
|
clear
|
|
print_banner "Install MaxMind GeoIP2 for Country Blocking"
|
|
echo ""
|
|
|
|
# Check if already installed
|
|
if command -v mmdbinspect >/dev/null 2>&1; then
|
|
print_success "MaxMind GeoIP2 tools already installed"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo "1. Sign up for free license at: https://www.maxmind.com/en/geolite2/signup"
|
|
echo "2. Get your license key from account page"
|
|
echo "3. Install CSF GeoIP module: /usr/local/csf/bin/csftest.pl -g"
|
|
echo "4. Configure CC_DENY in /etc/csf/csf.conf with country codes"
|
|
echo ""
|
|
echo "Example: CC_DENY = \"CN,RU,KP\" (block China, Russia, North Korea)"
|
|
echo ""
|
|
else
|
|
print_info "MaxMind GeoIP2 not detected"
|
|
echo ""
|
|
echo "To install MaxMind GeoIP2 for CSF country blocking:"
|
|
echo ""
|
|
echo "1. Sign up for free MaxMind account:"
|
|
echo " https://www.maxmind.com/en/geolite2/signup"
|
|
echo ""
|
|
echo "2. Get your license key from:"
|
|
echo " https://www.maxmind.com/en/accounts/current/license-key"
|
|
echo ""
|
|
echo "3. Install GeoIP Perl module:"
|
|
echo " yum install perl-Geo-IP"
|
|
echo " # or"
|
|
echo " cpan -i Geo::IP"
|
|
echo ""
|
|
echo "4. Test CSF GeoIP support:"
|
|
echo " /usr/local/csf/bin/csftest.pl -g"
|
|
echo ""
|
|
echo "5. Configure CC_DENY in /etc/csf/csf.conf:"
|
|
echo " CC_DENY = \"CN,RU\" (example: block China & Russia)"
|
|
echo ""
|
|
echo "6. Restart CSF:"
|
|
echo " csf -r"
|
|
echo ""
|
|
fi
|
|
|
|
# Show geographic analysis if available
|
|
if [ -s "$TEMP_DIR/high_risk_networks.txt" ]; then
|
|
echo "=========================================================══"
|
|
echo "High-Risk Networks Detected:"
|
|
echo ""
|
|
head -10 "$TEMP_DIR/high_risk_networks.txt" | while read count network; do
|
|
echo " • $network - $count high-risk IPs"
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
}
|
|
|
|
################################################################################
|
|
# INTERACTIVE CSF BLOCKING
|
|
################################################################################
|
|
|
|
offer_csf_blocking() {
|
|
echo ""
|
|
echo "==============================================================="
|
|
print_header "🛡 INTERACTIVE THREAT BLOCKING"
|
|
|
|
# Check if CSF is installed
|
|
if ! command -v csf >/dev/null 2>&1; then
|
|
print_warning "CSF (ConfigServer Security & Firewall) is not installed"
|
|
echo "Cannot offer automatic blocking without CSF"
|
|
return 0
|
|
fi
|
|
|
|
# Get high-risk IPs (score >= 70)
|
|
local high_risk_ips=()
|
|
local ip_scores=()
|
|
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
while read -r line; do
|
|
local score=$(echo "$line" | cut -d'|' -f1)
|
|
local ip=$(echo "$line" | cut -d'|' -f2)
|
|
|
|
# Only include scores >= 70 (HIGH and CRITICAL)
|
|
if [ "$score" -ge 70 ]; then
|
|
# Skip excluded IPs
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
|
|
# Skip false positives
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ] && grep -q "^$ip|" "$TEMP_DIR/false_positives.txt" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
|
|
high_risk_ips+=("$ip")
|
|
ip_scores+=("$score")
|
|
fi
|
|
done < <(awk -F'|' '{print $1 "|" $2}' "$TEMP_DIR/threat_scores.txt" | sort -rn)
|
|
fi
|
|
|
|
# If no high-risk IPs, nothing to block
|
|
if [ ${#high_risk_ips[@]} -eq 0 ]; then
|
|
print_info "No high-risk IPs detected (score >= 70)"
|
|
return 0
|
|
fi
|
|
|
|
# Show IPs that would be blocked
|
|
echo ""
|
|
echo "Found ${#high_risk_ips[@]} high-risk IP(s) with threat score >= 70:"
|
|
echo ""
|
|
|
|
local count=0
|
|
for i in "${!high_risk_ips[@]}"; do
|
|
count=$((count + 1))
|
|
local ip="${high_risk_ips[$i]}"
|
|
local score="${ip_scores[$i]}"
|
|
local requests=$(grep "^$ip|" "$TEMP_DIR/bot_ips.txt" 2>/dev/null | cut -d'|' -f2 || echo "0")
|
|
|
|
# Color code by severity
|
|
if [ "$score" -ge 90 ]; then
|
|
echo -e " ${RED}[$count] $ip${NC} - Risk: ${RED}$score/100 CRITICAL${NC} ($requests requests)"
|
|
elif [ "$score" -ge 80 ]; then
|
|
echo -e " ${YELLOW}[$count] $ip${NC} - Risk: ${YELLOW}$score/100 HIGH${NC} ($requests requests)"
|
|
else
|
|
echo -e " [$count] $ip - Risk: $score/100 ELEVATED ($requests requests)"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "==============================================================="
|
|
echo ""
|
|
|
|
# Ask user if they want to block
|
|
echo -e "${BOLD}Would you like to temporarily block these IPs using CSF?${NC}"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " 1) Block for 1 hour (temporary - auto-expires)"
|
|
echo " 2) Block for 24 hours (temporary - auto-expires)"
|
|
echo " 3) Block permanently (requires manual unblock)"
|
|
echo " 4) Don't block (manual review)"
|
|
echo ""
|
|
read -p "Select option [1-4]: " block_choice
|
|
|
|
case "$block_choice" in
|
|
1)
|
|
local duration=3600 # 1 hour in seconds
|
|
local duration_text="1 hour"
|
|
apply_csf_blocks "$duration" "$duration_text" "${high_risk_ips[@]}"
|
|
;;
|
|
2)
|
|
local duration=86400 # 24 hours in seconds
|
|
local duration_text="24 hours"
|
|
apply_csf_blocks "$duration" "$duration_text" "${high_risk_ips[@]}"
|
|
;;
|
|
3)
|
|
apply_csf_permanent_blocks "${high_risk_ips[@]}"
|
|
;;
|
|
4)
|
|
print_info "Skipping automatic blocking - manual review recommended"
|
|
echo "You can block IPs manually using: csf -td IP DURATION"
|
|
;;
|
|
*)
|
|
print_warning "Invalid option - skipping blocking"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
apply_csf_blocks() {
|
|
local duration=$1
|
|
local duration_text=$2
|
|
shift 2
|
|
local ips=("$@")
|
|
|
|
echo ""
|
|
print_info "Applying temporary CSF blocks for $duration_text..."
|
|
echo ""
|
|
|
|
local success_count=0
|
|
local fail_count=0
|
|
|
|
for ip in "${ips[@]}"; do
|
|
# Get threat score for comment
|
|
local score=$(grep "|$ip$" "$TEMP_DIR/threat_scores.txt" 2>/dev/null | cut -d'|' -f1 || echo "unknown")
|
|
|
|
# Use csf -td for temporary deny
|
|
if csf -td "$ip" "$duration" "Bot threat score: $score/100 - Auto-blocked by toolkit" >/dev/null 2>&1; then
|
|
echo -e " ${GREEN}${NC} Blocked $ip for $duration_text (score: $score/100)"
|
|
success_count=$((success_count + 1))
|
|
else
|
|
echo -e " ${RED}${NC} Failed to block $ip"
|
|
fail_count=$((fail_count + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [ $success_count -gt 0 ]; then
|
|
print_success "Successfully blocked $success_count IP(s) for $duration_text"
|
|
echo ""
|
|
echo "These blocks will automatically expire after $duration_text"
|
|
echo "To view temporary blocks: csf -t"
|
|
echo "To remove a block early: csf -tr IP"
|
|
fi
|
|
|
|
if [ $fail_count -gt 0 ]; then
|
|
print_warning "$fail_count IP(s) failed to block - check CSF configuration"
|
|
fi
|
|
|
|
# Restart CSF to apply changes
|
|
print_info "Restarting CSF to apply changes..."
|
|
if csf -r >/dev/null 2>&1; then
|
|
print_success "CSF restarted successfully"
|
|
else
|
|
print_warning "CSF restart may have failed - check manually with: csf -r"
|
|
fi
|
|
}
|
|
|
|
apply_csf_permanent_blocks() {
|
|
local ips=("$@")
|
|
|
|
echo ""
|
|
print_warning "Applying PERMANENT CSF blocks..."
|
|
echo "These will require manual removal using: csf -dr IP"
|
|
echo ""
|
|
read -p "Are you sure? This is permanent! (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Cancelled permanent blocking"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
local success_count=0
|
|
local fail_count=0
|
|
|
|
for ip in "${ips[@]}"; do
|
|
local score=$(grep "|$ip$" "$TEMP_DIR/threat_scores.txt" 2>/dev/null | cut -d'|' -f1 || echo "unknown")
|
|
|
|
# Use csf -d for permanent deny
|
|
if csf -d "$ip" "Bot threat score: $score/100 - Permanently blocked by toolkit" >/dev/null 2>&1; then
|
|
echo -e " ${GREEN}${NC} Permanently blocked $ip (score: $score/100)"
|
|
success_count=$((success_count + 1))
|
|
else
|
|
echo -e " ${RED}${NC} Failed to block $ip"
|
|
fail_count=$((fail_count + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [ $success_count -gt 0 ]; then
|
|
print_success "Successfully blocked $success_count IP(s) permanently"
|
|
echo ""
|
|
echo "To view blocked IPs: csf -g"
|
|
echo "To remove a block: csf -dr IP"
|
|
fi
|
|
|
|
if [ $fail_count -gt 0 ]; then
|
|
print_warning "$fail_count IP(s) failed to block - check CSF configuration"
|
|
fi
|
|
|
|
# Restart CSF
|
|
print_info "Restarting CSF to apply changes..."
|
|
if csf -r >/dev/null 2>&1; then
|
|
print_success "CSF restarted successfully"
|
|
else
|
|
print_warning "CSF restart may have failed - check manually with: csf -r"
|
|
fi
|
|
}
|
|
|
|
# Run the script
|
|
main "$@"
|