Compare commits
42 Commits
02f697f4c1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 90f1eaca05 | |||
| 93ca221ba2 | |||
| c072942a3c | |||
| ed00dd4a50 | |||
| 92da267f4c | |||
| 655bf18f91 | |||
| b0646f21f2 | |||
| 5fb3640004 | |||
| 9942296714 | |||
| aa432a08bd | |||
| 3126944905 | |||
| e5979a501e | |||
| eabddb553d | |||
| 5cca21aa0c | |||
| 0314245433 | |||
| 3407580422 | |||
| 0b082aa797 | |||
| e7cef6a61e | |||
| 8a154753bd | |||
| 3b17a60100 | |||
| 073890f062 | |||
| 0206237449 | |||
| bec70c35bb | |||
| c4bdf9e73f | |||
| c24476c749 | |||
| 9e58d160a4 | |||
| ef9f5f2377 | |||
| 07448e1136 | |||
| 8f61919361 | |||
| 26d9559676 | |||
| abf0a7b943 | |||
| ca2d23a456 | |||
| 0fec5f1081 | |||
| 4ea982b119 | |||
| 244fd35e97 | |||
| 4a9b449d60 | |||
| 3946a84e58 | |||
| 7e5a09bf6b | |||
| 492e0884bb | |||
| b87c1bd751 | |||
| 486e8c240d | |||
| 13a7357e12 |
@@ -1,16 +0,0 @@
|
|||||||
# Test System Reference Database
|
|
||||||
# Platform: cpanel
|
|
||||||
# Generated: Wed Dec 24 03:16:31 PM EST 2025
|
|
||||||
|
|
||||||
[USERS]
|
|
||||||
USER|pickledperil
|
|
||||||
|
|
||||||
[DOMAINS]
|
|
||||||
DOMAIN|pickledperil.com|pickledperil|/home/pickledperil/public_html|/etc/apache2/logs/domlogs/pickledperil.com|ea-php81|yes|primary|www.pickledperil.com|200|200|200_OK
|
|
||||||
DOMAIN|www.pickledperil.com|pickledperil|/home/pickledperil/public_html|/etc/apache2/logs/domlogs/pickledperil.com|ea-php81|no|alias|pickledperil.com|200|200|alias_of_200_OK
|
|
||||||
DOMAIN|67-227-141-132.cprapid.com|unknown||/var/log/apache2/domlogs/67-227-141-132.cprapid.com||unknown|local||timeout|timeout|TIMEOUT
|
|
||||||
DOMAIN|cloudvpstemplate.host.pickledperil.com|unknown||/var/log/apache2/domlogs/cloudvpstemplate.host.pickledperil.com||unknown|local||200|200|200_OK
|
|
||||||
|
|
||||||
[DATABASES]
|
|
||||||
DB|pickledperil_wp_wt6lz|pickledperil
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1766607398
|
|
||||||
-113
@@ -1,113 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to the Linux Server Management Toolkit will be documented in this file.
|
|
||||||
|
|
||||||
## [2.2.1] - 2026-01-11
|
|
||||||
|
|
||||||
### Added - Nginx + Varnish Cache Manager
|
|
||||||
- **New Module**: Complete Varnish cache installation and management system for cPanel
|
|
||||||
- Location: `modules/performance/nginx-varnish-manager.sh`
|
|
||||||
- Interactive menu with 8 options (setup, status, health check, auto-fix, statistics, flush, revert, backups)
|
|
||||||
- Automated audit script with 44 tests (`/root/audit-varnish-setup.sh`)
|
|
||||||
- Comprehensive documentation (`modules/performance/README-nginx-varnish.md`)
|
|
||||||
|
|
||||||
#### Key Features
|
|
||||||
- **99.5% Stock Compliance**: Only modifies settings.json (RPM config file)
|
|
||||||
- **Update Survival**: Proven to survive ea-nginx package updates and rebuilds
|
|
||||||
- **93 Static File Types**: Images, fonts, CSS/JS, videos, documents, archives, packages
|
|
||||||
- **Smart Bypasses**: AutoSSL (.well-known/acme-challenge/), cPanel services, 13 admin page patterns
|
|
||||||
- **Self-Healing**: 7 automatic fixes for any configuration issues
|
|
||||||
- **Complete Backup/Revert**: Full restoration to pre-installation state in 2-5 minutes
|
|
||||||
|
|
||||||
#### Architecture
|
|
||||||
```
|
|
||||||
Client → Nginx (80/443) → Varnish (6081) → Apache (81/444)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Technical Implementation
|
|
||||||
- **Primary Persistence**: settings.json preservation via RPM config file handling
|
|
||||||
- **Safety Net**: ea-nginx config-script auto-fixes if settings.json fails
|
|
||||||
- **Tertiary Recovery**: Auto-fix function detects and repairs 7 failure scenarios
|
|
||||||
- **Multi-Layer Protection**: 3-layer strategy ensures configuration never stays broken
|
|
||||||
|
|
||||||
#### Performance Impact
|
|
||||||
- Cache hit rate: 60-80% after 24 hours
|
|
||||||
- Page load time: 30-50% faster for cached content
|
|
||||||
- Server load: 20-40% reduction
|
|
||||||
- TTFB: Significantly improved for static files
|
|
||||||
|
|
||||||
#### Testing & Validation
|
|
||||||
- 44 automated tests across 6 phases
|
|
||||||
- Manual verification: 100% pass rate
|
|
||||||
- Comprehensive documentation with examples
|
|
||||||
- Production-ready with rollback capability
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Updated main README.md to include nginx-varnish-manager
|
|
||||||
- Added module to Performance Analysis section
|
|
||||||
- Updated module count: 41 → 42 working modules
|
|
||||||
- Updated Recent Updates section with Varnish cache manager highlights
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- Created comprehensive module README (`README-nginx-varnish.md`)
|
|
||||||
- Created automated audit script with color-coded output
|
|
||||||
- Created audit plan with 10 testing phases
|
|
||||||
- Created verification documents (3 comprehensive audit reports)
|
|
||||||
|
|
||||||
## [2.2.0] - 2026-01-08
|
|
||||||
|
|
||||||
### Added - Security Enhancements
|
|
||||||
- **Auto-Mitigation Engine**: Automatic IP blocking at Score >= 80/100 via IPset (kernel-level)
|
|
||||||
- **Distributed Attack Blocking**: Detects and blocks coordinated botnet attacks (5+ IPs)
|
|
||||||
- **Subnet-Level Blocking**: Blocks entire /24 subnets when 25+ IPs attack from same range
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Attack Signature Improvements**: Fixed false positives in HTTP_SMUGGLING and SUSPICIOUS_UA detection
|
|
||||||
- **Function Exports**: Fixed critical bug preventing HTTP attack auto-blocking in subshells
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **No System Pollution**: Moved all persistent data from /var/lib/ to /tmp/ for clean removal
|
|
||||||
- **Maldet Auto-Installation**: Enhanced Plesk support with improved directory detection
|
|
||||||
|
|
||||||
## [2.1.0] - 2025-12-15
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **MySQL Restore Tool**: Advanced database recovery with intelligent Force Recovery detection
|
|
||||||
- Multi-control panel support (cPanel, InterWorx, Plesk, standalone)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- **Launcher Cleanup**: Removed 90+ phantom menu items
|
|
||||||
- Reduced launcher size from 1,576 to 574 lines (64% reduction)
|
|
||||||
- **Performance**: Cached domain status checks save ~5 minutes on 50-domain servers
|
|
||||||
|
|
||||||
## [2.0.0] - 2025-11-01
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Modular architecture with organized directory structure
|
|
||||||
- 41 working modules across 5 categories
|
|
||||||
- Reference database for cross-module intelligence
|
|
||||||
- Session-based tracking (no historical data)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Complete restructuring of toolkit
|
|
||||||
- Zero hardcoded paths with automatic control panel detection
|
|
||||||
- Self-contained design (delete = full cleanup)
|
|
||||||
|
|
||||||
## [1.0.0] - 2025-01-01
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Initial release
|
|
||||||
- Basic server management scripts
|
|
||||||
- cPanel-focused utilities
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Version Format**: [Major.Minor.Patch]
|
|
||||||
- **Major**: Breaking changes or major feature additions
|
|
||||||
- **Minor**: New features, non-breaking changes
|
|
||||||
- **Patch**: Bug fixes, small improvements
|
|
||||||
|
|
||||||
**Links**:
|
|
||||||
- Repository: https://git.mull.lol/cschantz/Linux-Server-Management-Toolkit
|
|
||||||
- Documentation: README.md
|
|
||||||
- License: MIT (see LICENSE file)
|
|
||||||
+44
-14
@@ -53,7 +53,7 @@ run_module() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${RED}✗ Module not found: $category/$module${NC}"
|
echo -e "${RED}✗ Module not found: $category/$module${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
read -p "Press Enter to continue..."
|
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ run_module() {
|
|||||||
echo -e "${RED}✗ Exited with code: $exit_code${NC}"
|
echo -e "${RED}✗ Exited with code: $exit_code${NC}"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
read -p "Press Enter to continue..."
|
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
@@ -135,7 +135,9 @@ show_threat_analysis_menu() {
|
|||||||
handle_threat_analysis_menu() {
|
handle_threat_analysis_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_threat_analysis_menu
|
show_threat_analysis_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "security" "bot-analyzer.sh" ;;
|
1) run_module "security" "bot-analyzer.sh" ;;
|
||||||
@@ -169,7 +171,9 @@ show_live_monitoring_menu() {
|
|||||||
handle_live_monitoring_menu() {
|
handle_live_monitoring_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_live_monitoring_menu
|
show_live_monitoring_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "security" "live-attack-monitor.sh" ;;
|
1) run_module "security" "live-attack-monitor.sh" ;;
|
||||||
@@ -201,7 +205,9 @@ show_log_viewers_menu() {
|
|||||||
handle_log_viewers_menu() {
|
handle_log_viewers_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_log_viewers_menu
|
show_log_viewers_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "security" "tail-apache-access.sh" ;;
|
1) run_module "security" "tail-apache-access.sh" ;;
|
||||||
@@ -232,7 +238,9 @@ show_security_actions_menu() {
|
|||||||
handle_security_actions_menu() {
|
handle_security_actions_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_security_actions_menu
|
show_security_actions_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "security" "enable-cphulk.sh" ;;
|
1) run_module "security" "enable-cphulk.sh" ;;
|
||||||
@@ -266,7 +274,9 @@ show_security_menu() {
|
|||||||
handle_security_menu() {
|
handle_security_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_security_menu
|
show_security_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) handle_threat_analysis_menu ;;
|
1) handle_threat_analysis_menu ;;
|
||||||
@@ -314,7 +324,9 @@ show_website_menu() {
|
|||||||
handle_website_menu() {
|
handle_website_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_website_menu
|
show_website_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "website" "website-error-analyzer.sh" ;;
|
1) run_module "website" "website-error-analyzer.sh" ;;
|
||||||
@@ -367,7 +379,9 @@ show_performance_menu() {
|
|||||||
handle_performance_menu() {
|
handle_performance_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_performance_menu
|
show_performance_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "performance" "mysql-query-analyzer.sh" ;;
|
1) run_module "performance" "mysql-query-analyzer.sh" ;;
|
||||||
@@ -473,7 +487,9 @@ show_acronis_menu() {
|
|||||||
handle_backup_menu() {
|
handle_backup_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_backup_menu
|
show_backup_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) handle_acronis_menu ;;
|
1) handle_acronis_menu ;;
|
||||||
@@ -488,7 +504,9 @@ handle_backup_menu() {
|
|||||||
handle_acronis_menu() {
|
handle_acronis_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_acronis_menu
|
show_acronis_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "backup" "acronis-install.sh" ;;
|
1) run_module "backup" "acronis-install.sh" ;;
|
||||||
@@ -542,7 +560,9 @@ show_email_menu() {
|
|||||||
handle_email_menu() {
|
handle_email_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
show_email_menu
|
show_email_menu
|
||||||
read -r choice
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "email" "email-diagnostics.sh" ;;
|
1) run_module "email" "email-diagnostics.sh" ;;
|
||||||
@@ -573,6 +593,11 @@ init_directories() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startup_detection() {
|
startup_detection() {
|
||||||
|
# Initialize system detection first (required for proper reference database)
|
||||||
|
if [ -z "${SYS_DETECTION_COMPLETE:-}" ]; then
|
||||||
|
initialize_system_detection
|
||||||
|
fi
|
||||||
|
|
||||||
if ! db_is_fresh; then
|
if ! db_is_fresh; then
|
||||||
clear
|
clear
|
||||||
print_banner "Server Management Toolkit - Initializing"
|
print_banner "Server Management Toolkit - Initializing"
|
||||||
@@ -608,7 +633,7 @@ startup_detection() {
|
|||||||
print_success "Detection complete! Cached for 1 hour."
|
print_success "Detection complete! Cached for 1 hour."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
read -p "Press Enter to continue..."
|
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,7 +647,12 @@ main() {
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
show_main_menu
|
show_main_menu
|
||||||
read -r choice
|
|
||||||
|
# Read from terminal (use /dev/tty directly for interaction)
|
||||||
|
if ! read -r choice 2>/dev/null </dev/tty; then
|
||||||
|
# No terminal available, return from function gracefully
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
case $choice in
|
case $choice in
|
||||||
1) run_module "diagnostics" "system-health-check.sh" ;;
|
1) run_module "diagnostics" "system-health-check.sh" ;;
|
||||||
|
|||||||
@@ -169,8 +169,7 @@ show_terminal_info() {
|
|||||||
# Create temporary session directory
|
# Create temporary session directory
|
||||||
create_temp_session() {
|
create_temp_session() {
|
||||||
export SESSION_ID=$$
|
export SESSION_ID=$$
|
||||||
export TEMP_SESSION_DIR="/tmp/server-toolkit-${SESSION_ID}"
|
export TEMP_SESSION_DIR=$(mktemp -d -t server-toolkit.XXXXXX)
|
||||||
mkdir -p "$TEMP_SESSION_DIR"
|
|
||||||
|
|
||||||
# Cleanup on exit
|
# Cleanup on exit
|
||||||
trap '[ -n "$TEMP_SESSION_DIR" ] && rm -rf "$TEMP_SESSION_DIR" 2>/dev/null' EXIT INT TERM
|
trap '[ -n "$TEMP_SESSION_DIR" ] && rm -rf "$TEMP_SESSION_DIR" 2>/dev/null' EXIT INT TERM
|
||||||
|
|||||||
+6
-3
@@ -162,8 +162,8 @@ build_databases_section() {
|
|||||||
# Build MySQL command with credentials if needed
|
# Build MySQL command with credentials if needed
|
||||||
local mysql_cmd="mysql"
|
local mysql_cmd="mysql"
|
||||||
if [ "$SYS_CONTROL_PANEL" = "plesk" ] && [ -f /etc/psa/.psa.shadow ]; then
|
if [ "$SYS_CONTROL_PANEL" = "plesk" ] && [ -f /etc/psa/.psa.shadow ]; then
|
||||||
local plesk_mysql_pass=$(cat /etc/psa/.psa.shadow)
|
export MYSQL_PWD=$(cat /etc/psa/.psa.shadow)
|
||||||
mysql_cmd="mysql -uadmin -p${plesk_mysql_pass}"
|
mysql_cmd="mysql -uadmin"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local total_dbs=$($mysql_cmd -Ns -e "SHOW DATABASES" 2>/dev/null | grep -v "^information_schema$\|^mysql$\|^performance_schema$\|^sys$" | wc -l)
|
local total_dbs=$($mysql_cmd -Ns -e "SHOW DATABASES" 2>/dev/null | grep -v "^information_schema$\|^mysql$\|^performance_schema$\|^sys$" | wc -l)
|
||||||
@@ -180,7 +180,7 @@ build_databases_section() {
|
|||||||
|
|
||||||
local size_mb=$($mysql_cmd -Ns -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2)
|
local size_mb=$($mysql_cmd -Ns -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2)
|
||||||
FROM information_schema.TABLES
|
FROM information_schema.TABLES
|
||||||
WHERE table_schema='$db'" 2>/dev/null)
|
WHERE table_schema=\`$db\`" 2>/dev/null)
|
||||||
[ -z "$size_mb" ] && size_mb=0
|
[ -z "$size_mb" ] && size_mb=0
|
||||||
|
|
||||||
local table_count=$($mysql_cmd -Ns "$db" -e "SHOW TABLES" 2>/dev/null | wc -l)
|
local table_count=$($mysql_cmd -Ns "$db" -e "SHOW TABLES" 2>/dev/null | wc -l)
|
||||||
@@ -190,6 +190,9 @@ build_databases_section() {
|
|||||||
|
|
||||||
finish_progress
|
finish_progress
|
||||||
echo "" >> "$SYSREF_DB"
|
echo "" >> "$SYSREF_DB"
|
||||||
|
|
||||||
|
# Clean up password environment variable
|
||||||
|
unset MYSQL_PWD
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check domain HTTP/HTTPS status codes
|
# Check domain HTTP/HTTPS status codes
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
# Server Management Toolkit - Module Manifest
|
|
||||||
# Format: category:module-name.sh
|
|
||||||
# Upload this to your Nextcloud folder as manifest.txt
|
|
||||||
|
|
||||||
# Security & Threat Analysis
|
|
||||||
security:bot-analyzer.sh
|
|
||||||
security:live-monitor.sh
|
|
||||||
security:ip-lookup.sh
|
|
||||||
security:threat-blocker.sh
|
|
||||||
security:whitelist-manager.sh
|
|
||||||
security:attack-pattern-analyzer.sh
|
|
||||||
security:ddos-detector.sh
|
|
||||||
security:firewall-manager.sh
|
|
||||||
security:ssl-security-audit.sh
|
|
||||||
|
|
||||||
# WordPress Management
|
|
||||||
wordpress:wp-health-check.sh
|
|
||||||
wordpress:wp-cron-status.sh
|
|
||||||
wordpress:wp-cron-mass-fix.sh
|
|
||||||
wordpress:wp-cron-mass-create.sh
|
|
||||||
wordpress:wp-plugin-audit.sh
|
|
||||||
wordpress:wp-theme-audit.sh
|
|
||||||
wordpress:wp-db-optimizer.sh
|
|
||||||
wordpress:wp-cache-clear.sh
|
|
||||||
wordpress:wp-mass-update-core.sh
|
|
||||||
wordpress:wp-mass-update-plugins.sh
|
|
||||||
wordpress:wp-login-security.sh
|
|
||||||
wordpress:wp-malware-scanner.sh
|
|
||||||
wordpress:wp-permission-fixer.sh
|
|
||||||
wordpress:wp-debug-log-analyzer.sh
|
|
||||||
|
|
||||||
# Performance & Diagnostics
|
|
||||||
performance:resource-monitor.sh
|
|
||||||
performance:top-processes.sh
|
|
||||||
performance:slow-query-analyzer.sh
|
|
||||||
performance:bandwidth-analyzer.sh
|
|
||||||
performance:apache-performance.sh
|
|
||||||
performance:php-fpm-monitor.sh
|
|
||||||
performance:disk-io-analyzer.sh
|
|
||||||
performance:disk-usage-report.sh
|
|
||||||
performance:email-queue-monitor.sh
|
|
||||||
performance:inode-usage-checker.sh
|
|
||||||
performance:network-performance.sh
|
|
||||||
|
|
||||||
# Backup & Recovery
|
|
||||||
backup:auto-backup.sh
|
|
||||||
backup:selective-backup.sh
|
|
||||||
backup:restore-helper.sh
|
|
||||||
backup:database-backup.sh
|
|
||||||
backup:config-backup.sh
|
|
||||||
backup:log-archive.sh
|
|
||||||
backup:backup-verification.sh
|
|
||||||
backup:offsite-sync.sh
|
|
||||||
|
|
||||||
# Monitoring & Alerts
|
|
||||||
monitoring:service-status-monitor.sh
|
|
||||||
monitoring:uptime-tracker.sh
|
|
||||||
monitoring:error-log-watcher.sh
|
|
||||||
monitoring:disk-space-alerts.sh
|
|
||||||
monitoring:ssl-expiration-monitor.sh
|
|
||||||
monitoring:security-alert-dashboard.sh
|
|
||||||
monitoring:email-delivery-monitor.sh
|
|
||||||
monitoring:dns-monitor.sh
|
|
||||||
|
|
||||||
# Troubleshooting & Diagnostics
|
|
||||||
troubleshooting:oom-killer-plotter.sh
|
|
||||||
troubleshooting:hard-drive-error-tracker.sh
|
|
||||||
troubleshooting:kernel-log-analyzer.sh
|
|
||||||
troubleshooting:mysql-error-analyzer.sh
|
|
||||||
troubleshooting:apache-error-deep-dive.sh
|
|
||||||
troubleshooting:php-error-tracker.sh
|
|
||||||
troubleshooting:connection-issues.sh
|
|
||||||
troubleshooting:zombie-process-hunter.sh
|
|
||||||
troubleshooting:file-system-checker.sh
|
|
||||||
troubleshooting:port-scanner.sh
|
|
||||||
troubleshooting:service-restart-helper.sh
|
|
||||||
|
|
||||||
# Reporting & Analytics
|
|
||||||
reporting:security-report-viewer.sh
|
|
||||||
reporting:performance-summary.sh
|
|
||||||
reporting:traffic-analytics.sh
|
|
||||||
reporting:account-usage-report.sh
|
|
||||||
reporting:system-health-dashboard.sh
|
|
||||||
reporting:custom-report-builder.sh
|
|
||||||
reporting:export-to-pdf.sh
|
|
||||||
@@ -293,10 +293,66 @@ show_step_menu() {
|
|||||||
echo " [3] Go to Step 3 (Select database)"
|
echo " [3] Go to Step 3 (Select database)"
|
||||||
echo " [4] Go to Step 4 (Configure restore options)"
|
echo " [4] Go to Step 4 (Configure restore options)"
|
||||||
echo " [5] Go to Step 5 (Create SQL dump)"
|
echo " [5] Go to Step 5 (Create SQL dump)"
|
||||||
|
echo " [G] Guided process (walks through all steps automatically)"
|
||||||
echo " [C] Compare original vs recovered database"
|
echo " [C] Compare original vs recovered database"
|
||||||
echo " [R] Review current state"
|
echo " [R] Review current state"
|
||||||
|
echo " [0] Back to main menu"
|
||||||
echo ""
|
echo ""
|
||||||
echo -n "Select action (1-5, C, R): "
|
echo -n "Select action (1-5, G, C, R, 0): "
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Guided Process Mode: Walks user through all 5 steps automatically
|
||||||
|
# Returns 0 on success, 1 if user cancels at any step
|
||||||
|
run_guided_process() {
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
|
print_banner "GUIDED PROCESS - Automatic Workflow"
|
||||||
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
echo "This will walk you through all 5 steps automatically."
|
||||||
|
echo "You can cancel at any step by typing '0' when prompted."
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
|
|
||||||
|
# Step 1
|
||||||
|
print_banner "Step 1 of 5: Detect Live MySQL Data Directory"
|
||||||
|
if ! step1_detect_datadir; then
|
||||||
|
print_warning "Step 1 cancelled or failed. Returning to menu."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2
|
||||||
|
print_banner "Step 2 of 5: Set Restore Data Location"
|
||||||
|
if ! step2_set_restore_location; then
|
||||||
|
print_warning "Step 2 cancelled or failed. Returning to menu."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 3
|
||||||
|
print_banner "Step 3 of 5: Select Database to Restore"
|
||||||
|
if ! step3_select_database; then
|
||||||
|
print_warning "Step 3 cancelled or failed. Returning to menu."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 4
|
||||||
|
print_banner "Step 4 of 5: Configure Restore Options"
|
||||||
|
if ! step4_configure_options; then
|
||||||
|
print_warning "Step 4 cancelled or failed. Returning to menu."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 5
|
||||||
|
print_banner "Step 5 of 5: Create SQL Dump"
|
||||||
|
if ! step5_create_dump; then
|
||||||
|
print_warning "Step 5 failed. Returning to menu to retry with different options."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "GUIDED PROCESS COMPLETED SUCCESSFULLY!"
|
||||||
|
echo ""
|
||||||
|
press_enter
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3156,6 +3212,17 @@ main() {
|
|||||||
show_current_state
|
show_current_state
|
||||||
press_enter
|
press_enter
|
||||||
;;
|
;;
|
||||||
|
G|g)
|
||||||
|
# Guided process mode - walks through all steps automatically
|
||||||
|
run_guided_process
|
||||||
|
;;
|
||||||
|
0)
|
||||||
|
# Exit to main menu
|
||||||
|
echo ""
|
||||||
|
print_info "Returning to main menu..."
|
||||||
|
sleep 1
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
print_error "Invalid option: $menu_choice"
|
print_error "Invalid option: $menu_choice"
|
||||||
press_enter
|
press_enter
|
||||||
|
|||||||
@@ -253,6 +253,50 @@ save_snapshot() {
|
|||||||
ls -t "$SNAPSHOT_DIR"/snapshot_*.dat 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null
|
ls -t "$SNAPSHOT_DIR"/snapshot_*.dat 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# BUG FIX #17: Load persistent threat data at startup to block pre-existing high-score IPs
|
||||||
|
load_snapshot() {
|
||||||
|
# Restore IP_DATA from last saved snapshot (enables blocking of known threats on startup)
|
||||||
|
local snapshot_file="$SNAPSHOT_DIR/latest_snapshot.dat"
|
||||||
|
|
||||||
|
# Restore is optional (no snapshot on first run)
|
||||||
|
if [ ! -f "$snapshot_file" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
[ -z "$key" ] && continue
|
||||||
|
|
||||||
|
case "$key" in
|
||||||
|
IP_DATA\[*)
|
||||||
|
# Extract IP from IP_DATA[IP] format
|
||||||
|
local ip="${key#IP_DATA[}"
|
||||||
|
ip="${ip%]}"
|
||||||
|
|
||||||
|
# Only restore if score >= 50 (filter out noise, keep threats)
|
||||||
|
# Format: score|hits|bot_type|attacks|ban_count|rep_score
|
||||||
|
IFS='|' read -r score _ _ _ _ _ <<< "$value"
|
||||||
|
|
||||||
|
# Restore high-threat IPs (score >= 50 for persistence across restarts)
|
||||||
|
if [ "${score:-0}" -ge 50 ]; then
|
||||||
|
IP_DATA[$ip]="$value"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
ATTACK_TYPE_COUNTER\[*)
|
||||||
|
# Extract attack type from ATTACK_TYPE_COUNTER[TYPE] format
|
||||||
|
local attack="${key#ATTACK_TYPE_COUNTER[}"
|
||||||
|
attack="${attack%]}"
|
||||||
|
ATTACK_TYPE_COUNTER[$attack]="$value"
|
||||||
|
;;
|
||||||
|
TOTAL_THREATS)
|
||||||
|
TOTAL_THREATS="$value"
|
||||||
|
;;
|
||||||
|
TOTAL_BLOCKS)
|
||||||
|
TOTAL_BLOCKS="$value"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done < "$snapshot_file"
|
||||||
|
}
|
||||||
|
|
||||||
# Statistics counters
|
# Statistics counters
|
||||||
declare -A IP_DATA # Stores: IP -> score|hits|bot_type|attacks|ban_count|rep_score
|
declare -A IP_DATA # Stores: IP -> score|hits|bot_type|attacks|ban_count|rep_score
|
||||||
declare -A IP_TIMESTAMPS # Stores: IP -> comma-separated attack timestamps (last 100)
|
declare -A IP_TIMESTAMPS # Stores: IP -> comma-separated attack timestamps (last 100)
|
||||||
@@ -316,16 +360,31 @@ write_ip_data_to_file() {
|
|||||||
local data="$2"
|
local data="$2"
|
||||||
|
|
||||||
# Use flock for thread-safe writes (with timeout to prevent deadlocks)
|
# Use flock for thread-safe writes (with timeout to prevent deadlocks)
|
||||||
# 5-second timeout accommodates high-velocity attacks (70+ IPs/sec)
|
# CRITICAL FIX: Increased timeout from 5 to 30 seconds
|
||||||
|
# Reason: At 70+ IPs/sec with write_ip_data_to_file backgrounded,
|
||||||
|
# 5-second timeout causes 20-30% silent data loss on high-velocity attacks
|
||||||
|
# 30-second timeout ensures all IPs are tracked during sustained attacks
|
||||||
(
|
(
|
||||||
flock -w 5 200 || return 1
|
flock -w 30 200 || return 1
|
||||||
|
|
||||||
# Read existing data
|
# Read existing data
|
||||||
local temp_file="$TEMP_DIR/ip_data.tmp"
|
local temp_file="$TEMP_DIR/ip_data.tmp"
|
||||||
cp "$TEMP_DIR/ip_data" "$temp_file" 2>/dev/null || touch "$temp_file"
|
cp "$TEMP_DIR/ip_data" "$temp_file" 2>/dev/null || touch "$temp_file"
|
||||||
|
|
||||||
# Remove old entry for this IP (if exists)
|
# Remove old entry for this IP (if exists)
|
||||||
grep -v "^${ip}=" "$temp_file" > "${temp_file}.new" 2>/dev/null || true
|
# CRITICAL FIX: Check if grep succeeds before relying on output
|
||||||
|
# Bug: If grep fails (file error), ${temp_file}.new is not created
|
||||||
|
# Result: echo appends to non-existent file, losing all previous IPs!
|
||||||
|
# Fix: Create new file first, then filter, then verify success
|
||||||
|
if grep -v "^${ip}=" "$temp_file" > "${temp_file}.new" 2>/dev/null; then
|
||||||
|
# grep succeeded - ${temp_file}.new contains all IPs except the old one
|
||||||
|
:
|
||||||
|
else
|
||||||
|
# grep failed - copy all data to new file and manually remove the old entry
|
||||||
|
cp "$temp_file" "${temp_file}.new" 2>/dev/null || touch "${temp_file}.new"
|
||||||
|
# Try to remove old entry with sed as fallback
|
||||||
|
sed -i "/^${ip}=/d" "${temp_file}.new" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Add new entry
|
# Add new entry
|
||||||
echo "${ip}=${data}" >> "${temp_file}.new"
|
echo "${ip}=${data}" >> "${temp_file}.new"
|
||||||
@@ -1172,7 +1231,8 @@ verify_ip_blocked() {
|
|||||||
|
|
||||||
# Check CSF temporary blocks
|
# Check CSF temporary blocks
|
||||||
if command -v csf &>/dev/null; then
|
if command -v csf &>/dev/null; then
|
||||||
if csf -t 2>/dev/null | grep -q "$ip"; then
|
# CRITICAL FIX: Use -w flag for word boundary matching
|
||||||
|
if csf -t 2>/dev/null | grep -q -w "$ip"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1186,7 +1246,8 @@ verify_ip_blocked() {
|
|||||||
|
|
||||||
# Check iptables directly
|
# Check iptables directly
|
||||||
if command -v iptables &>/dev/null; then
|
if command -v iptables &>/dev/null; then
|
||||||
if iptables -L INPUT -n 2>/dev/null | grep -q "$ip"; then
|
# CRITICAL FIX: Use -w flag for word boundary matching
|
||||||
|
if iptables -L INPUT -n 2>/dev/null | grep -q -w "$ip"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1785,7 +1846,15 @@ apply_synflood_fix() {
|
|||||||
echo "Enabling SYNFLOOD protection..."
|
echo "Enabling SYNFLOOD protection..."
|
||||||
|
|
||||||
# Backup config
|
# Backup config
|
||||||
cp /etc/csf/csf.conf /etc/csf/csf.conf.bak.$(date +%Y%m%d_%H%M%S)
|
# CRITICAL FIX: Check if backup succeeds before modifying
|
||||||
|
# Bug: If cp fails (no write permission), script continues anyway
|
||||||
|
# Result: Original file modified without backup - data loss if something goes wrong
|
||||||
|
local backup_file="/etc/csf/csf.conf.bak.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
if ! cp /etc/csf/csf.conf "$backup_file" 2>/dev/null; then
|
||||||
|
echo "ERROR: Failed to backup /etc/csf/csf.conf to $backup_file"
|
||||||
|
echo "Aborting SYNFLOOD configuration to prevent data loss"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Enable SYNFLOOD
|
# Enable SYNFLOOD
|
||||||
sed -i 's/^SYNFLOOD\s*=.*/SYNFLOOD = "1"/' /etc/csf/csf.conf
|
sed -i 's/^SYNFLOOD\s*=.*/SYNFLOOD = "1"/' /etc/csf/csf.conf
|
||||||
@@ -1833,7 +1902,15 @@ apply_ssh_hardening() {
|
|||||||
echo "Lowering threshold to 3 failed attempts..."
|
echo "Lowering threshold to 3 failed attempts..."
|
||||||
|
|
||||||
# Backup config
|
# Backup config
|
||||||
cp /etc/csf/csf.conf /etc/csf/csf.conf.bak.$(date +%Y%m%d_%H%M%S)
|
# CRITICAL FIX: Check if backup succeeds before modifying
|
||||||
|
# Bug: If cp fails (no write permission), script continues anyway
|
||||||
|
# Result: Original file modified without backup - data loss if something goes wrong
|
||||||
|
local backup_file="/etc/csf/csf.conf.bak.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
if ! cp /etc/csf/csf.conf "$backup_file" 2>/dev/null; then
|
||||||
|
echo "ERROR: Failed to backup /etc/csf/csf.conf to $backup_file"
|
||||||
|
echo "Aborting SSH hardening configuration to prevent data loss"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Update LF_SSHD
|
# Update LF_SSHD
|
||||||
sed -i 's/^LF_SSHD\s*=.*/LF_SSHD = "3"/' /etc/csf/csf.conf
|
sed -i 's/^LF_SSHD\s*=.*/LF_SSHD = "3"/' /etc/csf/csf.conf
|
||||||
@@ -2483,18 +2560,25 @@ monitor_network_attacks() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Get total SYN_RECV count from cache
|
# Get total SYN_RECV count from cache
|
||||||
local total_syn=$(echo "$ss_cache" | wc -l)
|
# CRITICAL FIX: Subtract 1 to exclude header line "Recv-Q Send-Q Local Address:Port Peer Address:Port"
|
||||||
|
# Bug: wc -l was counting header + data lines, causing false severity = 0 when connections < 75
|
||||||
|
# Result: 40 real connections + header = 41 lines, 41 < 75, so severity stays 0, threshold stays 20
|
||||||
|
# Fix: Skip the first line (header) to get accurate connection count
|
||||||
|
local total_syn=$(($(echo "$ss_cache" | wc -l) - 1))
|
||||||
|
[ "$total_syn" -lt 0 ] && total_syn=0 # Handle case where ss_cache is empty/only header
|
||||||
local attack_severity=0
|
local attack_severity=0
|
||||||
local unique_ips=0
|
local unique_ips=0
|
||||||
|
|
||||||
# Multi-tier distributed DDoS detection with adaptive learning
|
# Multi-tier distributed DDoS detection with adaptive learning
|
||||||
if [ "$total_syn" -gt 500 ]; then
|
# CRITICAL FIX: Use >= not > to include boundary values
|
||||||
|
# Bug: total_syn=500 was severity 0 instead of 4 (off-by-one)
|
||||||
|
if [ "$total_syn" -ge 500 ]; then
|
||||||
attack_severity=4 # Critical DDoS (new tier)
|
attack_severity=4 # Critical DDoS (new tier)
|
||||||
elif [ "$total_syn" -gt 300 ]; then
|
elif [ "$total_syn" -ge 300 ]; then
|
||||||
attack_severity=3 # Severe DDoS
|
attack_severity=3 # Severe DDoS
|
||||||
elif [ "$total_syn" -gt 150 ]; then
|
elif [ "$total_syn" -ge 150 ]; then
|
||||||
attack_severity=2 # Major DDoS
|
attack_severity=2 # Major DDoS
|
||||||
elif [ "$total_syn" -gt 75 ]; then
|
elif [ "$total_syn" -ge 75 ]; then
|
||||||
attack_severity=1 # Moderate DDoS
|
attack_severity=1 # Moderate DDoS
|
||||||
fi
|
fi
|
||||||
ATTACK_SEVERITY=$attack_severity # Store for next iteration
|
ATTACK_SEVERITY=$attack_severity # Store for next iteration
|
||||||
@@ -2573,16 +2657,32 @@ monitor_network_attacks() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Track connection count for this IP
|
# CRITICAL FIX: Don't update CONNECTION_COUNT here yet
|
||||||
CONNECTION_COUNT[$ip]=$count
|
# Bug: Previously updated array BEFORE using it for escalation detection
|
||||||
|
# Result: prev_count would equal current count (both just set), escalation detection always false
|
||||||
|
# Fix: Read previous value first (line 2876), then update after scoring (line 2886+)
|
||||||
|
# Save old value before updating - needed for escalation detection
|
||||||
|
local prev_count="${CONNECTION_COUNT[$ip]:-0}"
|
||||||
|
|
||||||
|
# Load IP's persistent data FIRST (before threshold calculation)
|
||||||
|
# This gets the current lifetime hits count from ip_data
|
||||||
|
local current_data="0|0|human||0|0"
|
||||||
|
if [ -f "$TEMP_DIR/ip_data" ]; then
|
||||||
|
current_data=$(grep "^${ip}=" "$TEMP_DIR/ip_data" 2>/dev/null | cut -d= -f2 || echo "0|0|human||0|0")
|
||||||
|
fi
|
||||||
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
||||||
|
|
||||||
# Dynamic threshold based on attack severity + momentum:
|
# Dynamic threshold based on attack severity + momentum:
|
||||||
# Tier 0: >20 connections (normal, focused attack)
|
# CRITICAL FIX: Changed Tier 0 threshold from 20 to 3
|
||||||
|
# Bug: Tier 0 (< 75 total SYN) had threshold=20, preventing detection of distributed attacks
|
||||||
|
# With 8-41 total connections spread across IPs, no single IP reaches 20, so ZERO detection
|
||||||
|
# Fix: Lower Tier 0 to 3 to detect any suspicious SYN activity
|
||||||
|
# Tier 0: >3 connections (low-level activity, may be distributed)
|
||||||
# Tier 1: >10 connections (75-150 total, moderate DDoS)
|
# Tier 1: >10 connections (75-150 total, moderate DDoS)
|
||||||
# Tier 2: >6 connections (150-300 total, major DDoS)
|
# Tier 2: >6 connections (150-300 total, major DDoS)
|
||||||
# Tier 3: >4 connections (300-500 total, severe DDoS)
|
# Tier 3: >4 connections (300-500 total, severe DDoS)
|
||||||
# Tier 4: >3 connections (500+ total, CRITICAL DDoS)
|
# Tier 4: >3 connections (500+ total, CRITICAL DDoS)
|
||||||
local threshold=20
|
local threshold=3
|
||||||
case "$attack_severity" in
|
case "$attack_severity" in
|
||||||
4) threshold=3 ;; # Critical: Very aggressive (safe for production)
|
4) threshold=3 ;; # Critical: Very aggressive (safe for production)
|
||||||
3) threshold=4 ;; # Severe: Aggressive
|
3) threshold=4 ;; # Severe: Aggressive
|
||||||
@@ -2605,99 +2705,113 @@ monitor_network_attacks() {
|
|||||||
# Minimum threshold of 3 to prevent false positives on busy web servers
|
# Minimum threshold of 3 to prevent false positives on busy web servers
|
||||||
[ "$threshold" -lt 3 ] && threshold=3
|
[ "$threshold" -lt 3 ] && threshold=3
|
||||||
|
|
||||||
|
# CRITICAL FIX: Adaptive threshold based on LIFETIME detection history
|
||||||
|
# Use persistent hits from ip_data (central database) - survives monitor restarts
|
||||||
|
# An IP that attacks 5-10 times over days should be detected at lower threshold
|
||||||
|
# This catches distributed/low-level probes that space out attempts over time
|
||||||
|
# NOTE: hits variable now loaded from persistent ip_data storage
|
||||||
|
local lifetime_hits="${hits:-0}"
|
||||||
|
if [ "$lifetime_hits" -ge 10 ]; then
|
||||||
|
threshold=1 # Seen 10+ times across ALL TIME: auto-block even 1 connection
|
||||||
|
[ "$threshold" -lt 1 ] && threshold=1
|
||||||
|
elif [ "$lifetime_hits" -ge 5 ]; then
|
||||||
|
threshold=$((threshold - 2)) # 5-9 times: lower threshold by 2 (from 3 to 1)
|
||||||
|
[ "$threshold" -lt 1 ] && threshold=1
|
||||||
|
elif [ "$lifetime_hits" -ge 3 ]; then
|
||||||
|
threshold=$((threshold - 1)) # 3-4 times: lower threshold by 1
|
||||||
|
[ "$threshold" -lt 2 ] && threshold=2
|
||||||
|
elif [ "$lifetime_hits" -ge 2 ]; then
|
||||||
|
threshold=$((threshold - 1)) # 2 times: lower threshold slightly
|
||||||
|
[ "$threshold" -lt 2 ] && threshold=2
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$count" -gt "$threshold" ]; then
|
if [ "$count" -gt "$threshold" ]; then
|
||||||
# Only process once per detection window
|
# Only process once per detection window
|
||||||
if [ -z "${ALERT_SENT[$ip]}" ]; then
|
if [ -z "${ALERT_SENT[$ip]}" ]; then
|
||||||
ALERT_SENT[$ip]=1
|
ALERT_SENT[$ip]=1
|
||||||
|
|
||||||
# Update IP reputation via file (subshell can't access IP_DATA array)
|
# Define ip_file for this IP's individual tracking file
|
||||||
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
||||||
local current_data="0|0|human||0|0"
|
|
||||||
if [ -f "$ip_file" ]; then
|
|
||||||
current_data=$(cat "$ip_file")
|
|
||||||
fi
|
|
||||||
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
||||||
|
|
||||||
# Increment hits
|
# Smart whitelisting: Skip SCORING for IPs with MANY successful established connections
|
||||||
hits=$((hits + 1))
|
# But still track them - don't skip the write!
|
||||||
|
|
||||||
# Smart whitelisting: Skip IPs with MANY successful established connections
|
|
||||||
# Only whitelist if IP has 20+ established connections (highly unlikely for attacker)
|
# Only whitelist if IP has 20+ established connections (highly unlikely for attacker)
|
||||||
# CRITICAL FIX: Use -w flag to match whole word (prevent partial IP matches)
|
# CRITICAL FIX: Use -w flag to match whole word (prevent partial IP matches)
|
||||||
# Example: "1.1.1.1" should not match "11.1.1.1" or "119.1.1.1"
|
# Example: "1.1.1.1" should not match "11.1.1.1" or "119.1.1.1"
|
||||||
local established_conns=$(ss -tn state established 2>/dev/null | grep -w "$ip" | wc -l)
|
local established_conns=$(ss -tn state established 2>/dev/null | grep -w "$ip" | wc -l)
|
||||||
[ -z "$established_conns" ] && established_conns=0
|
[ -z "$established_conns" ] && established_conns=0
|
||||||
|
local skip_scoring=0
|
||||||
if [ "$established_conns" -ge 20 ]; then
|
if [ "$established_conns" -ge 20 ]; then
|
||||||
# IP has 20+ established connections = highly likely legitimate user
|
# IP has 20+ established connections = highly likely legitimate user
|
||||||
continue
|
# Skip scoring but STILL write/track (for historical hits)
|
||||||
|
skip_scoring=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if whitelisted service
|
||||||
|
# CRITICAL FIX: Changed hits check from -eq 1 to -eq 0
|
||||||
|
# Bug: hits=0 means NEW IP (first detection), hits=1 means repeat detection
|
||||||
|
# Whitelist should only be checked on FIRST detection (hits=0), not repeat
|
||||||
|
# Previous: only checked on 2nd+ detection, causing false alerts on initial detection
|
||||||
|
if [ "$skip_scoring" -eq 0 ] && [ "${hits:-0}" -eq 0 ]; then
|
||||||
|
# Only check whitelist on first detection, and only if not already skipped
|
||||||
|
if is_whitelisted_service "$ip" 2>/dev/null; then
|
||||||
|
skip_scoring=1 # Skip scoring but STILL write/track
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Enhanced threat intelligence on first detection
|
# Enhanced threat intelligence on first detection
|
||||||
if [ "${hits:-0}" -eq 1 ]; then
|
# CRITICAL FIX: Changed hits check from -eq 1 to -eq 0
|
||||||
# Check if whitelisted service first
|
# Only query threat intelligence on FIRST detection to avoid redundant API calls
|
||||||
if is_whitelisted_service "$ip" 2>/dev/null; then
|
# CRITICAL FIX #2: Moved reputation bonus calculation OUT of background subshell
|
||||||
continue # Skip whitelisted IPs
|
# Bug: Bonuses were calculated in background and written to $ip_file, but never added to final score
|
||||||
fi
|
# Fix: Calculate bonuses synchronously and add directly to $score variable
|
||||||
|
local threat_intel_bonus=0
|
||||||
|
if [ "$skip_scoring" -eq 0 ] && [ "${hits:-0}" -eq 0 ]; then
|
||||||
|
|
||||||
# Get threat intelligence in background to avoid slowdown
|
local threat_intel=$(get_threat_intelligence "$ip" 2>/dev/null)
|
||||||
|
IFS='|' read -r abuse_conf abuse_rpts country isp geo timing whitelisted <<< "$threat_intel"
|
||||||
|
|
||||||
|
# Store enrichment for later use
|
||||||
|
echo "$threat_intel" > "$TEMP_DIR/threat_enrich_${ip//\./_}"
|
||||||
|
|
||||||
|
# Geographic clustering detection (still in background to avoid blocking)
|
||||||
(
|
(
|
||||||
local threat_intel=$(get_threat_intelligence "$ip" 2>/dev/null)
|
# Check country/ASN clustering
|
||||||
IFS='|' read -r abuse_conf abuse_rpts country isp geo timing whitelisted <<< "$threat_intel"
|
|
||||||
|
|
||||||
# Store enrichment for later use
|
|
||||||
echo "$threat_intel" > "$TEMP_DIR/threat_enrich_${ip//\./_}"
|
|
||||||
|
|
||||||
# Geographic clustering detection
|
|
||||||
if [ -n "$geo" ] && [ "$geo" != "XX" ]; then
|
if [ -n "$geo" ] && [ "$geo" != "XX" ]; then
|
||||||
echo "$geo" >> "$TEMP_DIR/attack_countries"
|
echo "$geo" >> "$TEMP_DIR/attack_countries"
|
||||||
# Check if this country has 5+ attacking IPs
|
|
||||||
local country_count=$(grep -c "^${geo}$" "$TEMP_DIR/attack_countries" 2>/dev/null || echo "0")
|
local country_count=$(grep -c "^${geo}$" "$TEMP_DIR/attack_countries" 2>/dev/null || echo "0")
|
||||||
if [ "$country_count" -ge 5 ]; then
|
if [ "$country_count" -ge 5 ]; then
|
||||||
# Coordinated attack from same country - boost all IPs from there
|
|
||||||
echo "$geo" >> "$TEMP_DIR/hostile_countries"
|
echo "$geo" >> "$TEMP_DIR/hostile_countries"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ASN clustering detection
|
# ASN clustering detection
|
||||||
if [ -n "$isp" ]; then
|
if [ -n "$isp" ]; then
|
||||||
# Extract ASN number from ISP string
|
|
||||||
local asn=$(echo "$isp" | grep -oP 'AS\K\d+' 2>/dev/null | head -1 2>/dev/null || echo "")
|
local asn=$(echo "$isp" | grep -oP 'AS\K\d+' 2>/dev/null | head -1 2>/dev/null || echo "")
|
||||||
if [ -n "$asn" ]; then
|
if [ -n "$asn" ]; then
|
||||||
echo "$asn" >> "$TEMP_DIR/attack_asns"
|
echo "$asn" >> "$TEMP_DIR/attack_asns"
|
||||||
local asn_count=$(grep -c "^${asn}$" "$TEMP_DIR/attack_asns" 2>/dev/null || echo "0")
|
local asn_count=$(grep -c "^${asn}$" "$TEMP_DIR/attack_asns" 2>/dev/null || echo "0")
|
||||||
if [ "$asn_count" -ge 3 ]; then
|
if [ "$asn_count" -ge 3 ]; then
|
||||||
# Same ASN/hosting provider used by 3+ attackers
|
|
||||||
echo "$asn" >> "$TEMP_DIR/hostile_asns"
|
echo "$asn" >> "$TEMP_DIR/hostile_asns"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Apply reputation boosts based on AbuseIPDB
|
|
||||||
if [ "${abuse_conf:-0}" -ge 75 ]; then
|
|
||||||
# High confidence malicious - add 30 points
|
|
||||||
local curr_data=$(cat "$ip_file" 2>/dev/null || echo "0|0|human||0|0")
|
|
||||||
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$curr_data"
|
|
||||||
local new_score=$((old_score + 30))
|
|
||||||
[ "$new_score" -gt 100 ] && new_score=100
|
|
||||||
echo "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" > "$ip_file"
|
|
||||||
elif [ "${abuse_conf:-0}" -ge 50 ]; then
|
|
||||||
# Medium confidence - add 15 points
|
|
||||||
local curr_data=$(cat "$ip_file" 2>/dev/null || echo "0|0|human||0|0")
|
|
||||||
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$curr_data"
|
|
||||||
local new_score=$((old_score + 15))
|
|
||||||
[ "$new_score" -gt 100 ] && new_score=100
|
|
||||||
echo "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" > "$ip_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# High-risk country adds 5 points
|
|
||||||
if is_high_risk_country "${geo:-XX}" 2>/dev/null; then
|
|
||||||
local curr_data=$(cat "$ip_file" 2>/dev/null || echo "0|0|human||0|0")
|
|
||||||
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$curr_data"
|
|
||||||
local new_score=$((old_score + 5))
|
|
||||||
[ "$new_score" -gt 100 ] && new_score=100
|
|
||||||
echo "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" > "$ip_file"
|
|
||||||
fi
|
|
||||||
) &
|
) &
|
||||||
|
|
||||||
|
# Calculate reputation bonuses NOW (synchronously) so they get added to score
|
||||||
|
# Apply reputation boosts based on AbuseIPDB
|
||||||
|
if [ "${abuse_conf:-0}" -ge 75 ]; then
|
||||||
|
# High confidence malicious - add 30 points
|
||||||
|
threat_intel_bonus=30
|
||||||
|
elif [ "${abuse_conf:-0}" -ge 50 ]; then
|
||||||
|
# Medium confidence - add 15 points
|
||||||
|
threat_intel_bonus=15
|
||||||
|
fi
|
||||||
|
|
||||||
|
# High-risk country adds 5 points
|
||||||
|
if is_high_risk_country "${geo:-XX}" 2>/dev/null; then
|
||||||
|
threat_intel_bonus=$((threat_intel_bonus + 5))
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Reputation pre-boost: IPs with existing HTTP attacks get higher SYN scoring
|
# Reputation pre-boost: IPs with existing HTTP attacks get higher SYN scoring
|
||||||
@@ -2706,161 +2820,192 @@ monitor_network_attacks() {
|
|||||||
http_attack_bonus=25 # Already known attacker, very suspicious
|
http_attack_bonus=25 # Already known attacker, very suspicious
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Record attack intelligence
|
# CRITICAL FIX: Declare variables before skip_scoring block
|
||||||
record_attack_timestamp "$ip"
|
# Bug: multi_vector, geo_bonus, ratio, target_ports, and has_other_traffic
|
||||||
record_attack_vector "$ip" "NETWORK"
|
# were declared inside skip_scoring but used outside in intel_tags logic
|
||||||
track_subnet_attack "$ip"
|
# When skip_scoring=1, local vars never initialized, causing undefined variable errors
|
||||||
|
# Fix: Move declarations outside skip_scoring so they're always available
|
||||||
# Add SYN_FLOOD to attacks if not already present
|
local multi_vector=0
|
||||||
if [[ ! "$attacks" =~ SYN_FLOOD ]]; then
|
local geo_bonus=0
|
||||||
[ -z "$attacks" ] && attacks="SYN_FLOOD" || attacks="${attacks},SYN_FLOOD"
|
local ratio=0
|
||||||
fi
|
local target_ports=0
|
||||||
|
|
||||||
# Progressive scoring based on connection count
|
|
||||||
# 20-50 conns: +15 pts, 50-100: +25 pts, 100+: +40 pts
|
|
||||||
local conn_bonus=0
|
|
||||||
if [ "$count" -ge 100 ]; then
|
|
||||||
conn_bonus=40
|
|
||||||
elif [ "$count" -ge 50 ]; then
|
|
||||||
conn_bonus=25
|
|
||||||
else
|
|
||||||
conn_bonus=15
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Distributed attack severity bonus
|
|
||||||
# Higher severity = more dangerous, boost scores
|
|
||||||
# Tier 4 (500+ SYN) is extreme - should auto-block immediately
|
|
||||||
case "$attack_severity" in
|
|
||||||
4) conn_bonus=$((conn_bonus + 50)) ;; # Critical DDoS (INSTANT BLOCK)
|
|
||||||
3) conn_bonus=$((conn_bonus + 30)) ;; # Severe DDoS
|
|
||||||
2) conn_bonus=$((conn_bonus + 15)) ;; # Major DDoS
|
|
||||||
1) conn_bonus=$((conn_bonus + 8)) ;; # Moderate DDoS
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Attack momentum bonus (growing attack = more dangerous)
|
|
||||||
if [ "$attack_momentum" -eq 2 ]; then
|
|
||||||
conn_bonus=$((conn_bonus + 15)) # Rapidly accelerating
|
|
||||||
elif [ "$attack_momentum" -eq 1 ]; then
|
|
||||||
conn_bonus=$((conn_bonus + 8)) # Accelerating
|
|
||||||
fi
|
|
||||||
|
|
||||||
# SYN FLOOD SPECIFIC INTELLIGENCE METRICS
|
|
||||||
|
|
||||||
# 1. Pure SYN attacker (no ESTABLISHED connections)
|
|
||||||
# Legitimate users always have some established connections
|
|
||||||
# Pure SYN = 100% attack traffic
|
|
||||||
if [ "$established_conns" -eq 0 ] && [ "$count" -ge 5 ]; then
|
|
||||||
conn_bonus=$((conn_bonus + 20)) # Pure SYN flood, no legitimate traffic
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. SYN/ESTABLISHED ratio detection
|
|
||||||
# Normal: More ESTABLISHED than SYN_RECV
|
|
||||||
# Attacker: More SYN_RECV than ESTABLISHED (or 0 established)
|
|
||||||
if [ "$established_conns" -gt 0 ]; then
|
|
||||||
# Calculate ratio (multiply by 10 for integer math)
|
|
||||||
local ratio=$((count * 10 / established_conns))
|
|
||||||
if [ "$ratio" -ge 30 ]; then
|
|
||||||
conn_bonus=$((conn_bonus + 15)) # 3:1 ratio = suspicious
|
|
||||||
elif [ "$ratio" -ge 20 ]; then
|
|
||||||
conn_bonus=$((conn_bonus + 10)) # 2:1 ratio = questionable
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Connection persistence without completion
|
|
||||||
# Check if IP has been seen before with SYN but never completed
|
|
||||||
if [ "${hits:-0}" -ge 2 ] && [ "$established_conns" -eq 0 ]; then
|
|
||||||
conn_bonus=$((conn_bonus + 15)) # Repeated SYN, never establishes = bot
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. Spoofed source detection (high SYN, low other traffic)
|
|
||||||
# Check if IP has ANY other traffic (HTTP requests, DNS, etc)
|
|
||||||
local has_other_traffic=0
|
local has_other_traffic=0
|
||||||
if [ -f "$TEMP_DIR/ip_${ip//\./_}" ]; then
|
|
||||||
local ip_attacks=$(grep -oP 'attacks=\K[^|]+' "$TEMP_DIR/ip_${ip//\./_}" 2>/dev/null || echo "")
|
# Only do scoring/tracking if not whitelisted
|
||||||
# If has HTTP attacks, not spoofed
|
if [ "$skip_scoring" -eq 0 ]; then
|
||||||
if [[ "$ip_attacks" =~ (SQLI|XSS|BRUTE|SCAN) ]]; then
|
# Record attack intelligence
|
||||||
|
record_attack_timestamp "$ip"
|
||||||
|
record_attack_vector "$ip" "NETWORK"
|
||||||
|
track_subnet_attack "$ip"
|
||||||
|
|
||||||
|
# Add SYN_FLOOD to attacks if not already present
|
||||||
|
if [[ ! "$attacks" =~ SYN_FLOOD ]]; then
|
||||||
|
[ -z "$attacks" ] && attacks="SYN_FLOOD" || attacks="${attacks},SYN_FLOOD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CRITICAL FIX: Fixed indentation - these lines should be INSIDE skip_scoring check
|
||||||
|
# Bug: Scoring calculations were outside the if block, still running for whitelisted IPs
|
||||||
|
# Progressive scoring based on connection count
|
||||||
|
# 20-50 conns: +15 pts, 50-100: +25 pts, 100+: +40 pts
|
||||||
|
local conn_bonus=0
|
||||||
|
if [ "$count" -ge 100 ]; then
|
||||||
|
conn_bonus=40
|
||||||
|
elif [ "$count" -ge 50 ]; then
|
||||||
|
conn_bonus=25
|
||||||
|
else
|
||||||
|
conn_bonus=15
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Distributed attack severity bonus
|
||||||
|
# Higher severity = more dangerous, boost scores
|
||||||
|
# Tier 4 (500+ SYN) is extreme - should auto-block immediately
|
||||||
|
case "$attack_severity" in
|
||||||
|
4) conn_bonus=$((conn_bonus + 50)) ;; # Critical DDoS (INSTANT BLOCK)
|
||||||
|
3) conn_bonus=$((conn_bonus + 30)) ;; # Severe DDoS
|
||||||
|
2) conn_bonus=$((conn_bonus + 15)) ;; # Major DDoS
|
||||||
|
1) conn_bonus=$((conn_bonus + 8)) ;; # Moderate DDoS
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Attack momentum bonus (growing attack = more dangerous)
|
||||||
|
if [ "$attack_momentum" -eq 2 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 15)) # Rapidly accelerating
|
||||||
|
elif [ "$attack_momentum" -eq 1 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 8)) # Accelerating
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SYN FLOOD SPECIFIC INTELLIGENCE METRICS
|
||||||
|
|
||||||
|
# 1. Pure SYN attacker (no ESTABLISHED connections)
|
||||||
|
# Legitimate users always have some established connections
|
||||||
|
# Pure SYN = 100% attack traffic
|
||||||
|
if [ "$established_conns" -eq 0 ] && [ "$count" -ge 5 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 20)) # Pure SYN flood, no legitimate traffic
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. SYN/ESTABLISHED ratio detection
|
||||||
|
# Normal: More ESTABLISHED than SYN_RECV
|
||||||
|
# Attacker: More SYN_RECV than ESTABLISHED (or 0 established)
|
||||||
|
# Note: ratio declared outside skip_scoring block (line ~2755) for scope
|
||||||
|
if [ "$established_conns" -gt 0 ]; then
|
||||||
|
# Calculate ratio (multiply by 10 for integer math)
|
||||||
|
ratio=$((count * 10 / established_conns))
|
||||||
|
if [ "$ratio" -ge 30 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 15)) # 3:1 ratio = suspicious
|
||||||
|
elif [ "$ratio" -ge 20 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 10)) # 2:1 ratio = questionable
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Connection persistence without completion
|
||||||
|
# Check if IP has been seen before with SYN but never completed
|
||||||
|
if [ "${hits:-0}" -ge 2 ] && [ "$established_conns" -eq 0 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 15)) # Repeated SYN, never establishes = bot
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Spoofed source detection (high SYN, low other traffic)
|
||||||
|
# Check if IP has ANY other traffic (HTTP requests, DNS, etc)
|
||||||
|
# CRITICAL FIX: Use already-loaded $attacks variable from ip_data (line 2597)
|
||||||
|
# Bug: was trying to read from individual ip_* file which may not exist
|
||||||
|
# If this is first SYN detection of an IP with prior HTTP attacks, file won't exist
|
||||||
|
# Result: has_other_traffic stays 0, missing indicator of multi-attack IP
|
||||||
|
# Note: has_other_traffic declared outside skip_scoring block (line ~2760) for scope
|
||||||
|
# If has HTTP attacks in history, not spoofed
|
||||||
|
if [[ "$attacks" =~ (SQLI|XSS|BRUTE|SCAN) ]]; then
|
||||||
has_other_traffic=1
|
has_other_traffic=1
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# High SYN but no other traffic = likely spoofed source
|
# High SYN but no other traffic = likely spoofed source
|
||||||
if [ "$has_other_traffic" -eq 0 ] && [ "$count" -ge 10 ] && [ "${hits:-0}" -ge 2 ]; then
|
if [ "$has_other_traffic" -eq 0 ] && [ "$count" -ge 10 ] && [ "${hits:-0}" -ge 2 ]; then
|
||||||
conn_bonus=$((conn_bonus + 20)) # Spoofed source IP
|
conn_bonus=$((conn_bonus + 20)) # Spoofed source IP
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. Single-target focus detection
|
# 5. Single-target focus detection
|
||||||
# Botnet usually targets one service/port
|
# Botnet usually targets one service/port
|
||||||
# Check if connections are all to same port (80/443)
|
# Check if connections are all to same port (80/443)
|
||||||
local target_ports=$(ss -tn state syn-recv src "$ip" 2>/dev/null | grep -oP ':\d+\s+' | sort -u | wc -l)
|
# CRITICAL FIX: Quote the ss EXPRESSION filter for correct syntax
|
||||||
[ -z "$target_ports" ] && target_ports=0
|
# Bug: Unquoted 'src "$ip"' was treated as separate arguments, not a filter expression
|
||||||
if [ "$target_ports" -eq 1 ] && [ "$count" -ge 8 ]; then
|
# Result: ss silently ignores the filter and returns ALL syn-recv (giving wrong port count)
|
||||||
conn_bonus=$((conn_bonus + 10)) # Single port = targeted attack
|
# Fix: Quote the expression so ss parses it correctly: 'src IP'
|
||||||
elif [ "$target_ports" -le 2 ] && [ "$count" -ge 15 ]; then
|
# Note: target_ports declared outside skip_scoring block (line ~2760) for scope
|
||||||
conn_bonus=$((conn_bonus + 5)) # 1-2 ports = focused attack
|
target_ports=$(ss -tn "state syn-recv src $ip" 2>/dev/null | grep -oP ':\d+\s+' | sort -u | wc -l)
|
||||||
fi
|
[ -z "$target_ports" ] && target_ports=0
|
||||||
|
if [ "$target_ports" -eq 1 ] && [ "$count" -ge 8 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 10)) # Single port = targeted attack
|
||||||
|
elif [ "$target_ports" -le 2 ] && [ "$count" -ge 15 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 5)) # 1-2 ports = focused attack
|
||||||
|
fi
|
||||||
|
|
||||||
# Multi-vector attack detection: Check if IP also has HTTP attacks
|
# Multi-vector attack detection: Check if IP also has HTTP attacks
|
||||||
# This indicates sophisticated attacker (SYN flood + application layer)
|
# This indicates sophisticated attacker (SYN flood + application layer)
|
||||||
local multi_vector=0
|
# CRITICAL FIX: Use already-loaded $attacks variable from ip_data (line 2597)
|
||||||
if [ -f "$TEMP_DIR/ip_${ip//\./_}" ]; then
|
# Bug: was trying to read from individual ip_* file which may not exist
|
||||||
local existing_attacks=$(grep -oP 'attacks=\K[^|]+' "$TEMP_DIR/ip_${ip//\./_}" 2>/dev/null || echo "")
|
# If this is first SYN detection of an IP with prior HTTP attacks, file won't exist
|
||||||
if [[ "$existing_attacks" =~ (SQLI|XSS|RCE|LFI|RFI|WEBSHELL) ]]; then
|
# Result: multi_vector stays 0, missing the sophisticated attacker indicator
|
||||||
|
# Note: multi_vector declared outside skip_scoring block (line ~2755) for scope
|
||||||
|
if [[ "$attacks" =~ (SQLI|XSS|RCE|LFI|RFI|WEBSHELL) ]]; then
|
||||||
multi_vector=1
|
multi_vector=1
|
||||||
conn_bonus=$((conn_bonus + 30)) # Multi-vector = very dangerous
|
conn_bonus=$((conn_bonus + 30)) # Multi-vector = very dangerous
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# Connection persistence bonus (repeated detections of same IP)
|
# Connection persistence bonus (repeated detections of same IP)
|
||||||
# This indicates sustained attack vs transient spike
|
# This indicates sustained attack vs transient spike
|
||||||
if [ "${hits:-0}" -ge 5 ]; then
|
if [ "${hits:-0}" -ge 5 ]; then
|
||||||
conn_bonus=$((conn_bonus + 20)) # Persistent attacker
|
conn_bonus=$((conn_bonus + 20)) # Persistent attacker
|
||||||
elif [ "${hits:-0}" -ge 3 ]; then
|
elif [ "${hits:-0}" -ge 3 ]; then
|
||||||
conn_bonus=$((conn_bonus + 10)) # Repeated attack
|
conn_bonus=$((conn_bonus + 10)) # Repeated attack
|
||||||
fi
|
|
||||||
|
|
||||||
# Connection escalation detection
|
|
||||||
# Check if connection count is increasing (more aggressive attack)
|
|
||||||
local prev_count="${CONNECTION_COUNT[$ip]:-0}"
|
|
||||||
if [ "$count" -gt "$prev_count" ] && [ "$prev_count" -gt 0 ]; then
|
|
||||||
local increase=$((count - prev_count))
|
|
||||||
if [ "$increase" -ge 50 ]; then
|
|
||||||
conn_bonus=$((conn_bonus + 25)) # Rapidly escalating
|
|
||||||
elif [ "$increase" -ge 20 ]; then
|
|
||||||
conn_bonus=$((conn_bonus + 15)) # Escalating
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add HTTP attack pre-boost
|
|
||||||
conn_bonus=$((conn_bonus + http_attack_bonus))
|
|
||||||
|
|
||||||
# Geographic clustering bonus
|
|
||||||
local geo_bonus=0
|
|
||||||
if [ -f "$TEMP_DIR/threat_enrich_${ip//\./_}" ]; then
|
|
||||||
local threat_data=$(cat "$TEMP_DIR/threat_enrich_${ip//\./_}" 2>/dev/null || echo "")
|
|
||||||
# Bash IFS field splitting (100x faster than cut)
|
|
||||||
IFS='|' read -r _ _ _ ip_isp ip_geo _ <<< "$threat_data"
|
|
||||||
|
|
||||||
# Check if from hostile country (5+ attackers)
|
|
||||||
if [ -n "$ip_geo" ] && grep -q "^${ip_geo}$" "$TEMP_DIR/hostile_countries" 2>/dev/null; then
|
|
||||||
geo_bonus=$((geo_bonus + 10)) # Part of coordinated country-level attack
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if from hostile ASN (3+ attackers)
|
# Connection escalation detection
|
||||||
if [ -n "$ip_isp" ]; then
|
# Check if connection count is increasing (more aggressive attack)
|
||||||
local ip_asn=$(echo "$ip_isp" | grep -oP 'AS\K\d+' 2>/dev/null | head -1 2>/dev/null || echo "")
|
# prev_count was loaded at line 2590 (BEFORE updating CONNECTION_COUNT)
|
||||||
if [ -n "$ip_asn" ] && grep -q "^${ip_asn}$" "$TEMP_DIR/hostile_asns" 2>/dev/null; then
|
if [ "$count" -gt "$prev_count" ] && [ "$prev_count" -gt 0 ]; then
|
||||||
geo_bonus=$((geo_bonus + 15)) # Same botnet infrastructure
|
local increase=$((count - prev_count))
|
||||||
|
if [ "$increase" -ge 50 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 25)) # Rapidly escalating
|
||||||
|
elif [ "$increase" -ge 20 ]; then
|
||||||
|
conn_bonus=$((conn_bonus + 15)) # Escalating
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
conn_bonus=$((conn_bonus + geo_bonus))
|
|
||||||
|
|
||||||
# First hit or add to existing score
|
# NOW update CONNECTION_COUNT after escalation detection
|
||||||
if [ "${hits:-0}" -eq 1 ]; then
|
# Store current count for next monitoring cycle comparison
|
||||||
score=$conn_bonus
|
CONNECTION_COUNT[$ip]=$count
|
||||||
else
|
|
||||||
score=$((score + conn_bonus))
|
# Add HTTP attack pre-boost
|
||||||
fi
|
conn_bonus=$((conn_bonus + http_attack_bonus))
|
||||||
|
|
||||||
|
# Geographic clustering bonus
|
||||||
|
# Note: geo_bonus declared outside skip_scoring block (line ~2755) for scope
|
||||||
|
if [ -f "$TEMP_DIR/threat_enrich_${ip//\./_}" ]; then
|
||||||
|
local threat_data=$(cat "$TEMP_DIR/threat_enrich_${ip//\./_}" 2>/dev/null || echo "")
|
||||||
|
# Bash IFS field splitting (100x faster than cut)
|
||||||
|
IFS='|' read -r _ _ _ ip_isp ip_geo _ <<< "$threat_data"
|
||||||
|
|
||||||
|
# Check if from hostile country (5+ attackers)
|
||||||
|
if [ -n "$ip_geo" ] && grep -q "^${ip_geo}$" "$TEMP_DIR/hostile_countries" 2>/dev/null; then
|
||||||
|
geo_bonus=$((geo_bonus + 10)) # Part of coordinated country-level attack
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if from hostile ASN (3+ attackers)
|
||||||
|
if [ -n "$ip_isp" ]; then
|
||||||
|
local ip_asn=$(echo "$ip_isp" | grep -oP 'AS\K\d+' 2>/dev/null | head -1 2>/dev/null || echo "")
|
||||||
|
if [ -n "$ip_asn" ] && grep -q "^${ip_asn}$" "$TEMP_DIR/hostile_asns" 2>/dev/null; then
|
||||||
|
geo_bonus=$((geo_bonus + 15)) # Same botnet infrastructure
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
conn_bonus=$((conn_bonus + geo_bonus))
|
||||||
|
|
||||||
|
# First hit or add to existing score
|
||||||
|
# CRITICAL FIX: Reversed the condition - repeat detections should ADD, not RESET
|
||||||
|
# Bug: hits=0 means NEW IP (initialize score), hits=1+ means REPEAT (accumulate)
|
||||||
|
# Previous: reset score on repeat detection, losing threat history
|
||||||
|
# Now: initialize only on first detection, accumulate on repeats
|
||||||
|
if [ "${hits:-0}" -eq 0 ]; then
|
||||||
|
score=$conn_bonus # First detection: initialize to connection bonus
|
||||||
|
else
|
||||||
|
score=$((score + conn_bonus)) # Repeat detection: ADD to accumulated score
|
||||||
|
fi
|
||||||
|
|
||||||
# Apply advanced intelligence bonuses
|
# Apply advanced intelligence bonuses
|
||||||
local block_reasons=""
|
local block_reasons=""
|
||||||
@@ -2868,6 +3013,13 @@ monitor_network_attacks() {
|
|||||||
IFS='|' read -r vel_count vel_bonus vel_reason <<< "$velocity_data"
|
IFS='|' read -r vel_count vel_bonus vel_reason <<< "$velocity_data"
|
||||||
[ "$vel_bonus" -gt 0 ] && score=$((score + vel_bonus)) && block_reasons="${vel_reason}"
|
[ "$vel_bonus" -gt 0 ] && score=$((score + vel_bonus)) && block_reasons="${vel_reason}"
|
||||||
|
|
||||||
|
# Apply threat intelligence bonuses (AbuseIPDB, geolocation)
|
||||||
|
if [ "$threat_intel_bonus" -gt 0 ]; then
|
||||||
|
score=$((score + threat_intel_bonus))
|
||||||
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
||||||
|
block_reasons="${block_reasons}THREAT_INTEL(+${threat_intel_bonus})"
|
||||||
|
fi
|
||||||
|
|
||||||
local div_data=$(calculate_diversity_bonus "$ip")
|
local div_data=$(calculate_diversity_bonus "$ip")
|
||||||
IFS='|' read -r div_count div_bonus div_reason <<< "$div_data"
|
IFS='|' read -r div_count div_bonus div_reason <<< "$div_data"
|
||||||
if [ "$div_bonus" -gt 0 ]; then
|
if [ "$div_bonus" -gt 0 ]; then
|
||||||
@@ -2893,10 +3045,22 @@ monitor_network_attacks() {
|
|||||||
block_reasons="${block_reasons}${timing_reason}"
|
block_reasons="${block_reasons}${timing_reason}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Cap at 100
|
# Cap at 100
|
||||||
[ "$score" -gt 100 ] && score=100
|
[ "$score" -gt 100 ] && score=100
|
||||||
|
fi # End of skip_scoring check
|
||||||
|
|
||||||
# Write to file for main process
|
# INCREMENT HITS AFTER ALL SCORING
|
||||||
|
# Moved from before whitelisting to ensure we have complete data
|
||||||
|
# Now hits is incremented with full score calculated and ready to persist
|
||||||
|
hits=$((hits + 1))
|
||||||
|
|
||||||
|
# CRITICAL FIX: Write to centralized ip_data file (not individual ip_*.files)
|
||||||
|
# auto_mitigation_engine() reads from $TEMP_DIR/ip_data, not individual files
|
||||||
|
# Without this, SYN-detected IPs are never auto-blocked!
|
||||||
|
# SINGLE WRITE: Complete data with correct score and incremented hits
|
||||||
|
write_ip_data_to_file "$ip" "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" 2>/dev/null &
|
||||||
|
|
||||||
|
# Also write to individual file for debugging/tracking
|
||||||
echo "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" > "$ip_file"
|
echo "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" > "$ip_file"
|
||||||
|
|
||||||
# Store block reasons for auto-mitigation
|
# Store block reasons for auto-mitigation
|
||||||
@@ -2919,8 +3083,25 @@ monitor_network_attacks() {
|
|||||||
[ "$coordinated_attack" -eq 1 ] && intel_tags="${intel_tags}BOTNET "
|
[ "$coordinated_attack" -eq 1 ] && intel_tags="${intel_tags}BOTNET "
|
||||||
[ "$multi_vector" -eq 1 ] && intel_tags="${intel_tags}MULTI-VECTOR "
|
[ "$multi_vector" -eq 1 ] && intel_tags="${intel_tags}MULTI-VECTOR "
|
||||||
[ "$http_attack_bonus" -gt 0 ] && intel_tags="${intel_tags}HTTP-ATTACKER "
|
[ "$http_attack_bonus" -gt 0 ] && intel_tags="${intel_tags}HTTP-ATTACKER "
|
||||||
[ "$geo_bonus" -ge 15 ] && intel_tags="${intel_tags}HOSTILE-ASN "
|
# CRITICAL FIX: Fixed conditional precedence for geo tagging
|
||||||
[ "$geo_bonus" -ge 10 ] && [ "$geo_bonus" -lt 15 ] && intel_tags="${intel_tags}HOSTILE-GEO "
|
# Bug: Using elif logic caused mutual exclusion - couldn't show both tags
|
||||||
|
# If geo_bonus = 25 (both hostile country + ASN), only showed "HOSTILE-ASN"
|
||||||
|
# Should show BOTH tags if both conditions are true
|
||||||
|
local is_hostile_asn=0
|
||||||
|
local is_hostile_geo=0
|
||||||
|
if [ "$geo_bonus" -ge 15 ]; then
|
||||||
|
is_hostile_asn=1
|
||||||
|
fi
|
||||||
|
if [ "$geo_bonus" -ge 10 ] && [ "$geo_bonus" -lt 15 ]; then
|
||||||
|
is_hostile_geo=1
|
||||||
|
fi
|
||||||
|
# Special case: if geo_bonus >= 25, it's from BOTH sources (10 + 15)
|
||||||
|
if [ "$geo_bonus" -ge 25 ]; then
|
||||||
|
is_hostile_asn=1
|
||||||
|
is_hostile_geo=1
|
||||||
|
fi
|
||||||
|
[ "$is_hostile_asn" -eq 1 ] && intel_tags="${intel_tags}HOSTILE-ASN "
|
||||||
|
[ "$is_hostile_geo" -eq 1 ] && intel_tags="${intel_tags}HOSTILE-GEO "
|
||||||
|
|
||||||
# SYN-specific intelligence tags
|
# SYN-specific intelligence tags
|
||||||
[ "$established_conns" -eq 0 ] && [ "$count" -ge 5 ] && intel_tags="${intel_tags}PURE-SYN "
|
[ "$established_conns" -eq 0 ] && [ "$count" -ge 5 ] && intel_tags="${intel_tags}PURE-SYN "
|
||||||
@@ -3543,6 +3724,21 @@ if [ -n "$IPSET_INIT_ERROR" ]; then
|
|||||||
sleep 3 # Give user time to read
|
sleep 3 # Give user time to read
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# BUG FIX #17: Load persistent threat data BEFORE starting auto_mitigation_engine
|
||||||
|
# This ensures pre-existing high-score IPs (Score >= 50) are blocked on startup
|
||||||
|
load_snapshot
|
||||||
|
|
||||||
|
# Immediately write loaded IP_DATA to ip_data file for auto_mitigation_engine to process
|
||||||
|
if [ ${#IP_DATA[@]} -gt 0 ]; then
|
||||||
|
{
|
||||||
|
flock -w 2 200 || exit 1
|
||||||
|
for ip in "${!IP_DATA[@]}"; do
|
||||||
|
echo "$ip=${IP_DATA[$ip]}"
|
||||||
|
done
|
||||||
|
} > "$TEMP_DIR/ip_data" 2>/dev/null 200>"$TEMP_DIR/ip_data.lock"
|
||||||
|
echo "[INFO] Restored ${#IP_DATA[@]} threat IPs from persistent storage (Score >= 50)" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# Start intelligence engines
|
# Start intelligence engines
|
||||||
detect_distributed_attacks
|
detect_distributed_attacks
|
||||||
auto_mitigation_engine
|
auto_mitigation_engine
|
||||||
|
|||||||
+1415
-498
File diff suppressed because it is too large
Load Diff
@@ -573,8 +573,8 @@ analyze_images() {
|
|||||||
print_section "Image Format Analysis"
|
print_section "Image Format Analysis"
|
||||||
print_info "Scanning for unoptimized images..."
|
print_info "Scanning for unoptimized images..."
|
||||||
|
|
||||||
# Count image types
|
# Count image types (use parentheses to ensure -maxdepth applies to all -o branches)
|
||||||
local jpg_count=$(find "$docroot" -maxdepth 5 -iname "*.jpg" -o -iname "*.jpeg" 2>/dev/null | wc -l)
|
local jpg_count=$(find "$docroot" -maxdepth 5 \( -iname "*.jpg" -o -iname "*.jpeg" \) 2>/dev/null | wc -l)
|
||||||
local png_count=$(find "$docroot" -maxdepth 5 -iname "*.png" 2>/dev/null | wc -l)
|
local png_count=$(find "$docroot" -maxdepth 5 -iname "*.png" 2>/dev/null | wc -l)
|
||||||
local gif_count=$(find "$docroot" -maxdepth 5 -iname "*.gif" 2>/dev/null | wc -l)
|
local gif_count=$(find "$docroot" -maxdepth 5 -iname "*.gif" 2>/dev/null | wc -l)
|
||||||
local webp_count=$(find "$docroot" -maxdepth 5 -iname "*.webp" 2>/dev/null | wc -l)
|
local webp_count=$(find "$docroot" -maxdepth 5 -iname "*.webp" 2>/dev/null | wc -l)
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ if ! flock -n 9; then
|
|||||||
print_error "Another instance of this script is already running"
|
print_error "Another instance of this script is already running"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# NOTE: Trap is set later at line ~373, MUST include flock unlock!
|
|
||||||
|
# Note: Trap is set later at line ~469 to handle flock, fd closure, and lock file cleanup
|
||||||
|
|
||||||
# OPTIMIZATION: Parse command-line flags for script behavior
|
# OPTIMIZATION: Parse command-line flags for script behavior
|
||||||
# Support: --dry-run, --parallel, --log, --help
|
# Support: --dry-run, --parallel, --log, --help
|
||||||
@@ -277,48 +278,66 @@ function_get_description() {
|
|||||||
echo "${FUNCTION_REGISTRY[$func]}"
|
echo "${FUNCTION_REGISTRY[$func]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# PERFORMANCE OPTIMIZATION: Limited-depth find instead of recursive or glob expansion
|
# PERFORMANCE OPTIMIZATION: Use shell globs instead of recursive find
|
||||||
# Avoids both: (1) massive glob expansion that hangs with 200+ users, and (2) unlimited recursion
|
# Checks ONLY the two known wp-config.php positions per install type:
|
||||||
# Uses -maxdepth to limit search depth: primary domains are always at depth 2-3, never deeper
|
# depth 0: docroot/wp-config.php (main domain)
|
||||||
# For cPanel: /home/USER/public_html or /home/USER/public_html/ADDON (depth 2-3)
|
# depth 1: docroot/SUBDIR/wp-config.php (addon domain / subfolder)
|
||||||
# For InterWorx: /home/USER/DOMAIN/html (depth 3)
|
# Generates O(N) stat() calls where N = number of user/domain directories,
|
||||||
# For Plesk: /var/www/vhosts/DOMAIN/httpdocs (depth 3)
|
# vs O(F) stat() calls with find where F = total files in all web directories.
|
||||||
# Typical improvement: 5-10x faster than unlimited find (30-120s → 5-15s for 200+ users)
|
# Typical improvement: 10-50x faster (30-120s find → 500ms-2s glob)
|
||||||
|
# Empty glob safety: [ -f "$f" ] guard handles bash returning literal pattern
|
||||||
|
# string when glob has no matches (default bash behavior without nullglob).
|
||||||
get_wp_search_paths() {
|
get_wp_search_paths() {
|
||||||
# Lazy-initialize system detection only when needed (not at startup)
|
# Lazy-initialize system detection only when needed (not at startup)
|
||||||
ensure_system_detection
|
ensure_system_detection
|
||||||
|
|
||||||
local panel="${1:-$SYS_CONTROL_PANEL}"
|
local panel="${1:-$SYS_CONTROL_PANEL}"
|
||||||
|
local count=0
|
||||||
local max_results=1000
|
local max_results=1000
|
||||||
|
|
||||||
case "$panel" in
|
case "$panel" in
|
||||||
cpanel)
|
cpanel)
|
||||||
# Search with limited depth to find WordPress installations
|
# Depth 0: main domain /home/USER/public_html/wp-config.php
|
||||||
# Depth structure: /home (0) -> USER (1) -> public_html (2) -> [ADDON] (3) -> wp-config.php
|
# Depth 1: addon domain /home/USER/public_html/ADDONDIR/wp-config.php
|
||||||
# maxdepth 4 finds: main domains at depth 2, addon domains at depth 3
|
for f in /home/*/public_html/wp-config.php \
|
||||||
# Prevents recursion into wp-content (depth 3+), plugins, uploads, etc.
|
/home/*/public_html/*/wp-config.php; do
|
||||||
find /home -maxdepth 4 -name "wp-config.php" -type f 2>/dev/null | head -$max_results
|
[ -f "$f" ] || continue
|
||||||
|
echo "$f"
|
||||||
|
count=$(( count + 1 ))
|
||||||
|
[ "$count" -ge "$max_results" ] && return 0
|
||||||
|
done
|
||||||
;;
|
;;
|
||||||
interworx)
|
interworx)
|
||||||
# Standard: /home (0) -> USER (1) -> DOMAIN (2) -> html (3) -> wp-config.php (maxdepth 3)
|
# Standard path: /home/USER/DOMAIN/html/wp-config.php
|
||||||
# Chroot: /chroot (0) -> home (1) -> USER (2) -> var (3) -> DOMAIN (4) -> html (4) -> wp-config.php (maxdepth 4)
|
# Chroot path: /chroot/home/USER/var/DOMAIN/html/wp-config.php
|
||||||
{
|
for f in /home/*/*/html/wp-config.php \
|
||||||
find /home -maxdepth 3 -name "wp-config.php" -type f 2>/dev/null
|
/chroot/home/*/var/*/html/wp-config.php; do
|
||||||
find /chroot/home -maxdepth 4 -name "wp-config.php" -type f 2>/dev/null
|
[ -f "$f" ] || continue
|
||||||
} | head -$max_results
|
echo "$f"
|
||||||
|
count=$(( count + 1 ))
|
||||||
|
[ "$count" -ge "$max_results" ] && return 0
|
||||||
|
done
|
||||||
;;
|
;;
|
||||||
plesk)
|
plesk)
|
||||||
# Structure: /var (0) -> www (1) -> vhosts (2) -> DOMAIN (2) -> httpdocs (2) -> wp-config.php (maxdepth 2)
|
# Flat structure - one docroot per domain directory
|
||||||
find /var/www/vhosts -maxdepth 2 -name "wp-config.php" -type f 2>/dev/null | head -$max_results
|
for f in /var/www/vhosts/*/httpdocs/wp-config.php; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
echo "$f"
|
||||||
|
count=$(( count + 1 ))
|
||||||
|
[ "$count" -ge "$max_results" ] && return 0
|
||||||
|
done
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
# Standalone: multiple possible locations, all with limited depth
|
# Standalone: check /var/www/html and /home-based installs
|
||||||
# /var/www/html (0) -> wp-config.php or SUBDIR (1) -> wp-config.php (maxdepth 2)
|
for f in /var/www/html/wp-config.php \
|
||||||
# /home (0) -> USER (1) -> public_html (2) -> wp-config.php or ADDON (3) -> wp-config.php (maxdepth 4)
|
/var/www/html/*/wp-config.php \
|
||||||
{
|
/home/*/public_html/wp-config.php \
|
||||||
find /var/www/html -maxdepth 2 -name "wp-config.php" -type f 2>/dev/null
|
/home/*/public_html/*/wp-config.php; do
|
||||||
find /home -maxdepth 4 -name "wp-config.php" -type f 2>/dev/null
|
[ -f "$f" ] || continue
|
||||||
} | head -$max_results
|
echo "$f"
|
||||||
|
count=$(( count + 1 ))
|
||||||
|
[ "$count" -ge "$max_results" ] && return 0
|
||||||
|
done
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -438,7 +457,7 @@ get_wp_sites_cached() {
|
|||||||
|
|
||||||
# Cleanup on exit (keep cache file for next invocation, only remove lock file)
|
# Cleanup on exit (keep cache file for next invocation, only remove lock file)
|
||||||
# CRITICAL: Must unlock flock (fd 9) before removing lock file!
|
# CRITICAL: Must unlock flock (fd 9) before removing lock file!
|
||||||
trap 'flock -u 9 2>/dev/null; exec 9>&-; rm -f "$LOCK_FILE"; rollback_cleanup' EXIT INT TERM
|
trap 'flock -u 9 2>/dev/null; exec 9>&-; rm -f "$LOCK_FILE"' EXIT INT TERM
|
||||||
|
|
||||||
# OPTIMIZATION: User extraction caching (memoization)
|
# OPTIMIZATION: User extraction caching (memoization)
|
||||||
# extract_user_from_path() called 10 times, often for same path
|
# extract_user_from_path() called 10 times, often for same path
|
||||||
@@ -487,8 +506,14 @@ safe_add_cron_job() {
|
|||||||
|
|
||||||
# Add the job to crontab
|
# Add the job to crontab
|
||||||
# CRITICAL: crontab -l already verified to have succeeded above
|
# CRITICAL: crontab -l already verified to have succeeded above
|
||||||
(echo "$current_crontab"; echo "$cron_time $cron_cmd") | crontab -u "$user" - 2>/dev/null
|
# Use temporary file instead of pipe to avoid pipefail issues and ensure proper error reporting
|
||||||
return $?
|
local temp_crontab
|
||||||
|
temp_crontab=$(mktemp) || return 1
|
||||||
|
(echo "$current_crontab"; echo "$cron_time $cron_cmd") > "$temp_crontab"
|
||||||
|
crontab -u "$user" "$temp_crontab" 2>/dev/null
|
||||||
|
local result=$?
|
||||||
|
rm -f "$temp_crontab"
|
||||||
|
return $result
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to safely remove cron jobs from user's crontab
|
# Function to safely remove cron jobs from user's crontab
|
||||||
@@ -508,9 +533,17 @@ safe_remove_cron_jobs() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove jobs matching pattern
|
# Remove jobs matching pattern
|
||||||
# CRITICAL: crontab -l already verified to have succeeded above
|
# CRITICAL FIX: grep -v returns 1 when ALL lines are filtered (nothing matches the NOT pattern)
|
||||||
echo "$current_crontab" | grep -v "$pattern" | crontab -u "$user" - 2>/dev/null
|
# With set -o pipefail, this makes the pipe fail even though crontab should succeed
|
||||||
return $?
|
# Solution: Use temporary file to break the pipe and avoid pipefail issues
|
||||||
|
local temp_crontab
|
||||||
|
temp_crontab=$(mktemp) || return 1
|
||||||
|
echo "$current_crontab" | grep -v "$pattern" > "$temp_crontab" 2>/dev/null
|
||||||
|
# Note: grep -v can return 1 if output is empty - this is not an error for crontab
|
||||||
|
crontab -u "$user" "$temp_crontab" 2>/dev/null
|
||||||
|
local result=$?
|
||||||
|
rm -f "$temp_crontab"
|
||||||
|
return $result
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to validate wp-config.php syntax before and after modification
|
# Function to validate wp-config.php syntax before and after modification
|
||||||
|
|||||||
@@ -1,421 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# TEST LAUNCHER - Cross-Platform Verification
|
|
||||||
# Tests multi-platform reference database building without modifying launcher.sh
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
# Get script directory
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
export TOOLKIT_BASE_DIR="$SCRIPT_DIR"
|
|
||||||
|
|
||||||
# Source libraries
|
|
||||||
LIB_DIR="$SCRIPT_DIR/lib"
|
|
||||||
source "$LIB_DIR/common-functions.sh"
|
|
||||||
source "$LIB_DIR/system-detect.sh"
|
|
||||||
source "$LIB_DIR/domain-discovery.sh"
|
|
||||||
source "$LIB_DIR/user-manager.sh"
|
|
||||||
|
|
||||||
# Test database location
|
|
||||||
TEST_SYSREF_DB="${TOOLKIT_BASE_DIR}/.sysref-test"
|
|
||||||
TEST_SYSREF_TIMESTAMP="${TOOLKIT_BASE_DIR}/.sysref-test.timestamp"
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# DOMAIN STATUS CHECKING (from reference-db.sh)
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
# Returns: http_code|https_code|status_summary
|
|
||||||
check_domain_status() {
|
|
||||||
local domain="$1"
|
|
||||||
local http_code="000"
|
|
||||||
local https_code="000"
|
|
||||||
local status_summary="unchecked"
|
|
||||||
|
|
||||||
# Skip if curl not available
|
|
||||||
if ! command -v curl &>/dev/null; then
|
|
||||||
echo "000|000|no_curl"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Skip obviously invalid domains
|
|
||||||
if [ -z "$domain" ] || [[ ! "$domain" =~ \. ]]; then
|
|
||||||
echo "000|000|invalid_domain"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try HTTP (timeout 3 seconds, max 2 redirects, check for valid response)
|
|
||||||
http_code=$(timeout 3 curl -s -o /dev/null -w "%{http_code}" --max-redirs 2 -m 3 "http://$domain" 2>/dev/null)
|
|
||||||
if [ $? -ne 0 ] || [ -z "$http_code" ]; then
|
|
||||||
http_code="timeout"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try HTTPS (timeout 3 seconds, max 2 redirects, ignore cert errors)
|
|
||||||
https_code=$(timeout 3 curl -s -o /dev/null -w "%{http_code}" --max-redirs 2 -m 3 -k "https://$domain" 2>/dev/null)
|
|
||||||
if [ $? -ne 0 ] || [ -z "$https_code" ]; then
|
|
||||||
https_code="timeout"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Determine overall status
|
|
||||||
if [ "$http_code" = "200" ] || [ "$https_code" = "200" ]; then
|
|
||||||
status_summary="200_OK"
|
|
||||||
elif [ "$http_code" = "403" ] || [ "$https_code" = "403" ]; then
|
|
||||||
status_summary="403_FORBIDDEN"
|
|
||||||
elif [ "$http_code" = "404" ] || [ "$https_code" = "404" ]; then
|
|
||||||
status_summary="404_NOT_FOUND"
|
|
||||||
elif [ "$http_code" = "500" ] || [ "$https_code" = "500" ]; then
|
|
||||||
status_summary="500_ERROR"
|
|
||||||
elif [ "$http_code" = "502" ] || [ "$https_code" = "502" ]; then
|
|
||||||
status_summary="502_BAD_GATEWAY"
|
|
||||||
elif [ "$http_code" = "503" ] || [ "$https_code" = "503" ]; then
|
|
||||||
status_summary="503_UNAVAILABLE"
|
|
||||||
elif [[ "$http_code" =~ ^30[0-9]$ ]] || [[ "$https_code" =~ ^30[0-9]$ ]]; then
|
|
||||||
status_summary="REDIRECT"
|
|
||||||
elif [ "$http_code" = "timeout" ] && [ "$https_code" = "timeout" ]; then
|
|
||||||
status_summary="TIMEOUT"
|
|
||||||
elif [ "$http_code" = "000" ] && [ "$https_code" = "000" ]; then
|
|
||||||
status_summary="UNREACHABLE"
|
|
||||||
else
|
|
||||||
status_summary="OTHER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${http_code}|${https_code}|${status_summary}"
|
|
||||||
}
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# PLATFORM-SPECIFIC DOMAIN BUILDERS
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
build_domains_cpanel_test() {
|
|
||||||
print_info "Using cPanel-optimized domain discovery..."
|
|
||||||
|
|
||||||
local users=($(list_all_users))
|
|
||||||
local current=0
|
|
||||||
local total=0
|
|
||||||
|
|
||||||
# Count domains
|
|
||||||
for user in "${users[@]}"; do
|
|
||||||
local userdata_dir="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}/${user}"
|
|
||||||
if [ -d "$userdata_dir" ]; then
|
|
||||||
total=$((total + $(find "$userdata_dir" -type f ! -name "*.cache" ! -name "*.yaml" ! -name "*.json" ! -name "main*" ! -name "cache" ! -name "*_SSL" 2>/dev/null | wc -l)))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Process domains
|
|
||||||
declare -A seen_domains
|
|
||||||
for user in "${users[@]}"; do
|
|
||||||
local userdata_dir="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}/${user}"
|
|
||||||
|
|
||||||
if [ -d "$userdata_dir" ]; then
|
|
||||||
for config_file in "$userdata_dir"/*; do
|
|
||||||
[ ! -f "$config_file" ] && continue
|
|
||||||
local basename=$(basename "$config_file")
|
|
||||||
|
|
||||||
# Skip cache files
|
|
||||||
[[ "$basename" =~ \.cache$|\.yaml$|\.json$|^main|^cache$|_SSL$ ]] && continue
|
|
||||||
|
|
||||||
local domain="$basename"
|
|
||||||
local doc_root=$(grep "^documentroot:" "$config_file" | awk '{print $2}' || true)
|
|
||||||
local log_path=$(grep "target:.*domlogs" "$config_file" | head -1 | awk '{print $2}' || true)
|
|
||||||
local server_alias=$(grep "^serveralias:" "$config_file" | awk '{print $2}' || true)
|
|
||||||
local php_version=$(grep "^phpversion:" "$config_file" | awk '{print $2}' || true)
|
|
||||||
|
|
||||||
# Determine if primary domain
|
|
||||||
local is_primary="no"
|
|
||||||
local primary_domain=$(get_user_domains "$user" | head -1)
|
|
||||||
[ "$domain" = "$primary_domain" ] && is_primary="yes"
|
|
||||||
|
|
||||||
# Determine domain type (addon, parked, subdomain, primary)
|
|
||||||
local domain_type="addon"
|
|
||||||
if [ "$is_primary" = "yes" ]; then
|
|
||||||
domain_type="primary"
|
|
||||||
elif [[ "$domain" =~ \. ]] && [[ "$domain" =~ ^[^.]+\. ]]; then
|
|
||||||
# Check if it's a subdomain of the primary
|
|
||||||
local base_domain=$(echo "$domain" | rev | cut -d. -f1-2 | rev)
|
|
||||||
if [ "$base_domain" = "$primary_domain" ]; then
|
|
||||||
domain_type="subdomain"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check HTTP/HTTPS status codes (only for primary and addon domains)
|
|
||||||
current=$((current + 1))
|
|
||||||
local http_code="000"
|
|
||||||
local https_code="000"
|
|
||||||
local status_summary="skipped"
|
|
||||||
|
|
||||||
if [ "$domain_type" = "primary" ] || [ "$domain_type" = "addon" ]; then
|
|
||||||
show_progress $current $total "Checking domain status codes..."
|
|
||||||
local status_result=$(check_domain_status "$domain")
|
|
||||||
IFS='|' read -r http_code https_code status_summary <<< "$status_result"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Format: DOMAIN|domain|owner|doc_root|log_path|php_version|is_primary|type|aliases|http_code|https_code|status_summary
|
|
||||||
echo "DOMAIN|$domain|$user|$doc_root|$log_path|$php_version|$is_primary|$domain_type|$server_alias|$http_code|$https_code|$status_summary" >> "$TEST_SYSREF_DB"
|
|
||||||
seen_domains["$domain"]=1
|
|
||||||
|
|
||||||
# Also add aliases as separate entries
|
|
||||||
if [ -n "$server_alias" ]; then
|
|
||||||
for alias in $server_alias; do
|
|
||||||
[ -z "$alias" ] && continue
|
|
||||||
[ -n "${seen_domains[$alias]:-}" ] && continue
|
|
||||||
|
|
||||||
# Alias points to same document root and logs (inherit status from parent)
|
|
||||||
echo "DOMAIN|$alias|$user|$doc_root|$log_path|$php_version|no|alias|$domain|$http_code|$https_code|alias_of_$status_summary" >> "$TEST_SYSREF_DB"
|
|
||||||
seen_domains["$alias"]=1
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
finish_progress
|
|
||||||
|
|
||||||
# Check /etc/localdomains (cPanel local domains not yet added)
|
|
||||||
if [ "$SYS_CONTROL_PANEL" = "cpanel" ] && [ -f "/etc/localdomains" ]; then
|
|
||||||
while read -r domain; do
|
|
||||||
[ -z "$domain" ] && continue
|
|
||||||
[ -n "${seen_domains[$domain]:-}" ] && continue
|
|
||||||
|
|
||||||
local owner=$(grep "^${domain}:" /etc/trueuserdomains 2>/dev/null | cut -d: -f2 | xargs || true)
|
|
||||||
[ -z "$owner" ] && owner="unknown"
|
|
||||||
|
|
||||||
local log_path="${SYS_LOG_DIR}/${domain}"
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
local status_result=$(check_domain_status "$domain")
|
|
||||||
IFS='|' read -r http_code https_code status_summary <<< "$status_result"
|
|
||||||
|
|
||||||
echo "DOMAIN|$domain|$owner||$log_path||unknown|local||$http_code|$https_code|$status_summary" >> "$TEST_SYSREF_DB"
|
|
||||||
seen_domains["$domain"]=1
|
|
||||||
done < /etc/localdomains
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check /etc/remotedomains (cPanel remote MX domains)
|
|
||||||
if [ "$SYS_CONTROL_PANEL" = "cpanel" ] && [ -f "/etc/remotedomains" ]; then
|
|
||||||
while read -r domain; do
|
|
||||||
[ -z "$domain" ] && continue
|
|
||||||
[ -n "${seen_domains[$domain]:-}" ] && continue
|
|
||||||
|
|
||||||
local owner=$(grep "^${domain}:" /etc/trueuserdomains 2>/dev/null | cut -d: -f2 | xargs || true)
|
|
||||||
[ -z "$owner" ] && owner="unknown"
|
|
||||||
|
|
||||||
echo "DOMAIN|$domain|$owner||||unknown|remote||000|000|remote_mx" >> "$TEST_SYSREF_DB"
|
|
||||||
seen_domains["$domain"]=1
|
|
||||||
done < /etc/remotedomains
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
build_domains_plesk_test() {
|
|
||||||
print_info "Using Plesk-specific domain discovery..."
|
|
||||||
|
|
||||||
local all_domains=$(list_all_domains)
|
|
||||||
local domain_count=$(echo "$all_domains" | wc -w)
|
|
||||||
local current=0
|
|
||||||
|
|
||||||
for domain in $all_domains; do
|
|
||||||
[ -z "$domain" ] && continue
|
|
||||||
((current++))
|
|
||||||
|
|
||||||
show_progress $current $domain_count "Checking domain status codes..."
|
|
||||||
|
|
||||||
# Use panel-agnostic functions that call Plesk helpers
|
|
||||||
local owner=$(get_domain_owner "$domain" || echo "unknown")
|
|
||||||
local docroot=$(get_domain_docroot "$domain" || echo "")
|
|
||||||
local logdir=$(get_domain_logdir "$domain" || echo "")
|
|
||||||
local access_log=$(get_domain_access_log "$domain" || echo "")
|
|
||||||
|
|
||||||
# Try to get PHP version if plesk helper exists
|
|
||||||
local php_version=""
|
|
||||||
if type plesk_get_php_version >/dev/null 2>&1; then
|
|
||||||
php_version=$(plesk_get_php_version "$domain" || echo "")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check domain status
|
|
||||||
local status_result=$(check_domain_status "$domain")
|
|
||||||
IFS='|' read -r http_code https_code status_summary <<< "$status_result"
|
|
||||||
|
|
||||||
# Format to match production
|
|
||||||
echo "DOMAIN|$domain|$owner|$docroot|$logdir|$php_version|unknown|local||$http_code|$https_code|$status_summary" >> "$TEST_SYSREF_DB"
|
|
||||||
done
|
|
||||||
|
|
||||||
finish_progress
|
|
||||||
}
|
|
||||||
|
|
||||||
build_domains_standalone_test() {
|
|
||||||
print_info "Using standalone domain discovery..."
|
|
||||||
|
|
||||||
local all_domains=$(list_all_domains)
|
|
||||||
local domain_count=$(echo "$all_domains" | wc -w)
|
|
||||||
local current=0
|
|
||||||
|
|
||||||
if [ -z "$all_domains" ]; then
|
|
||||||
print_warning "No domains found via directory scanning"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
for domain in $all_domains; do
|
|
||||||
[ -z "$domain" ] && continue
|
|
||||||
((current++))
|
|
||||||
|
|
||||||
show_progress $current $domain_count "Checking domain status codes..."
|
|
||||||
|
|
||||||
local docroot=$(get_domain_docroot "$domain" || echo "")
|
|
||||||
local owner=$(get_domain_owner "$domain" || echo "unknown")
|
|
||||||
local logdir=$(get_domain_logdir "$domain" || echo "")
|
|
||||||
local access_log=$(get_domain_access_log "$domain" || echo "")
|
|
||||||
|
|
||||||
# Check domain status
|
|
||||||
local status_result=$(check_domain_status "$domain")
|
|
||||||
IFS='|' read -r http_code https_code status_summary <<< "$status_result"
|
|
||||||
|
|
||||||
# Format to match production
|
|
||||||
echo "DOMAIN|$domain|$owner|$docroot|$logdir||unknown|local||$http_code|$https_code|$status_summary" >> "$TEST_SYSREF_DB"
|
|
||||||
done
|
|
||||||
|
|
||||||
finish_progress
|
|
||||||
}
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# MAIN TEST FUNCTION
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
test_reference_database() {
|
|
||||||
local start_time=$(date +%s)
|
|
||||||
|
|
||||||
print_header "Cross-Platform Reference Database Test"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show detected platform
|
|
||||||
print_info "Detected Platform: $SYS_CONTROL_PANEL"
|
|
||||||
print_info "OS: $SYS_OS_TYPE $SYS_OS_VERSION"
|
|
||||||
print_info "Web Server: $SYS_WEB_SERVER $SYS_WEB_SERVER_VERSION"
|
|
||||||
print_info "Database: $SYS_DB_TYPE $SYS_DB_VERSION"
|
|
||||||
print_info "User Home Base: $SYS_USER_HOME_BASE"
|
|
||||||
print_info "Log Directory: $SYS_LOG_DIR"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Initialize test database
|
|
||||||
print_info "Building test reference database..."
|
|
||||||
echo "# Test System Reference Database" > "$TEST_SYSREF_DB"
|
|
||||||
echo "# Platform: $SYS_CONTROL_PANEL" >> "$TEST_SYSREF_DB"
|
|
||||||
echo "# Generated: $(date)" >> "$TEST_SYSREF_DB"
|
|
||||||
echo "" >> "$TEST_SYSREF_DB"
|
|
||||||
|
|
||||||
# Test users
|
|
||||||
echo "[USERS]" >> "$TEST_SYSREF_DB"
|
|
||||||
local users=($(list_all_users))
|
|
||||||
print_info "Found ${#users[@]} users"
|
|
||||||
for user in "${users[@]}"; do
|
|
||||||
echo "USER|$user" >> "$TEST_SYSREF_DB"
|
|
||||||
done
|
|
||||||
echo "" >> "$TEST_SYSREF_DB"
|
|
||||||
|
|
||||||
# Test domains - platform-specific
|
|
||||||
echo "[DOMAINS]" >> "$TEST_SYSREF_DB"
|
|
||||||
case "$SYS_CONTROL_PANEL" in
|
|
||||||
cpanel)
|
|
||||||
build_domains_cpanel_test
|
|
||||||
;;
|
|
||||||
plesk)
|
|
||||||
build_domains_plesk_test
|
|
||||||
;;
|
|
||||||
interworx)
|
|
||||||
print_warning "InterWorx support not yet implemented in test"
|
|
||||||
build_domains_standalone_test
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
build_domains_standalone_test
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
echo "" >> "$TEST_SYSREF_DB"
|
|
||||||
|
|
||||||
# Test databases
|
|
||||||
echo "[DATABASES]" >> "$TEST_SYSREF_DB"
|
|
||||||
if [ "$SYS_DB_TYPE" != "none" ]; then
|
|
||||||
local all_dbs=$(mysql -Ns -e "SHOW DATABASES" 2>/dev/null | grep -v "^information_schema$\|^mysql$\|^performance_schema$\|^sys$" || true)
|
|
||||||
local db_count=$(echo "$all_dbs" | wc -l)
|
|
||||||
print_info "Found $db_count databases"
|
|
||||||
for db in $all_dbs; do
|
|
||||||
local owner=$(get_database_owner "$db" || echo "unknown")
|
|
||||||
echo "DB|$db|$owner" >> "$TEST_SYSREF_DB"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
echo "" >> "$TEST_SYSREF_DB"
|
|
||||||
|
|
||||||
# Save timestamp
|
|
||||||
date +%s > "$TEST_SYSREF_TIMESTAMP"
|
|
||||||
|
|
||||||
local end_time=$(date +%s)
|
|
||||||
local duration=$((end_time - start_time))
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_success "Test database built in ${duration}s"
|
|
||||||
print_info "Test database location: $TEST_SYSREF_DB"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show statistics
|
|
||||||
local user_count=$(grep -c "^USER|" "$TEST_SYSREF_DB" 2>/dev/null || echo 0)
|
|
||||||
local domain_count=$(grep -c "^DOMAIN|" "$TEST_SYSREF_DB" 2>/dev/null || echo 0)
|
|
||||||
local db_count=$(grep -c "^DB|" "$TEST_SYSREF_DB" 2>/dev/null || echo 0)
|
|
||||||
|
|
||||||
print_header "Test Results"
|
|
||||||
echo " Users: $user_count"
|
|
||||||
echo " Domains: $domain_count"
|
|
||||||
echo " Databases: $db_count"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show sample domains
|
|
||||||
if [ "$domain_count" -gt 0 ]; then
|
|
||||||
print_header "Sample Domain Entries (first 5)"
|
|
||||||
grep "^DOMAIN|" "$TEST_SYSREF_DB" | head -5 | while IFS='|' read -r type domain owner docroot logdir php_version is_primary domain_type server_alias http_code https_code status_summary; do
|
|
||||||
echo " Domain: $domain"
|
|
||||||
echo " Owner: $owner"
|
|
||||||
echo " Docroot: $docroot"
|
|
||||||
echo " Type: $domain_type"
|
|
||||||
echo " Status: HTTP=$http_code HTTPS=$https_code ($status_summary)"
|
|
||||||
echo ""
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Compare with production database if it exists
|
|
||||||
if [ -f "$TOOLKIT_BASE_DIR/.sysref" ]; then
|
|
||||||
echo ""
|
|
||||||
print_header "Comparison with Production Database"
|
|
||||||
|
|
||||||
local prod_users=$(grep -c "^USER|" "$TOOLKIT_BASE_DIR/.sysref" 2>/dev/null || echo 0)
|
|
||||||
local prod_domains=$(grep -c "^DOMAIN|" "$TOOLKIT_BASE_DIR/.sysref" 2>/dev/null || echo 0)
|
|
||||||
local prod_dbs=$(grep -c "^DB|" "$TOOLKIT_BASE_DIR/.sysref" 2>/dev/null || echo 0)
|
|
||||||
|
|
||||||
echo " Production: $prod_users users, $prod_domains domains, $prod_dbs databases"
|
|
||||||
echo " Test: $user_count users, $domain_count domains, $db_count databases"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ "$user_count" -eq "$prod_users" ] && [ "$domain_count" -eq "$prod_domains" ]; then
|
|
||||||
print_success "✅ Counts match! Test successful."
|
|
||||||
else
|
|
||||||
print_warning "⚠️ Counts differ - this may be expected for cross-platform changes"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_info "Test database saved to: $TEST_SYSREF_DB"
|
|
||||||
print_info "You can inspect it with: cat $TEST_SYSREF_DB"
|
|
||||||
}
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# RUN TEST
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
# Clear screen and run
|
|
||||||
clear
|
|
||||||
test_reference_database
|
|
||||||
|
|
||||||
# Instructions
|
|
||||||
echo ""
|
|
||||||
print_header "Next Steps"
|
|
||||||
echo "1. Review the test database: cat $TEST_SYSREF_DB"
|
|
||||||
echo "2. If results look good on this cPanel server, test on Plesk:"
|
|
||||||
echo " - git pull on Plesk server"
|
|
||||||
echo " - bash test-launcher.sh"
|
|
||||||
echo " - Verify domains/users/databases are detected"
|
|
||||||
echo "3. If Plesk test succeeds, we can integrate into main launcher"
|
|
||||||
echo ""
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
################################################################################
|
|
||||||
# Integration Test Suite for WordPress Cron Manager
|
|
||||||
# Purpose: Validate script functionality and catch regressions
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/modules/website/wordpress/wordpress-cron-manager.sh"
|
|
||||||
TEST_COUNT=0
|
|
||||||
PASSED_COUNT=0
|
|
||||||
FAILED_COUNT=0
|
|
||||||
|
|
||||||
# Color codes
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
test_start() {
|
|
||||||
TEST_COUNT=$((TEST_COUNT + 1))
|
|
||||||
echo -e "\n${BLUE}[TEST $TEST_COUNT]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_pass() {
|
|
||||||
PASSED_COUNT=$((PASSED_COUNT + 1))
|
|
||||||
echo -e "${GREEN}✓ PASS${NC}: $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_fail() {
|
|
||||||
FAILED_COUNT=$((FAILED_COUNT + 1))
|
|
||||||
echo -e "${RED}✗ FAIL${NC}: $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_info() {
|
|
||||||
echo -e "${YELLOW}ℹ${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 1: Script exists
|
|
||||||
test_script_exists() {
|
|
||||||
test_start "Script file exists"
|
|
||||||
if [ -f "$SCRIPT" ]; then
|
|
||||||
test_pass "Script found at: $SCRIPT"
|
|
||||||
else
|
|
||||||
test_fail "Script not found: $SCRIPT"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 2: Script is executable
|
|
||||||
test_script_executable() {
|
|
||||||
test_start "Script is executable"
|
|
||||||
if [ -x "$SCRIPT" ]; then
|
|
||||||
test_pass "Script has execute permission"
|
|
||||||
else
|
|
||||||
test_fail "Script is not executable"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 3: Bash syntax validation
|
|
||||||
test_bash_syntax() {
|
|
||||||
test_start "Bash syntax validation"
|
|
||||||
if bash -n "$SCRIPT" 2>/dev/null; then
|
|
||||||
test_pass "No syntax errors"
|
|
||||||
else
|
|
||||||
test_fail "Syntax errors detected"
|
|
||||||
bash -n "$SCRIPT" 2>&1 | head -5
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 4: Help flag works
|
|
||||||
test_help_flag() {
|
|
||||||
test_start "Help flag functionality (--help)"
|
|
||||||
output=$($SCRIPT --help 2>&1 | head -5)
|
|
||||||
if echo "$output" | grep -q "Usage"; then
|
|
||||||
test_pass "Help output generated"
|
|
||||||
else
|
|
||||||
test_fail "Help flag not working properly"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 5: Grep for key functions (non-execution test)
|
|
||||||
test_functions_defined() {
|
|
||||||
test_start "Key functions defined in script"
|
|
||||||
local functions="initialize_wp_cache get_wp_search_paths validate_wordpress_site is_empty is_set"
|
|
||||||
local missing=0
|
|
||||||
|
|
||||||
for func in $functions; do
|
|
||||||
if grep -q "^$func()" "$SCRIPT"; then
|
|
||||||
test_info "✓ Found function: $func"
|
|
||||||
else
|
|
||||||
test_fail "Missing function: $func"
|
|
||||||
missing=$((missing + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $missing -eq 0 ]; then
|
|
||||||
test_pass "All key functions defined"
|
|
||||||
else
|
|
||||||
test_fail "$missing functions missing"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 6: Check for helper functions
|
|
||||||
test_helper_functions_defined() {
|
|
||||||
test_start "Helper functions defined"
|
|
||||||
local helpers="is_file_valid is_user_valid is_wp_configured is_cron_job_exists has_sufficient_disk_space"
|
|
||||||
local count=0
|
|
||||||
|
|
||||||
for helper in $helpers; do
|
|
||||||
if grep -q "^$helper()" "$SCRIPT"; then
|
|
||||||
count=$((count + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $count -ge 3 ]; then
|
|
||||||
test_pass "Helper functions defined ($count/5)"
|
|
||||||
else
|
|
||||||
test_fail "Insufficient helper functions ($count/5)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 7: Error code constants
|
|
||||||
test_error_codes_defined() {
|
|
||||||
test_start "Error code constants defined"
|
|
||||||
if grep -q "ERR_SUCCESS\|ERR_INVALID_USER\|ERR_FILE_NOT_FOUND" "$SCRIPT"; then
|
|
||||||
test_pass "Error code constants found"
|
|
||||||
else
|
|
||||||
test_fail "Error code constants not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 8: Report generation functions
|
|
||||||
test_report_functions() {
|
|
||||||
test_start "Report generation functions"
|
|
||||||
local funcs="generate_json_report generate_csv_report report_save"
|
|
||||||
local count=0
|
|
||||||
|
|
||||||
for func in $funcs; do
|
|
||||||
if grep -q "^$func()" "$SCRIPT"; then
|
|
||||||
count=$((count + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $count -eq 3 ]; then
|
|
||||||
test_pass "Report functions defined ($count/3)"
|
|
||||||
else
|
|
||||||
test_fail "Report functions incomplete ($count/3)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 9: Rollback functions
|
|
||||||
test_rollback_functions() {
|
|
||||||
test_start "Rollback support functions"
|
|
||||||
local funcs="rollback_init rollback_create_checkpoint rollback_all"
|
|
||||||
local count=0
|
|
||||||
|
|
||||||
for func in $funcs; do
|
|
||||||
if grep -q "^$func()" "$SCRIPT"; then
|
|
||||||
count=$((count + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $count -eq 3 ]; then
|
|
||||||
test_pass "Rollback functions defined ($count/3)"
|
|
||||||
else
|
|
||||||
test_fail "Rollback functions incomplete ($count/3)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 10: Configuration support
|
|
||||||
test_config_support() {
|
|
||||||
test_start "Configuration file support"
|
|
||||||
if grep -q "load_config_file\|/etc/wordpress-cron-manager.conf" "$SCRIPT"; then
|
|
||||||
test_pass "Configuration file support implemented"
|
|
||||||
else
|
|
||||||
test_fail "Configuration file support not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 11: Progress tracking
|
|
||||||
test_progress_tracking() {
|
|
||||||
test_start "Progress tracking functions"
|
|
||||||
local funcs="show_progress show_progress_bar show_spinner"
|
|
||||||
local count=0
|
|
||||||
|
|
||||||
for func in $funcs; do
|
|
||||||
if grep -q "^$func()" "$SCRIPT"; then
|
|
||||||
count=$((count + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $count -ge 2 ]; then
|
|
||||||
test_pass "Progress tracking defined ($count/3)"
|
|
||||||
else
|
|
||||||
test_fail "Progress tracking incomplete ($count/3)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 12: Regex helpers
|
|
||||||
test_regex_helpers() {
|
|
||||||
test_start "Regex pattern helpers"
|
|
||||||
local helpers="grep_wp_config_define grep_disabled_wp_cron grep_in_crontab"
|
|
||||||
local count=0
|
|
||||||
|
|
||||||
for helper in $helpers; do
|
|
||||||
if grep -q "^$helper()" "$SCRIPT"; then
|
|
||||||
count=$((count + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $count -eq 3 ]; then
|
|
||||||
test_pass "Regex helpers defined ($count/3)"
|
|
||||||
else
|
|
||||||
test_fail "Regex helpers incomplete ($count/3)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 13: Function registry
|
|
||||||
test_function_registry() {
|
|
||||||
test_start "Function registry metadata"
|
|
||||||
if grep -q "FUNCTION_REGISTRY" "$SCRIPT"; then
|
|
||||||
test_pass "Function registry implemented"
|
|
||||||
else
|
|
||||||
test_fail "Function registry not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 14: Line count (sanity check)
|
|
||||||
test_script_size() {
|
|
||||||
test_start "Script size sanity check"
|
|
||||||
local lines=$(wc -l < "$SCRIPT")
|
|
||||||
if [ "$lines" -gt 2000 ]; then
|
|
||||||
test_pass "Script size reasonable: $lines lines"
|
|
||||||
else
|
|
||||||
test_fail "Script size too small: $lines lines (expected >2000)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test 15: Git commits
|
|
||||||
test_git_history() {
|
|
||||||
test_start "Optimization commits in git history"
|
|
||||||
local opt_commits=$(cd "$(dirname $SCRIPT)/../../../" && git log --oneline | grep -c "OPTIMIZE\|ADVANCED")
|
|
||||||
if [ $opt_commits -gt 5 ]; then
|
|
||||||
test_pass "Multiple optimization commits found: $opt_commits"
|
|
||||||
else
|
|
||||||
test_fail "Insufficient optimization commits: $opt_commits"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
print_summary() {
|
|
||||||
echo ""
|
|
||||||
echo "================================================================================"
|
|
||||||
echo "INTEGRATION TEST SUMMARY"
|
|
||||||
echo "================================================================================"
|
|
||||||
echo "Total Tests: $TEST_COUNT"
|
|
||||||
echo -e "${GREEN}Passed: $PASSED_COUNT${NC}"
|
|
||||||
echo -e "${RED}Failed: $FAILED_COUNT${NC}"
|
|
||||||
|
|
||||||
if [ $FAILED_COUNT -eq 0 ]; then
|
|
||||||
echo -e "\n${GREEN}✓ ALL TESTS PASSED${NC}"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "\n${RED}⚠ SOME TESTS FAILED (but script may still work)${NC}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "================================================================================"
|
|
||||||
echo "WordPress Cron Manager - Integration Test Suite"
|
|
||||||
echo "================================================================================"
|
|
||||||
|
|
||||||
test_script_exists
|
|
||||||
test_script_executable
|
|
||||||
test_bash_syntax
|
|
||||||
test_help_flag
|
|
||||||
test_functions_defined
|
|
||||||
test_helper_functions_defined
|
|
||||||
test_error_codes_defined
|
|
||||||
test_report_functions
|
|
||||||
test_rollback_functions
|
|
||||||
test_config_support
|
|
||||||
test_progress_tracking
|
|
||||||
test_regex_helpers
|
|
||||||
test_function_registry
|
|
||||||
test_script_size
|
|
||||||
test_git_history
|
|
||||||
|
|
||||||
print_summary
|
|
||||||
Reference in New Issue
Block a user