Files
Linux-Server-Management-Too…/modules/security/malware-scanner.sh
T
cschantz 90f1eaca05 Enhance: Dynamic Maldet version detection - checks all sources for newest available
Improvements:
- Uses curl -I to check which sources are reachable
- Queries GitHub API to get actual version tags
- Compares versions to determine best available release
- Prioritizes official releases (rfxn.com) when available
- Falls back to GitHub releases with version info
- Shows user which sources are reachable and which version will be downloaded
- Longer timeout (15s) for slower networks
2026-04-21 19:19:25 -04:00

3444 lines
129 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
# Function to compare semantic versions (e.g., 1.6.5 vs 1.6.4)
compare_versions() {
local v1="$1" v2="$2"
[ "$v1" = "$v2" ] && echo "equal" && return
local IFS=.
local i ver1=($v1) ver2=($v2)
for ((i=0; i<${#ver1[@]} || i<${#ver2[@]}; i++)); do
if ((10#${ver1[i]:-0} > 10#${ver2[i]:-0})); then
echo "greater"
return
elif ((10#${ver1[i]:-0} < 10#${ver2[i]:-0})); then
echo "less"
return
fi
done
echo "equal"
}
# Check available versions from multiple sources
local rfxn_version="" github_version="" github_api_version=""
local best_source="" best_version="" best_url=""
# Source 1: Check rfxn.com for available versions
echo " [1/3] Checking rfxn.com..."
local rfxn_check=$(curl -sI "https://www.rfxn.com/downloads/maldetect-latest.tar.gz" --connect-timeout 5 2>/dev/null | grep -E "HTTP|Content-Length")
if echo "$rfxn_check" | grep -q "200\|302"; then
rfxn_version="latest"
echo " ✓ Available (latest release)"
else
echo " ✗ Not reachable"
fi
# Source 2: Check GitHub releases API for version info
echo " [2/3] Checking GitHub releases..."
local github_api_data=$(curl -s "https://api.github.com/repos/rfxn/maldet/releases/latest" --connect-timeout 5 2>/dev/null)
if echo "$github_api_data" | grep -q '"tag_name"'; then
github_api_version=$(echo "$github_api_data" | grep -o '"tag_name":"[^"]*' | head -1 | cut -d'"' -f4 | sed 's/^v//')
if [ -n "$github_api_version" ]; then
echo " ✓ Found version: $github_api_version"
fi
else
echo " ✗ API unreachable"
fi
# Source 3: Check GitHub main branch
echo " [3/3] Checking GitHub main branch..."
local github_main_check=$(curl -sI "https://github.com/rfxn/maldet/archive/refs/heads/main.tar.gz" --connect-timeout 5 2>/dev/null | grep -E "HTTP")
if echo "$github_main_check" | grep -q "200\|302"; then
github_version="main-branch"
echo " ✓ Available (main branch)"
else
echo " ✗ Not reachable"
fi
echo ""
# Determine best source based on version comparison
if [ -n "$github_api_version" ] && [ -n "$rfxn_version" ]; then
# Both available - prefer the version tag if we can parse rfxn version
echo " Multiple sources available. Selecting best version..."
best_source="github_api"
best_version="$github_api_version"
best_url=$(echo "$github_api_data" | grep -o '"tarball_url":"[^"]*' | head -1 | cut -d'"' -f4)
echo " → Downloading version $best_version from GitHub API"
elif [ -n "$rfxn_version" ]; then
best_source="rfxn"
best_version="latest"
best_url="https://www.rfxn.com/downloads/maldetect-latest.tar.gz"
echo " → Downloading from rfxn.com (official)"
elif [ -n "$github_api_version" ]; then
best_source="github_api"
best_version="$github_api_version"
best_url=$(echo "$github_api_data" | grep -o '"tarball_url":"[^"]*' | head -1 | cut -d'"' -f4)
echo " → Downloading version $best_version from GitHub API"
elif [ -n "$github_version" ]; then
best_source="github_main"
best_version="main-branch"
best_url="https://github.com/rfxn/maldet/archive/refs/heads/main.tar.gz"
echo " → Downloading from GitHub main branch (fallback)"
else
echo -e "${RED}✗ All sources unreachable${NC}"
echo ""
echo "Known working download URLs:"
echo " Official: https://www.rfxn.com/downloads/maldetect-latest.tar.gz"
echo " GitHub: https://github.com/rfxn/maldet/archive/refs/heads/main.tar.gz"
echo ""
return 1
fi
echo ""
# Download from the best source
local temp_file="maldetect-${best_version}.tar.gz"
echo "Downloading $best_version..."
if wget -q --timeout=15 -O "$temp_file" "$best_url" 2>/dev/null; then
echo -e "${GREEN}✓ Download successful${NC}"
else
echo -e "${RED}✗ Download failed from $best_source${NC}"
rm -f "$temp_file"
return 1
fi
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 (version: $best_version)${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-${best_version}.tar.gz" 2>/dev/null || true
else
echo -e "${RED}✗ Failed to extract archive${NC}"
rm -f "$temp_file"
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/<ui_directory_name>/"
echo " (Replace <ui_directory_name> 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/tty 2>/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/tty 2>/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/tty 2>/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