a704e250e1
FIXED: - detect_scanners() no longer blocks menu when scanners aren't installed - Removed show_scanner_installation_guide() call from detection - Menu always displays with option 9 'Install all scanners' - User can now select which scanners to install directly from menu BEHAVIOR CHANGE: - Before: No scanners → installation guide → exit code 1 → no menu - After: No scanners → menu with install option → user can install from there This restores the original user experience where the menu is always available.
2684 lines
98 KiB
Bash
Executable File
2684 lines
98 KiB
Bash
Executable File
#!/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
|
||
|
||
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"
|
||
"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
|
||
|
||
# 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
|
||
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 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}"
|
||
|
||
if [ -f "/usr/local/cpanel/cpanel" ]; then
|
||
# cPanel method - check if already installed but not configured
|
||
if rpm -qa | grep -q "cpanel-clamav"; then
|
||
echo -e "${GREEN}✓ ClamAV already installed (cPanel)${NC}"
|
||
else
|
||
/scripts/update_local_rpm_versions --edit target_settings.clamav installed 2>/dev/null
|
||
/scripts/check_cpanel_rpms --fix --targets=clamav 2>&1 | grep -E "Installing|Updating|up to date"
|
||
fi
|
||
elif command -v yum &>/dev/null; then
|
||
yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Updating|already installed"
|
||
elif command -v apt-get &>/dev/null; then
|
||
apt-get update && apt-get install -y clamav clamav-daemon
|
||
fi
|
||
|
||
if is_clamav_installed; then
|
||
echo -e "${GREEN}✓ ClamAV installed${NC}"
|
||
|
||
# Find freshclam binary
|
||
local freshclam_bin=$(command -v freshclam || find /usr -name freshclam 2>/dev/null | head -1)
|
||
|
||
# Update virus signatures immediately
|
||
if [ -n "$freshclam_bin" ]; then
|
||
echo " → Updating virus signatures (this may take a moment)..."
|
||
if "$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 status unclear (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..."
|
||
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
|
||
|
||
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}"
|
||
echo " Available directories in /tmp:"
|
||
ls -la /tmp | grep maldetect | sed 's/^/ /'
|
||
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
|
||
echo -e "${GREEN}✓ Maldet installed${NC}"
|
||
rm -f "$install_log"
|
||
|
||
# Update malware signatures immediately
|
||
echo " → Updating malware signatures..."
|
||
if maldet -u 2>&1 | grep -qE "update completed|signatures"; then
|
||
echo -e " ${GREEN}✓${NC} Signatures updated"
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} Signature update status unclear (continuing with current definitions)"
|
||
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
|
||
|
||
# Check for common Plesk issues
|
||
if command -v plesk >/dev/null 2>&1; then
|
||
echo -e "${YELLOW}Detected Plesk system - checking for conflicts...${NC}"
|
||
|
||
# Check if cron is accessible
|
||
if [ ! -w /var/spool/cron ] && [ ! -w /etc/cron.d ]; then
|
||
echo " → Cron directory permissions may be restricted"
|
||
fi
|
||
|
||
# Check if required directories exist
|
||
if [ ! -d /usr/local/sbin ]; then
|
||
echo " → /usr/local/sbin does not exist (required for maldet)"
|
||
fi
|
||
fi
|
||
|
||
return 1
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ Download failed - maldetect-current.tar.gz not found${NC}"
|
||
return 1
|
||
fi
|
||
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..."
|
||
|
||
# Use deployment script method (most reliable)
|
||
cd /tmp
|
||
if [ -f "imav-deploy.sh" ]; then
|
||
rm -f imav-deploy.sh
|
||
fi
|
||
|
||
wget -q https://repo.imunify360.cloudlinux.com/defence360/imav-deploy.sh
|
||
|
||
if [ -f imav-deploy.sh ]; then
|
||
# Run deployment script with progress indicators
|
||
bash imav-deploy.sh 2>&1 | grep -E "Installing|Installed|Complete|Error|Failed" || bash imav-deploy.sh
|
||
rm -f imav-deploy.sh
|
||
|
||
# Enable cPanel UI plugin if installed
|
||
if [ -f "/opt/alt/python35/share/imunify360/scripts/av-userside-plugin.sh" ]; then
|
||
echo " → Enabling cPanel UI plugin..."
|
||
/opt/alt/python35/share/imunify360/scripts/av-userside-plugin.sh &>/dev/null
|
||
fi
|
||
else
|
||
echo -e "${RED} Failed to download installation script${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 "$imunify_bin" update 2>&1 | grep -qE "updated|Success|completed"; then
|
||
echo -e " ${GREEN}✓${NC} Signatures updated"
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} Signature update status unclear (continuing with current definitions)"
|
||
fi
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ ImunifyAV installation failed${NC}"
|
||
fi
|
||
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 yum &>/dev/null; then
|
||
# RHEL/CentOS - EPEL repo (only on RHEL-based systems that have rpm)
|
||
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" || true
|
||
fi
|
||
|
||
# Install rkhunter
|
||
yum install -y rkhunter 2>&1 | grep -E "Installing|Installed|already installed" || true
|
||
elif command -v apt-get &>/dev/null; then
|
||
# Debian/Ubuntu - universe repo (rkhunter is in universe)
|
||
echo " → Ensuring universe repository is enabled..."
|
||
grep -q "universe" /etc/apt/sources.list 2>/dev/null || \
|
||
sed -i 's/^deb http/deb http universe\ndeb http/' /etc/apt/sources.list 2>/dev/null || true
|
||
|
||
apt-get update 2>&1 | grep -E "Hit|Get|Reading|Building" | head -3 || true
|
||
apt-get install -y rkhunter 2>&1 | grep -E "Setting up|already|newest" || true
|
||
fi
|
||
|
||
if is_rkhunter_installed; then
|
||
echo -e "${GREEN}✓ Rootkit Hunter installed${NC}"
|
||
|
||
# Update definitions
|
||
echo " → Updating rootkit definitions..."
|
||
rkhunter --update 2>&1 | grep -E "updated|downloaded" || rkhunter --update &>/dev/null
|
||
echo -e " ${GREEN}✓${NC} Definitions updated"
|
||
|
||
# Initialize baseline (propupd creates file property database)
|
||
echo " → Initializing baseline database..."
|
||
rkhunter --propupd &>/dev/null
|
||
echo -e " ${GREEN}✓${NC} Baseline initialized"
|
||
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 -F for literal matching (safe from regex injection)
|
||
domain_docroot=$(grep -F "^${domain}:" /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
|
||
|
||
if [ ${#AVAILABLE_SCANNERS[@]} -eq 0 ]; then
|
||
log_message "ERROR: No scanners found!"
|
||
echo -e "${RED}No malware scanners detected!${NC}"
|
||
exit 1
|
||
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 (FIXED: use -F flag for literal matching consistency)
|
||
grep -F "Rootkit" "$LOG_DIR/rkhunter.log" 2>/dev/null | grep -iF "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
|
||
show_scan_menu() {
|
||
# Build reference database once for the entire menu session
|
||
if command -v build_reference_database &>/dev/null; then
|
||
echo "Building system reference database..."
|
||
build_reference_database 2>/dev/null || true
|
||
clear
|
||
fi
|
||
|
||
while true; do
|
||
print_banner "Malware Scanner"
|
||
|
||
echo "Available Scanners:"
|
||
for scanner in "${available_scanners[@]}"; do
|
||
echo " • ${scanner^}"
|
||
done
|
||
echo ""
|
||
|
||
echo -e "${CYAN}Create New Scan:${NC}"
|
||
echo -e " ${CYAN}1.${NC} Scan entire server (ClamAV, Maldet, RKHunter)"
|
||
echo -e " ${CYAN}2.${NC} Scan all user accounts (All scanners - recommended)"
|
||
echo -e " ${CYAN}3.${NC} Scan specific user account (All scanners)"
|
||
echo -e " ${CYAN}4.${NC} Scan specific domain (All scanners)"
|
||
echo -e " ${CYAN}5.${NC} Scan custom path (All scanners)"
|
||
echo ""
|
||
echo -e "${CYAN}Monitor & Manage:${NC}"
|
||
echo -e " ${CYAN}6.${NC} Check scan status"
|
||
echo -e " ${CYAN}7.${NC} View scan results"
|
||
echo -e " ${CYAN}8.${NC} Delete scan sessions"
|
||
echo ""
|
||
echo -e "${CYAN}Configuration:${NC}"
|
||
echo -e " ${CYAN}9.${NC} Install all scanners"
|
||
echo -e " ${CYAN}10.${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-10): " choice
|
||
|
||
if ! [[ "$choice" =~ ^([0-9]|10)$ ]]; then
|
||
echo -e "${RED}Invalid option${NC}"
|
||
sleep 1
|
||
continue
|
||
fi
|
||
|
||
case $choice in
|
||
1) launch_standalone_scanner_menu "server"; break ;;
|
||
2) launch_standalone_scanner_menu "all_users"; break ;;
|
||
3) launch_standalone_scanner_menu "user"; break ;;
|
||
4) launch_standalone_scanner_menu "domain"; break ;;
|
||
5) launch_standalone_scanner_menu "custom"; break ;;
|
||
6) check_standalone_status; break ;;
|
||
7) view_scan_results; break ;;
|
||
8) delete_standalone_sessions; break ;;
|
||
9) install_all_scanners; break ;;
|
||
10) 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
|
||
|
||
show_scan_menu
|
||
}
|
||
|
||
# Run if executed directly
|
||
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
||
main "$@"
|
||
fi
|