0c4d970053
Added comprehensive IP reputation tracking to bot analyzer script.
UPDATED:
- modules/security/bot-analyzer.sh
* Now tracks ALL analyzed IPs in centralized reputation database
* Tags IPs with specific attack types discovered:
- SQL_INJECTION: SQL injection attempts
- XSS: Cross-site scripting attempts
- PATH_TRAVERSAL: Directory traversal attempts
- RCE: Remote code execution/shell upload attempts
- BRUTEFORCE: Login bruteforce attempts
- DDOS: Rapid-fire/DDoS patterns
- SCANNER: Suspicious user-agents
* Records hit counts for each IP
* Background processing for performance
* Waits for all updates to complete before finishing
HOW IT WORKS:
When bot analyzer calculates threat scores for each IP, it now:
1. Updates hit count in IP reputation database
2. Tags IP with ALL attack types found (not just one)
3. Runs in background to maintain analysis speed
4. Waits for all background updates before completing
EXAMPLE:
If bot analyzer finds an IP doing:
- SQL injection (15 points)
- XSS attacks (12 points)
- 1000 requests (5 points)
The IP gets:
- Total score: 32/100
- Tags: SQL_INJECTION + XSS
- Hit count: 1000
- Last activity: "Bot analyzer: SQL injection attempts"
This data is then available to ALL other scripts!
BENEFITS:
✓ Bot analysis intelligence shared across entire toolkit
✓ IPs tracked with multiple attack types
✓ Historical data persists between analysis runs
✓ Other scripts can check IP reputation before processing
✓ Build comprehensive threat profile over time
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
3275 lines
124 KiB
Bash
Executable File
3275 lines
124 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
#############################################################################
|
|
# Apache/cPanel Domain Log Bot & Botnet Analyzer
|
|
# Version: 3.1 Enhanced (with Library Integration)
|
|
# Advanced log analysis for bot activity, security threats, and botnets
|
|
#
|
|
# Features:
|
|
# - Comprehensive bot classification (legitimate, AI, monitoring, suspicious)
|
|
# - Enhanced attack vector detection (SQL injection, XSS, path traversal,
|
|
# RCE/shell upload, info disclosure, login bruteforce)
|
|
# - Threat scoring system (0-100 risk scores for each IP)
|
|
# - Time-series analysis with hourly traffic visualization
|
|
# - Response code intelligence (what are bots finding?)
|
|
# - False positive detection for legitimate monitoring services
|
|
# - Bandwidth cost estimation for bot traffic
|
|
# - Botnet pattern analysis (coordinated attacks, DDoS detection)
|
|
# - Prioritized blocklists sorted by threat severity
|
|
# - Actionable reports with copy-paste ready configurations
|
|
# - Performance optimized for large log files (>500k entries)
|
|
# - User filtering (analyze all users or specific user)
|
|
# - Auto-detects log directory based on control panel
|
|
#############################################################################
|
|
|
|
# Load libraries
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
|
source "$SCRIPT_DIR/lib/system-detect.sh"
|
|
source "$SCRIPT_DIR/lib/user-manager.sh"
|
|
source "$SCRIPT_DIR/lib/ip-reputation.sh"
|
|
|
|
# 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
|
|
#############################################################################
|
|
|
|
# Legitimate bots (search engines)
|
|
declare -A LEGIT_BOTS=(
|
|
["Googlebot"]="Google Search"
|
|
["Googlebot-Image"]="Google Images"
|
|
["Googlebot-Video"]="Google Video"
|
|
["Googlebot-News"]="Google News"
|
|
["Google-InspectionTool"]="Google Search Console"
|
|
["Storebot-Google"]="Google Merchant"
|
|
["APIs-Google"]="Google APIs"
|
|
["AdsBot-Google"]="Google Ads"
|
|
["Mediapartners-Google"]="Google AdSense"
|
|
["bingbot"]="Bing Search"
|
|
["msnbot"]="MSN Search"
|
|
["Slurp"]="Yahoo Search"
|
|
["DuckDuckBot"]="DuckDuckGo"
|
|
["Baiduspider"]="Baidu Search"
|
|
["YandexBot"]="Yandex Search"
|
|
)
|
|
|
|
# AI Bots
|
|
declare -A AI_BOTS=(
|
|
["GPTBot"]="OpenAI"
|
|
["ChatGPT-User"]="OpenAI ChatGPT"
|
|
["ClaudeBot"]="Anthropic Claude"
|
|
["Claude-Web"]="Anthropic Web"
|
|
["Bytespider"]="ByteDance (TikTok)"
|
|
["PetalBot"]="Huawei"
|
|
["CCBot"]="Common Crawl"
|
|
["anthropic-ai"]="Anthropic"
|
|
["Applebot"]="Apple Intelligence"
|
|
["facebookexternalhit"]="Facebook/Meta"
|
|
["Meta-ExternalAgent"]="Meta AI"
|
|
["cohere-ai"]="Cohere AI"
|
|
["PerplexityBot"]="Perplexity AI"
|
|
["YouBot"]="You.com AI"
|
|
["Diffbot"]="Diffbot AI"
|
|
["ImagesiftBot"]="ImageSift AI"
|
|
["Omgilibot"]="Omgili AI"
|
|
)
|
|
|
|
# Monitoring/SEO bots
|
|
declare -A MONITOR_BOTS=(
|
|
["AhrefsBot"]="Ahrefs SEO"
|
|
["SemrushBot"]="SEMrush SEO"
|
|
["MJ12bot"]="Majestic SEO"
|
|
["DotBot"]="Moz/OpenSite"
|
|
["BLEXBot"]="BLEXBot SEO"
|
|
["PingdomBot"]="Pingdom Monitoring"
|
|
["UptimeRobot"]="Uptime Monitoring"
|
|
["StatusCake"]="StatusCake Monitoring"
|
|
["SiteImprove"]="SiteImprove Analytics"
|
|
)
|
|
|
|
# Suspicious/Aggressive bots (malicious or security scanners)
|
|
declare -A SUSPICIOUS_BOTS=(
|
|
["MauiBot"]="Malicious crawler"
|
|
["DataForSeoBot"]="Data scraper"
|
|
["ZoominfoBot"]="Data harvester"
|
|
["MegaIndex"]="Aggressive crawler"
|
|
["SeznamBot"]="Aggressive crawler"
|
|
["Yeti"]="Naver crawler"
|
|
["serpstatbot"]="SEO crawler"
|
|
["LinkpadBot"]="Link checker"
|
|
["Nessus"]="Vulnerability scanner"
|
|
["Nikto"]="Security scanner"
|
|
["sqlmap"]="SQL injection tool"
|
|
["ZmEu"]="Scanner/exploit"
|
|
["masscan"]="Port scanner"
|
|
["nmap"]="Port scanner"
|
|
["wget"]="Command-line tool"
|
|
["curl"]="Command-line tool"
|
|
["python-requests"]="Script/automation"
|
|
)
|
|
|
|
#############################################################################
|
|
# 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
|
|
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
|
|
|
|
# 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" >> "$TEMP_DIR/parsed_logs.txt" 2>/dev/null
|
|
done
|
|
|
|
if [ ! -s "$TEMP_DIR/parsed_logs.txt" ]; then
|
|
print_alert "No log entries were parsed. Check log format or permissions."
|
|
return 1
|
|
fi
|
|
|
|
local line_count
|
|
line_count=$(wc -l < "$TEMP_DIR/parsed_logs.txt")
|
|
print_success "Logs parsed successfully ($line_count entries)"
|
|
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
|
|
}
|
|
}' "$TEMP_DIR/parsed_logs.txt" > "$TEMP_DIR/classified_bots.txt"
|
|
|
|
if [ ! -s "$TEMP_DIR/classified_bots.txt" ]; then
|
|
print_alert "Bot classification failed"
|
|
return 1
|
|
fi
|
|
|
|
local classified_count
|
|
classified_count=$(wc -l < "$TEMP_DIR/classified_bots.txt")
|
|
print_success "Bot classification complete ($classified_count entries)"
|
|
return 0
|
|
}
|
|
|
|
#############################################################################
|
|
# Threat Detection
|
|
#############################################################################
|
|
|
|
detect_threats() {
|
|
print_info "Detecting security threats..."
|
|
|
|
# Use a single AWK pass for multiple threat detections (more efficient)
|
|
awk -F'|' '
|
|
{
|
|
ip = $1
|
|
domain = $2
|
|
url = $3
|
|
status = $4
|
|
size = $5
|
|
ua = $6
|
|
method = $7
|
|
url_lower = tolower(url)
|
|
ua_lower = tolower(ua)
|
|
|
|
# SQL Injection patterns (enhanced)
|
|
if (match(url_lower, /union.*select|concat\(|benchmark\(|sleep\(|waitfor|cast\(|exec\(/) ||
|
|
match(url_lower, /information_schema|drop table|insert into|update.*set|delete from/) ||
|
|
match(url_lower, /%27|0x[0-9a-f]+|hex\(|unhex\(|load_file\(/)) {
|
|
print ip "|" domain "|" url "|" status "|sqli" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# XSS patterns
|
|
if (match(url_lower, /<script|javascript:|onerror=|onload=|<iframe|eval\(|alert\(/) ||
|
|
match(url_lower, /document\.cookie|document\.write|\.innerhtml/)) {
|
|
print ip "|" domain "|" url "|" status "|xss" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Path Traversal / LFI
|
|
if (match(url_lower, /\.\.\/|\.\.\\|etc\/passwd|etc\/shadow|boot\.ini|win\.ini/) ||
|
|
match(url_lower, /proc\/self|\/etc\/|c:\\|windows\/system32/)) {
|
|
print ip "|" domain "|" url "|" status "|path_traversal" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Shell upload / RCE attempts
|
|
if (match(url_lower, /cmd\.exe|\/bin\/bash|\/bin\/sh|phpinfo\(|system\(|exec\(|passthru\(/) ||
|
|
match(url_lower, /shell\.php|c99\.php|r57\.php|backdoor/) ||
|
|
(match(url_lower, /\.(php|jsp|asp|aspx)/) && method == "POST")) {
|
|
print ip "|" domain "|" url "|" status "|rce_upload" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Info Disclosure attempts
|
|
if (match(url_lower, /\.git\/|\.env|\.sql$|\.bak$|\.old$|config\.php|phpinfo|readme/) ||
|
|
match(url_lower, /web\.config|composer\.json|package\.json|\.htaccess|\.htpasswd/) ||
|
|
match(url_lower, /database\.sql|backup\.zip|dump\.sql/)) {
|
|
print ip "|" domain "|" url "|" status "|info_disclosure" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Login bruteforce
|
|
if (match(url_lower, /wp-login\.php|xmlrpc\.php/) && method == "POST") {
|
|
print ip "|" domain "|" url "|" status "|login_bruteforce" > "'"$TEMP_DIR"'/attack_vectors_raw.txt"
|
|
}
|
|
|
|
# Admin/sensitive endpoint probing
|
|
if (match(url_lower, /wp-admin|phpmyadmin|admin|administrator|login|wp-login|xmlrpc/) ||
|
|
match(url_lower, /\.env|\.git|\.sql|backup|config\./)) {
|
|
print ip "|" domain "|" url > "'"$TEMP_DIR"'/admin_probes_raw.txt"
|
|
}
|
|
|
|
# 404 scanning (reconnaissance)
|
|
if (status == "404" || status == "403") {
|
|
print ip "|" domain "|" url "|" status > "'"$TEMP_DIR"'/404_scans_raw.txt"
|
|
}
|
|
|
|
# Large data transfers (potential scraping)
|
|
if (size > 1000000) {
|
|
print ip "|" domain "|" url "|" size > "'"$TEMP_DIR"'/large_transfers_raw.txt"
|
|
}
|
|
|
|
# Suspicious user agents
|
|
if (match(ua_lower, /nikto|nmap|masscan|sqlmap|havij|acunetix|nessus|burp/) ||
|
|
match(ua_lower, /metasploit|<script|null|python-requests|go-http-client/)) {
|
|
print ip "|" ua > "'"$TEMP_DIR"'/suspicious_ua_raw.txt"
|
|
}
|
|
|
|
# Track response codes for intelligence
|
|
print status > "'"$TEMP_DIR"'/response_codes_raw.txt"
|
|
}
|
|
' "$TEMP_DIR/parsed_logs.txt"
|
|
|
|
# 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
|
|
awk -F'|' '{print $1"|"$3}' "$TEMP_DIR/parsed_logs.txt" | \
|
|
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
|
|
awk -F'|' '{print $1"|"$6}' "$TEMP_DIR/parsed_logs.txt" | \
|
|
sort | uniq > "$TEMP_DIR/ip_ua_pairs.txt"
|
|
|
|
# Pattern 3: Detect IP ranges (Class C networks) with suspicious activity
|
|
awk -F'|' '{print $1}' "$TEMP_DIR/parsed_logs.txt" | \
|
|
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
|
|
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
|
|
}
|
|
}' "$TEMP_DIR/parsed_logs.txt" | \
|
|
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
|
|
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
|
|
}
|
|
}' "$TEMP_DIR/classified_bots.txt" | 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" "$TEMP_DIR/parsed_logs.txt" | 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 < "$TEMP_DIR/parsed_logs.txt"
|
|
|
|
# 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))
|
|
|
|
# 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
|
|
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
|
|
}
|
|
}' "$TEMP_DIR/parsed_logs.txt" | 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
|
|
awk -F'|' '$9 != "unknown" {print $10}' "$TEMP_DIR/classified_bots.txt" | \
|
|
sort | uniq -c | sort -rn | head -5 > "$TEMP_DIR/top_bots.txt"
|
|
|
|
# Top 5 most-hit sites
|
|
awk -F'|' '{print $2}' "$TEMP_DIR/parsed_logs.txt" | \
|
|
sort | uniq -c | sort -rn | head -5 > "$TEMP_DIR/top_sites.txt"
|
|
|
|
# Top 5 most-hit URLs
|
|
awk -F'|' '{print $2"|"$3}' "$TEMP_DIR/parsed_logs.txt" | \
|
|
sort | uniq -c | sort -rn | head -5 > "$TEMP_DIR/top_urls.txt"
|
|
|
|
# Top 5 IP addresses by request count
|
|
awk -F'|' '{print $1}' "$TEMP_DIR/parsed_logs.txt" | \
|
|
sort | uniq -c | sort -rn | head -5 > "$TEMP_DIR/top_ips.txt"
|
|
|
|
# Traffic breakdown by bot type
|
|
awk -F'|' '{print $9}' "$TEMP_DIR/classified_bots.txt" | \
|
|
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"
|
|
grep "|$domain|" "$TEMP_DIR/classified_bots.txt" | \
|
|
awk -F'|' '{print $9}' | sort | uniq -c | sort -rn >> "$TEMP_DIR/domain_${domain}_stats.txt"
|
|
done < <(awk -F'|' '{print $2}' "$TEMP_DIR/parsed_logs.txt" | 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=$(wc -l < "$TEMP_DIR/parsed_logs.txt")
|
|
unique_ips=$(awk -F'|' '{print $1}' "$TEMP_DIR/parsed_logs.txt" | sort -u | wc -l)
|
|
unique_domains=$(awk -F'|' '{print $2}' "$TEMP_DIR/parsed_logs.txt" | sort -u | wc -l)
|
|
bot_requests=$(awk -F'|' '$9 != "unknown"' "$TEMP_DIR/classified_bots.txt" | wc -l)
|
|
|
|
# Count private/internal IPs (excluded from threat analysis)
|
|
private_ips=$(awk -F'|' '{print $1}' "$TEMP_DIR/parsed_logs.txt" | 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 grep -q "^$server_ip|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null; then
|
|
server_ip_hits=$((server_ip_hits + 1))
|
|
fi
|
|
done < "$TEMP_DIR/server_ips.txt"
|
|
fi
|
|
|
|
echo "Total Requests: $(printf "%'d" $total_requests)"
|
|
echo "Unique IPs: $(printf "%'d" $unique_ips)"
|
|
|
|
# Show breakdown if we have excluded IPs
|
|
if [ "$private_ips" -gt 0 ] || [ "$server_ip_hits" -gt 0 ]; then
|
|
excluded_total=$((private_ips + server_ip_hits))
|
|
echo " ├─ Excluded IPs: $(printf "%'d" $excluded_total)"
|
|
[ "$private_ips" -gt 0 ] && echo " │ ├─ Private/Internal: $private_ips"
|
|
[ "$server_ip_hits" -gt 0 ] && echo " │ └─ Server's own: $server_ip_hits"
|
|
echo " └─ External IPs: $(printf "%'d" $((unique_ips - excluded_total)))"
|
|
fi
|
|
|
|
echo "Domains Analyzed: $unique_domains"
|
|
echo "Bot Requests: $(printf "%'d" $bot_requests) ($(awk "BEGIN {printf \"%.1f\", ($bot_requests/$total_requests)*100}")%)"
|
|
|
|
# Show detected server IPs
|
|
if [ -f "$TEMP_DIR/server_ips.txt" ] && [ -s "$TEMP_DIR/server_ips.txt" ]; then
|
|
echo ""
|
|
echo " Server IPs Detected (excluded from threat analysis):"
|
|
while read -r server_ip; do
|
|
echo " • $server_ip"
|
|
done < "$TEMP_DIR/server_ips.txt"
|
|
fi
|
|
echo ""
|
|
|
|
# Traffic breakdown
|
|
echo "Traffic Breakdown:"
|
|
while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
type=$(echo "$line" | awk '{print $2}')
|
|
pct=$(awk "BEGIN {printf \"%.1f\", ($count/$total_requests)*100}")
|
|
|
|
case $type in
|
|
legit) echo " Legitimate Bots: $(printf "%'7d" $count) ($pct%)" ;;
|
|
ai) echo " AI Bots: $(printf "%'7d" $count) ($pct%)" ;;
|
|
monitor) echo " 📡 Monitoring/SEO: $(printf "%'7d" $count) ($pct%)" ;;
|
|
suspicious) echo " Suspicious Bots: $(printf "%'7d" $count) ($pct%)" ;;
|
|
unidentified_bot) echo " ❓ Unidentified Bots: $(printf "%'7d" $count) ($pct%)" ;;
|
|
unknown) echo " Regular Traffic: $(printf "%'7d" $count) ($pct%)" ;;
|
|
esac
|
|
done < "$TEMP_DIR/traffic_breakdown.txt"
|
|
|
|
# TIME-SERIES ANALYSIS
|
|
if [ -s "$TEMP_DIR/hourly_bot_traffic.txt" ]; then
|
|
echo ""
|
|
echo "Bot Traffic Timeline (hourly):"
|
|
max_bot_traffic=$(awk '{print $1}' "$TEMP_DIR/hourly_bot_traffic.txt" | sort -rn | head -1)
|
|
while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
hour=$(echo "$line" | awk '{print $2}')
|
|
# Create simple bar chart
|
|
bar_width=$((count * 10 / max_bot_traffic))
|
|
[ $bar_width -eq 0 ] && [ $count -gt 0 ] && bar_width=1
|
|
bar=$(printf '█%.0s' $(seq 1 $bar_width))
|
|
spaces=$(printf '░%.0s' $(seq 1 $((10 - bar_width))))
|
|
|
|
# Detect spikes (>2x average)
|
|
avg_traffic=$((total_requests / 24))
|
|
spike=""
|
|
[ $count -gt $((avg_traffic * 2)) ] && spike=" SPIKE"
|
|
|
|
# Strip leading zeros to avoid octal interpretation
|
|
hour_num=$((10#$hour))
|
|
next_hour=$((hour_num + 1))
|
|
printf " %02d:00-%02d:00: %s%s %'6d bot requests%s\n" "$hour_num" "$next_hour" "$bar" "$spaces" "$count" "$spike"
|
|
done < "$TEMP_DIR/hourly_bot_traffic.txt"
|
|
fi
|
|
|
|
# RESPONSE CODE INTELLIGENCE
|
|
if [ -s "$TEMP_DIR/response_codes.txt" ]; then
|
|
echo ""
|
|
echo "Response Code Analysis:"
|
|
while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
code=$(echo "$line" | awk '{print $2}')
|
|
pct=$(awk "BEGIN {printf \"%.1f\", ($count/$total_requests)*100}")
|
|
|
|
case $code in
|
|
200) echo " 200 (Success): $(printf "%'7d" $count) ($pct%) Bots are getting data" ;;
|
|
404) echo " 404 (Not Found): $(printf "%'7d" $count) ($pct%) Scanning for vulnerabilities" ;;
|
|
403) echo " 403 (Forbidden): $(printf "%'7d" $count) ($pct%) Blocked by existing rules" ;;
|
|
401) echo " 401 (Unauthorized):$(printf "%'7d" $count) ($pct%) Login attempts failing" ;;
|
|
500|502|503) echo " $code (Server Error):$(printf "%'7d" $count) ($pct%) Check if exploit triggered" ;;
|
|
301|302) echo " $code (Redirect): $(printf "%'7d" $count) ($pct%)" ;;
|
|
*) echo " $code: $(printf "%'7d" $count) ($pct%)" ;;
|
|
esac
|
|
done < "$TEMP_DIR/response_codes.txt" | head -7
|
|
fi
|
|
|
|
# FALSE POSITIVE WARNINGS
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ]; then
|
|
echo ""
|
|
echo "Whitelist Recommendations (Legitimate Services):"
|
|
while read -r line; do
|
|
ip=$(echo "$line" | cut -d'|' -f1)
|
|
service=$(echo "$line" | cut -d'|' -f2)
|
|
domain=$(echo "$line" | cut -d'|' -f4)
|
|
req_count=$(grep -c "^$ip|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null || echo 0)
|
|
echo " $ip - $req_count requests - Identified as: $service"
|
|
echo " → Domain: $domain"
|
|
echo " → Action: VERIFY OWNERSHIP then whitelist"
|
|
done < "$TEMP_DIR/false_positives.txt" | head -6
|
|
fi
|
|
|
|
# TOP 5 THREATS
|
|
print_header "TOP 5 THREATS (with recommended actions)"
|
|
|
|
echo "1. Highest Risk IPs (by threat score):"
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
counter=1
|
|
while read -r line && [ $counter -le 10 ]; do
|
|
score=$(echo "$line" | cut -d'|' -f1)
|
|
ip=$(echo "$line" | cut -d'|' -f2)
|
|
count=$(echo "$line" | cut -d'|' -f3)
|
|
|
|
# Determine threat level and action based on score
|
|
if [ "$score" -ge 80 ]; then
|
|
threat_level="CRITICAL"
|
|
threat_icon=""
|
|
action="BLOCK IMMEDIATELY + INVESTIGATE"
|
|
echo -e " ${RED}[$counter] $ip - RISK: $score/100 $threat_icon $threat_level${NC}"
|
|
elif [ "$score" -ge 60 ]; then
|
|
threat_level="HIGH"
|
|
threat_icon=""
|
|
action="BLOCK or AGGRESSIVE RATE LIMIT"
|
|
echo -e " ${YELLOW}[$counter] $ip - RISK: $score/100 $threat_icon $threat_level${NC}"
|
|
elif [ "$score" -ge 40 ]; then
|
|
threat_level="MODERATE"
|
|
threat_icon=""
|
|
action="RATE LIMIT RECOMMENDED"
|
|
echo " [$counter] $ip - RISK: $score/100 $threat_icon $threat_level"
|
|
else
|
|
threat_level="LOW"
|
|
threat_icon=""
|
|
action="MONITOR"
|
|
echo " [$counter] $ip - RISK: $score/100 $threat_icon $threat_level"
|
|
fi
|
|
|
|
echo " $count requests - Action: $action"
|
|
|
|
# Show which attack vectors this IP used
|
|
attack_types=""
|
|
grep -q "$ip" "$TEMP_DIR/sqli_attempts.txt" 2>/dev/null && attack_types="${attack_types}SQL-Injection "
|
|
grep -q "$ip" "$TEMP_DIR/xss_attempts.txt" 2>/dev/null && attack_types="${attack_types}XSS "
|
|
grep -q "$ip" "$TEMP_DIR/path_traversal_attempts.txt" 2>/dev/null && attack_types="${attack_types}Path-Traversal "
|
|
grep -q "$ip" "$TEMP_DIR/rce_upload_attempts.txt" 2>/dev/null && attack_types="${attack_types}RCE/Upload "
|
|
grep -q "$ip" "$TEMP_DIR/login_bruteforce_attempts.txt" 2>/dev/null && attack_types="${attack_types}Login-Bruteforce "
|
|
grep -q "$ip" "$TEMP_DIR/suspicious_ua.txt" 2>/dev/null && attack_types="${attack_types}Scanner-UA "
|
|
grep -q "$ip" "$TEMP_DIR/rapid_fire_ips.txt" 2>/dev/null && attack_types="${attack_types}DDoS-Pattern "
|
|
|
|
[ -n "$attack_types" ] && echo " Attack vectors: $attack_types"
|
|
|
|
counter=$((counter + 1))
|
|
done < "$TEMP_DIR/threat_scores.txt"
|
|
else
|
|
echo " No significant threats detected "
|
|
fi
|
|
echo ""
|
|
|
|
echo "2. Top Aggressive Bots:"
|
|
counter=1
|
|
while read -r line && [ $counter -le 5 ]; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
bot=$(echo "$line" | awk '{$1=""; print $0}' | xargs)
|
|
|
|
action="Allow"
|
|
if echo "$bot" | grep -qiE "ahrefs|semrush|dotbot|blex|megaindex"; then
|
|
action="Consider blocking (aggressive)"
|
|
fi
|
|
|
|
echo " [$counter] $bot - $count requests - Action: $action"
|
|
counter=$((counter + 1))
|
|
done < "$TEMP_DIR/top_bots.txt"
|
|
echo ""
|
|
|
|
echo "3. Admin Endpoint Probing:"
|
|
if [ -s "$TEMP_DIR/admin_probes.txt" ]; then
|
|
head -3 "$TEMP_DIR/admin_probes.txt" | while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
domain=$(echo "$line" | awk '{print $2}' | cut -d'|' -f2)
|
|
url=$(echo "$line" | awk '{print $2}' | cut -d'|' -f3)
|
|
printf " %s attempts - IP: %s - %s%s\n" "$count" "$ip" "$domain" "$url"
|
|
done
|
|
echo " Action: Verify legitimate admin access or block"
|
|
else
|
|
echo " None detected "
|
|
fi
|
|
echo ""
|
|
|
|
echo "4. 404 Scanners (Reconnaissance):"
|
|
if [ -s "$TEMP_DIR/404_scans.txt" ]; then
|
|
head -3 "$TEMP_DIR/404_scans.txt" | awk '$1 > 10 {
|
|
count = $1
|
|
$1 = ""
|
|
gsub(/^[[:space:]]+\|?/, "")
|
|
split($0, parts, "|")
|
|
printf " %s failed requests - IP: %s - %s%s\n", count, parts[1], parts[2], parts[3]
|
|
}'
|
|
else
|
|
echo " None detected "
|
|
fi
|
|
echo ""
|
|
|
|
echo "5. Large Data Transfers:"
|
|
if [ -s "$TEMP_DIR/large_transfers.txt" ]; then
|
|
# Calculate total bot bandwidth
|
|
total_bot_bandwidth=0
|
|
if [ -f "$TEMP_DIR/classified_bots.txt" ]; then
|
|
total_bot_bandwidth=$(awk -F'|' '$9 != "unknown" && $5 ~ /^[0-9]+$/ {sum += $5} END {print sum}' "$TEMP_DIR/classified_bots.txt")
|
|
fi
|
|
|
|
if [ -n "$total_bot_bandwidth" ] && [ "$total_bot_bandwidth" -gt 0 ]; then
|
|
bot_bandwidth_mb=$(awk "BEGIN {printf \"%.0f\", $total_bot_bandwidth/1048576}")
|
|
bot_bandwidth_gb=$(awk "BEGIN {printf \"%.2f\", $total_bot_bandwidth/1073741824}")
|
|
# Estimate cost at $0.09/GB (typical CDN pricing)
|
|
estimated_cost=$(awk "BEGIN {printf \"%.2f\", ($total_bot_bandwidth/1073741824) * 0.09}")
|
|
|
|
total_bandwidth=$(awk -F'|' '$5 ~ /^[0-9]+$/ {sum += $5} END {print sum}' "$TEMP_DIR/parsed_logs.txt")
|
|
bot_pct=$(awk "BEGIN {printf \"%.1f\", ($total_bot_bandwidth/$total_bandwidth)*100}")
|
|
|
|
echo ""
|
|
echo " 💰 Bandwidth Impact:"
|
|
echo " Total bot bandwidth: ${bot_bandwidth_mb} MB (${bot_bandwidth_gb} GB) - ${bot_pct}% of total"
|
|
echo " Estimated cost: \$$estimated_cost (at \$0.09/GB CDN pricing)"
|
|
fi
|
|
echo ""
|
|
echo " Top bandwidth consumers:"
|
|
|
|
head -3 "$TEMP_DIR/large_transfers.txt" | while read -r line; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
ip=$(echo "$line" | awk '{print $2}' | cut -d'|' -f1)
|
|
domain=$(echo "$line" | awk '{print $2}' | cut -d'|' -f2)
|
|
url=$(echo "$line" | awk '{print $2}' | cut -d'|' -f3)
|
|
size=$(echo "$line" | awk '{print $2}' | cut -d'|' -f4)
|
|
size_mb=$(awk "BEGIN {printf \"%.1f\", $size/1048576}")
|
|
total_ip_mb=$(awk "BEGIN {printf \"%.0f\", $size * $count / 1048576}")
|
|
printf " %s transfers from %s - %.1f MB avg (%s MB total) - %s%s\n" "$count" "$ip" "$size_mb" "$total_ip_mb" "$domain" "$url"
|
|
done
|
|
echo " Action: Verify if scraping, consider serving WebP/optimized images"
|
|
else
|
|
echo " None detected "
|
|
fi
|
|
|
|
# TOP 5 TARGETED SITES
|
|
print_header "TOP 5 TARGETED SITES (with risk breakdown)"
|
|
|
|
counter=1
|
|
while read -r line && [ $counter -le 5 ]; do
|
|
count=$(echo "$line" | awk '{print $1}')
|
|
domain=$(echo "$line" | awk '{print $2}')
|
|
|
|
echo "[$counter] $domain - $count requests"
|
|
|
|
# Show traffic breakdown for this domain
|
|
if [ -f "$TEMP_DIR/domain_${domain}_stats.txt" ]; then
|
|
tail -n +2 "$TEMP_DIR/domain_${domain}_stats.txt" | while read -r stat_line; do
|
|
stat_count=$(echo "$stat_line" | awk '{print $1}')
|
|
stat_type=$(echo "$stat_line" | awk '{print $2}')
|
|
pct=$(awk "BEGIN {printf \"%.1f\", ($stat_count/$count)*100}")
|
|
|
|
case $stat_type in
|
|
suspicious) echo -e " ${YELLOW}Suspicious: $stat_count ($pct%)${NC}" ;;
|
|
ai) echo " AI Bots: $stat_count ($pct%)" ;;
|
|
legit) echo " Legit Bots: $stat_count ($pct%)" ;;
|
|
unknown) echo " Regular: $stat_count ($pct%)" ;;
|
|
*) echo " $stat_type: $stat_count ($pct%)" ;;
|
|
esac
|
|
done
|
|
fi
|
|
echo ""
|
|
|
|
counter=$((counter + 1))
|
|
done < "$TEMP_DIR/top_sites.txt"
|
|
|
|
# BLOCKLIST
|
|
print_header "COPY-PASTE READY BLOCKLIST (Prioritized by Threat Score)"
|
|
|
|
echo "# Apache .htaccess format:"
|
|
echo "# Add to .htaccess in document root"
|
|
echo "# IPs sorted by risk score (highest first)"
|
|
echo ""
|
|
|
|
# Use threat scores to prioritize blocklist (exclude false positives and excluded IPs)
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
# Get IPs with score >= 60 (HIGH and CRITICAL)
|
|
awk -F'|' '$1 >= 60 {print $2 "|" $1}' "$TEMP_DIR/threat_scores.txt" | head -30 | while read -r entry; do
|
|
ip=$(echo "$entry" | cut -d'|' -f1)
|
|
score=$(echo "$entry" | cut -d'|' -f2)
|
|
|
|
# Skip excluded IPs (private, localhost, server's own)
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
|
|
# Skip if in false positives
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ] && grep -q "^$ip|" "$TEMP_DIR/false_positives.txt" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
|
|
echo "Deny from $ip # Risk score: $score/100"
|
|
done
|
|
else
|
|
# Fallback to old method
|
|
{
|
|
[ -s "$TEMP_DIR/suspicious_ua.txt" ] && awk '{print $2}' "$TEMP_DIR/suspicious_ua.txt" | cut -d'|' -f1
|
|
[ -s "$TEMP_DIR/attack_vectors_raw.txt" ] && awk -F'|' '{print $1}' "$TEMP_DIR/attack_vectors_raw.txt" | sort -u
|
|
[ -s "$TEMP_DIR/rapid_fire_ips.txt" ] && head -10 "$TEMP_DIR/rapid_fire_ips.txt" | awk '{print $2}'
|
|
} | sort -u | head -30 | while read -r ip; do
|
|
echo "Deny from $ip"
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
echo "# User-Agent blocking (add to .htaccess):"
|
|
echo "SetEnvIfNoCase User-Agent \"nikto|nmap|masscan|sqlmap|havij\" bad_bot"
|
|
echo "SetEnvIfNoCase User-Agent \"acunetix|nessus|burp|metasploit\" bad_bot"
|
|
echo ""
|
|
echo "# Optional: Block aggressive SEO bots"
|
|
echo "# SetEnvIfNoCase User-Agent \"AhrefsBot|SemrushBot|MJ12bot|DotBot\" bad_bot"
|
|
echo ""
|
|
echo "Order Allow,Deny"
|
|
echo "Allow from all"
|
|
echo "Deny from env=bad_bot"
|
|
|
|
echo ""
|
|
echo "# CSF/iptables format:"
|
|
echo "# Run these commands as root:"
|
|
echo ""
|
|
|
|
# Same prioritized list for CSF
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
awk -F'|' '$1 >= 60 {print $2 "|" $1}' "$TEMP_DIR/threat_scores.txt" | head -30 | while read -r entry; do
|
|
ip=$(echo "$entry" | cut -d'|' -f1)
|
|
score=$(echo "$entry" | cut -d'|' -f2)
|
|
|
|
# Skip excluded IPs (private, localhost, server's own)
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
|
|
# Skip if in false positives
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ] && grep -q "^$ip|" "$TEMP_DIR/false_positives.txt" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
|
|
echo "csf -d $ip \"Threat score: $score/100\""
|
|
done
|
|
else
|
|
# Fallback
|
|
{
|
|
[ -s "$TEMP_DIR/suspicious_ua.txt" ] && awk '{print $2}' "$TEMP_DIR/suspicious_ua.txt" | cut -d'|' -f1
|
|
[ -s "$TEMP_DIR/attack_vectors_raw.txt" ] && awk -F'|' '{print $1}' "$TEMP_DIR/attack_vectors_raw.txt" | sort -u
|
|
[ -s "$TEMP_DIR/rapid_fire_ips.txt" ] && head -10 "$TEMP_DIR/rapid_fire_ips.txt" | awk '{print $2}'
|
|
} | sort -u | head -30 | while read -r ip; do
|
|
echo "csf -d $ip \"Bot/Scanner threat\""
|
|
done
|
|
fi
|
|
|
|
# SUMMARY
|
|
print_header "📋 SUMMARY & RECOMMENDATIONS"
|
|
|
|
threat_score=0
|
|
|
|
# Calculate threat score from attack vectors
|
|
[ -s "$TEMP_DIR/sqli_attempts.txt" ] && threat_score=$((threat_score + 15))
|
|
[ -s "$TEMP_DIR/xss_attempts.txt" ] && threat_score=$((threat_score + 12))
|
|
[ -s "$TEMP_DIR/path_traversal_attempts.txt" ] && threat_score=$((threat_score + 15))
|
|
[ -s "$TEMP_DIR/rce_upload_attempts.txt" ] && threat_score=$((threat_score + 20))
|
|
[ -s "$TEMP_DIR/login_bruteforce_attempts.txt" ] && threat_score=$((threat_score + 10))
|
|
[ -s "$TEMP_DIR/suspicious_ua.txt" ] && threat_score=$((threat_score + 8))
|
|
[ -s "$TEMP_DIR/rapid_fire_ips.txt" ] && threat_score=$((threat_score + 5))
|
|
[ $(wc -l < "$TEMP_DIR/admin_probes.txt" 2>/dev/null || echo 0) -gt 10 ] && threat_score=$((threat_score + 3))
|
|
|
|
# Count high-risk IPs
|
|
high_risk_count=0
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
high_risk_count=$(awk -F'|' '$1 >= 60' "$TEMP_DIR/threat_scores.txt" | wc -l)
|
|
fi
|
|
|
|
if [ $threat_score -ge 25 ] || [ $high_risk_count -ge 5 ]; then
|
|
print_alert "THREAT LEVEL: CRITICAL - Immediate action required"
|
|
echo " Summary: Multiple attack vectors detected from $high_risk_count high-risk IPs"
|
|
echo ""
|
|
echo " Immediate Actions:"
|
|
echo " 1. ⚡ Apply the blocklist above IMMEDIATELY (prioritized by threat score)"
|
|
echo " 2. Review admin access logs for successful breaches"
|
|
echo " 3. 🛡 Enable ModSecurity WAF or Cloudflare if not already active"
|
|
echo " 4. 🔄 Update all CMS platforms and plugins urgently"
|
|
echo " 5. 🔐 Force password reset for admin accounts if login attempts detected"
|
|
echo " 6. Re-run this analysis in 1 hour to verify blocks are working"
|
|
elif [ $threat_score -ge 12 ] || [ $high_risk_count -ge 2 ]; then
|
|
print_warning "THREAT LEVEL: HIGH - Action recommended within 24 hours"
|
|
echo " Summary: Significant threat activity from $high_risk_count high-risk IPs"
|
|
echo ""
|
|
echo " Recommended Actions:"
|
|
echo " 1. Review and apply the blocklist above (focus on CRITICAL/HIGH scores)"
|
|
echo " 2. Enable rate limiting for admin endpoints"
|
|
echo " 3. Monitor logs closely for the next 24-48 hours"
|
|
echo " 4. Consider implementing fail2ban or similar IDS"
|
|
echo " 5. Review and update security plugins/modules"
|
|
elif [ $threat_score -ge 5 ]; then
|
|
print_warning "THREAT LEVEL: MODERATE - Routine security maintenance"
|
|
echo " Summary: Normal bot activity with some suspicious patterns"
|
|
echo ""
|
|
echo " Recommended Actions:"
|
|
echo " 1. Review suspicious IPs in the report"
|
|
echo " 2. Consider rate limiting aggressive bots"
|
|
echo " 3. Continue routine log monitoring"
|
|
echo " 4. Block aggressive SEO bots if impacting performance"
|
|
else
|
|
print_success "THREAT LEVEL: ✅ LOW - Normal operation"
|
|
echo " Summary: Minimal threat activity detected"
|
|
echo ""
|
|
echo " Recommended Actions:"
|
|
echo " 1. Continue routine log monitoring"
|
|
echo " 2. Review false positive warnings to whitelist legitimate services"
|
|
echo " 3. Consider blocking aggressive SEO bots if bandwidth is a concern"
|
|
fi
|
|
|
|
echo ""
|
|
echo "==============================================================="
|
|
echo "Report saved to: $OUTPUT_FILE"
|
|
echo "==============================================================="
|
|
}
|
|
|
|
################################################################################
|
|
# BASELINE HEALTH CHECK - Test domains before making changes
|
|
################################################################################
|
|
|
|
baseline_health_check() {
|
|
print_info "Performing baseline health check on all domains..."
|
|
echo ""
|
|
|
|
# Create baseline health file
|
|
> "$TEMP_DIR/baseline_health.txt"
|
|
> "$TEMP_DIR/domain_list.txt"
|
|
|
|
# Get all domains from logs (we'll test these)
|
|
find "$LOG_DIR" -type f -name "*.com" -o -name "*.net" -o -name "*.org" 2>/dev/null | \
|
|
xargs -r basename -a 2>/dev/null | \
|
|
sort -u > "$TEMP_DIR/domain_list.txt"
|
|
|
|
# If no domains found from log files, try reference database
|
|
if [ ! -s "$TEMP_DIR/domain_list.txt" ]; then
|
|
if [ -s "$TOOLKIT_BASE_DIR/.sysref" ]; then
|
|
grep "^DOMAIN|" "$TOOLKIT_BASE_DIR/.sysref" 2>/dev/null | \
|
|
cut -d'|' -f2 | sort -u > "$TEMP_DIR/domain_list.txt"
|
|
fi
|
|
fi
|
|
|
|
local domain_count=$(wc -l < "$TEMP_DIR/domain_list.txt" 2>/dev/null || echo "0")
|
|
|
|
if [ "$domain_count" -eq 0 ]; then
|
|
print_warning "No domains found for health check"
|
|
return 0
|
|
fi
|
|
|
|
print_info "Testing $domain_count domain(s)..."
|
|
echo ""
|
|
|
|
local tested=0
|
|
local working=0
|
|
local broken=0
|
|
|
|
while read -r domain; do
|
|
[ -z "$domain" ] && continue
|
|
|
|
tested=$((tested + 1))
|
|
|
|
# Test HTTP and HTTPS
|
|
local http_status=""
|
|
local https_status=""
|
|
local result=""
|
|
|
|
# Try HTTP first (timeout 5 seconds)
|
|
http_status=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "http://$domain" 2>/dev/null || echo "timeout")
|
|
|
|
# Try HTTPS (timeout 5 seconds)
|
|
https_status=$(curl -s -o /dev/null -w "%{http_code}" -m 5 -k "https://$domain" 2>/dev/null || echo "timeout")
|
|
|
|
# Determine overall status
|
|
if [ "$http_status" = "200" ] || [ "$https_status" = "200" ]; then
|
|
result="200_OK"
|
|
working=$((working + 1))
|
|
echo -e " ${GREEN}${NC} $domain - HTTP:$http_status HTTPS:$https_status"
|
|
elif [ "$http_status" = "301" ] || [ "$http_status" = "302" ] || [ "$https_status" = "301" ] || [ "$https_status" = "302" ]; then
|
|
result="REDIRECT"
|
|
working=$((working + 1))
|
|
echo -e " ${YELLOW}→${NC} $domain - Redirect (HTTP:$http_status HTTPS:$https_status)"
|
|
elif [ "$http_status" = "403" ] || [ "$https_status" = "403" ]; then
|
|
result="403_FORBIDDEN"
|
|
broken=$((broken + 1))
|
|
echo -e " ${RED}${NC} $domain - Forbidden (HTTP:$http_status HTTPS:$https_status)"
|
|
elif [ "$http_status" = "timeout" ] && [ "$https_status" = "timeout" ]; then
|
|
result="TIMEOUT"
|
|
broken=$((broken + 1))
|
|
echo -e " ${RED}⏱${NC} $domain - Timeout (unreachable)"
|
|
else
|
|
result="ERROR_${http_status}_${https_status}"
|
|
broken=$((broken + 1))
|
|
echo -e " ${YELLOW}?${NC} $domain - HTTP:$http_status HTTPS:$https_status"
|
|
fi
|
|
|
|
# Store baseline: domain|http_status|https_status|result
|
|
echo "$domain|$http_status|$https_status|$result" >> "$TEMP_DIR/baseline_health.txt"
|
|
|
|
done < "$TEMP_DIR/domain_list.txt"
|
|
|
|
echo ""
|
|
print_success "Baseline health check complete: $working working, $broken with issues"
|
|
echo ""
|
|
}
|
|
|
|
verify_domains_still_working() {
|
|
print_info "Verifying domains still work after changes..."
|
|
echo ""
|
|
|
|
if [ ! -s "$TEMP_DIR/baseline_health.txt" ]; then
|
|
print_warning "No baseline health data available"
|
|
return 0
|
|
fi
|
|
|
|
local changes_detected=0
|
|
local now_broken=0
|
|
|
|
while IFS='|' read -r domain baseline_http baseline_https baseline_result; do
|
|
[ -z "$domain" ] && continue
|
|
|
|
# Re-test domain
|
|
local http_status=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "http://$domain" 2>/dev/null || echo "timeout")
|
|
local https_status=$(curl -s -o /dev/null -w "%{http_code}" -m 5 -k "https://$domain" 2>/dev/null || echo "timeout")
|
|
|
|
# Determine new status
|
|
local new_result=""
|
|
if [ "$http_status" = "200" ] || [ "$https_status" = "200" ]; then
|
|
new_result="200_OK"
|
|
elif [ "$http_status" = "301" ] || [ "$http_status" = "302" ] || [ "$https_status" = "301" ] || [ "$https_status" = "302" ]; then
|
|
new_result="REDIRECT"
|
|
elif [ "$http_status" = "403" ] || [ "$https_status" = "403" ]; then
|
|
new_result="403_FORBIDDEN"
|
|
elif [ "$http_status" = "timeout" ] && [ "$https_status" = "timeout" ]; then
|
|
new_result="TIMEOUT"
|
|
else
|
|
new_result="ERROR"
|
|
fi
|
|
|
|
# Compare to baseline
|
|
if [ "$baseline_result" != "$new_result" ]; then
|
|
changes_detected=$((changes_detected + 1))
|
|
|
|
# Check if it got worse
|
|
if [ "$baseline_result" = "200_OK" ] || [ "$baseline_result" = "REDIRECT" ]; then
|
|
if [ "$new_result" = "403_FORBIDDEN" ] || [ "$new_result" = "TIMEOUT" ] || [ "$new_result" = "ERROR" ]; then
|
|
now_broken=$((now_broken + 1))
|
|
echo -e " ${RED}BROKEN:${NC} $domain"
|
|
echo -e " Before: $baseline_result (HTTP:$baseline_http HTTPS:$baseline_https)"
|
|
echo -e " After: $new_result (HTTP:$http_status HTTPS:$https_status)"
|
|
echo -e " ${RED}WARNING: This domain stopped working after your changes!${NC}"
|
|
echo ""
|
|
fi
|
|
# Check if it got better
|
|
elif [ "$baseline_result" = "403_FORBIDDEN" ] || [ "$baseline_result" = "TIMEOUT" ]; then
|
|
if [ "$new_result" = "200_OK" ] || [ "$new_result" = "REDIRECT" ]; then
|
|
echo -e " ${GREEN}✅ FIXED:${NC} $domain"
|
|
echo -e " Before: $baseline_result"
|
|
echo -e " After: $new_result"
|
|
echo ""
|
|
fi
|
|
fi
|
|
fi
|
|
done < "$TEMP_DIR/baseline_health.txt"
|
|
|
|
if [ $now_broken -gt 0 ]; then
|
|
echo ""
|
|
print_alert "WARNING: $now_broken domain(s) stopped working after your changes!"
|
|
echo ""
|
|
echo "Recommended actions:"
|
|
echo " 1. Review the firewall rules you just applied"
|
|
echo " 2. Check CSF temporary blocks: csf -t"
|
|
echo " 3. Check CSF deny list: csf -g"
|
|
echo " 4. Consider reverting changes if issues persist"
|
|
echo ""
|
|
elif [ $changes_detected -eq 0 ]; then
|
|
print_success "All domains still working normally"
|
|
else
|
|
print_success "Some status changes detected but no domains broken"
|
|
fi
|
|
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
}
|
|
|
|
#############################################################################
|
|
# Main Execution
|
|
#############################################################################
|
|
|
|
main() {
|
|
echo ""
|
|
print_header "Starting Apache/cPanel Bot Analysis"
|
|
|
|
# Check if log directory exists
|
|
if [ ! -d "$LOG_DIR" ]; then
|
|
print_alert "Error: Log directory not found: $LOG_DIR"
|
|
echo "Please specify the correct log directory with -l option"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if logs exist
|
|
local find_opts=()
|
|
if [ -n "$HOURS_BACK" ]; then
|
|
local minutes=$((HOURS_BACK * 60))
|
|
find_opts+=(-mmin -"$minutes")
|
|
elif [ -n "$DAYS_BACK" ]; then
|
|
find_opts+=(-mtime -"$DAYS_BACK")
|
|
fi
|
|
|
|
# User filtering
|
|
if [ -n "$FILTER_USER" ]; then
|
|
print_info "Filtering logs for user: $FILTER_USER"
|
|
user_domains=$(get_user_domains "$FILTER_USER")
|
|
if [ -z "$user_domains" ]; then
|
|
print_error "No domains found for user: $FILTER_USER"
|
|
exit 1
|
|
fi
|
|
print_info "User has $(echo "$user_domains" | wc -l) domain(s)"
|
|
fi
|
|
|
|
log_count=$(find "$LOG_DIR" -type f ! -name "*-bytes_log" ! -name "*.offset" ! -name "*error_log" "${find_opts[@]}" 2>/dev/null | wc -l)
|
|
if [ "$log_count" -eq 0 ]; then
|
|
print_alert "Error: No log files found in $LOG_DIR"
|
|
if [ -n "$HOURS_BACK" ]; then
|
|
echo "No logs found from the last $HOURS_BACK hours"
|
|
elif [ -n "$DAYS_BACK" ]; then
|
|
echo "No logs found from the last $DAYS_BACK days"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
print_info "Found $log_count log files to analyze"
|
|
if [ -n "$HOURS_BACK" ]; then
|
|
print_info "Analyzing logs from the last $HOURS_BACK hours"
|
|
elif [ -n "$DAYS_BACK" ]; then
|
|
print_info "Analyzing logs from the last $DAYS_BACK days"
|
|
fi
|
|
|
|
# Baseline health check - test all domains before analysis
|
|
baseline_health_check
|
|
|
|
# Execute analysis pipeline with error handling
|
|
parse_logs || {
|
|
print_alert "Log parsing failed"
|
|
exit 1
|
|
}
|
|
|
|
classify_bots || {
|
|
print_alert "Bot classification failed"
|
|
exit 1
|
|
}
|
|
|
|
detect_server_ips
|
|
detect_threats
|
|
detect_botnets
|
|
analyze_time_series
|
|
calculate_threat_scores
|
|
detect_false_positives
|
|
generate_statistics
|
|
generate_report
|
|
|
|
print_success "Analysis complete!"
|
|
echo ""
|
|
echo "Report location: $OUTPUT_FILE"
|
|
|
|
# Analyze threat patterns and generate recommendations
|
|
analyze_domain_threats
|
|
analyze_geographic_threats
|
|
generate_recommendations
|
|
|
|
# Ask user what to do next
|
|
show_post_analysis_menu
|
|
}
|
|
|
|
################################################################################
|
|
# DOMAIN-LEVEL THREAT ANALYSIS
|
|
################################################################################
|
|
|
|
analyze_domain_threats() {
|
|
print_info "Analyzing per-domain threat patterns..."
|
|
|
|
# Create domain threat analysis file
|
|
> "$TEMP_DIR/domain_threats.txt"
|
|
> "$TEMP_DIR/domain_high_risk_ips.txt"
|
|
|
|
# Get all unique domains from parsed logs
|
|
awk -F'|' '{print $2}' "$TEMP_DIR/parsed_logs.txt" 2>/dev/null | sort -u > "$TEMP_DIR/all_domains.txt"
|
|
|
|
# For each domain, calculate threat metrics
|
|
while read -r domain; do
|
|
[ -z "$domain" ] && continue
|
|
|
|
# Total requests to this domain
|
|
local total_requests=$(grep -c "^[^|]*|$domain|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null || echo "0")
|
|
|
|
# Bot requests to this domain
|
|
local bot_requests=$(grep "|$domain|" "$TEMP_DIR/classified_bots.txt" 2>/dev/null | wc -l || echo "0")
|
|
|
|
# High-risk IPs hitting this domain (score >= 70)
|
|
local high_risk_count=0
|
|
local high_risk_ips=""
|
|
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
while read -r score_line; do
|
|
local score=$(echo "$score_line" | cut -d'|' -f1)
|
|
local ip=$(echo "$score_line" | cut -d'|' -f2)
|
|
|
|
if [ "$score" -ge 70 ]; then
|
|
# Check if this IP hit this domain
|
|
if grep -q "^$ip|$domain|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null; then
|
|
local ip_requests=$(grep -c "^$ip|$domain|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null || echo "0")
|
|
high_risk_count=$((high_risk_count + 1))
|
|
high_risk_ips="${high_risk_ips}${ip}:${score}:${ip_requests} "
|
|
fi
|
|
fi
|
|
done < "$TEMP_DIR/threat_scores.txt"
|
|
fi
|
|
|
|
# Attack attempts targeting this domain
|
|
local attack_attempts=0
|
|
if [ -s "$TEMP_DIR/attack_vectors_raw.txt" ]; then
|
|
attack_attempts=$(grep "|$domain|" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | wc -l || echo "0")
|
|
fi
|
|
|
|
# Calculate bot percentage
|
|
local bot_percentage=0
|
|
if [ "$total_requests" -gt 0 ]; then
|
|
bot_percentage=$(awk "BEGIN {printf \"%.1f\", ($bot_requests / $total_requests) * 100}")
|
|
fi
|
|
|
|
# Store domain threat data
|
|
# Format: domain|total_requests|bot_requests|bot_percentage|high_risk_ip_count|attack_attempts|high_risk_ips_detail
|
|
echo "$domain|$total_requests|$bot_requests|$bot_percentage|$high_risk_count|$attack_attempts|$high_risk_ips" >> "$TEMP_DIR/domain_threats.txt"
|
|
|
|
# Track which high-risk IPs hit which domains
|
|
if [ $high_risk_count -gt 0 ]; then
|
|
echo "$domain|$high_risk_count|$high_risk_ips" >> "$TEMP_DIR/domain_high_risk_ips.txt"
|
|
fi
|
|
|
|
done < "$TEMP_DIR/all_domains.txt"
|
|
|
|
# Sort by high-risk IP count (descending)
|
|
sort -t'|' -k5 -rn "$TEMP_DIR/domain_threats.txt" > "$TEMP_DIR/domain_threats_sorted.txt"
|
|
|
|
print_success "Domain threat analysis complete"
|
|
}
|
|
|
|
################################################################################
|
|
# GEOGRAPHIC ANALYSIS (Country-based threat tracking)
|
|
################################################################################
|
|
|
|
analyze_geographic_threats() {
|
|
print_info "Analyzing geographic distribution of threats..."
|
|
|
|
# Create geographic analysis file
|
|
> "$TEMP_DIR/geo_analysis.txt"
|
|
> "$TEMP_DIR/geo_needs_maxmind.txt"
|
|
|
|
# Check if GeoIP/MaxMind is available
|
|
local has_geoip=false
|
|
if command -v geoiplookup >/dev/null 2>&1 || command -v mmdbinspect >/dev/null 2>&1; then
|
|
has_geoip=true
|
|
fi
|
|
|
|
if [ "$has_geoip" = false ]; then
|
|
# Can't do full geographic analysis without GeoIP
|
|
# But we can still detect if traffic looks suspicious by analyzing IP ranges
|
|
|
|
# Count high-risk IPs by /24 network
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
awk -F'|' '$1 >= 70 {
|
|
split($2, ip, ".")
|
|
network = ip[1]"."ip[2]"."ip[3]".0/24"
|
|
print network
|
|
}' "$TEMP_DIR/threat_scores.txt" | sort | uniq -c | sort -rn > "$TEMP_DIR/high_risk_networks.txt"
|
|
|
|
local network_count=$(wc -l < "$TEMP_DIR/high_risk_networks.txt" 2>/dev/null || echo "0")
|
|
local total_high_risk=$(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" | wc -l)
|
|
|
|
if [ "$network_count" -gt 10 ] || [ "$total_high_risk" -gt 50 ]; then
|
|
# Multiple networks or many IPs suggests distributed attack
|
|
# Recommend MaxMind for geographic blocking
|
|
echo "DISTRIBUTED|$network_count networks|$total_high_risk IPs|MaxMind recommended" > "$TEMP_DIR/geo_needs_maxmind.txt"
|
|
fi
|
|
fi
|
|
|
|
print_info "Geographic analysis limited (MaxMind GeoIP2 not installed)"
|
|
else
|
|
# Full geographic analysis with GeoIP
|
|
print_info "Performing full geographic analysis with GeoIP..."
|
|
|
|
# TODO: Implement full GeoIP lookups when available
|
|
# This would lookup each high-risk IP and count by country
|
|
fi
|
|
|
|
print_success "Geographic analysis complete"
|
|
}
|
|
|
|
################################################################################
|
|
# RECOMMENDATION ENGINE
|
|
################################################################################
|
|
|
|
generate_recommendations() {
|
|
print_info "Generating intelligent recommendations..."
|
|
|
|
# Initialize recommendation file
|
|
> "$TEMP_DIR/recommendations.txt"
|
|
local rec_count=0
|
|
|
|
# Get total unique high-risk IPs
|
|
local total_high_risk_ips=0
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
total_high_risk_ips=$(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | wc -l || echo "0")
|
|
fi
|
|
|
|
# Get total domains affected
|
|
local total_domains=$(wc -l < "$TEMP_DIR/all_domains.txt" 2>/dev/null || echo "0")
|
|
local affected_domains=0
|
|
if [ -s "$TEMP_DIR/domain_high_risk_ips.txt" ]; then
|
|
affected_domains=$(wc -l < "$TEMP_DIR/domain_high_risk_ips.txt" || echo "0")
|
|
fi
|
|
|
|
# Determine attack scope: single domain vs server-wide
|
|
local attack_scope="unknown"
|
|
local primary_target=""
|
|
local primary_target_percentage=0
|
|
|
|
if [ $affected_domains -eq 1 ] && [ $total_domains -gt 1 ]; then
|
|
attack_scope="single_domain"
|
|
primary_target=$(head -1 "$TEMP_DIR/domain_high_risk_ips.txt" 2>/dev/null | cut -d'|' -f1)
|
|
# Calculate what % of high-risk IPs are targeting this domain
|
|
local domain_risk_count=$(head -1 "$TEMP_DIR/domain_high_risk_ips.txt" 2>/dev/null | cut -d'|' -f2)
|
|
if [ $total_high_risk_ips -gt 0 ]; then
|
|
primary_target_percentage=$(awk "BEGIN {printf \"%.0f\", ($domain_risk_count / $total_high_risk_ips) * 100}")
|
|
fi
|
|
elif [ $affected_domains -gt 1 ] && [ $total_domains -gt 1 ]; then
|
|
# Check if one domain is getting most of the traffic
|
|
local top_domain_count=$(head -1 "$TEMP_DIR/domain_threats_sorted.txt" 2>/dev/null | cut -d'|' -f5)
|
|
if [ "$top_domain_count" -gt 0 ] && [ $total_high_risk_ips -gt 0 ]; then
|
|
local top_percentage=$(awk "BEGIN {printf \"%.0f\", ($top_domain_count / $total_high_risk_ips) * 100}")
|
|
if [ "$top_percentage" -ge 75 ]; then
|
|
attack_scope="primary_target"
|
|
primary_target=$(head -1 "$TEMP_DIR/domain_threats_sorted.txt" 2>/dev/null | cut -d'|' -f1)
|
|
primary_target_percentage=$top_percentage
|
|
else
|
|
attack_scope="server_wide"
|
|
fi
|
|
else
|
|
attack_scope="server_wide"
|
|
fi
|
|
elif [ $affected_domains -eq $total_domains ] && [ $total_domains -gt 1 ]; then
|
|
attack_scope="server_wide"
|
|
elif [ $total_domains -eq 1 ]; then
|
|
attack_scope="single_server"
|
|
primary_target=$(head -1 "$TEMP_DIR/all_domains.txt" 2>/dev/null)
|
|
fi
|
|
|
|
# RECOMMENDATION #1: IP Blocking Strategy
|
|
if [ $total_high_risk_ips -gt 0 ]; then
|
|
rec_count=$((rec_count + 1))
|
|
if [ $total_high_risk_ips -le 10 ]; then
|
|
echo "REC|$rec_count|ip_block_temp|Block $total_high_risk_ips high-risk IPs for 1 hour|HIGH|CSF temporary block recommended for ${total_high_risk_ips} IPs with threat score >= 70" >> "$TEMP_DIR/recommendations.txt"
|
|
elif [ $total_high_risk_ips -le 50 ]; then
|
|
echo "REC|$rec_count|ip_block_temp|Block $total_high_risk_ips high-risk IPs for 24 hours|HIGH|Large number of threats detected - 24hr block recommended" >> "$TEMP_DIR/recommendations.txt"
|
|
else
|
|
echo "REC|$rec_count|ip_block_perm|Permanently block $total_high_risk_ips high-risk IPs|CRITICAL|Severe bot attack detected - permanent blocking recommended" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
fi
|
|
|
|
# RECOMMENDATION #2: Connection Limit (CSF CT_LIMIT)
|
|
# Only recommend if CSF is installed and CT_LIMIT is enabled
|
|
if command -v csf >/dev/null 2>&1 && [ -f /etc/csf/csf.conf ]; then
|
|
# Check if CT_LIMIT is enabled (not set to 0)
|
|
local current_ct_limit=$(grep "^CT_LIMIT" /etc/csf/csf.conf 2>/dev/null | grep -oP '"\K[0-9]+' || echo "0")
|
|
|
|
if [ "$current_ct_limit" -gt 0 ]; then
|
|
# Check concurrent connections from top IPs
|
|
local max_connections=0
|
|
if [ -s "$TEMP_DIR/rapid_fire_ips.txt" ]; then
|
|
max_connections=$(head -1 "$TEMP_DIR/rapid_fire_ips.txt" 2>/dev/null | awk '{print $1}' || echo "0")
|
|
fi
|
|
|
|
if [ "$max_connections" -gt 100 ] && [ "$max_connections" -lt "$current_ct_limit" ]; then
|
|
rec_count=$((rec_count + 1))
|
|
local recommended_limit=$((max_connections - 20))
|
|
echo "REC|$rec_count|csf_ct_limit|Reduce CSF CT_LIMIT from $current_ct_limit to $recommended_limit|MEDIUM|High concurrent connections detected ($max_connections from single IP)" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# RECOMMENDATION #3: Domain-Specific .htaccess Protection
|
|
if [ "$attack_scope" = "single_domain" ] || [ "$attack_scope" = "primary_target" ]; then
|
|
rec_count=$((rec_count + 1))
|
|
echo "REC|$rec_count|htaccess_domain|Add bot blocking to $primary_target .htaccess|HIGH|${primary_target_percentage}% of attacks target this domain" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
|
|
# RECOMMENDATION #4: Server-wide Apache Protection
|
|
if [ "$attack_scope" = "server_wide" ]; then
|
|
rec_count=$((rec_count + 1))
|
|
echo "REC|$rec_count|apache_global|Add global bot blocking to Apache pre-virtualhost|HIGH|Attack affects $affected_domains of $total_domains domains" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
|
|
# RECOMMENDATION #5: WordPress-specific (if attack patterns show wp-admin/wp-login attempts)
|
|
local wp_attacks=0
|
|
if [ -s "$TEMP_DIR/attack_vectors_raw.txt" ]; then
|
|
wp_attacks=$(grep -i "wp-admin\|wp-login\|xmlrpc" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | wc -l || echo "0")
|
|
fi
|
|
|
|
if [ $wp_attacks -gt 50 ]; then
|
|
rec_count=$((rec_count + 1))
|
|
|
|
# Determine which domains have WordPress
|
|
local wp_domain_count=0
|
|
local wp_target_domain=""
|
|
|
|
if [ -s "$TEMP_DIR/attack_vectors_raw.txt" ]; then
|
|
# Get unique domains with WP attacks
|
|
wp_domain_count=$(grep -i "wp-admin\|wp-login\|xmlrpc" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | cut -d'|' -f2 | sort -u | wc -l || echo "0")
|
|
wp_target_domain=$(grep -i "wp-admin\|wp-login\|xmlrpc" "$TEMP_DIR/attack_vectors_raw.txt" 2>/dev/null | cut -d'|' -f2 | sort -u | head -1)
|
|
fi
|
|
|
|
# Generate appropriate recommendation based on how many domains have WordPress attacks
|
|
if [ $wp_domain_count -eq 1 ] || [ "$attack_scope" = "single_domain" ] || [ "$attack_scope" = "single_server" ]; then
|
|
# Single domain being attacked
|
|
echo "REC|$rec_count|wp_hardening|Harden WordPress on $wp_target_domain|HIGH|$wp_attacks WordPress login/admin attempts detected" >> "$TEMP_DIR/recommendations.txt"
|
|
elif [ "$attack_scope" = "primary_target" ]; then
|
|
# One primary target but others also affected
|
|
echo "REC|$rec_count|wp_hardening|Harden WordPress on $primary_target|HIGH|$wp_attacks WordPress login/admin attempts detected" >> "$TEMP_DIR/recommendations.txt"
|
|
else
|
|
# Multiple domains with WordPress attacks
|
|
echo "REC|$rec_count|wp_hardening|Harden WordPress across $wp_domain_count domains|HIGH|$wp_attacks WordPress login/admin attempts detected" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
fi
|
|
|
|
# PORTFLOOD Protection removed - not appropriate for web servers with many sites
|
|
# Blocking ports 80/443 based on connection count breaks legitimate traffic
|
|
|
|
# RECOMMENDATION #7: CSF SYNFLOOD Protection (if DDoS patterns detected)
|
|
if [ -s "$TEMP_DIR/rapid_fire_ips.txt" ]; then
|
|
local ddos_count=$(wc -l < "$TEMP_DIR/rapid_fire_ips.txt" || echo "0")
|
|
if [ $ddos_count -gt 10 ]; then
|
|
rec_count=$((rec_count + 1))
|
|
echo "REC|$rec_count|csf_synflood|Enable CSF SYNFLOOD protection|HIGH|$ddos_count potential DDoS sources detected" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
fi
|
|
|
|
# RECOMMENDATION #8: MaxMind GeoIP for Country Blocking (if distributed attack)
|
|
if [ -s "$TEMP_DIR/geo_needs_maxmind.txt" ]; then
|
|
local geo_info=$(cat "$TEMP_DIR/geo_needs_maxmind.txt")
|
|
local network_count=$(echo "$geo_info" | cut -d'|' -f2 | grep -oP '\d+' || echo "0")
|
|
local ip_count=$(echo "$geo_info" | cut -d'|' -f3 | grep -oP '\d+' || echo "0")
|
|
|
|
rec_count=$((rec_count + 1))
|
|
echo "REC|$rec_count|install_maxmind|Install MaxMind GeoIP2 for country-based blocking|MEDIUM|Distributed attack from $network_count networks ($ip_count IPs) - geographic blocking recommended" >> "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
|
|
# Store attack scope for menu system
|
|
echo "$attack_scope|$primary_target|$primary_target_percentage|$affected_domains|$total_domains" > "$TEMP_DIR/attack_scope.txt"
|
|
|
|
print_success "Generated $rec_count recommendations"
|
|
}
|
|
|
|
################################################################################
|
|
# POST-ANALYSIS MENU
|
|
################################################################################
|
|
|
|
show_post_analysis_menu() {
|
|
# Load attack scope information
|
|
local attack_scope="unknown"
|
|
local primary_target=""
|
|
local primary_target_percentage=0
|
|
local affected_domains=0
|
|
local total_domains=0
|
|
|
|
if [ -s "$TEMP_DIR/attack_scope.txt" ]; then
|
|
local scope_data=$(cat "$TEMP_DIR/attack_scope.txt")
|
|
attack_scope=$(echo "$scope_data" | cut -d'|' -f1)
|
|
primary_target=$(echo "$scope_data" | cut -d'|' -f2)
|
|
primary_target_percentage=$(echo "$scope_data" | cut -d'|' -f3)
|
|
affected_domains=$(echo "$scope_data" | cut -d'|' -f4)
|
|
total_domains=$(echo "$scope_data" | cut -d'|' -f5)
|
|
fi
|
|
|
|
# Check if there are any recommendations
|
|
local has_recommendations=false
|
|
local rec_count=0
|
|
if [ -s "$TEMP_DIR/recommendations.txt" ]; then
|
|
has_recommendations=true
|
|
rec_count=$(wc -l < "$TEMP_DIR/recommendations.txt")
|
|
fi
|
|
|
|
# Show menu
|
|
echo ""
|
|
echo "==============================================================="
|
|
print_header "THREAT ANALYSIS SUMMARY"
|
|
echo ""
|
|
|
|
# Display attack scope
|
|
case "$attack_scope" in
|
|
single_domain)
|
|
print_warning "ATTACK SCOPE: Single Domain Target"
|
|
echo " • Primary Target: $primary_target"
|
|
echo " • This domain is receiving 100% of high-risk traffic"
|
|
echo " • Recommendation: Domain-specific protection"
|
|
;;
|
|
primary_target)
|
|
print_warning "ATTACK SCOPE: Primarily Targeting One Domain"
|
|
echo " • Primary Target: $primary_target ($primary_target_percentage% of attacks)"
|
|
echo " • Other domains also affected: $affected_domains of $total_domains total"
|
|
echo " • Recommendation: Focus protection on primary target"
|
|
;;
|
|
server_wide)
|
|
print_alert "ATTACK SCOPE: Server-Wide Attack"
|
|
echo " • Multiple domains under attack: $affected_domains of $total_domains"
|
|
echo " • Attack is distributed across the server"
|
|
echo " • Recommendation: Server-wide protection needed"
|
|
;;
|
|
single_server)
|
|
print_info "ATTACK SCOPE: Single-Domain Server"
|
|
echo " • Target: $primary_target (only domain on server)"
|
|
echo " • Server-level protection will apply to this domain"
|
|
;;
|
|
*)
|
|
print_info "No significant threats detected"
|
|
;;
|
|
esac
|
|
|
|
echo ""
|
|
|
|
# Display recommendations
|
|
if [ "$has_recommendations" = true ]; then
|
|
echo "==============================================================="
|
|
print_header "RECOMMENDED ACTIONS ($rec_count recommendations)"
|
|
echo ""
|
|
|
|
local count=0
|
|
while IFS='|' read -r rec_type rec_num action_type action_title priority description; do
|
|
count=$((count + 1))
|
|
|
|
# Color code by priority
|
|
local priority_color=""
|
|
local priority_icon=""
|
|
case "$priority" in
|
|
CRITICAL)
|
|
priority_color="${RED}"
|
|
priority_icon=""
|
|
;;
|
|
HIGH)
|
|
priority_color="${YELLOW}"
|
|
priority_icon=""
|
|
;;
|
|
MEDIUM)
|
|
priority_color="${BLUE}"
|
|
priority_icon=""
|
|
;;
|
|
*)
|
|
priority_color="${NC}"
|
|
priority_icon=" "
|
|
;;
|
|
esac
|
|
|
|
echo -e " ${BOLD}[$count]${NC} $priority_icon $action_title"
|
|
echo -e " ${priority_color}Priority: $priority${NC} - $description"
|
|
echo ""
|
|
done < "$TEMP_DIR/recommendations.txt"
|
|
|
|
echo "==============================================================="
|
|
echo ""
|
|
echo "What would you like to do?"
|
|
echo ""
|
|
echo " 1) Go to Take Action Menu (implement recommended actions)"
|
|
echo " 2) Review Individual Recommendations (detailed view)"
|
|
echo " 3) Go Back (return to main menu)"
|
|
echo ""
|
|
read -p "Select option [1-3]: " menu_choice
|
|
|
|
case "$menu_choice" in
|
|
1)
|
|
show_action_menu
|
|
;;
|
|
2)
|
|
show_detailed_recommendations
|
|
;;
|
|
3)
|
|
print_info "Returning to main menu..."
|
|
return 0
|
|
;;
|
|
*)
|
|
print_warning "Invalid option - returning to main menu"
|
|
return 0
|
|
;;
|
|
esac
|
|
else
|
|
print_success "No recommendations - your server appears secure"
|
|
echo ""
|
|
echo "Press Enter to return to main menu..."
|
|
read
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# DETAILED RECOMMENDATIONS VIEWER
|
|
################################################################################
|
|
|
|
show_detailed_recommendations() {
|
|
clear
|
|
print_banner "Detailed Recommendations"
|
|
echo ""
|
|
|
|
if [ ! -s "$TEMP_DIR/recommendations.txt" ]; then
|
|
print_warning "No recommendations available"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_post_analysis_menu
|
|
return
|
|
fi
|
|
|
|
local count=0
|
|
while IFS='|' read -r rec_type rec_num action_type action_title priority description; do
|
|
count=$((count + 1))
|
|
|
|
echo "==============================================================="
|
|
echo -e "${BOLD}Recommendation #$count:${NC} $action_title"
|
|
echo "==============================================================="
|
|
echo ""
|
|
echo "Priority: $priority"
|
|
echo "Action Type: $action_type"
|
|
echo "Description: $description"
|
|
echo ""
|
|
|
|
# Show specific details based on action type
|
|
case "$action_type" in
|
|
ip_block_temp|ip_block_perm)
|
|
echo "Affected IPs:"
|
|
awk -F'|' '$1 >= 70 {printf " • %s (score: %s)\n", $2, $1}' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | head -10
|
|
;;
|
|
htaccess_domain)
|
|
local target_domain=$(echo "$action_title" | grep -oP 'to \K[^ ]+')
|
|
echo "Target Domain: $target_domain"
|
|
if [ -s "$TEMP_DIR/domain_threats_sorted.txt" ]; then
|
|
grep "^$target_domain|" "$TEMP_DIR/domain_threats_sorted.txt" | while IFS='|' read -r domain total_req bot_req bot_pct high_risk attacks ips; do
|
|
echo " • Total Requests: $total_req"
|
|
echo " • Bot Requests: $bot_req ($bot_pct%)"
|
|
echo " • High-Risk IPs: $high_risk"
|
|
echo " • Attack Attempts: $attacks"
|
|
done
|
|
fi
|
|
;;
|
|
apache_global)
|
|
echo "Affected Domains:"
|
|
if [ -s "$TEMP_DIR/domain_high_risk_ips.txt" ]; then
|
|
awk -F'|' '{printf " • %s (%s high-risk IPs)\n", $1, $2}' "$TEMP_DIR/domain_high_risk_ips.txt" | head -10
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
echo ""
|
|
done < "$TEMP_DIR/recommendations.txt"
|
|
|
|
echo "==============================================================="
|
|
echo ""
|
|
read -p "Press Enter to return to action menu..."
|
|
show_post_analysis_menu
|
|
}
|
|
|
|
################################################################################
|
|
# ACTION MENU (IMPLEMENT RECOMMENDATIONS)
|
|
################################################################################
|
|
|
|
show_action_menu() {
|
|
clear
|
|
print_banner "Take Action Menu"
|
|
echo ""
|
|
|
|
# Build hash table of recommended actions with their priorities
|
|
declare -A recommended_actions
|
|
declare -A action_priorities
|
|
declare -A action_descriptions
|
|
|
|
if [ -s "$TEMP_DIR/recommendations.txt" ]; then
|
|
while IFS='|' read -r rec_type rec_num action_type action_title priority description; do
|
|
recommended_actions["$action_type"]=1
|
|
action_priorities["$action_type"]="$priority"
|
|
action_descriptions["$action_type"]="$description"
|
|
done < "$TEMP_DIR/recommendations.txt"
|
|
fi
|
|
|
|
# Display all available actions (not just recommended ones)
|
|
echo "All Available Actions:"
|
|
echo ""
|
|
echo "Legend: = Recommended by analysis"
|
|
echo ""
|
|
|
|
local count=0
|
|
declare -a action_types
|
|
declare -a action_titles
|
|
declare -a action_descs
|
|
|
|
# Define all possible actions
|
|
# 1. IP Blocking Actions
|
|
count=$((count + 1))
|
|
action_types[$count]="ip_block_temp_1hr"
|
|
action_titles[$count]="Block high-risk IPs for 1 hour (CSF temporary)"
|
|
action_descs[$count]="Temporary firewall block, auto-expires after 1 hour"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[ip_block_temp]}" "${action_priorities[ip_block_temp]}"
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="ip_block_temp_24hr"
|
|
action_titles[$count]="Block high-risk IPs for 24 hours (CSF temporary)"
|
|
action_descs[$count]="Temporary firewall block, auto-expires after 24 hours"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[ip_block_temp]}" "${action_priorities[ip_block_temp]}"
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="ip_block_perm"
|
|
action_titles[$count]="Block high-risk IPs permanently (CSF permanent)"
|
|
action_descs[$count]="Permanent firewall block - requires manual removal"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[ip_block_perm]}" "${action_priorities[ip_block_perm]}"
|
|
|
|
echo ""
|
|
echo "------------------------------------------------------------─"
|
|
echo ""
|
|
|
|
# 2. Domain/Site Protection
|
|
count=$((count + 1))
|
|
action_types[$count]="htaccess_domain"
|
|
action_titles[$count]="Add bot blocking to specific domain .htaccess"
|
|
action_descs[$count]="Domain-level protection via Apache .htaccess rules"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[htaccess_domain]}" "${action_priorities[htaccess_domain]}"
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="apache_global"
|
|
action_titles[$count]="Add global bot blocking to Apache (all domains)"
|
|
action_descs[$count]="Server-wide Apache configuration, affects all sites"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[apache_global]}" "${action_priorities[apache_global]}"
|
|
|
|
echo ""
|
|
echo "------------------------------------------------------------─"
|
|
echo ""
|
|
|
|
# 3. CSF Firewall Configuration
|
|
count=$((count + 1))
|
|
action_types[$count]="csf_ct_limit"
|
|
action_titles[$count]="Adjust CSF connection tracking limit (CT_LIMIT)"
|
|
action_descs[$count]="Limit concurrent connections per IP address"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[csf_ct_limit]}" "${action_priorities[csf_ct_limit]}"
|
|
|
|
# PORTFLOOD action removed - not appropriate for web servers
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="csf_synflood"
|
|
action_titles[$count]="Enable CSF SYNFLOOD protection"
|
|
action_descs[$count]="Protect against SYN flood DDoS attacks"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[csf_synflood]}" "${action_priorities[csf_synflood]}"
|
|
|
|
echo ""
|
|
echo "------------------------------------------------------------─"
|
|
echo ""
|
|
|
|
# 4. Geographic & Application Hardening
|
|
count=$((count + 1))
|
|
action_types[$count]="install_maxmind"
|
|
action_titles[$count]="Install MaxMind GeoIP2 for country-based blocking"
|
|
action_descs[$count]="Enable geographic filtering with CSF CC_DENY (requires free MaxMind license)"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[install_maxmind]}" "${action_priorities[install_maxmind]}"
|
|
|
|
count=$((count + 1))
|
|
action_types[$count]="wp_hardening"
|
|
action_titles[$count]="WordPress security hardening"
|
|
action_descs[$count]="Protect WordPress login and admin areas"
|
|
display_action_option $count "${action_types[$count]}" "${action_titles[$count]}" "${action_descs[$count]}" "${recommended_actions[wp_hardening]}" "${action_priorities[wp_hardening]}"
|
|
|
|
echo ""
|
|
echo "============================================================═"
|
|
echo ""
|
|
echo " 0) Go Back"
|
|
echo ""
|
|
read -p "Select action [0-$count]: " action_choice
|
|
|
|
# Validate choice
|
|
if [ "$action_choice" = "0" ]; then
|
|
show_post_analysis_menu
|
|
return
|
|
elif [ "$action_choice" -lt 1 ] || [ "$action_choice" -gt "$count" ] 2>/dev/null; then
|
|
print_warning "Invalid selection"
|
|
sleep 2
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Execute selected action
|
|
local selected_type="${action_types[$action_choice]}"
|
|
execute_action "$selected_type" "$action_choice"
|
|
}
|
|
|
|
# Helper function to display action options
|
|
display_action_option() {
|
|
local num=$1
|
|
local action_type=$2
|
|
local title=$3
|
|
local desc=$4
|
|
local is_recommended=$5
|
|
local priority=$6
|
|
|
|
# Show recommendation marker and priority if recommended
|
|
if [ -n "$is_recommended" ]; then
|
|
case "$priority" in
|
|
CRITICAL)
|
|
echo -e " ${RED}$num)${NC} ${BOLD}$title${NC} ${RED} RECOMMENDED [CRITICAL]${NC}"
|
|
;;
|
|
HIGH)
|
|
echo -e " ${YELLOW}$num)${NC} ${BOLD}$title${NC} ${YELLOW} RECOMMENDED [HIGH]${NC}"
|
|
;;
|
|
MEDIUM)
|
|
echo -e " ${BLUE}$num)${NC} ${BOLD}$title${NC} ${BLUE} RECOMMENDED [MEDIUM]${NC}"
|
|
;;
|
|
*)
|
|
echo -e " ${GREEN}$num)${NC} ${BOLD}$title${NC} ${GREEN} RECOMMENDED${NC}"
|
|
;;
|
|
esac
|
|
else
|
|
echo -e " $num) $title"
|
|
fi
|
|
echo " $desc"
|
|
}
|
|
|
|
################################################################################
|
|
# ACTION EXECUTION ENGINE
|
|
################################################################################
|
|
|
|
execute_action() {
|
|
local action_type="$1"
|
|
local rec_number="$2"
|
|
|
|
case "$action_type" in
|
|
ip_block_temp_1hr)
|
|
execute_ip_blocking_specific "1hr"
|
|
;;
|
|
ip_block_temp_24hr)
|
|
execute_ip_blocking_specific "24hr"
|
|
;;
|
|
ip_block_temp)
|
|
execute_ip_blocking "temp"
|
|
;;
|
|
ip_block_perm)
|
|
execute_ip_blocking "perm"
|
|
;;
|
|
csf_ct_limit)
|
|
execute_csf_ct_limit
|
|
;;
|
|
csf_synflood)
|
|
execute_csf_synflood
|
|
;;
|
|
htaccess_domain)
|
|
execute_htaccess_domain_blocking
|
|
;;
|
|
apache_global)
|
|
execute_apache_global_blocking
|
|
;;
|
|
install_maxmind)
|
|
execute_install_maxmind
|
|
;;
|
|
wp_hardening)
|
|
execute_wp_hardening
|
|
;;
|
|
rate_limiting)
|
|
execute_rate_limiting
|
|
;;
|
|
*)
|
|
print_warning "Action type '$action_type' not yet implemented"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
;;
|
|
esac
|
|
}
|
|
|
|
execute_ip_blocking_specific() {
|
|
local duration_type="$1" # "1hr" or "24hr"
|
|
|
|
clear
|
|
print_banner "IP Blocking - CSF Temporary Block"
|
|
echo ""
|
|
|
|
# Check if CSF is installed
|
|
if ! command -v csf >/dev/null 2>&1; then
|
|
print_warning "CSF (ConfigServer Security & Firewall) is not installed"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Get high-risk IPs
|
|
if [ ! -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
print_warning "No threat scores available"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
local high_risk_count=$(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | wc -l || echo "0")
|
|
|
|
if [ "$high_risk_count" -eq 0 ]; then
|
|
print_info "No high-risk IPs detected (score >= 70)"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Set duration based on type
|
|
local duration
|
|
local duration_text
|
|
if [ "$duration_type" = "1hr" ]; then
|
|
duration=3600
|
|
duration_text="1 hour"
|
|
else
|
|
duration=86400
|
|
duration_text="24 hours"
|
|
fi
|
|
|
|
echo "This will block $high_risk_count high-risk IPs for $duration_text"
|
|
echo ""
|
|
echo "High-risk IPs (top 10):"
|
|
awk -F'|' '$1 >= 70 {printf " • %s (score: %s, %s requests)\n", $2, $1, $3}' "$TEMP_DIR/threat_scores.txt" | head -10
|
|
echo ""
|
|
|
|
if [ "$high_risk_count" -gt 10 ]; then
|
|
echo " ... and $((high_risk_count - 10)) more"
|
|
echo ""
|
|
fi
|
|
|
|
read -p "Proceed with blocking for $duration_text? (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Collect IPs to block
|
|
local -a ips_to_block
|
|
while IFS='|' read -r score ip requests; do
|
|
if [ "$score" -ge 70 ]; then
|
|
# Skip excluded IPs
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
# Skip false positives
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ] && grep -q "^$ip|" "$TEMP_DIR/false_positives.txt" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
ips_to_block+=("$ip")
|
|
fi
|
|
done < "$TEMP_DIR/threat_scores.txt"
|
|
|
|
# Apply blocks
|
|
echo ""
|
|
print_info "Applying CSF blocks for $duration_text..."
|
|
echo ""
|
|
|
|
local success_count=0
|
|
local fail_count=0
|
|
|
|
for ip in "${ips_to_block[@]}"; do
|
|
local score=$(grep "|$ip|" "$TEMP_DIR/threat_scores.txt" 2>/dev/null | cut -d'|' -f1 || echo "unknown")
|
|
|
|
if csf -td "$ip" "$duration" "Bot threat score: $score/100 - Auto-blocked by toolkit" >/dev/null 2>&1; then
|
|
echo -e " ${GREEN}${NC} Blocked $ip for $duration_text (score: $score/100)"
|
|
success_count=$((success_count + 1))
|
|
else
|
|
echo -e " ${RED}${NC} Failed to block $ip"
|
|
fail_count=$((fail_count + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [ $success_count -gt 0 ]; then
|
|
print_success "Successfully blocked $success_count IP(s) for $duration_text"
|
|
echo ""
|
|
echo "These blocks will automatically expire after $duration_text"
|
|
echo "To view temporary blocks: csf -t"
|
|
echo "To remove a block early: csf -tr IP"
|
|
fi
|
|
|
|
if [ $fail_count -gt 0 ]; then
|
|
print_warning "$fail_count IP(s) failed to block - check CSF configuration"
|
|
fi
|
|
|
|
# Restart CSF
|
|
print_info "Restarting CSF to apply changes..."
|
|
if csf -r >/dev/null 2>&1; then
|
|
print_success "CSF restarted successfully"
|
|
else
|
|
print_warning "CSF restart may have failed - check manually with: csf -r"
|
|
fi
|
|
|
|
echo ""
|
|
# Verify domains still work after blocking
|
|
verify_domains_still_working
|
|
|
|
show_action_menu
|
|
}
|
|
|
|
execute_ip_blocking() {
|
|
local block_mode="$1" # "temp" or "perm"
|
|
|
|
if [ "$block_mode" = "temp" ]; then
|
|
# Call the existing CSF blocking function
|
|
offer_csf_blocking
|
|
else
|
|
# Permanent blocking
|
|
clear
|
|
print_banner "Permanent IP Blocking"
|
|
echo ""
|
|
print_alert "WARNING: Permanent blocks must be manually removed later"
|
|
echo ""
|
|
echo "This will permanently block all high-risk IPs (score >= 70)"
|
|
echo ""
|
|
read -p "Are you sure you want to proceed? (yes/no): " confirm
|
|
|
|
if [ "$confirm" = "yes" ]; then
|
|
offer_csf_blocking
|
|
else
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
fi
|
|
fi
|
|
}
|
|
|
|
execute_csf_ct_limit() {
|
|
clear
|
|
print_banner "Update CSF Connection Tracking Limit"
|
|
echo ""
|
|
|
|
# Check if CSF is installed
|
|
if ! command -v csf >/dev/null 2>&1; then
|
|
print_warning "CSF is not installed on this server"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Get recommended limit from recommendation
|
|
local recommended_limit=$(grep "|csf_ct_limit|" "$TEMP_DIR/recommendations.txt" 2>/dev/null | grep -oP 'to \K[0-9]+' || echo "100")
|
|
|
|
# Get current CT_LIMIT
|
|
local current_limit=$(grep "^CT_LIMIT" /etc/csf/csf.conf 2>/dev/null | grep -oP '"\K[0-9]+' || echo "unknown")
|
|
|
|
echo "Current CT_LIMIT: $current_limit"
|
|
echo "Recommended CT_LIMIT: $recommended_limit"
|
|
echo ""
|
|
echo "This will modify /etc/csf/csf.conf and restart CSF"
|
|
echo ""
|
|
read -p "Enter new CT_LIMIT value [$recommended_limit]: " new_limit
|
|
|
|
# Use recommended if nothing entered
|
|
[ -z "$new_limit" ] && new_limit=$recommended_limit
|
|
|
|
# Validate it's a number
|
|
if ! [[ "$new_limit" =~ ^[0-9]+$ ]]; then
|
|
print_warning "Invalid number"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Update CSF config
|
|
print_info "Updating CT_LIMIT to $new_limit..."
|
|
|
|
if [ -f /etc/csf/csf.conf ]; then
|
|
sed -i "s/^CT_LIMIT = .*/CT_LIMIT = \"$new_limit\"/" /etc/csf/csf.conf
|
|
|
|
# Restart CSF
|
|
print_info "Restarting CSF..."
|
|
csf -r >/dev/null 2>&1
|
|
|
|
print_success "CT_LIMIT updated successfully to $new_limit"
|
|
else
|
|
print_warning "Could not find /etc/csf/csf.conf"
|
|
fi
|
|
|
|
echo ""
|
|
# Verify domains still work after CT_LIMIT change
|
|
verify_domains_still_working
|
|
|
|
show_action_menu
|
|
}
|
|
|
|
execute_htaccess_domain_blocking() {
|
|
clear
|
|
print_banner "Add Bot Blocking to Domain .htaccess"
|
|
echo ""
|
|
|
|
# Get target domain from recommendation
|
|
local target_domain=$(grep "|htaccess_domain|" "$TEMP_DIR/recommendations.txt" 2>/dev/null | head -1 | grep -oP 'to \K[^ ]+' || echo "")
|
|
|
|
if [ -z "$target_domain" ]; then
|
|
print_warning "Could not determine target domain"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
echo "Target Domain: $target_domain"
|
|
echo ""
|
|
|
|
# Find document root for this domain using reference database
|
|
local doc_root=""
|
|
if [ -s "$TOOLKIT_BASE_DIR/.sysref" ]; then
|
|
doc_root=$(grep "^DOMAIN|$target_domain|" "$TOOLKIT_BASE_DIR/.sysref" 2>/dev/null | head -1 | cut -d'|' -f4)
|
|
fi
|
|
|
|
if [ -z "$doc_root" ]; then
|
|
print_warning "Document root not found in reference database"
|
|
echo "Please enter the document root manually:"
|
|
read -p "Document root: " doc_root
|
|
else
|
|
echo "Document root: $doc_root"
|
|
fi
|
|
|
|
if [ ! -d "$doc_root" ]; then
|
|
print_warning "Document root does not exist: $doc_root"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
local htaccess_file="$doc_root/.htaccess"
|
|
|
|
echo ""
|
|
echo "This will add bot blocking rules to: $htaccess_file"
|
|
echo ""
|
|
read -p "Proceed? (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Create backup
|
|
if [ -f "$htaccess_file" ]; then
|
|
cp "$htaccess_file" "$htaccess_file.backup.$(date +%Y%m%d_%H%M%S)"
|
|
print_info "Backed up existing .htaccess"
|
|
fi
|
|
|
|
# Generate bot blocking rules
|
|
print_info "Adding bot blocking rules..."
|
|
|
|
# Get high-risk IPs for this domain
|
|
local block_ips=$(grep "^[^|]*|$target_domain|" "$TEMP_DIR/parsed_logs.txt" 2>/dev/null | cut -d'|' -f1 | sort -u | while read ip; do
|
|
# Check if this IP has high threat score
|
|
if grep -q "|$ip$" "$TEMP_DIR/threat_scores.txt" 2>/dev/null; then
|
|
local score=$(grep "|$ip$" "$TEMP_DIR/threat_scores.txt" | cut -d'|' -f1)
|
|
if [ "$score" -ge 70 ]; then
|
|
echo "$ip"
|
|
fi
|
|
fi
|
|
done)
|
|
|
|
# Add rules to .htaccess
|
|
{
|
|
echo ""
|
|
echo "# Bot blocking rules added by toolkit on $(date)"
|
|
echo "# High-risk IPs (threat score >= 70)"
|
|
echo "<IfModule mod_authz_core.c>"
|
|
for ip in $block_ips; do
|
|
echo " Require not ip $ip"
|
|
done
|
|
echo "</IfModule>"
|
|
echo ""
|
|
} >> "$htaccess_file"
|
|
|
|
local block_count=$(echo "$block_ips" | wc -w)
|
|
print_success "Added blocking rules for $block_count IPs to $htaccess_file"
|
|
echo ""
|
|
echo "Backup saved to: $htaccess_file.backup.$(date +%Y%m%d_%H%M%S)"
|
|
echo ""
|
|
|
|
# Verify domains still work after .htaccess changes
|
|
verify_domains_still_working
|
|
|
|
show_action_menu
|
|
}
|
|
|
|
execute_apache_global_blocking() {
|
|
clear
|
|
print_banner "Add Global Bot Blocking to Apache"
|
|
echo ""
|
|
|
|
print_warning "This feature will add blocking rules to Apache pre-virtualhost configuration"
|
|
echo "This affects ALL domains on the server"
|
|
echo ""
|
|
|
|
# Determine Apache config location
|
|
local apache_conf=""
|
|
if [ -d "/etc/apache2/conf.d" ]; then
|
|
apache_conf="/etc/apache2/conf.d/bot_blocking.conf"
|
|
elif [ -d "/etc/httpd/conf.d" ]; then
|
|
apache_conf="/etc/httpd/conf.d/bot_blocking.conf"
|
|
else
|
|
print_warning "Could not determine Apache config directory"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
echo "Configuration will be written to: $apache_conf"
|
|
echo ""
|
|
read -p "Proceed? (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Create backup if file exists
|
|
if [ -f "$apache_conf" ]; then
|
|
cp "$apache_conf" "$apache_conf.backup.$(date +%Y%m%d_%H%M%S)"
|
|
print_info "Backed up existing configuration"
|
|
fi
|
|
|
|
# Generate global blocking rules
|
|
print_info "Generating global bot blocking configuration..."
|
|
|
|
{
|
|
echo "# Global bot blocking rules"
|
|
echo "# Generated by toolkit on $(date)"
|
|
echo ""
|
|
echo "<IfModule mod_authz_core.c>"
|
|
echo " # Block high-risk IPs (threat score >= 70)"
|
|
|
|
awk -F'|' '$1 >= 70 {print " Require not ip " $2}' "$TEMP_DIR/threat_scores.txt" 2>/dev/null
|
|
|
|
echo "</IfModule>"
|
|
echo ""
|
|
} > "$apache_conf"
|
|
|
|
local block_count=$(awk -F'|' '$1 >= 70' "$TEMP_DIR/threat_scores.txt" 2>/dev/null | wc -l)
|
|
print_success "Created global blocking configuration with $block_count IPs"
|
|
|
|
echo ""
|
|
echo "Restarting Apache to apply changes..."
|
|
|
|
if systemctl restart httpd 2>/dev/null || systemctl restart apache2 2>/dev/null; then
|
|
print_success "Apache restarted successfully"
|
|
else
|
|
print_warning "Could not restart Apache - please restart manually"
|
|
fi
|
|
|
|
echo ""
|
|
# Verify domains still work after Apache global blocking
|
|
verify_domains_still_working
|
|
|
|
show_action_menu
|
|
}
|
|
|
|
execute_wp_hardening() {
|
|
clear
|
|
print_banner "WordPress Hardening"
|
|
echo ""
|
|
print_info "WordPress hardening feature coming soon..."
|
|
echo ""
|
|
echo "Recommended manual actions:"
|
|
echo " • Install Wordfence or similar security plugin"
|
|
echo " • Enable two-factor authentication"
|
|
echo " • Limit login attempts"
|
|
echo " • Disable XML-RPC if not needed"
|
|
echo " • Use strong passwords"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
}
|
|
|
|
execute_rate_limiting() {
|
|
clear
|
|
print_banner "Enable Rate Limiting"
|
|
echo ""
|
|
print_info "Rate limiting modules like mod_evasive/mod_security can help with application-level DoS"
|
|
echo ""
|
|
echo "For better bot protection, consider:"
|
|
echo " - IP blocking (options 1-3) - Block specific attacking IPs"
|
|
echo " - CSF CT_LIMIT adjustment (option 4) - Limit connections per IP"
|
|
echo " - .htaccess rules (option 5) - Domain-specific blocking"
|
|
echo ""
|
|
echo "This option (rate limiting) is currently a placeholder for future implementation."
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
}
|
|
|
|
# execute_csf_portflood() removed - not appropriate for web servers with 400+ sites
|
|
# Blocking ports 80/443 based on connection count would break legitimate traffic
|
|
|
|
execute_csf_synflood() {
|
|
clear
|
|
print_banner "Enable CSF SYNFLOOD Protection"
|
|
echo ""
|
|
|
|
if ! command -v csf >/dev/null 2>&1; then
|
|
print_warning "CSF is not installed on this server"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Get current SYNFLOOD setting
|
|
local current_synflood=$(grep "^SYNFLOOD = " /etc/csf/csf.conf 2>/dev/null | grep -oP '"\K[^"]+' || echo "0")
|
|
|
|
echo "Current SYNFLOOD protection: ${current_synflood}"
|
|
echo ""
|
|
echo "SYNFLOOD protects against SYN flood DDoS attacks by limiting"
|
|
echo "the rate of new TCP connections."
|
|
echo ""
|
|
echo "Recommended settings:"
|
|
echo " SYNFLOOD = \"1\" (enable protection)"
|
|
echo " SYNFLOOD_RATE = \"100/s\" (100 connections per second)"
|
|
echo " SYNFLOOD_BURST = \"150\" (allow burst of 150)"
|
|
echo ""
|
|
read -p "Enable SYNFLOOD protection? (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Operation cancelled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
return
|
|
fi
|
|
|
|
# Update CSF config
|
|
print_info "Enabling SYNFLOOD protection..."
|
|
if [ -f /etc/csf/csf.conf ]; then
|
|
sed -i 's/^SYNFLOOD = .*/SYNFLOOD = "1"/' /etc/csf/csf.conf
|
|
sed -i 's/^SYNFLOOD_RATE = .*/SYNFLOOD_RATE = "100\/s"/' /etc/csf/csf.conf
|
|
sed -i 's/^SYNFLOOD_BURST = .*/SYNFLOOD_BURST = "150"/' /etc/csf/csf.conf
|
|
|
|
# Restart CSF
|
|
print_info "Restarting CSF..."
|
|
csf -r >/dev/null 2>&1
|
|
|
|
print_success "SYNFLOOD protection enabled"
|
|
else
|
|
print_warning "Could not find /etc/csf/csf.conf"
|
|
fi
|
|
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
}
|
|
|
|
execute_install_maxmind() {
|
|
clear
|
|
print_banner "Install MaxMind GeoIP2 for Country Blocking"
|
|
echo ""
|
|
|
|
# Check if already installed
|
|
if command -v mmdbinspect >/dev/null 2>&1; then
|
|
print_success "MaxMind GeoIP2 tools already installed"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo "1. Sign up for free license at: https://www.maxmind.com/en/geolite2/signup"
|
|
echo "2. Get your license key from account page"
|
|
echo "3. Install CSF GeoIP module: /usr/local/csf/bin/csftest.pl -g"
|
|
echo "4. Configure CC_DENY in /etc/csf/csf.conf with country codes"
|
|
echo ""
|
|
echo "Example: CC_DENY = \"CN,RU,KP\" (block China, Russia, North Korea)"
|
|
echo ""
|
|
else
|
|
print_info "MaxMind GeoIP2 not detected"
|
|
echo ""
|
|
echo "To install MaxMind GeoIP2 for CSF country blocking:"
|
|
echo ""
|
|
echo "1. Sign up for free MaxMind account:"
|
|
echo " https://www.maxmind.com/en/geolite2/signup"
|
|
echo ""
|
|
echo "2. Get your license key from:"
|
|
echo " https://www.maxmind.com/en/accounts/current/license-key"
|
|
echo ""
|
|
echo "3. Install GeoIP Perl module:"
|
|
echo " yum install perl-Geo-IP"
|
|
echo " # or"
|
|
echo " cpan -i Geo::IP"
|
|
echo ""
|
|
echo "4. Test CSF GeoIP support:"
|
|
echo " /usr/local/csf/bin/csftest.pl -g"
|
|
echo ""
|
|
echo "5. Configure CC_DENY in /etc/csf/csf.conf:"
|
|
echo " CC_DENY = \"CN,RU\" (example: block China & Russia)"
|
|
echo ""
|
|
echo "6. Restart CSF:"
|
|
echo " csf -r"
|
|
echo ""
|
|
fi
|
|
|
|
# Show geographic analysis if available
|
|
if [ -s "$TEMP_DIR/high_risk_networks.txt" ]; then
|
|
echo "=========================================================══"
|
|
echo "High-Risk Networks Detected:"
|
|
echo ""
|
|
head -10 "$TEMP_DIR/high_risk_networks.txt" | while read count network; do
|
|
echo " • $network - $count high-risk IPs"
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
read -p "Press Enter to continue..."
|
|
show_action_menu
|
|
}
|
|
|
|
################################################################################
|
|
# INTERACTIVE CSF BLOCKING
|
|
################################################################################
|
|
|
|
offer_csf_blocking() {
|
|
echo ""
|
|
echo "==============================================================="
|
|
print_header "🛡 INTERACTIVE THREAT BLOCKING"
|
|
|
|
# Check if CSF is installed
|
|
if ! command -v csf >/dev/null 2>&1; then
|
|
print_warning "CSF (ConfigServer Security & Firewall) is not installed"
|
|
echo "Cannot offer automatic blocking without CSF"
|
|
return 0
|
|
fi
|
|
|
|
# Get high-risk IPs (score >= 70)
|
|
local high_risk_ips=()
|
|
local ip_scores=()
|
|
|
|
if [ -s "$TEMP_DIR/threat_scores.txt" ]; then
|
|
while read -r line; do
|
|
local score=$(echo "$line" | cut -d'|' -f1)
|
|
local ip=$(echo "$line" | cut -d'|' -f2)
|
|
|
|
# Only include scores >= 70 (HIGH and CRITICAL)
|
|
if [ "$score" -ge 70 ]; then
|
|
# Skip excluded IPs
|
|
if is_excluded_ip "$ip"; then
|
|
continue
|
|
fi
|
|
|
|
# Skip false positives
|
|
if [ -s "$TEMP_DIR/false_positives.txt" ] && grep -q "^$ip|" "$TEMP_DIR/false_positives.txt" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
|
|
high_risk_ips+=("$ip")
|
|
ip_scores+=("$score")
|
|
fi
|
|
done < <(awk -F'|' '{print $1 "|" $2}' "$TEMP_DIR/threat_scores.txt" | sort -rn)
|
|
fi
|
|
|
|
# If no high-risk IPs, nothing to block
|
|
if [ ${#high_risk_ips[@]} -eq 0 ]; then
|
|
print_info "No high-risk IPs detected (score >= 70)"
|
|
return 0
|
|
fi
|
|
|
|
# Show IPs that would be blocked
|
|
echo ""
|
|
echo "Found ${#high_risk_ips[@]} high-risk IP(s) with threat score >= 70:"
|
|
echo ""
|
|
|
|
local count=0
|
|
for i in "${!high_risk_ips[@]}"; do
|
|
count=$((count + 1))
|
|
local ip="${high_risk_ips[$i]}"
|
|
local score="${ip_scores[$i]}"
|
|
local requests=$(grep "^$ip|" "$TEMP_DIR/bot_ips.txt" 2>/dev/null | cut -d'|' -f2 || echo "0")
|
|
|
|
# Color code by severity
|
|
if [ "$score" -ge 90 ]; then
|
|
echo -e " ${RED}[$count] $ip${NC} - Risk: ${RED}$score/100 CRITICAL${NC} ($requests requests)"
|
|
elif [ "$score" -ge 80 ]; then
|
|
echo -e " ${YELLOW}[$count] $ip${NC} - Risk: ${YELLOW}$score/100 HIGH${NC} ($requests requests)"
|
|
else
|
|
echo -e " [$count] $ip - Risk: $score/100 ELEVATED ($requests requests)"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "==============================================================="
|
|
echo ""
|
|
|
|
# Ask user if they want to block
|
|
echo -e "${BOLD}Would you like to temporarily block these IPs using CSF?${NC}"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " 1) Block for 1 hour (temporary - auto-expires)"
|
|
echo " 2) Block for 24 hours (temporary - auto-expires)"
|
|
echo " 3) Block permanently (requires manual unblock)"
|
|
echo " 4) Don't block (manual review)"
|
|
echo ""
|
|
read -p "Select option [1-4]: " block_choice
|
|
|
|
case "$block_choice" in
|
|
1)
|
|
local duration=3600 # 1 hour in seconds
|
|
local duration_text="1 hour"
|
|
apply_csf_blocks "$duration" "$duration_text" "${high_risk_ips[@]}"
|
|
;;
|
|
2)
|
|
local duration=86400 # 24 hours in seconds
|
|
local duration_text="24 hours"
|
|
apply_csf_blocks "$duration" "$duration_text" "${high_risk_ips[@]}"
|
|
;;
|
|
3)
|
|
apply_csf_permanent_blocks "${high_risk_ips[@]}"
|
|
;;
|
|
4)
|
|
print_info "Skipping automatic blocking - manual review recommended"
|
|
echo "You can block IPs manually using: csf -td IP DURATION"
|
|
;;
|
|
*)
|
|
print_warning "Invalid option - skipping blocking"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
apply_csf_blocks() {
|
|
local duration=$1
|
|
local duration_text=$2
|
|
shift 2
|
|
local ips=("$@")
|
|
|
|
echo ""
|
|
print_info "Applying temporary CSF blocks for $duration_text..."
|
|
echo ""
|
|
|
|
local success_count=0
|
|
local fail_count=0
|
|
|
|
for ip in "${ips[@]}"; do
|
|
# Get threat score for comment
|
|
local score=$(grep "|$ip$" "$TEMP_DIR/threat_scores.txt" 2>/dev/null | cut -d'|' -f1 || echo "unknown")
|
|
|
|
# Use csf -td for temporary deny
|
|
if csf -td "$ip" "$duration" "Bot threat score: $score/100 - Auto-blocked by toolkit" >/dev/null 2>&1; then
|
|
echo -e " ${GREEN}${NC} Blocked $ip for $duration_text (score: $score/100)"
|
|
success_count=$((success_count + 1))
|
|
else
|
|
echo -e " ${RED}${NC} Failed to block $ip"
|
|
fail_count=$((fail_count + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [ $success_count -gt 0 ]; then
|
|
print_success "Successfully blocked $success_count IP(s) for $duration_text"
|
|
echo ""
|
|
echo "These blocks will automatically expire after $duration_text"
|
|
echo "To view temporary blocks: csf -t"
|
|
echo "To remove a block early: csf -tr IP"
|
|
fi
|
|
|
|
if [ $fail_count -gt 0 ]; then
|
|
print_warning "$fail_count IP(s) failed to block - check CSF configuration"
|
|
fi
|
|
|
|
# Restart CSF to apply changes
|
|
print_info "Restarting CSF to apply changes..."
|
|
if csf -r >/dev/null 2>&1; then
|
|
print_success "CSF restarted successfully"
|
|
else
|
|
print_warning "CSF restart may have failed - check manually with: csf -r"
|
|
fi
|
|
}
|
|
|
|
apply_csf_permanent_blocks() {
|
|
local ips=("$@")
|
|
|
|
echo ""
|
|
print_warning "Applying PERMANENT CSF blocks..."
|
|
echo "These will require manual removal using: csf -dr IP"
|
|
echo ""
|
|
read -p "Are you sure? This is permanent! (yes/no): " confirm
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
print_info "Cancelled permanent blocking"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
local success_count=0
|
|
local fail_count=0
|
|
|
|
for ip in "${ips[@]}"; do
|
|
local score=$(grep "|$ip$" "$TEMP_DIR/threat_scores.txt" 2>/dev/null | cut -d'|' -f1 || echo "unknown")
|
|
|
|
# Use csf -d for permanent deny
|
|
if csf -d "$ip" "Bot threat score: $score/100 - Permanently blocked by toolkit" >/dev/null 2>&1; then
|
|
echo -e " ${GREEN}${NC} Permanently blocked $ip (score: $score/100)"
|
|
success_count=$((success_count + 1))
|
|
else
|
|
echo -e " ${RED}${NC} Failed to block $ip"
|
|
fail_count=$((fail_count + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [ $success_count -gt 0 ]; then
|
|
print_success "Successfully blocked $success_count IP(s) permanently"
|
|
echo ""
|
|
echo "To view blocked IPs: csf -g"
|
|
echo "To remove a block: csf -dr IP"
|
|
fi
|
|
|
|
if [ $fail_count -gt 0 ]; then
|
|
print_warning "$fail_count IP(s) failed to block - check CSF configuration"
|
|
fi
|
|
|
|
# Restart CSF
|
|
print_info "Restarting CSF to apply changes..."
|
|
if csf -r >/dev/null 2>&1; then
|
|
print_success "CSF restarted successfully"
|
|
else
|
|
print_warning "CSF restart may have failed - check manually with: csf -r"
|
|
fi
|
|
}
|
|
|
|
# Run the script
|
|
main "$@"
|