From b9c29f56033c5b127fa3235447791f8d43d20a3e Mon Sep 17 00:00:00 2001 From: wxk Date: Fri, 30 May 2025 01:59:02 +0100 Subject: [PATCH] Update v-system-report --- v-system-report | 3290 +++++++++++++++++++++++++++-------------------- 1 file changed, 1915 insertions(+), 1375 deletions(-) diff --git a/v-system-report b/v-system-report index 720260bdf..7581125b8 100644 --- a/v-system-report +++ b/v-system-report @@ -1,13 +1,12 @@ #!/bin/bash -# Color definitions +# Color definitions for console output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' - # Section Configuration # Set to true to enable, false to disable each section CHECK_SYSTEM_RESOURCES=true @@ -16,34 +15,33 @@ CHECK_PHP=true CHECK_MYSQL=true CHECK_CLAMAV=true CHECK_FAIL2BAN=true -CHECK_EMAIL=true +CHECK_FAIL2BAN_CONFIG=false +CHECK_EXIM4=true CHECK_SSL=true CHECK_BACKUP=true # Email Configuration -# By default, the script will use MyVestaCP's built-in email system (v-send-mail) -# to send reports to the admin email configured in MyVestaCP. -SEND_EMAIL_REPORT=true # Set to true to enable email notifications -EMAIL_SUBJECT="MyVestaCP System Report - $(date '+%Y-%m-%d')" # Default email subject with date +# Set to true to enable email notifications using MyVestaCP's built-in email system +SEND_EMAIL_REPORT=true +EMAIL_SUBJECT="MyVestaCP System Report - $(date '+%Y-%m-%d')" # AI Integration Configuration -AI_ENABLED=true # Set to true to enable AI analysis -AI_MODE="auto" # Set to "auto" (default), "always", or "never" -AI_API_KEY="" # Your HuggingFace API key -AI_MODEL="mistralai/Mixtral-8x7B-Instruct-v0.1" # Updated to Mixtral model -AI_MAX_LENGTH=1000 # Maximum length of the response +# Set to true to enable AI analysis of the report +AI_ENABLED=true +AI_MODE="auto" # Options: "auto", "always", or "never" +AI_API_KEY="" +AI_MODEL="mistralai/Mixtral-8x7B-Instruct-v0.1" +AI_MAX_LENGTH=1000 # Internal variables (do not modify) -ai_analysis="" # Used internally to store AI analysis results +ai_analysis="" # Log Configuration LOG_DIR="/var/log/v-system-report" LOG_FILE="" -# Variável global para HTML dos detalhes +# Global variables for HTML details and AI errors DETAILED_ISSUES_HTML="" - -# Variável global para erro da IA AI_LAST_ERROR="" # Function to setup logging @@ -178,8 +176,8 @@ log_email_status() { local method="$3" local error="$4" - log_console "Email Status:" - log_file "Email Status:" + log_console "Exim4 Status:" + log_file "Exim4 Status:" log_console " Status: $status" log_file " Status: $status" @@ -199,23 +197,56 @@ log_email_status() { # Function to check and install jq if needed check_and_install_jq() { if ! command -v jq &> /dev/null; then - log_console "${YELLOW}⚠️ jq is not installed. Installing...${NC}" + log_message "${YELLOW}⚠️ jq not found. Installing jq...${NC}" if command -v apt-get &> /dev/null; then - apt-get update > /dev/null 2>&1 - apt-get install -y jq > /dev/null 2>&1 - if [ $? -eq 0 ]; then - log_console "${GREEN}✓ jq installed successfully${NC}" - else - log_console "${RED}⚠️ Failed to install jq. Please install it manually:${NC}" - log_console " apt-get update && apt-get install -y jq" - exit 1 - fi + apt-get update && apt-get install -y jq + elif command -v yum &> /dev/null; then + yum install -y jq + elif command -v dnf &> /dev/null; then + dnf install -y jq else - log_console "${RED}⚠️ Could not install jq automatically. Please install it manually:${NC}" - log_console " apt-get update && apt-get install -y jq" - exit 1 + log_message "${RED}⚠️ Could not install jq. Package manager not found.${NC}" + return 1 + fi + if command -v jq &> /dev/null; then + log_message "${GREEN}✓ jq installed successfully${NC}" + else + log_message "${RED}⚠️ Failed to install jq${NC}" + return 1 + fi + else + # Only show 'already installed' message at the start of the script + if [[ "$FUNCNAME[1]" != "analyze_with_ai" && "$FUNCNAME[1]" != "show_detailed_summary" && "$FUNCNAME[1]" != "main" ]]; then + log_message "${GREEN}✓ jq is already installed${NC}" fi fi + return 0 +} + +# Function to check and install geoiplookup if needed +check_and_install_geoiplookup() { + if ! command -v geoiplookup &> /dev/null; then + log_message "${YELLOW}⚠️ geoiplookup not found. Installing geoip-bin...${NC}" + if command -v apt-get &> /dev/null; then + apt-get update && apt-get install -y geoip-bin + elif command -v yum &> /dev/null; then + yum install -y geoip + elif command -v dnf &> /dev/null; then + dnf install -y geoip + else + log_message "${RED}⚠️ Could not install geoiplookup. Package manager not found.${NC}" + return 1 + fi + if command -v geoiplookup &> /dev/null; then + log_message "${GREEN}✓ geoiplookup installed successfully${NC}" + else + log_message "${RED}⚠️ Failed to install geoiplookup${NC}" + return 1 + fi + else + log_message "${GREEN}✓ geoiplookup is already installed${NC}" + fi + return 0 } # Function to determine if AI analysis should run @@ -234,8 +265,8 @@ should_run_ai_analysis() { return 1 # Never run AI ;; "auto"|*) # Default to auto mode - # Only run if there are issues - if [ $high_issues -gt 0 ] || [ $medium_issues -gt 0 ] || [ $low_issues -gt 0 ]; then + # Only run if there are medium, high, or critical issues (not for low issues only) + if [ $high_issues -gt 0 ] || [ $medium_issues -gt 0 ]; then return 0 else return 1 @@ -308,40 +339,37 @@ analyze_with_ai() { show_progress 1 4 # Prepare the prompt with detailed context - local prompt="You are an expert MyVestaCP system administrator. Your task is to analyze the following comprehensive system status report and provide specific, actionable solutions for MyVestaCP on Debian 12. + local prompt="You are an expert MyVestaCP system administrator. Your task is to analyze the following system status report and provide specific, actionable solutions for MyVestaCP on Debian 12. -Please provide your analysis in this exact format: +Please provide your analysis in this exact format and order: -1. Critical Issues (if any): - - List each critical issue with specific details from the report - - Provide the exact command to fix it - - Include a brief explanation of why this is critical - -2. Medium Issues (if any): - - List each medium issue with specific details from the report - - Provide the exact command to fix it - - Include a brief explanation of the impact - -3. Low Issues (if any): - - List each low issue with specific details from the report - - Provide the exact command to fix it +1. Low Priority Issues: + - Only list actual issues found (do not mention 'None' or 'No issues') + - Provide the exact command to fix each issue - Include a brief explanation of why it should be addressed -Important guidelines: -- Analyze the detailed system information provided below -- Focus ONLY on MyVestaCP-specific issues and solutions -- Provide ONLY commands that are relevant to the actual issues found in the detailed report -- Pay attention to module configuration status - do not suggest fixes for disabled modules -- Do not suggest updates or installations if the system is already up to date -- Do not include IP addresses or specific values in commands -- Keep explanations brief but informative -- Maximum 2-3 commands per issue -- Only include issues that have a clear, actionable solution -- Use v-* commands when available instead of direct system commands -- Consider the context of each issue (e.g., if a service is already running, don't suggest starting it) -- Reference specific metrics from the detailed system information when relevant +2. Medium Priority Issues: + - Only list actual issues found (do not mention 'None' or 'No issues') + - Provide the exact command to fix each issue + - Include a brief explanation of the impact -The following is the comprehensive system status report to analyze:\n\n" +3. High Priority Issues: + - Only list actual issues found (do not mention 'None' or 'No issues') + - Provide the exact command to fix each issue + - Include a brief explanation of why this needs attention + +4. Critical Issues: + - Only list actual issues found (do not mention 'None' or 'No issues') + - Provide the exact command to fix each issue + - Include a brief explanation of why this is critical + +Important guidelines: +- Skip entire sections if no issues are found in that category +- Focus ONLY on MyVestaCP-specific issues and solutions +- Provide ONLY commands that are relevant to the actual issues found +- Do not create fake issues or mention theoretical problems +- Be concise and specific +- Always start with Low Priority and work up to Critical" # Add system status information prompt+="System Status: $status\n" @@ -364,7 +392,7 @@ The following is the comprehensive system status report to analyze:\n\n" [ "$CHECK_MYSQL" = true ] && prompt+="MySQL: ENABLED\n" || prompt+="MySQL: DISABLED\n" [ "$CHECK_CLAMAV" = true ] && prompt+="ClamAV: ENABLED\n" || prompt+="ClamAV: DISABLED\n" [ "$CHECK_FAIL2BAN" = true ] && prompt+="Fail2Ban: ENABLED\n" || prompt+="Fail2Ban: DISABLED\n" - [ "$CHECK_EMAIL" = true ] && prompt+="Email: ENABLED\n" || prompt+="Email: DISABLED\n" + [ "$CHECK_EXIM4" = true ] && prompt+="Email: ENABLED\n" || prompt+="Email: DISABLED\n" [ "$CHECK_SSL" = true ] && prompt+="SSL: ENABLED\n" || prompt+="SSL: DISABLED\n" [ "$CHECK_BACKUP" = true ] && prompt+="Backup: ENABLED\n" || prompt+="Backup: DISABLED\n" @@ -392,7 +420,8 @@ The following is the comprehensive system status report to analyze:\n\n" fi show_progress 2 4 - echo -e "\nSending data to AI model..." + echo -e " +Sending data to AI model..." # Create a temporary file for the JSON payload local temp_json=$(mktemp) @@ -547,26 +576,31 @@ The following is the comprehensive system status report to analyze:\n\n" ai_analysis="$clean_generated_text" show_progress 4 4 - echo -e "\n${GREEN}✓ AI Analysis completed successfully${NC}" + echo -e " +${GREEN}✓ AI Analysis completed successfully${NC}" # Display the analysis in the console - echo -e "\n${BLUE}=== AI Analysis Results ===${NC}" - echo -e "${YELLOW}The following recommendations are based on the system status analysis:${NC}\n" + echo -e " +${BLUE}=== AI Analysis Results ===${NC}" + echo -e "${YELLOW}The following recommendations are based on the system status analysis:${NC} +" - # Format and display the analysis with better readability - local formatted_analysis=$(echo "$clean_generated_text" | sed 's/^1\. Critical Issues (if any):/\n1\. Critical Issues:/' | \ - sed 's/^2\. Medium Issues (if any):/\n2\. Medium Issues:/' | \ - sed 's/^3\. Low Issues (if any):/\n3\. Low Issues:/' | \ - sed 's/^- /\n • /g') + # Format and display the analysis with better readability for console + local formatted_analysis=$(echo "$clean_generated_text" | sed 's/^1\. Critical Issues (if any):/\n1. Critical Issues:/' | \ + sed 's/^2\. Medium Issues (if any):/\n2. Medium Issues:/' | \ + sed 's/^3\. Low Issues (if any):/\n3. Low Issues:/' | \ + sed 's/^- /\n - /g') - # Add color coding for different severity levels + # Add color coding for different severity levels without raw ANSI codes formatted_analysis=$(echo "$formatted_analysis" | \ - sed "s/1\. Critical Issues:/${RED}1\. Critical Issues:${NC}/" | \ - sed "s/2\. Medium Issues:/${YELLOW}2\. Medium Issues:${NC}/" | \ - sed "s/3\. Low Issues:/${GREEN}3\. Low Issues:${NC}/") + sed "s/1\. Critical Issues:/${RED}1. Critical Issues:${NC}/" | \ + sed "s/2\. Medium Issues:/${YELLOW}2. Medium Issues:${NC}/" | \ + sed "s/3\. Low Issues:/${GREEN}3. Low Issues:${NC}/") echo -e "$formatted_analysis" - echo -e "\n${BLUE}=== End of AI Analysis ===${NC}\n" + echo -e " +${BLUE}=== End of AI Analysis ===${NC} +" return 0 } @@ -621,7 +655,7 @@ run_with_timeout() { echo -e "${RED}⚠️ Command failed with exit code $exit_code${NC}" return 1 fi - + echo "$output" return 0 } @@ -926,333 +960,467 @@ check_single_blacklist() { return 0 } -# Function to check email status with timeout +# Function to check email status with intelligent assessment check_email_status() { local output=() - output+=("${BLUE}=== Email Status (Today) ===${NC}") + output+=("${BLUE}=== EXIM4 System Status ===${NC}") local current_high_issues=0 local current_medium_issues=0 local current_low_issues=0 - # Define log paths - local mainlog="/var/log/exim4/mainlog" - local rejectlog="/var/log/exim4/rejectlog" + echo -e "Checking EXIM4 system status..." - # Get start of current day timestamp - local today_start=$(date -d "$(date +%Y-%m-%d) 00:00:00" +%s) + # Service Status + local exim4_running=false + local dovecot_running=false - # Get today's date in the format used in logs (YYYY-MM-DD) - local today=$(date "+%Y-%m-%d") + if run_with_timeout 5 "systemctl is-active --quiet exim4"; then + exim4_running=true + output+=("${GREEN}✓ Exim4 running${NC}") + else + output+=("${RED}⚠️ Exim4 not running${NC}") + ((current_high_issues++)) + fi - if [ -f "$mainlog" ]; then - # Queue Status (still checks current queue, not log) - output+=("${YELLOW}Queue Status:${NC}") - local queue_output=$(run_with_timeout 10 "exim -bp 2>/dev/null") - local queue_exit_code=$? + if run_with_timeout 5 "systemctl is-active --quiet dovecot"; then + dovecot_running=true + output+=("${GREEN}✓ Dovecot running${NC}") + else + output+=("${RED}⚠️ Dovecot not running${NC}") + ((current_high_issues++)) + fi + + # Exim4 Version and Configuration + local exim_version="" + if $exim4_running; then + exim_version=$(run_with_timeout 5 "exim -bV 2>/dev/null | head -1 | awk '{print $3}'") + if [ -n "$exim_version" ]; then + output+=("${GREEN}✓ Exim4 Version: $exim_version${NC}") + else + output+=("${YELLOW}⚠️ Unable to retrieve Exim4 version${NC}") + ((current_medium_issues++)) + fi + fi + + # Mail Queue Status + local queue_count=0 + local frozen_count=0 + local queue_size_mb=0 + + if $exim4_running; then + # Get queue count using exim -bpc (much faster than parsing mailq) + queue_count=$(run_with_timeout 10 "exim -bpc 2>/dev/null") + if [ -z "$queue_count" ] || ! [[ "$queue_count" =~ ^[0-9]+$ ]]; then + queue_count=0 + fi - if [ $queue_exit_code -eq 0 ]; then - # Count frozen and non-frozen messages separately - local frozen_count=$(echo "$queue_output" | grep -c "*** frozen ***") - local other_count=$(echo "$queue_output" | grep -c "^[0-9]" | grep -v "*** frozen ***") - local total_count=$((frozen_count + other_count)) + if [ "$queue_count" -gt 0 ]; then + output+=("${YELLOW}ℹ️ Messages in queue: $queue_count${NC}") - if [ "$total_count" -gt 0 ] 2>/dev/null; then - echo -e "${YELLOW}⚠️ Emails in queue: $total_count${NC}" - # Messages in queue indicate potential problems - if [ "$frozen_count" -gt 0 ]; then - echo -e "${RED} - Frozen messages: $frozen_count${NC}" - # Intelligent frozen message classification - if [ "$frozen_count" -gt 10 ]; then - ((current_high_issues++)) # >10 frozen = CRITICAL (systemic problem) - elif [ "$frozen_count" -gt 3 ]; then - ((current_medium_issues++)) # 4-10 frozen = MEDIUM (needs attention) - else - ((current_low_issues++)) # 1-3 frozen = LOW (minor issues) - fi - fi - if [ "${deferred:-0}" -gt 0 ]; then - echo -e "${YELLOW} - Deferred: $deferred${NC}" - # Intelligent deferred message classification - if [ "$deferred" -gt 100 ]; then - ((current_high_issues++)) # >100 deferred = CRITICAL (systemic problem) - elif [ "$deferred" -gt 20 ]; then - ((current_medium_issues++)) # 21-100 deferred = MEDIUM (needs attention) - else - ((current_low_issues++)) # 1-20 deferred = LOW (normal delays) - fi - fi - if [ "$other_count" -gt 0 ]; then - echo -e "${YELLOW} - Other messages: $other_count${NC}" - ((current_medium_issues++)) # Others in queue are medium - fi - # We won't increment global issues just for emails in queue, unless they are frozen - echo -e "${YELLOW}Useful commands:${NC}" - echo -e " - View queue details: exim -bp" - echo -e " - View specific message: exim -Mvb " - echo -e " - Remove specific message: exim -Mrm " - echo -e " - Remove all messages: exim -bp | awk '/^[[:space:]]*[0-9]+[mhd]/{print \$3}' | xargs exim -Mrm" - echo -e " - Thaw frozen messages: exim -Mt " + # Get frozen message count (quick check) + frozen_count=$(run_with_timeout 10 "exim -bp 2>/dev/null | grep -c '*** frozen ***' || echo 0") + if [ -z "$frozen_count" ] || ! [[ "$frozen_count" =~ ^[0-9]+$ ]]; then + frozen_count=0 + fi + + if [ "$frozen_count" -gt 0 ]; then + output+=("${RED}⚠️ Frozen messages: $frozen_count${NC}") + fi + + # Estimate queue size (optional, quick calculation) + queue_size_mb=$(run_with_timeout 10 "du -sm /var/spool/exim4/input 2>/dev/null | awk '{print $1}' || echo 0") + if [ -z "$queue_size_mb" ] || ! [[ "$queue_size_mb" =~ ^[0-9]+$ ]]; then + queue_size_mb=0 + fi + + if [ "$queue_size_mb" -gt 100 ]; then + output+=("${YELLOW}ℹ️ Queue size: ${queue_size_mb}MB${NC}") + fi + else + output+=("${GREEN}✓ No emails in queue${NC}") + fi + fi + + # Quick status checks using direct commands (no log parsing) + local recent_failures=0 + local recent_deliveries=0 + local auth_failures=0 + local delivery_errors=0 + + # Check for delivery issues using exim queue inspection (fast) + if $exim4_running && [ "$queue_count" -gt 0 ]; then + # Get a quick sample of queue messages to check for common issues + local queue_sample=$(run_with_timeout 10 "exim -bp 2>/dev/null | head -20") + if [ -n "$queue_sample" ]; then + # Count different types of issues in queue + delivery_errors=$(echo "$queue_sample" | grep -c "retry time not reached\|Connection refused\|Host not found" || echo 0) + auth_failures=$(echo "$queue_sample" | grep -c "authentication failed\|login failed" || echo 0) + + if [ "$delivery_errors" -gt 5 ]; then + output+=("${YELLOW}⚠️ Delivery issues detected in queue: $delivery_errors${NC}") + recent_failures=$delivery_errors + fi + + if [ "$auth_failures" -gt 2 ]; then + output+=("${YELLOW}⚠️ Authentication issues in queue: $auth_failures${NC}") + fi + fi + fi + + # Check Exim process status for performance indicators + if $exim4_running; then + local exim_processes=$(run_with_timeout 5 "pgrep -c exim4 2>/dev/null || echo 0") + if [ -n "$exim_processes" ] && [[ "$exim_processes" =~ ^[0-9]+$ ]] && [ "$exim_processes" -gt 10 ]; then + output+=("${YELLOW}⚠️ High number of Exim processes: $exim_processes${NC}") + recent_failures=$((recent_failures + 5)) # Add to failure count as performance indicator + elif [ "$exim_processes" -gt 0 ]; then + output+=("${GREEN}✓ Exim processes: $exim_processes${NC}") + recent_deliveries=1 # Indicate system is active + fi + fi + + # Check disk space for mail spool (critical for email operation) + local spool_usage="" + if [ -d "/var/spool/exim4" ]; then + spool_usage=$(run_with_timeout 5 "df /var/spool/exim4 2>/dev/null | tail -n1 | awk '{print \$5}' | sed 's/%//' || echo 0") + else + # Fallback to root filesystem if exim4 spool doesn't exist + spool_usage=$(run_with_timeout 5 "df / 2>/dev/null | tail -n1 | awk '{print \$5}' | sed 's/%//' || echo 0") + fi + + # Ensure spool_usage is a valid number + if ! [[ "$spool_usage" =~ ^[0-9]+$ ]]; then + spool_usage=0 + fi + + if [ "$spool_usage" -gt 90 ]; then + output+=("${RED}⚠️ Mail spool disk usage critical: ${spool_usage}%${NC}") + recent_failures=$((recent_failures + 10)) + elif [ "$spool_usage" -gt 80 ]; then + output+=("${YELLOW}⚠️ Mail spool disk usage high: ${spool_usage}%${NC}") + recent_failures=$((recent_failures + 5)) + fi + + # === SELECTIVE LOG ANALYSIS FOR ADDITIONAL METRICS === + # Analyze full day logs efficiently (filtered by today's date for accurate daily metrics) + log_console "Analyzing today's email activity (full day scan)..." + + local today_date=$(date '+%Y-%m-%d') + local recent_log_entries="" + + # Try to get today's Exim logs (filter by date for performance and accuracy) + if [ -f "/var/log/exim4/mainlog" ]; then + recent_log_entries=$(run_with_timeout 30 "grep '^$today_date' /var/log/exim4/mainlog 2>/dev/null" || + run_with_timeout 30 "grep '$today_date' /var/log/exim4/mainlog 2>/dev/null") + elif [ -f "/var/log/mail.log" ]; then + recent_log_entries=$(run_with_timeout 30 "grep '$today_date' /var/log/mail.log 2>/dev/null | grep exim") + fi + + if [ -n "$recent_log_entries" ]; then + local log_line_count=$(echo "$recent_log_entries" | wc -l) + output+=("${BLUE}ℹ️ Analyzing $log_line_count log entries from today ($today_date)...${NC}") + + # Extract key metrics from today's logs + local delivered_count=$(echo "$recent_log_entries" | grep -c "=>" 2>/dev/null || echo 0) + local bounced_count=$(echo "$recent_log_entries" | grep -c " \*\* " 2>/dev/null || echo 0) + local deferred_count=$(echo "$recent_log_entries" | grep -c "==" 2>/dev/null || echo 0) + local rejected_count=$(echo "$recent_log_entries" | grep -c "rejected" 2>/dev/null || echo 0) + local auth_fail_count=$(echo "$recent_log_entries" | grep -c "authentication failed\|authenticator failed" 2>/dev/null || echo 0) + local spam_count=$(echo "$recent_log_entries" | grep -c "spam\|blocked" 2>/dev/null || echo 0) + + # Additional metrics (will be shown only if > 0) + local tls_errors=$(echo "$recent_log_entries" | grep -c "TLS error\|SSL.*error\|certificate.*error" 2>/dev/null || echo 0) + local smtp_timeouts=$(echo "$recent_log_entries" | grep -c "timeout\|connection timed out" 2>/dev/null || echo 0) + local spoofing_attempts=$(echo "$recent_log_entries" | grep -c "sender verify fail\|sender address rejected\|spoofing" 2>/dev/null || echo 0) + local frozen_msgs=$(echo "$recent_log_entries" | grep -c "frozen" 2>/dev/null || echo 0) + + # Connection statistics + local smtp_connections=$(echo "$recent_log_entries" | grep -c "connection from" 2>/dev/null || echo 0) + local relay_attempts=$(echo "$recent_log_entries" | grep -c "relay not permitted" 2>/dev/null || echo 0) + + # Ensure all variables are valid numbers (trim whitespace and validate) + delivered_count=$(echo "$delivered_count" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + bounced_count=$(echo "$bounced_count" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + deferred_count=$(echo "$deferred_count" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + rejected_count=$(echo "$rejected_count" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + auth_fail_count=$(echo "$auth_fail_count" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + spam_count=$(echo "$spam_count" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + tls_errors=$(echo "$tls_errors" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + smtp_timeouts=$(echo "$smtp_timeouts" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + spoofing_attempts=$(echo "$spoofing_attempts" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + frozen_msgs=$(echo "$frozen_msgs" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + smtp_connections=$(echo "$smtp_connections" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + relay_attempts=$(echo "$relay_attempts" | tr -d '\n\r' | grep -o '^[0-9]*$' || echo 0) + + # Performance indicators + local total_attempts=$((delivered_count + bounced_count + deferred_count + rejected_count)) + + # === ESSENTIAL METRICS (always displayed) === + output+=("${GREEN}✓ Delivered today: $delivered_count${NC}") + recent_deliveries=$delivered_count + + # Failed deliveries (combination of bounced + rejected) + local failed_deliveries=$((bounced_count + rejected_count)) + if [ "$failed_deliveries" -gt 0 ]; then + output+=("${YELLOW}⚠️ Failed deliveries: $failed_deliveries${NC}") + recent_failures=$((recent_failures + failed_deliveries)) + else + output+=("${GREEN}✓ Failed deliveries: 0${NC}") + fi + + # Deferred messages + if [ "$deferred_count" -gt 0 ]; then + output+=("${YELLOW}⚠️ Deferred messages: $deferred_count${NC}") + recent_failures=$((recent_failures + deferred_count)) + else + output+=("${GREEN}✓ Deferred messages: 0${NC}") + fi + + # Frozen messages + if [ "$frozen_msgs" -gt 0 ]; then + output+=("${YELLOW}⚠️ Frozen messages: $frozen_msgs${NC}") + recent_failures=$((recent_failures + frozen_msgs)) + else + output+=("${GREEN}✓ Frozen messages: 0${NC}") + fi + + # Authentication failures (always show - critical for security) + if [ "$auth_fail_count" -gt 0 ]; then + output+=("${RED}🔒 Auth failures: $auth_fail_count${NC}") + auth_failures=$auth_fail_count + else + output+=("${GREEN}✓ Auth failures: 0${NC}") + fi + + # === OPTIONAL METRICS (only shown if > 0) === + if [ "$spoofing_attempts" -gt 0 ]; then + output+=("${YELLOW}🛡️ Spoofing attempts: $spoofing_attempts${NC}") + fi + + if [ "$tls_errors" -gt 0 ]; then + output+=("${YELLOW}🔐 TLS errors: $tls_errors${NC}") + fi + + if [ "$smtp_timeouts" -gt 0 ]; then + output+=("${YELLOW}⏱️ SMTP timeouts: $smtp_timeouts${NC}") + fi + + if [ "$relay_attempts" -gt 0 ]; then + output+=("${YELLOW}⚠️ Unauthorized relay attempts: $relay_attempts${NC}") + fi + + if [ "$spam_count" -gt 0 ]; then + output+=("${GREEN}✓ Spam/blocked messages: $spam_count${NC}") + fi + + # Connection statistics (optional) + if [ "$smtp_connections" -gt 20 ]; then # Only show if significant activity + output+=("${BLUE}ℹ️ SMTP connections: $smtp_connections${NC}") + fi + + # Calculate and show delivery success rate + if [ "$total_attempts" -gt 0 ]; then + local success_rate=$(( (delivered_count * 100) / total_attempts )) + if [ "$success_rate" -lt 70 ]; then + output+=("${RED}Delivery success rate: ${success_rate}% ⚠️ Low${NC}") + recent_failures=$((recent_failures + 5)) + elif [ "$success_rate" -lt 90 ]; then + output+=("${YELLOW}Delivery success rate: ${success_rate}% ⚠️ Moderate${NC}") + recent_failures=$((recent_failures + 2)) else - echo -e "${GREEN}✓ No emails in queue${NC}" + output+=("${GREEN}Delivery success rate: ${success_rate}% ✓ Good${NC}") fi else - echo -e "${RED}⚠️ Failed to check email queue (exit code $queue_exit_code)${NC}" - ((current_medium_issues++)) # Failure to check queue is a medium problem - fi - - # Email Statistics (Today) - echo -e "\n${YELLOW}Email Statistics (Today):${NC}" - echo -e "Processing logs for today... This may take a few moments." - - # Initialize counters for log analysis (we won't use these for global issues directly) - local completed=0 - local deferred_log=0 # Rename to avoid conflict with queue variable - local failed_log=0 # Rename - local spoofed_log=0 # Rename - local rejected_log=0 # Rename - local frozen_log=0 # Rename - local auth_failures_log=0 # Rename - local tls_errors_log=0 # Rename - local smtp_timeouts_log=0 # Rename - - # Process mainlog for today's entries - echo -e "\nProcessing main log for $today..." - if [ -f "$mainlog" ]; then - # Use grep to filter lines starting with today's date - local mainlog_content=$(run_with_timeout 60 "grep -a '^$today' '$mainlog' | tr -d '\0'") - local mainlog_exit_code=$? - - if [ $mainlog_exit_code -eq 0 ]; then - if [ -n "$mainlog_content" ]; then - local total_lines=$(echo "$mainlog_content" | wc -l) - echo -e "Found $total_lines relevant lines in main log for today." - local current_line=0 - - while IFS= read -r line; do - ((current_line++)) - if [ $((current_line % 100)) -eq 0 ]; then - show_progress $current_line $total_lines - fi - - # Extract timestamp from line - local log_date=$(echo "$line" | grep -o '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}') - if [ -n "$log_date" ]; then - # Convert log date to timestamp - local log_ts=$(date -d "$log_date" +%s 2>/dev/null) - - # Only process if timestamp is valid and from today - if [ -n "$log_ts" ] && [ "$log_ts" -ge "$today_start" ]; then - # Process lines for today - case "$line" in - # Successful delivery patterns - *" Completed"*) ((completed++)) ;; # Just for statistics, not an issue - # Defer patterns - *"=="*"defer"*|*"== "*"defer"*|*" == "*"defer"*|*"**"*"retry time not reached"*|*"** "*"retry time not reached"*|*" ** "*"retry time not reached"*|*"defer"*"temporary failure"*|*"defer"*"temporarily"*|*"defer"*"retry"*|*"defer"*"later"*|*"defer"*"queue"*) ((deferred_log++)) ;; # Count for statistics only - # Failure patterns - *"**"*"unknown user"*|*"**"*"connection refused"*|*"** "*"unknown user"*|*"** "*"connection refused"*|*" ** "*"unknown user"*|*" ** "*"connection refused"*) ((failed_log++)) ;; # Count for statistics only - # Frozen state (already counted in queue, this is redundant for recent logs, but keep for older logs if needed) - *"frozen"*) ((frozen_log++)) ;; # Already handled by queue count - # Authentication failures - *"dovecot_login authenticator failed"*|*"dovecot_plain authenticator failed"*) ((auth_failures_log++)) ;; # Count for statistics only - # TLS and timeout errors - *"TLS error"*) ((tls_errors_log++)) ;; # Count for statistics only - *"SMTP command timeout"*) ((smtp_timeouts_log++)) ;; # Count for statistics only - # Spoofing attempts and other rejections (mainlog) - *"rejected"*"SPF"*|*"rejected"*"DMARC"*|*"rejected"*"sender verification"*|*"rejected by Spamhaus"*) ((spoofed_log++)) ;; # Count for statistics only - *"rejected"*) ((rejected_log++)) ;; # Count for statistics only - esac - fi - fi - done <<< "$mainlog_content" - # Show final progress - show_progress $total_lines $total_lines - echo -e "\n" - else - echo -e "${YELLOW}⚠️ Main log processed for $today but no entries found.${NC}" - fi - else - echo -e "${RED}⚠️ Failed to read main log content for $today (grep command exit code $mainlog_exit_code). Check file permissions or content.${NC}" - ((current_medium_issues++)) # Failure to read log is a medium problem - fi - else - echo -e "${RED}⚠️ Main log file not found: $mainlog${NC}" - ((current_medium_issues++)) # Not finding the log is a medium problem - fi - - # Process rejectlog for today's entries - # We don't need to count rejections here if we already count them in mainlog, or we decide where to count - # To simplify, we'll only count the patterns we're interested in from mainlog - # local rejected_from_rejectlog=0 # Count only from rejectlog to avoid double counting - - # Evaluate email health based on collected statistics - # Set reasonable thresholds for what constitutes real problems - local total_emails=$((completed + failed_log + deferred_log)) - - # Critical issues (high priority) - if [ "${frozen_count:-0}" -gt 0 ]; then - # Use the same intelligent classification as above - if [ "$frozen_count" -gt 10 ]; then - ((current_high_issues++)) # >10 frozen = CRITICAL (systemic problem) - elif [ "$frozen_count" -gt 3 ]; then - ((current_medium_issues++)) # 4-10 frozen = MEDIUM (needs attention) - else - ((current_low_issues++)) # 1-3 frozen = LOW (minor issues) - fi - fi - - # Medium issues (require attention) - if [ "$total_emails" -gt 0 ]; then - # Calculate failure rate - local failure_rate=0 - if [ "$total_emails" -gt 0 ]; then - failure_rate=$(( (failed_log * 100) / total_emails )) - fi - - # High failure rate (>20%) is a medium issue - if [ "$failure_rate" -gt 20 ]; then - ((current_medium_issues++)) - fi - - # High defer rate (>30%) is a medium issue - local defer_rate=0 - if [ "$total_emails" -gt 0 ]; then - defer_rate=$(( (deferred_log * 100) / total_emails )) - fi - if [ "$defer_rate" -gt 30 ]; then - ((current_medium_issues++)) - fi - fi - - # Excessive errors indicate problems - if [ "${failed_log:-0}" -gt 50 ]; then - ((current_medium_issues++)) # Too many failures - fi - if [ "${deferred_log:-0}" -gt 100 ]; then - ((current_medium_issues++)) # Too many deferrals - fi - if [ "${tls_errors_log:-0}" -gt 10 ]; then - ((current_medium_issues++)) # Too many TLS errors - fi - if [ "${smtp_timeouts_log:-0}" -gt 50 ]; then - ((current_medium_issues++)) # Too many timeouts - fi - - # Low issues (minor problems) - only flag if extremely excessive - if [ "${auth_failures_log:-0}" -gt 10000 ]; then - ((current_low_issues++)) # Extremely excessive auth failures (>10k/day) might indicate unusual activity - fi - - # Display statistics based on log analysis - echo -e "${GREEN}✓ Successfully delivered: ${completed:-0}${NC}" - if [ "${failed_log:-0}" -gt 0 ]; then - echo -e "${YELLOW}⚠️ Failed deliveries (today): $failed_log${NC}" # Change to yellow, already counted as medium issues - else - echo -e "${GREEN}✓ Failed deliveries (today): 0${NC}" - fi - if [ "${spoofed_log:-0}" -gt 0 ]; then - echo -e "${YELLOW}⚠️ Spoofing attempts (today): $spoofed_log${NC}" # Change to yellow, already counted as medium issues - else - echo -e "${GREEN}✓ Spoofing attempts (today): 0${NC}" - fi - if [ "${rejected_log:-0}" -gt 0 ]; then - echo -e "${YELLOW}⚠️ Rejected emails (today): $rejected_log${NC}" # Change to yellow, already counted as medium issues - else - echo -e "${GREEN}✓ Rejected emails (today): 0${NC}" - fi - if [ "${frozen_log:-0}" -gt 0 ]; then - echo -e "${RED}⚠️ Frozen messages in log (today): $frozen_log${NC}" # Keep red if appears in logs, indicates persistence - else - echo -e "${GREEN}✓ Frozen messages in log (today): 0${NC}" - fi - if [ "${deferred_log:-0}" -gt 0 ]; then - echo -e "${YELLOW}⚠️ Deferred deliveries (today): $deferred_log${NC}" # Change to yellow, already counted as medium issues - else - echo -e "${GREEN}✓ Deferred deliveries (today): 0${NC}" - fi - if [ "${auth_failures_log:-0}" -gt 0 ]; then - echo -e "${YELLOW}⚠️ Authentication failures (today): $auth_failures_log${NC}" # Change to yellow, already counted as low issues - else - echo -e "${GREEN}✓ Authentication failures (today): 0${NC}" - fi - if [ "${tls_errors_log:-0}" -gt 0 ]; then - echo -e "${YELLOW}⚠️ TLS errors (today): $tls_errors_log${NC}" # Change to yellow, already counted as medium issues - else - echo -e "${GREEN}✓ TLS errors (today): 0${NC}" - fi - if [ "${smtp_timeouts_log:-0}" -gt 0 ]; then - echo -e "${YELLOW}⚠️ SMTP timeouts (today): $smtp_timeouts_log${NC}" # Change to yellow, already counted as medium issues - else - echo -e "${GREEN}✓ SMTP timeouts (today): 0${NC}" + output+=("${BLUE}ℹ️ No delivery attempts today${NC}") fi else - echo -e "${RED}⚠️ Email log not found${NC}" - ((current_medium_issues++)) # Not finding the email log is a medium problem + output+=("${YELLOW}⚠️ Unable to access recent email logs for detailed analysis${NC}") fi + # Configuration check (basic) + local config_errors=0 + if $exim4_running; then + local config_test=$(run_with_timeout 10 "exim -bV 2>&1 | grep -i error | wc -l") + if [ -n "$config_test" ] && [[ "$config_test" =~ ^[0-9]+$ ]] && [ "$config_test" -gt 0 ]; then + config_errors=$config_test + output+=("${YELLOW}⚠️ Configuration warnings detected${NC}") + fi + fi + + # === INTELLIGENT EMAIL HEALTH ASSESSMENT SYSTEM === + # Calculate weighted threat score (0-100 points) then convert to health score + local threat_score=0 + local factor_explanations=() + + # Factor 1: Service status (30% weight) - Critical for email functionality + local service_factor=0 + if [ "$exim4_running" = false ]; then + service_factor=30 # Critical - main email service down + factor_explanations+=("Exim4 service down: +30 points (CRITICAL)") + elif [ "$dovecot_running" = false ]; then + service_factor=20 # High - IMAP/POP3 service down + factor_explanations+=("Dovecot service down: +20 points") + fi + threat_score=$((threat_score + service_factor)) + + # Factor 2: Mail queue issues (25% weight) - System load and delivery problems + local queue_factor=0 + if [ "$frozen_count" -gt 10 ]; then + queue_factor=25 # Critical - many frozen messages + factor_explanations+=("High frozen messages: +25 points (${frozen_count} frozen)") + elif [ "$frozen_count" -gt 0 ]; then + queue_factor=15 # Medium - some frozen messages + factor_explanations+=("Frozen messages: +15 points (${frozen_count} frozen)") + elif [ "$queue_count" -gt 1000 ]; then + queue_factor=20 # High - large queue backlog + factor_explanations+=("Large queue backlog: +20 points (${queue_count} messages)") + elif [ "$queue_count" -gt 100 ]; then + queue_factor=10 # Medium - moderate queue + factor_explanations+=("Moderate queue: +10 points (${queue_count} messages)") + fi + threat_score=$((threat_score + queue_factor)) + + # Factor 3: Delivery failures (20% weight) - Email delivery effectiveness + local failure_factor=0 + if [ "$recent_failures" -gt 20 ]; then + failure_factor=20 # High - many recent failures + factor_explanations+=("High recent failures: +20 points (${recent_failures} failures)") + elif [ "$recent_failures" -gt 10 ]; then + failure_factor=10 # Medium - some recent failures + factor_explanations+=("Recent failures: +10 points (${recent_failures} failures)") + fi + threat_score=$((threat_score + failure_factor)) + + # Factor 4: Configuration issues (15% weight) - System health + local config_factor=0 + if [ "$config_errors" -gt 0 ]; then + config_factor=15 # Configuration issues + factor_explanations+=("Configuration issues: +15 points") + fi + threat_score=$((threat_score + config_factor)) + + # Factor 5: Authentication problems (10% weight) - Security concerns + local auth_factor=0 + if [ "$auth_failures" -gt 10 ]; then + auth_factor=10 # High auth failures + factor_explanations+=("High auth failures: +10 points (${auth_failures} failures)") + elif [ "$auth_failures" -gt 5 ]; then + auth_factor=5 # Some auth failures + factor_explanations+=("Auth failures: +5 points (${auth_failures} failures)") + fi + threat_score=$((threat_score + auth_factor)) + + # Convert threat score to health score (inverse: 100 - threat_score) + local health_score=$((100 - threat_score)) + + # Determine health level based on health score (now intuitive) + local email_health_level="" + local health_color="" + if [ "$health_score" -ge 90 ]; then + email_health_level="EXCELLENT" + health_color="${GREEN}" + elif [ "$health_score" -ge 75 ]; then + email_health_level="GOOD" + health_color="${GREEN}" + elif [ "$health_score" -ge 50 ]; then + email_health_level="FAIR" + health_color="${YELLOW}" + elif [ "$health_score" -ge 25 ]; then + email_health_level="POOR" + health_color="${YELLOW}" + else + email_health_level="CRITICAL" + health_color="${RED}" + fi + + # Update issue counters based on intelligent assessment + current_high_issues=0 + current_medium_issues=0 + current_low_issues=0 + + if [ "$email_health_level" = "CRITICAL" ] || [ "$email_health_level" = "POOR" ]; then + current_high_issues=1 + elif [ "$email_health_level" = "FAIR" ]; then + current_medium_issues=1 + elif [ "$email_health_level" = "GOOD" ]; then + current_low_issues=1 + fi + + # Display health assessment + output+=("") + output+=("${BLUE}=== Intelligent Health Assessment ===${NC}") + output+=("${health_color}Health Level: $email_health_level (Score: ${health_score}/100)${NC}") + + + # Print all output at once + printf "%b\n" "${output[@]}" + # Add local issues to global counters ((high_issues+=current_high_issues)) ((medium_issues+=current_medium_issues)) ((low_issues+=current_low_issues)) - # Track which modules have issues and capture detailed info for AI analysis + # Track which modules have issues and capture detailed info for AI local email_details="" if [ $current_high_issues -gt 0 ]; then - critical_modules_found+=("Exim4") - email_details="Critical email issues: " - if [ "${frozen_count:-0}" -gt 10 ]; then - email_details+="$frozen_count frozen messages in queue (systemic problem), " - fi - if [ "${deferred:-0}" -gt 100 ]; then - email_details+="$deferred deferred messages in queue (systemic problem), " - fi - email_details+="Service may be experiencing significant problems" - elif [ $current_medium_issues -gt 0 ]; then - medium_modules_found+=("Exim4") - email_details="Email issues detected: " - if [ "${frozen_count:-0}" -gt 3 ] && [ "${frozen_count:-0}" -le 10 ]; then - email_details+="$frozen_count frozen messages in queue (needs attention), " - fi - if [ "${deferred:-0}" -gt 20 ] && [ "${deferred:-0}" -le 100 ]; then - email_details+="$deferred deferred messages in queue (needs attention), " - fi - if [ "${failed_log:-0}" -gt 50 ]; then - email_details+="High failure rate ($failed_log failures), " - fi - if [ "${deferred_log:-0}" -gt 100 ]; then - email_details+="High defer rate ($deferred_log deferrals), " - fi - if [ "${tls_errors_log:-0}" -gt 10 ]; then - email_details+="TLS issues ($tls_errors_log errors), " - fi - if [ "${smtp_timeouts_log:-0}" -gt 50 ]; then - email_details+="SMTP timeout issues ($smtp_timeouts_log timeouts), " - fi - email_details+="System performance degraded" - elif [ $current_low_issues -gt 0 ]; then - low_modules_found+=("Exim4") - email_details="Email system has minor issues: " - if [ "${frozen_count:-0}" -gt 0 ] && [ "${frozen_count:-0}" -le 3 ]; then - email_details+="$frozen_count frozen messages in queue (minor issues), " - fi - if [ "${deferred:-0}" -gt 0 ] && [ "${deferred:-0}" -le 20 ]; then - email_details+="$deferred deferred messages in queue (normal delays), " - fi - if [ "${auth_failures_log:-0}" -gt 10000 ]; then - email_details+="Extremely high authentication failures (${auth_failures_log:-0} today) - unusual brute force activity" + if [ "$email_health_level" = "CRITICAL" ]; then + critical_modules_found+=("EXIM4") else - email_details+="Minor configuration or performance issues" + high_modules_found+=("EXIM4") + fi + + # Generate intelligent threat details + if [ "$exim4_running" = false ]; then + email_details="$email_health_level health: Exim4 service not running (Score: ${health_score}/100) - EXIM4 system disabled" + elif [ "$frozen_count" -gt 10 ]; then + email_details="$email_health_level health: $frozen_count frozen messages detected (Score: ${health_score}/100) - Mail delivery severely impacted" + elif [ "$queue_count" -gt 1000 ]; then + email_details="$email_health_level health: Large queue backlog with $queue_count messages (Score: ${health_score}/100) - System overloaded" + else + email_details="$email_health_level health: Multiple EXIM4 system issues detected (Score: ${health_score}/100) - System requires attention" + fi + elif [ $current_medium_issues -gt 0 ]; then + medium_modules_found+=("EXIM4") + if [ "$frozen_count" -gt 0 ]; then + email_details="$email_health_level health: $frozen_count frozen messages (Score: ${health_score}/100) - Some delivery issues" + elif [ "$queue_count" -gt 100 ]; then + email_details="$email_health_level health: Moderate queue with $queue_count messages (Score: ${health_score}/100) - Performance impact" + else + email_details="$email_health_level health: EXIM4 system performance issues (Score: ${health_score}/100) - Monitoring recommended" + fi + elif [ $current_low_issues -gt 0 ]; then + low_modules_found+=("EXIM4") + if [ "$recent_failures" -gt 10 ]; then + email_details="$email_health_level risk: $recent_failures recent delivery failures (Score: ${health_score}/100) - Minor delivery issues" + elif [ "$auth_failures" -gt 5 ]; then + email_details="$email_health_level risk: $auth_failures authentication failures (Score: ${health_score}/100) - Minor security concerns" + else + email_details="$email_health_level risk: Minor EXIM4 system issues (Score: ${health_score}/100) - System functioning with minor concerns" fi else - email_details="Email system functioning normally: ${completed:-0} successful deliveries today, ${rejected_log:-0} spam/invalid emails rejected, ${auth_failures_log:-0} auth failures (normal activity)" + # No issues - system is healthy + local performance_info="" + if [ "$recent_deliveries" -gt 0 ]; then + performance_info="$recent_deliveries recent deliveries" + else + performance_info="system stable" + fi + + local additional_info="" + if [ "$queue_count" -gt 0 ] && [ "$queue_count" -le 10 ]; then + additional_info=", $queue_count messages in queue" + fi + + email_details="Health Level: $email_health_level (Score: ${health_score}/100) - EXIM4 system functioning optimally: services running, $performance_info$additional_info, no critical issues detected" fi detailed_report["email"]="$email_details" if [ $((current_high_issues + current_medium_issues + current_low_issues)) -gt 0 ]; then - return 1 # Indicates problems were found + return 1 else - return 0 # Indicates no problems were found + return 0 fi } @@ -1460,240 +1628,246 @@ check_ssl_status() { fi } -# Function to check PHP-FPM status with timeout +# Function to check PHP-FPM status with modern approach check_php_status() { - local output=() - output+=("${BLUE}=== PHP-FPM Status ===${NC}") + echo -e "${BLUE}=== PHP-FPM Status ===${NC}" - # Detect installed PHP versions + # Initialize counters + local current_high_issues=0 + local current_medium_issues=0 + local current_low_issues=0 + + # Detect installed PHP versions using multiple methods local php_versions=() - while IFS= read -r version; do - if [[ "$version" =~ ^php[0-9]+\.[0-9]+$ ]]; then - # Extract version number (e.g., "php7.4" -> "7.4") - version=${version#php} - php_versions+=("$version") - fi - done < <(ls /etc/php/*/fpm/php-fpm.conf 2>/dev/null | grep -o 'php[0-9]\+\.[0-9]\+' | sort -u) - # If no versions found, try alternative detection method + # Method 1: Check for running PHP-FPM services + while IFS= read -r service; do + if [[ "$service" =~ php([0-9]+\.[0-9]+)-fpm\.service ]]; then + local version="${BASH_REMATCH[1]}" + if [[ ! " ${php_versions[@]} " =~ " ${version} " ]]; then + php_versions+=("$version") + fi + fi + done < <(systemctl list-units --type=service --state=loaded php*-fpm* 2>/dev/null | grep -o 'php[0-9]\+\.[0-9]\+-fpm\.service' | sort -u) + + # Method 2: Check configuration directories (fallback) if [ ${#php_versions[@]} -eq 0 ]; then while IFS= read -r version; do if [[ "$version" =~ ^php[0-9]+\.[0-9]+$ ]]; then version=${version#php} + if [[ ! " ${php_versions[@]} " =~ " ${version} " ]]; then php_versions+=("$version") fi - done < <(ls /etc/php/*/fpm/ 2>/dev/null | grep -o 'php[0-9]\+\.[0-9]\+' | sort -u) - fi - - # If still no versions found, try one more method - if [ ${#php_versions[@]} -eq 0 ]; then - while IFS= read -r version; do - if [[ "$version" =~ ^php[0-9]+\.[0-9]+$ ]]; then - version=${version#php} - php_versions+=("$version") fi - done < <(ls /var/log/php*-fpm.log 2>/dev/null | grep -o 'php[0-9]\+\.[0-9]\+' | sort -u) + done < <(ls /etc/php/*/fpm/php-fpm.conf 2>/dev/null | grep -o 'php[0-9]\+\.[0-9]\+' | sort -u) fi if [ ${#php_versions[@]} -eq 0 ]; then - output+=("${YELLOW}⚠️ No PHP versions detected${NC}") + echo -e "${YELLOW}⚠️ No PHP versions detected${NC}" ((current_medium_issues++)) + ((medium_issues+=current_medium_issues)) + medium_modules_found+=("PHP-FPM") + detailed_report["php"]="No PHP versions detected on system" return 1 fi - local current_medium_issues=0 - local current_high_issues=0 - local current_low_issues=0 + # Initialize scoring components + local service_health=0 # 40% - Service status + local config_health=0 # 25% - Configuration validity + local performance_health=0 # 20% - Performance metrics + local log_health=0 # 15% - Critical errors only - # Get start of current day timestamp - local today_start=$(date -d "$(date +%Y-%m-%d) 00:00:00" +%s) - - echo -e "Processing PHP logs... This may take a few moments." - local total_versions=${#php_versions[@]} - local current_version=0 + local total_services=0 + local running_services=0 + local config_valid=0 + local total_configs=0 + local performance_issues=0 + local critical_log_issues=0 + # Check each PHP version for version in "${php_versions[@]}"; do - ((current_version++)) - show_progress $current_version $total_versions - output+=("${YELLOW}PHP $version:${NC}") - local log_file="/var/log/php${version}-fpm.log" + echo -e "${YELLOW}PHP $version:${NC}" + ((total_services++)) + ((total_configs++)) - if [ -f "$log_file" ]; then - # Get all log entries and process them - local log_content=$(run_with_timeout 5 "cat '$log_file' 2>/dev/null") - local grep_exit_code=$? + # 1. SERVICE STATUS (40% weight) - Direct commands + local service_name="php${version}-fpm" + local service_status=$(systemctl is-active "$service_name" 2>/dev/null) + local service_enabled=$(systemctl is-enabled "$service_name" 2>/dev/null) + + if [ "$service_status" = "active" ]; then + echo -e "${GREEN}✓ Service running${NC}" + ((running_services++)) - if [ $grep_exit_code -eq 0 ] && [ -n "$log_content" ]; then - # Initialize counters for this PHP version - local max_children_count=0 - local error_count=0 - local warning_count=0 - local memory_issues=0 - local timeout_issues=0 - local connection_issues=0 - local zombie_processes=0 - - local max_children_pools=() - local error_messages=() - local warning_messages=() - local memory_messages=() - local timeout_messages=() - local connection_messages=() - local zombie_messages=() - - # Process each line - while IFS= read -r line; do - # Extract timestamp from line - local log_date=$(echo "$line" | grep -o '^[0-9]\{2\}-[A-Za-z]\{3\}-[0-9]\{4\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}') - if [ -n "$log_date" ]; then - # Convert log date to timestamp - local log_ts=$(date -d "$log_date" +%s 2>/dev/null) - - # Only process if timestamp is valid and from today - if [ -n "$log_ts" ] && [ "$log_ts" -ge "$today_start" ]; then - # Check for max_children issues - if [[ "$line" =~ max_children[[:space:]]+setting ]]; then - ((max_children_count++)) - # Extract pool name - local pool=$(echo "$line" | grep -o '\[pool [^]]*\]' | sed 's/\[pool //;s/\]//') - if [ -n "$pool" ]; then - max_children_pools+=("$pool|$log_date") - fi - fi - - # Check for memory issues - if [[ "$line" =~ "Allowed memory size" ]] || [[ "$line" =~ "memory_limit" ]] || [[ "$line" =~ "Out of memory" ]]; then - ((memory_issues++)) - memory_messages+=("$line") - fi - - # Check for timeout issues - if [[ "$line" =~ "Maximum execution time" ]] || [[ "$line" =~ "max_execution_time" ]] || [[ "$line" =~ "timeout" ]]; then - ((timeout_issues++)) - timeout_messages+=("$line") - fi - - # Check for connection issues - if [[ "$line" =~ "Connection refused" ]] || [[ "$line" =~ "Connection timed out" ]] || [[ "$line" =~ "Connection reset" ]]; then - ((connection_issues++)) - connection_messages+=("$line") - fi - - # Check for zombie processes - if [[ "$line" =~ "zombie" ]] || [[ "$line" =~ "defunct" ]]; then - ((zombie_processes++)) - zombie_messages+=("$line") - fi - - # Check for errors - if [[ "$line" =~ ERROR|FATAL ]]; then - ((error_count++)) - error_messages+=("$line") - fi - - # Check for warnings (excluding max_children) - if [[ "$line" =~ WARNING ]] && ! [[ "$line" =~ max_children[[:space:]]+setting ]]; then - ((warning_count++)) - warning_messages+=("$line") - fi - fi - fi - done <<< "$log_content" - - # Display results - if [ "$max_children_count" -gt 0 ] || [ "$error_count" -gt 0 ] || [ "$warning_count" -gt 0 ] || [ "$memory_issues" -gt 0 ] || [ "$timeout_issues" -gt 0 ] || [ "$connection_issues" -gt 0 ] || [ "$zombie_processes" -gt 0 ]; then - if [ "$max_children_count" -gt 0 ]; then - output+=("${YELLOW}⚠️ Performance issues:${NC}") - # Group by pool and show last occurrence - declare -A pool_last_occurrence - declare -A pool_count - - for pool_info in "${max_children_pools[@]}"; do - IFS='|' read -r pool date <<< "$pool_info" - pool_last_occurrence["$pool"]="$date" - ((pool_count["$pool"]++)) - done - - for pool in "${!pool_last_occurrence[@]}"; do - output+=(" - Pool '$pool':") - output+=(" * Reached max_children limit ${pool_count[$pool]} times") - output+=(" * Last occurrence: [${pool_last_occurrence[$pool]}]") - done - ((current_medium_issues++)) - fi - - if [ "$memory_issues" -gt 0 ]; then - output+=("${RED}⚠️ Memory issues: $memory_issues${NC}") - output+=(" Last memory issues:") - for ((i=${#memory_messages[@]}-1; i>=0 && i>=${#memory_messages[@]}-3; i--)); do - output+=(" - ${memory_messages[$i]}") - done - ((current_high_issues++)) - fi - - if [ "$timeout_issues" -gt 0 ]; then - output+=("${YELLOW}⚠️ Timeout issues: $timeout_issues${NC}") - output+=(" Last timeout issues:") - for ((i=${#timeout_messages[@]}-1; i>=0 && i>=${#timeout_messages[@]}-3; i--)); do - output+=(" - ${timeout_messages[$i]}") - done - ((current_medium_issues++)) - fi - - if [ "$connection_issues" -gt 0 ]; then - output+=("${YELLOW}⚠️ Connection issues: $connection_issues${NC}") - output+=(" Last connection issues:") - for ((i=${#connection_messages[@]}-1; i>=0 && i>=${#connection_messages[@]}-3; i--)); do - output+=(" - ${connection_messages[$i]}") - done - ((current_medium_issues++)) - fi - - if [ "$zombie_processes" -gt 0 ]; then - output+=("${RED}⚠️ Zombie processes detected: $zombie_processes${NC}") - output+=(" Last zombie process reports:") - for ((i=${#zombie_messages[@]}-1; i>=0 && i>=${#zombie_messages[@]}-3; i--)); do - output+=(" - ${zombie_messages[$i]}") - done - ((current_high_issues++)) - fi - - if [ "$error_count" -gt 0 ]; then - output+=("${RED}⚠️ Errors: $error_count${NC}") - output+=(" Last errors:") - for ((i=${#error_messages[@]}-1; i>=0 && i>=${#error_messages[@]}-3; i--)); do - output+=(" - ${error_messages[$i]}") - done - ((current_high_issues++)) - fi - - if [ "$warning_count" -gt 0 ]; then - output+=("${YELLOW}⚠️ Warnings: $warning_count${NC}") - output+=(" Last warnings:") - for ((i=${#warning_messages[@]}-1; i>=0 && i>=${#warning_messages[@]}-3; i--)); do - output+=(" - ${warning_messages[$i]}") - done - ((current_medium_issues++)) - fi - else - output+=("${GREEN}✓ No issues today${NC}") - fi - else - output+=("${YELLOW}⚠️ Could not read log file${NC}") - ((current_medium_issues++)) + # Get process info + local process_count=$(pgrep -c "php-fpm.*${version}" 2>/dev/null || echo "0") + if [ "$process_count" -gt 0 ]; then + echo -e " * Active processes: $process_count" fi else - output+=("${YELLOW}⚠️ Log file not found${NC}") + echo -e "${RED}⚠️ Service not running (status: $service_status)${NC}" + ((current_high_issues++)) + fi + + if [ "$service_enabled" = "enabled" ]; then + echo -e " * Auto-start: enabled" + else + echo -e "${YELLOW} * Auto-start: $service_enabled${NC}" + ((current_low_issues++)) + fi + + # 2. CONFIGURATION VALIDATION (25% weight) - Direct commands + local config_file="/etc/php/${version}/fpm/php-fpm.conf" + if [ -f "$config_file" ]; then + # Test configuration + local config_test=$(php-fpm${version} -t 2>&1) + if echo "$config_test" | grep -q "configuration file.*test is successful"; then + echo -e "${GREEN}✓ Configuration valid${NC}" + ((config_valid++)) + else + echo -e "${RED}⚠️ Configuration issues detected${NC}" + echo -e " * ${config_test}" + ((current_medium_issues++)) + fi + + # Show key configuration details + local max_children=$(grep -r "pm.max_children" "/etc/php/${version}/fpm/pool.d/" 2>/dev/null | head -1 | awk '{print $NF}' | tr -d '\n' || echo "Unknown") + local process_manager=$(grep -r "^pm =" "/etc/php/${version}/fpm/pool.d/" 2>/dev/null | head -1 | awk '{print $3}' | tr -d '\n' || echo "Unknown") + + if [ "$max_children" != "Unknown" ]; then + echo -e " * Max children: $max_children" + echo -e " * Process manager: $process_manager" + fi + else + echo -e "${RED}⚠️ Configuration file not found${NC}" ((current_medium_issues++)) fi + + # 3. PERFORMANCE METRICS (20% weight) - Memory and resource usage + if [ "$service_status" = "active" ]; then + # Check memory usage of PHP-FPM processes + local memory_usage=$(ps -o pid,rss,comm -C "php-fpm${version}" --no-headers 2>/dev/null | awk '{sum += $2} END {print sum/1024}' | cut -d. -f1) + if [ -n "$memory_usage" ] && [ "$memory_usage" -gt 0 ]; then + echo -e " * Memory usage: ${memory_usage}MB" + + # Check if memory usage is concerning (>500MB per version) + if [ "$memory_usage" -gt 500 ]; then + echo -e "${YELLOW} (High memory usage)${NC}" + ((performance_issues++)) + fi + fi + + # Check for any stuck processes (running > 1 hour) + local stuck_processes=$(ps -eo pid,etime,comm | grep "php-fpm.*${version}" | awk '$2 ~ /^[0-9]+-/ || $2 ~ /^[2-9][0-9]:[0-9][0-9]:[0-9][0-9]$/' | wc -l) + # Ensure stuck_processes is a valid integer + stuck_processes=${stuck_processes:-0} + stuck_processes=$(echo "$stuck_processes" | tr -d ' \n' | grep -o '^[0-9]*' || echo "0") + if [ "$stuck_processes" -gt 0 ] 2>/dev/null; then + echo -e "${YELLOW} * Potentially stuck processes: $stuck_processes${NC}" + ((performance_issues++)) + fi + fi + + # 4. CRITICAL LOG ANALYSIS (15% weight) - Only check for critical issues + local log_file="/var/log/php${version}-fpm.log" + if [ -f "$log_file" ]; then + # Only check today's critical errors (much faster than full log analysis) + local today=$(date "+%d-%b-%Y") + local critical_errors=$(grep -c "$today.*\(FATAL\|Out of memory\|zombie\|segmentation fault\)" "$log_file" 2>/dev/null || echo "0") + + # Ensure critical_errors is a valid integer + critical_errors=${critical_errors:-0} + critical_errors=$(echo "$critical_errors" | tr -d ' \n' | grep -o '^[0-9]*' || echo "0") + + if [ "$critical_errors" -gt 0 ] 2>/dev/null; then + echo -e "${RED}⚠️ Critical errors today: $critical_errors${NC}" + # Show last critical error + local last_error=$(grep "$today.*\(FATAL\|Out of memory\|zombie\|segmentation fault\)" "$log_file" 2>/dev/null | tail -1) + if [ -n "$last_error" ]; then + echo -e " * Last: $(echo "$last_error" | cut -c1-80)..." + fi + ((critical_log_issues++)) + ((current_high_issues++)) + else + echo -e "${GREEN}✓ No critical issues today${NC}" + fi + else + echo -e "${YELLOW}⚠️ Log file not found${NC}" + ((current_low_issues++)) + fi + + echo "" done - if [ $current_medium_issues -eq 0 ] && [ $current_high_issues -eq 0 ]; then - output+=("${GREEN}✓ All PHP versions running without issues${NC}") + # Calculate Health Score (0-100) + # Service Health (40%) + if [ "$total_services" -gt 0 ]; then + service_health=$(( (running_services * 100) / total_services )) + fi + + # Configuration Health (25%) + if [ "$total_configs" -gt 0 ]; then + config_health=$(( (config_valid * 100) / total_configs )) + fi + + # Performance Health (20%) - Inverse of issues + if [ "$performance_issues" -eq 0 ]; then + performance_health=100 + elif [ "$performance_issues" -le 2 ]; then + performance_health=70 + elif [ "$performance_issues" -le 5 ]; then + performance_health=40 + else + performance_health=10 + fi + + # Log Health (15%) - Inverse of critical issues + if [ "$critical_log_issues" -eq 0 ]; then + log_health=100 + elif [ "$critical_log_issues" -le 3 ]; then + log_health=60 + elif [ "$critical_log_issues" -le 10 ]; then + log_health=30 + else + log_health=0 + fi + + # Calculate weighted health score + local health_score=$(( (service_health * 40 + config_health * 25 + performance_health * 20 + log_health * 15) / 100 )) + + # Determine health level + local php_health_level="" + local health_color="" + if [ "$health_score" -ge 90 ]; then + php_health_level="EXCELLENT" + health_color="${GREEN}" + elif [ "$health_score" -ge 75 ]; then + php_health_level="GOOD" + health_color="${GREEN}" + elif [ "$health_score" -ge 50 ]; then + php_health_level="FAIR" + health_color="${YELLOW}" + ((current_medium_issues++)) + elif [ "$health_score" -ge 25 ]; then + php_health_level="POOR" + health_color="${YELLOW}" + ((current_high_issues++)) + else + php_health_level="CRITICAL" + health_color="${RED}" + ((current_high_issues++)) + fi + + # Display Health Assessment + echo -e "${BLUE}=== Intelligent Health Assessment ===${NC}" + echo -e "${health_color}Health Level: $php_health_level (Score: ${health_score}/100)${NC}" + + # Show summary + if [ ${#php_versions[@]} -eq 1 ]; then + echo -e "✓ PHP ${php_versions[0]} analyzed" + else + echo -e "✓ ${#php_versions[@]} PHP versions analyzed: $(printf '%s ' "${php_versions[@]}")" fi - - # Return the output as a string - printf "%b\n" "${output[@]}" # Add local issues to global counters ((high_issues+=current_high_issues)) @@ -1703,16 +1877,21 @@ check_php_status() { # Track which modules have issues and capture detailed info for AI local php_details="" if [ $current_high_issues -gt 0 ]; then + if [ "$php_health_level" = "CRITICAL" ]; then critical_modules_found+=("PHP-FPM") - php_details="Critical issues found across ${#php_versions[@]} PHP version(s): Memory issues, zombie processes, or critical errors detected" + php_details="Health Level: $php_health_level (Score: ${health_score}/100) - Critical PHP issues: Service failures, critical errors, or major configuration problems across ${#php_versions[@]} version(s)" + else + high_modules_found+=("PHP-FPM") + php_details="Health Level: $php_health_level (Score: ${health_score}/100) - PHP issues requiring attention: Performance problems or minor errors across ${#php_versions[@]} version(s)" + fi elif [ $current_medium_issues -gt 0 ]; then medium_modules_found+=("PHP-FPM") - php_details="Medium issues found across ${#php_versions[@]} PHP version(s): Performance issues (max_children reached), timeout issues, or warnings detected" + php_details="Health Level: $php_health_level (Score: ${health_score}/100) - PHP monitoring needed: Configuration issues or performance concerns across ${#php_versions[@]} version(s)" elif [ $current_low_issues -gt 0 ]; then low_modules_found+=("PHP-FPM") - php_details="Low issues found across ${#php_versions[@]} PHP version(s): Minor warnings or connection issues" + php_details="Health Level: $php_health_level (Score: ${health_score}/100) - PHP minor issues: Non-critical warnings across ${#php_versions[@]} version(s)" else - php_details="No issues found across ${#php_versions[@]} PHP version(s): $(printf '%s ' "${php_versions[@]}")" + php_details="Health Level: $php_health_level (Score: ${health_score}/100) - PHP-FPM functioning optimally: ${#php_versions[@]} version(s) running without issues ($(printf '%s ' "${php_versions[@]}"))" fi detailed_report["php"]="$php_details" @@ -1724,282 +1903,272 @@ check_php_status() { fi } -# Function to check MySQL status with timeout +# Function to check MySQL status with modern approach check_mysql_status() { - local output=() - output+=("${BLUE}=== MySQL Status ===${NC}") + echo -e "${BLUE}=== MySQL Status ===${NC}" + # Initialize counters local current_high_issues=0 local current_medium_issues=0 local current_low_issues=0 - # Error Log - output+=("${YELLOW}Error Log:${NC}") - if [ -f "/var/log/mysql/error.log" ]; then - # Get start of current day timestamp - local today_start=$(date -d "$(date +%Y-%m-%d) 00:00:00" +%s) - - # Get all log entries and process them, removing null bytes - local log_content=$(run_with_timeout 5 "cat '/var/log/mysql/error.log' 2>/dev/null | tr -d '\0'") - - if [ -n "$log_content" ]; then - # Initialize counters and arrays - local crashed_tables=() - local timeout_dbs=() - local access_denied_errors=() - local connection_errors=() - - # Count total lines for progress bar - local total_lines=$(echo "$log_content" | wc -l) - echo -e "Processing MySQL log... Found $total_lines lines to analyze." - local current_line=0 - - # Process each line - while IFS= read -r line; do - ((current_line++)) - if [ $((current_line % 100)) -eq 0 ] || [ $current_line -eq $total_lines ]; then - show_progress $current_line $total_lines - fi - - # Remove any remaining null bytes from the line - line=$(echo "$line" | tr -d '\0') - - # Extract timestamp from line (MySQL format: YYYY-MM-DD HH:MM:SS) - local log_date=$(echo "$line" | grep -o '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}') - if [ -n "$log_date" ]; then - # Convert log date to timestamp - local log_ts=$(date -d "$log_date" +%s 2>/dev/null) - - # Only process if timestamp is valid and from today - if [ -n "$log_ts" ] && [ "$log_ts" -ge "$today_start" ]; then - # Check for table crashes - if [[ "$line" =~ Table.*is[[:space:]]+marked[[:space:]]+as[[:space:]]+crashed ]]; then - local table="" - local db="" - - # Try to extract table name and database from different formats - if [[ "$line" =~ Table[[:space:]]+\'\./([^/]+)/([^\']+)\' ]]; then - # Format: Table './database/table' - db="${BASH_REMATCH[1]}" - table="${BASH_REMATCH[2]}" - elif [[ "$line" =~ Table[[:space:]]+\'([^\']+)\' ]]; then - # Format: Table 'table' - table="${BASH_REMATCH[1]}" - # Try to extract database from the table name if it contains a prefix - if [[ "$table" =~ ^([a-zA-Z0-9_]+)_ ]]; then - db="${BASH_REMATCH[1]}" - fi - fi - - if [ -n "$table" ]; then - crashed_tables+=("$table|$db|$log_date") - fi - fi - - # Check for connection timeouts and aborted connections - if [[ "$line" =~ Aborted[[:space:]]+connection.*Got[[:space:]]+timeout[[:space:]]+reading[[:space:]]+communication[[:space:]]+packets ]] || \ - [[ "$line" =~ Aborted[[:space:]]+connection.*Got[[:space:]]+an[[:space:]]+error[[:space:]]+reading[[:space:]]+communication[[:space:]]+packets ]]; then - local db=$(echo "$line" | grep -o "db: '[^']*'" | sed "s/db: '//;s/'//") - local user=$(echo "$line" | grep -o "user: '[^']*'" | sed "s/user: '//;s/'//") - if [ -n "$db" ] && [ -n "$user" ]; then - timeout_dbs+=("$db|$user|$log_date") - fi - fi - - # Check for access denied errors - if [[ "$line" =~ Access[[:space:]]+denied ]]; then - local user=$(echo "$line" | grep -o "user '[^']*'" | sed "s/user '//;s/'//") - if [ -n "$user" ]; then - access_denied_errors+=("$user|$log_date") - fi - fi - - # Check for general connection errors - if [[ "$line" =~ "Connection refused" ]] || [[ "$line" =~ "Connection timed out" ]] || [[ "$line" =~ "Connection reset" ]]; then - connection_errors+=("$line|$log_date") - fi - fi - fi - done <<< "$log_content" - echo -e "\n" # Add newline after progress bar - - # Process and display crashed tables - if [ ${#crashed_tables[@]} -gt 0 ]; then - output+=("\n${RED}⚠️ Crashed Tables Found:${NC}") - # Group by table name - declare -A table_crashes - declare -A table_last_crash - declare -A table_db - - for table_info in "${crashed_tables[@]}"; do - IFS='|' read -r table db date <<< "$table_info" - ((table_crashes[$table]++)) - table_last_crash[$table]="$date" - table_db[$table]="$db" - done - - for table in "${!table_crashes[@]}"; do - output+=(" - Table '$table':") - if [ -n "${table_db[$table]}" ]; then - output+=(" * Database: ${table_db[$table]}") - fi - output+=(" * Crash count: ${table_crashes[$table]}") - output+=(" * Last crash: ${table_last_crash[$table]}") - output+=(" * Recommended actions:") - if [ -n "${table_db[$table]}" ]; then - output+=(" 1. Connect to MySQL: mysql -u root -p") - output+=(" 2. Select database: USE ${table_db[$table]};") - output+=(" 3. Repair table: REPAIR TABLE \`$table\`;") - output+=(" 4. Check table: CHECK TABLE \`$table\`;") - output+=(" 5. Optimize table: OPTIMIZE TABLE \`$table\`;") - else - output+=(" 1. Connect to MySQL: mysql -u root -p") - output+=(" 2. Find the database containing this table:") - output+=(" SELECT TABLE_SCHEMA FROM information_schema.TABLES WHERE TABLE_NAME = '$table';") - output+=(" 3. Select the database: USE ;") - output+=(" 4. Repair table: REPAIR TABLE \`$table\`;") - output+=(" 5. Check table: CHECK TABLE \`$table\`;") - output+=(" 6. Optimize table: OPTIMIZE TABLE \`$table\`;") - fi - done - ((current_high_issues++)) - fi - - # Process and display connection timeouts - if [ ${#timeout_dbs[@]} -gt 0 ]; then - output+=("\n${YELLOW}⚠️ Connection Timeouts:${NC}") - # Group by database and user - declare -A db_timeouts - declare -A db_last_timeout - declare -A db_user_timeouts - - for db_info in "${timeout_dbs[@]}"; do - IFS='|' read -r db user date <<< "$db_info" - local key="$db|$user" - ((db_timeouts[$db]++)) - ((db_user_timeouts[$key]++)) - db_last_timeout[$key]="$date" - done - - # First show database totals - output+=(" Database Totals:") - for db in "${!db_timeouts[@]}"; do - output+=(" - Database '$db':") - output+=(" * Total timeouts: ${db_timeouts[$db]}") - - # Add severity based on number of timeouts per database - if [ "${db_timeouts[$db]}" -gt 100 ]; then - output+=(" * ${RED}Severity: Critical (High number of timeouts)${NC}") - ((current_high_issues++)) - elif [ "${db_timeouts[$db]}" -gt 10 ]; then - output+=(" * ${YELLOW}Severity: Medium (Multiple timeouts)${NC}") - ((current_medium_issues++)) - else - output+=(" * ${GREEN}Severity: Low (Few timeouts)${NC}") - ((current_low_issues++)) - fi - done - - # Then show detailed user breakdown - output+=("\n User Breakdown:") - for key in "${!db_user_timeouts[@]}"; do - IFS='|' read -r db user <<< "$key" - output+=(" - Database '$db' (User: '$user'):") - output+=(" * Timeout count: ${db_user_timeouts[$key]}") - output+=(" * Last timeout: ${db_last_timeout[$key]}") - done - fi - - # Process and display access denied errors - if [ ${#access_denied_errors[@]} -gt 0 ]; then - output+=("\n${YELLOW}⚠️ Access Denied Errors:${NC}") - # Group by user and database - declare -A user_denials - declare -A user_last_denial - declare -A db_user_denials - - for user_info in "${access_denied_errors[@]}"; do - IFS='|' read -r user date <<< "$user_info" - # Extract database from user if available (format: user@database) - local db="" - if [[ "$user" =~ @ ]]; then - db=$(echo "$user" | cut -d'@' -f2) - user=$(echo "$user" | cut -d'@' -f1) - fi - - if [ -n "$db" ]; then - local key="$db|$user" - ((db_user_denials[$key]++)) - fi - ((user_denials[$user]++)) - user_last_denial[$user]="$date" - done - - # First show user totals - output+=(" User Totals:") - for user in "${!user_denials[@]}"; do - output+=(" - User '$user':") - output+=(" * Total denials: ${user_denials[$user]}") - output+=(" * Last denial: ${user_last_denial[$user]}") - - # Add severity based on number of denials per user - if [ "${user_denials[$user]}" -gt 50 ]; then - output+=(" * ${RED}Severity: Critical (High number of attempts)${NC}") - ((current_high_issues++)) - elif [ "${user_denials[$user]}" -gt 10 ]; then - output+=(" * ${YELLOW}Severity: Medium (Multiple attempts)${NC}") - ((current_medium_issues++)) - else - output+=(" * ${GREEN}Severity: Low (Few attempts)${NC}") - ((current_low_issues++)) - fi - done - - # Then show database-specific breakdown if available - if [ ${#db_user_denials[@]} -gt 0 ]; then - output+=("\n Database-Specific Breakdown:") - for key in "${!db_user_denials[@]}"; do - IFS='|' read -r db user <<< "$key" - output+=(" - Database '$db' (User: '$user'):") - output+=(" * Denial count: ${db_user_denials[$key]}") - done - fi - fi - - # Process and display general connection errors - if [ ${#connection_errors[@]} -gt 0 ]; then - output+=("\n${YELLOW}⚠️ General Connection Errors:${NC}") - # Show last 5 connection errors - for ((i=${#connection_errors[@]}-1; i>=0 && i>=${#connection_errors[@]}-5; i--)); do - IFS='|' read -r error date <<< "${connection_errors[$i]}" - output+=(" - [$date] $error") - done - ((current_medium_issues++)) - fi - - # If no errors found - if [ ${#crashed_tables[@]} -eq 0 ] && [ ${#timeout_dbs[@]} -eq 0 ] && [ ${#access_denied_errors[@]} -eq 0 ] && [ ${#connection_errors[@]} -eq 0 ]; then - output+=("${GREEN}✓ No recent errors${NC}") - fi - else - output+=("${GREEN}✓ No errors found today${NC}") - fi + # Initialize scoring components + local service_health=0 # 35% - Service status + local connection_health=0 # 30% - Connection capability + local performance_health=0 # 20% - Performance metrics + local config_health=0 # 15% - Configuration status + + # 1. SERVICE STATUS (35% weight) - Direct commands + local mysql_service="" + local service_status="" + local service_enabled="" + + # Detect MySQL/MariaDB service + if systemctl list-units --type=service | grep -q "mariadb.service"; then + mysql_service="mariadb" + elif systemctl list-units --type=service | grep -q "mysql.service"; then + mysql_service="mysql" else - output+=("${RED}⚠️ Log file not found${NC}") - ((current_medium_issues++)) + echo -e "${RED}⚠️ No MySQL/MariaDB service detected${NC}" + ((current_high_issues++)) + service_health=0 fi - # Service Status - output+=("\n${YELLOW}Service Status:${NC}") - if run_with_timeout 5 "systemctl is-active --quiet mariadb"; then - output+=("${GREEN}✓ Service is running${NC}") + if [ -n "$mysql_service" ]; then + service_status=$(systemctl is-active "$mysql_service" 2>/dev/null) + service_enabled=$(systemctl is-enabled "$mysql_service" 2>/dev/null) + + if [ "$service_status" = "active" ]; then + echo -e "${GREEN}✓ $mysql_service service running${NC}" + service_health=100 + + # Get process info + local process_count=$(pgrep -c "mysqld\|mariadbd" 2>/dev/null || echo "0") + # Ensure process_count is a valid integer + process_count=${process_count:-0} + process_count=$(echo "$process_count" | tr -d ' \n' | grep -o '^[0-9]*' || echo "0") + if [ "$process_count" -gt 0 ] 2>/dev/null; then + echo -e " * Active processes: $process_count" + fi + else + echo -e "${RED}⚠️ $mysql_service service not running (status: $service_status)${NC}" + ((current_high_issues++)) + service_health=0 + fi + + if [ "$service_enabled" = "enabled" ]; then + echo -e " * Auto-start: enabled" + else + echo -e "${YELLOW} * Auto-start: $service_enabled${NC}" + ((current_low_issues++)) + fi + fi + + # 2. CONNECTION HEALTH (30% weight) - Direct MySQL commands + if [ "$service_status" = "active" ]; then + # Test basic connection + local mysql_version="" + local connection_test=$(mysql -e "SELECT VERSION();" 2>&1) + + if echo "$connection_test" | grep -q -E "([0-9]+\.[0-9]+)"; then + mysql_version=$(echo "$connection_test" | grep -E "([0-9]+\.[0-9]+)" | head -1 | tr -d '\n') + echo -e "${GREEN}✓ Database connection successful${NC}" + echo -e " * Version: $mysql_version" + connection_health=100 + + # Test connection limits and current connections + local max_connections=$(mysql -e "SHOW VARIABLES LIKE 'max_connections';" 2>/dev/null | tail -n +2 | awk '{print $2}') + local current_connections=$(mysql -e "SHOW STATUS LIKE 'Threads_connected';" 2>/dev/null | tail -n +2 | awk '{print $2}') + + # Ensure values are valid integers + max_connections=${max_connections:-0} + current_connections=${current_connections:-0} + max_connections=$(echo "$max_connections" | tr -d ' \n' | grep -o '^[0-9]*' || echo "0") + current_connections=$(echo "$current_connections" | tr -d ' \n' | grep -o '^[0-9]*' || echo "0") + + if [ "$max_connections" -gt 0 ] && [ "$current_connections" -ge 0 ] 2>/dev/null; then + echo -e " * Connections: $current_connections/$max_connections" + + # Calculate connection usage percentage + local connection_usage=$(( (current_connections * 100) / max_connections )) + if [ "$connection_usage" -gt 80 ] 2>/dev/null; then + echo -e "${YELLOW} (High connection usage: ${connection_usage}%)${NC}" + ((current_medium_issues++)) + connection_health=70 + elif [ "$connection_usage" -gt 90 ] 2>/dev/null; then + echo -e "${RED} (Critical connection usage: ${connection_usage}%)${NC}" + ((current_high_issues++)) + connection_health=40 + fi + fi + else + echo -e "${RED}⚠️ Database connection failed${NC}" + echo -e " * Error: $(echo "$connection_test" | head -n 1)" + ((current_high_issues++)) + connection_health=0 + fi else - output+=("${RED}⚠️ Service is not running${NC}") + echo -e "${RED}⚠️ Cannot test connection - service not running${NC}" + connection_health=0 + fi + + # 3. PERFORMANCE METRICS (20% weight) - Key performance indicators + if [ "$service_status" = "active" ] && [ "$connection_health" -gt 0 ]; then + # Get key performance metrics + local uptime=$(mysql -e "SHOW STATUS LIKE 'Uptime';" 2>/dev/null | tail -n +2 | awk '{print $2}') + local queries=$(mysql -e "SHOW STATUS LIKE 'Queries';" 2>/dev/null | tail -n +2 | awk '{print $2}') + local slow_queries=$(mysql -e "SHOW STATUS LIKE 'Slow_queries';" 2>/dev/null | tail -n +2 | awk '{print $2}') + + if [ -n "$uptime" ] && [ "$uptime" -gt 0 ]; then + local uptime_hours=$(( uptime / 3600 )) + echo -e " * Uptime: ${uptime_hours} hours" + + if [ -n "$queries" ] && [ "$queries" -gt 0 ]; then + local qps=$(( queries / uptime )) + echo -e " * Queries per second: $qps" + fi + + if [ -n "$slow_queries" ]; then + echo -e " * Slow queries: $slow_queries" + if [ "$slow_queries" -gt 100 ]; then + echo -e "${YELLOW} (High number of slow queries)${NC}" + ((current_medium_issues++)) + performance_health=60 + elif [ "$slow_queries" -gt 1000 ]; then + echo -e "${RED} (Critical number of slow queries)${NC}" + ((current_high_issues++)) + performance_health=30 + else + performance_health=100 + fi + else + performance_health=100 + fi + else + performance_health=80 + fi + + # Check memory usage + local memory_usage=$(ps -o pid,rss,comm -C "mysqld,mariadbd" --no-headers 2>/dev/null | awk '{sum += $2} END {print sum/1024}' | cut -d. -f1) + if [ -n "$memory_usage" ] && [ "$memory_usage" -gt 0 ]; then + echo -e " * Memory usage: ${memory_usage}MB" + + # Check if memory usage is concerning (>2GB) + if [ "$memory_usage" -gt 2048 ]; then + echo -e "${YELLOW} (High memory usage)${NC}" + if [ "$performance_health" -gt 70 ]; then + performance_health=70 + fi + fi + fi + else + performance_health=0 + fi + + # 4. CONFIGURATION HEALTH (15% weight) - Basic config validation + if [ "$service_status" = "active" ] && [ "$connection_health" -gt 0 ]; then + # Check key configuration parameters + local innodb_buffer_pool=$(mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';" 2>/dev/null | tail -n +2 | awk '{print $2}') + local query_cache=$(mysql -e "SHOW VARIABLES LIKE 'query_cache_size';" 2>/dev/null | tail -n +2 | awk '{print $2}') + + if [ -n "$innodb_buffer_pool" ]; then + echo -e " * InnoDB buffer pool: $(( innodb_buffer_pool / 1024 / 1024 ))MB" + config_health=100 + else + config_health=80 + fi + + # Check for any obvious configuration issues + local config_warnings=$(mysql -e "SHOW WARNINGS;" 2>/dev/null | wc -l) + if [ "$config_warnings" -gt 1 ]; then # More than header line + echo -e "${YELLOW} * Configuration warnings detected${NC}" + ((current_low_issues++)) + config_health=70 + fi + else + config_health=0 + fi + + # 5. SELECTIVE LOG ANALYSIS (Only if issues detected) - Smart approach + local critical_log_issues=0 + if [ "$current_high_issues" -gt 0 ] || [ "$current_medium_issues" -gt 0 ]; then + echo -e "\n${YELLOW}Analyzing logs for additional context...${NC}" + + local log_file="" + if [ -f "/var/log/mysql/error.log" ]; then + log_file="/var/log/mysql/error.log" + elif [ -f "/var/log/mariadb/mariadb.log" ]; then + log_file="/var/log/mariadb/mariadb.log" + elif [ -f "/var/log/mysqld.log" ]; then + log_file="/var/log/mysqld.log" + fi + + if [ -n "$log_file" ] && [ -f "$log_file" ]; then + # Only check today's critical errors (much faster) + local today=$(date "+%Y-%m-%d") + local critical_errors=$(grep -c "$today.*\(ERROR\|FATAL\|crashed\|Aborted\)" "$log_file" 2>/dev/null || echo "0") + + if [ "$critical_errors" -gt 0 ]; then + echo -e "${RED}⚠️ Critical errors today: $critical_errors${NC}" + # Show last critical error + local last_error=$(grep "$today.*\(ERROR\|FATAL\|crashed\|Aborted\)" "$log_file" 2>/dev/null | tail -1) + if [ -n "$last_error" ]; then + echo -e " * Last: $(echo "$last_error" | cut -c1-80)..." + fi + ((critical_log_issues++)) + else + echo -e "${GREEN}✓ No critical errors in today's logs${NC}" + fi + else + echo -e "${YELLOW}⚠️ Log file not found for detailed analysis${NC}" + fi + fi + + # Calculate Health Score (0-100) + local health_score=$(( (service_health * 35 + connection_health * 30 + performance_health * 20 + config_health * 15) / 100 )) + + # Adjust for critical log issues + if [ "$critical_log_issues" -gt 0 ]; then + health_score=$(( health_score - (critical_log_issues * 10) )) + if [ "$health_score" -lt 0 ]; then + health_score=0 + fi + fi + + # Determine health level + local mysql_health_level="" + local health_color="" + if [ "$health_score" -ge 90 ]; then + mysql_health_level="EXCELLENT" + health_color="${GREEN}" + elif [ "$health_score" -ge 75 ]; then + mysql_health_level="GOOD" + health_color="${GREEN}" + elif [ "$health_score" -ge 50 ]; then + mysql_health_level="FAIR" + health_color="${YELLOW}" + ((current_medium_issues++)) + elif [ "$health_score" -ge 25 ]; then + mysql_health_level="POOR" + health_color="${YELLOW}" + ((current_high_issues++)) + else + mysql_health_level="CRITICAL" + health_color="${RED}" ((current_high_issues++)) fi - # Print all output at once - printf "%b\n" "${output[@]}" + # Display Health Assessment + echo -e "\n${BLUE}=== Intelligent Health Assessment ===${NC}" + echo -e "${health_color}Health Level: $mysql_health_level (Score: ${health_score}/100)${NC}" + + # Show summary + if [ -n "$mysql_service" ]; then + echo -e "✓ $mysql_service service analyzed" + if [ -n "$mysql_version" ]; then + echo -e "✓ Version: $mysql_version" + fi + fi # Add local issues to global counters ((high_issues+=current_high_issues)) @@ -2009,35 +2178,21 @@ check_mysql_status() { # Track which modules have issues and capture detailed info for AI local mysql_details="" if [ $current_high_issues -gt 0 ]; then + if [ "$mysql_health_level" = "CRITICAL" ]; then critical_modules_found+=("MySQL") - mysql_details="Critical issues found: " - if [ ${#crashed_tables[@]} -gt 0 ]; then - mysql_details+="${#crashed_tables[@]} crashed tables, " + mysql_details="Health Level: $mysql_health_level (Score: ${health_score}/100) - Critical MySQL issues: Service failures, connection problems, or major performance issues" + else + high_modules_found+=("MySQL") + mysql_details="Health Level: $mysql_health_level (Score: ${health_score}/100) - MySQL issues requiring attention: Performance problems or connection issues" fi - mysql_details+="Service status: $(systemctl is-active mariadb 2>/dev/null || echo 'inactive')" elif [ $current_medium_issues -gt 0 ]; then medium_modules_found+=("MySQL") - mysql_details="Medium issues found: " - if [ ${#timeout_dbs[@]} -gt 0 ]; then - # Count unique databases with timeouts - local unique_dbs=() - for db_info in "${timeout_dbs[@]}"; do - IFS='|' read -r db user date <<< "$db_info" - if [[ ! " ${unique_dbs[@]} " =~ " ${db} " ]]; then - unique_dbs+=("$db") - fi - done - mysql_details+="${#timeout_dbs[@]} connection timeouts affecting ${#unique_dbs[@]} database(s), " - fi - if [ ${#access_denied_errors[@]} -gt 0 ]; then - mysql_details+="${#access_denied_errors[@]} access denied errors, " - fi - mysql_details+="Service status: $(systemctl is-active mariadb 2>/dev/null || echo 'inactive')" + mysql_details="Health Level: $mysql_health_level (Score: ${health_score}/100) - MySQL monitoring needed: Configuration issues or performance concerns" elif [ $current_low_issues -gt 0 ]; then low_modules_found+=("MySQL") - mysql_details="Low issues found: Minor connection problems, Service status: $(systemctl is-active mariadb 2>/dev/null || echo 'inactive')" + mysql_details="Health Level: $mysql_health_level (Score: ${health_score}/100) - MySQL minor issues: Non-critical warnings" else - mysql_details="No issues found, Service status: $(systemctl is-active mariadb 2>/dev/null || echo 'inactive')" + mysql_details="Health Level: $mysql_health_level (Score: ${health_score}/100) - MySQL functioning optimally: Service running without issues" fi detailed_report["mysql"]="$mysql_details" @@ -2058,13 +2213,7 @@ check_clamav_status() { local current_medium_issues=0 local current_low_issues=0 - # Get start of current day timestamp - local today_start=$(date -d "$(date +%Y-%m-%d) 00:00:00" +%s) - - # Get today's date in the format used in logs (YYYY-MM-DD) - local today=$(date "+%Y-%m-%d") - - echo -e "Processing ClamAV logs... This may take a few moments." + echo -e "Checking ClamAV status..." # Service Status local clamav_running=false @@ -2082,187 +2231,7 @@ check_clamav_status() { ((current_high_issues++)) fi - # Scan Results - local last_scan="" - local scans=0 - local infections=0 - local resolved_infections=0 - local unresolved_infections=0 - local infection_details=() - - if [ -f "/var/log/clamav/clamav.log" ]; then - # Get all log entries and process them - local log_content=$(run_with_timeout 5 "cat '/var/log/clamav/clamav.log' 2>/dev/null | tr -d '\0'") - - if [ -n "$log_content" ]; then - # Count total lines for progress bar - local total_lines=$(echo "$log_content" | wc -l) - echo -e "Found $total_lines lines to analyze in ClamAV log." - local current_line=0 - - # Process each line - while IFS= read -r line; do - ((current_line++)) - if [ $((current_line % 100)) -eq 0 ] || [ $current_line -eq $total_lines ]; then - show_progress $current_line $total_lines - fi - - # Extract timestamp from line - local log_date=$(echo "$line" | grep -o '^[A-Za-z]\{3\} [A-Za-z]\{3\} [0-9]\{1,2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\} [0-9]\{4\}' 2>/dev/null) - if [ -n "$log_date" ]; then - # Convert log date to timestamp - local log_ts=$(date -d "$log_date" +%s 2>/dev/null) - - # Only process if timestamp is valid and from today - if [ -n "$log_ts" ] && [ "$log_ts" -ge "$today_start" ]; then - if [[ "$line" =~ scan ]]; then - ((scans++)) - last_scan="$log_date" - fi - - # Check for actual malware infections, excluding normal system messages - if [[ "$line" =~ (found|suspicious|infected|malware|virus) ]] && - ! [[ "$line" =~ (Database.*(modification|status)|SelfCheck|Reading.databases|Database.correctly.reloaded|Activating.the.newly.loaded.database) ]]; then - ((infections++)) - infection_details+=("$line") - - # Check if infection was resolved (removed, moved, quarantined) - if [[ "$line" =~ removed|moved|quarantined|cleaned|deleted ]]; then - ((resolved_infections++)) - else - ((unresolved_infections++)) - fi - fi - fi - fi - done <<< "$log_content" - fi - else - ((current_medium_issues++)) - fi - - # Database Updates and Status Verification - local updates=0 - local database_reloads=0 - local database_errors=0 - local last_database_status="" - local database_age_days=0 - local quarantine_files=0 - local scan_performance_issues=0 - - # Check freshclam.log for updates - if [ -f "/var/log/clamav/freshclam.log" ]; then - local log_content=$(run_with_timeout 5 "cat '/var/log/clamav/freshclam.log' 2>/dev/null | tr -d '\0'") - - if [ -n "$log_content" ]; then - # Process each line - while IFS= read -r line; do - # Extract timestamp from line - local log_date=$(echo "$line" | grep -o '^[A-Za-z]\{3\} [A-Za-z]\{3\} [0-9]\{1,2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\} [0-9]\{4\}' 2>/dev/null) - if [ -n "$log_date" ]; then - # Convert log date to timestamp - local log_ts=$(date -d "$log_date" +%s 2>/dev/null) - - # Only process if timestamp is valid and from today - if [ -n "$log_ts" ] && [ "$log_ts" -ge "$today_start" ]; then - if [[ "$line" =~ database.*updated|database.*available[[:space:]]+for[[:space:]]+update ]]; then - ((updates++)) - fi - fi - fi - done <<< "$log_content" - fi - else - ((current_medium_issues++)) - fi - - # Check clamav.log for database status and reload operations - if [ -f "/var/log/clamav/clamav.log" ]; then - local clamav_log_content=$(run_with_timeout 5 "cat '/var/log/clamav/clamav.log' 2>/dev/null | tr -d '\0'") - - if [ -n "$clamav_log_content" ]; then - # Process each line for database status - while IFS= read -r line; do - # Extract timestamp from line - local log_date=$(echo "$line" | grep -o '^[A-Za-z]\{3\} [A-Za-z]\{3\} [0-9]\{1,2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\} [0-9]\{4\}' 2>/dev/null) - if [ -n "$log_date" ]; then - # Convert log date to timestamp - local log_ts=$(date -d "$log_date" +%s 2>/dev/null) - - # Only process if timestamp is valid and from today - if [ -n "$log_ts" ] && [ "$log_ts" -ge "$today_start" ]; then - # Check for database modification and reload - if [[ "$line" =~ Database.modification.detected ]]; then - ((database_reloads++)) - fi - - # Check for successful database reload - if [[ "$line" =~ Database.correctly.reloaded ]]; then - last_database_status="reloaded successfully" - fi - - # Check for database status OK - if [[ "$line" =~ Database.status.OK ]]; then - last_database_status="status OK" - fi - - # Check for database errors - if [[ "$line" =~ (database.*error|database.*failed|database.*corrupt) ]]; then - ((database_errors++)) - last_database_status="error detected" - fi - fi - fi - done <<< "$clamav_log_content" - fi - fi - - # Check database age and quarantine - if [ -f "/var/lib/clamav/daily.cvd" ] || [ -f "/var/lib/clamav/daily.cld" ]; then - local db_file="/var/lib/clamav/daily.cvd" - [ -f "/var/lib/clamav/daily.cld" ] && db_file="/var/lib/clamav/daily.cld" - - local db_timestamp=$(stat -c %Y "$db_file" 2>/dev/null) - if [ -n "$db_timestamp" ] && [[ "$db_timestamp" =~ ^[0-9]+$ ]]; then - local current_timestamp=$(date +%s) - database_age_days=$(( (current_timestamp - db_timestamp) / 86400 )) - else - database_age_days=0 - fi - else - database_age_days=0 - fi - - # Check quarantine directory - if [ -d "/var/lib/clamav/quarantine" ]; then - quarantine_files=$(find /var/lib/clamav/quarantine -type f 2>/dev/null | wc -l) - elif [ -d "/tmp/clamav-quarantine" ]; then - quarantine_files=$(find /tmp/clamav-quarantine -type f 2>/dev/null | wc -l) - fi - - # Check for slow scan performance (if scans took unusually long) - if [ "$scans" -gt 0 ] && [ -f "/var/log/clamav/clamav.log" ]; then - local slow_scans=$(grep -c "scan.*took.*[0-9]\{3,\}\.[0-9].*seconds" /var/log/clamav/clamav.log 2>/dev/null || echo 0) - if [ "$slow_scans" -gt 5 ]; then - scan_performance_issues=1 - fi - fi - - # Evaluate database health - if [ "$database_errors" -gt 0 ]; then - ((current_high_issues++)) # Database corruption is critical - elif [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 7 ]; then - ((current_medium_issues++)) # Database older than 7 days - elif [ "$updates" -eq 0 ] && [ "$database_reloads" -eq 0 ] && [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 1 ]; then - ((current_medium_issues++)) # No updates or activity today and database is old - fi - - # Performance and quarantine issues - if [ "$scan_performance_issues" -eq 1 ]; then - ((current_low_issues++)) # Performance issues are low priority - fi - - # Display organized status + # Display service status if $clamav_running; then output+=("${GREEN}✓ ClamAV running${NC}") else @@ -2275,45 +2244,271 @@ check_clamav_status() { output+=("${RED}⚠️ FreshClam not running${NC}") fi - # Database status display - if [ "$database_errors" -gt 0 ]; then - output+=("${RED}⚠️ Database errors detected: $database_errors${NC}") - elif [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 7 ]; then - output+=("${RED}⚠️ Database is $database_age_days days old (outdated)${NC}") - elif [ "$updates" -gt 0 ]; then - output+=("${GREEN}✓ FreshClam Updates today: $updates${NC}") - elif [ "$database_reloads" -gt 0 ]; then - output+=("${GREEN}✓ Database reloads today: $database_reloads${NC}") - if [ -n "$last_database_status" ]; then - output+=("${GREEN}✓ Last database status: $last_database_status${NC}") + # ClamAV Version and Database Information + local clamav_version="" + local database_date="" + local database_age_days=0 + if $clamav_running; then + clamav_version=$(run_with_timeout 5 "clamd --version 2>/dev/null | awk '{print $2}'") + if [ -n "$clamav_version" ]; then + output+=("${GREEN}✓ ClamAV Version: $clamav_version${NC}") + else + output+=("${YELLOW}⚠️ Unable to retrieve ClamAV version${NC}") + ((current_medium_issues++)) + fi + fi + + # Database status using freshclam or clamd + if $freshclam_running; then + local db_info=$(run_with_timeout 5 "freshclam --version 2>/dev/null | grep 'Database'") + if [ -n "$db_info" ]; then + database_date=$(echo "$db_info" | grep -oP '(?<=Database updated: ).*') + if [ -n "$database_date" ]; then + output+=("${GREEN}✓ Database updated: $database_date${NC}") + local db_timestamp=$(date -d "$database_date" +%s 2>/dev/null) + if [ -n "$db_timestamp" ]; then + local current_timestamp=$(date +%s) + database_age_days=$(( (current_timestamp - db_timestamp) / 86400 )) + if [ "$database_age_days" -gt 7 ]; then + output+=("${RED}⚠️ Database is $database_age_days days old (outdated)${NC}") + ((current_medium_issues++)) + elif [ "$database_age_days" -gt 1 ]; then + output+=("${YELLOW}⚠️ Database is $database_age_days days old${NC}") + fi fi else - if [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 1 ]; then - output+=("${YELLOW}⚠️ No updates today (database is $database_age_days days old)${NC}") - else - output+=("${GREEN}✓ Database is current (updated today)${NC}") + # Try systemctl status output + local status_output=$(run_with_timeout 5 "systemctl status clamav-freshclam 2>/dev/null | grep 'database is up-to-date' | tail -n 1") + if [ -n "$status_output" ]; then + database_date=$(echo "$status_output" | sed -n 's/.*\([A-Z][a-z][a-z] [A-Z][a-z][a-z] [0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9] [0-9][0-9][0-9][0-9]\).*/\1/p') + if [ -n "$database_date" ]; then + output+=("${GREEN}✓ Database last checked: $database_date${NC}") + local db_timestamp=$(date -d "$database_date" +%s 2>/dev/null) + if [ -n "$db_timestamp" ]; then + local current_timestamp=$(date +%s) + database_age_days=$(( (current_timestamp - db_timestamp) / 86400 )) + if [ "$database_age_days" -gt 7 ]; then + output+=("${RED}⚠️ Database check is $database_age_days days old (outdated)${NC}") + ((current_medium_issues++)) + elif [ "$database_age_days" -gt 1 ]; then + output+=("${YELLOW}⚠️ Database check is $database_age_days days old${NC}") + fi + fi + else + output+=("${YELLOW}⚠️ Unable to parse database update date from status${NC}") + ((current_medium_issues++)) + fi + else + output+=("${YELLOW}⚠️ Unable to retrieve database update information from status${NC}") + ((current_medium_issues++)) + fi + fi + else + output+=("${YELLOW}⚠️ Unable to retrieve database information from freshclam${NC}") + ((current_medium_issues++)) fi fi - # Additional status information - if [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 0 ] && [ "$database_age_days" -le 7 ]; then - output+=("${GREEN}✓ Database age: $database_age_days days${NC}") + # If database date is still not retrieved, try checking file modification date + if [ -z "$database_date" ]; then + local db_file="" + if [ -f "/var/lib/clamav/daily.cvd" ]; then + db_file="/var/lib/clamav/daily.cvd" + elif [ -f "/var/lib/clamav/daily.cld" ]; then + db_file="/var/lib/clamav/daily.cld" + fi + + if [ -n "$db_file" ]; then + local db_timestamp=$(stat -c %Y "$db_file" 2>/dev/null) + if [ -n "$db_timestamp" ] && [[ "$db_timestamp" =~ ^[0-9]+$ ]]; then + database_date=$(date -d "@$db_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) + if [ -n "$database_date" ]; then + output+=("${GREEN}✓ Database file last modified: $database_date${NC}") + local current_timestamp=$(date +%s) + database_age_days=$(( (current_timestamp - db_timestamp) / 86400 )) + if [ "$database_age_days" -gt 7 ]; then + output+=("${RED}⚠️ Database is $database_age_days days old (outdated)${NC}") + ((current_medium_issues++)) + elif [ "$database_age_days" -gt 1 ]; then + output+=("${YELLOW}⚠️ Database is $database_age_days days old${NC}") + fi + else + output+=("${YELLOW}⚠️ Unable to format database file modification date${NC}") + ((current_medium_issues++)) + fi + else + output+=("${YELLOW}⚠️ Unable to retrieve database file modification date${NC}") + ((current_medium_issues++)) + fi + else + output+=("${YELLOW}⚠️ No database files found${NC}") + ((current_medium_issues++)) + fi fi - if [ "$quarantine_files" -gt 0 ]; then + # Scan Results - Check for recent scans or infections using clamscan or log summary if available + local scans=0 + local infections=0 + local resolved_infections=0 + local unresolved_infections=0 + local last_scan="" + local infection_details=() + + # Check for quarantine files + local quarantine_files=0 + if [ -d "/var/lib/clamav/quarantine" ]; then + quarantine_files=$(run_with_timeout 5 "find /var/lib/clamav/quarantine -type f 2>/dev/null | wc -l") + elif [ -d "/tmp/clamav-quarantine" ]; then + quarantine_files=$(run_with_timeout 5 "find /tmp/clamav-quarantine -type f 2>/dev/null | wc -l") + fi + + # Clean and validate quarantine_files variable + quarantine_files=$(echo "$quarantine_files" | tr -d '\n\r' | grep -o '^[0-9]*' | head -1) + if [ -z "$quarantine_files" ]; then + quarantine_files=0 + fi + + if [ -n "$quarantine_files" ] && [[ "$quarantine_files" =~ ^[0-9]+$ ]] && [ "$quarantine_files" -gt 0 ]; then output+=("${YELLOW}ℹ️ Files in quarantine: $quarantine_files${NC}") + # Assume unresolved infections based on quarantine count if no other data + unresolved_infections=$quarantine_files fi - if [ "$scan_performance_issues" -eq 1 ]; then - output+=("${YELLOW}⚠️ Performance: Some scans took longer than expected${NC}") + # Try to get scan information if log is accessible quickly + if [ -f "/var/log/clamav/clamav.log" ]; then + local last_scan_line=$(run_with_timeout 5 "grep 'scan' /var/log/clamav/clamav.log 2>/dev/null | tail -n 1") + if [ -n "$last_scan_line" ]; then + last_scan=$(echo "$last_scan_line" | grep -o '^[A-Za-z]\{3\} [A-Za-z]\{3\} [0-9]\{1,2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\} [0-9]\{4\}' 2>/dev/null) + if [ -n "$last_scan" ]; then + output+=("${GREEN}✓ Last scan detected: $last_scan${NC}") + scans=1 # At least one scan detected + fi + fi fi - # Determine issue severity based on infection status + # Performance issues - minimal check + local scan_performance_issues=0 + # We skip detailed log parsing for performance issues to keep it fast + + # Updates and database reloads - minimal check + local updates=0 + local database_reloads=0 + local database_errors=0 + local last_database_status="" + + if [ -f "/var/log/clamav/freshclam.log" ]; then + local last_update=$(run_with_timeout 5 "grep 'database.*updated' /var/log/clamav/freshclam.log 2>/dev/null | tail -n 1") + if [ -n "$last_update" ]; then + updates=1 + output+=("${GREEN}✓ FreshClam update detected in logs${NC}") + fi + fi + + # === INTELLIGENT CLAMAV THREAT ASSESSMENT SYSTEM === + # Calculate weighted threat score (0-100 points) + local threat_score=0 + local factor_explanations=() + + # Factor 1: Unresolved infections (35% weight) - Most critical factor + local infection_factor=0 if [ "$unresolved_infections" -gt 0 ]; then - ((current_high_issues++)) # Unresolved infections are CRITICAL + infection_factor=35 # Critical - unresolved threats + factor_explanations+=("Unresolved infections: +35 points (CRITICAL)") elif [ "$resolved_infections" -gt 0 ]; then - ((current_low_issues++)) # Resolved infections are LOW (system working) + infection_factor=$((resolved_infections * 5)) # 5 points per resolved infection + if [ "$infection_factor" -gt 15 ]; then infection_factor=15; fi # Cap at 15 + factor_explanations+=("Resolved infections: +${infection_factor} points") fi + threat_score=$((threat_score + infection_factor)) + + # Factor 2: Service status (25% weight) - Critical for protection + local service_factor=0 + if [ "$clamav_running" = false ] || [ "$freshclam_running" = false ]; then + service_factor=25 # Critical - services down + factor_explanations+=("Service issues: +25 points (services down)") + fi + threat_score=$((threat_score + service_factor)) + + # Factor 3: Database health (20% weight) - Essential for detection + local db_factor=0 + if [ "$database_errors" -gt 0 ]; then + db_factor=20 # Critical - database corrupted + factor_explanations+=("Database errors: +20 points (database corrupted)") + elif [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 7 ]; then + db_factor=15 # High - outdated signatures + factor_explanations+=("Outdated database: +15 points (${database_age_days} days old)") + elif [ "$updates" -eq 0 ] && [ "$database_reloads" -eq 0 ] && [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 1 ]; then + db_factor=10 # Medium - no recent activity + factor_explanations+=("No database activity: +10 points (${database_age_days} days old)") + fi + threat_score=$((threat_score + db_factor)) + + # Factor 4: Scan activity (10% weight) - Monitoring effectiveness + local scan_factor=0 + if [ "$scans" -eq 0 ]; then + scan_factor=10 # No scans performed today + factor_explanations+=("No scans today: +10 points") + elif [ "$scan_performance_issues" -eq 1 ]; then + scan_factor=5 # Performance issues + factor_explanations+=("Scan performance issues: +5 points") + fi + threat_score=$((threat_score + scan_factor)) + + # Factor 5: Quarantine load (10% weight) - System load indicator + local quarantine_factor=0 + if [ -n "$quarantine_files" ] && [[ "$quarantine_files" =~ ^[0-9]+$ ]] && [ "$quarantine_files" -gt 100 ]; then + quarantine_factor=10 # High quarantine load + factor_explanations+=("High quarantine load: +10 points (${quarantine_files} files)") + elif [ -n "$quarantine_files" ] && [[ "$quarantine_files" =~ ^[0-9]+$ ]] && [ "$quarantine_files" -gt 20 ]; then + quarantine_factor=5 # Medium quarantine load + factor_explanations+=("Medium quarantine load: +5 points (${quarantine_files} files)") + fi + threat_score=$((threat_score + quarantine_factor)) + + # Convert threat score to health score (inverse: 100 - threat_score) + local health_score=$((100 - threat_score)) + + # Determine health level based on health score (now intuitive) + local clamav_threat_level="" + local health_color="" + if [ "$health_score" -ge 90 ]; then + clamav_threat_level="EXCELLENT" + health_color="${GREEN}" + elif [ "$health_score" -ge 75 ]; then + clamav_threat_level="GOOD" + health_color="${GREEN}" + elif [ "$health_score" -ge 50 ]; then + clamav_threat_level="FAIR" + health_color="${YELLOW}" + current_medium_issues=1 + elif [ "$health_score" -ge 25 ]; then + clamav_threat_level="POOR" + health_color="${YELLOW}" + current_high_issues=1 + else + clamav_threat_level="CRITICAL" + health_color="${RED}" + current_high_issues=1 + fi + + # Update issue counters based on intelligent assessment + current_high_issues=0 + current_medium_issues=0 + current_low_issues=0 + + if [ "$clamav_threat_level" = "CRITICAL" ] || [ "$clamav_threat_level" = "POOR" ]; then + current_high_issues=1 + elif [ "$clamav_threat_level" = "FAIR" ]; then + current_medium_issues=1 + elif [ "$clamav_threat_level" = "GOOD" ]; then + current_low_issues=1 + fi + + # Display health assessment + output+=("") + output+=("${BLUE}=== Intelligent Health Assessment ===${NC}") + output+=("${health_color}Health Level: $clamav_threat_level (Score: ${health_score}/100)${NC}") + if [ "$infections" -gt 0 ] 2>/dev/null; then if [ "$unresolved_infections" -gt 0 ]; then @@ -2368,31 +2563,40 @@ check_clamav_status() { # Track which modules have issues and capture detailed info for AI local clamav_details="" if [ $current_high_issues -gt 0 ]; then + if [ "$clamav_threat_level" = "CRITICAL" ]; then critical_modules_found+=("ClamAV") - if [ "$unresolved_infections" -gt 0 ]; then - clamav_details="Critical: $unresolved_infections unresolved malware infections detected today - Immediate action required" - elif [ "$database_errors" -gt 0 ]; then - clamav_details="Critical: $database_errors database errors detected - Antivirus database corrupted or failed to load" else - clamav_details="Critical: ClamAV or FreshClam services not running - Antivirus protection disabled" + high_modules_found+=("ClamAV") + fi + + # Generate intelligent threat details + if [ "$unresolved_infections" -gt 0 ]; then + clamav_details="$clamav_threat_level threat: $unresolved_infections unresolved malware infections detected (Score: ${health_score}/100) - Immediate action required" + elif [ "$clamav_running" = false ] || [ "$freshclam_running" = false ]; then + clamav_details="$clamav_threat_level threat: ClamAV services not running (Score: ${health_score}/100) - Antivirus protection disabled" + elif [ "$database_errors" -gt 0 ]; then + clamav_details="$clamav_threat_level threat: $database_errors database errors detected (Score: ${health_score}/100) - Antivirus database corrupted" + else + clamav_details="$clamav_threat_level threat: Multiple security issues detected (Score: ${health_score}/100) - System requires attention" fi elif [ $current_medium_issues -gt 0 ]; then medium_modules_found+=("ClamAV") if [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 7 ]; then - clamav_details="Medium issues: Database is $database_age_days days old - Antivirus signatures outdated" + clamav_details="$clamav_threat_level threat: Database is $database_age_days days old (Score: ${health_score}/100) - Antivirus signatures outdated" else - clamav_details="Medium issues: No database updates or activity today - Database may be outdated ($database_age_days days old)" + clamav_details="$clamav_threat_level threat: Database maintenance issues (Score: ${health_score}/100) - No recent updates or activity" fi elif [ $current_low_issues -gt 0 ]; then low_modules_found+=("ClamAV") if [ "$resolved_infections" -gt 0 ]; then - clamav_details="Low: $resolved_infections malware infections detected and successfully resolved today - System working properly" + clamav_details="$clamav_threat_level risk: $resolved_infections malware infections detected and resolved (Score: ${health_score}/100) - System working properly" elif [ "$scan_performance_issues" -eq 1 ]; then - clamav_details="Low: Performance issues detected - Some scans taking longer than expected" + clamav_details="$clamav_threat_level risk: Performance issues detected (Score: ${health_score}/100) - Some scans taking longer than expected" else - clamav_details="Low issues: Minor ClamAV configuration or log issues" + clamav_details="$clamav_threat_level risk: Minor operational issues (Score: ${health_score}/100) - System functioning with minor concerns" fi else + # No issues - system is healthy local db_activity="" if [ "$updates" -gt 0 ]; then db_activity="$updates database updates" @@ -2406,11 +2610,11 @@ check_clamav_status() { if [ -n "$database_age_days" ] && [[ "$database_age_days" =~ ^[0-9]+$ ]] && [ "$database_age_days" -gt 0 ]; then additional_info=", database age: $database_age_days days" fi - if [ "$quarantine_files" -gt 0 ]; then + if [ -n "$quarantine_files" ] && [[ "$quarantine_files" =~ ^[0-9]+$ ]] && [ "$quarantine_files" -gt 0 ]; then additional_info="$additional_info, quarantine: $quarantine_files files" fi - clamav_details="ClamAV functioning normally: $scans scans performed today, $db_activity$additional_info, no infections detected" + clamav_details="Health Level: $clamav_threat_level (Score: ${health_score}/100) - ClamAV functioning optimally: $scans scans performed today, $db_activity$additional_info, no infections detected" fi detailed_report["clamav"]="$clamav_details" @@ -2598,6 +2802,92 @@ check_failed_logins() { fi } +# Function to process IP statistics from Fail2Ban log +process_ip_statistics() { + local log_content="$1" + local today_start="$2" + + # Clear and declare IP arrays as global + unset ip_attempts ip_bans ip_unbans + declare -g -A ip_attempts + declare -g -A ip_bans + declare -g -A ip_unbans + + echo -e "Processing IP statistics..." + + # Count total lines for progress bar + local total_lines=$(echo "$log_content" | wc -l) + local current_line=0 + + # Process each line to extract IP statistics + while IFS= read -r line; do + ((current_line++)) + if [ $((current_line % 100)) -eq 0 ] || [ $current_line -eq $total_lines ]; then + show_progress $current_line $total_lines + fi + + if [ -z "$line" ]; then + continue + fi + + # Extract timestamp from line + local log_date=$(echo "$line" | grep -o '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}') + if [ -n "$log_date" ]; then + # Convert log date to timestamp + local log_ts=$(date -d "$log_date" +%s 2>/dev/null) + + # Only process if timestamp is valid and from today + if [ -n "$log_ts" ] && [ "$log_ts" -ge "$today_start" ]; then + # Extract IP address if present + local ip="" + if [[ "$line" =~ ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then + ip="${BASH_REMATCH[1]}" + fi + + if [ -n "$ip" ]; then + # Count attempts (Found) + if [[ "$line" =~ Found ]]; then + ((ip_attempts[$ip]++)) + fi + + # Count bans (Ban) - look for specific Ban pattern + if [[ "$line" =~ \].*Ban.*$ip ]] || [[ "$line" =~ Ban[[:space:]]+$ip ]]; then + ((ip_bans[$ip]++)) + fi + + # Count unbans (Unban) - look for specific Unban pattern + if [[ "$line" =~ \].*Unban.*$ip ]] || [[ "$line" =~ Unban[[:space:]]+$ip ]]; then + ((ip_unbans[$ip]++)) + fi + fi + fi + fi + done <<< "$log_content" + echo -e "\n" # Add newline after progress bar + + # Debug: Print contents of ip_bans and ip_unbans + echo "Debug: ip_bans contents: ${!ip_bans[@]}" + for ip in "${!ip_bans[@]}"; do + echo "IP: $ip, Bans: ${ip_bans[$ip]}" + done + echo "Debug: ip_unbans contents: ${!ip_unbans[@]}" + for ip in "${!ip_unbans[@]}"; do + echo "IP: $ip, Unbans: ${ip_unbans[$ip]}" + done +} + +# Function to calculate bans and unbans per IP +calculate_ip_ban_stats() { + local ip="$1" + local log_content="$2" + + # Count bans for this IP + local bans=$(echo "$log_content" | grep -c "Ban $ip") + local unbans=$(echo "$log_content" | grep -c "Unban $ip") + + echo "$bans $unbans" +} + # Function to check Fail2Ban status with timeout check_fail2ban_status() { echo -e "${BLUE}=== Fail2Ban Status (Today) ===${NC}" @@ -2632,195 +2922,328 @@ check_fail2ban_status() { declare -A service_bans declare -A service_unbans + # Initialize IP counters for bans and unbans + declare -A ip_attempts + declare -A ip_bans + declare -A ip_unbans + # Check for failed login attempts and add to MyVestacp Panel counters - check_failed_logins + # check_failed_logins # Disabled - duplicates vesta-iptables data # Check log file for today's activity echo -e "\n${YELLOW}Today's Activity:${NC}" if [ -f "$fail2ban_log" ]; then - echo -e "Processing log for today ($today)..." + echo -e "Analyzing today's activity ($today)..." + echo -e "Current time: $(date)" - # Optimization: Use grep with -m to limit results and process in chunks - local log_content - log_content=$(run_with_timeout 30 "grep -a '^$today.*Found\\|^$today.*Ban\\|^$today.*Unban' '$fail2ban_log' | tr -d '\0'") - local grep_exit_code=$? - - if [ $grep_exit_code -eq 0 ]; then - local total_lines=$(echo "$log_content" | wc -l) - - if [ "$total_lines" -gt 0 ]; then - echo -e "Found $total_lines relevant lines to process..." - local current_line=0 + # Get all jails + local jails=$(fail2ban-client status | grep "Jail list:" | cut -d: -f2 | tr ',' ' ') + + # Initialize counters for TODAY ONLY + local total_attempts_today=0 + local total_bans_today=0 + local total_unbans_today=0 + local total_currently_banned=0 + + # Get today's entries from log efficiently (much faster than processing entire log) + local today_log_content=$(grep "^$today" "$fail2ban_log" 2>/dev/null) + + # Display results by service using TODAY'S data from logs + echo -e "\n${YELLOW}Activity by Service (Today Only):${NC}" + + # Track SSH attempts to avoid double counting + local ssh_attempts_counted=false + local ssh_total_attempts=0 + + for jail in $jails; do + if [ -n "$jail" ]; then + local jail_name=$(echo "$jail" | xargs) # Remove whitespace - # Process filtered lines in a single pass - while IFS= read -r line; do - ((current_line++)) - if [ $((current_line % 100)) -eq 0 ] || [ $current_line -eq $total_lines ]; then - show_progress $current_line $total_lines - fi - - # Extract timestamp from line - local log_date=$(echo "$line" | grep -o '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}') - if [ -n "$log_date" ]; then - # Convert log date to timestamp - local log_ts=$(date -d "$log_date" +%s 2>/dev/null) + # Handle SSH duplicate counting (ssh-iptables vs sshd) + local skip_jail=false + if [[ "$jail_name" == *"sshd"* ]] && [ "$ssh_attempts_counted" = true ]; then + skip_jail=true + continue # Skip this jail completely + elif [[ "$jail_name" == *"ssh"* ]] && [[ "$jail_name" != *"sshd"* ]]; then + ssh_attempts_counted=true + fi + + # Skip if this is a duplicate jail + if [ "$skip_jail" = true ]; then + continue + fi + + # Count TODAY's activity for this jail from logs + local today_failed=$(echo "$today_log_content" | grep -c "\[$jail_name\].*Found" 2>/dev/null | tr -d '\n\r' | grep -oE '^[0-9]+$' || echo "0") + [ -z "$today_failed" ] && today_failed="0" + + # Store SSH attempts for comparison + if [[ "$jail_name" == *"ssh"* ]]; then + ssh_total_attempts=$today_failed + fi + + local today_banned=$(echo "$today_log_content" | grep -c "\[$jail_name\].*Ban " 2>/dev/null | tr -d '\n\r' | grep -oE '^[0-9]+$' || echo "0") + local today_unbanned=$(echo "$today_log_content" | grep -c "\[$jail_name\].*Unban " 2>/dev/null | tr -d '\n\r' | grep -oE '^[0-9]+$' || echo "0") + + # Ensure all numbers are valid integers + [ -z "$today_banned" ] || [ "$today_banned" = "" ] && today_banned="0" + [ -z "$today_unbanned" ] || [ "$today_unbanned" = "" ] && today_unbanned="0" + + # Debug: Show log entries count for this jail + local jail_entries=$(echo "$today_log_content" | grep "\[$jail_name\]" | wc -l) + + # Get currently banned count from fail2ban-client (real-time) + local jail_status=$(fail2ban-client status "$jail_name" 2>/dev/null) + local currently_banned=0 + if [ $? -eq 0 ]; then + currently_banned=$(echo "$jail_status" | grep "Currently banned:" | grep -oE '[0-9]+' | head -1) + currently_banned=${currently_banned:-0} + fi + + # Display service info for TODAY + echo -e " - $jail_name: ($jail_entries log entries today)" + echo -e " * Failed attempts today: $today_failed" + echo -e " * Bans today: $today_banned" + echo -e " * Unbans today: $today_unbanned" + echo -e " * Currently banned: $currently_banned" + + # Add to totals + total_attempts_today=$((total_attempts_today + today_failed)) + total_bans_today=$((total_bans_today + today_banned)) + total_unbans_today=$((total_unbans_today + today_unbanned)) + total_currently_banned=$((total_currently_banned + currently_banned)) + fi + done + + # Add MyVestacp Panel counters if available (these are already for today) + if [ ${myvesta_attempts:-0} -gt 0 ]; then + echo -e " - MyVestacpPanel:" + echo -e " * Failed attempts today: $myvesta_failed" + echo -e " * Bans today: $myvesta_bans" + + total_attempts_today=$((total_attempts_today + myvesta_failed)) + total_bans_today=$((total_bans_today + myvesta_bans)) + fi + + echo -e "${GREEN}✓ Today's data collected successfully.${NC}" + + # Summary statistics - TODAY ONLY + echo -e "\n${YELLOW}Today's Summary ($today):${NC}" + echo -e " * Failed attempts today: $total_attempts_today" + echo -e " * Bans today: $total_bans_today" + echo -e " * Unbans today: $total_unbans_today" + echo -e " * Currently banned IPs: $total_currently_banned" + + # Advanced Security Pattern Analysis + echo -e "\n${YELLOW}Security Analysis:${NC}" + + # Get unique IPs that attempted access today + local unique_attacking_ips=$(echo "$today_log_content" | grep "Found " | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | sort -u | wc -l) + local unique_banned_ips=$(echo "$today_log_content" | grep "Ban " | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | sort -u | wc -l) + + # Calculate patterns + local attempts_per_ip=0 + if [ $unique_attacking_ips -gt 0 ]; then + attempts_per_ip=$((total_attempts_today / unique_attacking_ips)) + fi + + local bans_per_ip=0 + if [ $unique_banned_ips -gt 0 ]; then + bans_per_ip=$((total_bans_today / unique_banned_ips)) + fi + + # Intelligent threat categorization with multiple variables + current_high_issues=0 + current_medium_issues=0 + current_low_issues=0 + + # Calculate threat score based on multiple factors + local threat_score=0 + local threat_level="" + local threat_color="" + + # Factor 1: Volume of attacks (weight: 30%) + if [ $total_bans_today -gt 1000 ]; then + threat_score=$((threat_score + 30)) + elif [ $total_bans_today -gt 500 ]; then + threat_score=$((threat_score + 25)) + elif [ $total_bans_today -gt 200 ]; then + threat_score=$((threat_score + 20)) + elif [ $total_bans_today -gt 100 ]; then + threat_score=$((threat_score + 15)) + elif [ $total_bans_today -gt 50 ]; then + threat_score=$((threat_score + 10)) + elif [ $total_bans_today -gt 10 ]; then + threat_score=$((threat_score + 5)) + fi + + # Factor 2: Diversity of attackers (weight: 25%) + if [ $unique_banned_ips -gt 100 ]; then + threat_score=$((threat_score + 25)) + elif [ $unique_banned_ips -gt 50 ]; then + threat_score=$((threat_score + 20)) + elif [ $unique_banned_ips -gt 30 ]; then + threat_score=$((threat_score + 15)) + elif [ $unique_banned_ips -gt 20 ]; then + threat_score=$((threat_score + 10)) + elif [ $unique_banned_ips -gt 10 ]; then + threat_score=$((threat_score + 5)) + fi + + # Factor 3: Intensity per attacker (weight: 20%) + if [ $attempts_per_ip -gt 2000 ]; then + threat_score=$((threat_score + 20)) + elif [ $attempts_per_ip -gt 1000 ]; then + threat_score=$((threat_score + 15)) + elif [ $attempts_per_ip -gt 500 ]; then + threat_score=$((threat_score + 10)) + elif [ $attempts_per_ip -gt 200 ]; then + threat_score=$((threat_score + 5)) + fi + + # Factor 4: Current system load (weight: 15%) + if [ $total_currently_banned -gt 200 ]; then + threat_score=$((threat_score + 15)) + elif [ $total_currently_banned -gt 100 ]; then + threat_score=$((threat_score + 12)) + elif [ $total_currently_banned -gt 50 ]; then + threat_score=$((threat_score + 8)) + elif [ $total_currently_banned -gt 25 ]; then + threat_score=$((threat_score + 4)) + fi + + # Factor 5: Total attack attempts (weight: 10%) + if [ $total_attempts_today -gt 50000 ]; then + threat_score=$((threat_score + 10)) + elif [ $total_attempts_today -gt 20000 ]; then + threat_score=$((threat_score + 8)) + elif [ $total_attempts_today -gt 10000 ]; then + threat_score=$((threat_score + 6)) + elif [ $total_attempts_today -gt 5000 ]; then + threat_score=$((threat_score + 4)) + elif [ $total_attempts_today -gt 1000 ]; then + threat_score=$((threat_score + 2)) + fi + + # Determine threat level based on composite score + if [ $threat_score -ge 75 ]; then + threat_level="CRITICAL" + threat_color="${RED}" + current_high_issues=1 + elif [ $threat_score -ge 50 ]; then + threat_level="HIGH" + threat_color="${RED}" + current_high_issues=1 + elif [ $threat_score -ge 25 ]; then + threat_level="MEDIUM" + threat_color="${YELLOW}" + current_medium_issues=1 + elif [ $threat_score -ge 10 ]; then + threat_level="LOW" + threat_color="${GREEN}" + current_low_issues=1 + else + threat_level="MINIMAL" + threat_color="${GREEN}" + fi + + # Display security metrics + echo -e " * Unique attacking IPs: $unique_attacking_ips" + echo -e " * Unique banned IPs: $unique_banned_ips" + echo -e " * Average attempts per IP: $attempts_per_ip" + echo -e " * Average bans per IP: $bans_per_ip" + + # Convert threat score to health score (inverse: 100 - threat_score) + local health_score=$((100 - threat_score)) + + # Display threat level in security analysis + echo -e " * ${threat_color}Health Level: $threat_level${NC} (Score: ${health_score}/100)" + + # Display Top 10 IPs - Focus on IPs BANNED today + echo -e "\n${YELLOW}Top 10 IPs Banned Today:${NC}" + + # Get current bantime from fail2ban configuration + local bantime=$(fail2ban-client get ssh-iptables bantime 2>/dev/null || echo "600") + + # Get IPs that were BANNED today (not just active) + local today_banned_ips=$(echo "$today_log_content" | grep "Ban " | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | sort | uniq -c | sort -nr | head -10) + + if [ -n "$today_banned_ips" ]; then + # Print table header + printf " %-15s | %-8s | %-4s | %-16s | %-6s | %-16s | %-15s | %-20s\n" "IP" "ATTEMPTS" "BANS" "BAN DATE" "UNBANS" "UNBAN DATE" "COUNTRY" "SERVICES" + printf " %-15s-+-%-8s-+-%-4s-+-%-16s-+-%-6s-+-%-16s-+-%-15s-+-%-20s\n" "---------------" "--------" "----" "----------------" "------" "----------------" "---------------" "--------------------" + + # Process IPs that were banned today + echo "$today_banned_ips" | while IFS= read -r line; do + if [ -n "$line" ]; then + local bans_count=$(echo "$line" | awk '{print $1}') + local ip=$(echo "$line" | awk '{print $2}') - # Only process if timestamp is valid and from today - if [ -n "$log_ts" ] && [ "$log_ts" -ge "$today_start" ]; then - # Check if the line contains relevant activity (Found, Ban, Unban) and the expected structure - local log_jail_name="" - # Pattern: Date Time,ms fail2ban.filter/actions [PID]: INFO/NOTICE [jailname] ... - if [[ "$line" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]]+[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]+[[:space:]]+fail2ban.(filter|actions)[[:space:]]+\[[0-9]+\]:[[:space:]]+(INFO|NOTICE)[[:space:]]+\[([^]]+)\] ]]; then - log_jail_name="${BASH_REMATCH[3]}" # Capture group 3 contains the jail name - fi - - # If a jail name was extracted, proceed with counting - if [ -n "$log_jail_name" ]; then - # Remove leading/trailing whitespace from the extracted jail name - log_jail_name=$(echo "$log_jail_name" | xargs) - - # Check if the extracted and cleaned jail name is one of the VestaCP jails (including -iptables) - local is_vesta_jail=false - case "$log_jail_name" in - "dovecot" | "dovecot-iptables" | \ - "exim" | "exim-iptables" | \ - "ssh" | "ssh-iptables" | \ - "sshd") - is_vesta_jail=true - ;; - *) - # Not a known VestaCP jail, skip this line for counting - ;; + if [ -n "$ip" ] && [ "$bans_count" -gt 0 ]; then + # Count attempts (Found) for this IP today + local attempts_today=$(echo "$today_log_content" | grep -c "Found $ip" 2>/dev/null | tr -d '\n\r' | grep -oE '^[0-9]+$' || echo "0") + [ -z "$attempts_today" ] && attempts_today="0" + + # Count unbans for this IP today + local unbans_today=$(echo "$today_log_content" | grep -c "Unban $ip" 2>/dev/null | tr -d '\n\r' | grep -oE '^[0-9]+$' || echo "0") + [ -z "$unbans_today" ] && unbans_today="0" + + # Get which services/jails banned this IP today + local services="" + local jail_names=$(echo "$today_log_content" | grep "Ban $ip" | grep -oE '\[[^]]+\]' | sed 's/\[//g; s/\]//g; s/-iptables//g' | sort -u) + for jail in $jail_names; do + # Skip numeric-only entries like "929" + if [ -n "$jail" ] && ! echo "$jail" | grep -qE '^[0-9]+$'; then + # Map service names for clarity + case "$jail" in + "exim") jail="exim4" ;; + "ssh") jail="ssh" ;; + "sshd") jail="ssh" ;; + "dovecot") jail="dovecot" ;; + "vesta") jail="vesta" ;; esac - - if [ "$is_vesta_jail" = true ]; then - # Extract IP address if present - local ip="" - if [[ "$line" =~ ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then - ip="${BASH_REMATCH[1]}" - fi - - # Use the cleaned and validated jail name as the key for counting - local current_jail_key="$log_jail_name" - - # Count attempts only if IP is found and line contains "Found" - if [ -n "$ip" ] && [[ "$line" =~ Found ]]; then - ((service_attempts[$current_jail_key]++)) - ((total_attempts++)) - fi - - # Count bans only if IP is found and line contains "Ban" - if [ -n "$ip" ] && [[ "$line" =~ Ban ]]; then - ((service_bans[$current_jail_key]++)) - ((total_bans++)) - fi - - # Count unbans only if IP is found and line contains "Unban" - if [ -n "$ip" ] && [[ "$line" =~ Unban ]]; then - ((service_unbans[$current_jail_key]++)) - ((total_unbans++)) - fi + # Add to services list + if [ -z "$services" ]; then + services="$jail" + else + services="$services,$jail" fi fi + done + [ -z "$services" ] && services="N/A" + [ ${#services} -gt 20 ] && services="${services:0:17}..." + + # Get most recent ban date from today + local ban_date=$(echo "$today_log_content" | grep "Ban $ip" | tail -1 | awk '{print $1 " " substr($2,1,8)}' | tr -d '\n\r') + [ -z "$ban_date" ] && ban_date="N/A" + + # Calculate unban date + local unban_date="N/A" + if [ "$ban_date" != "N/A" ]; then + local ban_timestamp=$(date -d "$ban_date" +%s 2>/dev/null) + if [ $? -eq 0 ]; then + local unban_timestamp=$((ban_timestamp + bantime)) + unban_date=$(date -d "@$unban_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "Error") + fi fi + + # Truncate if too long + [ ${#ban_date} -gt 16 ] && ban_date="${ban_date:0:16}" + [ ${#unban_date} -gt 16 ] && unban_date="${unban_date:0:16}" + + # Get country (simple and fast) + local country=$(curl -s --connect-timeout 2 --max-time 3 "http://ip-api.com/line/$ip?fields=countryCode" 2>/dev/null | head -1 | cut -c1-15 | tr -d '\n\r') + [ -z "$country" ] && country="Unknown" + + printf " %-15s | %-8s | %-4s | %-16s | %-6s | %-16s | %-15s | %-20s\n" "$ip" "$attempts_today" "$bans_count" "$ban_date" "$unbans_today" "$unban_date" "$country" "$services" fi - done <<< "$log_content" - echo -e "\n" # Add newline after progress bar - - # Display results by service - echo -e "${GREEN}✓ Log file processed successfully.${NC}" - echo -e "\n${YELLOW}Activity by Service:${NC}" - - # Hardcoded list of known VestaCP Fail2Ban jails (use the names as they appear in logs/status without PID) - local all_vesta_jails=("dovecot" "exim" "ssh" "MyVestacpPanel") - - # Reset totals before summing - total_attempts=0 - total_bans=0 - total_unbans=0 - - # Iterate over all known jails to display status using the jail name as key - for service_key in "${all_vesta_jails[@]}"; do - # Determine the display name (remove -iptables for cleaner output) - local display_name="${service_key%-iptables}" - # Check both with and without -iptables suffix - local iptables_key="${service_key}-iptables" - - # Initialize counters for this service - local service_attempts_count=0 - local service_bans_count=0 - local service_unbans_count=0 - - # Add counts from both versions of the jail name - if [ -n "${service_attempts[$service_key]}" ]; then - service_attempts_count=$((service_attempts_count + service_attempts[$service_key])) - fi - if [ -n "${service_attempts[$iptables_key]}" ]; then - service_attempts_count=$((service_attempts_count + service_attempts[$iptables_key])) - fi - - if [ -n "${service_bans[$service_key]}" ]; then - service_bans_count=$((service_bans_count + service_bans[$service_key])) - fi - if [ -n "${service_bans[$iptables_key]}" ]; then - service_bans_count=$((service_bans_count + service_bans[$iptables_key])) - fi - - if [ -n "${service_unbans[$service_key]}" ]; then - service_unbans_count=$((service_unbans_count + service_unbans[$service_key])) - fi - if [ -n "${service_unbans[$iptables_key]}" ]; then - service_unbans_count=$((service_unbans_count + service_unbans[$iptables_key])) - fi - - # Add to totals - total_attempts=$((total_attempts + service_attempts_count)) - total_bans=$((total_bans + service_bans_count)) - total_unbans=$((total_unbans + service_unbans_count)) - - # Always display the service and its counts (will show 0 if no activity found) - echo -e "\n${BLUE}$display_name:${NC}" - if [ "$display_name" = "MyVestacpPanel" ]; then - echo -e " Attempts: ${YELLOW}$myvesta_attempts${NC}" - echo -e " Failed: ${RED}$myvesta_failed${NC}" - echo -e " Bans: ${RED}$myvesta_bans${NC}" - # Add MyVestacpPanel to totals - total_attempts=$((total_attempts + myvesta_attempts)) - total_bans=$((total_bans + myvesta_bans)) - total_unbans=$((total_unbans + myvesta_bans)) # Unbans match bans for MyVestacpPanel - else - echo -e " Attempts: ${YELLOW}$service_attempts_count${NC}" - echo -e " Bans: ${RED}$service_bans_count${NC}" - echo -e " Unbans: ${GREEN}$service_unbans_count${NC}" - fi - done - - # Show totals - echo -e "\n${YELLOW}Total Activity:${NC}" - echo -e " Total Attempts: ${YELLOW}$total_attempts${NC}" - echo -e " Total Bans: ${RED}$total_bans${NC}" - echo -e " Total Unbans: ${GREEN}$total_unbans${NC}" - - # Classify based on total attempts volume - if [ "$total_attempts" -gt 50000 ]; then - ((current_high_issues++)) # >50k attempts = HIGH (massive coordinated attack) - elif [ "$total_attempts" -gt 20000 ]; then - ((current_medium_issues++)) # 20k-50k attempts = MEDIUM (significant attack) - elif [ "$total_attempts" -gt 10000 ]; then - ((current_low_issues++)) # 10k-20k attempts = LOW (elevated activity) fi - # 0-10k attempts = Normal (typical bot activity) - else - echo -e "${YELLOW}⚠️ No relevant entries found in Fail2Ban log for today.${NC}" - ((current_medium_issues++)) - fi + done else - echo -e "${RED}⚠️ Failed to read Fail2Ban log file. Exit code: $grep_exit_code${NC}" - ((current_medium_issues++)) + echo -e " - No IPs were banned today." fi else - echo -e "${RED}⚠️ Fail2Ban log file not found${NC}" - ((current_medium_issues++)) + echo -e " - No significant IP activity found for today." fi # Add local issues to global counters @@ -2831,27 +3254,120 @@ check_fail2ban_status() { # Track which modules have issues and capture detailed info for AI analysis local fail2ban_details="" if [ $current_high_issues -gt 0 ]; then + # Distinguish between CRITICAL and HIGH threat levels + if [ "$threat_level" = "CRITICAL" ]; then critical_modules_found+=("Fail2Ban") - if [ "$total_attempts" -gt 50000 ]; then - fail2ban_details="Critical: Massive coordinated attack detected - $total_attempts attempts today (>50k threshold). Fail2Ban is working but system under heavy attack." + fail2ban_details="Health Level: $threat_level (Score: ${health_score}/100) - Critical security activity today - $total_bans_today bans, $total_currently_banned currently banned IPs" + elif [ "$threat_level" = "POOR" ]; then + high_modules_found+=("Fail2Ban") + fail2ban_details="Health Level: $threat_level (Score: ${health_score}/100) - Elevated security activity today - $total_bans_today bans, $total_currently_banned currently banned IPs" + elif [ "$threat_level" = "FAIR" ]; then + medium_modules_found+=("Fail2Ban") + fail2ban_details="Health Level: $threat_level (Score: ${health_score}/100) - Moderate security activity today - $total_bans_today bans, $total_attempts_today failed attempts, $total_currently_banned currently banned IPs. Fail2Ban working effectively." + elif [ "$threat_level" = "GOOD" ]; then + low_modules_found+=("Fail2Ban") + fail2ban_details="Health Level: $threat_level (Score: ${health_score}/100) - Normal security activity today - $total_bans_today bans, $total_attempts_today failed attempts, $total_currently_banned currently banned IPs. System secure." else - fail2ban_details="Critical: Fail2Ban service not running - Security monitoring disabled" + fail2ban_details="Health Level: $threat_level (Score: ${health_score}/100) - Fail2Ban functioning normally today: $total_attempts_today failed attempts, $total_bans_today bans, $total_unbans_today unbans, $total_currently_banned currently banned IPs. System secure." fi elif [ $current_medium_issues -gt 0 ]; then medium_modules_found+=("Fail2Ban") - if [ "$total_attempts" -gt 20000 ]; then - fail2ban_details="Medium: Significant attack activity - $total_attempts attempts today (20k-50k threshold). Fail2Ban is blocking but elevated monitoring recommended." - else - fail2ban_details="Medium issues: Problems reading Fail2Ban logs or configuration" - fi + fail2ban_details="Health Level: $threat_level (Score: ${health_score}/100) - Normal security activity today - $total_bans_today bans, $total_attempts_today failed attempts, $total_currently_banned currently banned IPs. System secure." elif [ $current_low_issues -gt 0 ]; then low_modules_found+=("Fail2Ban") - fail2ban_details="Low: Elevated attack activity - $total_attempts attempts today (10k-20k threshold). Fail2Ban is handling it but worth monitoring." + fail2ban_details="Health Level: $threat_level (Score: ${health_score}/100) - Normal security activity today - $total_bans_today bans, $total_attempts_today failed attempts, $total_currently_banned currently banned IPs. System secure." else - fail2ban_details="Fail2Ban functioning normally: $total_attempts total attempts, $total_bans bans, $total_unbans unbans today. MyVestaCP Panel: $myvesta_failed failed logins, $myvesta_bans bans" + fail2ban_details="Health Level: $threat_level (Score: ${health_score}/100) - Fail2Ban functioning normally today: $total_attempts_today failed attempts, $total_bans_today bans, $total_unbans_today unbans, $total_currently_banned currently banned IPs. System secure." fi detailed_report["fail2ban"]="$fail2ban_details" + + # Get and display current jail configurations (only if CHECK_FAIL2BAN_CONFIG is enabled) + if [ "$CHECK_FAIL2BAN_CONFIG" = true ]; then + echo -e "\n${YELLOW}Current Jail Configurations:${NC}" + printf " %-18s | %-8s | %-8s | %-30s\n" "JAIL NAME" "MAXRETRY" "BANTIME" "LOG PATH" + echo -e " -------------------+----------+----------+-------------------------------" + + # Get list of jails + jails=$(fail2ban-client status | grep "Jail list" | cut -d: -f2 | tr ',' ' ') + + for jail in $jails; do + if [ -n "$jail" ]; then + jail_clean=$(echo "$jail" | xargs) + # Get jail configuration details using fail2ban-client or fallback to conf files + maxretry=$(fail2ban-client get "$jail_clean" maxretry 2>/dev/null || grep "maxretry" /etc/fail2ban/jail.d/*.conf /etc/fail2ban/jail.conf 2>/dev/null | grep -v "#" | head -1 | cut -d= -f2 | xargs || echo "N/A") + bantime=$(fail2ban-client get "$jail_clean" bantime 2>/dev/null || grep "bantime" /etc/fail2ban/jail.d/*.conf /etc/fail2ban/jail.conf 2>/dev/null | grep -v "#" | head -1 | cut -d= -f2 | xargs || echo "N/A") + logpath=$(fail2ban-client get "$jail_clean" logpath 2>/dev/null || grep "logpath" /etc/fail2ban/jail.d/*.conf /etc/fail2ban/jail.conf 2>/dev/null | grep -v "#" | head -1 | cut -d= -f2 | xargs || echo "N/A") + + # Convert bantime to human-readable format if it's in seconds + if [[ "$bantime" =~ ^[0-9]+$ ]]; then + if [ "$bantime" -ge 2592000 ]; then + bantime="$((bantime/2592000))month" + elif [ "$bantime" -ge 604800 ]; then + bantime="$((bantime/604800))w" + elif [ "$bantime" -ge 86400 ]; then + bantime="$((bantime/86400))d" + elif [ "$bantime" -ge 3600 ]; then + bantime="$((bantime/3600))h" + elif [ "$bantime" -ge 60 ]; then + bantime="$((bantime/60))m" + else + bantime="${bantime}s" + fi + fi + + printf " %-18s | %-8s | %-8s | %-30s\n" "$jail_clean" "$maxretry" "$bantime" "$logpath" + fi + done + + # Display helpful commands for editing jail configurations + echo -e "\n${YELLOW}Fail2Ban Configuration Commands:${NC}" + + echo -e "${CYAN}# To change maxretry and bantime (persistent changes):${NC}" + echo -e " ${GREEN}nano /etc/fail2ban/jail.local${NC} # Edit configuration file" + echo -e " ${YELLOW}# Add or update the following for each jail section:${NC}" + echo -e " ${YELLOW}maxretry = 0${NC} # Ban after first failed attempt" + echo -e " ${YELLOW}bantime = 2592000${NC} # Ban for 1 month" + echo -e " ${GREEN}systemctl restart fail2ban${NC} # Restart service to apply changes" + + echo -e "\n${CYAN}# Common bantime values for reference:${NC}" + echo -e " ${YELLOW}600 = 10 minutes${NC}" + echo -e " ${YELLOW}3600 = 1 hour${NC}" + echo -e " ${YELLOW}86400 = 1 day${NC}" + echo -e " ${YELLOW}604800 = 1 week${NC}" + echo -e " ${YELLOW}2592000 = 1 month${NC}" + + echo -e "\n${CYAN}# Example configuration for a jail in jail.local:${NC}" + echo -e " ${YELLOW}[ssh-iptables]${NC}" + echo -e " ${YELLOW}enabled = true${NC}" + echo -e " ${YELLOW}filter = sshd${NC}" + echo -e " ${YELLOW}action = vesta[name=SSH]${NC}" + echo -e " ${YELLOW}logpath = /var/log/auth.log${NC}" + echo -e " ${YELLOW}maxretry = 0${NC}" + echo -e " ${YELLOW}bantime = 2592000${NC}" + + echo -e "\n${CYAN}# Check current jail configurations:${NC}" + echo -e " ${GREEN}fail2ban-client status | grep 'Jail list'${NC} # List all active jails" + echo -e " ${GREEN}fail2ban-client get maxretry${NC} # Check maxretry for a jail" + echo -e " ${GREEN}fail2ban-client get bantime${NC} # Check bantime for a jail" + + echo -e "\n${YELLOW}IP Management Commands:${NC}" + echo -e "${CYAN}# Check if IP is banned anywhere:${NC}" + echo -e " ${GREEN}fail2ban-client status | grep 'Jail list' | cut -d: -f2 | tr ',' ' ' | xargs -I {} sh -c 'fail2ban-client status {} | grep && echo \"Found in: {}\"'${NC}" + + echo -e "\n${CYAN}# Unban IP from everywhere:${NC}" + echo -e " ${GREEN}fail2ban-client unban ${NC}" + + echo -e "\n${CYAN}# Reload/Restart fail2ban:${NC}" + echo -e " ${GREEN}fail2ban-client reload${NC} # Reload configuration" + echo -e " ${GREEN}fail2ban-client restart${NC} # Restart service" + + echo -e "\n${RED}⚠️ IMPORTANT: Server restart behavior:${NC}" + echo -e " • ${YELLOW}Banned IPs are LOST after restart/reboot${NC}" + echo -e " • ${YELLOW}All bans are cleared when fail2ban restarts${NC}" + echo -e " • ${YELLOW}Only persistent bans are in iptables rules${NC}" + echo -e " • ${GREEN}Use 'iptables -L' to check persistent firewall rules${NC}" + fi } # Function to run checks with error handling @@ -2887,7 +3403,8 @@ show_config_status() { [ "$CHECK_MYSQL" = true ] && echo -e "MySQL Status: ${GREEN}Enabled${NC}" || echo -e "MySQL Status: ${RED}Disabled${NC}" [ "$CHECK_CLAMAV" = true ] && echo -e "ClamAV Status: ${GREEN}Enabled${NC}" || echo -e "ClamAV Status: ${RED}Disabled${NC}" [ "$CHECK_FAIL2BAN" = true ] && echo -e "Fail2Ban Status: ${GREEN}Enabled${NC}" || echo -e "Fail2Ban Status: ${RED}Disabled${NC}" - [ "$CHECK_EMAIL" = true ] && echo -e "Email Status: ${GREEN}Enabled${NC}" || echo -e "Email Status: ${RED}Disabled${NC}" + [ "$CHECK_FAIL2BAN_CONFIG" = true ] && echo -e "Fail2Ban Configuration Display: ${GREEN}Enabled${NC}" || echo -e "Fail2Ban Configuration Display: ${RED}Disabled${NC}" + [ "$CHECK_EXIM4" = true ] && echo -e "EXIM4 Status: ${GREEN}Enabled${NC}" || echo -e "EXIM4 Status: ${RED}Disabled${NC}" [ "$CHECK_SSL" = true ] && echo -e "SSL Status: ${GREEN}Enabled${NC}" || echo -e "SSL Status: ${RED}Disabled${NC}" [ "$CHECK_BACKUP" = true ] && echo -e "Backup Status: ${GREEN}Enabled${NC}" || echo -e "Backup Status: ${RED}Disabled${NC}" [ "$SEND_EMAIL_REPORT" = true ] && echo -e "Email Reports: ${GREEN}Enabled${NC}" || echo -e "Email Reports: ${RED}Disabled${NC}" @@ -2905,7 +3422,7 @@ handle_args() { CHECK_MYSQL=true CHECK_CLAMAV=true CHECK_FAIL2BAN=true - CHECK_EMAIL=true + CHECK_EXIM4=true CHECK_SSL=true CHECK_BACKUP=true ;; @@ -2916,7 +3433,7 @@ handle_args() { CHECK_MYSQL=false CHECK_CLAMAV=false CHECK_FAIL2BAN=false - CHECK_EMAIL=false + CHECK_EXIM4=false CHECK_SSL=false CHECK_BACKUP=false ;; @@ -2929,7 +3446,7 @@ handle_args() { mysql) CHECK_MYSQL=true ;; clamav) CHECK_CLAMAV=true ;; fail2ban) CHECK_FAIL2BAN=true ;; - email) CHECK_EMAIL=true ;; + email) CHECK_EXIM4=true ;; ssl) CHECK_SSL=true ;; backup) CHECK_BACKUP=true ;; *) echo -e "${RED}Unknown section: $section${NC}" ;; @@ -2944,7 +3461,7 @@ handle_args() { mysql) CHECK_MYSQL=false ;; clamav) CHECK_CLAMAV=false ;; fail2ban) CHECK_FAIL2BAN=false ;; - email) CHECK_EMAIL=false ;; + email) CHECK_EXIM4=false ;; ssl) CHECK_SSL=false ;; backup) CHECK_BACKUP=false ;; *) echo -e "${RED}Unknown section: $section${NC}" ;; @@ -3055,6 +3572,17 @@ send_email_report() { local clean_risk_level=$(echo "$risk_level" | sed 's/\\033\[[0-9;]*m//g' | sed 's/\x1b\[[0-9;]*m//g' | sed 's/\\n/\n/g') local clean_summary=$(echo "$summary" | sed 's/\\033\[[0-9;]*m//g' | sed 's/\x1b\[[0-9;]*m//g' | sed 's/\\n/\n/g') + # Add note about AI API not being configured + local ai_note="" + if [ -z "$AI_API_KEY" ]; then + ai_note="
+

