#!/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, / "'"$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| "'"$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 "" for ip in $block_ips; do echo " Require not ip $ip" done echo "" 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 "" 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 "" 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 "$@"