Compare commits
25 Commits
dev
...
dfdca4fc5d
| Author | SHA1 | Date | |
|---|---|---|---|
| dfdca4fc5d | |||
| ccb1c47b60 | |||
| 0c4d970053 | |||
| 9ff7308de0 | |||
| 9cc203a87e | |||
| c73111eda1 | |||
| 0697e17783 | |||
| bce7bd1d28 | |||
| e668efa41c | |||
| a13c324d8e | |||
| 7c19a2b3a5 | |||
| 1dd950b358 | |||
| 6a6c8c036e | |||
| e21e4a9fb7 | |||
| 1674a62eae | |||
| 585785847b | |||
| e4eaf9afb5 | |||
| ab70a6e569 | |||
| b77e8bb9dd | |||
| 60cdad8e7b | |||
| 6632d96c82 | |||
| 1d77cad16c | |||
| 5cb34c913e | |||
| c96423539f | |||
| ade8011149 |
@@ -46,15 +46,16 @@ server-toolkit/
|
|||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Running
|
### Installation & Running
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Direct method
|
# Download and run in one command
|
||||||
bash /root/server-toolkit/launcher.sh
|
curl -sL https://git.mull.lol/cschantz/Linux-Server-Management-Toolkit/archive/main.tar.gz | tar xz && cd linux-server-management-toolkit && bash launcher.sh
|
||||||
|
```
|
||||||
|
|
||||||
# Or make executable and run
|
Or if already downloaded:
|
||||||
chmod +x /root/server-toolkit/launcher.sh
|
```bash
|
||||||
/root/server-toolkit/launcher.sh
|
bash /root/server-toolkit/launcher.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✨ Key Features
|
## ✨ Key Features
|
||||||
|
|||||||
+157
-38
@@ -222,10 +222,11 @@ show_bot_analysis_menu() {
|
|||||||
echo -e " ${CYAN}1)${NC} Full Bot Analysis - Complete scan (all logs)"
|
echo -e " ${CYAN}1)${NC} Full Bot Analysis - Complete scan (all logs)"
|
||||||
echo -e " ${CYAN}2)${NC} Quick Scan (1 hour) - Recent activity only"
|
echo -e " ${CYAN}2)${NC} Quick Scan (1 hour) - Recent activity only"
|
||||||
echo -e " ${CYAN}3)${NC} Live Monitor - Real-time threat tracking"
|
echo -e " ${CYAN}3)${NC} Live Monitor - Real-time threat tracking"
|
||||||
echo -e " ${CYAN}4)${NC} IP Lookup & Investigation - Deep-dive on specific IP"
|
echo -e " ${CYAN}4)${NC} IP Reputation Manager - Query/manage IP database (NEW!)"
|
||||||
echo -e " ${CYAN}5)${NC} DDoS Pattern Detector - Identify DDoS attacks"
|
echo -e " ${CYAN}5)${NC} IP Lookup & Investigation - Deep-dive on specific IP"
|
||||||
echo -e " ${CYAN}6)${NC} Traffic Pattern Analysis - Bandwidth & connection patterns"
|
echo -e " ${CYAN}6)${NC} DDoS Pattern Detector - Identify DDoS attacks"
|
||||||
echo -e " ${CYAN}7)${NC} User-Agent Analysis - Bot fingerprinting"
|
echo -e " ${CYAN}7)${NC} Traffic Pattern Analysis - Bandwidth & connection patterns"
|
||||||
|
echo -e " ${CYAN}8)${NC} User-Agent Analysis - Bot fingerprinting"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${RED}0)${NC} Back to Analysis Menu"
|
echo -e " ${RED}0)${NC} Back to Analysis Menu"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -432,26 +433,18 @@ show_wordpress_menu() {
|
|||||||
show_banner
|
show_banner
|
||||||
echo -e "${BLUE}${BOLD}🌐 Website Management${NC}"
|
echo -e "${BLUE}${BOLD}🌐 Website Management${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}General Website Tools:${NC}"
|
echo -e "${BOLD}Error Analysis & Diagnostics:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BLUE}1)${NC} 🔍 Website Error Analyzer - Find 500/config errors (filters bots)"
|
echo -e " ${BLUE}1)${NC} 🔍 Website Error Analyzer - Find 500/config errors (filters bots)"
|
||||||
|
echo -e " ${RED}2)${NC} 🔥 Fast 500 Error Tracker - ONLY 500s + root cause diagnosis"
|
||||||
|
echo -e " ${BLUE}3)${NC} 📋 Debug Log Analyzer - Parse WP debug logs"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}WordPress Tools:${NC}"
|
echo -e "${BOLD}WordPress Management:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BLUE}2)${NC} Health Check (All Sites) - Scan all WP installations"
|
echo -e " ${BLUE}4)${NC} 🏥 Health & Maintenance → Audits, optimization, cleanup"
|
||||||
echo -e " ${BLUE}3)${NC} WP-Cron Status - Check cron job status"
|
echo -e " ${BLUE}5)${NC} ⚙️ WP-Cron Management → Status, fixes, system cron setup"
|
||||||
echo -e " ${BLUE}4)${NC} WP-Cron Mass Fix - Fix/enable cron on all sites"
|
echo -e " ${BLUE}6)${NC} 🔄 Mass Updates → Core, plugins, themes updates"
|
||||||
echo -e " ${BLUE}5)${NC} WP-Cron Mass Create - Setup proper system crons"
|
echo -e " ${BLUE}7)${NC} 🔒 Security & Compliance → Malware scan, permissions, login audit"
|
||||||
echo -e " ${BLUE}6)${NC} Plugin Audit - Security scan of plugins"
|
|
||||||
echo -e " ${BLUE}7)${NC} Theme Audit - Security scan of themes"
|
|
||||||
echo -e " ${BLUE}8)${NC} Database Optimizer - Clean/optimize WP databases"
|
|
||||||
echo -e " ${BLUE}9)${NC} Cache Clear (All Sites) - Clear all WP caches"
|
|
||||||
echo -e " ${BLUE}10)${NC} Mass Update Core - Update WordPress core (all)"
|
|
||||||
echo -e " ${BLUE}11)${NC} Mass Update Plugins - Update plugins (all sites)"
|
|
||||||
echo -e " ${BLUE}12)${NC} Login Security Audit - Check for weak passwords"
|
|
||||||
echo -e " ${BLUE}13)${NC} Malware Scanner - Scan for infected files"
|
|
||||||
echo -e " ${BLUE}14)${NC} Permission Fixer - Fix file permissions"
|
|
||||||
echo -e " ${BLUE}15)${NC} Debug Log Analyzer - Parse WP debug logs"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${RED}0)${NC} Back to Main Menu"
|
echo -e " ${RED}0)${NC} Back to Main Menu"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -459,6 +452,68 @@ show_wordpress_menu() {
|
|||||||
echo -n "Select option: "
|
echo -n "Select option: "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# WordPress Health & Maintenance submenu
|
||||||
|
show_wp_health_menu() {
|
||||||
|
show_banner
|
||||||
|
echo -e "${BLUE}${BOLD}🏥 WordPress Health & Maintenance${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLUE}1)${NC} Health Check (All Sites) - Scan all WP installations"
|
||||||
|
echo -e " ${BLUE}2)${NC} Database Optimizer - Clean/optimize WP databases"
|
||||||
|
echo -e " ${BLUE}3)${NC} Cache Clear (All Sites) - Clear all WP caches"
|
||||||
|
echo -e " ${BLUE}4)${NC} Plugin Audit - Security scan of plugins"
|
||||||
|
echo -e " ${BLUE}5)${NC} Theme Audit - Security scan of themes"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${RED}0)${NC} Back to Website Management"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo -n "Select option: "
|
||||||
|
}
|
||||||
|
|
||||||
|
# WP-Cron Management submenu
|
||||||
|
show_wp_cron_menu() {
|
||||||
|
show_banner
|
||||||
|
echo -e "${BLUE}${BOLD}⚙️ WP-Cron Management${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLUE}1)${NC} WP-Cron Status - Check cron job status"
|
||||||
|
echo -e " ${BLUE}2)${NC} WP-Cron Mass Fix - Fix/enable cron on all sites"
|
||||||
|
echo -e " ${BLUE}3)${NC} WP-Cron Mass Create - Setup proper system crons"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${RED}0)${NC} Back to Website Management"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo -n "Select option: "
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mass Updates submenu
|
||||||
|
show_wp_updates_menu() {
|
||||||
|
show_banner
|
||||||
|
echo -e "${BLUE}${BOLD}🔄 WordPress Mass Updates${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLUE}1)${NC} Mass Update Core - Update WordPress core (all)"
|
||||||
|
echo -e " ${BLUE}2)${NC} Mass Update Plugins - Update plugins (all sites)"
|
||||||
|
echo -e " ${BLUE}3)${NC} Mass Update Themes - Update themes (all sites)"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${RED}0)${NC} Back to Website Management"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo -n "Select option: "
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security & Compliance submenu
|
||||||
|
show_wp_security_menu() {
|
||||||
|
show_banner
|
||||||
|
echo -e "${BLUE}${BOLD}🔒 WordPress Security & Compliance${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLUE}1)${NC} Malware Scanner - Scan for infected files"
|
||||||
|
echo -e " ${BLUE}2)${NC} Permission Fixer - Fix file permissions"
|
||||||
|
echo -e " ${BLUE}3)${NC} Login Security Audit - Check for weak passwords"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${RED}0)${NC} Back to Website Management"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo -n "Select option: "
|
||||||
|
}
|
||||||
|
|
||||||
# Performance & Diagnostics menu
|
# Performance & Diagnostics menu
|
||||||
show_performance_menu() {
|
show_performance_menu() {
|
||||||
show_banner
|
show_banner
|
||||||
@@ -504,6 +559,10 @@ show_backup_menu() {
|
|||||||
echo -e " ${YELLOW}7)${NC} Backup Verification - Test backup integrity"
|
echo -e " ${YELLOW}7)${NC} Backup Verification - Test backup integrity"
|
||||||
echo -e " ${YELLOW}8)${NC} Off-site Sync - Sync to remote storage"
|
echo -e " ${YELLOW}8)${NC} Off-site Sync - Sync to remote storage"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo -e "${BOLD}Data Management:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${RED}9)${NC} 🗑️ Cleanup Toolkit Data - Remove IP reputation & temp files"
|
||||||
|
echo ""
|
||||||
echo -e " ${RED}0)${NC} Back to Main Menu"
|
echo -e " ${RED}0)${NC} Back to Main Menu"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
||||||
@@ -929,15 +988,16 @@ handle_bot_analysis_menu() {
|
|||||||
1) run_module "security" "bot-analyzer.sh" ;;
|
1) run_module "security" "bot-analyzer.sh" ;;
|
||||||
2) run_module "security" "bot-analyzer.sh" -H "${QUICK_SCAN_HOURS:-1}" ;;
|
2) run_module "security" "bot-analyzer.sh" -H "${QUICK_SCAN_HOURS:-1}" ;;
|
||||||
3) run_module "security" "live-monitor.sh" ;;
|
3) run_module "security" "live-monitor.sh" ;;
|
||||||
4)
|
4) run_module "security" "ip-reputation-manager.sh" ;;
|
||||||
|
5)
|
||||||
show_banner
|
show_banner
|
||||||
echo -e "${BOLD}IP Lookup & Investigation${NC}"
|
echo -e "${BOLD}IP Lookup & Investigation${NC}"
|
||||||
read -p "Enter IP address: " ip
|
read -p "Enter IP address: " ip
|
||||||
[ -n "$ip" ] && run_module "security" "ip-lookup.sh" "$ip"
|
[ -n "$ip" ] && run_module "security" "ip-lookup.sh" "$ip"
|
||||||
;;
|
;;
|
||||||
5) run_module "security" "ddos-detector.sh" ;;
|
6) run_module "security" "ddos-detector.sh" ;;
|
||||||
6) run_module "security" "traffic-pattern-analysis.sh" ;;
|
7) run_module "security" "traffic-pattern-analysis.sh" ;;
|
||||||
7) run_module "security" "user-agent-analysis.sh" ;;
|
8) run_module "security" "user-agent-analysis.sh" ;;
|
||||||
0) return ;;
|
0) return ;;
|
||||||
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
||||||
esac
|
esac
|
||||||
@@ -1148,20 +1208,78 @@ handle_wordpress_menu() {
|
|||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "website" "website-error-analyzer.sh" ;;
|
1) run_module "website" "website-error-analyzer.sh" ;;
|
||||||
2) run_module "wordpress" "wp-health-check.sh" ;;
|
2) run_module "website" "500-error-tracker.sh" ;;
|
||||||
3) run_module "wordpress" "wp-cron-status.sh" ;;
|
3) run_module "wordpress" "wp-debug-log-analyzer.sh" ;;
|
||||||
4) run_module "wordpress" "wp-cron-mass-fix.sh" ;;
|
4) handle_wp_health_menu ;;
|
||||||
5) run_module "wordpress" "wp-cron-mass-create.sh" ;;
|
5) handle_wp_cron_menu ;;
|
||||||
6) run_module "wordpress" "wp-plugin-audit.sh" ;;
|
6) handle_wp_updates_menu ;;
|
||||||
7) run_module "wordpress" "wp-theme-audit.sh" ;;
|
7) handle_wp_security_menu ;;
|
||||||
8) run_module "wordpress" "wp-db-optimizer.sh" ;;
|
0) return ;;
|
||||||
9) run_module "wordpress" "wp-cache-clear.sh" ;;
|
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
||||||
10) run_module "wordpress" "wp-mass-update-core.sh" ;;
|
esac
|
||||||
11) run_module "wordpress" "wp-mass-update-plugins.sh" ;;
|
done
|
||||||
12) run_module "wordpress" "wp-login-security.sh" ;;
|
}
|
||||||
13) run_module "wordpress" "wp-malware-scanner.sh" ;;
|
|
||||||
14) run_module "wordpress" "wp-permission-fixer.sh" ;;
|
# WP Health & Maintenance submenu handler
|
||||||
15) run_module "wordpress" "wp-debug-log-analyzer.sh" ;;
|
handle_wp_health_menu() {
|
||||||
|
while true; do
|
||||||
|
show_wp_health_menu
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) run_module "wordpress" "wp-health-check.sh" ;;
|
||||||
|
2) run_module "wordpress" "wp-db-optimizer.sh" ;;
|
||||||
|
3) run_module "wordpress" "wp-cache-clear.sh" ;;
|
||||||
|
4) run_module "wordpress" "wp-plugin-audit.sh" ;;
|
||||||
|
5) run_module "wordpress" "wp-theme-audit.sh" ;;
|
||||||
|
0) return ;;
|
||||||
|
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# WP-Cron Management submenu handler
|
||||||
|
handle_wp_cron_menu() {
|
||||||
|
while true; do
|
||||||
|
show_wp_cron_menu
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) run_module "wordpress" "wp-cron-status.sh" ;;
|
||||||
|
2) run_module "wordpress" "wp-cron-mass-fix.sh" ;;
|
||||||
|
3) run_module "wordpress" "wp-cron-mass-create.sh" ;;
|
||||||
|
0) return ;;
|
||||||
|
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mass Updates submenu handler
|
||||||
|
handle_wp_updates_menu() {
|
||||||
|
while true; do
|
||||||
|
show_wp_updates_menu
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) run_module "wordpress" "wp-mass-update-core.sh" ;;
|
||||||
|
2) run_module "wordpress" "wp-mass-update-plugins.sh" ;;
|
||||||
|
3) run_module "wordpress" "wp-mass-update-themes.sh" ;;
|
||||||
|
0) return ;;
|
||||||
|
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security & Compliance submenu handler
|
||||||
|
handle_wp_security_menu() {
|
||||||
|
while true; do
|
||||||
|
show_wp_security_menu
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) run_module "wordpress" "wp-malware-scanner.sh" ;;
|
||||||
|
2) run_module "wordpress" "wp-permission-fixer.sh" ;;
|
||||||
|
3) run_module "wordpress" "wp-login-security.sh" ;;
|
||||||
0) return ;;
|
0) return ;;
|
||||||
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
||||||
esac
|
esac
|
||||||
@@ -1206,6 +1324,7 @@ handle_backup_menu() {
|
|||||||
6) run_module "backup" "log-archive.sh" ;;
|
6) run_module "backup" "log-archive.sh" ;;
|
||||||
7) run_module "backup" "backup-verification.sh" ;;
|
7) run_module "backup" "backup-verification.sh" ;;
|
||||||
8) run_module "backup" "offsite-sync.sh" ;;
|
8) run_module "backup" "offsite-sync.sh" ;;
|
||||||
|
9) run_module "maintenance" "cleanup-toolkit-data.sh" ;;
|
||||||
0) return ;;
|
0) return ;;
|
||||||
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
*) echo -e "${RED}Invalid option${NC}"; sleep 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -0,0 +1,575 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# IP Reputation Management Library
|
||||||
|
################################################################################
|
||||||
|
# Purpose: Centralized IP reputation tracking across all toolkit scripts
|
||||||
|
# Features:
|
||||||
|
# - Fast lookups using indexed file structure
|
||||||
|
# - Tracks: hits, country, last seen, reputation score, attack types
|
||||||
|
# - Optimized for high-volume traffic (attacks with thousands of IPs)
|
||||||
|
# - Automatic cleanup of old entries
|
||||||
|
# - GeoIP integration
|
||||||
|
# - Shared across all monitoring/analysis scripts
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
# Database location
|
||||||
|
IP_REP_DB_DIR="${IP_REP_DB_DIR:-/var/lib/server-toolkit/ip-reputation}"
|
||||||
|
IP_REP_DB="$IP_REP_DB_DIR/ip_database.db"
|
||||||
|
IP_REP_INDEX="$IP_REP_DB_DIR/ip_index.idx"
|
||||||
|
IP_REP_LOCK="$IP_REP_DB_DIR/.db.lock"
|
||||||
|
|
||||||
|
# Reputation score thresholds
|
||||||
|
REP_SCORE_CRITICAL=80 # Definitely malicious
|
||||||
|
REP_SCORE_HIGH=60 # Likely malicious
|
||||||
|
REP_SCORE_MEDIUM=40 # Suspicious
|
||||||
|
REP_SCORE_LOW=20 # Borderline
|
||||||
|
REP_SCORE_SAFE=0 # Safe/legitimate
|
||||||
|
|
||||||
|
# Attack type flags (bitmask for efficient storage)
|
||||||
|
ATTACK_FLAG_SQL_INJECTION=1
|
||||||
|
ATTACK_FLAG_XSS=2
|
||||||
|
ATTACK_FLAG_PATH_TRAVERSAL=4
|
||||||
|
ATTACK_FLAG_RCE=8
|
||||||
|
ATTACK_FLAG_BRUTEFORCE=16
|
||||||
|
ATTACK_FLAG_DDOS=32
|
||||||
|
ATTACK_FLAG_BOT=64
|
||||||
|
ATTACK_FLAG_SCANNER=128
|
||||||
|
ATTACK_FLAG_EXPLOIT=256
|
||||||
|
|
||||||
|
# Initialize the IP reputation database
|
||||||
|
init_ip_reputation_db() {
|
||||||
|
mkdir -p "$IP_REP_DB_DIR" 2>/dev/null
|
||||||
|
|
||||||
|
# Create empty database if it doesn't exist
|
||||||
|
if [ ! -f "$IP_REP_DB" ]; then
|
||||||
|
touch "$IP_REP_DB"
|
||||||
|
chmod 600 "$IP_REP_DB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$IP_REP_INDEX" ]; then
|
||||||
|
touch "$IP_REP_INDEX"
|
||||||
|
chmod 600 "$IP_REP_INDEX"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Database format (pipe-delimited for fast parsing):
|
||||||
|
# IP|HIT_COUNT|REPUTATION_SCORE|COUNTRY|ATTACK_FLAGS|FIRST_SEEN|LAST_SEEN|LAST_ACTIVITY|NOTES
|
||||||
|
# Example:
|
||||||
|
# 192.168.1.100|523|75|US|193|1730000000|1730800000|SQL injection on /admin|Auto-flagged
|
||||||
|
|
||||||
|
# Lock management for concurrent access
|
||||||
|
acquire_lock() {
|
||||||
|
local timeout=10
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
while [ -f "$IP_REP_LOCK" ] && [ $elapsed -lt $timeout ]; do
|
||||||
|
sleep 0.1
|
||||||
|
elapsed=$((elapsed + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $elapsed -ge $timeout ]; then
|
||||||
|
# Stale lock, remove it
|
||||||
|
rm -f "$IP_REP_LOCK" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
touch "$IP_REP_LOCK"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_lock() {
|
||||||
|
rm -f "$IP_REP_LOCK" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fast IP lookup using hash-based index for O(1) lookups
|
||||||
|
# Returns: IP data if found, empty if not found
|
||||||
|
lookup_ip() {
|
||||||
|
local ip="$1"
|
||||||
|
|
||||||
|
[ -z "$ip" ] && return 1
|
||||||
|
[ ! -f "$IP_REP_DB" ] && return 1
|
||||||
|
|
||||||
|
# Calculate hash bucket (first octet for IPv4 distributes IPs across 256 buckets)
|
||||||
|
local hash_bucket="${ip%%.*}"
|
||||||
|
local hash_file="${IP_REP_DB_DIR}/hash_${hash_bucket}.idx"
|
||||||
|
|
||||||
|
# Fast path: Check hash bucket first (much smaller file to grep)
|
||||||
|
if [ -f "$hash_file" ]; then
|
||||||
|
# Hash bucket contains line numbers for IPs in this bucket
|
||||||
|
local line_num=$(grep -m 1 "^${ip}|" "$hash_file" 2>/dev/null | cut -d'|' -f2)
|
||||||
|
if [ -n "$line_num" ]; then
|
||||||
|
# Direct line access - O(1) lookup!
|
||||||
|
sed -n "${line_num}p" "$IP_REP_DB" 2>/dev/null
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: Linear search (for IPs not yet indexed)
|
||||||
|
# Use tac to read file backwards, then grep for first match
|
||||||
|
# This ensures we get the LATEST entry for IPs with duplicates
|
||||||
|
tac "$IP_REP_DB" 2>/dev/null | grep -m 1 "^${ip}|" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add or update IP in database
|
||||||
|
# Usage: update_ip_reputation IP [HIT_INCREMENT] [SCORE_DELTA] [ATTACK_FLAGS] [ACTIVITY_NOTE]
|
||||||
|
update_ip_reputation() {
|
||||||
|
local ip="$1"
|
||||||
|
local hit_increment="${2:-1}"
|
||||||
|
local score_delta="${3:-0}"
|
||||||
|
local new_attack_flags="${4:-0}"
|
||||||
|
local activity_note="${5:-}"
|
||||||
|
|
||||||
|
[ -z "$ip" ] && return 1
|
||||||
|
|
||||||
|
init_ip_reputation_db
|
||||||
|
acquire_lock
|
||||||
|
|
||||||
|
local existing
|
||||||
|
existing=$(lookup_ip "$ip")
|
||||||
|
|
||||||
|
local current_time=$(date +%s)
|
||||||
|
|
||||||
|
if [ -n "$existing" ]; then
|
||||||
|
# Parse existing entry
|
||||||
|
IFS='|' read -r old_ip hit_count rep_score country attack_flags first_seen last_seen last_activity notes <<< "$existing"
|
||||||
|
|
||||||
|
# Update values
|
||||||
|
hit_count=$((hit_count + hit_increment))
|
||||||
|
rep_score=$((rep_score + score_delta))
|
||||||
|
|
||||||
|
# Cap reputation score at 0-100
|
||||||
|
[ $rep_score -lt 0 ] && rep_score=0
|
||||||
|
[ $rep_score -gt 100 ] && rep_score=100
|
||||||
|
|
||||||
|
# Merge attack flags (bitwise OR)
|
||||||
|
attack_flags=$((attack_flags | new_attack_flags))
|
||||||
|
|
||||||
|
last_seen="$current_time"
|
||||||
|
|
||||||
|
# Update activity note if provided
|
||||||
|
if [ -n "$activity_note" ]; then
|
||||||
|
last_activity="$activity_note"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# OPTIMIZATION: Append-only writes (much faster than sed -i delete)
|
||||||
|
# Append updated entry to end of file
|
||||||
|
echo "$ip|$hit_count|$rep_score|$country|$attack_flags|$first_seen|$last_seen|$last_activity|$notes" >> "$IP_REP_DB"
|
||||||
|
|
||||||
|
# Mark for compaction (file will have duplicates until compact_database runs)
|
||||||
|
touch "${IP_REP_DB}.needs_compact" 2>/dev/null
|
||||||
|
else
|
||||||
|
# New entry
|
||||||
|
local country=$(get_ip_country "$ip")
|
||||||
|
echo "$ip|$hit_increment|$score_delta|$country|$new_attack_flags|$current_time|$current_time|$activity_note|" >> "$IP_REP_DB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_lock
|
||||||
|
|
||||||
|
# Auto-compact if file has lots of duplicates (from append-only writes)
|
||||||
|
# Check if compaction is needed (marked file exists)
|
||||||
|
if [ -f "${IP_REP_DB}.needs_compact" ]; then
|
||||||
|
local db_size=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
# Compact if database >50k lines (likely has significant duplicates)
|
||||||
|
# Use random check to avoid all processes compacting simultaneously
|
||||||
|
if [ "$db_size" -gt 50000 ] && [ $((RANDOM % 200)) -eq 0 ]; then
|
||||||
|
compact_database & # Background process (includes rebuild_index)
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rebuild index automatically when database grows significantly
|
||||||
|
# Check if hash index exists and is fresh
|
||||||
|
local db_size=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo "0")
|
||||||
|
local hash_count=$(ls -1 "${IP_REP_DB_DIR}"/hash_*.idx 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
# Rebuild if:
|
||||||
|
# 1. Database has >10k IPs but no hash index exists
|
||||||
|
# 2. Database has >100k IPs and 1% chance (frequent enough during attacks)
|
||||||
|
if [ "$hash_count" -eq 0 ] && [ "$db_size" -gt 10000 ]; then
|
||||||
|
rebuild_index & # Background process
|
||||||
|
elif [ "$db_size" -gt 100000 ] && [ $((RANDOM % 100)) -eq 0 ]; then
|
||||||
|
rebuild_index & # Background process
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get IP country using multiple methods
|
||||||
|
get_ip_country() {
|
||||||
|
local ip="$1"
|
||||||
|
local country="??"
|
||||||
|
|
||||||
|
# Method 1: Check if geoiplookup is available
|
||||||
|
if command -v geoiplookup >/dev/null 2>&1; then
|
||||||
|
country=$(geoiplookup "$ip" 2>/dev/null | grep -oP 'Country Edition: \K[A-Z]{2}' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Method 2: Check if geoiplookup6 for IPv6
|
||||||
|
if [ -z "$country" ] || [ "$country" = "??" ]; then
|
||||||
|
if command -v geoiplookup6 >/dev/null 2>&1 && [[ "$ip" =~ : ]]; then
|
||||||
|
country=$(geoiplookup6 "$ip" 2>/dev/null | grep -oP 'Country Edition: \K[A-Z]{2}' | head -1)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Method 3: Check /usr/share/GeoIP databases directly
|
||||||
|
if [ -z "$country" ] || [ "$country" = "??" ]; then
|
||||||
|
if [ -f "/usr/share/GeoIP/GeoIP.dat" ] && command -v geoiplookup >/dev/null 2>&1; then
|
||||||
|
country=$(geoiplookup "$ip" 2>/dev/null | awk -F': ' '{print $2}' | cut -d',' -f1 | head -1)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Method 4: Fallback - use whois (slower, only if critically needed)
|
||||||
|
# Disabled by default for performance
|
||||||
|
# if [ -z "$country" ] || [ "$country" = "??" ]; then
|
||||||
|
# country=$(whois "$ip" 2>/dev/null | grep -iE "^country:" | head -1 | awk '{print $2}')
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# Default if all methods fail
|
||||||
|
[ -z "$country" ] && country="??"
|
||||||
|
|
||||||
|
echo "$country"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Increment IP hit count (fast path for common case)
|
||||||
|
increment_ip_hits() {
|
||||||
|
local ip="$1"
|
||||||
|
local increment="${2:-1}"
|
||||||
|
|
||||||
|
update_ip_reputation "$ip" "$increment" 0 0 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Flag IP for specific attack type
|
||||||
|
flag_ip_attack() {
|
||||||
|
local ip="$1"
|
||||||
|
local attack_type="$2"
|
||||||
|
local score_increase="${3:-5}"
|
||||||
|
local note="${4:-$attack_type}"
|
||||||
|
|
||||||
|
local attack_flag=0
|
||||||
|
|
||||||
|
case "$attack_type" in
|
||||||
|
SQL_INJECTION|sql) attack_flag=$ATTACK_FLAG_SQL_INJECTION; score_increase=15 ;;
|
||||||
|
XSS|xss) attack_flag=$ATTACK_FLAG_XSS; score_increase=10 ;;
|
||||||
|
PATH_TRAVERSAL|path) attack_flag=$ATTACK_FLAG_PATH_TRAVERSAL; score_increase=12 ;;
|
||||||
|
RCE|rce|shell) attack_flag=$ATTACK_FLAG_RCE; score_increase=20 ;;
|
||||||
|
BRUTEFORCE|brute) attack_flag=$ATTACK_FLAG_BRUTEFORCE; score_increase=8 ;;
|
||||||
|
DDOS|ddos) attack_flag=$ATTACK_FLAG_DDOS; score_increase=10 ;;
|
||||||
|
BOT|bot) attack_flag=$ATTACK_FLAG_BOT; score_increase=3 ;;
|
||||||
|
SCANNER|scan) attack_flag=$ATTACK_FLAG_SCANNER; score_increase=5 ;;
|
||||||
|
EXPLOIT|exploit) attack_flag=$ATTACK_FLAG_EXPLOIT; score_increase=15 ;;
|
||||||
|
*) attack_flag=0; score_increase=5 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
update_ip_reputation "$ip" 1 "$score_increase" "$attack_flag" "$note"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mark IP as legitimate (reduces reputation score)
|
||||||
|
mark_ip_legitimate() {
|
||||||
|
local ip="$1"
|
||||||
|
local note="${2:-Marked as legitimate}"
|
||||||
|
|
||||||
|
update_ip_reputation "$ip" 0 -20 0 "$note"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get IP reputation category
|
||||||
|
get_ip_reputation_category() {
|
||||||
|
local score="$1"
|
||||||
|
|
||||||
|
if [ $score -ge $REP_SCORE_CRITICAL ]; then
|
||||||
|
echo "CRITICAL"
|
||||||
|
elif [ $score -ge $REP_SCORE_HIGH ]; then
|
||||||
|
echo "HIGH"
|
||||||
|
elif [ $score -ge $REP_SCORE_MEDIUM ]; then
|
||||||
|
echo "MEDIUM"
|
||||||
|
elif [ $score -ge $REP_SCORE_LOW ]; then
|
||||||
|
echo "LOW"
|
||||||
|
else
|
||||||
|
echo "SAFE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get attack types from flags
|
||||||
|
decode_attack_flags() {
|
||||||
|
local flags="$1"
|
||||||
|
local attacks=""
|
||||||
|
|
||||||
|
[ $((flags & ATTACK_FLAG_SQL_INJECTION)) -ne 0 ] && attacks="${attacks}SQL,"
|
||||||
|
[ $((flags & ATTACK_FLAG_XSS)) -ne 0 ] && attacks="${attacks}XSS,"
|
||||||
|
[ $((flags & ATTACK_FLAG_PATH_TRAVERSAL)) -ne 0 ] && attacks="${attacks}PATH,"
|
||||||
|
[ $((flags & ATTACK_FLAG_RCE)) -ne 0 ] && attacks="${attacks}RCE,"
|
||||||
|
[ $((flags & ATTACK_FLAG_BRUTEFORCE)) -ne 0 ] && attacks="${attacks}BRUTE,"
|
||||||
|
[ $((flags & ATTACK_FLAG_DDOS)) -ne 0 ] && attacks="${attacks}DDOS,"
|
||||||
|
[ $((flags & ATTACK_FLAG_BOT)) -ne 0 ] && attacks="${attacks}BOT,"
|
||||||
|
[ $((flags & ATTACK_FLAG_SCANNER)) -ne 0 ] && attacks="${attacks}SCAN,"
|
||||||
|
[ $((flags & ATTACK_FLAG_EXPLOIT)) -ne 0 ] && attacks="${attacks}EXPLOIT,"
|
||||||
|
|
||||||
|
# Remove trailing comma
|
||||||
|
attacks="${attacks%,}"
|
||||||
|
|
||||||
|
[ -z "$attacks" ] && attacks="NONE"
|
||||||
|
|
||||||
|
echo "$attacks"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Query and display IP information
|
||||||
|
query_ip_reputation() {
|
||||||
|
local ip="$1"
|
||||||
|
|
||||||
|
init_ip_reputation_db
|
||||||
|
|
||||||
|
local data
|
||||||
|
data=$(lookup_ip "$ip")
|
||||||
|
|
||||||
|
if [ -z "$data" ]; then
|
||||||
|
echo "IP $ip not found in reputation database"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS='|' read -r ip hit_count rep_score country attack_flags first_seen last_seen last_activity notes <<< "$data"
|
||||||
|
|
||||||
|
local category=$(get_ip_reputation_category "$rep_score")
|
||||||
|
local attacks=$(decode_attack_flags "$attack_flags")
|
||||||
|
local first_seen_date=$(date -d "@$first_seen" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$first_seen")
|
||||||
|
local last_seen_date=$(date -d "@$last_seen" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$last_seen")
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "IP Address: $ip"
|
||||||
|
echo "Country: $country"
|
||||||
|
echo "Reputation: $rep_score/100 [$category]"
|
||||||
|
echo "Total Hits: $hit_count"
|
||||||
|
echo "Attack Types: $attacks"
|
||||||
|
echo "First Seen: $first_seen_date"
|
||||||
|
echo "Last Seen: $last_seen_date"
|
||||||
|
echo "Last Activity: ${last_activity:-None recorded}"
|
||||||
|
echo "Notes: ${notes:-None}"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get top IPs by reputation score
|
||||||
|
get_top_malicious_ips() {
|
||||||
|
local limit="${1:-20}"
|
||||||
|
|
||||||
|
init_ip_reputation_db
|
||||||
|
|
||||||
|
[ ! -f "$IP_REP_DB" ] && return 1
|
||||||
|
|
||||||
|
# OPTIMIZATION: For large files, use partial sort (much faster)
|
||||||
|
# Only sort enough to find top N instead of sorting entire file
|
||||||
|
local db_size=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$db_size" -gt 100000 ]; then
|
||||||
|
# For very large databases, use awk to find high-scoring IPs first
|
||||||
|
# then sort only those (much faster than sorting 500k lines)
|
||||||
|
awk -F'|' '$3 >= 50' "$IP_REP_DB" | sort -t'|' -k3 -rn | head -n "$limit"
|
||||||
|
else
|
||||||
|
# For smaller databases, regular sort is fine
|
||||||
|
sort -t'|' -k3 -rn "$IP_REP_DB" | head -n "$limit"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get top IPs by hit count
|
||||||
|
get_top_active_ips() {
|
||||||
|
local limit="${1:-20}"
|
||||||
|
|
||||||
|
init_ip_reputation_db
|
||||||
|
|
||||||
|
[ ! -f "$IP_REP_DB" ] && return 1
|
||||||
|
|
||||||
|
# OPTIMIZATION: For large files, filter first then sort
|
||||||
|
local db_size=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$db_size" -gt 100000 ]; then
|
||||||
|
# Filter to IPs with >100 hits, then sort (much faster)
|
||||||
|
awk -F'|' '$2 >= 100' "$IP_REP_DB" | sort -t'|' -k2 -rn | head -n "$limit"
|
||||||
|
else
|
||||||
|
# For smaller databases, regular sort is fine
|
||||||
|
sort -t'|' -k2 -rn "$IP_REP_DB" | head -n "$limit"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up old entries (not seen in X days)
|
||||||
|
cleanup_old_ips() {
|
||||||
|
local days_old="${1:-90}"
|
||||||
|
|
||||||
|
init_ip_reputation_db
|
||||||
|
acquire_lock
|
||||||
|
|
||||||
|
local cutoff_time=$(($(date +%s) - (days_old * 86400)))
|
||||||
|
local temp_file="${IP_REP_DB}.tmp"
|
||||||
|
|
||||||
|
# Keep only IPs seen within the cutoff time
|
||||||
|
awk -F'|' -v cutoff="$cutoff_time" '$7 >= cutoff' "$IP_REP_DB" > "$temp_file"
|
||||||
|
|
||||||
|
mv "$temp_file" "$IP_REP_DB"
|
||||||
|
|
||||||
|
release_lock
|
||||||
|
|
||||||
|
echo "Cleaned up IPs not seen in $days_old days"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compact database to remove duplicate IP entries (from append-only writes)
|
||||||
|
compact_database() {
|
||||||
|
init_ip_reputation_db
|
||||||
|
acquire_lock
|
||||||
|
|
||||||
|
echo "Compacting database (removing duplicate IP entries)..."
|
||||||
|
|
||||||
|
local temp_db="${IP_REP_DB}.compact_tmp"
|
||||||
|
local original_size=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
# Use awk to keep only the LAST occurrence of each IP (most recent data)
|
||||||
|
# Read file backwards, keep first occurrence of each IP, then reverse again
|
||||||
|
tac "$IP_REP_DB" | awk -F'|' '!seen[$1]++' | tac > "$temp_db"
|
||||||
|
|
||||||
|
# Replace original with compacted version
|
||||||
|
mv "$temp_db" "$IP_REP_DB"
|
||||||
|
|
||||||
|
local new_size=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo "0")
|
||||||
|
local removed=$((original_size - new_size))
|
||||||
|
|
||||||
|
# Remove compaction marker
|
||||||
|
rm -f "${IP_REP_DB}.needs_compact" 2>/dev/null
|
||||||
|
|
||||||
|
release_lock
|
||||||
|
|
||||||
|
echo "Compaction complete: Removed $removed duplicate entries ($original_size → $new_size IPs)"
|
||||||
|
|
||||||
|
# Rebuild index after compaction
|
||||||
|
rebuild_index
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rebuild index for faster lookups (for very large databases)
|
||||||
|
rebuild_index() {
|
||||||
|
init_ip_reputation_db
|
||||||
|
acquire_lock
|
||||||
|
|
||||||
|
echo "Rebuilding hash-based index for fast lookups..."
|
||||||
|
|
||||||
|
# Remove old hash files
|
||||||
|
rm -f "${IP_REP_DB_DIR}"/hash_*.idx 2>/dev/null
|
||||||
|
|
||||||
|
# Build hash buckets (256 buckets based on first octet)
|
||||||
|
# This distributes 500k IPs into ~2k IPs per bucket = MUCH faster
|
||||||
|
local line_num=0
|
||||||
|
while IFS='|' read -r ip rest; do
|
||||||
|
((line_num++))
|
||||||
|
|
||||||
|
# Calculate hash bucket from first octet
|
||||||
|
local hash_bucket="${ip%%.*}"
|
||||||
|
local hash_file="${IP_REP_DB_DIR}/hash_${hash_bucket}.idx"
|
||||||
|
|
||||||
|
# Store IP and its line number in the hash bucket file
|
||||||
|
echo "${ip}|${line_num}" >> "$hash_file"
|
||||||
|
done < "$IP_REP_DB"
|
||||||
|
|
||||||
|
# Sort each hash bucket file for faster grep
|
||||||
|
for hash_file in "${IP_REP_DB_DIR}"/hash_*.idx; do
|
||||||
|
[ -f "$hash_file" ] && sort -t'|' -k1 -o "$hash_file" "$hash_file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Also create main sorted index for compatibility
|
||||||
|
sort -t'|' -k1 "$IP_REP_DB" > "$IP_REP_INDEX"
|
||||||
|
|
||||||
|
release_lock
|
||||||
|
|
||||||
|
echo "Index rebuilt: $(ls -1 "${IP_REP_DB_DIR}"/hash_*.idx 2>/dev/null | wc -l) hash buckets created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export reputation database to readable format
|
||||||
|
export_ip_reputation() {
|
||||||
|
local output_file="${1:-/tmp/ip_reputation_export_$(date +%Y%m%d_%H%M%S).txt}"
|
||||||
|
|
||||||
|
init_ip_reputation_db
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "SERVER TOOLKIT - IP REPUTATION DATABASE EXPORT"
|
||||||
|
echo "Generated: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
echo "Total IPs: $(wc -l < "$IP_REP_DB" 2>/dev/null || echo 0)"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
printf "%-15s | %-7s | %-4s | %-8s | %-6s | %-30s | %-19s\n" \
|
||||||
|
"IP ADDRESS" "HITS" "CTRY" "REP" "LEVEL" "ATTACKS" "LAST SEEN"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# Sort by reputation score, descending
|
||||||
|
sort -t'|' -k3 -rn "$IP_REP_DB" | while IFS='|' read -r ip hit_count rep_score country attack_flags first_seen last_seen last_activity notes; do
|
||||||
|
local category=$(get_ip_reputation_category "$rep_score")
|
||||||
|
local attacks=$(decode_attack_flags "$attack_flags")
|
||||||
|
local last_seen_date=$(date -d "@$last_seen" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$last_seen")
|
||||||
|
|
||||||
|
printf "%-15s | %-7s | %-4s | %-3s/100 | %-8s | %-30s | %-19s\n" \
|
||||||
|
"$ip" "$hit_count" "$country" "$rep_score" "$category" "${attacks:0:30}" "$last_seen_date"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
} > "$output_file"
|
||||||
|
|
||||||
|
echo "IP reputation database exported to: $output_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if IP should be blocked (based on reputation)
|
||||||
|
should_block_ip() {
|
||||||
|
local ip="$1"
|
||||||
|
local threshold="${2:-$REP_SCORE_HIGH}" # Default: block if reputation >= 60
|
||||||
|
|
||||||
|
local data
|
||||||
|
data=$(lookup_ip "$ip")
|
||||||
|
|
||||||
|
[ -z "$data" ] && return 1 # Unknown IP, don't block
|
||||||
|
|
||||||
|
IFS='|' read -r _ _ rep_score _ _ _ _ _ _ <<< "$data"
|
||||||
|
|
||||||
|
[ $rep_score -ge $threshold ] && return 0 # Should block
|
||||||
|
return 1 # Should not block
|
||||||
|
}
|
||||||
|
|
||||||
|
# Batch import IPs from various sources
|
||||||
|
import_ips_from_log() {
|
||||||
|
local log_file="$1"
|
||||||
|
local attack_type="${2:-SUSPICIOUS}"
|
||||||
|
local score_per_hit="${3:-5}"
|
||||||
|
|
||||||
|
[ ! -f "$log_file" ] && return 1
|
||||||
|
|
||||||
|
# Extract IPs and count occurrences
|
||||||
|
grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' "$log_file" | \
|
||||||
|
sort | uniq -c | while read count ip; do
|
||||||
|
update_ip_reputation "$ip" "$count" "$score_per_hit" 0 "Imported from $log_file"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Imported IPs from $log_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Statistics summary
|
||||||
|
show_ip_statistics() {
|
||||||
|
init_ip_reputation_db
|
||||||
|
|
||||||
|
local total_ips=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo 0)
|
||||||
|
local critical=$(awk -F'|' -v thresh=$REP_SCORE_CRITICAL '$3 >= thresh' "$IP_REP_DB" 2>/dev/null | wc -l)
|
||||||
|
local high=$(awk -F'|' -v low=$REP_SCORE_HIGH -v hi=$REP_SCORE_CRITICAL '$3 >= low && $3 < hi' "$IP_REP_DB" 2>/dev/null | wc -l)
|
||||||
|
local medium=$(awk -F'|' -v low=$REP_SCORE_MEDIUM -v hi=$REP_SCORE_HIGH '$3 >= low && $3 < hi' "$IP_REP_DB" 2>/dev/null | wc -l)
|
||||||
|
local low=$(awk -F'|' -v low=$REP_SCORE_LOW -v hi=$REP_SCORE_MEDIUM '$3 >= low && $3 < hi' "$IP_REP_DB" 2>/dev/null | wc -l)
|
||||||
|
local safe=$(awk -F'|' -v thresh=$REP_SCORE_LOW '$3 < thresh' "$IP_REP_DB" 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "IP REPUTATION DATABASE STATISTICS"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Total Tracked IPs: $total_ips"
|
||||||
|
echo ""
|
||||||
|
echo "By Reputation Level:"
|
||||||
|
echo " CRITICAL (≥80): $critical"
|
||||||
|
echo " HIGH (60-79): $high"
|
||||||
|
echo " MEDIUM (40-59): $medium"
|
||||||
|
echo " LOW (20-39): $low"
|
||||||
|
echo " SAFE (<20): $safe"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize on library load
|
||||||
|
init_ip_reputation_db
|
||||||
Executable
+243
@@ -0,0 +1,243 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Server Toolkit Data Cleanup
|
||||||
|
################################################################################
|
||||||
|
# Purpose: Remove all toolkit-generated data (for wiping before system transfer)
|
||||||
|
# Use Case: When moving toolkit to another server or fresh start
|
||||||
|
#
|
||||||
|
# What gets cleaned:
|
||||||
|
# - IP reputation database
|
||||||
|
# - Temporary analysis files
|
||||||
|
# - Cached data
|
||||||
|
# - Generated reports
|
||||||
|
# - Session data
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
||||||
|
|
||||||
|
# Require root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
print_error "This script must be run as root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_banner "Server Toolkit Data Cleanup"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}${BOLD}⚠️ WARNING ⚠️${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "This will remove ALL data collected by the Server Toolkit:"
|
||||||
|
echo ""
|
||||||
|
echo " • IP reputation database (/var/lib/server-toolkit/)"
|
||||||
|
echo " • Temporary analysis files (/tmp/)"
|
||||||
|
echo " • Generated reports"
|
||||||
|
echo " • Cached data"
|
||||||
|
echo " • Session files"
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}This action CANNOT be undone!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Use this when:"
|
||||||
|
echo " ✓ Moving toolkit to a different server"
|
||||||
|
echo " ✓ Starting fresh analysis"
|
||||||
|
echo " ✓ Removing server-specific data before sharing"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}────────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo ""
|
||||||
|
read -p "Type 'yes' to confirm cleanup: " confirm
|
||||||
|
|
||||||
|
if [ "$confirm" != "yes" ]; then
|
||||||
|
echo ""
|
||||||
|
print_error "Cleanup cancelled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Starting cleanup..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Track what was cleaned
|
||||||
|
cleaned_count=0
|
||||||
|
cleaned_size=0
|
||||||
|
|
||||||
|
# Function to safely remove directory/file and track size
|
||||||
|
safe_remove() {
|
||||||
|
local path="$1"
|
||||||
|
local description="$2"
|
||||||
|
|
||||||
|
if [ -e "$path" ]; then
|
||||||
|
# Calculate size before removing
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
size=$(du -sb "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
else
|
||||||
|
size=$(stat -c%s "$path" 2>/dev/null || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove
|
||||||
|
rm -rf "$path" 2>/dev/null
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
cleaned_size=$((cleaned_size + size))
|
||||||
|
((cleaned_count++))
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: $description"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e " ${RED}✗${NC} Failed to remove: $description"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e " ${DIM}○${NC} Not found: $description (already clean)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo -e "${BOLD}IP Reputation Database:${NC}"
|
||||||
|
safe_remove "/var/lib/server-toolkit/ip-reputation" "IP reputation database (including hash index)"
|
||||||
|
safe_remove "/var/lib/server-toolkit" "Toolkit data directory"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BOLD}Temporary Analysis Files:${NC}"
|
||||||
|
# Bot analyzer temp files
|
||||||
|
for pattern in /tmp/bot_analysis_* /tmp/*_bot_*.txt; do
|
||||||
|
if ls $pattern 2>/dev/null | grep -q .; then
|
||||||
|
rm -f $pattern 2>/dev/null
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: Bot analysis temp files"
|
||||||
|
((cleaned_count++))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 500 error tracker temp files
|
||||||
|
for pattern in /tmp/500-tracker-* /tmp/*500*.txt; do
|
||||||
|
if ls $pattern 2>/dev/null | grep -q .; then
|
||||||
|
rm -rf $pattern 2>/dev/null
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: 500 error tracker temp files"
|
||||||
|
((cleaned_count++))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Live monitoring temp files
|
||||||
|
for pattern in /tmp/live-monitor-* /tmp/*monitor*.tmp; do
|
||||||
|
if ls $pattern 2>/dev/null | grep -q .; then
|
||||||
|
rm -rf $pattern 2>/dev/null
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: Live monitoring temp files"
|
||||||
|
((cleaned_count++))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Error analyzer temp files
|
||||||
|
for pattern in /tmp/error_analysis_* /tmp/*error*.tmp; do
|
||||||
|
if ls $pattern 2>/dev/null | grep -q .; then
|
||||||
|
rm -f $pattern 2>/dev/null
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: Error analyzer temp files"
|
||||||
|
((cleaned_count++))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generic toolkit temp files
|
||||||
|
for pattern in /tmp/toolkit_* /tmp/server-toolkit*; do
|
||||||
|
if ls $pattern 2>/dev/null | grep -q .; then
|
||||||
|
rm -rf $pattern 2>/dev/null
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: Generic toolkit temp files"
|
||||||
|
((cleaned_count++))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BOLD}Generated Reports:${NC}"
|
||||||
|
# Look for common report locations
|
||||||
|
for pattern in /tmp/*_report_*.txt /tmp/*_analysis_*.txt /root/*toolkit*.txt /root/*_report*.txt; do
|
||||||
|
if ls $pattern 2>/dev/null | grep -q .; then
|
||||||
|
count=$(ls $pattern 2>/dev/null | wc -l)
|
||||||
|
rm -f $pattern 2>/dev/null
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: $count report file(s)"
|
||||||
|
((cleaned_count++))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BOLD}Cache and Session Data:${NC}"
|
||||||
|
# Cached analysis data
|
||||||
|
if [ -d "/var/cache/server-toolkit" ]; then
|
||||||
|
safe_remove "/var/cache/server-toolkit" "Toolkit cache directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Session/lock files
|
||||||
|
for pattern in /var/run/server-toolkit* /var/lock/server-toolkit*; do
|
||||||
|
if ls $pattern 2>/dev/null | grep -q .; then
|
||||||
|
rm -f $pattern 2>/dev/null
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: Session/lock files"
|
||||||
|
((cleaned_count++))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BOLD}Log Files (Optional):${NC}"
|
||||||
|
echo -n "Remove toolkit execution logs? (yes/no) [no]: "
|
||||||
|
read remove_logs
|
||||||
|
remove_logs="${remove_logs:-no}"
|
||||||
|
|
||||||
|
if [ "$remove_logs" = "yes" ]; then
|
||||||
|
for pattern in /var/log/server-toolkit*.log; do
|
||||||
|
if ls $pattern 2>/dev/null | grep -q .; then
|
||||||
|
count=$(ls $pattern 2>/dev/null | wc -l)
|
||||||
|
rm -f $pattern 2>/dev/null
|
||||||
|
echo -e " ${GREEN}✓${NC} Removed: $count log file(s)"
|
||||||
|
((cleaned_count++))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo -e " ${DIM}○${NC} Logs kept (skipped)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}────────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Convert size to human readable
|
||||||
|
if [ $cleaned_size -lt 1024 ]; then
|
||||||
|
size_human="${cleaned_size}B"
|
||||||
|
elif [ $cleaned_size -lt 1048576 ]; then
|
||||||
|
size_human="$((cleaned_size / 1024))KB"
|
||||||
|
elif [ $cleaned_size -lt 1073741824 ]; then
|
||||||
|
size_human="$((cleaned_size / 1048576))MB"
|
||||||
|
else
|
||||||
|
size_human="$((cleaned_size / 1073741824))GB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}${BOLD}✓ Cleanup Complete!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Summary:"
|
||||||
|
echo " Items removed: $cleaned_count"
|
||||||
|
echo " Space freed: $size_human"
|
||||||
|
echo ""
|
||||||
|
echo "The toolkit is now clean and ready for:"
|
||||||
|
echo " • Transfer to another server"
|
||||||
|
echo " • Fresh analysis start"
|
||||||
|
echo " • Sharing without server-specific data"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify critical directories are gone
|
||||||
|
missing=0
|
||||||
|
[ -d "/var/lib/server-toolkit" ] && { echo -e "${YELLOW}Warning: /var/lib/server-toolkit still exists${NC}"; ((missing++)); }
|
||||||
|
[ -d "/tmp/live-monitor-current" ] && { echo -e "${YELLOW}Warning: /tmp/live-monitor-current still exists${NC}"; ((missing++)); }
|
||||||
|
|
||||||
|
if [ $missing -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Some directories could not be removed (may be in use)${NC}"
|
||||||
|
echo "Try stopping any running toolkit scripts and run cleanup again."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
@@ -27,6 +27,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|||||||
source "$SCRIPT_DIR/lib/common-functions.sh"
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
||||||
source "$SCRIPT_DIR/lib/system-detect.sh"
|
source "$SCRIPT_DIR/lib/system-detect.sh"
|
||||||
source "$SCRIPT_DIR/lib/user-manager.sh"
|
source "$SCRIPT_DIR/lib/user-manager.sh"
|
||||||
|
source "$SCRIPT_DIR/lib/ip-reputation.sh"
|
||||||
|
|
||||||
# Default configuration (auto-detected from system)
|
# Default configuration (auto-detected from system)
|
||||||
LOG_DIR="${SYS_LOG_DIR:-/var/log/apache2/domlogs}"
|
LOG_DIR="${SYS_LOG_DIR:-/var/log/apache2/domlogs}"
|
||||||
@@ -925,9 +926,29 @@ calculate_threat_scores() {
|
|||||||
|
|
||||||
# Only output IPs with score > 0
|
# Only output IPs with score > 0
|
||||||
[ $score -gt 0 ] && echo "$score|$ip|$req_count"
|
[ $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"
|
done | sort -t'|' -k1 -rn > "$TEMP_DIR/threat_scores.txt"
|
||||||
|
|
||||||
print_success "Threat scores calculated"
|
# Wait for background IP reputation updates to complete
|
||||||
|
wait
|
||||||
|
|
||||||
|
print_success "Threat scores calculated and IP reputation updated"
|
||||||
}
|
}
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|||||||
Executable
+494
@@ -0,0 +1,494 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# IP Reputation Manager
|
||||||
|
################################################################################
|
||||||
|
# Purpose: View, query, and manage the centralized IP reputation database
|
||||||
|
# Features:
|
||||||
|
# - Query individual IPs
|
||||||
|
# - View top malicious IPs
|
||||||
|
# - View top active IPs
|
||||||
|
# - Export database
|
||||||
|
# - Database statistics
|
||||||
|
# - Cleanup old entries
|
||||||
|
# - Manual IP flagging/whitelisting
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
# Get script directory
|
||||||
|
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/ip-reputation.sh"
|
||||||
|
|
||||||
|
# Require root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
print_error "This script must be run as root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Menu display
|
||||||
|
show_menu() {
|
||||||
|
clear
|
||||||
|
print_banner "IP Reputation Manager"
|
||||||
|
|
||||||
|
# Show quick stats
|
||||||
|
local total_ips=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo 0)
|
||||||
|
local db_size=$(du -h "$IP_REP_DB" 2>/dev/null | awk '{print $1}' || echo "0B")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}${BOLD}Database Status:${NC} $total_ips IPs tracked | Size: $db_size"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Query & View:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${GREEN}1)${NC} Query IP Reputation - Look up specific IP"
|
||||||
|
echo -e " ${GREEN}2)${NC} Top Malicious IPs - Highest reputation scores"
|
||||||
|
echo -e " ${GREEN}3)${NC} Top Active IPs - Most hits/requests"
|
||||||
|
echo -e " ${GREEN}4)${NC} Database Statistics - Overview of tracked IPs"
|
||||||
|
echo -e " ${GREEN}5)${NC} Live Monitoring - Real-time reputation updates"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Database Management:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BLUE}6)${NC} Export Database - Export to readable text file"
|
||||||
|
echo -e " ${BLUE}7)${NC} Cleanup Old Entries - Remove IPs not seen in X days"
|
||||||
|
echo -e " ${BLUE}8)${NC} Compact Database - Remove duplicate entries (faster writes)"
|
||||||
|
echo -e " ${BLUE}9)${NC} Rebuild Index - Optimize database for speed"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Manual Actions:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}10)${NC} Flag IP as Malicious - Manually mark IP as threat"
|
||||||
|
echo -e " ${YELLOW}11)${NC} Mark IP as Legitimate - Whitelist/reduce score"
|
||||||
|
echo -e " ${YELLOW}12)${NC} Import IPs from Log - Batch import from file"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${RED}0)${NC} Exit"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo -n "Select option: "
|
||||||
|
}
|
||||||
|
|
||||||
|
# Query individual IP
|
||||||
|
query_ip_interactive() {
|
||||||
|
clear
|
||||||
|
print_banner "Query IP Reputation"
|
||||||
|
echo ""
|
||||||
|
echo -n "Enter IP address to query: "
|
||||||
|
read -r ip_address
|
||||||
|
|
||||||
|
if [ -z "$ip_address" ]; then
|
||||||
|
print_error "No IP address provided"
|
||||||
|
press_enter
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate IP format (basic check)
|
||||||
|
if ! [[ "$ip_address" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||||
|
print_error "Invalid IP address format"
|
||||||
|
press_enter
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
query_ip_reputation "$ip_address"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# View top malicious IPs
|
||||||
|
view_top_malicious() {
|
||||||
|
clear
|
||||||
|
print_banner "Top Malicious IPs"
|
||||||
|
echo ""
|
||||||
|
echo -n "How many top IPs to show? [20]: "
|
||||||
|
read -r limit
|
||||||
|
limit="${limit:-20}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}${BOLD}Top $limit Most Malicious IPs (by Reputation Score)${NC}"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
printf "%-15s | %-7s | %-4s | %-8s | %-8s | %-30s\n" \
|
||||||
|
"IP ADDRESS" "HITS" "CTRY" "REP" "LEVEL" "ATTACK TYPES"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
get_top_malicious_ips "$limit" | while IFS='|' read -r ip hit_count rep_score country attack_flags first_seen last_seen last_activity notes; do
|
||||||
|
local category=$(get_ip_reputation_category "$rep_score")
|
||||||
|
local attacks=$(decode_attack_flags "$attack_flags")
|
||||||
|
|
||||||
|
# Color code by reputation
|
||||||
|
local color="$NC"
|
||||||
|
case "$category" in
|
||||||
|
CRITICAL) color="$RED$BOLD" ;;
|
||||||
|
HIGH) color="$RED" ;;
|
||||||
|
MEDIUM) color="$YELLOW" ;;
|
||||||
|
LOW) color="$CYAN" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf "${color}%-15s | %-7s | %-4s | %-3s/100 | %-8s | %-30s${NC}\n" \
|
||||||
|
"$ip" "$hit_count" "$country" "$rep_score" "$category" "${attacks:0:30}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# View top active IPs
|
||||||
|
view_top_active() {
|
||||||
|
clear
|
||||||
|
print_banner "Top Active IPs"
|
||||||
|
echo ""
|
||||||
|
echo -n "How many top IPs to show? [20]: "
|
||||||
|
read -r limit
|
||||||
|
limit="${limit:-20}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}${BOLD}Top $limit Most Active IPs (by Hit Count)${NC}"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
printf "%-15s | %-7s | %-4s | %-8s | %-8s | %-30s\n" \
|
||||||
|
"IP ADDRESS" "HITS" "CTRY" "REP" "LEVEL" "ATTACK TYPES"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
get_top_active_ips "$limit" | while IFS='|' read -r ip hit_count rep_score country attack_flags first_seen last_seen last_activity notes; do
|
||||||
|
local category=$(get_ip_reputation_category "$rep_score")
|
||||||
|
local attacks=$(decode_attack_flags "$attack_flags")
|
||||||
|
|
||||||
|
# Color code by hit count
|
||||||
|
local color="$NC"
|
||||||
|
if [ $hit_count -gt 10000 ]; then
|
||||||
|
color="$RED$BOLD"
|
||||||
|
elif [ $hit_count -gt 1000 ]; then
|
||||||
|
color="$YELLOW"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "${color}%-15s | %-7s | %-4s | %-3s/100 | %-8s | %-30s${NC}\n" \
|
||||||
|
"$ip" "$hit_count" "$country" "$rep_score" "$category" "${attacks:0:30}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show statistics
|
||||||
|
view_statistics() {
|
||||||
|
clear
|
||||||
|
print_banner "Database Statistics"
|
||||||
|
echo ""
|
||||||
|
show_ip_statistics
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Additional stats
|
||||||
|
echo "Recent Activity (Last 24 hours):"
|
||||||
|
local cutoff=$(($(date +%s) - 86400))
|
||||||
|
local recent_count=$(awk -F'|' -v cut="$cutoff" '$7 >= cut' "$IP_REP_DB" 2>/dev/null | wc -l)
|
||||||
|
echo " Active IPs: $recent_count"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Top countries
|
||||||
|
echo "Top Countries by IP Count:"
|
||||||
|
awk -F'|' '{print $4}' "$IP_REP_DB" 2>/dev/null | grep -v '^$' | sort | uniq -c | sort -rn | head -5 | \
|
||||||
|
while read count country; do
|
||||||
|
printf " %-4s: %s IPs\n" "$country" "$count"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export database
|
||||||
|
export_database_interactive() {
|
||||||
|
clear
|
||||||
|
print_banner "Export IP Reputation Database"
|
||||||
|
echo ""
|
||||||
|
echo -n "Enter output file path [/tmp/ip_reputation_export.txt]: "
|
||||||
|
read -r output_path
|
||||||
|
output_path="${output_path:-/tmp/ip_reputation_export.txt}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Exporting database to $output_path..."
|
||||||
|
export_ip_reputation "$output_path"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_success "Database exported successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "View with: cat $output_path"
|
||||||
|
echo "Or: less $output_path"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup old entries
|
||||||
|
cleanup_database_interactive() {
|
||||||
|
clear
|
||||||
|
print_banner "Cleanup Old Entries"
|
||||||
|
echo ""
|
||||||
|
echo "Remove IPs that haven't been seen in how many days?"
|
||||||
|
echo ""
|
||||||
|
echo -n "Days [90]: "
|
||||||
|
read -r days
|
||||||
|
days="${days:-90}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "This will remove IPs not seen in the last $days days."
|
||||||
|
echo -n "Continue? (yes/no): "
|
||||||
|
read -r confirm
|
||||||
|
|
||||||
|
if [ "$confirm" != "yes" ]; then
|
||||||
|
echo "Cancelled"
|
||||||
|
press_enter
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
cleanup_old_ips "$days"
|
||||||
|
echo ""
|
||||||
|
print_success "Cleanup complete!"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compact database
|
||||||
|
compact_database_interactive() {
|
||||||
|
clear
|
||||||
|
print_banner "Compact Database"
|
||||||
|
echo ""
|
||||||
|
local total_before=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo 0)
|
||||||
|
echo "Current database size: $total_before entries"
|
||||||
|
echo ""
|
||||||
|
echo "This will remove duplicate IP entries created by fast append-only writes."
|
||||||
|
echo "The database will be compacted and re-indexed."
|
||||||
|
echo ""
|
||||||
|
echo -n "Continue? (yes/no): "
|
||||||
|
read -r confirm
|
||||||
|
|
||||||
|
if [ "$confirm" != "yes" ]; then
|
||||||
|
echo "Cancelled"
|
||||||
|
press_enter
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
compact_database
|
||||||
|
echo ""
|
||||||
|
print_success "Database compacted successfully!"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rebuild index
|
||||||
|
rebuild_index_interactive() {
|
||||||
|
clear
|
||||||
|
print_banner "Rebuild Database Index"
|
||||||
|
echo ""
|
||||||
|
echo "Rebuilding index for optimized lookups..."
|
||||||
|
rebuild_index
|
||||||
|
echo ""
|
||||||
|
print_success "Index rebuilt successfully!"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Flag IP as malicious
|
||||||
|
flag_ip_interactive() {
|
||||||
|
clear
|
||||||
|
print_banner "Flag IP as Malicious"
|
||||||
|
echo ""
|
||||||
|
echo -n "Enter IP address: "
|
||||||
|
read -r ip_address
|
||||||
|
|
||||||
|
if [ -z "$ip_address" ]; then
|
||||||
|
print_error "No IP address provided"
|
||||||
|
press_enter
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Attack Type:"
|
||||||
|
echo " 1) SQL Injection"
|
||||||
|
echo " 2) XSS"
|
||||||
|
echo " 3) Path Traversal"
|
||||||
|
echo " 4) RCE/Shell Upload"
|
||||||
|
echo " 5) Brute Force"
|
||||||
|
echo " 6) DDoS"
|
||||||
|
echo " 7) Bot/Scanner"
|
||||||
|
echo " 8) Exploit"
|
||||||
|
echo ""
|
||||||
|
echo -n "Select [1]: "
|
||||||
|
read -r attack_choice
|
||||||
|
attack_choice="${attack_choice:-1}"
|
||||||
|
|
||||||
|
case "$attack_choice" in
|
||||||
|
1) attack_type="SQL_INJECTION" ;;
|
||||||
|
2) attack_type="XSS" ;;
|
||||||
|
3) attack_type="PATH_TRAVERSAL" ;;
|
||||||
|
4) attack_type="RCE" ;;
|
||||||
|
5) attack_type="BRUTEFORCE" ;;
|
||||||
|
6) attack_type="DDOS" ;;
|
||||||
|
7) attack_type="SCANNER" ;;
|
||||||
|
8) attack_type="EXPLOIT" ;;
|
||||||
|
*) attack_type="SUSPICIOUS" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -n "Notes/Description: "
|
||||||
|
read -r notes
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Flagging $ip_address for $attack_type..."
|
||||||
|
flag_ip_attack "$ip_address" "$attack_type" 0 "$notes"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_success "IP flagged successfully!"
|
||||||
|
echo ""
|
||||||
|
query_ip_reputation "$ip_address"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mark IP as legitimate
|
||||||
|
whitelist_ip_interactive() {
|
||||||
|
clear
|
||||||
|
print_banner "Mark IP as Legitimate"
|
||||||
|
echo ""
|
||||||
|
echo -n "Enter IP address: "
|
||||||
|
read -r ip_address
|
||||||
|
|
||||||
|
if [ -z "$ip_address" ]; then
|
||||||
|
print_error "No IP address provided"
|
||||||
|
press_enter
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -n "Reason/Notes: "
|
||||||
|
read -r notes
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Marking $ip_address as legitimate..."
|
||||||
|
mark_ip_legitimate "$ip_address" "$notes"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_success "IP marked as legitimate!"
|
||||||
|
echo ""
|
||||||
|
query_ip_reputation "$ip_address"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Import from log file
|
||||||
|
import_log_interactive() {
|
||||||
|
clear
|
||||||
|
print_banner "Import IPs from Log File"
|
||||||
|
echo ""
|
||||||
|
echo -n "Enter log file path: "
|
||||||
|
read -r log_path
|
||||||
|
|
||||||
|
if [ ! -f "$log_path" ]; then
|
||||||
|
print_error "File not found: $log_path"
|
||||||
|
press_enter
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Attack Type (will be applied to all IPs):"
|
||||||
|
echo " 1) Suspicious (default)"
|
||||||
|
echo " 2) SQL Injection"
|
||||||
|
echo " 3) XSS"
|
||||||
|
echo " 4) Bot/Scanner"
|
||||||
|
echo " 5) DDoS"
|
||||||
|
echo ""
|
||||||
|
echo -n "Select [1]: "
|
||||||
|
read -r attack_choice
|
||||||
|
attack_choice="${attack_choice:-1}"
|
||||||
|
|
||||||
|
case "$attack_choice" in
|
||||||
|
2) attack_type="SQL_INJECTION" ;;
|
||||||
|
3) attack_type="XSS" ;;
|
||||||
|
4) attack_type="SCANNER" ;;
|
||||||
|
5) attack_type="DDOS" ;;
|
||||||
|
*) attack_type="SUSPICIOUS" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Importing IPs from $log_path as $attack_type..."
|
||||||
|
import_ips_from_log "$log_path" "$attack_type" 5
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_success "Import complete!"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# Live monitoring (real-time updates)
|
||||||
|
live_monitoring() {
|
||||||
|
clear
|
||||||
|
print_banner "Live IP Reputation Monitoring"
|
||||||
|
echo ""
|
||||||
|
echo "Watching database for changes... (Press Ctrl+C to exit)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local last_count=0
|
||||||
|
local last_update=0
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local current_count=$(wc -l < "$IP_REP_DB" 2>/dev/null || echo 0)
|
||||||
|
local current_time=$(stat -c %Y "$IP_REP_DB" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ $current_count -ne $last_count ] || [ $current_time -ne $last_update ]; then
|
||||||
|
clear
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "LIVE IP REPUTATION MONITORING - $(date '+%H:%M:%S')"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
show_ip_statistics
|
||||||
|
echo ""
|
||||||
|
echo "Recent Top 10 Updates:"
|
||||||
|
get_top_malicious_ips 10 | head -10 | while IFS='|' read -r ip hit_count rep_score country attack_flags _ _ _ _; do
|
||||||
|
local category=$(get_ip_reputation_category "$rep_score")
|
||||||
|
local attacks=$(decode_attack_flags "$attack_flags")
|
||||||
|
printf "%-15s | %5s hits | %3s/100 | %-8s | %s\n" "$ip" "$hit_count" "$rep_score" "$category" "${attacks:0:40}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "Press Ctrl+C to exit | Refreshing every 2 seconds..."
|
||||||
|
|
||||||
|
last_count=$current_count
|
||||||
|
last_update=$current_time
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main loop
|
||||||
|
main() {
|
||||||
|
while true; do
|
||||||
|
show_menu
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) query_ip_interactive ;;
|
||||||
|
2) view_top_malicious ;;
|
||||||
|
3) view_top_active ;;
|
||||||
|
4) view_statistics ;;
|
||||||
|
5) live_monitoring ;;
|
||||||
|
6) export_database_interactive ;;
|
||||||
|
7) cleanup_database_interactive ;;
|
||||||
|
8) compact_database_interactive ;;
|
||||||
|
9) rebuild_index_interactive ;;
|
||||||
|
10) flag_ip_interactive ;;
|
||||||
|
11) whitelist_ip_interactive ;;
|
||||||
|
12) import_log_interactive ;;
|
||||||
|
0)
|
||||||
|
clear
|
||||||
|
echo "Exiting..."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Invalid option"
|
||||||
|
sleep 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main
|
||||||
|
main
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
source "$SCRIPT_DIR/lib/common-functions.sh"
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
||||||
source "$SCRIPT_DIR/lib/system-detect.sh"
|
source "$SCRIPT_DIR/lib/system-detect.sh"
|
||||||
|
source "$SCRIPT_DIR/lib/ip-reputation.sh"
|
||||||
|
|
||||||
# Require root
|
# Require root
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
@@ -341,6 +342,21 @@ process_threat_event() {
|
|||||||
local threat_level=$(classify_threat_level "${IP_COUNTER[$ip]}")
|
local threat_level=$(classify_threat_level "${IP_COUNTER[$ip]}")
|
||||||
IP_THREAT_LEVEL[$ip]="$threat_level"
|
IP_THREAT_LEVEL[$ip]="$threat_level"
|
||||||
|
|
||||||
|
# Track in centralized IP reputation database
|
||||||
|
# Map attack types to reputation flags
|
||||||
|
local rep_attack_type="SUSPICIOUS"
|
||||||
|
case "$attack_type" in
|
||||||
|
SSH_BRUTEFORCE) rep_attack_type="BRUTEFORCE" ;;
|
||||||
|
SQL_INJECTION) rep_attack_type="SQL_INJECTION" ;;
|
||||||
|
XSS_ATTACK) rep_attack_type="XSS" ;;
|
||||||
|
PATH_TRAVERSAL) rep_attack_type="PATH_TRAVERSAL" ;;
|
||||||
|
EXPLOIT) rep_attack_type="EXPLOIT" ;;
|
||||||
|
DDOS) rep_attack_type="DDOS" ;;
|
||||||
|
BOT) rep_attack_type="BOT" ;;
|
||||||
|
*) rep_attack_type="SCANNER" ;;
|
||||||
|
esac
|
||||||
|
flag_ip_attack "$ip" "$rep_attack_type" 0 "$attack_type: $details" >/dev/null 2>&1 &
|
||||||
|
|
||||||
# Log to feed
|
# Log to feed
|
||||||
log_event "$ip" "$attack_type" "$(get_threat_color "$threat_level")" "$details"
|
log_event "$ip" "$attack_type" "$(get_threat_color "$threat_level")" "$details"
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+839
@@ -0,0 +1,839 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Fast 500 Error Tracker
|
||||||
|
################################################################################
|
||||||
|
# Purpose: ONLY track 500 errors - find them fast and show WHY
|
||||||
|
# No menus, no options - just find 500s and diagnose the root cause
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
||||||
|
source "$SCRIPT_DIR/lib/ip-reputation.sh"
|
||||||
|
|
||||||
|
# Ensure color variables are set
|
||||||
|
DIM='\033[2m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
|
||||||
|
print_banner "Fast 500 Error Tracker"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Scanning for 500 errors and diagnosing root causes..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Ask for time range
|
||||||
|
echo -e "${CYAN}How far back to scan?${NC}"
|
||||||
|
echo " 1) Last 24 hours (default)"
|
||||||
|
echo " 2) Last 7 days"
|
||||||
|
echo " 3) Last 30 days"
|
||||||
|
echo ""
|
||||||
|
read -p "Select option [1]: " time_choice
|
||||||
|
time_choice=${time_choice:-1}
|
||||||
|
|
||||||
|
case $time_choice in
|
||||||
|
1) HOURS_TO_SCAN=24 ;;
|
||||||
|
2) HOURS_TO_SCAN=168 ;;
|
||||||
|
3) HOURS_TO_SCAN=720 ;;
|
||||||
|
*) HOURS_TO_SCAN=24 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "→ Scanning last $HOURS_TO_SCAN hours of access logs..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
TEMP_DIR="/tmp/500-tracker-$$"
|
||||||
|
mkdir -p "$TEMP_DIR"
|
||||||
|
trap "rm -rf $TEMP_DIR" EXIT
|
||||||
|
|
||||||
|
ERRORS_500="$TEMP_DIR/errors_500.txt"
|
||||||
|
ERROR_DETAILS="$TEMP_DIR/error_details.txt"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DOMLOGS_DIR="/var/log/apache2/domlogs"
|
||||||
|
|
||||||
|
# Get cutoff time
|
||||||
|
cutoff_time=$(date -d "$HOURS_TO_SCAN hours ago" +%s 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
declare -A domain_count
|
||||||
|
declare -A domain_user
|
||||||
|
total_500s=0
|
||||||
|
filtered_bots=0
|
||||||
|
|
||||||
|
# Scan all domain access logs for 500 errors (including subdirectories)
|
||||||
|
while IFS= read -r log; do
|
||||||
|
[ -f "$log" ] || continue
|
||||||
|
[[ "$log" =~ (bytes_log|offset|error_log|ftpxferlog|-ssl_log)$ ]] && continue
|
||||||
|
|
||||||
|
# Extract domain from log filename
|
||||||
|
domain="${log##*/}"
|
||||||
|
domain="${domain%%-*}"
|
||||||
|
|
||||||
|
# Skip non-domain system logs (proxy, localhost, etc.)
|
||||||
|
[[ "$domain" =~ ^(proxy|localhost|default|cpanel|webmail|whm|cpcalendars|cpcontacts|webdisk)$ ]] && continue
|
||||||
|
|
||||||
|
# Find cPanel user for this domain
|
||||||
|
user=$(grep -l "DNS.*$domain" /var/cpanel/users/* 2>/dev/null | head -1 | xargs basename 2>/dev/null)
|
||||||
|
[ -z "$user" ] && user="unknown"
|
||||||
|
|
||||||
|
domain_user["$domain"]="$user"
|
||||||
|
|
||||||
|
# Scan for 500 errors in this log
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Check for 500 status code
|
||||||
|
if [[ "$line" =~ '"'[[:space:]](5[0-9]{2})[[:space:]] ]]; then
|
||||||
|
status="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
|
# Time filtering
|
||||||
|
if [ "$cutoff_time" != "0" ]; then
|
||||||
|
if [[ "$line" =~ \[([0-9]{2}/[A-Z][a-z]{2}/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2}) ]]; then
|
||||||
|
log_date="${BASH_REMATCH[1]}"
|
||||||
|
log_time=$(date -d "${log_date/:/ }" +%s 2>/dev/null || echo "0")
|
||||||
|
[ "$log_time" != "0" ] && [ "$log_time" -lt "$cutoff_time" ] && continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse log line to get IP first
|
||||||
|
read -r ip _ _ timestamp _ request _ _ <<< "$line"
|
||||||
|
|
||||||
|
# IP-based filtering - skip non-relevant IPs
|
||||||
|
# Skip localhost/internal IPs
|
||||||
|
if [[ "$ip" =~ ^(127\.|::1|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then
|
||||||
|
((filtered_bots++))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Skip common cloud scanner IPs and known bot networks
|
||||||
|
# AWS, GCP, Azure scanner ranges, security scanners
|
||||||
|
if [[ "$ip" =~ ^(3\.|13\.|18\.|34\.|35\.|52\.|54\.) ]] || \
|
||||||
|
[[ "$ip" =~ ^(104\.196\.|104\.154\.|130\.211\.|35\.184\.|35\.185\.|35\.186\.|35\.187\.|35\.188\.|35\.189\.|35\.19) ]] || \
|
||||||
|
[[ "$ip" =~ ^(20\.|40\.|51\.|52\.|13\.64\.|13\.65\.|13\.66\.|13\.67\.|13\.68\.) ]]; then
|
||||||
|
# Check if it's actually a bot by examining user agent
|
||||||
|
line_lower="${line,,}"
|
||||||
|
if [[ "$line_lower" =~ (bot|crawler|spider|scanner|monitor|cloud|amazon|google|microsoft|azure) ]]; then
|
||||||
|
((filtered_bots++))
|
||||||
|
# Track bot IP with BOT flag
|
||||||
|
flag_ip_attack "$ip" "BOT" 0 "500 error - filtered as bot/scanner" >/dev/null 2>&1 &
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bot/Scanner filtering - skip noise
|
||||||
|
line_lower="${line,,}"
|
||||||
|
if [[ "$line_lower" =~ (bot|crawler|spider|scraper|scanner|check|monitor|uptime|pingdom|newrelic|datadog|nagios|zabbix|prtg|gomez|keynote|catchpoint|dotcom-monitor|site24x7|uptimerobot|statuscake|nodequery|hetrixtools|freshping|uptrendscom|siteuptime|montastic|updown\.io|apex|alertsite|webmon|wormly) ]]; then
|
||||||
|
((filtered_bots++))
|
||||||
|
# Track monitoring/uptime bot
|
||||||
|
flag_ip_attack "$ip" "BOT" 0 "Monitoring/uptime bot" >/dev/null 2>&1 &
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$line_lower" =~ (semrush|ahrefs|moz|majestic|serpstat|screaming|screamingfrog|sitebulb|linkchecker|validator|scanner|security|acunetix|nessus|openvas|burp|nikto|skipfish|w3af|sqlmap|metasploit|nmap|masscan|zmap|shodan|censys|binaryedge) ]]; then
|
||||||
|
((filtered_bots++))
|
||||||
|
# Track scanner with higher score
|
||||||
|
flag_ip_attack "$ip" "SCANNER" 0 "Security scanner detected" >/dev/null 2>&1 &
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$line_lower" =~ (curl|wget|python|perl|ruby|java|go-http|libwww|axios|node-fetch|http\.client|httpie|postman|insomnia|apachehttp|okhttp|httpclient) ]]; then
|
||||||
|
((filtered_bots++))
|
||||||
|
# Track programmatic access
|
||||||
|
flag_ip_attack "$ip" "BOT" 0 "HTTP library/tool" >/dev/null 2>&1 &
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract URL
|
||||||
|
if [[ "$request" =~ '"'[A-Z]+[[:space:]]([^[:space:]]+) ]]; then
|
||||||
|
url="${BASH_REMATCH[1]:0:80}"
|
||||||
|
else
|
||||||
|
url="/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract timestamp
|
||||||
|
if [[ "$line" =~ \[([^]]+)\] ]]; then
|
||||||
|
timestamp="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
timestamp="unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
((domain_count["$domain"]++))
|
||||||
|
((total_500s++))
|
||||||
|
|
||||||
|
# Track IP in reputation database (500 error = slight increase in score)
|
||||||
|
# Most 500s are due to server issues, not attacks, so low score increase
|
||||||
|
increment_ip_hits "$ip" 1 >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
# Save for analysis
|
||||||
|
echo "$domain|$user|$status|$url|$timestamp|$ip" >> "$ERRORS_500"
|
||||||
|
fi
|
||||||
|
done < <(tail -n 100000 "$log" 2>/dev/null)
|
||||||
|
done < <(find "$DOMLOGS_DIR" -type f ! -name "*bytes_log" ! -name "*offset*" ! -name "*error_log" ! -name "*ftpxferlog*" ! -name "*-ssl_log" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$total_500s" -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${GREEN} ✓ NO 500 ERRORS FOUND IN LAST $HOURS_TO_SCAN HOURS!${NC}"
|
||||||
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " 500 ERRORS DETECTED: $total_500s errors (filtered out $filtered_bots bot/scanner requests)"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${RED}${BOLD}TOP AFFECTED DOMAINS:${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Sort and display top domains
|
||||||
|
for domain in "${!domain_count[@]}"; do
|
||||||
|
echo "${domain_count[$domain]} $domain ${domain_user[$domain]}"
|
||||||
|
done | sort -rn | head -10 | while read count domain user; do
|
||||||
|
printf " ${RED}●${NC} %-40s ${BOLD}%4d errors${NC} (user: %s)\n" "$domain" "$count" "$user"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " DIAGNOSING ROOT CAUSES"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Detailed diagnostic tracking - per URL/file
|
||||||
|
DETAILED_DIAGNOSIS="$TEMP_DIR/detailed_diagnosis.txt"
|
||||||
|
> "$DETAILED_DIAGNOSIS"
|
||||||
|
|
||||||
|
declare -A diagnosed_causes
|
||||||
|
declare -A cause_examples
|
||||||
|
|
||||||
|
total_to_diagnose=$(wc -l < "$ERRORS_500")
|
||||||
|
current_line=0
|
||||||
|
|
||||||
|
echo "Analyzing $total_to_diagnose errors for root causes..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
while IFS='|' read -r domain user status url timestamp ip; do
|
||||||
|
[ -z "$domain" ] && continue
|
||||||
|
|
||||||
|
((current_line++))
|
||||||
|
# Show progress every 500 errors
|
||||||
|
if [ $((current_line % 500)) -eq 0 ]; then
|
||||||
|
echo -ne "\rAnalyzed $current_line / $total_to_diagnose errors..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
diagnosis=""
|
||||||
|
cause="UNKNOWN"
|
||||||
|
specific_file=""
|
||||||
|
|
||||||
|
# Get document root
|
||||||
|
docroot="/home/$user/public_html"
|
||||||
|
|
||||||
|
# Try to determine the actual file being requested
|
||||||
|
if [[ "$url" =~ ^/([^?]+) ]]; then
|
||||||
|
url_path="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
|
# Handle common patterns
|
||||||
|
if [ -z "$url_path" ] || [ "$url_path" = "/" ]; then
|
||||||
|
possible_files=("$docroot/index.php" "$docroot/index.html")
|
||||||
|
elif [[ "$url_path" =~ \.php$ ]]; then
|
||||||
|
possible_files=("$docroot/$url_path")
|
||||||
|
else
|
||||||
|
possible_files=("$docroot/$url_path" "$docroot/${url_path}.php" "$docroot/$url_path/index.php")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the actual file
|
||||||
|
for pf in "${possible_files[@]}"; do
|
||||||
|
if [ -f "$pf" ]; then
|
||||||
|
specific_file="$pf"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find PHP error log - check multiple locations
|
||||||
|
error_log=""
|
||||||
|
if [ "$user" != "unknown" ]; then
|
||||||
|
for potential_log in \
|
||||||
|
"/home/$user/public_html/error_log" \
|
||||||
|
"/home/$user/logs/error_log" \
|
||||||
|
"/home/$user/error_log" \
|
||||||
|
"/var/log/apache2/domlogs/$domain-error_log" \
|
||||||
|
"/usr/local/apache/domlogs/$domain"; do
|
||||||
|
|
||||||
|
if [ -f "$potential_log" ]; then
|
||||||
|
error_log="$potential_log"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if error log exists and has recent errors
|
||||||
|
if [ -n "$error_log" ] && [ -f "$error_log" ]; then
|
||||||
|
# Look for errors matching this URL/timestamp
|
||||||
|
recent_error=$(tail -1000 "$error_log" | grep -F "$url" | tail -1)
|
||||||
|
|
||||||
|
# If no URL match, get most recent error
|
||||||
|
[ -z "$recent_error" ] && recent_error=$(tail -500 "$error_log" | grep -E "Fatal error|Parse error|syntax error|memory.*exhausted|database|MySQL|Permission denied|failed to open stream" | tail -1)
|
||||||
|
|
||||||
|
if [ -n "$recent_error" ]; then
|
||||||
|
# Extract file path from error if present
|
||||||
|
if [[ "$recent_error" =~ in[[:space:]](/[^:]+\.php) ]]; then
|
||||||
|
error_file="${BASH_REMATCH[1]}"
|
||||||
|
specific_file="$error_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine cause and diagnose
|
||||||
|
if [[ "$recent_error" =~ (memory.*exhausted|Allowed memory size) ]]; then
|
||||||
|
cause="PHP_MEMORY_EXHAUSTED"
|
||||||
|
|
||||||
|
# Check current memory limit
|
||||||
|
mem_limit=$(grep -h "memory_limit" "$docroot/.user.ini" "$docroot/../.php.ini" 2>/dev/null | tail -1 | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
[ -z "$mem_limit" ] && mem_limit=$(php -r "echo ini_get('memory_limit');" 2>/dev/null)
|
||||||
|
|
||||||
|
diagnosis="$domain$url - Memory exhausted (current limit: ${mem_limit:-unknown})"
|
||||||
|
[ -n "$specific_file" ] && diagnosis="$diagnosis - File: $specific_file"
|
||||||
|
|
||||||
|
elif [[ "$recent_error" =~ (failed to open stream|Permission denied) ]]; then
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
|
||||||
|
# Extract the file with permission issue
|
||||||
|
if [[ "$recent_error" =~ failed\ to\ open\ stream.*\'([^\']+)\' ]]; then
|
||||||
|
perm_file="${BASH_REMATCH[1]}"
|
||||||
|
elif [[ "$recent_error" =~ Permission\ denied.*\'([^\']+)\' ]]; then
|
||||||
|
perm_file="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
perm_file="$specific_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check permissions on that specific file
|
||||||
|
if [ -n "$perm_file" ] && [ -e "$perm_file" ]; then
|
||||||
|
file_perms=$(stat -c "%a" "$perm_file" 2>/dev/null)
|
||||||
|
file_owner=$(stat -c "%U:%G" "$perm_file" 2>/dev/null)
|
||||||
|
diagnosis="$domain$url - Permission denied on: $perm_file (perms: $file_perms, owner: $file_owner)"
|
||||||
|
|
||||||
|
# Check if file is readable by Apache
|
||||||
|
if [ -f "$perm_file" ]; then
|
||||||
|
if ! sudo -u nobody test -r "$perm_file" 2>/dev/null; then
|
||||||
|
diagnosis="$diagnosis - File NOT readable by Apache (nobody user)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
diagnosis="$domain$url - Permission denied (file in error: $perm_file - does not exist)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$recent_error" =~ (Fatal error|PHP Fatal error) ]]; then
|
||||||
|
if [[ "$recent_error" =~ (undefined function|Call to undefined function) ]]; then
|
||||||
|
cause="MISSING_PHP_FUNCTION"
|
||||||
|
|
||||||
|
# Extract function name
|
||||||
|
if [[ "$recent_error" =~ Call\ to\ undefined\ function\ ([a-zA-Z0-9_]+) ]]; then
|
||||||
|
missing_func="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
|
# Check which extension provides this function
|
||||||
|
ext_info=$(php -r "if(function_exists('$missing_func')) echo 'EXISTS'; else echo 'MISSING';" 2>&1)
|
||||||
|
|
||||||
|
diagnosis="$domain$url - Missing function: $missing_func"
|
||||||
|
[ -n "$specific_file" ] && diagnosis="$diagnosis - in file: $specific_file"
|
||||||
|
else
|
||||||
|
diagnosis="$domain$url - Missing PHP function - $recent_error"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
cause="PHP_FATAL_ERROR"
|
||||||
|
diagnosis="$domain$url - PHP Fatal Error"
|
||||||
|
[ -n "$specific_file" ] && diagnosis="$diagnosis - File: $specific_file"
|
||||||
|
diagnosis="$diagnosis - ${recent_error:0:200}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$recent_error" =~ (Parse error|syntax error) ]]; then
|
||||||
|
cause="PHP_SYNTAX_ERROR"
|
||||||
|
|
||||||
|
# Extract line number if present
|
||||||
|
if [[ "$recent_error" =~ line\ ([0-9]+) ]]; then
|
||||||
|
error_line="${BASH_REMATCH[1]}"
|
||||||
|
diagnosis="$domain$url - PHP Syntax Error at line $error_line"
|
||||||
|
else
|
||||||
|
diagnosis="$domain$url - PHP Syntax Error"
|
||||||
|
fi
|
||||||
|
[ -n "$specific_file" ] && diagnosis="$diagnosis - File: $specific_file"
|
||||||
|
|
||||||
|
elif [[ "$recent_error" =~ (database|MySQL|Can\'t connect|Access denied for user) ]]; then
|
||||||
|
cause="DATABASE_CONNECTION"
|
||||||
|
|
||||||
|
if [[ "$recent_error" =~ Access\ denied\ for\ user\ \'([^\']+)\' ]]; then
|
||||||
|
db_user="${BASH_REMATCH[1]}"
|
||||||
|
diagnosis="$domain$url - Database access denied for user: $db_user"
|
||||||
|
elif [[ "$recent_error" =~ Unknown\ database\ \'([^\']+)\' ]]; then
|
||||||
|
db_name="${BASH_REMATCH[1]}"
|
||||||
|
diagnosis="$domain$url - Unknown database: $db_name"
|
||||||
|
else
|
||||||
|
diagnosis="$domain$url - Database connection error"
|
||||||
|
fi
|
||||||
|
[ -n "$specific_file" ] && diagnosis="$diagnosis - from file: $specific_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save detailed diagnosis only if we identified a specific cause
|
||||||
|
if [ "$cause" != "UNKNOWN" ] && [ -n "$diagnosis" ]; then
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
else
|
||||||
|
# No matching error in error_log - run comprehensive checks
|
||||||
|
if [ "$user" != "unknown" ]; then
|
||||||
|
issue_found=""
|
||||||
|
|
||||||
|
# Check 1: .htaccess issues
|
||||||
|
if [ -f "$docroot/.htaccess" ]; then
|
||||||
|
htaccess_file="$docroot/.htaccess"
|
||||||
|
htaccess_issues=""
|
||||||
|
|
||||||
|
# Invalid PHP directives
|
||||||
|
if grep -qE "^[[:space:]]*(php_value|php_flag|php_admin_value|php_admin_flag)" "$htaccess_file" 2>/dev/null; then
|
||||||
|
if ! apache2ctl -M 2>/dev/null | grep -q "php.*module"; then
|
||||||
|
htaccess_issues="PHP directives incompatible with FPM"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Malformed RewriteRule
|
||||||
|
bad_rewrite=$(grep -nE "RewriteRule.*\[.*[^]]*$" "$htaccess_file" 2>/dev/null | head -1)
|
||||||
|
[ -n "$bad_rewrite" ] && htaccess_issues="${htaccess_issues:+$htaccess_issues; }Malformed RewriteRule: ${bad_rewrite:0:50}"
|
||||||
|
|
||||||
|
# Missing RewriteBase with RewriteRule
|
||||||
|
if grep -q "RewriteRule" "$htaccess_file" 2>/dev/null; then
|
||||||
|
if ! grep -q "RewriteBase" "$htaccess_file" 2>/dev/null; then
|
||||||
|
# Check if rules use relative paths
|
||||||
|
if grep -qE "RewriteRule.*\^[^/]" "$htaccess_file" 2>/dev/null; then
|
||||||
|
htaccess_issues="${htaccess_issues:+$htaccess_issues; }Missing RewriteBase with relative paths"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$htaccess_issues" ]; then
|
||||||
|
cause="HTACCESS_ERROR"
|
||||||
|
diagnosis="$domain$url - $htaccess_issues"
|
||||||
|
issue_found="yes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 2: PHP file exists and is readable
|
||||||
|
if [ -z "$issue_found" ] && [ -n "$specific_file" ]; then
|
||||||
|
if [ ! -f "$specific_file" ]; then
|
||||||
|
cause="FILE_NOT_FOUND"
|
||||||
|
diagnosis="$domain$url - File does not exist: $specific_file"
|
||||||
|
issue_found="yes"
|
||||||
|
elif [ ! -r "$specific_file" ]; then
|
||||||
|
file_perms=$(stat -c "%a" "$specific_file" 2>/dev/null)
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
diagnosis="$domain$url - File not readable: $specific_file (perms: $file_perms)"
|
||||||
|
issue_found="yes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 3: Document root permissions
|
||||||
|
if [ -z "$issue_found" ]; then
|
||||||
|
if [ ! -d "$docroot" ]; then
|
||||||
|
cause="DOCROOT_MISSING"
|
||||||
|
diagnosis="$domain$url - Document root does not exist: $docroot"
|
||||||
|
issue_found="yes"
|
||||||
|
else
|
||||||
|
docroot_perms=$(stat -c "%a" "$docroot" 2>/dev/null)
|
||||||
|
if [ "$docroot_perms" != "755" ] && [ "$docroot_perms" != "750" ] && [ "$docroot_perms" != "711" ]; then
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
diagnosis="$domain$url - Docroot bad perms: $docroot ($docroot_perms, should be 755)"
|
||||||
|
issue_found="yes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 4: PHP handler/version issues
|
||||||
|
if [ -z "$issue_found" ] && [ -n "$specific_file" ] && [ -f "$specific_file" ]; then
|
||||||
|
# Check if PHP is configured for this domain
|
||||||
|
php_handler=$(grep -i "phpversion\|php_admin_value" /var/cpanel/userdata/$user/$domain 2>/dev/null | head -1)
|
||||||
|
if [ -z "$php_handler" ]; then
|
||||||
|
# Check if .htaccess tries to set PHP version but fails
|
||||||
|
if grep -qE "AddHandler.*php|SetHandler.*php" "$docroot/.htaccess" 2>/dev/null; then
|
||||||
|
cause="PHP_HANDLER_ERROR"
|
||||||
|
diagnosis="$domain$url - PHP handler misconfigured (check .htaccess AddHandler/SetHandler)"
|
||||||
|
issue_found="yes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 5: WordPress-specific issues (if WP detected)
|
||||||
|
if [ -z "$issue_found" ] && [ -f "$docroot/wp-config.php" ]; then
|
||||||
|
# Check if wp-config.php is readable
|
||||||
|
if [ ! -r "$docroot/wp-config.php" ]; then
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
diagnosis="$domain$url - wp-config.php not readable"
|
||||||
|
issue_found="yes"
|
||||||
|
else
|
||||||
|
# Check for WP_DEBUG causing issues
|
||||||
|
if grep -q "define.*WP_DEBUG.*true" "$docroot/wp-config.php" 2>/dev/null; then
|
||||||
|
# Check if WP_DEBUG_DISPLAY is also true (can cause 500s with some errors)
|
||||||
|
if grep -q "define.*WP_DEBUG_DISPLAY.*true" "$docroot/wp-config.php" 2>/dev/null; then
|
||||||
|
cause="WP_DEBUG_ERROR"
|
||||||
|
diagnosis="$domain$url - WP_DEBUG_DISPLAY enabled (may cause 500 on warnings)"
|
||||||
|
issue_found="yes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save diagnosis if we found an issue
|
||||||
|
if [ -n "$issue_found" ] && [ -n "$diagnosis" ]; then
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# No error in error_log at all - check .htaccess
|
||||||
|
if [ "$user" != "unknown" ] && [ -f "$docroot/.htaccess" ]; then
|
||||||
|
htaccess_file="$docroot/.htaccess"
|
||||||
|
htaccess_issues=""
|
||||||
|
|
||||||
|
# Check for invalid PHP directives
|
||||||
|
if grep -qE "^[[:space:]]*(php_value|php_flag|php_admin_value|php_admin_flag)" "$htaccess_file" 2>/dev/null; then
|
||||||
|
if ! apache2ctl -M 2>/dev/null | grep -q "php.*module"; then
|
||||||
|
htaccess_issues="PHP directives (php_value/php_flag) incompatible with current PHP handler (not mod_php)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for malformed RewriteRule
|
||||||
|
bad_rewrite=$(grep -nE "RewriteRule.*\[.*[^]]*$" "$htaccess_file" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$bad_rewrite" ]; then
|
||||||
|
htaccess_issues="${htaccess_issues:+$htaccess_issues; }Malformed RewriteRule: $bad_rewrite"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$htaccess_issues" ]; then
|
||||||
|
cause="HTACCESS_ERROR"
|
||||||
|
diagnosis="$domain$url - .htaccess error: $htaccess_issues"
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# No error log found - check .htaccess and permissions thoroughly
|
||||||
|
if [ "$user" != "unknown" ]; then
|
||||||
|
htaccess_file="$docroot/.htaccess"
|
||||||
|
|
||||||
|
if [ -f "$htaccess_file" ]; then
|
||||||
|
# Check .htaccess file permissions first
|
||||||
|
htaccess_perms=$(stat -c "%a" "$htaccess_file" 2>/dev/null)
|
||||||
|
htaccess_owner=$(stat -c "%U:%G" "$htaccess_file" 2>/dev/null)
|
||||||
|
|
||||||
|
# Check if readable by Apache
|
||||||
|
htaccess_readable="yes"
|
||||||
|
if ! sudo -u nobody test -r "$htaccess_file" 2>/dev/null; then
|
||||||
|
htaccess_readable="no"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Actually validate .htaccess syntax
|
||||||
|
htaccess_test=$(apache2ctl -t 2>&1)
|
||||||
|
htaccess_issues=""
|
||||||
|
|
||||||
|
# Check for invalid directives
|
||||||
|
if grep -qE "^[[:space:]]*(php_value|php_flag|php_admin_value|php_admin_flag)" "$htaccess_file" 2>/dev/null; then
|
||||||
|
if ! apache2ctl -M 2>/dev/null | grep -q "php.*module"; then
|
||||||
|
htaccess_issues="PHP directives (php_value/php_flag) incompatible with current PHP handler (not mod_php)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for malformed RewriteRule
|
||||||
|
bad_rewrite=$(grep -nE "RewriteRule.*\[.*[^]]*$" "$htaccess_file" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$bad_rewrite" ]; then
|
||||||
|
htaccess_issues="${htaccess_issues:+$htaccess_issues; }Malformed RewriteRule: $bad_rewrite"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for RewriteCond without following RewriteRule
|
||||||
|
orphan_cond=$(grep -n "RewriteCond" "$htaccess_file" | while read line; do
|
||||||
|
linenum=$(echo "$line" | cut -d: -f1)
|
||||||
|
nextline=$((linenum + 1))
|
||||||
|
next=$(sed -n "${nextline}p" "$htaccess_file")
|
||||||
|
if [[ ! "$next" =~ RewriteRule|RewriteCond ]]; then
|
||||||
|
echo "Line $linenum: RewriteCond without RewriteRule"
|
||||||
|
fi
|
||||||
|
done | head -1)
|
||||||
|
|
||||||
|
if [ -n "$orphan_cond" ]; then
|
||||||
|
htaccess_issues="${htaccess_issues:+$htaccess_issues; }$orphan_cond"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for syntax errors in .htaccess
|
||||||
|
if echo "$htaccess_test" | grep -qi "syntax error"; then
|
||||||
|
syntax_err=$(echo "$htaccess_test" | grep -i "syntax error" | head -1)
|
||||||
|
htaccess_issues="${htaccess_issues:+$htaccess_issues; }Apache syntax error: $syntax_err"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$htaccess_readable" = "no" ]; then
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
diagnosis="$domain$url - .htaccess not readable by Apache (perms: $htaccess_perms, owner: $htaccess_owner)"
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
elif [ -n "$htaccess_issues" ]; then
|
||||||
|
cause="HTACCESS_ERROR"
|
||||||
|
diagnosis="$domain$url - .htaccess error: $htaccess_issues"
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
else
|
||||||
|
# .htaccess appears valid - check document root and file permissions
|
||||||
|
docroot_perms=$(stat -c "%a" "$docroot" 2>/dev/null)
|
||||||
|
docroot_owner=$(stat -c "%U:%G" "$docroot" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$docroot_perms" != "755" ] && [ "$docroot_perms" != "750" ]; then
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
diagnosis="$domain$url - Document root incorrect permissions (perms: $docroot_perms, owner: $docroot_owner, should be 755)"
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
elif [ -n "$specific_file" ] && [ -f "$specific_file" ]; then
|
||||||
|
# Check the specific PHP file permissions
|
||||||
|
file_perms=$(stat -c "%a" "$specific_file" 2>/dev/null)
|
||||||
|
file_owner=$(stat -c "%U:%G" "$specific_file" 2>/dev/null)
|
||||||
|
file_readable="yes"
|
||||||
|
|
||||||
|
if ! sudo -u nobody test -r "$specific_file" 2>/dev/null; then
|
||||||
|
file_readable="no"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$file_readable" = "no" ]; then
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
diagnosis="$domain$url - File not readable by Apache: $specific_file (perms: $file_perms, owner: $file_owner)"
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_PHP_ERROR_LOGGED"]++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# No .htaccess - check document root and file permissions
|
||||||
|
docroot_perms=$(stat -c "%a" "$docroot" 2>/dev/null)
|
||||||
|
docroot_owner=$(stat -c "%U:%G" "$docroot" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$docroot_perms" != "755" ] && [ "$docroot_perms" != "750" ]; then
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
diagnosis="$domain$url - Document root incorrect permissions (perms: $docroot_perms, owner: $docroot_owner, should be 755)"
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
elif [ -n "$specific_file" ] && [ -f "$specific_file" ]; then
|
||||||
|
# Check the specific PHP file
|
||||||
|
file_perms=$(stat -c "%a" "$specific_file" 2>/dev/null)
|
||||||
|
file_owner=$(stat -c "%U:%G" "$specific_file" 2>/dev/null)
|
||||||
|
|
||||||
|
if ! sudo -u nobody test -r "$specific_file" 2>/dev/null; then
|
||||||
|
cause="PERMISSION_ERROR"
|
||||||
|
diagnosis="$domain$url - File not readable: $specific_file (perms: $file_perms, owner: $file_owner)"
|
||||||
|
echo "$cause|$diagnosis" >> "$DETAILED_DIAGNOSIS"
|
||||||
|
((diagnosed_causes["$cause"]++))
|
||||||
|
[ -z "${cause_examples[$cause]}" ] && cause_examples["$cause"]="$diagnosis"
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_ERROR_LOG_FILE"]++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_ERROR_LOG_FILE"]++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
((diagnosed_causes["NO_ERROR_LOG_FILE"]++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$ERRORS_500"
|
||||||
|
|
||||||
|
echo -e "\rAnalyzed $total_to_diagnose / $total_to_diagnose errors - Complete! "
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Display diagnosed causes
|
||||||
|
echo -e "${CYAN}${BOLD}ROOT CAUSES IDENTIFIED:${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for cause in "${!diagnosed_causes[@]}"; do
|
||||||
|
count="${diagnosed_causes[$cause]}"
|
||||||
|
echo "$count|$cause"
|
||||||
|
done | sort -rn | while IFS='|' read count cause; do
|
||||||
|
clean_cause=$(echo "$cause" | tr '_' ' ')
|
||||||
|
|
||||||
|
case "$cause" in
|
||||||
|
PHP_MEMORY_EXHAUSTED)
|
||||||
|
echo -e "${RED}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} Increase memory_limit in /home/USER/public_html/.user.ini"
|
||||||
|
echo -e " ${YELLOW}Set:${NC} memory_limit = 512M"
|
||||||
|
;;
|
||||||
|
PHP_FATAL_ERROR)
|
||||||
|
echo -e "${RED}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} Review recent code/plugin changes"
|
||||||
|
;;
|
||||||
|
PHP_SYNTAX_ERROR)
|
||||||
|
echo -e "${RED}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} Check for PHP syntax errors in recent file edits"
|
||||||
|
;;
|
||||||
|
MISSING_PHP_FUNCTION)
|
||||||
|
echo -e "${RED}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} Install missing PHP extension (check error for which one)"
|
||||||
|
;;
|
||||||
|
DATABASE_CONNECTION)
|
||||||
|
echo -e "${RED}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} Check database credentials or MySQL service"
|
||||||
|
;;
|
||||||
|
HTACCESS_ERROR)
|
||||||
|
echo -e "${RED}.HTACCESS ERROR DETECTED${NC} - $count occurrences"
|
||||||
|
;;
|
||||||
|
PERMISSION_ERROR)
|
||||||
|
echo -e "${RED}PERMISSION ERROR DETECTED${NC} - $count occurrences"
|
||||||
|
;;
|
||||||
|
NO_ERROR_LOG_FILE)
|
||||||
|
echo -e "${YELLOW}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Note:${NC} Checked multiple error_log locations - none found"
|
||||||
|
echo -e " ${YELLOW}Check:${NC} May need to enable PHP error logging"
|
||||||
|
;;
|
||||||
|
NO_PHP_ERROR_LOGGED)
|
||||||
|
echo -e "${YELLOW}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Note:${NC} 500 error but no PHP error in log - likely .htaccess or Apache config"
|
||||||
|
;;
|
||||||
|
FILE_NOT_FOUND)
|
||||||
|
echo -e "${RED}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} Requested file does not exist - check URL or restore missing files"
|
||||||
|
;;
|
||||||
|
PHP_HANDLER_ERROR)
|
||||||
|
echo -e "${RED}PHP HANDLER ERROR${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} PHP handler misconfigured - check cPanel PHP version or .htaccess AddHandler"
|
||||||
|
;;
|
||||||
|
WP_DEBUG_ERROR)
|
||||||
|
echo -e "${YELLOW}WORDPRESS DEBUG ERROR${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} Disable WP_DEBUG_DISPLAY in wp-config.php or fix underlying warnings"
|
||||||
|
;;
|
||||||
|
DOCROOT_MISSING)
|
||||||
|
echo -e "${RED}$clean_cause${NC} - $count occurrences"
|
||||||
|
echo -e " ${YELLOW}Fix:${NC} Document root directory missing - restore from backup or check domain configuration"
|
||||||
|
;;
|
||||||
|
UNKNOWN)
|
||||||
|
# Skip - these are errors we couldn't diagnose
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${INFO_COLOR}$clean_cause${NC} - $count occurrences"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Show example if we have one
|
||||||
|
if [ -n "${cause_examples[$cause]}" ]; then
|
||||||
|
example="${cause_examples[$cause]}"
|
||||||
|
echo -e " ${DIM}Example: ${example:0:120}...${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " DETAILED 500 ERROR LIST"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Show most frequent URLs getting 500s
|
||||||
|
echo "Most Frequent 500 Error URLs:"
|
||||||
|
echo ""
|
||||||
|
cut -d'|' -f1,4 "$ERRORS_500" | sort | uniq -c | sort -rn | head -15 | while read count domain_url; do
|
||||||
|
domain=$(echo "$domain_url" | cut -d'|' -f1)
|
||||||
|
url=$(echo "$domain_url" | cut -d'|' -f2)
|
||||||
|
user="${domain_user[$domain]}"
|
||||||
|
printf " ${RED}%4d×${NC} %s%s ${DIM}(user: %s)${NC}\n" "$count" "$domain" "$url" "$user"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " SPECIFIC DIAGNOSTICS (per URL/file)"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Show detailed diagnostics grouped by cause and issue pattern
|
||||||
|
if [ -f "$DETAILED_DIAGNOSIS" ] && [ -s "$DETAILED_DIAGNOSIS" ]; then
|
||||||
|
for cause_type in PHP_MEMORY_EXHAUSTED PERMISSION_ERROR HTACCESS_ERROR PHP_FATAL_ERROR PHP_SYNTAX_ERROR MISSING_PHP_FUNCTION DATABASE_CONNECTION FILE_NOT_FOUND PHP_HANDLER_ERROR WP_DEBUG_ERROR DOCROOT_MISSING; do
|
||||||
|
|
||||||
|
cause_count=$(grep "^${cause_type}|" "$DETAILED_DIAGNOSIS" 2>/dev/null | wc -l)
|
||||||
|
cause_count=${cause_count//[^0-9]/} # Remove any non-numeric characters
|
||||||
|
cause_count=${cause_count:-0} # Default to 0 if empty
|
||||||
|
|
||||||
|
if [ "$cause_count" -gt 0 ]; then
|
||||||
|
cause_display=$(echo "$cause_type" | tr '_' ' ')
|
||||||
|
echo -e "${RED}${BOLD}$cause_display ($cause_count occurrences)${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Group by unique error pattern (not domain)
|
||||||
|
declare -A issue_domains
|
||||||
|
|
||||||
|
# First pass: collect all domains per issue pattern
|
||||||
|
declare -A pattern_domains_temp
|
||||||
|
|
||||||
|
while IFS='|' read -r ctype full_diag; do
|
||||||
|
# Extract just the error part (after domain/)
|
||||||
|
issue_pattern=$(echo "$full_diag" | sed 's/^[^ ]* - //')
|
||||||
|
domain_part=$(echo "$full_diag" | grep -oP '^[^/]+')
|
||||||
|
|
||||||
|
# Append to temporary storage
|
||||||
|
pattern_domains_temp[$issue_pattern]+="$domain_part"$'\n'
|
||||||
|
done < <(grep "^${cause_type}|" "$DETAILED_DIAGNOSIS" 2>/dev/null)
|
||||||
|
|
||||||
|
# Second pass: deduplicate and limit to 5 unique domains per pattern
|
||||||
|
for pattern in "${!pattern_domains_temp[@]}"; do
|
||||||
|
# Get unique domains, limit to 5
|
||||||
|
unique_list=$(echo "${pattern_domains_temp[$pattern]}" | sort -u | head -5 | tr '\n' ',')
|
||||||
|
# Remove trailing comma
|
||||||
|
unique_list=${unique_list%,}
|
||||||
|
|
||||||
|
# Count total unique domains
|
||||||
|
total_unique=$(echo "${pattern_domains_temp[$pattern]}" | sort -u | wc -l)
|
||||||
|
|
||||||
|
# Add "..." if there are more than 5
|
||||||
|
if [ "$total_unique" -gt 5 ]; then
|
||||||
|
unique_list="$unique_list,..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
issue_domains[$pattern]="$unique_list"
|
||||||
|
done
|
||||||
|
|
||||||
|
unset pattern_domains_temp
|
||||||
|
|
||||||
|
# Display grouped issues
|
||||||
|
shown=0
|
||||||
|
for pattern in "${!issue_domains[@]}"; do
|
||||||
|
[ $shown -ge 10 ] && break
|
||||||
|
((shown++))
|
||||||
|
|
||||||
|
domains="${issue_domains[$pattern]}"
|
||||||
|
domain_count=$(echo "$domains" | tr ',' '\n' | grep -v '^\.\.\.$' | wc -l)
|
||||||
|
|
||||||
|
echo -e " ${YELLOW}Issue:${NC} $pattern"
|
||||||
|
echo -e " ${DIM}Affected ($domain_count):${NC} ${domains//,/, }"
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#issue_domains[@]}" -gt 10 ]; then
|
||||||
|
remaining=$((${#issue_domains[@]} - 10))
|
||||||
|
echo -e " ${DIM}... and $remaining more issue patterns${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset issue_domains
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "No detailed diagnostics available."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Full error list saved to: $ERRORS_500"
|
||||||
|
echo "Detailed diagnostics saved to: $DETAILED_DIAGNOSIS"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
press_enter
|
||||||
@@ -12,6 +12,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|||||||
source "$SCRIPT_DIR/lib/common-functions.sh"
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
||||||
source "$SCRIPT_DIR/lib/system-detect.sh"
|
source "$SCRIPT_DIR/lib/system-detect.sh"
|
||||||
source "$SCRIPT_DIR/lib/user-manager.sh"
|
source "$SCRIPT_DIR/lib/user-manager.sh"
|
||||||
|
source "$SCRIPT_DIR/lib/ip-reputation.sh"
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
APACHE_ERROR_LOG="/var/log/apache2/error_log"
|
APACHE_ERROR_LOG="/var/log/apache2/error_log"
|
||||||
@@ -190,127 +191,200 @@ echo ""
|
|||||||
|
|
||||||
is_noise() {
|
is_noise() {
|
||||||
local line="$1"
|
local line="$1"
|
||||||
|
local line_lower="${line,,}" # Convert to lowercase once
|
||||||
|
|
||||||
# Bot/Scanner patterns
|
# Bot/Scanner patterns
|
||||||
if echo "$line" | grep -qiE "bot|crawler|spider|scanner|nikto|nmap|masscan|sqlmap|nessus|acunetix|burp|shodan|censys|zgrab|nuclei|semrush|ahrefs|mj12"; then
|
[[ "$line_lower" =~ (bot|crawler|spider|scanner|nikto|nmap|masscan|sqlmap|nessus|acunetix|burp|shodan|censys|zgrab|nuclei|semrush|ahrefs|mj12) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Known scanner IPs
|
# Known scanner IPs
|
||||||
if echo "$line" | grep -qE "45\.148\.|185\.220\.|89\.248\.165\."; then
|
[[ "$line" =~ (45\.148\.|185\.220\.|89\.248\.165\.) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# WordPress plugin deprecation warnings (not user-facing)
|
# WordPress plugin deprecation warnings (not user-facing)
|
||||||
if echo "$line" | grep -qiE "PHP Deprecated|Deprecated.*plugin|call_user_func_array.*deprecated"; then
|
[[ "$line" =~ (PHP\ Deprecated|Deprecated.*plugin|call_user_func_array.*deprecated) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Non-critical PHP notices
|
# Non-critical PHP notices (unless critical indicators present)
|
||||||
if echo "$line" | grep -qiE "PHP Notice.*Undefined (variable|index|offset)" && \
|
if [[ "$line" =~ PHP\ Notice.*Undefined\ (variable|index|offset) ]] && ! [[ "$line_lower" =~ (fatal|error\ 500|white\ screen) ]]; then
|
||||||
! echo "$line" | grep -qiE "fatal|error 500|white screen"; then
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Missing favicon/common assets (not real errors)
|
# Missing favicon/common assets (not real errors)
|
||||||
if echo "$line" | grep -qiE "favicon\.ico|apple-touch-icon|robots\.txt|sitemap\.xml.*404"; then
|
[[ "$line_lower" =~ (favicon\.ico|apple-touch-icon|robots\.txt|sitemap\.xml.*404) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Security probes
|
# Security probes
|
||||||
if echo "$line" | grep -qiE "wp-admin|wp-login|phpMyAdmin|phpmyadmin|\.env|\.git|config\.php|xmlrpc|eval-stdin|shell\.php|c99\.php|r57\.php|adminer\.php" && \
|
if [[ "$line_lower" =~ (wp-admin|wp-login|phpmyadmin|\.env|\.git|config\.php|xmlrpc|eval-stdin|shell\.php|c99\.php|r57\.php|adminer\.php) ]] && \
|
||||||
echo "$line" | grep -qiE "404|403|not found"; then
|
[[ "$line" =~ (404|403|not\ found) ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# WordPress auto-updates and cron (normal operations)
|
# WordPress auto-updates and cron (normal operations)
|
||||||
if echo "$line" | grep -qiE "wp-cron\.php|doing_cron|auto.*update"; then
|
[[ "$line_lower" =~ (wp-cron\.php|doing_cron|auto.*update) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Common plugin update checks (not errors)
|
# Common plugin update checks (not errors)
|
||||||
if echo "$line" | grep -qiE "update-check|version-check|api\.wordpress\.org"; then
|
[[ "$line_lower" =~ (update-check|version-check|api\.wordpress\.org) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
is_critical_user_facing() {
|
is_critical_user_facing() {
|
||||||
local line="$1"
|
local line="$1"
|
||||||
|
local line_lower="${line,,}"
|
||||||
|
|
||||||
# 500 Internal Server Error (users see white page)
|
# 500 Internal Server Error (users see white page)
|
||||||
if echo "$line" | grep -qiE " 500 |Internal Server Error"; then
|
[[ "$line" =~ (\ 500\ |Internal\ Server\ Error) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PHP Fatal Errors (breaks functionality)
|
# PHP Fatal Errors (breaks functionality)
|
||||||
if echo "$line" | grep -qiE "PHP Fatal error|Fatal error:.*in /"; then
|
[[ "$line" =~ (PHP\ Fatal\ error|Fatal\ error:.*in\ /) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PHP Parse Errors (site completely broken)
|
# PHP Parse Errors (site completely broken)
|
||||||
if echo "$line" | grep -qiE "PHP Parse error|syntax error"; then
|
[[ "$line" =~ (PHP\ Parse\ error|syntax\ error) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Database connection failures (site down)
|
# Database connection failures (site down)
|
||||||
if echo "$line" | grep -qiE "Error establishing.*database|Can't connect.*MySQL|Access denied for user.*database|Too many connections|MySQL server has gone away"; then
|
[[ "$line" =~ (Error\ establishing.*database|Can\'t\ connect.*MySQL|Access\ denied\ for\ user.*database|Too\ many\ connections|MySQL\ server\ has\ gone\ away) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Memory exhaustion (white screen/incomplete pages)
|
# Memory exhaustion (white screen/incomplete pages)
|
||||||
if echo "$line" | grep -qiE "Allowed memory size.*exhausted|Out of memory|Fatal.*memory"; then
|
[[ "$line" =~ (Allowed\ memory\ size.*exhausted|Out\ of\ memory|Fatal.*memory) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Segmentation fault (complete crash)
|
# Segmentation fault (complete crash)
|
||||||
if echo "$line" | grep -qiE "Segmentation fault|signal 11"; then
|
[[ "$line" =~ (Segmentation\ fault|signal\ 11) ]] && return 0
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Permission denied on critical files
|
# Permission denied on critical files
|
||||||
if echo "$line" | grep -qiE "Permission denied" && \
|
if [[ "$line" =~ Permission\ denied ]] && [[ "$line" =~ (index\.php|wp-config\.php|\.htaccess|config\.php) ]]; then
|
||||||
echo "$line" | grep -qE "index\.php|wp-config\.php|\.htaccess|config\.php"; then
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# File not found for MAIN page (404 on homepage/index)
|
# File not found for MAIN page (404 on homepage/index)
|
||||||
if echo "$line" | grep -qiE "File does not exist.*index\.(php|html)" || \
|
[[ "$line" =~ (File\ does\ not\ exist.*index\.(php|html)|\ 404\ .*\"\ \"GET\ /) ]] && return 0
|
||||||
echo "$line" | grep -qE " 404 .*\" \"GET / "; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
extract_useful_info() {
|
extract_useful_info() {
|
||||||
local line="$1"
|
local line="$1"
|
||||||
|
local domain="unknown"
|
||||||
|
local file_path=""
|
||||||
|
local error_msg
|
||||||
|
|
||||||
# Extract domain
|
# Extract domain using bash regex (faster than grep|sed pipeline)
|
||||||
domain=$(echo "$line" | grep -oE '\[vhost [^:]+' | sed 's/\[vhost //' || \
|
if [[ "$line" =~ \[vhost\ ([^:]+) ]]; then
|
||||||
echo "$line" | grep -oE '[a-zA-Z0-9.-]+\.(com|net|org|io|co|uk|us|dev)' | head -1 || \
|
domain="${BASH_REMATCH[1]}"
|
||||||
echo "$line" | grep -oE '/home/[^/]+' | sed 's|/home/||' || echo "unknown")
|
elif [[ "$line" =~ ([a-zA-Z0-9.-]+\.(com|net|org|io|co|uk|us|dev)) ]]; then
|
||||||
|
domain="${BASH_REMATCH[1]}"
|
||||||
# Extract file path if PHP error
|
elif [[ "$line" =~ /home/([^/]+) ]]; then
|
||||||
file_path=$(echo "$line" | grep -oE "in /[^ ]+\.php" | sed 's/in //' || echo "")
|
domain="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
# Extract error message (clean up ModSec noise, timestamps, etc.)
|
|
||||||
error_msg=$(echo "$line" | \
|
|
||||||
sed 's/^\[.*\] //' | \
|
|
||||||
sed 's/\[client [^]]*\] //' | \
|
|
||||||
sed 's/\[unique_id "[^"]*"\]//g' | \
|
|
||||||
sed 's/\[pid [^]]*\]//g' | \
|
|
||||||
sed 's/\[tid [^]]*\]//g' | \
|
|
||||||
grep -v "^$" | \
|
|
||||||
cut -c1-150)
|
|
||||||
|
|
||||||
# Skip if error message is empty or just whitespace
|
|
||||||
if [ -z "$(echo "$error_msg" | tr -d '[:space:]')" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$domain|$file_path|$error_msg"
|
# Extract file path if PHP error
|
||||||
|
if [[ "$line" =~ in\ (/[^ ]+\.php) ]]; then
|
||||||
|
file_path="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract error message (clean up ModSec noise, timestamps, etc.)
|
||||||
|
# Use single sed command instead of pipeline
|
||||||
|
error_msg=$(echo "$line" | sed -E 's/^\[.*\] //; s/\[client [^]]*\] //; s/\[unique_id "[^"]*"\]//g; s/\[pid [^]]*\]//g; s/\[tid [^]]*\]//g' | cut -c1-150)
|
||||||
|
|
||||||
|
# Skip if error message is empty or just whitespace
|
||||||
|
error_msg="${error_msg#"${error_msg%%[![:space:]]*}"}" # ltrim
|
||||||
|
error_msg="${error_msg%"${error_msg##*[![:space:]]}"}" # rtrim
|
||||||
|
[ -z "$error_msg" ] && return 1
|
||||||
|
|
||||||
|
# Correlate to root cause
|
||||||
|
local root_cause=$(correlate_root_cause "$line" "$error_msg" "$domain")
|
||||||
|
|
||||||
|
echo "$domain|$file_path|$error_msg|$root_cause"
|
||||||
|
}
|
||||||
|
|
||||||
|
correlate_root_cause() {
|
||||||
|
local line="$1"
|
||||||
|
local error_msg="$2"
|
||||||
|
local domain="$3"
|
||||||
|
local cause="UNKNOWN"
|
||||||
|
local line_lower="${line,,}"
|
||||||
|
local error_lower="${error_msg,,}"
|
||||||
|
|
||||||
|
# .htaccess issues
|
||||||
|
if [[ "$line_lower" =~ (\.htaccess|invalid\ command|rewriterule|rewritecond) ]]; then
|
||||||
|
cause="HTACCESS"
|
||||||
|
|
||||||
|
# ModSecurity blocks
|
||||||
|
elif [[ "$line_lower" =~ (modsecurity|mod_security|waf) ]]; then
|
||||||
|
cause="MODSECURITY"
|
||||||
|
|
||||||
|
# PHP memory limit
|
||||||
|
elif [[ "$error_lower" =~ (memory.*exhausted|allowed\ memory\ size) ]]; then
|
||||||
|
# Get current memory limit for this domain/user
|
||||||
|
if [ -n "$domain" ] && [ "$domain" != "unknown" ]; then
|
||||||
|
user=$(grep -l "DNS.*$domain" /var/cpanel/users/* 2>/dev/null | head -1 | xargs basename 2>/dev/null)
|
||||||
|
if [ -n "$user" ]; then
|
||||||
|
mem_limit=$(grep -h "memory_limit" /home/$user/public_html/.user.ini /home/$user/.php.ini 2>/dev/null | tail -1 | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
[ -n "$mem_limit" ] && cause="PHP_MEMORY:$mem_limit" || cause="PHP_MEMORY:default"
|
||||||
|
else
|
||||||
|
cause="PHP_MEMORY"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
cause="PHP_MEMORY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PHP max execution time
|
||||||
|
elif [[ "$error_lower" =~ (max_execution_time|maximum\ execution\ time) ]]; then
|
||||||
|
cause="PHP_TIMEOUT"
|
||||||
|
|
||||||
|
# PHP upload/post size limits
|
||||||
|
elif [[ "$error_lower" =~ (upload_max_filesize|post_max_size) ]]; then
|
||||||
|
cause="PHP_UPLOAD_LIMIT"
|
||||||
|
|
||||||
|
# File permissions
|
||||||
|
elif [[ "$error_lower" =~ (permission\ denied|failed\ to\ open\ stream.*permission) ]]; then
|
||||||
|
cause="FILE_PERMISSIONS"
|
||||||
|
|
||||||
|
# Missing PHP modules/extensions
|
||||||
|
elif [[ "$error_lower" =~ (undefined\ function|call\ to\ undefined\ function|class\ .*\ not\ found) ]]; then
|
||||||
|
# Try to identify which module
|
||||||
|
if [[ "$error_lower" =~ (imagecreate|imagefilter|gd_) ]]; then
|
||||||
|
cause="MISSING_PHP_GD"
|
||||||
|
elif [[ "$error_msg" =~ (curl_|CURL) ]]; then
|
||||||
|
cause="MISSING_PHP_CURL"
|
||||||
|
elif [[ "$error_msg" =~ (json_|JSON) ]]; then
|
||||||
|
cause="MISSING_PHP_JSON"
|
||||||
|
elif [[ "$error_lower" =~ (mysqli|mysql_connect) ]]; then
|
||||||
|
cause="MISSING_PHP_MYSQLI"
|
||||||
|
else
|
||||||
|
cause="MISSING_PHP_MODULE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Database issues
|
||||||
|
elif [[ "$error_lower" =~ (database|mysql|mysqli) ]]; then
|
||||||
|
if [[ "$error_lower" =~ (too\ many\ connections|max_connections) ]]; then
|
||||||
|
cause="DB_MAX_CONNECTIONS"
|
||||||
|
elif [[ "$error_lower" =~ (access\ denied|authentication) ]]; then
|
||||||
|
cause="DB_AUTH_FAILED"
|
||||||
|
elif [[ "$error_lower" =~ (gone\ away|lost\ connection) ]]; then
|
||||||
|
cause="DB_TIMEOUT"
|
||||||
|
else
|
||||||
|
cause="DB_ERROR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apache configuration
|
||||||
|
elif [[ "$error_lower" =~ (invalid\ uri|request\ exceeded\ the\ limit|limitrequestline) ]]; then
|
||||||
|
cause="APACHE_CONFIG"
|
||||||
|
|
||||||
|
# PHP parse/syntax errors (code issues)
|
||||||
|
elif [[ "$error_lower" =~ (parse\ error|syntax\ error|unexpected) ]]; then
|
||||||
|
cause="PHP_SYNTAX_ERROR"
|
||||||
|
|
||||||
|
# File not found (may indicate bad symlinks or missing files)
|
||||||
|
elif [[ "$error_lower" =~ (no\ such\ file|failed\ to\ open\ stream) ]]; then
|
||||||
|
cause="MISSING_FILE"
|
||||||
|
|
||||||
|
# Generic PHP fatal error
|
||||||
|
elif [[ "$error_lower" =~ fatal\ error ]]; then
|
||||||
|
cause="PHP_FATAL_ERROR"
|
||||||
|
|
||||||
|
# 500 errors without specific cause
|
||||||
|
elif [[ "$error_msg" =~ (500|Internal\ Server) ]]; then
|
||||||
|
cause="SERVER_ERROR_500"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$cause"
|
||||||
}
|
}
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -356,41 +430,93 @@ while IFS='|' read -r log_path log_type; do
|
|||||||
uri=$(echo "$line" | grep -oE "uri: [^ ]+" | sed 's/uri: //' || echo "")
|
uri=$(echo "$line" | grep -oE "uri: [^ ]+" | sed 's/uri: //' || echo "")
|
||||||
domain=$(echo "$line" | grep -oE "hostname: [^ ]+" | sed 's/hostname: //' || echo "unknown")
|
domain=$(echo "$line" | grep -oE "hostname: [^ ]+" | sed 's/hostname: //' || echo "unknown")
|
||||||
|
|
||||||
echo "$domain||ModSecurity blocked: $attack_type $uri" >> "$CRITICAL_ERRORS"
|
# Extract rule ID if available
|
||||||
|
rule_id=$(echo "$line" | grep -oE "id \"[0-9]+\"" | grep -oE "[0-9]+" || echo "")
|
||||||
|
root_cause="MODSECURITY"
|
||||||
|
[ -n "$rule_id" ] && root_cause="MODSECURITY:rule_$rule_id"
|
||||||
|
|
||||||
|
echo "$domain||ModSecurity blocked: $attack_type $uri|$root_cause" >> "$CRITICAL_ERRORS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
done < <(tail -n 5000 "$log_path" 2>/dev/null)
|
done < <(tail -n 5000 "$log_path" 2>/dev/null)
|
||||||
|
|
||||||
elif $is_access_log; then
|
elif $is_access_log; then
|
||||||
# Access log - look for 5xx status codes
|
# Access log - look for 5xx status codes
|
||||||
|
# Use time-based filtering if possible, otherwise last 50k lines
|
||||||
|
cutoff_time=$(date -d "$HOURS_TO_ANALYZE hours ago" +%s 2>/dev/null || echo "0")
|
||||||
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
((total_lines++))
|
((total_lines++))
|
||||||
|
|
||||||
# Skip if bot/scanner
|
# Skip if bot/scanner
|
||||||
if is_noise "$line"; then
|
if is_noise "$line"; then
|
||||||
((filtered_out++))
|
((filtered_out++))
|
||||||
|
# Track bot/scanner IPs
|
||||||
|
read -r ip _ _ _ _ _ _ _ <<< "$line"
|
||||||
|
if [[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||||
|
flag_ip_attack "$ip" "BOT" 0 "Bot/scanner filtered in error analysis" >/dev/null 2>&1 &
|
||||||
|
fi
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract status code and URL
|
# Time filtering (Apache format: [DD/Mon/YYYY:HH:MM:SS +ZONE])
|
||||||
if echo "$line" | grep -qE '" 5[0-9]{2} '; then
|
if [ "$cutoff_time" != "0" ]; then
|
||||||
status=$(echo "$line" | grep -oE '" 5[0-9]{2} ' | tr -d '" ')
|
if [[ "$line" =~ \[([0-9]{2}/[A-Z][a-z]{2}/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2}) ]]; then
|
||||||
url=$(echo "$line" | awk '{print $7}' | cut -c1-80)
|
log_date="${BASH_REMATCH[1]}"
|
||||||
ip=$(echo "$line" | awk '{print $1}')
|
log_time=$(date -d "${log_date/:/ }" +%s 2>/dev/null || echo "0")
|
||||||
domain=$(basename "$log_path" | sed 's/-.*//')
|
[ "$log_time" != "0" ] && [ "$log_time" -lt "$cutoff_time" ] && continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract status code and URL using bash regex and read
|
||||||
|
if [[ "$line" =~ '"'[[:space:]](5[0-9]{2})[[:space:]] ]]; then
|
||||||
|
status="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
|
# Parse Apache log format: IP - - [timestamp] "METHOD URL PROTOCOL" STATUS SIZE
|
||||||
|
read -r ip _ _ timestamp _ request status_check _ <<< "$line"
|
||||||
|
|
||||||
|
# Extract URL from request (format: "GET /path HTTP/1.1")
|
||||||
|
if [[ "$request" =~ '"'[A-Z]+[[:space:]]([^[:space:]]+) ]]; then
|
||||||
|
url="${BASH_REMATCH[1]:0:80}"
|
||||||
|
else
|
||||||
|
url="/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract timestamp
|
||||||
|
if [[ "$line" =~ \[([^]]+)\] ]]; then
|
||||||
|
timestamp="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
timestamp=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get domain from log filename
|
||||||
|
domain="${log_path##*/}" # basename
|
||||||
|
domain="${domain%%-*}" # remove everything after first dash
|
||||||
|
|
||||||
# Apply domain filter if set
|
# Apply domain filter if set
|
||||||
if [ -n "$FILTER_DOMAIN" ] && [ "$domain" != "$FILTER_DOMAIN" ]; then
|
if [ -n "$FILTER_DOMAIN" ] && [ "$domain" != "$FILTER_DOMAIN" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Determine root cause from status code
|
||||||
|
case $status in
|
||||||
|
500) root_cause="SERVER_ERROR_500" ;;
|
||||||
|
502) root_cause="APACHE_CONFIG:bad_gateway" ;;
|
||||||
|
503) root_cause="APACHE_CONFIG:service_unavailable" ;;
|
||||||
|
504) root_cause="APACHE_CONFIG:gateway_timeout" ;;
|
||||||
|
*) root_cause="SERVER_ERROR_5XX" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
((critical_found++))
|
((critical_found++))
|
||||||
echo "$domain||HTTP $status - $url (from $ip)" >> "$CRITICAL_ERRORS"
|
echo "$domain||[$timestamp] HTTP $status - $url (from $ip)|$root_cause" >> "$CRITICAL_ERRORS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
done < <(tail -n 5000 "$log_path" 2>/dev/null)
|
done < <(tail -n 50000 "$log_path" 2>/dev/null)
|
||||||
else
|
else
|
||||||
# Error log - look for critical errors
|
# Error log - look for critical errors
|
||||||
|
# Use time-based filtering if possible, otherwise last 50k lines
|
||||||
|
cutoff_time=$(date -d "$HOURS_TO_ANALYZE hours ago" +%s 2>/dev/null || echo "0")
|
||||||
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
((total_lines++))
|
((total_lines++))
|
||||||
|
|
||||||
@@ -400,12 +526,21 @@ while IFS='|' read -r log_path log_type; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Time filtering (Apache/PHP error log format: [Day Mon DD HH:MM:SS YYYY])
|
||||||
|
if [ "$cutoff_time" != "0" ]; then
|
||||||
|
if [[ "$line" =~ \[([A-Z][a-z]{2}\ [A-Z][a-z]{2}\ [0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}\ [0-9]{4})\] ]]; then
|
||||||
|
log_date="${BASH_REMATCH[1]}"
|
||||||
|
log_time=$(date -d "$log_date" +%s 2>/dev/null || echo "0")
|
||||||
|
[ "$log_time" != "0" ] && [ "$log_time" -lt "$cutoff_time" ] && continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Apply user/domain filter if set
|
# Apply user/domain filter if set
|
||||||
if [ -n "$FILTER_USER" ]; then
|
if [ -n "$FILTER_USER" ]; then
|
||||||
echo "$line" | grep -q "/home/$FILTER_USER" || continue
|
[[ "$line" =~ /home/$FILTER_USER ]] || continue
|
||||||
fi
|
fi
|
||||||
if [ -n "$FILTER_DOMAIN" ]; then
|
if [ -n "$FILTER_DOMAIN" ]; then
|
||||||
echo "$line" | grep -q "$FILTER_DOMAIN" || continue
|
[[ "$line" =~ $FILTER_DOMAIN ]] || continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if it's critical and user-facing
|
# Check if it's critical and user-facing
|
||||||
@@ -414,7 +549,7 @@ while IFS='|' read -r log_path log_type; do
|
|||||||
extract_useful_info "$line" >> "$CRITICAL_ERRORS"
|
extract_useful_info "$line" >> "$CRITICAL_ERRORS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
done < <(tail -n 10000 "$log_path" 2>/dev/null)
|
done < <(tail -n 50000 "$log_path" 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
done < "$LOG_FILES_LIST"
|
done < "$LOG_FILES_LIST"
|
||||||
@@ -462,8 +597,8 @@ echo ""
|
|||||||
|
|
||||||
# Group identical errors and count them
|
# Group identical errors and count them
|
||||||
awk -F'|' '{
|
awk -F'|' '{
|
||||||
key = $1 "|" $3 # domain|error_msg (skip file_path for grouping)
|
key = $1 "|" $3 "|" $4 # domain|error_msg|root_cause
|
||||||
file[$1"|"$3] = $2 # Store file path
|
file[$1"|"$3"|"$4] = $2 # Store file path
|
||||||
count[key]++
|
count[key]++
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
@@ -471,14 +606,15 @@ END {
|
|||||||
split(key, parts, "|")
|
split(key, parts, "|")
|
||||||
domain = parts[1]
|
domain = parts[1]
|
||||||
error = parts[2]
|
error = parts[2]
|
||||||
|
root_cause = parts[3]
|
||||||
file_path = file[key]
|
file_path = file[key]
|
||||||
|
|
||||||
# Skip empty errors
|
# Skip empty errors
|
||||||
if (length(error) == 0) next
|
if (length(error) > 0) {
|
||||||
|
print count[key] "|" domain "|" file_path "|" error "|" root_cause
|
||||||
print count[key] "|" domain "|" file_path "|" error
|
|
||||||
}
|
}
|
||||||
}' "$CRITICAL_ERRORS" | sort -t'|' -k1 -rn | head -20 | while IFS='|' read -r count domain file_path error_msg; do
|
}
|
||||||
|
}' "$CRITICAL_ERRORS" | sort -t'|' -k1 -rn | head -20 | while IFS='|' read -r count domain file_path error_msg root_cause; do
|
||||||
|
|
||||||
# Color code by frequency
|
# Color code by frequency
|
||||||
if [ "$count" -ge 10 ]; then
|
if [ "$count" -ge 10 ]; then
|
||||||
@@ -496,14 +632,150 @@ END {
|
|||||||
[ -n "$domain" ] && [ "$domain" != "unknown" ] && echo " Domain: $domain"
|
[ -n "$domain" ] && [ "$domain" != "unknown" ] && echo " Domain: $domain"
|
||||||
[ -n "$file_path" ] && echo " File: $file_path"
|
[ -n "$file_path" ] && echo " File: $file_path"
|
||||||
echo " Error: $error_msg"
|
echo " Error: $error_msg"
|
||||||
|
|
||||||
|
# Display root cause with color coding and actionable fix
|
||||||
|
if [ -n "$root_cause" ] && [ "$root_cause" != "UNKNOWN" ]; then
|
||||||
|
echo -ne " ${CYAN}Root Cause: ${BOLD}"
|
||||||
|
|
||||||
|
case "$root_cause" in
|
||||||
|
HTACCESS)
|
||||||
|
echo -e "${YELLOW}⚙️ .htaccess Configuration${NC}"
|
||||||
|
echo " → Check .htaccess syntax in domain root"
|
||||||
|
echo " → Look for invalid RewriteRule or directives"
|
||||||
|
;;
|
||||||
|
MODSECURITY*)
|
||||||
|
rule=$(echo "$root_cause" | cut -d':' -f2)
|
||||||
|
echo -e "${YELLOW}🛡️ ModSecurity WAF Block${NC}"
|
||||||
|
[ -n "$rule" ] && echo " → Rule ID: $rule"
|
||||||
|
echo " → Check if this is a false positive"
|
||||||
|
echo " → Review: /usr/local/apache/logs/modsec_audit.log"
|
||||||
|
;;
|
||||||
|
PHP_MEMORY*)
|
||||||
|
limit=$(echo "$root_cause" | cut -d':' -f2)
|
||||||
|
echo -e "${RED}💾 PHP Memory Exhausted${NC}"
|
||||||
|
[ -n "$limit" ] && echo " → Current limit: $limit"
|
||||||
|
echo " → Increase memory_limit in .user.ini or php.ini"
|
||||||
|
echo " → Recommended: 256M minimum, 512M for WooCommerce"
|
||||||
|
;;
|
||||||
|
PHP_TIMEOUT)
|
||||||
|
echo -e "${YELLOW}⏱️ PHP Execution Timeout${NC}"
|
||||||
|
echo " → Increase max_execution_time in php.ini"
|
||||||
|
echo " → Recommended: 300 for imports/backups"
|
||||||
|
;;
|
||||||
|
PHP_UPLOAD_LIMIT)
|
||||||
|
echo -e "${YELLOW}📤 PHP Upload Size Limit${NC}"
|
||||||
|
echo " → Increase upload_max_filesize and post_max_size"
|
||||||
|
echo " → Match both values (e.g., 64M)"
|
||||||
|
;;
|
||||||
|
FILE_PERMISSIONS)
|
||||||
|
echo -e "${RED}🔒 File Permission Denied${NC}"
|
||||||
|
echo " → Check file ownership and permissions"
|
||||||
|
echo " → Files should be 644, directories 755"
|
||||||
|
echo " → Owner should match cPanel user"
|
||||||
|
;;
|
||||||
|
MISSING_PHP_GD)
|
||||||
|
echo -e "${RED}📦 Missing PHP GD Extension${NC}"
|
||||||
|
echo " → Install: yum install ea-phpXX-php-gd"
|
||||||
|
echo " → Required for image processing"
|
||||||
|
;;
|
||||||
|
MISSING_PHP_CURL)
|
||||||
|
echo -e "${RED}📦 Missing PHP cURL Extension${NC}"
|
||||||
|
echo " → Install: yum install ea-phpXX-php-curl"
|
||||||
|
;;
|
||||||
|
MISSING_PHP_*)
|
||||||
|
module=$(echo "$root_cause" | sed 's/MISSING_PHP_//' | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo -e "${RED}📦 Missing PHP Extension: $module${NC}"
|
||||||
|
echo " → Install: yum install ea-phpXX-php-$module"
|
||||||
|
;;
|
||||||
|
DB_MAX_CONNECTIONS)
|
||||||
|
echo -e "${RED}🗄️ Database Max Connections Reached${NC}"
|
||||||
|
echo " → Check: mysql -e 'SHOW VARIABLES LIKE \"max_connections\"'"
|
||||||
|
echo " → Increase max_connections in my.cnf"
|
||||||
|
echo " → Or optimize slow queries reducing connection time"
|
||||||
|
;;
|
||||||
|
DB_AUTH_FAILED)
|
||||||
|
echo -e "${RED}🗄️ Database Authentication Failed${NC}"
|
||||||
|
echo " → Verify credentials in wp-config.php / config files"
|
||||||
|
echo " → Check database user permissions"
|
||||||
|
;;
|
||||||
|
DB_TIMEOUT)
|
||||||
|
echo -e "${YELLOW}🗄️ Database Connection Timeout${NC}"
|
||||||
|
echo " → MySQL server may be overloaded"
|
||||||
|
echo " → Check slow query log"
|
||||||
|
;;
|
||||||
|
DB_ERROR)
|
||||||
|
echo -e "${RED}🗄️ Database Error${NC}"
|
||||||
|
echo " → Check MySQL error log for details"
|
||||||
|
;;
|
||||||
|
APACHE_CONFIG*)
|
||||||
|
issue=$(echo "$root_cause" | cut -d':' -f2)
|
||||||
|
echo -e "${YELLOW}⚙️ Apache Configuration${NC}"
|
||||||
|
[ -n "$issue" ] && echo " → Issue: $issue"
|
||||||
|
echo " → Check Apache config and vhost settings"
|
||||||
|
;;
|
||||||
|
PHP_SYNTAX_ERROR)
|
||||||
|
echo -e "${RED}⚠️ PHP Syntax Error in Code${NC}"
|
||||||
|
echo " → Review recent code changes"
|
||||||
|
echo " → Check for missing semicolons, brackets, quotes"
|
||||||
|
;;
|
||||||
|
MISSING_FILE)
|
||||||
|
echo -e "${YELLOW}📄 File Not Found${NC}"
|
||||||
|
echo " → File may have been deleted or moved"
|
||||||
|
echo " → Check for broken symlinks"
|
||||||
|
;;
|
||||||
|
PHP_FATAL_ERROR)
|
||||||
|
echo -e "${RED}⚠️ PHP Fatal Error${NC}"
|
||||||
|
echo " → Review PHP error logs for details"
|
||||||
|
echo " → May be compatibility issue or code bug"
|
||||||
|
;;
|
||||||
|
SERVER_ERROR_500)
|
||||||
|
echo -e "${RED}🔥 Generic 500 Internal Server Error${NC}"
|
||||||
|
echo " → Check PHP/Apache error logs for specifics"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${NC}$root_cause"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
done
|
done
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Root Cause Summary
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " 📊 ROOT CAUSE BREAKDOWN"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Count errors by root cause
|
||||||
|
cut -d'|' -f4 "$CRITICAL_ERRORS" | grep -v "^$" | sort | uniq -c | sort -rn | while read -r count cause; do
|
||||||
|
# Clean up cause display
|
||||||
|
clean_cause=$(echo "$cause" | sed 's/:.*//; s/_/ /g')
|
||||||
|
|
||||||
|
# Color code by severity
|
||||||
|
case "$cause" in
|
||||||
|
PHP_MEMORY*|DB_MAX_CONNECTIONS|PHP_FATAL_ERROR|SERVER_ERROR_500)
|
||||||
|
color="${RED}"; icon="🔥" ;;
|
||||||
|
HTACCESS|MODSECURITY*|PHP_TIMEOUT|DB_*)
|
||||||
|
color="${YELLOW}"; icon="⚠️ " ;;
|
||||||
|
MISSING_PHP*|FILE_PERMISSIONS)
|
||||||
|
color="${YELLOW}"; icon="📦" ;;
|
||||||
|
*)
|
||||||
|
color="${INFO_COLOR}"; icon="•" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf " ${color}${icon} %-35s %4d errors${NC}\n" "$clean_cause" "$count"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Intelligent Recommendations
|
# Intelligent Recommendations
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo " 🔧 RECOMMENDED ACTIONS"
|
echo " 🔧 RECOMMENDED ACTIONS"
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|||||||
Reference in New Issue
Block a user