#!/bin/bash ################################################################################ # Malware Scanner ################################################################################ # Purpose: Comprehensive malware scanning with multiple engines # Supports: ImunifyAV, ClamAV, Maldet (LMD) # Scan scope: Single domain, user account, or entire server ################################################################################ set -eo pipefail # Color definitions (matching launcher.sh) RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # Source required libraries (warn if missing, but allow graceful degradation) source "$SCRIPT_DIR/lib/common-functions.sh" 2>/dev/null || \ { echo "WARNING: common-functions.sh not found - some features may not work" >&2; } source "$SCRIPT_DIR/lib/system-detect.sh" 2>/dev/null || \ { echo "WARNING: system-detect.sh not found - control panel detection may fail" >&2; } source "$SCRIPT_DIR/lib/user-manager.sh" 2>/dev/null || \ { echo "WARNING: user-manager.sh not found - user selection may not work" >&2; } source "$SCRIPT_DIR/lib/reference-db.sh" 2>/dev/null || true # Optional source "$SCRIPT_DIR/lib/ip-reputation.sh" 2>/dev/null || true # Optional # Arrays for docroots and scanners declare -a docroot_array declare -a sanitized_docroot declare -a remove_docroot declare -a available_scanners # Validate that required functions were sourced from libraries # These functions must exist for the script to work properly validate_required_functions() { local required_functions=( "confirm" "print_header" "print_banner" "select_user_interactive" "get_user_domains" ) for func in "${required_functions[@]}"; do if ! declare -f "$func" &>/dev/null; then echo "ERROR: Required function '$func' not found." >&2 echo " Check that library files exist in: $SCRIPT_DIR/lib/" >&2 return 1 fi done return 0 } # Validate functions early if ! validate_required_functions; then exit 1 fi # Auto-detect web server document root for ImunifyAV standalone UI path get_web_root_for_imunify() { local detected_root="" # Try Apache on Debian/Ubuntu (apache2ctl) if command -v apache2ctl &>/dev/null; then detected_root=$(apache2ctl -S 2>/dev/null | grep "^\*:" | head -1 | awk '{print $NF}' | sed 's/*://' || echo "") if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then echo "$detected_root" return 0 fi fi # Try Apache on RHEL/CentOS (httpd -S) if command -v httpd &>/dev/null; then detected_root=$(httpd -S 2>/dev/null | grep "^\*:" | head -1 | awk '{print $NF}' | sed 's/*://' || echo "") if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then echo "$detected_root" return 0 fi fi # Try Nginx (nginx -T) if command -v nginx &>/dev/null; then detected_root=$(nginx -T 2>/dev/null | grep "^\s*root " | head -1 | awk '{print $NF}' | sed 's/;//' || echo "") if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then echo "$detected_root" return 0 fi fi # Try parsing Apache config files directly for conf_file in /etc/apache2/apache2.conf /etc/httpd/conf/httpd.conf /etc/apache2/sites-enabled/*.conf /etc/httpd/conf.d/*.conf; do if [ -f "$conf_file" ] 2>/dev/null; then detected_root=$(grep -E "^\s*DocumentRoot|^\s*root " "$conf_file" 2>/dev/null | head -1 | awk '{print $NF}' | sed 's/"//g' || echo "") if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then echo "$detected_root" return 0 fi fi done # Try Nginx config files directly for conf_file in /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf /etc/nginx/sites-enabled/*.conf; do if [ -f "$conf_file" ] 2>/dev/null; then detected_root=$(grep -E "^\s*root " "$conf_file" 2>/dev/null | head -1 | awk '{print $NF}' | sed 's/;//' || echo "") if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then echo "$detected_root" return 0 fi fi done # Try common default locations in order of likelihood for path in /var/www/html /home /srv/www /var/www /usr/share/nginx/html /var/www/vhosts; do if [ -d "$path" ] && [ -w "$path" ]; then echo "$path" return 0 fi done # Absolute fallback echo "/var/www/html" } # Individual scanner detection functions is_imunify_installed() { command -v imunify-antivirus &>/dev/null || [ -f "/usr/bin/imunify-antivirus" ] } is_clamav_installed() { command -v clamscan &>/dev/null || \ [ -f "/usr/local/cpanel/3rdparty/bin/clamscan" ] || \ (command -v rpm &>/dev/null && rpm -qa 2>/dev/null | grep -q "cpanel-clamav") || \ (command -v dpkg &>/dev/null && dpkg -l 2>/dev/null | grep -q "^ii.*clamav") } is_maldet_installed() { command -v maldet &>/dev/null || [ -f "/usr/local/sbin/maldet" ] } is_rkhunter_installed() { command -v rkhunter &>/dev/null || [ -f "/usr/bin/rkhunter" ] } # Scanner detection detect_scanners() { available_scanners=() if is_imunify_installed; then available_scanners+=("imunify") fi if is_clamav_installed; then available_scanners+=("clamav") fi if is_maldet_installed; then available_scanners+=("maldet") fi if is_rkhunter_installed; then available_scanners+=("rkhunter") fi # Note: If no scanners are found, available_scanners array will be empty # Menu option 9 allows installation, so we don't exit here # Just return success to allow menu to display return 0 } # Show installation instructions for missing scanners (DEPRECATED - menu always shows now) show_scanner_installation_guide() { echo -e "${YELLOW}Available Malware Scanners:${NC}" echo "" # Check ImunifyAV if ! is_imunify_installed; then echo -e "${CYAN}ImunifyAV${NC} - FREE real-time malware scanner" echo " Status: Not installed" echo " Installation (cPanel):" echo " yum install imunify-antivirus imunify-antivirus-cpanel" echo " /opt/alt/python35/share/imunify360/scripts/av-userside-plugin.sh" echo " Installation (script method):" echo " wget https://repo.imunify360.cloudlinux.com/defence360/imav-deploy.sh" echo " bash imav-deploy.sh" echo " Note: ImunifyAV is FREE. Imunify360 is the paid version." echo " Docs: https://docs.imunify360.com/imunifyav/" echo "" else echo -e "${GREEN}✓ ImunifyAV${NC} - Installed (FREE version)" echo "" fi # Check ClamAV if ! is_clamav_installed; then echo -e "${CYAN}ClamAV${NC} - Open source antivirus engine" echo " Status: Not installed" echo " Installation (cPanel):" echo " /scripts/update_local_rpm_versions --edit target_settings.clamav installed" echo " /scripts/check_cpanel_rpms --fix --targets=clamav" echo " Installation (manual):" echo " yum install clamav clamav-update # RHEL/CentOS" echo " apt-get install clamav clamav-daemon # Debian/Ubuntu" echo " freshclam # Update virus definitions" echo "" else echo -e "${GREEN}✓ ClamAV${NC} - Installed" echo "" fi # Check Maldet if ! is_maldet_installed; then echo -e "${CYAN}Maldet (LMD)${NC} - Linux Malware Detect" echo " Status: Not installed" echo " Installation:" echo " cd /tmp" echo " wget http://www.rfxn.com/downloads/maldetect-current.tar.gz" echo " tar -xzf maldetect-current.tar.gz" echo " cd maldetect-*" echo " ./install.sh" echo " Docs: https://www.rfxn.com/projects/linux-malware-detect/" echo "" else echo -e "${GREEN}✓ Maldet${NC} - Installed" echo "" fi # Check Rootkit Hunter if ! is_rkhunter_installed; then echo -e "${CYAN}Rootkit Hunter${NC} - Rootkit/backdoor/exploit scanner" echo " Status: Not installed" echo " Installation:" echo " yum install epel-release -y # Enable EPEL repo" echo " yum install rkhunter -y" echo " rkhunter --update # Update definitions" echo " rkhunter --propupd # Initialize baseline" echo " Docs: https://rkhunter.sourceforge.net/" echo "" else echo -e "${GREEN}✓ Rootkit Hunter${NC} - Installed" echo "" fi echo -e "${YELLOW}Recommendation:${NC} Install at least ClamAV + RKHunter (both free) for comprehensive protection" echo "" } # Install individual scanners install_maldet_only() { echo "" print_header "Installing Maldet (Linux Malware Detection)" echo "" if is_maldet_installed; then echo -e "${GREEN}✓ Maldet is already installed${NC}" echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true return 0 fi echo "Maldet is a fast, Linux-specific malware scanner" echo "Repository: https://github.com/rfxn/maldet" echo "" echo "Checking available versions..." echo "" cd /tmp || return 1 # Try to download from sources in order with aggressive timeout handling # Skip pre-checking (can hang on firewall-intercepted HTTPS) # Just attempt downloads directly with proper timeouts local download_success=false local temp_file="maldetect-latest.tar.gz" local best_source="" # Download sources in priority order # Format: "name|url|label" (using | as delimiter to avoid splitting https://) local sources=( "rfxn|https://www.rfxn.com/downloads/maldetect-latest.tar.gz|rfxn.com (official)" "github-api|https://api.github.com/repos/rfxn/maldet/archive/refs/heads/main.tar.gz|GitHub API" "github|https://github.com/rfxn/maldet/archive/refs/heads/main.tar.gz|GitHub direct" ) echo "Attempting to download from sources..." echo "" for source_info in "${sources[@]}"; do IFS='|' read -r source_name source_url source_label <<< "$source_info" echo " Trying $source_label..." # Try download with aggressive timeout # --timeout: fail if no progress for this many seconds # --read-timeout: fail if no data received for this many seconds if wget -q --timeout=30 --read-timeout=10 -O "$temp_file" "$source_url" 2>/dev/null; then if [ -f "$temp_file" ] && [ -s "$temp_file" ]; then echo -e " ${GREEN}✓ Download successful from $source_label${NC}" download_success=true best_source="$source_label" break fi rm -f "$temp_file" fi # Also try with curl as fallback if [ "$download_success" = false ]; then if curl -f --connect-timeout 10 --max-time 30 -L -o "$temp_file" "$source_url" 2>/dev/null; then if [ -f "$temp_file" ] && [ -s "$temp_file" ]; then echo -e " ${GREEN}✓ Download successful from $source_label${NC}" download_success=true best_source="$source_label" break fi rm -f "$temp_file" fi fi if [ "$download_success" = false ]; then echo -e " ${RED}✗ Failed or timeout${NC}" fi done echo "" if [ "$download_success" = false ]; then # All sources timed out or failed - check for offline options echo -e "${YELLOW}All download sources failed or timed out.${NC}" echo "" echo "Checking for pre-downloaded archives or system packages..." echo "" # Check for pre-cached/pre-downloaded file local local_archive="" for path in /root/maldetect*.tar.gz /tmp/maldetect*.tar.gz /opt/maldetect*.tar.gz ~/maldetect*.tar.gz; do if [ -f "$path" ]; then local_archive="$path" echo -e "${GREEN}✓ Found archive: $path${NC}" break fi done if [ -n "$local_archive" ]; then echo "" echo "Using pre-downloaded archive..." temp_file="/tmp/maldetect-offline.tar.gz" if cp "$local_archive" "$temp_file" 2>/dev/null && [ -f "$temp_file" ] && [ -s "$temp_file" ]; then download_success=true best_source="offline-archive" echo -e "${GREEN}✓ Archive ready for extraction${NC}" else echo -e "${RED}✗ Failed to copy or validate archive${NC}" download_success=false temp_file="" fi else echo -e "${RED}✗ No local archive found${NC}" echo "" echo "All sources unreachable. To install Maldet, use one of these methods:" echo "" echo " METHOD 1 - Download on networked machine and transfer:" echo " On another server with internet:" echo " wget https://www.rfxn.com/downloads/maldetect-latest.tar.gz" echo " Then copy to this server:" echo " scp maldetect-latest.tar.gz root@YOUR-SERVER:/root/" echo " Then run this installer again" echo "" echo " METHOD 2 - GitHub source as alternative:" echo " wget https://github.com/rfxn/maldet/archive/refs/heads/main.tar.gz -O /root/maldetect.tar.gz" echo " (then run this installer again)" echo "" echo "Once you place the archive in /root/ (or /tmp/ or /opt/), run installer again." echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true return 1 fi fi # Extract and install if we have a valid archive if [ "$download_success" = true ]; then if [ ! -f "$temp_file" ] || [ ! -s "$temp_file" ]; then echo -e "${RED}✗ Archive file missing or empty: $temp_file${NC}" return 1 fi echo "Installing from $best_source..." echo "" # Extract and install echo " Extracting archive..." if tar xzf "$temp_file" 2>/dev/null; then echo " Running installer..." if cd maldetect-* 2>/dev/null && bash install.sh > /tmp/maldet-install.log 2>&1; then echo -e " ${GREEN}✓ Maldet installed successfully${NC}" # Update signatures in background echo "" echo "Updating malware signatures..." if command -v maldet &>/dev/null; then maldet -u > /dev/null 2>&1 & echo " (signatures updating in background)" fi else echo -e " ${RED}✗ Installation failed. Check /tmp/maldet-install.log${NC}" fi cd /tmp rm -rf maldetect-* maldetect-latest.tar.gz 2>/dev/null || true else echo -e " ${RED}✗ Failed to extract archive${NC}" rm -f "$temp_file" fi else echo -e "${RED}✗ No valid archive available for extraction${NC}" fi echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true } install_clamav_only() { echo "" print_header "Installing ClamAV (Open Source Antivirus)" echo "" if is_clamav_installed; then echo -e "${GREEN}✓ ClamAV is already installed${NC}" echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true return 0 fi echo "Installing ClamAV and updating virus definitions..." echo "" if command -v yum &>/dev/null; then echo "Using yum package manager..." yum install -y clamav clamav-daemon clamav-update 2>&1 | tail -5 elif command -v apt-get &>/dev/null; then echo "Using apt package manager..." apt-get update > /dev/null 2>&1 apt-get install -y clamav clamav-daemon 2>&1 | tail -5 else echo -e "${RED}✗ No compatible package manager found${NC}" echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true return 1 fi echo "" if is_clamav_installed; then echo -e "${GREEN}✓ ClamAV installed successfully${NC}" # Update signatures echo "" echo "Updating virus signatures..." for freshclam_path in /usr/bin/freshclam /usr/sbin/freshclam /usr/local/bin/freshclam; do if [ -x "$freshclam_path" ]; then timeout 60 "$freshclam_path" > /dev/null 2>&1 & echo " (signatures updating in background)" break fi done else echo -e "${RED}✗ Installation may have failed${NC}" fi echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true } install_rkhunter_only() { echo "" print_header "Installing RKHunter (Rootkit Detection)" echo "" if is_rkhunter_installed; then echo -e "${GREEN}✓ RKHunter is already installed${NC}" echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true return 0 fi echo "Installing RKHunter..." echo "" if command -v yum &>/dev/null; then echo "Using yum package manager..." yum install -y epel-release 2>&1 > /dev/null || true yum install -y rkhunter 2>&1 | tail -3 elif command -v apt-get &>/dev/null; then echo "Using apt package manager..." apt-get update > /dev/null 2>&1 apt-get install -y rkhunter 2>&1 | tail -3 else echo -e "${RED}✗ No compatible package manager found${NC}" echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true return 1 fi echo "" if is_rkhunter_installed; then echo -e "${GREEN}✓ RKHunter installed successfully${NC}" else echo -e "${RED}✗ Installation may have failed${NC}" fi echo "" read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true } # Install all scanners at once install_all_scanners() { echo "" print_header "Install All Malware Scanners" echo "This will install:" echo " • ClamAV (free, open source)" echo " • Maldet (free, Linux-specific)" echo " • ImunifyAV (FREE version)" echo " • Rootkit Hunter (free, rootkit detection)" echo "" echo -e "${YELLOW}Note: ImunifyAV is FREE. Imunify360 is the paid version.${NC}" echo "" read -p "Proceed with installation? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Cancelled." read -p "Press Enter to continue..." return 0 fi echo "" echo "==========================================" echo "Installing Scanners" echo "==========================================" echo "" # Install ClamAV if ! is_clamav_installed; then echo -e "${CYAN}[1/4] Installing ClamAV...${NC}" # Try control panel-specific methods first if [ -f "/usr/local/cpanel/cpanel" ]; then # cPanel method - use cPanel's package management only if rpm -qa 2>/dev/null | grep -q "cpanel-clamav"; then echo -e "${GREEN}✓ ClamAV already installed (cPanel)${NC}" else echo " → Installing via cPanel package manager..." # Check if cPanel scripts exist before using them if [ -f "/scripts/update_local_rpm_versions" ] && [ -f "/scripts/check_cpanel_rpms" ]; then /scripts/update_local_rpm_versions --edit target_settings.clamav installed 2>/dev/null || true if ! /scripts/check_cpanel_rpms --fix --targets=clamav 2>&1 | tail -3; then # cPanel scripts failed, fall back to standard yum echo " → cPanel package manager unavailable, trying standard yum..." yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Installed|already" || echo " (installation in progress)" fi else # cPanel scripts don't exist, fall back to standard yum echo " → cPanel tools not available, using standard package manager..." yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Installed|already" || echo " (installation in progress)" fi fi # IMPORTANT: Don't fall through to standard yum - cPanel packages conflict! elif [ -f "/usr/local/psa/version" ]; then # Plesk method - use standard package manager echo " → Detected Plesk system, using standard package manager..." if command -v yum &>/dev/null; then yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Installed|already installed" || echo " (installation may already be complete)" elif command -v apt-get &>/dev/null; then apt-get update 2>&1 | grep -E "Reading|Building|Hit|Get" | head -3 || true apt-get install -y clamav clamav-daemon 2>&1 | grep -E "Setting up|already|newest" || echo " (installation may already be complete)" fi elif command -v yum &>/dev/null; then # RHEL/CentOS based systems (non-cPanel) echo " → Installing via yum..." yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Installed|already installed" || echo " (installation may already be complete)" elif command -v apt-get &>/dev/null; then # Debian/Ubuntu: Update package list first, then install ClamAV echo " → Updating package list..." apt-get update 2>&1 | grep -E "Reading|Building|Hit|Get" | head -3 || true echo " → Installing ClamAV..." apt-get install -y clamav clamav-daemon 2>&1 | grep -E "Setting up|already|newest" || echo " (installation may already be complete)" fi if is_clamav_installed; then echo -e "${GREEN}✓ ClamAV installed${NC}" # Find freshclam binary - try standard locations first before using find local freshclam_bin="" for path in /usr/bin/freshclam /usr/sbin/freshclam \ /usr/local/bin/freshclam /usr/local/sbin/freshclam \ /usr/local/cpanel/3rdparty/bin/freshclam; do if [ -x "$path" ]; then freshclam_bin="$path" break fi done # Only use find as last resort if standard paths don't work if [ -z "$freshclam_bin" ]; then freshclam_bin=$(find /usr/local /usr -name freshclam -type f 2>/dev/null | head -1) fi # Update virus signatures immediately if [ -n "$freshclam_bin" ]; then echo " → Updating virus signatures (timeout 60s)..." if timeout 60 "$freshclam_bin" 2>&1 | grep -qE "updated|Downloaded|up-to-date"; then echo -e " ${GREEN}✓${NC} Signatures updated" else echo -e " ${YELLOW}⚠${NC} Signature update inconclusive (may still be current)" fi fi else echo -e "${RED}✗ ClamAV installation failed${NC}" fi else echo -e "${GREEN}✓ ClamAV already installed${NC}" fi echo "" # Install Maldet if ! is_maldet_installed; then echo -e "${CYAN}[2/4] Installing Maldet...${NC}" ( cd /tmp || { echo -e "${RED}✗ Cannot access /tmp${NC}"; return 1; } # Download Maldet echo " → Downloading Maldet..." # Try HTTPS first (more secure), fallback to HTTP if needed if ! wget -q https://www.rfxn.com/downloads/maldetect-current.tar.gz 2>/dev/null; then if ! wget -q http://www.rfxn.com/downloads/maldetect-current.tar.gz; then echo -e "${RED}✗ Download failed - check internet connectivity${NC}" return 1 fi fi if [ -f maldetect-current.tar.gz ]; then echo " → Extracting archive..." if ! tar -xzf maldetect-current.tar.gz 2>/dev/null; then echo -e "${RED}✗ Extraction failed - archive may be corrupted${NC}" rm -f maldetect-current.tar.gz return 1 fi # Find the extracted directory local maldet_dir=$(find /tmp -maxdepth 1 -type d -name "maldetect-*" 2>/dev/null | head -1) if [ -z "$maldet_dir" ]; then echo -e "${RED}✗ Cannot find extracted directory${NC}" cd /tmp rm -rf "maldetect-"* return 1 fi # Change to extracted directory if ! cd "$maldet_dir"; then echo -e "${RED}✗ Cannot access directory: $maldet_dir${NC}" cd /tmp rm -rf "maldetect-"* return 1 fi # Run installation with error capture echo " → Running installation script..." local install_log="/tmp/maldet-install-$$.log" if ./install.sh > "$install_log" 2>&1; then install_exit=0 else install_exit=$? fi # Cleanup cd /tmp rm -rf "maldetect-"* # Check if installation succeeded if is_maldet_installed; then # Verify we have version 2.0 or newer local maldet_bin=$(command -v maldet || find /usr/local -name maldet -type f 2>/dev/null | head -1) local maldet_version="" if [ -n "$maldet_bin" ]; then maldet_version=$("$maldet_bin" -v 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' | head -1) fi # Check version is 2.0 or newer if [ -n "$maldet_version" ]; then local major_version=$(echo "$maldet_version" | cut -d. -f1) if [ "$major_version" -lt 2 ]; then echo -e "${YELLOW}⚠ Warning: Maldet version $maldet_version installed (2.0+ recommended for performance)${NC}" else echo -e "${GREEN}✓${NC} Maldet $maldet_version installed (2.0+ performance optimizations)" fi else echo -e "${GREEN}✓ Maldet installed${NC}" fi rm -f "$install_log" # Update malware signatures immediately with timeout echo " → Updating malware signatures..." if [ -n "$maldet_bin" ]; then if timeout 120 "$maldet_bin" -u 2>&1 | grep -qE "update completed|signatures"; then echo -e " ${GREEN}✓${NC} Signatures updated" else echo -e " ${YELLOW}⚠${NC} Signature update inconclusive (continuing with current definitions)" fi fi else echo -e "${RED}✗ Maldet installation failed${NC}" # Show diagnostic information if [ -f "$install_log" ]; then echo -e "${YELLOW}Installation output (last 10 lines):${NC}" tail -10 "$install_log" | sed 's/^/ /' echo "" echo -e "${YELLOW}Full log saved to: $install_log${NC}" fi fi return 0 else echo -e "${RED}✗ Download failed - maldetect-current.tar.gz not found${NC}" return 0 fi ) || true else echo -e "${GREEN}✓ Maldet already installed${NC}" fi echo "" # Install ImunifyAV (FREE version) if ! is_imunify_installed; then echo -e "${CYAN}[3/4] Installing ImunifyAV (FREE)...${NC}" echo " This may take several minutes - please wait..." # ── STANDALONE DETECTION ───────────────────────────────────────── # Detect whether this is a standalone system (no cPanel, no Plesk). # InterWorx is also treated as standalone for ImunifyAV purposes # because imav-deploy.sh does not recognise it as a "panel". local imav_is_standalone=0 if [ ! -f "/usr/local/cpanel/cpanel" ] && [ ! -f "/usr/local/psa/version" ]; then imav_is_standalone=1 fi # ── STANDALONE: INTEGRATION.CONF SETUP ─────────────────────────── if [ "$imav_is_standalone" -eq 1 ]; then echo "" echo -e "${YELLOW} ⚠ Standalone system detected (no cPanel or Plesk found)${NC}" echo " ImunifyAV requires a web server path for its UI on standalone systems." echo "" local imav_conf_dir="/etc/sysconfig/imunify360" local imav_conf_file="$imav_conf_dir/integration.conf" local imav_ui_path="" # Check if integration.conf already exists with ui_path set if [ -f "$imav_conf_file" ] && grep -q "^ui_path" "$imav_conf_file" 2>/dev/null; then # Already configured - read existing value for display only imav_ui_path=$(grep "^ui_path" "$imav_conf_file" | head -1 | cut -d'=' -f2 | tr -d ' ') echo -e " ${GREEN}✓${NC} integration.conf already exists with ui_path: $imav_ui_path" echo " Proceeding with existing configuration." else # Auto-detect web server document root (no prompting) local imav_detected_root imav_detected_root=$(get_web_root_for_imunify) imav_ui_path="$imav_detected_root/imunifyav" echo -e " ${GREEN}✓${NC} Auto-detected web root: $imav_detected_root" echo " UI will be deployed to: $imav_ui_path" fi # Create config directory if needed if [ "$imav_is_standalone" -ne 2 ]; then echo " → Creating $imav_conf_dir ..." mkdir -p "$imav_conf_dir" || { echo -e "${RED} ✗ Cannot create $imav_conf_dir - check permissions. Skipping ImunifyAV.${NC}" imav_is_standalone=2 } fi # Write minimal integration.conf (only ui_path is required) if [ "$imav_is_standalone" -ne 2 ]; then printf '[paths]\nui_path = %s\n' "$imav_ui_path" > "$imav_conf_file" || { echo -e "${RED} ✗ Cannot write $imav_conf_file. Skipping ImunifyAV.${NC}" imav_is_standalone=2 } fi if [ "$imav_is_standalone" -ne 2 ]; then echo -e " ${GREEN}✓${NC} integration.conf written: ui_path = $imav_ui_path" fi # SELinux warning for RHEL-family systems if [ "$imav_is_standalone" -ne 2 ] && command -v getenforce &>/dev/null; then local selinux_status selinux_status=$(getenforce 2>/dev/null || echo "Unknown") if [ "$selinux_status" = "Enforcing" ]; then echo "" echo -e " ${YELLOW}⚠ SELinux is Enforcing${NC}" echo " After installation, ImunifyAV may need an SELinux policy module." echo " If the UI is inaccessible, run:" echo " ausearch -c 'imunify' | audit2allow -M imunify && semodule -i imunify.pp" fi fi echo "" fi # ── END STANDALONE SETUP ───────────────────────────────────────── # Only proceed with download/deploy if not cancelled (imav_is_standalone != 2) if [ "${imav_is_standalone:-0}" -ne 2 ]; then # Use deployment script method (most reliable) cd /tmp if [ -f "imav-deploy.sh" ]; then rm -f imav-deploy.sh fi # Download deployment script with timeout if timeout 30 wget -q -O imav-deploy.sh https://repo.imunify360.cloudlinux.com/defence360/imav-deploy.sh 2>/dev/null; then if [ ! -f imav-deploy.sh ] || [ ! -s imav-deploy.sh ]; then echo -e "${RED} Failed to download installation script (empty file)${NC}" else # Run deployment script with timeout and capture output echo " → Running deployment script..." local deploy_log="/tmp/imav-deploy-$$.log" if timeout 300 bash imav-deploy.sh > "$deploy_log" 2>&1; then # Check if any actual installation happened if grep -qiE "installed|complete|success" "$deploy_log"; then echo " → Deployment script executed" else echo " → Deployment script ran (check for errors below)" fi # Show any errors from deployment if grep -qi "error\|failed\|conflict" "$deploy_log"; then echo -e " ${YELLOW}⚠ Warnings detected:${NC}" grep -iE "error|failed|conflict" "$deploy_log" | sed 's/^/ /' | head -3 fi else echo -e "${YELLOW} ⚠ Deployment script timed out or failed${NC}" fi rm -f "$deploy_log" rm -f imav-deploy.sh # Try to start the service if installed if command -v systemctl &>/dev/null && is_imunify_installed; then echo " → Starting ImunifyAV service..." systemctl start imunify-antivirus 2>/dev/null || true fi fi else echo -e "${RED} Failed to download installation script (network error or timeout)${NC}" fi if is_imunify_installed; then echo -e "${GREEN}✓ ImunifyAV (FREE) installed${NC}" echo " No license key required - this is the FREE version" # Find imunify-antivirus binary local imunify_bin=$(command -v imunify-antivirus || find /usr -name imunify-antivirus 2>/dev/null | head -1) # Update malware signatures immediately if [ -n "$imunify_bin" ]; then echo " → Updating malware signatures..." if timeout 60 "$imunify_bin" update 2>&1 | grep -qiE "updated|Success|completed"; then echo -e " ${GREEN}✓${NC} Signatures updated" else echo -e " ${YELLOW}⚠${NC} Signature update inconclusive (continuing with current definitions)" fi fi # ── STANDALONE: POST-INSTALL UI URL HINT ───────────────── if [ "$imav_is_standalone" -eq 1 ] && [ -n "${imav_ui_path:-}" ]; then echo "" echo -e " ${CYAN}ImunifyAV UI path:${NC} $imav_ui_path" echo " Configure your web server to serve that directory, then" echo " access the UI at: http://YOUR-SERVER-IP//" echo " (Replace with the last component of the path above)" fi # ── END POST-INSTALL HINT ───────────────────────────────── else echo -e "${RED}✗ ImunifyAV installation failed${NC}" if [ "$imav_is_standalone" -eq 1 ]; then echo -e "${YELLOW} Note: Verify integration.conf at $imav_conf_file is correct${NC}" echo -e "${YELLOW} and that $imav_ui_path is accessible by your web server.${NC}" else echo -e "${YELLOW} Note: ImunifyAV FREE is primarily supported on CloudLinux, cPanel, and Plesk systems${NC}" fi fi fi # ── END CANCELLED GUARD ─────────────────────────────────────────── else echo -e "${GREEN}✓ ImunifyAV already installed${NC}" fi echo "" # Install Rootkit Hunter if ! is_rkhunter_installed; then echo -e "${CYAN}[4/4] Installing Rootkit Hunter...${NC}" # Ensure repo is enabled (OS-specific) if command -v dnf &>/dev/null; then # CentOS 8+, RHEL 8+, Fedora - use dnf as primary package manager if ! rpm -qa 2>/dev/null | grep -q epel-release; then echo " → Installing EPEL repository..." dnf install -y epel-release 2>&1 | grep -E "Installing|Installed|already installed" || echo " (repo may already be enabled)" fi # Install rkhunter dnf install -y rkhunter 2>&1 | grep -E "Installing|Installed|already installed" || echo " (installation may already be complete)" elif command -v yum &>/dev/null; then # CentOS 7, RHEL 7 - use yum if ! rpm -qa 2>/dev/null | grep -q epel-release; then echo " → Installing EPEL repository..." yum install -y epel-release 2>&1 | grep -E "Installing|Installed|already installed" || echo " (repo may already be enabled)" fi # Install rkhunter yum install -y rkhunter 2>&1 | grep -E "Installing|Installed|already installed" || echo " (installation may already be complete)" elif command -v apt-get &>/dev/null; then # Debian/Ubuntu - universe repo (rkhunter is in universe) echo " → Ensuring universe repository is enabled..." if ! grep -q "universe" /etc/apt/sources.list 2>/dev/null; then # Add universe to existing deb lines (handles both HTTP and HTTPS) sed -i 's/^\(deb.*\) \(main\|restricted\)$/\1 \2 universe/' /etc/apt/sources.list 2>/dev/null || true apt-get update 2>&1 | grep -E "Hit|Get|Reading|Building" | head -3 || true fi apt-get install -y rkhunter 2>&1 | grep -E "Setting up|already|newest" || echo " (installation may already be complete)" fi if is_rkhunter_installed; then echo -e "${GREEN}✓ Rootkit Hunter installed${NC}" # Update definitions echo " → Updating rootkit definitions..." if timeout 120 rkhunter --update 2>&1 | grep -qE "updated|downloaded"; then echo -e " ${GREEN}✓${NC} Definitions updated" else echo -e " ${YELLOW}⚠${NC} Definitions update inconclusive (continuing)" fi # Initialize baseline (propupd creates file property database) echo " → Initializing baseline database..." if timeout 300 rkhunter --propupd 2>&1 | grep -q "Updating" || timeout 300 rkhunter --propupd &>/dev/null; then echo -e " ${GREEN}✓${NC} Baseline initialized" else echo -e " ${YELLOW}⚠${NC} Baseline initialization inconclusive" fi else echo -e "${RED}✗ Rootkit Hunter installation failed${NC}" fi else echo -e "${GREEN}✓ Rootkit Hunter already installed${NC}" fi echo "" echo "==========================================" echo "Installation Complete" echo "==========================================" echo "" # Re-detect scanners detect_scanners echo "" read -p "Press Enter to continue..." } # Detect control panel and gather docroots detect_control_panel() { docroot_array=() # Use system-detect.sh if available, otherwise detect if [ -n "$SYS_CONTROL_PANEL" ]; then CONTROL_PANEL="$SYS_CONTROL_PANEL" elif [ -f "/etc/userdatadomains" ]; then CONTROL_PANEL="cpanel" elif [ -f "/usr/local/psa/version" ]; then CONTROL_PANEL="plesk" elif [ -d "/usr/local/interworx/" ]; then CONTROL_PANEL="interworx" else CONTROL_PANEL="none" fi # cPanel-specific setup if [ "$CONTROL_PANEL" = "cpanel" ]; then # Add cPanel 3rdparty bin to PATH only for cPanel export PATH=/usr/local/cpanel/3rdparty/bin/:$PATH while IFS= read -r line; do # Format: domain: user==owner==main==domain==docroot==... # Extract docroot (field 5, 0-indexed field 4) using awk directly docroot=$(awk -F'==' '{print $5}' <<< "$line") [ -n "$docroot" ] && [ -d "$docroot" ] && docroot_array+=("$docroot") done < <(cut -d: -f2- /etc/userdatadomains | sort -u) # Plesk-specific elif [ "$CONTROL_PANEL" = "plesk" ]; then while IFS= read -r domain; do docroot=$(plesk bin site -i "$domain" 2>/dev/null | grep "WWW-Root" | awk '{print $2}') [ -n "$docroot" ] && docroot_array+=("$docroot") done < <(plesk bin site --list 2>/dev/null) # InterWorx-specific (improved with proper path structure) elif [ "$CONTROL_PANEL" = "interworx" ]; then # InterWorx structure: /home/username/domain.com/html # Find all html directories in the InterWorx structure while IFS= read -r docroot; do [ -n "$docroot" ] && [ -d "$docroot" ] && docroot_array+=("$docroot") done < <(find /home/*/*/html -maxdepth 0 -type d 2>/dev/null | sort -u) else CONTROL_PANEL="none" echo -e "${YELLOW}No control panel detected${NC}" echo "Manual path selection required" return 1 fi # Remove subdirectory docroots (avoid scanning same files twice) sanitize_docroots return 0 } # Remove subdirectory docroots from array (optimized single-pass algorithm) sanitize_docroots() { sanitized_docroot=() # For each docroot, check if it's a parent of any other docroot for current in "${docroot_array[@]}"; do is_subdir=0 # Check if current path is a subdirectory of any other path for other in "${docroot_array[@]}"; do # Use [[ ]] for substring matching (safe, doesn't use regex) if [[ "$current" != "$other" && "$current" == "$other"/* ]]; then is_subdir=1 break fi done # Only add if it's NOT a subdirectory of another path if [ $is_subdir -eq 0 ]; then sanitized_docroot+=("$current") fi done } # Get docroots for specific user get_user_docroots() { local username="$1" local user_docroots=() if [ "$CONTROL_PANEL" = "cpanel" ]; then while IFS= read -r line; do docroot=$(awk -F'==' '{print $5}' <<< "$line") [ -n "$docroot" ] && [ -d "$docroot" ] && user_docroots+=("$docroot") done < <(grep -F ":${username}==" /etc/userdatadomains | cut -d: -f2- | sort -u) elif [ "$CONTROL_PANEL" = "interworx" ]; then # Use user-manager.sh to get all domains for this user local domains=$(get_user_domains "$username") if [ -n "$domains" ]; then while IFS= read -r domain; do # InterWorx: /home/username/domain.com/html local docroot="/home/${username}/${domain}/html" [ -d "$docroot" ] && user_docroots+=("$docroot") done <<< "$domains" fi else echo -e "${RED}User-specific scanning only supported on cPanel/InterWorx${NC}" return 1 fi echo "${user_docroots[@]}" } # Get docroot for specific domain get_domain_docroot() { local domain="$1" local domain_docroot="" if [ "$CONTROL_PANEL" = "cpanel" ]; then # Use grep with word boundary for safe matching (avoid regex injection) domain_docroot=$(grep "^$(printf '%s\n' "$domain" | sed 's/[[\.*^$/]/\\&/g'):" /etc/userdatadomains | cut -d= -f5 | sed 's/==/=/g') elif [ "$CONTROL_PANEL" = "plesk" ]; then domain_docroot=$(plesk bin site -i "$domain" 2>/dev/null | grep "WWW-Root" | awk '{print $2}') elif [ "$CONTROL_PANEL" = "interworx" ]; then # Find which user owns this domain using vhost configs # Use safer approach - validate glob results before processing local username="" for conf_file in /etc/httpd/conf.d/vhost_*.conf; do [ -f "$conf_file" ] || continue if grep -qF "ServerName ${domain}" "$conf_file" 2>/dev/null; then username=$(grep "SuexecUserGroup" "$conf_file" 2>/dev/null | awk '{print $2}' | head -1) [ -n "$username" ] && break fi done if [ -n "$username" ]; then # InterWorx: /home/username/domain.com/html domain_docroot="/home/${username}/${domain}/html" fi else echo -e "${RED}Domain lookup only supported on cPanel/Plesk/InterWorx${NC}" return 1 fi echo "$domain_docroot" } # Memory check before scanning check_memory() { local total_mem=$(free -m | awk '/^Mem:/{print $2}') local avail_mem=$(free -m | awk '/^Mem:/{print $7}') local min_total=2048 # 2GB local min_avail=512 # 512MB if [ "$total_mem" -lt "$min_total" ] || [ "$avail_mem" -lt "$min_avail" ]; then echo -e "${YELLOW}WARNING: Low memory detected${NC}" echo "Total: ${total_mem}MB | Available: ${avail_mem}MB" echo "" echo "Running a full scan may cause high load or OOM conditions." echo "" read -p "Continue anyway? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Scan cancelled" return 1 fi fi return 0 } # ImunifyAV scanner # Generate standalone malware scan script generate_standalone_scanner() { local scan_paths=("$@") if [ ${#scan_paths[@]} -eq 0 ]; then echo -e "${RED}No paths to scan${NC}" return 1 fi # Create session ID and directory local session_id="malware-$(date +%Y%m%d-%H%M%S)" local session_dir="/opt/${session_id}" echo "" print_header "Generating Standalone Scanner" echo "Session ID: $session_id" echo "Location: $session_dir" echo "" # Create directory structure mkdir -p "$session_dir"/{logs,results} # Create standalone scan script cat > "$session_dir/scan.sh" << 'STANDALONE_EOF' #!/bin/bash set -o pipefail ################################################################################ # Standalone Malware Scanner ################################################################################ # Auto-generated by Server Management Toolkit # This script is self-contained and can run independently ################################################################################ # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # Detect control panel (for IP reputation tracking) if [ -z "$CONTROL_PANEL" ]; then if [ -f "/usr/local/cpanel/version" ]; then CONTROL_PANEL="cpanel" elif [ -f "/usr/local/psa/version" ]; then CONTROL_PANEL="plesk" elif [ -d "/home/interworx" ] || [ -f "/usr/bin/iworx-helper" ] || ([ -d "/chroot/home" ] && [ -f "/usr/bin/nodeworx" ]); then CONTROL_PANEL="interworx" else CONTROL_PANEL="standalone" fi fi # Detect log directory based on control panel if [ -z "$SYS_LOG_DIR" ]; then case "$CONTROL_PANEL" in cpanel) SYS_LOG_DIR="/var/log/apache2/domlogs" ;; plesk) SYS_LOG_DIR="/var/www/vhosts/system" ;; interworx) SYS_LOG_DIR="/home" ;; *) SYS_LOG_DIR="/var/log" ;; esac fi # Detect user home base directory based on control panel if [ -z "$SYS_USER_HOME_BASE" ]; then case "$CONTROL_PANEL" in cpanel) SYS_USER_HOME_BASE="/home" ;; plesk) SYS_USER_HOME_BASE="/var/www/vhosts" ;; interworx) SYS_USER_HOME_BASE="/chroot/home" ;; *) SYS_USER_HOME_BASE="/home" ;; esac fi # Get script directory SCAN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOG_DIR="$SCAN_DIR/logs" RESULTS_DIR="$SCAN_DIR/results" # Create log and results directories mkdir -p "$LOG_DIR" "$RESULTS_DIR" # Session info SESSION_LOG="$LOG_DIR/session.log" SUMMARY_FILE="$RESULTS_DIR/summary.txt" INFECTED_LIST="$RESULTS_DIR/infected_files.txt" # Logging function log_message() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$SESSION_LOG" } # Confirm user action (y/n prompt) confirm() { local prompt="$1" local response read -t 10 -p "$prompt (y/n): " response /dev/null || response="n" [[ "$response" =~ ^[Yy]$ ]] } # Activity spinner for long-running scans show_spinner() { local pid=$1 local message=$2 local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' local i=0 while kill -0 "$pid" 2>/dev/null; do i=$(( (i+1) % 10 )) printf "\r ⏳ %s %s " "$message" "${spin:$i:1}" sleep 0.2 done printf "\r ✓ %s - Complete\n" "$message" } # Format elapsed time format_time() { local seconds=$1 if [ "$seconds" -lt 60 ]; then echo "${seconds}s" elif [ "$seconds" -lt 3600 ]; then printf "%dm %ds" $((seconds / 60)) $((seconds % 60)) else printf "%dh %dm" $((seconds / 3600)) $(((seconds % 3600) / 60)) fi } # Cleanup function for trap handler cleanup_on_exit() { local exit_code=$? echo "" # Remove running marker file rm -f "$SCAN_DIR/.scan_running" # Only log if session log exists if [ -f "$SESSION_LOG" ]; then log_message "Cleanup triggered (exit code: $exit_code)" fi # Remove temporarily installed RKHunter if [ "${RKHUNTER_TEMP_INSTALLED:-false}" = "true" ]; then if [ -f "$SESSION_LOG" ]; then log_message "Removing temporarily installed RKHunter..." fi echo "→ Cleaning up: Removing Rootkit Hunter..." if command -v yum &>/dev/null; then if yum remove -y rkhunter &>/dev/null 2>&1; then if [ -f "$SESSION_LOG" ]; then log_message "RKHunter removed successfully" fi else if [ -f "$SESSION_LOG" ]; then log_message "WARNING: Failed to remove RKHunter (yum command failed)" fi fi elif command -v apt-get &>/dev/null; then if apt-get remove -y rkhunter &>/dev/null 2>&1; then if [ -f "$SESSION_LOG" ]; then log_message "RKHunter removed successfully" fi else if [ -f "$SESSION_LOG" ]; then log_message "WARNING: Failed to remove RKHunter (apt-get command failed)" fi fi fi fi # Save interrupted status (only if summary file directory exists) if [ "$exit_code" -ne 0 ] && [ -d "$RESULTS_DIR" ]; then { echo "" echo "SCAN INTERRUPTED" echo "Exit code: $exit_code" echo "Time: $(date)" } >> "$SUMMARY_FILE" if [ -f "$SESSION_LOG" ]; then log_message "Scan interrupted with exit code: $exit_code" fi fi } # Set trap for cleanup on exit, interrupt, or termination trap cleanup_on_exit EXIT INT TERM # Banner clear echo "========================================" echo "Standalone Malware Scanner" echo "========================================" echo "Session: $(basename "$SCAN_DIR")" echo "Started: $(date)" echo "========================================" echo "" log_message "Scan session started" # Create marker file to indicate scan is running touch "$SCAN_DIR/.scan_running" # Detect available scanners AVAILABLE_SCANNERS=() if command -v imunify-antivirus &>/dev/null; then AVAILABLE_SCANNERS+=("imunify") log_message "Detected: ImunifyAV" fi if command -v clamscan &>/dev/null; then AVAILABLE_SCANNERS+=("clamav") log_message "Detected: ClamAV" fi if command -v maldet &>/dev/null; then AVAILABLE_SCANNERS+=("maldet") log_message "Detected: Maldet" fi # Track if rkhunter was auto-installed (for cleanup) RKHUNTER_TEMP_INSTALLED=false if command -v rkhunter &>/dev/null; then AVAILABLE_SCANNERS+=("rkhunter") log_message "Detected: Rootkit Hunter" else # Auto-install rkhunter temporarily for this scan log_message "RKHunter not found - installing temporarily..." echo "→ Installing Rootkit Hunter (temporary, will be removed after scan)..." if command -v yum &>/dev/null; then # Ensure EPEL is available for RHEL-based systems if ! rpm -qa | grep -q epel-release; then log_message "RKHunter: Installing EPEL repository..." yum install -y epel-release &>/dev/null || log_message "WARNING: EPEL install failed" fi # Install rkhunter via yum log_message "RKHunter: Installing via yum..." if yum install -y rkhunter &>/dev/null; then # Update definitions and initialize baseline if rkhunter --update &>/dev/null; then log_message "RKHunter: Database update successful" else log_message "WARNING: RKHunter database update failed" fi if rkhunter --propupd &>/dev/null; then log_message "RKHunter: Property baseline created" else log_message "WARNING: RKHunter property baseline creation failed" fi AVAILABLE_SCANNERS+=("rkhunter") RKHUNTER_TEMP_INSTALLED=true log_message "RKHunter installed temporarily" echo " ✓ RKHunter installed (will be removed after scan)" else log_message "WARNING: RKHunter yum install failed" fi elif command -v apt-get &>/dev/null; then # Install rkhunter via apt-get on Debian-based systems log_message "RKHunter: Installing via apt-get..." if apt-get update &>/dev/null && apt-get install -y rkhunter &>/dev/null; then # Update definitions and initialize baseline if rkhunter --update &>/dev/null; then log_message "RKHunter: Database update successful" else log_message "WARNING: RKHunter database update failed" fi if rkhunter --propupd &>/dev/null; then log_message "RKHunter: Property baseline created" else log_message "WARNING: RKHunter property baseline creation failed" fi AVAILABLE_SCANNERS+=("rkhunter") RKHUNTER_TEMP_INSTALLED=true log_message "RKHunter installed temporarily" echo " ✓ RKHunter installed (will be removed after scan)" else log_message "WARNING: RKHunter apt-get install failed" fi else log_message "WARNING: Neither yum nor apt-get found - cannot auto-install RKHunter" fi fi # Filter scanners if MALDET_ONLY is set (for Maldet-specific menu) if [ "${MALDET_ONLY:-0}" = "1" ]; then log_message "Maldet-only mode enabled" echo "🔍 Running Maldet-only scan (fastest, Linux-focused)" echo "" # Check if Maldet is available if [[ " ${AVAILABLE_SCANNERS[@]} " =~ " maldet " ]]; then AVAILABLE_SCANNERS=("maldet") log_message "Filtered to Maldet only" else log_message "ERROR: Maldet not installed but MALDET_ONLY was set" echo -e "${RED}ERROR: Maldet is not installed${NC}" exit 1 fi fi # If no scanners found, show installation guide and exit gracefully if [ ${#AVAILABLE_SCANNERS[@]} -eq 0 ]; then log_message "WARNING: No scanners found on this system" echo "" echo -e "${RED}No malware scanners detected!${NC}" echo "" echo -e "${YELLOW}Available Malware Scanners:${NC}" echo "" echo -e "${CYAN}ImunifyAV${NC} - FREE real-time malware scanner" echo " Status: Not installed" echo " Installation (cPanel):" echo " yum install imunify-antivirus imunify-antivirus-cpanel" echo " /opt/alt/python35/share/imunify360/scripts/av-userside-plugin.sh" echo " Installation (manual):" echo " wget https://repo.imunify360.cloudlinux.com/defence360/imav-deploy.sh" echo " bash imav-deploy.sh" echo " Docs: https://docs.imunify360.com/imunifyav/" echo "" echo -e "${CYAN}ClamAV${NC} - Open source antivirus engine" echo " Status: Not installed" echo " Installation:" echo " yum install clamav clamav-update # RHEL/CentOS" echo " apt-get install clamav clamav-daemon # Debian/Ubuntu" echo "" echo -e "${CYAN}Maldet (LMD)${NC} - Linux Malware Detect" echo " Status: Not installed" echo " Installation:" echo " cd /tmp && wget http://www.rfxn.com/downloads/maldetect-current.tar.gz" echo " tar -xzf maldetect-current.tar.gz && cd maldetect-*" echo " ./install.sh" echo "" echo -e "${CYAN}Rootkit Hunter${NC} - Rootkit/backdoor/exploit scanner" echo " Status: Not installed" echo " Installation:" echo " yum install epel-release rkhunter # RHEL/CentOS" echo " apt-get install rkhunter # Debian/Ubuntu" echo "" echo -e "${YELLOW}Recommendation:${NC} Install at least ClamAV + RKHunter (both free)" echo "" exit 0 fi log_message "Found ${#AVAILABLE_SCANNERS[@]} scanner(s): ${AVAILABLE_SCANNERS[*]}" # Scan paths (will be replaced) SCAN_PATHS=() PLACEHOLDER_SCAN_PATHS # Validate scan paths log_message "Validating scan paths..." VALID_PATHS=() for path in "${SCAN_PATHS[@]}"; do if [ -e "$path" ]; then if [ -r "$path" ]; then VALID_PATHS+=("$path") log_message "✓ Valid path: $path" else log_message "WARNING: Path not readable: $path (skipping)" echo "⚠️ WARNING: Cannot read $path (permission denied) - skipping" fi else log_message "WARNING: Path does not exist: $path (skipping)" echo "⚠️ WARNING: Path does not exist: $path - skipping" fi done if [ ${#VALID_PATHS[@]} -eq 0 ]; then log_message "ERROR: No valid paths to scan!" echo -e "${RED}ERROR: No valid paths to scan!${NC}" exit 1 fi # Use only valid paths SCAN_PATHS=("${VALID_PATHS[@]}") log_message "Scanning ${#SCAN_PATHS[@]} valid path(s)" # Check available disk space for logs log_message "Checking disk space..." SCAN_DIR_FS=$(df -P "$SCAN_DIR" | tail -1 | awk '{print $6}') AVAILABLE_KB=$(df -P "$SCAN_DIR" | tail -1 | awk '{print $4}') AVAILABLE_MB=$((AVAILABLE_KB / 1024)) if [ "$AVAILABLE_MB" -lt 100 ]; then log_message "WARNING: Low disk space ($AVAILABLE_MB MB available)" echo "⚠️ WARNING: Low disk space on $SCAN_DIR_FS ($AVAILABLE_MB MB available)" echo "Scan logs may be large. Recommend at least 100 MB free space." echo "" read -t 10 -p "Continue anyway? (y/N): " continue_scan /dev/null || continue_scan="n" if [[ ! "$continue_scan" =~ ^[Yy]$ ]]; then log_message "Scan cancelled due to low disk space" echo "Scan cancelled." exit 0 fi else log_message "Disk space OK: $AVAILABLE_MB MB available" fi # Initialize summary { echo "==========================================" echo "Malware Scan Summary Report" echo "==========================================" echo "Session: $(basename "$SCAN_DIR")" echo "Started: $(date)" echo "Scanners: ${AVAILABLE_SCANNERS[*]}" echo "Paths: ${#SCAN_PATHS[@]}" echo "" printf '%s\n' "${SCAN_PATHS[@]}" echo "" echo "==========================================" echo "" } > "$SUMMARY_FILE" # Track completion SCANNERS_COMPLETED=0 TOTAL_SCANNERS=${#AVAILABLE_SCANNERS[@]} # Run each scanner for scanner in "${AVAILABLE_SCANNERS[@]}"; do SCANNER_NUM=$((SCANNERS_COMPLETED + 1)) echo "" echo "" echo "==========================================" echo -e "${CYAN}${BOLD}Scanner $SCANNER_NUM of $TOTAL_SCANNERS: ${scanner^}${NC}" echo "==========================================" echo "" log_message "Starting ${scanner} scan ($SCANNER_NUM/$TOTAL_SCANNERS)" { echo "Scanner: ${scanner^}" echo "Started: $(date)" echo "---" } >> "$SUMMARY_FILE" case "$scanner" in imunify) # ImunifyAV has built-in exclusions that prevent comprehensive system scanning # Only use ImunifyAV for user-focused scans (not full server scans) if [ "${#SCAN_PATHS[@]}" -eq 1 ] && [ "${SCAN_PATHS[0]}" = "/" ]; then echo "" echo "ℹ️ Skipping ImunifyAV for full server scan" echo " Reason: ImunifyAV has built-in exclusions that skip system directories" echo " ClamAV and Maldet will provide comprehensive coverage instead" echo "" log_message "ImunifyAV: Skipped (not suitable for full server scans - use ClamAV/Maldet instead)" { echo "⊘ ImunifyAV scan skipped (not suitable for full system scans)" } >> "$SUMMARY_FILE" continue fi SCAN_START=$(date +%s) log_message "ImunifyAV: Updating signatures" if ! timeout 300 imunify-antivirus update &>> "$LOG_DIR/imunify.log"; then log_message "WARNING: ImunifyAV update failed (continuing with existing signatures)" echo "⚠️ WARNING: Signature update failed, using existing signatures" fi log_message "ImunifyAV: Starting on-demand scan" echo "" echo " 📁 Scanning paths: ${SCAN_PATHS[@]}" echo " ⏳ Scanner: ImunifyAV" echo "" IMUNIFY_INFECTED=0 # Run ImunifyAV scan with timeout (2 hours max) # Use --output-format to make parsing easier, with --timeout to prevent hanging for path in "${SCAN_PATHS[@]}"; do if [ ! -d "$path" ]; then log_message "ImunifyAV: Skipping non-existent path: $path" continue fi log_message "ImunifyAV: Scanning $path" echo " Scanning: $path" # Run scan with timeout to prevent hanging on status checks # ImunifyAV scan - use 'queue put' command with path as positional argument timeout 7200 imunify-antivirus malware on-demand queue put "$path" &>> "$LOG_DIR/imunify.log" & IMUNIFY_PID=$! # Monitor with simple timeout (don't try to parse imunify status which hangs) if [ -n "$IMUNIFY_PID" ] && kill -0 "$IMUNIFY_PID" 2>/dev/null; then wait "$IMUNIFY_PID" IMUNIFY_EXIT=$? else log_message "ERROR: ImunifyAV PID not found for $path" IMUNIFY_EXIT=1 fi if [ "$IMUNIFY_EXIT" -eq 124 ]; then log_message "ERROR: ImunifyAV scan timed out after 2 hours for $path" echo " ⏱️ Scan timed out (2 hour limit)" elif [ "$IMUNIFY_EXIT" -ne 0 ]; then log_message "WARNING: ImunifyAV scan exited with code $IMUNIFY_EXIT for $path" echo " ⚠️ Scan completed with code $IMUNIFY_EXIT" fi done # Try to get count of malicious files from malicious list (if available) # FIXED Issue 4B: Defensive header detection malicious_output=$(timeout 60 imunify-antivirus malware malicious list 2>/dev/null) IMUNIFY_MALICIOUS_EXIT=$? IMUNIFY_INFECTED=0 # FIXED Issue 4A: Distinguish timeout from other errors case "$IMUNIFY_MALICIOUS_EXIT" in 0) # Success - validate the output and count lines if [ -n "$malicious_output" ]; then # Check if first line looks like header (contains "Path", "ID", "Threat", etc.) first_line=$(echo "$malicious_output" | head -1) if [[ "$first_line" == *"Path"* ]] || [[ "$first_line" == *"ID"* ]] || [[ "$first_line" == *"Threat"* ]]; then IMUNIFY_INFECTED=$(echo "$malicious_output" | tail -n +2 | wc -l) else IMUNIFY_INFECTED=$(echo "$malicious_output" | wc -l) fi # Ensure it's numeric if ! [[ "$IMUNIFY_INFECTED" =~ ^[0-9]+$ ]]; then IMUNIFY_INFECTED=0 fi fi ;; 124) log_message "WARNING: ImunifyAV malicious list command timed out after 60s" IMUNIFY_INFECTED=0 ;; *) log_message "WARNING: Failed to get ImunifyAV malicious count (exit: $IMUNIFY_MALICIOUS_EXIT)" IMUNIFY_INFECTED=0 ;; esac SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo " ✓ ImunifyAV scan complete" echo " ⏱️ Duration: ${DURATION}s" echo "" echo "✓ ImunifyAV scan complete - Found: $IMUNIFY_INFECTED | Duration: ${DURATION}s" | tee -a "$SUMMARY_FILE" log_message "ImunifyAV: Scan complete - $IMUNIFY_INFECTED malicious files in ${DURATION}s" ;; clamav) SCAN_START=$(date +%s) if command -v freshclam &>/dev/null; then log_message "ClamAV: Updating signatures" if ! freshclam &>> "$LOG_DIR/clamav.log"; then log_message "WARNING: ClamAV signature update failed (continuing with existing signatures)" echo "⚠️ WARNING: Signature update failed, using existing signatures" fi fi log_message "ClamAV: Starting scan with activity monitoring" echo "" echo " 📁 Scanning path(s): ${SCAN_PATHS[@]}" echo " ⏳ Scanner: ClamAV (comprehensive virus scan...)" echo "" # ClamAV returns 1 if infected files found, 0 if clean, >1 for errors # Run in background with timeout (2 hours) and activity monitoring timeout 7200 clamscan --infected --recursive "${SCAN_PATHS[@]}" &>> "$LOG_DIR/clamav.log" & CLAM_PID=$! # Monitor activity by watching log file growth last_size=0 last_filename="" stall_counter=0 while kill -0 "$CLAM_PID" 2>/dev/null; do # Get current log size and file count from log if [ -f "$LOG_DIR/clamav.log" ]; then # FIXED Issue 5B: Improved error handling for stat current_size=$(stat -c%s "$LOG_DIR/clamav.log" 2>/dev/null) if [ -z "$current_size" ]; then current_size=0 fi # Try to get current file being scanned (FIXED Issue 5A: simpler, more robust pattern) current_file=$(grep -oE '\./[^ ]+|/[^ ]+' "$LOG_DIR/clamav.log" 2>/dev/null | tail -1) if [ -n "$current_file" ]; then filename=$(basename "$current_file" 2>/dev/null || echo "...") # Only update display when filename changes if [ "$filename" != "$last_filename" ]; then elapsed=$(($(date +%s) - SCAN_START)) printf "\r Scanning: %s | Elapsed: %s " \ "${filename:0:50}" "$(format_time "$elapsed")" last_filename="$filename" fi fi # Check for stalled scan (no log growth in 60 seconds) if [ "$current_size" -eq "$last_size" ]; then stall_counter=$((stall_counter + 1)) if [ "$stall_counter" -eq 300 ]; then # 60 seconds (300 * 0.2s) - log only once log_message "WARNING: ClamAV scan appears stalled (no activity for 60s)" fi else stall_counter=0 fi last_size=$current_size fi sleep 0.2 done # Wait for scan to complete and get exit code if [ -n "$CLAM_PID" ] && kill -0 "$CLAM_PID" 2>/dev/null; then wait "$CLAM_PID" CLAM_EXIT=$? else log_message "ERROR: ClamAV PID not found (scan process may have exited prematurely)" CLAM_EXIT=1 fi echo "" # New line after spinner if [ "$CLAM_EXIT" -eq 124 ]; then log_message "ERROR: ClamAV scan timed out after 2 hours" echo " ⏱️ Scan timed out (exceeded 2 hour limit)" echo "ClamAV scan timed out" >> "$SUMMARY_FILE" SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo "" continue elif [ "$CLAM_EXIT" -gt 1 ]; then log_message "ERROR: ClamAV scan failed with exit code $CLAM_EXIT" echo " ✗ Scan failed (exit code: $CLAM_EXIT) - check logs" echo "ClamAV scan failed (exit code: $CLAM_EXIT)" >> "$SUMMARY_FILE" SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo "" continue fi # Extract infected files (FIXED: acceptable current approach) grep "FOUND" "$LOG_DIR/clamav.log" 2>/dev/null | cut -d: -f1 >> "$INFECTED_LIST" 2>/dev/null || true # Get scan stats from log (FIXED Issue 1B: robust number extraction independent of column position) CLAMAV_FILES_SCANNED=$(grep "Scanned files:" "$LOG_DIR/clamav.log" 2>/dev/null | tail -1 | grep -oE '[0-9]+' | head -1 || echo "0") CLAM_INFECTED=$(grep -c "FOUND" "$LOG_DIR/clamav.log" 2>/dev/null) || CLAM_INFECTED=0 # Validate numbers (ensure they're numeric) if ! [[ "$CLAMAV_FILES_SCANNED" =~ ^[0-9]+$ ]]; then CLAMAV_FILES_SCANNED=0 fi SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo " ✓ Scanned $CLAMAV_FILES_SCANNED files" echo " ⏱️ Duration: ${DURATION}s" echo "" echo "✓ ClamAV scan complete - Found: $CLAM_INFECTED | Duration: ${DURATION}s" | tee -a "$SUMMARY_FILE" log_message "ClamAV: Scan complete - $CLAM_INFECTED infected files in ${DURATION}s" ;; maldet) SCAN_START=$(date +%s) log_message "Maldet: Updating signatures" if ! maldet -u &>> "$LOG_DIR/maldet.log"; then log_message "WARNING: Maldet signature update failed (continuing with existing signatures)" echo "⚠️ WARNING: Signature update failed, using existing signatures" fi log_message "Maldet: Starting scan with live progress" echo "" echo " 📁 Scanning path(s): ${SCAN_PATHS[@]}" echo " ⏳ Scanner: Maldet/LMD (Linux-specific malware detection...)" echo "" # Scan each path individually with -a (scan-all) flag # Note: -a flag scans all files regardless of modification time # Cannot combine -a with -f (file-list), so we loop through paths MALDET_EXIT=0 MALDET_PIDS=() for path in "${SCAN_PATHS[@]}"; do if [ ! -d "$path" ]; then log_message "Maldet: Skipping non-existent path: $path" continue fi log_message "Maldet: Scanning $path with -a (all files)" # Run with -a (scan-all) for comprehensive scanning # Timeout after 2 hours per path, run in background for better progress tracking timeout 7200 maldet -b -a "$path" &>> "$LOG_DIR/maldet.log" & MALDET_PIDS+=($!) # Give scan a moment to start sleep 1 done # Wait for all maldet scans to complete and collect exit codes for pid in "${MALDET_PIDS[@]}"; do # Validate PID is numeric and non-zero before checking process if [ -n "$pid" ] && [ "$pid" -gt 0 ] && kill -0 "$pid" 2>/dev/null; then wait "$pid" exit_code=$? if [ "$exit_code" -ne 0 ]; then MALDET_EXIT=$exit_code fi fi done echo "" # New line after progress if [ "$MALDET_EXIT" -eq 124 ]; then log_message "ERROR: Maldet scan timed out after 2 hours" echo " ⏱️ Scan timed out (exceeded 2 hour limit)" echo "Maldet scan timed out" >> "$SUMMARY_FILE" SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo "" continue elif [ "$MALDET_EXIT" -ne 0 ]; then log_message "ERROR: Maldet scan failed with exit code $MALDET_EXIT" echo " ✗ Scan failed (exit code: $MALDET_EXIT) - check logs" echo "Maldet scan failed (exit code: $MALDET_EXIT)" >> "$SUMMARY_FILE" SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo "" continue fi # Extract scan results from event log (more reliable than parsing output) # Maldet logs to /usr/local/maldetect/logs/event_log # Use dynamic path search for portability across all platforms (FIXED Issue 2: comprehensive path discovery) local event_log="" # Search standard locations in order of likelihood for search_path in \ "/usr/local/maldetect/logs/event_log" \ "/opt/maldetect/logs/event_log" \ "/var/log/maldetect/event_log" \ "/var/lib/maldetect/logs/event_log"; do if [ -f "$search_path" ]; then event_log="$search_path" break fi done # Fallback: Search entire filesystem for event_log if standard paths not found if [ -z "$event_log" ] || [ ! -f "$event_log" ]; then event_log=$(find /usr/local/maldetect -name "event_log" -type f 2>/dev/null | head -1) fi if [ -z "$event_log" ] || [ ! -f "$event_log" ]; then event_log=$(find /opt -name "event_log" -type f 2>/dev/null | head -1) fi if [ -z "$event_log" ] || [ ! -f "$event_log" ]; then event_log=$(find /var -name "event_log" -type f 2>/dev/null | head -1) fi MALDET_FILES_SCANNED="0" MALDET_HITS="0" if [ -f "$event_log" ]; then # Use -E instead of -P for portability (BSD grep doesn't support -P) # FIXED Issue 2A: Robust parsing independent of format variations last_line=$(grep "scan completed" "$event_log" 2>/dev/null | tail -1) MALDET_FILES_SCANNED=$(echo "$last_line" | grep -oE '[0-9]+ files' 2>/dev/null | grep -oE '^[0-9]+' || echo "0") MALDET_HITS=$(echo "$last_line" | grep -oE '[0-9]+ (malware hits|malicious)' 2>/dev/null | grep -oE '^[0-9]+' || echo "0") fi # Validate numbers if ! [[ "$MALDET_FILES_SCANNED" =~ ^[0-9]+$ ]]; then MALDET_FILES_SCANNED=0 fi if ! [[ "$MALDET_HITS" =~ ^[0-9]+$ ]]; then MALDET_HITS=0 fi SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo " ✓ Scanned $MALDET_FILES_SCANNED files" echo " ⏱️ Duration: ${DURATION}s" echo "" echo "✓ Maldet scan complete - Found: ${MALDET_HITS:-0} | Duration: ${DURATION}s" | tee -a "$SUMMARY_FILE" log_message "Maldet: Scan complete - ${MALDET_HITS:-0} hits in ${DURATION}s" ;; rkhunter) SCAN_START=$(date +%s) log_message "RKHunter: Updating definitions" if ! rkhunter --update &>> "$LOG_DIR/rkhunter.log"; then log_message "WARNING: RKHunter update failed (continuing with existing definitions)" echo "⚠️ WARNING: Definition update failed, using existing definitions" fi log_message "RKHunter: Starting scan with live test display" echo "" echo " 🔍 System scan: Checking for rootkits, backdoors, exploits" echo " ⏳ Scanner: Rootkit Hunter (system-wide integrity check...)" echo "" # Run with timeout (30 minutes, RKHunter is usually fast) # Show test names as they run # FIXED Issue 3A: Capture output to variable FIRST to avoid subshell exit code loss output=$(timeout 1800 rkhunter --check --skip-keypress --report-warnings-only 2>&1) RKH_EXIT=$? # Log output and display progress (using process substitution to avoid subshell issues) while IFS= read -r line; do # Parse test names: "Checking for..." or "Testing..." if [[ "$line" =~ ^Checking\ for\ (.+)$ ]] || [[ "$line" =~ ^Testing\ (.+)$ ]]; then test_name="${BASH_REMATCH[1]:-}" [ -n "$test_name" ] && printf "\r → %-60s" "${test_name:0:60}" elif [[ "$line" =~ ^Scanning\ (.+)$ ]]; then scan_item="${BASH_REMATCH[1]:-}" [ -n "$scan_item" ] && printf "\r → Scanning: %-50s" "${scan_item:0:50}" fi done < <(echo "$output" | tee -a "$LOG_DIR/rkhunter.log") echo "" # New line after test display if [ "$RKH_EXIT" -eq 124 ]; then log_message "ERROR: RKHunter scan timed out after 30 minutes" echo " ⏱️ Scan timed out (exceeded 30 minute limit)" echo "RKHunter scan timed out" >> "$SUMMARY_FILE" SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo "" continue elif [ "$RKH_EXIT" -gt 1 ] && [ "$RKH_EXIT" -ne 127 ]; then log_message "WARNING: RKHunter scan completed with exit code $RKH_EXIT" echo " ⚠️ Scan completed with warnings (exit code: $RKH_EXIT)" fi # Extract warnings (FIXED Issue 3B: add numeric validation) RKH_WARNINGS=$(grep -c "Warning:" "$LOG_DIR/rkhunter.log" 2>/dev/null) || RKH_WARNINGS=0 if ! [[ "$RKH_WARNINGS" =~ ^[0-9]+$ ]]; then RKH_WARNINGS=0 fi # Extract any rootkits found (search for rootkit entries with found status) grep "Rootkit" "$LOG_DIR/rkhunter.log" 2>/dev/null | grep -i "found" >> "$INFECTED_LIST" 2>/dev/null || true SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) echo " ✓ System integrity check complete" echo " ⏱️ Duration: ${DURATION}s" echo "" echo "✓ RKHunter scan complete - Warnings: $RKH_WARNINGS | Duration: ${DURATION}s" | tee -a "$SUMMARY_FILE" log_message "RKHunter: Scan complete - $RKH_WARNINGS warnings in ${DURATION}s" ;; esac echo "" | tee -a "$SUMMARY_FILE" ((SCANNERS_COMPLETED++)) # Wait between scanners if [ "${SCANNERS_COMPLETED:-0}" -lt "$TOTAL_SCANNERS" ]; then echo "Waiting 3 seconds before next scanner..." sleep 3 fi done # Finalize report with consolidated summary { echo "==========================================" echo "Scan Session Complete" echo "Completed: $(date)" echo "==========================================" echo "" # Consolidated Scanner Results Table echo "SCANNER RESULTS SUMMARY:" echo "────────────────────────────────────────" # ImunifyAV results if echo "${AVAILABLE_SCANNERS[*]}" | grep -q "imunify"; then IMUNIFY_COUNT=$(grep -o "ImunifyAV scan complete - Found: [0-9]*" "$SUMMARY_FILE" | grep -o "[0-9]*$" || echo "N/A") printf "%-20s %s\n" "ImunifyAV:" "$IMUNIFY_COUNT threats detected" fi # ClamAV results if echo "${AVAILABLE_SCANNERS[*]}" | grep -q "clamav"; then CLAM_COUNT=$(grep -o "ClamAV scan complete - Found: [0-9]*" "$SUMMARY_FILE" | grep -o "[0-9]*$" || echo "N/A") printf "%-20s %s\n" "ClamAV:" "$CLAM_COUNT infected files" fi # Maldet results if echo "${AVAILABLE_SCANNERS[*]}" | grep -q "maldet"; then printf "%-20s %s\n" "Maldet:" "Scan complete (check logs)" fi # RKHunter results if echo "${AVAILABLE_SCANNERS[*]}" | grep -q "rkhunter"; then RKH_COUNT=$(grep -o "RKHunter scan complete - Warnings: [0-9]*" "$SUMMARY_FILE" | grep -o "[0-9]*$" || echo "N/A") printf "%-20s %s\n" "Rootkit Hunter:" "$RKH_COUNT warnings" fi echo "────────────────────────────────────────" echo "" if [ -f "$INFECTED_LIST" ] && [ -s "$INFECTED_LIST" ]; then echo "⚠️ INFECTED FILES DETECTED:" echo "" sort -u "$INFECTED_LIST" echo "" echo "ACTION REQUIRED: Review and quarantine/remove infected files" echo "" # IP Reputation Integration: Feature temporarily disabled # TODO: IP flagging feature requires refactoring to fix subshell scope issues and undefined function dependencies # See audit report: CRITICAL-2, CRITICAL-3 for detailed findings # This feature will be re-enabled in a future update with proper implementation # # Disabled functionality: # - Would correlate infected files with Apache logs to find uploading IPs # - Would flag malicious IPs in reputation database # - Requires flag_ip_attack() function import and subshell scope fixes echo "" else echo "✓ No infected files detected by automated scan." echo "" echo "Review individual scanner logs for detailed information:" echo " • ImunifyAV: $LOG_DIR/imunify.log" echo " • ClamAV: $LOG_DIR/clamav.log" echo " • Maldet: $LOG_DIR/maldet.log" echo " • RKHunter: $LOG_DIR/rkhunter.log" fi } >> "$SUMMARY_FILE" # Validate scan results log_message "Validating scan results..." validation_issues=0 # Check that each scanner produced output for scanner in "${AVAILABLE_SCANNERS[@]}"; do case "$scanner" in imunify) if [ ! -s "$LOG_DIR/imunify.log" ]; then log_message "WARNING: ImunifyAV log file is empty or missing" echo "⚠️ WARNING: ImunifyAV scan may not have completed properly" >> "$SUMMARY_FILE" ((validation_issues++)) fi ;; clamav) if [ ! -s "$LOG_DIR/clamav.log" ]; then log_message "WARNING: ClamAV log file is empty or missing" echo "⚠️ WARNING: ClamAV scan may not have completed properly" >> "$SUMMARY_FILE" ((validation_issues++)) else # Verify ClamAV reached the summary line if ! grep -q "Scanned files:" "$LOG_DIR/clamav.log"; then log_message "WARNING: ClamAV scan may have been interrupted (no summary found)" echo "⚠️ WARNING: ClamAV scan may have been interrupted" >> "$SUMMARY_FILE" ((validation_issues++)) fi fi ;; maldet) if [ ! -s "$LOG_DIR/maldet.log" ]; then log_message "WARNING: Maldet log file is empty or missing" echo "⚠️ WARNING: Maldet scan may not have completed properly" >> "$SUMMARY_FILE" ((validation_issues++)) fi ;; rkhunter) if [ ! -s "$LOG_DIR/rkhunter.log" ]; then log_message "WARNING: RKHunter log file is empty or missing" echo "⚠️ WARNING: RKHunter scan may not have completed properly" >> "$SUMMARY_FILE" ((validation_issues++)) fi ;; esac done if [ $validation_issues -eq 0 ]; then log_message "All scans completed successfully - validation passed" echo "" >> "$SUMMARY_FILE" echo "✓ Scan Validation: All scanners completed successfully" >> "$SUMMARY_FILE" else log_message "WARNING: $validation_issues validation issue(s) found - review logs carefully" echo "" >> "$SUMMARY_FILE" echo "⚠️ Scan Validation: $validation_issues issue(s) found - review logs" >> "$SUMMARY_FILE" fi # Generate client report automatically (inline to work in standalone scripts) log_message "Generating client-facing security report" # Check if function exists, if not generate inline if declare -f generate_client_report > /dev/null 2>&1; then generate_client_report "$SCAN_DIR" > /dev/null 2>&1 else # Inline client report generation for standalone scripts client_report_file="$RESULTS_DIR/client_report.txt" # Extract scan info (using safe delimiters to avoid injection) scan_date=$(grep "Started:" "$SUMMARY_FILE" | head -1 | sed 's|Started: ||' || echo "Unknown") scan_paths=$(sed -n '/^Paths:/,/^$/p' "$SUMMARY_FILE" | tail -n +2 | grep -v "^$" | tr '\n' ', ' | sed 's|, $||' || echo "$SYS_USER_HOME_BASE") # Analyze infected files for false positives real_threats_count=0 false_positives_list="" real_threats_list="" if [ -f "$RESULTS_DIR/infected_files.txt" ] && [ -s "$RESULTS_DIR/infected_files.txt" ]; then while IFS= read -r file; do if [[ "$file" =~ /logs?/.*\.(log|gz|bz2)$ ]] || \ [[ "$file" =~ /awstats/ ]] || \ [[ "$file" =~ /tmp/.*\.txt$ ]] || \ [[ "$file" =~ \.log\.[0-9]+$ ]]; then false_positives_list="${false_positives_list} • $file"$'\n' else real_threats_list="${real_threats_list}📁 $file"$'\n' ((real_threats_count++)) fi done < "$RESULTS_DIR/infected_files.txt" fi # Generate report { echo "MALWARE SCAN REPORT - $scan_date" echo "═══════════════════════════════════════════════════════════" echo "" echo "Scanned with: ImunifyAV, ClamAV, Linux Maldet, RKHunter" echo "" if [ "$real_threats_count" -eq 0 ]; then echo "RESULT: ✅ No malware found - your server is clean" else echo "RESULT: ⚠️ $real_threats_count infected file(s) detected" echo "" echo "INFECTED FILES:" echo "$real_threats_list" echo "NEXT STEPS:" echo " 1. Remove infected files immediately" echo " 2. Change all passwords" echo " 3. Update WordPress/plugins to latest versions" fi if [ -n "$false_positives_list" ]; then echo "" echo "───────────────────────────────────────────────────────────" echo "NOTE: Attack attempts were detected in your server logs." echo "These were successfully blocked. No action needed." fi echo "" echo "Scan ID: $(basename "$SCAN_DIR")" } > "$client_report_file" fi # Display completion clear echo "==========================================" echo -e "${GREEN}Malware Scan Complete!${NC}" echo "==========================================" echo "Session: $(basename "$SCAN_DIR")" echo "Completed: $(date)" echo "" echo "Results saved to:" echo " Summary: $SUMMARY_FILE" echo " Logs: $LOG_DIR/" echo "" echo -e "${CYAN}Client Report (copy/paste for tickets):${NC}" echo " $RESULTS_DIR/client_report.txt" echo "" # Show summary cat "$SUMMARY_FILE" echo "" echo "==========================================" echo "" echo -e "${CYAN}TIP:${NC} To view the client-friendly report:" echo " cat $RESULTS_DIR/client_report.txt" echo "" # Prompt for cleanup (RKHunter cleanup handled by trap) echo "" if confirm "Delete scan script? (Logs and results will be preserved)"; then log_message "User requested cleanup - deleting scan script" echo "" echo "Removing scan script..." rm -f "$SCAN_DIR/scan.sh" echo -e "${GREEN}✓ Scan script deleted${NC}" echo "" echo "Results preserved at: $SCAN_DIR" echo "" else log_message "User chose to keep scan script" fi echo "Scan script and results preserved at: $SCAN_DIR" echo "" echo "You can:" echo " • Review logs: ls $LOG_DIR" echo " • View summary: cat $SUMMARY_FILE" if [ -n "$SCAN_DIR" ] && [[ "$SCAN_DIR" != "/" ]]; then echo " • Delete scan directory manually: rm -rf \"$SCAN_DIR\"" fi echo "" echo "Press Ctrl+A then D to detach from this screen session," echo "or press Enter to open an interactive shell in this session..." echo "" read -t 30 -p "" /dev/null || true # Keep screen session alive with an interactive shell echo "" echo "Opening interactive shell. Type 'exit' to close this screen session." echo "" log_message "Scan session ended - opening interactive shell" exec bash STANDALONE_EOF # Replace placeholder with actual paths paths_declaration="SCAN_PATHS=(" for path in "${scan_paths[@]}"; do paths_declaration+="\"$path\" " done paths_declaration+=")" # Escape special characters for sed (handle /, \, &, |, $) # CRITICAL FIX: Must escape the delimiter (|) as well since we use it in the sed command escaped_paths=$(printf '%s\n' "$paths_declaration" | sed -e 's/[\/&|]/\\&/g') if ! sed -i "s|PLACEHOLDER_SCAN_PATHS|$escaped_paths|" "$session_dir/scan.sh"; then echo -e "${RED}ERROR: Failed to generate standalone scanner script${NC}" return 1 fi # Make executable chmod +x "$session_dir/scan.sh" # Check if screen is installed if ! command -v screen &>/dev/null; then echo -e "${YELLOW}Warning: 'screen' not installed${NC}" echo "" echo "Screen allows you to detach from the scan session." echo "" echo "Options:" echo " 1. Auto-install screen (recommended)" echo " 2. Use nohup fallback (run in background without screen)" echo " 3. Cancel" echo "" read -p "Select option: " screen_option case "$screen_option" in 1) echo "" echo "Installing screen..." if command -v yum &>/dev/null; then yum install -y screen elif command -v apt-get &>/dev/null; then apt-get update && apt-get install -y screen else echo -e "${RED}Unable to auto-install. Install manually: yum install screen${NC}" read -p "Press Enter to continue..." return 1 fi if ! command -v screen &>/dev/null; then echo -e "${RED}Installation failed${NC}" read -p "Press Enter to continue..." return 1 fi echo -e "${GREEN}✓ Screen installed successfully${NC}" echo "" ;; 2) # Use nohup fallback echo "" echo "Launching scan with nohup (background mode)..." nohup bash "$session_dir/scan.sh" > "$session_dir/logs/nohup.out" 2>&1 & scan_pid=$! sleep 1 if ps -p $scan_pid > /dev/null 2>&1; then echo "" echo -e "${GREEN}✓ Standalone scanner started successfully!${NC}" echo "" echo "Session ID: $session_id" echo "Process ID: $scan_pid" echo "Results directory: $session_dir/results/" echo "" echo -e "${CYAN}Monitor the scan:${NC}" echo " tail -f $session_dir/logs/session.log" echo "" echo -e "${CYAN}Check if still running:${NC}" echo " ps -p $scan_pid" echo "" echo -e "${GREEN}You can now safely delete the toolkit.${NC}" echo -e "${GREEN}The scan will continue running independently.${NC}" echo "" # Store session info in reference database store_reference "malware_standalone_latest" "$session_id" store_reference "malware_standalone_${session_id}_dir" "$session_dir" store_reference "malware_standalone_${session_id}_pid" "$scan_pid" read -p "Press Enter to continue..." return 0 else echo -e "${RED}Failed to start scan${NC}" echo "Run manually: bash $session_dir/scan.sh" read -p "Press Enter to continue..." return 1 fi ;; 3) echo "Cancelled." read -p "Press Enter to continue..." return 0 ;; *) echo -e "${RED}Invalid option${NC}" read -p "Press Enter to continue..." return 1 ;; esac fi # Launch in screen session echo "Launching scan in screen session..." screen -dmS "$session_id" bash "$session_dir/scan.sh" sleep 1 # Verify screen started (FIXED: use -F flag for literal matching) if screen -list | grep -qF "$session_id"; then echo "" echo -e "${GREEN}✓ Standalone scanner started successfully!${NC}" echo "" echo "Session ID: $session_id" echo "Screen session: $session_id" echo "Results directory: $session_dir/results/" echo "" echo -e "${CYAN}Monitor the scan:${NC}" echo " screen -r $session_id" echo "" echo -e "${CYAN}Check progress:${NC}" echo " tail -f $session_dir/logs/session.log" echo "" echo -e "${CYAN}Detach from screen:${NC}" echo " Press: Ctrl+A then D" echo "" echo -e "${GREEN}You can now safely delete the toolkit.${NC}" echo -e "${GREEN}The scan will continue running independently.${NC}" echo "" # Store session info in reference database store_reference "malware_standalone_latest" "$session_id" store_reference "malware_standalone_${session_id}_dir" "$session_dir" else echo -e "${RED}Failed to start screen session${NC}" echo "Run manually: bash $session_dir/scan.sh" fi read -p "Press Enter to continue..." } # Compare results from multiple scanners compare_scan_results() { echo "" print_header "Compare Scanner Results" # Get latest multiscan session local latest_session=$(get_reference "malware_multiscan_latest") if [ -z "$latest_session" ]; then echo "No multi-scanner sessions found." echo "" echo "Run a scan with 'All Available Scanners' option first." read -p "Press Enter to continue..." return fi local report_file=$(get_reference "malware_multiscan_${latest_session}") if [ -f "$report_file" ]; then echo "Latest multi-scanner session: $latest_session" echo "" less "$report_file" else echo "Report file not found: $report_file" fi echo "" read -p "Press Enter to continue..." } # Launch standalone scanner menu launch_standalone_scanner_menu() { local preset_scope="$1" # Optional: server, user, domain, custom echo "" print_header "Launch Standalone Scanner" echo "This will create a self-contained scanner in /opt/ that runs" echo "independently. You can safely delete the toolkit after launching." echo "" if ! detect_control_panel; then read -p "Press Enter to continue..." return 1 fi # Validate that docroots were found if [ ${#sanitized_docroot[@]} -eq 0 ]; then echo -e "${YELLOW}WARNING: No docroots found on this system${NC}" echo "You can still:" echo " 1. Scan specific paths manually (custom path option)" echo " 2. Scan from root (entire server)" echo "" fi echo "Control Panel: ${CONTROL_PANEL^}" echo "Available Scanners: ${available_scanners[@]}" echo "" local scope_choice local scan_paths=() local scan_description="" # If preset scope provided, use it; otherwise show menu if [ -n "$preset_scope" ]; then case "$preset_scope" in server) scope_choice=1 ;; all_users) scope_choice=2 ;; user) scope_choice=3 ;; domain) scope_choice=4 ;; custom) scope_choice=5 ;; *) scope_choice=0 ;; esac else echo "Select scan scope:" echo " 1. Entire server (scan from / - WARNING: may take several hours)" echo " 2. Specific user account" echo " 3. Specific domain" echo " 4. Custom path" echo " 0. Cancel" echo "" read -p "Select option: " scope_choice fi case $scope_choice in 1) # Entire server scan_paths=("/") scan_description="full server scan" echo "" echo -e "${YELLOW}WARNING: Full server scan from /${NC}" echo "This will scan the ENTIRE filesystem including:" echo " • All user directories" echo " • System files" echo " • Application files" echo "" echo "This scan may take several hours and use significant resources." echo "" read -p "Are you sure you want to proceed? (yes/no): " confirm_full_scan if [ "$confirm_full_scan" != "yes" ]; then echo "Cancelled." read -p "Press Enter to continue..." return 0 fi echo "" echo "Scan scope: Entire server from /" ;; 2) # All user accounts echo "" echo "Scanning all user home directories..." # Determine user directories based on control panel # Each panel has different home directory structures scan_paths=() case "$CONTROL_PANEL" in plesk) # Plesk: /var/www/vhosts/username/ (exclude 'system' subdirectory) # Find all user vhosts (skip 'system' which contains configs) while IFS= read -r vhost_dir; do [ -n "$vhost_dir" ] && [ -d "$vhost_dir" ] && scan_paths+=("$vhost_dir") done < <(find /var/www/vhosts -maxdepth 1 -type d -name "[a-zA-Z0-9]*" ! -name "system" ! -name "." 2>/dev/null | sort) scan_description="all Plesk user vhosts (excluding system configs)" ;; cpanel) # cPanel: /home/username/ (standard) while IFS= read -r home_dir; do [ -n "$home_dir" ] && [ -d "$home_dir" ] && scan_paths+=("$home_dir") done < <(find /home -maxdepth 1 -type d ! -name "." ! -name ".." ! -name "lost+found" 2>/dev/null | sort) scan_description="all cPanel user home directories" ;; interworx) # InterWorx: /home/username/domain.com/html # Can also scan /home/username for all user content while IFS= read -r user_dir; do [ -n "$user_dir" ] && [ -d "$user_dir" ] && scan_paths+=("$user_dir") done < <(find /home -maxdepth 1 -type d ! -name "." ! -name ".." 2>/dev/null | sort) scan_description="all InterWorx user directories" ;; *) # Standalone: /home/username/ while IFS= read -r home_dir; do [ -n "$home_dir" ] && [ -d "$home_dir" ] && scan_paths+=("$home_dir") done < <(find /home -maxdepth 1 -type d ! -name "." ! -name ".." ! -name "lost+found" 2>/dev/null | sort) scan_description="all user home directories" ;; esac # Check if any paths were found if [ ${#scan_paths[@]} -eq 0 ]; then echo -e "${YELLOW}Warning: No user directories found${NC}" read -p "Press Enter to continue..." return 1 fi echo "Control Panel: ${CONTROL_PANEL^}" echo "User directories found: ${#scan_paths[@]}" echo "Scan scope: $scan_description" ;; 3) # Specific user echo "" echo "Available users:" select_user_interactive "Select user account to scan" if [ -z "$SELECTED_USER" ]; then echo "No user selected." read -p "Press Enter to continue..." return 1 fi # Get user's docroots (FIXED: more specific path matching to avoid false matches like 'test' matching 'username_test') for docroot in "${sanitized_docroot[@]}"; do # Match patterns: /home/username/ or /var/www/vhosts/username/ or /chroot/home/username/ if [[ "$docroot" == */home/$SELECTED_USER/* ]] || [[ "$docroot" == */vhosts/$SELECTED_USER/* ]] || [[ "$docroot" == */chroot/home/$SELECTED_USER/* ]]; then scan_paths+=("$docroot") fi done if [ ${#scan_paths[@]} -eq 0 ]; then echo -e "${RED}No docroots found for user: $SELECTED_USER${NC}" read -p "Press Enter to continue..." return 1 fi scan_description="user $SELECTED_USER" echo "Found ${#scan_paths[@]} docroots for $SELECTED_USER" ;; 4) # Specific domain echo "" read -p "Enter domain name: " domain if [ -z "$domain" ]; then echo "No domain entered." read -p "Press Enter to continue..." return 1 fi # Find docroot for domain (FIXED: more specific matching to distinguish 'example' from 'example-prod' or 'prefix_example') for docroot in "${sanitized_docroot[@]}"; do # Match patterns: domain.com/html or domain.com/public_html or /domain.com/httpdocs if [[ "$docroot" == *"/$domain/"* ]] || [[ "$docroot" == *"/$domain"* ]]; then scan_paths+=("$docroot") fi done if [ ${#scan_paths[@]} -eq 0 ]; then echo -e "${RED}No docroot found for domain: $domain${NC}" read -p "Press Enter to continue..." return 1 fi scan_description="domain $domain" echo "Found docroot: ${scan_paths[0]}" ;; 5) # Custom path echo "" read -p "Enter path to scan: " custom_path if [ -z "$custom_path" ]; then echo "No path entered." read -p "Press Enter to continue..." return 1 fi if [ ! -d "$custom_path" ]; then echo -e "${RED}Path does not exist: $custom_path${NC}" read -p "Press Enter to continue..." return 1 fi scan_paths=("$custom_path") scan_description="custom path $custom_path" ;; 0) return 0 ;; *) echo -e "${RED}Invalid option${NC}" sleep 1 return 1 ;; esac # Confirm before generating echo "" echo -e "${YELLOW}Ready to generate standalone scanner${NC}" echo "Scope: $scan_description" echo "Paths: ${#scan_paths[@]}" echo "Scanners: ${available_scanners[@]}" echo "" read -p "Generate and launch? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Cancelled." read -p "Press Enter to continue..." return 0 fi # Generate and launch standalone scanner generate_standalone_scanner "${scan_paths[@]}" } # Check status of all standalone scanners check_standalone_status() { echo "" print_header "Standalone Scanner Status" # Find all malware-* directories in /opt (proper array initialization to handle spaces in names) if [ ! -d /opt ]; then echo "ERROR: /opt directory not found or not accessible" read -p "Press Enter to continue..." return 1 fi local standalone_dirs=() while IFS= read -r dir; do [ -n "$dir" ] && standalone_dirs+=("$dir") done < <(find /opt -maxdepth 1 -type d -name "malware-*" 2>/dev/null | sort -r) if [ ${#standalone_dirs[@]} -eq 0 ]; then echo "No standalone scanner sessions found." echo "" read -p "Press Enter to continue..." return 0 fi echo "Active Sessions:" echo "" local running_count=0 local completed_count=0 local error_count=0 for dir in "${standalone_dirs[@]}"; do local session_name=$(basename "$dir") # Check if still running by looking for bash process executing scan.sh # Use pgrep with exact match to avoid false positives from viewers/editors if pgrep -f "bash $dir/scan.sh" > /dev/null 2>&1 || [ -f "$dir/.scan_running" ]; then echo -e " ${GREEN}●${NC} $session_name [RUNNING]" ((running_count++)) # Show progress if available if [ -f "$dir/logs/session.log" ]; then local last_log=$(tail -1 "$dir/logs/session.log" 2>/dev/null) echo " Latest: $last_log" fi elif [ -f "$dir/results/summary.txt" ]; then # Check if completed successfully if grep -q "Multi-Scanner Session Complete\|Scan session ended" "$dir/results/summary.txt" 2>/dev/null; then echo -e " ${CYAN}✓${NC} $session_name [COMPLETED]" ((completed_count++)) # Show infected count if available if [ -f "$dir/results/infected_files.txt" ] && [ -s "$dir/results/infected_files.txt" ]; then local infected_count=$(wc -l < "$dir/results/infected_files.txt") echo -e " Found: ${RED}$infected_count infected files${NC}" fi else echo -e " ${RED}✗${NC} $session_name [ERROR/INCOMPLETE]" ((error_count++)) fi else echo -e " ${YELLOW}?${NC} $session_name [UNKNOWN - no results yet]" fi echo "" done echo "Summary:" echo " Running: $running_count" echo " Completed: $completed_count" echo " Errors: $error_count" echo " Total: ${#standalone_dirs[@]}" echo "" read -p "Press Enter to continue..." } # Delete standalone scanner sessions delete_standalone_sessions() { echo "" print_header "Delete Standalone Scanner Sessions" # Find all malware-* directories in /opt (proper array initialization to handle spaces in names) if [ ! -d /opt ]; then echo "ERROR: /opt directory not found or not accessible" read -p "Press Enter to continue..." return 1 fi local standalone_dirs=() while IFS= read -r dir; do [ -n "$dir" ] && standalone_dirs+=("$dir") done < <(find /opt -maxdepth 1 -type d -name "malware-*" 2>/dev/null | sort -r) if [ ${#standalone_dirs[@]} -eq 0 ]; then echo "No standalone scanner sessions found." echo "" read -p "Press Enter to continue..." return 0 fi echo "Available sessions:" echo "" # List sessions with status local i=1 for dir in "${standalone_dirs[@]}"; do local session_name=$(basename "$dir") local status="completed" if pgrep -f "bash $dir/scan.sh" > /dev/null 2>&1 || [ -f "$dir/.scan_running" ]; then status="${GREEN}running${NC}" fi echo -e " $i. $session_name [$status]" ((i++)) done echo "" echo " A. Delete all completed sessions" echo " 0. Cancel" echo "" read -p "Select session to delete (or A for all completed): " delete_choice case "$delete_choice" in 0) return 0 ;; [Aa]) # Delete all completed sessions echo "" local deleted=0 for dir in "${standalone_dirs[@]}"; do if ! pgrep -f "$dir/scan.sh" > /dev/null 2>&1; then echo "Deleting: $(basename "$dir")" rm -rf "$dir" ((deleted++)) fi done echo "" echo -e "${GREEN}✓ Deleted $deleted completed session(s)${NC}" ;; *) # Delete specific session # Validate numeric input if ! [[ "$delete_choice" =~ ^[0-9]+$ ]]; then echo -e "${RED}Invalid choice (must be a number)${NC}" read -p "Press Enter to continue..." return 1 fi if [ "$delete_choice" -lt 1 ] || [ "$delete_choice" -gt ${#standalone_dirs[@]} ]; then echo -e "${RED}Invalid choice (out of range)${NC}" read -p "Press Enter to continue..." return 1 fi local selected_dir="${standalone_dirs[$((delete_choice-1))]}" local session_name=$(basename "$selected_dir") # Check if running if pgrep -f "$selected_dir/scan.sh" > /dev/null 2>&1; then echo "" echo -e "${YELLOW}Warning: This scan is currently running!${NC}" read -p "Stop scan and delete? (yes/no): " confirm_running if [ "$confirm_running" = "yes" ]; then pkill -f "$selected_dir/scan.sh" sleep 1 rm -rf "$selected_dir" echo -e "${GREEN}✓ Stopped and deleted: $session_name${NC}" else echo "Cancelled." fi else echo "" read -p "Delete $session_name? (yes/no): " confirm_delete if [ "$confirm_delete" = "yes" ]; then rm -rf "$selected_dir" echo -e "${GREEN}✓ Deleted: $session_name${NC}" else echo "Cancelled." fi fi ;; esac echo "" read -p "Press Enter to continue..." } # Main scan menu # Maldet-specific scan menu (dedicated section for fastest scanner) maldet_scan_submenu() { while true; do echo "" print_header "Maldet Scanner - Linux Malware Detection" echo "Fast, efficient, Linux-specific malware detection" echo "" # Show installation status if is_maldet_installed; then echo -e "${GREEN}✓ Status: Installed${NC}" else echo -e "${RED}✗ Status: NOT installed${NC}" fi echo "" echo "Select option:" echo -e " ${CYAN}1.${NC} Scan entire server (fastest comprehensive scan)" echo -e " ${CYAN}2.${NC} Scan all user accounts" echo -e " ${CYAN}3.${NC} Scan specific user account" echo -e " ${CYAN}4.${NC} Scan specific domain" echo -e " ${CYAN}5.${NC} Scan custom path" echo "" echo -e " ${CYAN}6.${NC} Update Maldet signatures" echo -e " ${CYAN}7.${NC} View Maldet results" echo -e " ${CYAN}8.${NC} Install Maldet" echo "" echo -e " ${RED}0.${NC} Back to main menu" echo "" while true; do read -p "Select option (0-8): " choice if ! [[ "$choice" =~ ^[0-8]$ ]]; then echo -e "${RED}Invalid option${NC}" sleep 1 continue fi case $choice in 1) maldet_launch_scan "server"; break ;; 2) maldet_launch_scan "all_users"; break ;; 3) maldet_launch_scan "user"; break ;; 4) maldet_launch_scan "domain"; break ;; 5) maldet_launch_scan "custom"; break ;; 6) maldet_update_signatures; break ;; 7) maldet_view_results; break ;; 8) install_maldet_only; break ;; 0) return 0 ;; esac done done } # Launch Maldet-specific scan with different scope options maldet_launch_scan() { local scope="$1" echo "" print_header "Launching Maldet Scan - $scope" # Check if Maldet is installed if ! is_maldet_installed; then echo -e "${RED}✗ Maldet is not installed${NC}" echo "" read -p "Install Maldet now? (yes/no): " install_choice if [ "$install_choice" = "yes" ]; then install_all_scanners maldet_scan_submenu fi return 1 fi # Find Maldet binary local maldet_bin=$(command -v maldet || find /usr/local -name maldet -type f 2>/dev/null | head -1) if [ -z "$maldet_bin" ]; then echo -e "${RED}✗ Maldet binary not found${NC}" read -p "Press Enter to continue..." return 1 fi echo "" echo "Creating Maldet-only scan session..." echo "Scope: $scope" echo "" # For now, launch via the existing scanner menu but only with Maldet # Store preference for Maldet-only scanning export MALDET_ONLY=1 launch_standalone_scanner_menu "$scope" unset MALDET_ONLY } # Update Maldet signatures maldet_update_signatures() { echo "" print_header "Updating Maldet Signatures" # Check if Maldet is installed if ! is_maldet_installed; then echo -e "${RED}✗ Maldet is not installed${NC}" echo "" read -p "Install Maldet now? (yes/no): " install_choice if [ "$install_choice" = "yes" ]; then install_all_scanners fi return 1 fi local maldet_bin=$(command -v maldet || find /usr/local -name maldet -type f 2>/dev/null | head -1) if [ -z "$maldet_bin" ]; then echo -e "${RED}✗ Maldet binary not found${NC}" read -p "Press Enter to continue..." return 1 fi echo "Updating Maldet malware signatures..." echo "(This may take a few moments)" echo "" if timeout 120 "$maldet_bin" -u 2>&1 | tee /tmp/maldet-update.log | grep -E "updated|completed|signatures"; then echo "" echo -e "${GREEN}✓ Signatures updated successfully${NC}" else echo "" echo -e "${YELLOW}⚠ Signature update may have completed (check output above)${NC}" fi echo "" read -p "Press Enter to continue..." } # View Maldet-specific results maldet_view_results() { echo "" print_header "Maldet Scan Results" if ! is_maldet_installed; then echo -e "${RED}✗ Maldet is not installed${NC}" echo "" read -p "Press Enter to continue..." return 1 fi local maldet_bin=$(command -v maldet || find /usr/local -name maldet -type f 2>/dev/null | head -1) if [ -z "$maldet_bin" ]; then echo -e "${RED}✗ Maldet binary not found${NC}" read -p "Press Enter to continue..." return 1 fi echo "Recent Maldet scans:" echo "" if "$maldet_bin" -l 2>/dev/null | head -20; then echo "" else echo "No Maldet scans found" echo "" fi read -p "Press Enter to continue..." } show_scan_menu() { # Ensure print_banner is available before calling it if ! declare -f "print_banner" &>/dev/null; then echo "ERROR: print_banner function not found" >&2 return 1 fi # Ensure reference database is fresh (only rebuild if > 1 hour old) if command -v db_ensure_fresh &>/dev/null; then db_ensure_fresh 2>/dev/null || true clear fi while true; do # Call print_banner - MUST succeed print_banner "Malware Scanner" || { echo "ERROR: print_banner failed" >&2 return 1 } echo "Available Scanners:" if [ ${#available_scanners[@]} -eq 0 ]; then echo " (None currently installed)" else for scanner in "${available_scanners[@]}"; do echo " • ${scanner^}" done fi echo "" echo -e "${CYAN}Maldet Scanner (Fast, Linux-focused):${NC}" echo -e " ${CYAN}1.${NC} Maldet menu (dedicated scanner)" echo "" echo -e "${CYAN}Create New Scan (All Scanners):${NC}" echo -e " ${CYAN}2.${NC} Scan entire server (ClamAV, Maldet, RKHunter)" echo -e " ${CYAN}3.${NC} Scan all user accounts (All scanners - recommended)" echo -e " ${CYAN}4.${NC} Scan specific user account (All scanners)" echo -e " ${CYAN}5.${NC} Scan specific domain (All scanners)" echo -e " ${CYAN}6.${NC} Scan custom path (All scanners)" echo "" echo -e "${CYAN}Monitor & Manage:${NC}" echo -e " ${CYAN}7.${NC} Check scan status" echo -e " ${CYAN}8.${NC} View scan results" echo -e " ${CYAN}9.${NC} Delete scan sessions" echo "" echo -e "${CYAN}Configuration:${NC}" echo -e " ${CYAN}10.${NC} Install Maldet (fast, Linux-specific)" echo -e " ${CYAN}11.${NC} Install ClamAV (open source antivirus)" echo -e " ${CYAN}12.${NC} Install RKHunter (rootkit detection)" echo -e " ${CYAN}13.${NC} Install ALL scanners (recommended)" echo -e " ${CYAN}14.${NC} Scanner settings" echo "" echo -e " ${RED}0.${NC} Back" echo "" # Validate choice input with retry loop while true; do read -p "Select option (0-14): " choice if ! [[ "$choice" =~ ^([0-9]|1[0-4])$ ]]; then echo -e "${RED}Invalid option${NC}" sleep 1 continue fi case $choice in 1) maldet_scan_submenu; break ;; 2) launch_standalone_scanner_menu "server"; break ;; 3) launch_standalone_scanner_menu "all_users"; break ;; 4) launch_standalone_scanner_menu "user"; break ;; 5) launch_standalone_scanner_menu "domain"; break ;; 6) launch_standalone_scanner_menu "custom"; break ;; 7) check_standalone_status; break ;; 8) view_scan_results; break ;; 9) delete_standalone_sessions; break ;; 10) install_maldet_only; break ;; 11) install_clamav_only; break ;; 12) install_rkhunter_only; break ;; 13) install_all_scanners; break ;; 14) scanner_settings; break ;; 0) return 0 ;; esac done done } # View scan results view_scan_results() { echo "" print_header "Scan Results" echo "Select results to view:" echo " 1. Toolkit scan results" echo " 2. Standalone scanner results (/opt)" echo " 0. Back" echo "" read -p "Option: " result_type case "$result_type" in 1) # Toolkit scan results echo "" echo "Select scanner to view results:" local i=1 for scanner in "${available_scanners[@]}"; do echo " $i. ${scanner^}" ((i++)) done echo "" read -p "Scanner: " scanner_choice # Validate numeric input if ! [[ "$scanner_choice" =~ ^[0-9]+$ ]]; then echo -e "${RED}Invalid choice (must be a number)${NC}" read -p "Press Enter to continue..." return 1 fi if [ "$scanner_choice" -lt 1 ] || [ "$scanner_choice" -gt ${#available_scanners[@]} ]; then echo -e "${RED}Invalid choice (out of range)${NC}" read -p "Press Enter to continue..." return 1 fi local selected_scanner="${available_scanners[$((scanner_choice-1))]}" echo "" case "$selected_scanner" in imunify) echo "Recent ImunifyAV scans:" imunify-antivirus malware on-demand list --since $(date --date="7 days ago" '+%s') 2>/dev/null || echo "No scans found" ;; clamav) echo "Recent ClamAV scans:" find "$SCRIPT_DIR/logs/malware-scans" -name "clamav_*.log" -mtime -7 2>/dev/null | sort -r | head -5 || echo "No scans found" ;; maldet) echo "Recent Maldet scans:" maldet -l 2>/dev/null || echo "No scans found" ;; esac ;; 2) # Standalone scanner results echo "" echo "Standalone scanner sessions:" echo "" # Find all malware-* directories in /opt local standalone_dirs=($(find /opt -maxdepth 1 -type d -name "malware-*" 2>/dev/null | sort -r)) if [ ${#standalone_dirs[@]} -eq 0 ]; then echo "No standalone scanner sessions found in /opt" echo "" read -p "Press Enter to continue..." return 0 fi # List sessions local i=1 for dir in "${standalone_dirs[@]}"; do local session_name=$(basename "$dir") local scan_date=$(echo "$session_name" | sed 's/malware-//') # Check if still running local status="completed" if pgrep -f "$dir/scan.sh" > /dev/null 2>&1; then status="running" fi echo " $i. $session_name [$status]" ((i++)) done echo "" read -p "Select session (or 0 to cancel): " session_choice # Validate numeric input if ! [[ "$session_choice" =~ ^[0-9]+$ ]]; then echo -e "${RED}Invalid choice (must be a number)${NC}" read -p "Press Enter to continue..." return 1 fi if [ "$session_choice" = "0" ]; then return 0 fi if [ "$session_choice" -lt 1 ] || [ "$session_choice" -gt ${#standalone_dirs[@]} ]; then echo -e "${RED}Invalid choice (out of range)${NC}" read -p "Press Enter to continue..." return 1 fi local selected_dir="${standalone_dirs[$((session_choice-1))]}" echo "" echo "Session: $(basename $selected_dir)" echo "Location: $selected_dir" echo "" # Show results if [ -f "$selected_dir/results/summary.txt" ]; then echo "=== Summary ===" cat "$selected_dir/results/summary.txt" echo "" else echo "Summary not yet available (scan may still be running)" echo "" fi # Show infected files if any if [ -f "$selected_dir/results/infected_files.txt" ] && [ -s "$selected_dir/results/infected_files.txt" ]; then echo "=== Infected Files ===" cat "$selected_dir/results/infected_files.txt" echo "" fi # Show recent log entries if [ -f "$selected_dir/logs/session.log" ]; then echo "=== Recent Log Entries ===" tail -20 "$selected_dir/logs/session.log" echo "" fi echo "View full logs:" echo " tail -f $selected_dir/logs/session.log" echo "" # Offer to generate client report echo -e "${CYAN}Actions:${NC}" echo " 1. Generate client-facing security report" echo " 0. Back to menu" echo "" read -p "Select action (or press Enter to continue): " action_choice case "$action_choice" in 1) generate_client_report "$selected_dir" ;; 0|"") # Continue ;; *) echo -e "${RED}Invalid option${NC}" ;; esac ;; 0) return 0 ;; *) echo -e "${RED}Invalid option${NC}" ;; esac echo "" read -p "Press Enter to continue..." } # Scanner settings scanner_settings() { echo "" print_header "Scanner Settings" echo "Settings (placeholder for future enhancements):" echo " • Auto-quarantine infected files" echo " • Email notifications" echo " • Scheduled scans" echo " • Custom exclusions" echo "" echo "Coming soon..." read -p "Press Enter to continue..." } # Generate client-facing security report generate_client_report() { local scan_dir="$1" if [ ! -d "$scan_dir" ]; then echo -e "${RED}Scan directory not found${NC}" return 1 fi local summary_file="$scan_dir/results/summary.txt" local infected_file="$scan_dir/results/infected_files.txt" local clamav_log="$scan_dir/logs/clamav.log" local session_log="$scan_dir/logs/session.log" local report_file="$scan_dir/results/client_report.txt" if [ ! -f "$summary_file" ]; then echo -e "${RED}Summary file not found - scan may not be complete${NC}" return 1 fi # Extract scan info local session_name=$(basename "$scan_dir") local scan_date=$(grep "Started:" "$summary_file" | head -1 | sed 's/Started: //') local scan_paths=$(sed -n '/^Paths:/,/^$/p' "$summary_file" | tail -n +2 | grep -v "^$" | tr '\n' ', ' | sed 's/, $//') # Count threats local total_threats=0 local imunify_count=$(grep -o "ImunifyAV:.*[0-9]* threats" "$summary_file" | grep -o "[0-9]*" || echo "0") local clamav_count=$(grep -o "ClamAV:.*[0-9]* infected" "$summary_file" | grep -o "[0-9]*" || echo "0") local maldet_hits=$(grep -o "Maldet:.*[0-9]* hits" "$summary_file" | grep -o "[0-9]*" || echo "0") # Calculate total (only real malware, not rootkit warnings) total_threats=$((imunify_count + clamav_count + maldet_hits)) # Analyze infected files for false positives local real_threats=() local false_positives=() if [ -f "$infected_file" ] && [ -s "$infected_file" ]; then while IFS= read -r file; do # Check if likely false positive (logs, stats, cache) if [[ "$file" =~ /logs?/.*\.(log|gz|bz2)$ ]] || \ [[ "$file" =~ /awstats/ ]] || \ [[ "$file" =~ /tmp/.*\.txt$ ]] || \ [[ "$file" =~ \.log\.[0-9]+$ ]]; then false_positives+=("$file") else real_threats+=("$file") fi done < "$infected_file" fi # Generate report { echo "MALWARE SCAN REPORT - $scan_date" echo "═══════════════════════════════════════════════════════════" echo "" echo "Scanned with: ImunifyAV, ClamAV, Linux Maldet, RKHunter" echo "" if [ ${#real_threats[@]} -eq 0 ]; then echo "RESULT: ✅ No malware found - your server is clean" else echo "RESULT: ⚠️ ${#real_threats[@]} infected file(s) detected" echo "" echo "INFECTED FILES:" for file in "${real_threats[@]}"; do echo " • $file" done echo "" echo "NEXT STEPS:" echo " 1. Remove infected files immediately" echo " 2. Change all passwords" echo " 3. Update WordPress/plugins to latest versions" fi if [ ${#false_positives[@]} -gt 0 ]; then echo "" echo "───────────────────────────────────────────────────────────" echo "NOTE: Attack attempts were detected in your server logs." echo "These were successfully blocked. No action needed." fi echo "" echo "Scan ID: $session_name" } > "$report_file" # Display the report echo "" print_header "Client Security Report Generated" echo "" cat "$report_file" echo "" echo -e "${GREEN}Report saved to:${NC} $report_file" echo "" echo "You can now copy/paste this report into your support ticket." echo "" } # Main execution main() { # Detect scanners (populate available_scanners array) # Don't exit if none found - menu option 9 allows installation detect_scanners || true # Verify show_scan_menu exists and is callable if ! declare -f "show_scan_menu" &>/dev/null; then echo "ERROR: show_scan_menu function not found" >&2 return 1 fi # Call the menu function show_scan_menu } # Run if executed directly if [ "${BASH_SOURCE[0]}" = "${0}" ]; then main "$@" fi