diff --git a/modules/security/bot-analyzer.sh b/modules/security/bot-analyzer.sh index a87c83d..ee43209 100755 --- a/modules/security/bot-analyzer.sh +++ b/modules/security/bot-analyzer.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -eo pipefail ############################################################################# # Apache/cPanel Domain Log Bot & Botnet Analyzer @@ -50,6 +51,12 @@ 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 +# Cache CSF availability (avoid checking command_v csf 5 times) +CSF_AVAILABLE=false +if command -v csf >/dev/null 2>&1 && [ -f /etc/csf/csf.conf ]; then + CSF_AVAILABLE=true +fi + # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in @@ -461,9 +468,25 @@ parse_logs() { user_agent = "-" } + # Extract additional headers for enhanced analysis + referer = "-" + accept_lang = "-" + accept_encoding = "-" + + # Extract Referer header + if (match($0, /"([^"]*)"[[:space:]]*"[^"]*"[[:space:]]*$/, ref)) { + referer = ref[1] + if (referer == "") referer = "-" + } + + # Try to extract Accept-Language from log (if available) + if (match($0, /Accept-Language: ([^ ,;]*)/i, al)) { + accept_lang = al[1] + } + # Only output valid entries if (ip != "" && ip !~ /^[[:space:]]*$/) { - print ip "|" domain "|" request_url "|" status "|" size "|" user_agent "|" http_method "|" timestamp + print ip "|" domain "|" request_url "|" status "|" size "|" user_agent "|" http_method "|" timestamp "|" referer "|" accept_lang } }' "$logfile" 2>/dev/null done @@ -623,6 +646,155 @@ classify_bots() { return 0 } +############################################################################# +# NEW: Header Analysis for Bot Detection +############################################################################# + +analyze_headers() { + print_info "Analyzing request headers for bot patterns..." + + # Analyze header patterns to improve bot detection accuracy + awk -F'|' ' + { + ip = $1 + domain = $2 + url = $3 + status = $4 + size = $5 + ua = $6 + method = $7 + timestamp = $8 + referer = $9 + accept_lang = $10 + + ua_lower = tolower(ua) + referer_lower = tolower(referer) + + # Pattern 1: Empty or missing Accept-Language (bots often have none) + if (accept_lang == "-" || accept_lang == "") { + empty_lang[ip]++ + } + + # Pattern 2: All-accepting Accept-Language (bots accept everything) + # Real browsers: en-US,en;q=0.9 (specific negotiation) + # Bots: */* or empty + if (accept_lang == "*/*" || accept_lang == "*") { + accepts_all[ip]++ + } + + # Pattern 3: Suspicious Referer patterns + # Bots often have no referer or fake ones + if (referer == "-" || referer == "") { + no_referer[ip]++ + } + + # Pattern 4: Referer from suspicious sources + if (match(referer_lower, /badbot|crawler|scanner|nikto|nmap|metasploit|sqlmap/)) { + suspicious_referer[ip]++ + } + + # Pattern 5: Referer mismatch (referer domain != target domain) + # Real users: referer usually from same domain or search engine + # Bots: random referer or none + if (referer != "-" && !match(referer_lower, domain)) { + if (!match(referer_lower, /google|bing|yahoo|facebook|twitter|reddit|instagram/)) { + cross_domain_referer[ip]++ + } + } + + # Pattern 6: HEAD requests (bot reconnaissance) + # Some bots use HEAD to test server without loading content + if (method == "HEAD") { + head_requests[ip]++ + } + + # Pattern 7: Options/Trace requests (security testing) + # Real users never use these + if (method == "OPTIONS" || method == "TRACE") { + dangerous_methods[ip]++ + } + } + END { + # Flag IPs with multiple suspicious header patterns + for (ip in empty_lang) { + score = 0 + + # Assign points for suspicious header combinations + if (ip in empty_lang) score += 2 + if (ip in accepts_all) score += 3 + if (ip in no_referer) score += 1 + if (ip in suspicious_referer) score += 5 + if (ip in cross_domain_referer && (ip in no_referer)) score += 2 + if (ip in head_requests && (head_requests[ip] > 50)) score += 4 + if (ip in dangerous_methods) score += 10 + + # Only flag if high header suspicion score + if (score >= 8) { + print ip "|header_anomaly|" score > "'"$TEMP_DIR"'/header_anomalies.txt" + } + } + }' < "$TEMP_DIR/parsed_logs.txt" + + # Create file if it doesn't exist + touch "$TEMP_DIR/header_anomalies.txt" + print_success "Header analysis complete" +} + +############################################################################# +# NEW: Entry Point Analysis (where bots start) +############################################################################# + +analyze_entry_points() { + print_info "Analyzing first request patterns (bot vs. user entry points)..." + + # Get first request from each IP + awk -F'|' ' + BEGIN { + ip_first_request[ip] = url + ip_first_status[ip] = status + } + { + ip = $1 + url = $3 + status = $4 + + # Track first request from each IP (first occurrence in sorted logs) + if (!(ip in first_seen)) { + first_seen[ip] = 1 + ip_first_request[ip] = url + ip_first_status[ip] = status + } + } + END { + for (ip in ip_first_request) { + url = ip_first_request[ip] + status = ip_first_status[ip] + url_lower = tolower(url) + + # Suspicious entry points indicate bot/scanner + if (match(url_lower, /wp-admin|phpmyadmin|admin|xmlrpc|shell\.php|\.env|\.git|backdoor|config\.php/)) { + print ip "|admin_entry|" url "|" status > "'"$TEMP_DIR"'/suspicious_entry_points.txt" + } + # Legitimate entry: homepage or search + else if (match(url_lower, /^\/index|^\/$|^\/search|^\/page|^\/category/)) { + print ip "|normal_entry|" url > "'"$TEMP_DIR"'/normal_entry_points.txt" + } + # Unusual but possible: static files + else if (match(url_lower, /\.(css|js|jpg|png|gif|woff|svg)$/)) { + print ip "|static_entry|" url > "'"$TEMP_DIR"'/static_entry_points.txt" + } + } + }' < "$TEMP_DIR/parsed_logs.txt" + + # Count suspicious entry points + if [ -f "$TEMP_DIR/suspicious_entry_points.txt" ]; then + suspicious_count=$(wc -l < "$TEMP_DIR/suspicious_entry_points.txt") + print_success "Found $suspicious_count IPs with suspicious entry points" + else + touch "$TEMP_DIR/suspicious_entry_points.txt" + fi +} + ############################################################################# # Threat Detection ############################################################################# @@ -744,9 +916,9 @@ detect_threats() { # 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" | \ + grep "|$attack_type$" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | \ awk -F'|' '{print $1"|"$2"|"$3"|"$4}' | \ - sort | uniq -c | sort -rn > "$TEMP_DIR/${attack_type}_attempts.txt" + sort | uniq -c | sort -rn > "$TEMP_DIR/${attack_type}_attempts.txt" || true done # Old sqli file for backwards compatibility @@ -792,6 +964,127 @@ detect_threats() { print_success "Threat detection complete" } +############################################################################# +# NEW: URL Entropy Analysis (detects fuzzing/scanning) +############################################################################# + +analyze_url_entropy() { + print_info "Analyzing URL parameter entropy (fuzzing detection)..." + + # Detect IPs that generate random parameters (scanning/fuzzing behavior) + awk -F'|' ' + { + ip = $1 + url = $3 + url_lower = tolower(url) + + # Extract base path (before query string) + if (match(url, /([^?]+)/, path)) { + base_path = path[1] + } else { + base_path = url + } + + # Extract query parameter values (not keys) + if (match(url, /\?(.+)/, query)) { + param_string = query[1] + + # Count numeric parameters + if (match(param_string, /[0-9]+/)) { + numeric_params[ip base_path]++ + } + } + + # Track URLs from each IP + urls_per_ip[ip]++ + unique_paths[ip][base_path]++ + } + END { + # Find IPs hitting many unique paths with numeric variations + for (ip in urls_per_ip) { + unique_path_count = length(unique_paths[ip]) + + # If IP hits >20 URLs with lots of numeric params = scanning + if (urls_per_ip[ip] > 20 && unique_path_count > 5) { + # Likely fuzzing/parameter scanning + print ip "|parameter_fuzzing|" urls_per_ip[ip] "|" unique_path_count > "'"$TEMP_DIR"'/fuzzing_ips.txt" + } + } + }' < "$TEMP_DIR/parsed_logs.txt" + + # Create file if it doesn't exist + touch "$TEMP_DIR/fuzzing_ips.txt" + print_success "URL entropy analysis complete" +} + +############################################################################# +# NEW: Request Timing Analysis (DDoS & bot behavior detection) +############################################################################# + +analyze_request_timing() { + print_info "Analyzing request timing patterns (DDoS detection)..." + + # Analyze timing consistency to detect bots/DDoS + awk -F'|' ' + { + ip = $1 + timestamp = $8 + + # Parse timestamp to get seconds (simplified) + if (match(timestamp, /([0-9]{2}):([0-9]{2}):([0-9]{2})/, t)) { + seconds = t[1] * 3600 + t[2] * 60 + t[3] + + # Store timestamps for analysis + if (!(ip in request_times)) { + request_count[ip] = 0 + request_times[ip] = "" + } + + request_count[ip]++ + request_times[ip] = request_times[ip] seconds "," + } + } + END { + # Analyze timing patterns + for (ip in request_count) { + count = request_count[ip] + + # If more than 50 requests in the log + if (count > 50) { + # Split times and calculate average interval + split(request_times[ip], times, ",") + + total_intervals = 0 + interval_count = 0 + + for (i = 2; i < length(times); i++) { + if (times[i] > 0 && times[i-1] > 0) { + interval = times[i] - times[i-1] + if (interval < 0) interval += 86400 # Handle day boundary + + total_intervals += interval + interval_count++ + } + } + + if (interval_count > 0) { + avg_interval = total_intervals / interval_count + + # Very consistent timing = bot (typically 0.5-2 seconds apart) + # Real users: highly variable (5-60+ seconds) + if (avg_interval < 3 && count > 100) { + print ip "|consistent_bot_timing|" avg_interval "|" count > "'"$TEMP_DIR"'/timing_anomalies.txt" + } + } + } + } + }' < "$TEMP_DIR/parsed_logs.txt" + + # Create file if it doesn't exist + touch "$TEMP_DIR/timing_anomalies.txt" + print_success "Request timing analysis complete" +} + ############################################################################# # NEW: Success Rate & Behavior Analysis (Added for accuracy improvement) ############################################################################# @@ -1106,6 +1399,30 @@ calculate_threat_scores() { ip_fail_rates["$ip"]=$fail_rate done < "$TEMP_DIR/ip_success_rates.txt" + # NEW: Load header anomalies + declare -A header_anomalies + [ -f "$TEMP_DIR/header_anomalies.txt" ] && while IFS='|' read -r ip anomaly_type score; do + header_anomalies["$ip"]=$score + done < "$TEMP_DIR/header_anomalies.txt" + + # NEW: Load suspicious entry points + declare -A suspicious_entry_ips + [ -f "$TEMP_DIR/suspicious_entry_points.txt" ] && while IFS='|' read -r ip entry_type url status; do + suspicious_entry_ips["$ip"]=1 + done < "$TEMP_DIR/suspicious_entry_points.txt" + + # NEW: Load fuzzing/parameter scanning IPs + declare -A fuzzing_ips + [ -f "$TEMP_DIR/fuzzing_ips.txt" ] && while IFS='|' read -r ip fuzz_type total_urls unique_paths; do + fuzzing_ips["$ip"]=$total_urls + done < "$TEMP_DIR/fuzzing_ips.txt" + + # NEW: Load timing anomalies (consistent bot timing) + declare -A timing_anomalies + [ -f "$TEMP_DIR/timing_anomalies.txt" ] && while IFS='|' read -r ip timing_type avg_interval total_reqs; do + timing_anomalies["$ip"]=$avg_interval + done < "$TEMP_DIR/timing_anomalies.txt" + # Now calculate scores for each IP (using pre-counted requests) for ip in "${!ip_request_counts[@]}"; do # Skip excluded IPs @@ -1152,6 +1469,36 @@ calculate_threat_scores() { [ -n "${threat_ips_suspicious[$ip]}" ] && score=$((score + 10)) [ -n "${threat_ips_ddos[$ip]}" ] && score=$((score + 10)) + # NEW: Header anomalies (strong indicator of bots) + if [ -n "${header_anomalies[$ip]}" ]; then + header_score=${header_anomalies[$ip]} + if [ "$header_score" -ge 12 ]; then + score=$((score + 8)) # Multiple header suspicions + elif [ "$header_score" -ge 8 ]; then + score=$((score + 5)) # Moderate header anomalies + fi + fi + + # NEW: Suspicious entry point (direct jump to admin/config) + if [ -n "${suspicious_entry_ips[$ip]}" ]; then + score=$((score + 6)) # Direct attack attempt without probing + fi + + # NEW: Fuzzing/parameter scanning behavior + if [ -n "${fuzzing_ips[$ip]}" ]; then + fuzz_requests=${fuzzing_ips[$ip]} + if [ "$fuzz_requests" -gt 100 ]; then + score=$((score + 7)) # Aggressive fuzzing + elif [ "$fuzz_requests" -gt 50 ]; then + score=$((score + 4)) # Moderate fuzzing + fi + fi + + # NEW: Timing anomalies (very consistent request timing = bot) + if [ -n "${timing_anomalies[$ip]}" ]; then + score=$((score + 6)) # Very consistent timing indicates automation + fi + # Admin probing - IMPROVED: Raised threshold to 50 (only failed attempts counted) admin_count=${threat_admin_count[$ip]:-0} if [ "$admin_count" -gt 100 ] 2>/dev/null; then @@ -1226,22 +1573,20 @@ calculate_threat_scores() { detect_false_positives() { print_info "Detecting legitimate services (false positives)..." - # Known monitoring service patterns + # Known monitoring service patterns and legitimate CDNs cat "$TEMP_DIR/parsed_logs.txt" | awk -F'|' '{ ip = $1 domain = $2 url = $3 ua = tolower($6) - # Pingdom + # Monitoring Services 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 } @@ -1250,12 +1595,28 @@ detect_false_positives() { print ip "|WordPress Cache Preload|" ua "|" domain } # Legitimate backup services - else if (match(ua, /jetpack|vaultpress|updraftplus/)) { + else if (match(ua, /jetpack|vaultpress|updraftplus|backwpup/)) { print ip "|Backup Service|" ua "|" domain } + # NEW: Google services + else if (match(ua, /googlebot|google web preview|google-read-aloud|bingbot|slurp|duckduckbot/)) { + print ip "|Search Engine Bot|" ua "|" domain + } + # NEW: Content delivery networks (usually legit) + else if (match(ua, /cloudflare|akamai|fastly|cloudfront|edgecast|maxcdn|amazon/)) { + print ip "|CDN Service|" ua "|" domain + } + # NEW: Analytics services + else if (match(ua, /googleanalytics|fbexternalhit|twitterbot|linkedinbot|pinterestbot|whatsapp|telegram/)) { + print ip "|Analytics\/Social Service|" ua "|" domain + } + # NEW: Payment processors (legitimate POST to checkout) + else if (match(url, /checkout|payment|paypal|stripe|square/) && match(ua, /paypal|stripe|square/)) { + print ip "|Payment Processor|" ua "|" domain + } }' | sort -u > "$TEMP_DIR/false_positives.txt" - print_success "False positive detection complete" + print_success "False positive detection complete ($(wc -l < "$TEMP_DIR/false_positives.txt") legitimate services identified)" } ############################################################################# @@ -1315,14 +1676,97 @@ generate_statistics() { while read -r domain; do echo "$domain" > "$TEMP_DIR/domain_${domain}_stats.txt" - grep "^$domain|" "$TEMP_DIR/domain_bot_types.txt" | cut -d'|' -f2 | \ - sort | uniq -c | sort -rn >> "$TEMP_DIR/domain_${domain}_stats.txt" + grep "^$domain|" "$TEMP_DIR/domain_bot_types.txt" 2>/dev/null | cut -d'|' -f2 | \ + sort | uniq -c | sort -rn >> "$TEMP_DIR/domain_${domain}_stats.txt" || true done < "$TEMP_DIR/all_domains.txt" fi print_success "Statistics generated" } +############################################################################# +# NEW: Comparison Reports (detect trends) +############################################################################# + +generate_comparison_report() { + print_info "Generating trend analysis..." + + # Store current results for comparison with previous analysis + local history_dir="$TOOLKIT_TMP_DIR/analysis_history" + mkdir -p "$history_dir" + + local timestamp=$(date +%Y%m%d_%H%M%S) + local today=$(date +%Y%m%d) + local latest_report="$history_dir/latest_analysis_$today.txt" + + # Extract key metrics from current analysis + { + echo "Timestamp: $timestamp" + echo "Total_Requests: $(wc -l < "$TEMP_DIR/parsed_logs.txt" 2>/dev/null || echo 0)" + echo "Unique_IPs: $(awk -F'|' '{print $1}' "$TEMP_DIR/parsed_logs.txt" 2>/dev/null | sort -u | wc -l || echo 0)" + echo "High_Risk_IPs: $(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | wc -l || echo 0)" + echo "Attack_Vectors: $(awk -F'|' '{print $5}' "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | sort -u | wc -l || echo 0)" + echo "SQL_Injection: $(wc -l < "$TEMP_DIR/sqli_attempts.txt" 2>/dev/null || echo 0)" + echo "XSS_Attempts: $(wc -l < "$TEMP_DIR/xss_attempts.txt" 2>/dev/null || echo 0)" + echo "Bot_Traffic: $(wc -l < "$TEMP_DIR/classified_bots.txt" 2>/dev/null || echo 0)" + echo "Suspected_Scanners: $(wc -l < "$TEMP_DIR/high_failure_ips.txt" 2>/dev/null || echo 0)" + echo "Header_Anomalies: $(wc -l < "$TEMP_DIR/header_anomalies.txt" 2>/dev/null || echo 0)" + echo "Entry_Point_Suspicious: $(wc -l < "$TEMP_DIR/suspicious_entry_points.txt" 2>/dev/null || echo 0)" + echo "Fuzzing_IPs: $(wc -l < "$TEMP_DIR/fuzzing_ips.txt" 2>/dev/null || echo 0)" + } > "$latest_report" + + # Compare with previous day's analysis + local yesterday=$(date -d "1 day ago" +%Y%m%d 2>/dev/null || date -v-1d +%Y%m%d 2>/dev/null) + local previous_report="$history_dir/latest_analysis_${yesterday}.txt" + + if [ -f "$previous_report" ]; then + echo "" + print_header "THREAT TREND ANALYSIS (Compared to previous day)" + + # Extract metrics and calculate differences + local curr_high_risk=$(grep "^High_Risk_IPs:" "$latest_report" | cut -d: -f2 | tr -d ' ') + local prev_high_risk=$(grep "^High_Risk_IPs:" "$previous_report" | cut -d: -f2 | tr -d ' ') + local risk_diff=$((curr_high_risk - prev_high_risk)) + local risk_pct=0 + + if [ "$prev_high_risk" -gt 0 ]; then + risk_pct=$((risk_diff * 100 / prev_high_risk)) + fi + + # Display trend + if [ "$risk_diff" -gt 0 ]; then + echo "⚠️ High-Risk IPs: $curr_high_risk (↑ $risk_diff, $risk_pct% increase)" + elif [ "$risk_diff" -lt 0 ]; then + echo "✓ High-Risk IPs: $curr_high_risk (↓ $((risk_diff * -1)), ${risk_pct}% decrease)" + else + echo "→ High-Risk IPs: $curr_high_risk (no change)" + fi + + # Repeat for other metrics + local curr_sql=$(grep "^SQL_Injection:" "$latest_report" | cut -d: -f2 | tr -d ' ') + local prev_sql=$(grep "^SQL_Injection:" "$previous_report" | cut -d: -f2 | tr -d ' ') + local sql_diff=$((curr_sql - prev_sql)) + + if [ "$sql_diff" -gt 0 ]; then + echo "⚠️ SQL Injection Attempts: $curr_sql (↑ $sql_diff new attempts)" + elif [ "$sql_diff" -lt 0 ]; then + echo "✓ SQL Injection Attempts: $curr_sql (↓ $((sql_diff * -1)) fewer)" + fi + + # Track repeat attackers + local repeat_attackers=0 + if [ -f "$history_dir/known_attackers_${yesterday}.txt" ]; then + repeat_attackers=$(grep -Fx -f <(awk -F'|' '$1 >= 70 {print $2}' "$TEMP_DIR/threat_scores.txt" 2>/dev/null) "$history_dir/known_attackers_${yesterday}.txt" 2>/dev/null | wc -l || echo 0) + if [ "$repeat_attackers" -gt 0 ]; then + echo "🔄 Repeat Attackers: $repeat_attackers IPs from previous day" + fi + fi + fi + + # Save current high-risk IPs for tomorrow's comparison + awk -F'|' '$1 >= 70 {print $2}' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | sort -u > "$history_dir/known_attackers_${today}.txt" +} + ############################################################################# # Report Generation ############################################################################# @@ -1374,6 +1818,66 @@ generate_report() { echo "" alert_count=$((alert_count + 1)) fi + + # NEW: Check for header anomalies (bot signatures) + if [ -s "$TEMP_DIR/header_anomalies.txt" ]; then + header_count=$(wc -l < "$TEMP_DIR/header_anomalies.txt") + print_alert "Header-based bot signatures detected: $header_count IPs" + echo " These IPs show suspicious header patterns (missing/unusual Accept-Language, Referer, etc.)" + head -5 "$TEMP_DIR/header_anomalies.txt" | while read -r line; do + ip=$(echo "$line" | awk -F'|' '{print $1}') + anomaly_type=$(echo "$line" | awk -F'|' '{print $2}') + score=$(echo "$line" | awk -F'|' '{print $3}') + printf " • %s - Anomaly score: %s (detected: %s)\n" "$ip" "$score" "$anomaly_type" + done + echo "" + alert_count=$((alert_count + 1)) + fi + + # NEW: Check for suspicious entry points + if [ -s "$TEMP_DIR/suspicious_entry_points.txt" ]; then + entry_count=$(wc -l < "$TEMP_DIR/suspicious_entry_points.txt") + print_alert "Suspicious entry points detected: $entry_count IPs" + echo " These IPs skip homepage/search and go straight to admin/config:" + head -5 "$TEMP_DIR/suspicious_entry_points.txt" | while read -r line; do + ip=$(echo "$line" | awk -F'|' '{print $1}') + url=$(echo "$line" | awk -F'|' '{print $3}') + status=$(echo "$line" | awk -F'|' '{print $4}') + printf " • %s → %s (HTTP %s)\n" "$ip" "$url" "$status" + done + echo "" + alert_count=$((alert_count + 1)) + fi + + # NEW: Check for fuzzing/scanning behavior + if [ -s "$TEMP_DIR/fuzzing_ips.txt" ]; then + fuzz_count=$(wc -l < "$TEMP_DIR/fuzzing_ips.txt") + print_alert "Parameter fuzzing/scanning detected: $fuzz_count IPs" + echo " These IPs are testing random parameters (vulnerability scanning):" + head -5 "$TEMP_DIR/fuzzing_ips.txt" | while read -r line; do + ip=$(echo "$line" | awk -F'|' '{print $1}') + total_urls=$(echo "$line" | awk -F'|' '{print $3}') + unique_paths=$(echo "$line" | awk -F'|' '{print $4}') + printf " • %s - %s URLs across %s paths\n" "$ip" "$total_urls" "$unique_paths" + done + echo "" + alert_count=$((alert_count + 1)) + fi + + # NEW: Check for timing anomalies (bot signatures) + if [ -s "$TEMP_DIR/timing_anomalies.txt" ]; then + timing_count=$(wc -l < "$TEMP_DIR/timing_anomalies.txt") + print_alert "Consistent timing pattern detected: $timing_count IPs" + echo " These IPs show mechanical request patterns (bot behavior):" + head -5 "$TEMP_DIR/timing_anomalies.txt" | while read -r line; do + ip=$(echo "$line" | awk -F'|' '{print $1}') + avg_interval=$(echo "$line" | awk -F'|' '{print $3}') + total_reqs=$(echo "$line" | awk -F'|' '{print $4}') + printf " • %s - %.1f seconds average between requests (%s total requests)\n" "$ip" "$avg_interval" "$total_reqs" + done + echo "" + alert_count=$((alert_count + 1)) + fi # Check for rapid-fire IPs (potential DDoS) if [ -s "$TEMP_DIR/rapid_fire_ips.txt" ]; then @@ -2148,14 +2652,21 @@ main() { exit 1 } + # NEW: Enhanced analysis functions + analyze_headers # Detect header-based bot patterns + analyze_entry_points # Detect suspicious entry points + analyze_url_entropy # Detect fuzzing/parameter scanning + analyze_request_timing # Detect DDoS patterns via timing + detect_server_ips detect_threats - analyze_success_rates # NEW: Analyze success/failure rates for better accuracy + analyze_success_rates # Analyze success/failure rates for better accuracy detect_botnets analyze_time_series calculate_threat_scores detect_false_positives generate_statistics + generate_comparison_report # NEW: Show trends vs previous day generate_report print_success "Analysis complete!" @@ -2380,7 +2891,7 @@ generate_recommendations() { # 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 + if [ "$CSF_AVAILABLE" = true ]; 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") @@ -2427,7 +2938,7 @@ generate_recommendations() { 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) + 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 || echo "") fi # Generate appropriate recommendation based on how many domains have WordPress attacks @@ -2651,7 +3162,7 @@ show_detailed_recommendations() { local target_domain=$(echo "$action_title" | grep -oP 'to \K[^ ]+' 2>/dev/null) 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 + grep "^$target_domain|" "$TEMP_DIR/domain_threats_sorted.txt" 2>/dev/null | 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" @@ -2895,7 +3406,7 @@ execute_ip_blocking_specific() { echo "" # Check if CSF is installed - if ! command -v csf >/dev/null 2>&1; then + if [ "$CSF_AVAILABLE" != true ]; then print_warning "CSF (ConfigServer Security & Firewall) is not installed" echo "" read -p "Press Enter to continue..." @@ -3052,7 +3563,7 @@ execute_csf_ct_limit() { echo "" # Check if CSF is installed - if ! command -v csf >/dev/null 2>&1; then + if [ "$CSF_AVAILABLE" != true ]; then print_warning "CSF is not installed on this server" echo "" read -p "Press Enter to continue..." @@ -3129,7 +3640,7 @@ execute_htaccess_domain_blocking() { # Find document root for this domain using reference database local doc_root="" if [ -s "$SCRIPT_DIR/.sysref" ]; then - doc_root=$(grep "^DOMAIN|$target_domain|" "$SCRIPT_DIR/.sysref" 2>/dev/null | head -1 | cut -d'|' -f4) + doc_root=$(grep "^DOMAIN|$target_domain|" "$SCRIPT_DIR/.sysref" 2>/dev/null | head -1 | cut -d'|' -f4 || echo "") fi if [ -z "$doc_root" ]; then @@ -3173,15 +3684,15 @@ execute_htaccess_domain_blocking() { print_info "Adding bot blocking rules..." # Get high-risk IPs for this domain - local block_ips=$(cat "$TEMP_DIR/parsed_logs.txt" 2>/dev/null | grep "^[^|]*|$target_domain|" | cut -d'|' -f1 | sort -u | while read ip; do + local block_ips=$(cat "$TEMP_DIR/parsed_logs.txt" 2>/dev/null | grep "^[^|]*|$target_domain|" 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 + local score=$(grep "|$ip$" "$TEMP_DIR/threat_scores.txt" 2>/dev/null | cut -d'|' -f1 || echo "0") + if [ "${score:-0}" -ge 70 ]; then echo "$ip" fi fi - done) + done || true) # Add rules to .htaccess { @@ -3326,7 +3837,7 @@ execute_csf_synflood() { print_banner "Enable CSF SYNFLOOD Protection" echo "" - if ! command -v csf >/dev/null 2>&1; then + if [ "$CSF_AVAILABLE" != true ]; then print_warning "CSF is not installed on this server" echo "" read -p "Press Enter to continue..." @@ -3447,7 +3958,7 @@ offer_csf_blocking() { print_header "🛡 INTERACTIVE THREAT BLOCKING" # Check if CSF is installed - if ! command -v csf >/dev/null 2>&1; then + if [ "$CSF_AVAILABLE" != true ]; then print_warning "CSF (ConfigServer Security & Firewall) is not installed" echo "Cannot offer automatic blocking without CSF" return 0