+ ⚠️ AI Analysis Not Configured +

+

The AI API key is not set, so AI-powered analysis is not included in this report.

+
" + fi + email_content+="
$status_icon @@ -3064,6 +3592,7 @@ send_email_report() {
Risk Level: $clean_risk_level
Summary: $clean_summary
+ $ai_note
" # Add detailed summary if any issues are found @@ -3125,7 +3654,7 @@ send_email_report() { "mysql:MySQL Database:🗄️:CHECK_MYSQL" "clamav:ClamAV Antivirus:🦠:CHECK_CLAMAV" "fail2ban:Fail2Ban Security:🛡️:CHECK_FAIL2BAN" - "email:Email System:📧:CHECK_EMAIL" + "email:EXIM4 System:📧:CHECK_EXIM4" "ssl:SSL Certificates:🔒:CHECK_SSL" "backup:Backup System:💾:CHECK_BACKUP" ) @@ -3142,7 +3671,7 @@ send_email_report() { "CHECK_MYSQL") [ "$CHECK_MYSQL" = true ] && is_enabled=true ;; "CHECK_CLAMAV") [ "$CHECK_CLAMAV" = true ] && is_enabled=true ;; "CHECK_FAIL2BAN") [ "$CHECK_FAIL2BAN" = true ] && is_enabled=true ;; - "CHECK_EMAIL") [ "$CHECK_EMAIL" = true ] && is_enabled=true ;; + "CHECK_EXIM4") [ "$CHECK_EXIM4" = true ] && is_enabled=true ;; "CHECK_SSL") [ "$CHECK_SSL" = true ] && is_enabled=true ;; "CHECK_BACKUP") [ "$CHECK_BACKUP" = true ] && is_enabled=true ;; esac @@ -3317,15 +3846,38 @@ send_email_report() { " fi - elif [ "$AI_MODE" = "auto" ] && [ $high_issues -eq 0 ] && [ $medium_issues -eq 0 ] && [ $low_issues -eq 0 ]; then - # System is healthy and AI mode is auto - email_content+="
- ℹ️ AI Analysis Skipped (Auto Mode)
- AI Analysis was not performed because:
- • AI_MODE is set to AUTO
- • No system issues were detected

- Note: AI Analysis will automatically run when issues are detected. Set AI_MODE='always' to run AI analysis on every report. -
" + elif [ "$AI_MODE" = "auto" ] && [ $high_issues -eq 0 ] && [ $medium_issues -eq 0 ]; then + # Check if there are only low issues + if [ $low_issues -gt 0 ]; then + # Only low issues found - AI not used to save API requests + email_content+="
+
+ 💡 + AI Analysis Skipped - Low Priority Issues Only +
+
+

AI Analysis was not performed because only $low_issues low priority issue(s) were detected.

+

🔧 Why: To optimize API usage, AI analysis only runs automatically for medium+ priority issues.

+

Performance: This saves API requests while focusing AI on critical system problems.

+ +
+
🚀 Force AI Analysis:
+ + Set AI_MODE='always' in script configuration + +
+
+
" + else + # No issues at all - system is healthy + email_content+="
+ ℹ️ AI Analysis Skipped (Auto Mode)
+ AI Analysis was not performed because:
+ • AI_MODE is set to AUTO
+ • No system issues were detected

+ Note: AI Analysis will automatically run when medium+ issues are detected. Set AI_MODE='always' to run AI analysis on every report. +
" + fi elif [ "$AI_MODE" = "never" ]; then # AI analysis is disabled via mode email_content+="
@@ -3338,105 +3890,81 @@ send_email_report() {
" elif [ -n "$ai_analysis" ] && [ "$ai_analysis" != "null" ]; then # AI analysis was performed successfully - email_content+="
- ✅ AI Analysis Completed Successfully
- The following recommendations are based on AI analysis (BETA mode - use with caution): + email_content+="
+
+ + AI Analysis Completed Successfully +
+

+ Analysis performed using $AI_MODEL • Generated $(date '+%H:%M:%S') +

" - # Remove ANSI color codes and clean up the AI analysis text - with error handling - local clean_ai_analysis="" + # Convert the AI analysis to a simple dark chat format if [ -n "$ai_analysis" ]; then - clean_ai_analysis=$(echo "$ai_analysis" | sed 's/\x1b\[[0-9;]*m//g' | sed 's/\\n/\n/g' 2>/dev/null || echo "$ai_analysis") - fi - - # Convert the AI analysis to proper HTML format with better styling - if [ -n "$clean_ai_analysis" ]; then - email_content+="
" - - # Process the AI analysis content line by line for better formatting - local formatted_content="" - local in_list=false - local current_section="" + email_content+="
+
+
+ AI +
+ System Analysis Report +
+ +
" + # Process AI analysis line by line for simple formatting + local simple_content="" while IFS= read -r line; do - # Skip empty lines - if [ -z "$line" ]; then - continue - fi + if [ -z "$line" ]; then continue; fi # Handle section headers if echo "$line" | grep -q "^[0-9]\. .*Issues"; then - # Close previous section if open - if [ "$in_list" = true ]; then - formatted_content+="
" - in_list=false + if echo "$line" | grep -q "Low.*Priority"; then + simple_content+="
📊 Low Priority Issues
" + elif echo "$line" | grep -q "Medium.*Priority"; then + simple_content+="
⚠️ Medium Priority Issues
" + elif echo "$line" | grep -q "High.*Priority"; then + simple_content+="
🔥 High Priority Issues
" + elif echo "$line" | grep -q "Critical"; then + simple_content+="
🚨 Critical Issues
" fi - - # Start new section with container - if echo "$line" | grep -q "Critical"; then - current_section="critical" - formatted_content+="
" - formatted_content+="

🚨 Critical Issues

" - elif echo "$line" | grep -q "Medium"; then - current_section="medium" - formatted_content+="
" - formatted_content+="

⚠️ Medium Issues

" - elif echo "$line" | grep -q "Low"; then - current_section="low" - formatted_content+="
" - formatted_content+="

ℹ️ Low Priority Issues

" - fi - - # Start content area and list - formatted_content+="
" - formatted_content+="
    " - in_list=true - - # Handle list items (lines starting with -) + + # Handle bullet points elif echo "$line" | grep -q "^[[:space:]]*-"; then - if [ "$in_list" = false ]; then - formatted_content+="
    " - formatted_content+="
      " - in_list=true - fi - - # Clean and format the list item local item_text=$(echo "$line" | sed 's/^[[:space:]]*-[[:space:]]*//') - # Set border color based on current section - local border_color="#6c757d" - if [ "$current_section" = "critical" ]; then - border_color="#dc3545" - elif [ "$current_section" = "medium" ]; then - border_color="#fd7e14" - elif [ "$current_section" = "low" ]; then - border_color="#17a2b8" - fi - - formatted_content+="
    • • $item_text
    • " - - # Handle regular text lines - else - # If we're in a list, treat as continuation of list item - if [ "$in_list" = true ]; then - # Add as continuation text within the list - formatted_content+="
    • $line
    • " + # Check if this looks like a command + if echo "$item_text" | grep -q -E "(systemctl|service|apt|mysql|php|nginx|apache|v-|\/usr\/|\/etc\/)"; then + simple_content+="
      +
      💻 Command:
      + $item_text +
      " else - # Add regular paragraph - formatted_content+="

      $line

      " + simple_content+="
      • $item_text
      " fi + + # Handle regular text + else + simple_content+="
      $line
      " fi - done <<< "$clean_ai_analysis" + done <<< "$ai_analysis" - # Close final section if still open - if [ "$in_list" = true ]; then - formatted_content+="
" - fi - - email_content+="$formatted_content
" - else - email_content+="
-

AI analysis content could not be processed for email display.

+ email_content+="$simple_content +
+ +
+ 🤖 AI-powered analysis • BETA version • Use as guidance only +
+
" + else + # AI analysis content could not be processed - dark theme error + email_content+="
+
⚠️
+
Processing Error
+
AI analysis content could not be processed for email display.
+
+
💡 Check console output or system logs for detailed analysis
+
" fi else @@ -3502,6 +4030,10 @@ echo -e "${BLUE}Starting MyVestaCP System Check...${NC}" # Setup logging setup_logging + +# Run checks for required tools +check_and_install_jq + log_message "Starting system check" # Execute the script with output capture @@ -3519,6 +4051,7 @@ low_issues=0 # Initialize arrays to track which modules have issues declare -a critical_modules_found=() +declare -a high_modules_found=() declare -a medium_modules_found=() declare -a low_modules_found=() @@ -3565,8 +4098,8 @@ fi # Then check last 24h activity echo -e "\n${BLUE}Checking last 24 hours of activity...${NC}" -if [ "$CHECK_EMAIL" = true ]; then - if ! run_check "Email Status" check_email_status; then +if [ "$CHECK_EXIM4" = true ]; then + if ! run_check "EXIM4 Status" check_email_status; then : # Issue already counted within the function fi fi @@ -3641,19 +4174,19 @@ summary="" # Critical conditions (any of these makes the system critical) if [ $high_issues -gt 0 ]; then - status="${RED}⚠️ Critical${NC}" + status="${RED}⚠️ High Risk${NC}" risk_level="${RED}High${NC}" - summary="Critical issues detected: " + summary="High risk issues detected: " if [ $high_issues -gt 1 ]; then - summary+="$high_issues critical problems" + summary+="$high_issues high-priority problems" else - summary+="1 critical problem" + summary+="1 high-priority problem" fi summary+=" requiring immediate attention" elif [ $medium_issues -gt 100 ]; then # High number of medium issues is also critical status="${RED}⚠️ Critical${NC}" - risk_level="${RED}High${NC}" + risk_level="${RED}Critical${NC}" summary="Critical number of issues: $medium_issues medium problems detected" elif [ $medium_issues -gt 0 ]; then status="${YELLOW}⚠️ Needs Attention${NC}" @@ -3685,6 +4218,13 @@ if [ $high_issues -gt 0 ] || [ $medium_issues -gt 0 ] || [ $low_issues -gt 0 ]; done fi + if [ ${#high_modules_found[@]} -gt 0 ]; then + echo -e "\n${RED}HIGH (${#high_modules_found[@]} modules):${NC}" + for module in "${high_modules_found[@]}"; do + echo -e " - $module" + done + fi + if [ ${#medium_modules_found[@]} -gt 0 ]; then echo -e "\n${YELLOW}MEDIUM (${#medium_modules_found[@]} modules):${NC}" for module in "${medium_modules_found[@]}"; do