e7c73417a2
Issue: GitHub API curl was hanging even with timeouts on network-restricted server Solution: Use tested v2.0.1-rc4 directly instead of querying API - Eliminates hanging during 'Checking available versions...' - Falls back to main branch if release unavailable - Tested and verified to work (51,545 signatures) When new version available, update one line with new version number.
3500 lines
132 KiB
Bash
Executable File
3500 lines
132 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
|
||
|
||
# Color definitions (matching launcher.sh)
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
|
||
# Source required libraries (warn if missing, but allow graceful degradation)
|
||
source "$SCRIPT_DIR/lib/common-functions.sh" 2>/dev/null || \
|
||
{ echo "WARNING: common-functions.sh not found - some features may not work" >&2; }
|
||
source "$SCRIPT_DIR/lib/system-detect.sh" 2>/dev/null || \
|
||
{ echo "WARNING: system-detect.sh not found - control panel detection may fail" >&2; }
|
||
source "$SCRIPT_DIR/lib/user-manager.sh" 2>/dev/null || \
|
||
{ echo "WARNING: user-manager.sh not found - user selection may not work" >&2; }
|
||
source "$SCRIPT_DIR/lib/reference-db.sh" 2>/dev/null || true # Optional
|
||
source "$SCRIPT_DIR/lib/ip-reputation.sh" 2>/dev/null || true # Optional
|
||
|
||
# Arrays for docroots and scanners
|
||
declare -a docroot_array
|
||
declare -a sanitized_docroot
|
||
declare -a remove_docroot
|
||
declare -a available_scanners
|
||
|
||
# Validate that required functions were sourced from libraries
|
||
# These functions must exist for the script to work properly
|
||
validate_required_functions() {
|
||
local required_functions=(
|
||
"confirm"
|
||
"print_header"
|
||
"print_banner"
|
||
"select_user_interactive"
|
||
"get_user_domains"
|
||
)
|
||
|
||
for func in "${required_functions[@]}"; do
|
||
if ! declare -f "$func" &>/dev/null; then
|
||
echo "ERROR: Required function '$func' not found." >&2
|
||
echo " Check that library files exist in: $SCRIPT_DIR/lib/" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
return 0
|
||
}
|
||
|
||
# Validate functions early
|
||
if ! validate_required_functions; then
|
||
exit 1
|
||
fi
|
||
|
||
# Auto-detect web server document root for ImunifyAV standalone UI path
|
||
get_web_root_for_imunify() {
|
||
local detected_root=""
|
||
|
||
# Try Apache on Debian/Ubuntu (apache2ctl)
|
||
if command -v apache2ctl &>/dev/null; then
|
||
detected_root=$(apache2ctl -S 2>/dev/null | grep "^\*:" | head -1 | awk '{print $NF}' | sed 's/*://' || echo "")
|
||
if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then
|
||
echo "$detected_root"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Try Apache on RHEL/CentOS (httpd -S)
|
||
if command -v httpd &>/dev/null; then
|
||
detected_root=$(httpd -S 2>/dev/null | grep "^\*:" | head -1 | awk '{print $NF}' | sed 's/*://' || echo "")
|
||
if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then
|
||
echo "$detected_root"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Try Nginx (nginx -T)
|
||
if command -v nginx &>/dev/null; then
|
||
detected_root=$(nginx -T 2>/dev/null | grep "^\s*root " | head -1 | awk '{print $NF}' | sed 's/;//' || echo "")
|
||
if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then
|
||
echo "$detected_root"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Try parsing Apache config files directly
|
||
for conf_file in /etc/apache2/apache2.conf /etc/httpd/conf/httpd.conf /etc/apache2/sites-enabled/*.conf /etc/httpd/conf.d/*.conf; do
|
||
if [ -f "$conf_file" ] 2>/dev/null; then
|
||
detected_root=$(grep -E "^\s*DocumentRoot|^\s*root " "$conf_file" 2>/dev/null | head -1 | awk '{print $NF}' | sed 's/"//g' || echo "")
|
||
if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then
|
||
echo "$detected_root"
|
||
return 0
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# Try Nginx config files directly
|
||
for conf_file in /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf /etc/nginx/sites-enabled/*.conf; do
|
||
if [ -f "$conf_file" ] 2>/dev/null; then
|
||
detected_root=$(grep -E "^\s*root " "$conf_file" 2>/dev/null | head -1 | awk '{print $NF}' | sed 's/;//' || echo "")
|
||
if [ -n "$detected_root" ] && [ -d "$detected_root" ]; then
|
||
echo "$detected_root"
|
||
return 0
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# Try common default locations in order of likelihood
|
||
for path in /var/www/html /home /srv/www /var/www /usr/share/nginx/html /var/www/vhosts; do
|
||
if [ -d "$path" ] && [ -w "$path" ]; then
|
||
echo "$path"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
# Absolute fallback
|
||
echo "/var/www/html"
|
||
}
|
||
|
||
# Individual scanner detection functions
|
||
is_imunify_installed() {
|
||
command -v imunify-antivirus &>/dev/null || [ -f "/usr/bin/imunify-antivirus" ]
|
||
}
|
||
|
||
is_clamav_installed() {
|
||
command -v clamscan &>/dev/null || \
|
||
[ -f "/usr/local/cpanel/3rdparty/bin/clamscan" ] || \
|
||
(command -v rpm &>/dev/null && rpm -qa 2>/dev/null | grep -q "cpanel-clamav") || \
|
||
(command -v dpkg &>/dev/null && dpkg -l 2>/dev/null | grep -q "^ii.*clamav")
|
||
}
|
||
|
||
is_maldet_installed() {
|
||
command -v maldet &>/dev/null || [ -f "/usr/local/sbin/maldet" ]
|
||
}
|
||
|
||
is_rkhunter_installed() {
|
||
command -v rkhunter &>/dev/null || [ -f "/usr/bin/rkhunter" ]
|
||
}
|
||
|
||
# Scanner detection
|
||
detect_scanners() {
|
||
available_scanners=()
|
||
|
||
if is_imunify_installed; then
|
||
available_scanners+=("imunify")
|
||
fi
|
||
|
||
if is_clamav_installed; then
|
||
available_scanners+=("clamav")
|
||
fi
|
||
|
||
if is_maldet_installed; then
|
||
available_scanners+=("maldet")
|
||
fi
|
||
|
||
if is_rkhunter_installed; then
|
||
available_scanners+=("rkhunter")
|
||
fi
|
||
|
||
# Note: If no scanners are found, available_scanners array will be empty
|
||
# Menu option 9 allows installation, so we don't exit here
|
||
# Just return success to allow menu to display
|
||
return 0
|
||
}
|
||
|
||
# Show installation instructions for missing scanners (DEPRECATED - menu always shows now)
|
||
show_scanner_installation_guide() {
|
||
echo -e "${YELLOW}Available Malware Scanners:${NC}"
|
||
echo ""
|
||
|
||
# Check ImunifyAV
|
||
if ! is_imunify_installed; then
|
||
echo -e "${CYAN}ImunifyAV${NC} - FREE real-time malware scanner"
|
||
echo " Status: Not installed"
|
||
echo " Installation (cPanel):"
|
||
echo " yum install imunify-antivirus imunify-antivirus-cpanel"
|
||
echo " /opt/alt/python35/share/imunify360/scripts/av-userside-plugin.sh"
|
||
echo " Installation (script method):"
|
||
echo " wget https://repo.imunify360.cloudlinux.com/defence360/imav-deploy.sh"
|
||
echo " bash imav-deploy.sh"
|
||
echo " Note: ImunifyAV is FREE. Imunify360 is the paid version."
|
||
echo " Docs: https://docs.imunify360.com/imunifyav/"
|
||
echo ""
|
||
else
|
||
echo -e "${GREEN}✓ ImunifyAV${NC} - Installed (FREE version)"
|
||
echo ""
|
||
fi
|
||
|
||
# Check ClamAV
|
||
if ! is_clamav_installed; then
|
||
echo -e "${CYAN}ClamAV${NC} - Open source antivirus engine"
|
||
echo " Status: Not installed"
|
||
echo " Installation (cPanel):"
|
||
echo " /scripts/update_local_rpm_versions --edit target_settings.clamav installed"
|
||
echo " /scripts/check_cpanel_rpms --fix --targets=clamav"
|
||
echo " Installation (manual):"
|
||
echo " yum install clamav clamav-update # RHEL/CentOS"
|
||
echo " apt-get install clamav clamav-daemon # Debian/Ubuntu"
|
||
echo " freshclam # Update virus definitions"
|
||
echo ""
|
||
else
|
||
echo -e "${GREEN}✓ ClamAV${NC} - Installed"
|
||
echo ""
|
||
fi
|
||
|
||
# Check Maldet
|
||
if ! is_maldet_installed; then
|
||
echo -e "${CYAN}Maldet (LMD)${NC} - Linux Malware Detect"
|
||
echo " Status: Not installed"
|
||
echo " Installation:"
|
||
echo " cd /tmp"
|
||
echo " wget http://www.rfxn.com/downloads/maldetect-current.tar.gz"
|
||
echo " tar -xzf maldetect-current.tar.gz"
|
||
echo " cd maldetect-*"
|
||
echo " ./install.sh"
|
||
echo " Docs: https://www.rfxn.com/projects/linux-malware-detect/"
|
||
echo ""
|
||
else
|
||
echo -e "${GREEN}✓ Maldet${NC} - Installed"
|
||
echo ""
|
||
fi
|
||
|
||
# Check Rootkit Hunter
|
||
if ! is_rkhunter_installed; then
|
||
echo -e "${CYAN}Rootkit Hunter${NC} - Rootkit/backdoor/exploit scanner"
|
||
echo " Status: Not installed"
|
||
echo " Installation:"
|
||
echo " yum install epel-release -y # Enable EPEL repo"
|
||
echo " yum install rkhunter -y"
|
||
echo " rkhunter --update # Update definitions"
|
||
echo " rkhunter --propupd # Initialize baseline"
|
||
echo " Docs: https://rkhunter.sourceforge.net/"
|
||
echo ""
|
||
else
|
||
echo -e "${GREEN}✓ Rootkit Hunter${NC} - Installed"
|
||
echo ""
|
||
fi
|
||
|
||
echo -e "${YELLOW}Recommendation:${NC} Install at least ClamAV + RKHunter (both free) for comprehensive protection"
|
||
echo ""
|
||
}
|
||
|
||
# Install individual scanners
|
||
install_maldet_only() {
|
||
echo ""
|
||
print_header "Installing Maldet (Linux Malware Detection)"
|
||
echo ""
|
||
|
||
if is_maldet_installed; then
|
||
echo -e "${GREEN}✓ Maldet is already installed${NC}"
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
return 0
|
||
fi
|
||
|
||
echo "Maldet is a fast, Linux-specific malware scanner"
|
||
echo "Repository: https://github.com/rfxn/maldet"
|
||
echo ""
|
||
echo "Checking available versions..."
|
||
echo ""
|
||
|
||
cd /tmp || return 1
|
||
|
||
# Try to download from sources in order with aggressive timeout handling
|
||
# Skip pre-checking (can hang on firewall-intercepted HTTPS)
|
||
# Just attempt downloads directly with proper timeouts
|
||
|
||
local download_success=false
|
||
local temp_file="maldetect-latest.tar.gz"
|
||
local best_source=""
|
||
|
||
# Clean up any empty/partial files from previous failed attempts
|
||
if [ -f "$temp_file" ] && [ ! -s "$temp_file" ]; then
|
||
rm -f "$temp_file"
|
||
fi
|
||
|
||
# Download sources in priority order
|
||
# Format: "name|url|label" (using | as delimiter to avoid splitting https://)
|
||
# Note: Repository is rfxn/linux-malware-detect (not maldet)
|
||
# Latest tested version: v2.0.1-rc4 (Apr 20, 2026)
|
||
local sources=(
|
||
"github-release|https://api.github.com/repos/rfxn/linux-malware-detect/tarball/v2.0.1-rc4|GitHub Release v2.0.1-rc4"
|
||
"github-main|https://github.com/rfxn/linux-malware-detect/archive/refs/heads/main.tar.gz|GitHub main branch (Development)"
|
||
)
|
||
|
||
echo "Attempting to download from sources..."
|
||
echo ""
|
||
|
||
for source_info in "${sources[@]}"; do
|
||
IFS='|' read -r source_name source_url source_label <<< "$source_info"
|
||
|
||
echo " Trying $source_label..."
|
||
|
||
# Clean up any previous attempt file before trying
|
||
rm -f "$temp_file"
|
||
|
||
# Try download with aggressive timeout
|
||
# --timeout: fail if no progress for this many seconds
|
||
# --read-timeout: fail if no data received for this many seconds
|
||
if wget -q --timeout=30 --read-timeout=10 -O "$temp_file" "$source_url" 2>/dev/null; then
|
||
# Verify download created a non-empty file (wget creates empty file on failure)
|
||
if [ -f "$temp_file" ] && [ -s "$temp_file" ]; then
|
||
echo -e " ${GREEN}✓ Download successful ($(du -h "$temp_file" | cut -f1))${NC}"
|
||
download_success=true
|
||
best_source="$source_label"
|
||
break
|
||
else
|
||
# Download command succeeded but file is empty - network/proxy issue
|
||
rm -f "$temp_file"
|
||
echo -e " ${RED}✗ Download created empty file (network/firewall issue)${NC}"
|
||
fi
|
||
else
|
||
# wget command failed
|
||
rm -f "$temp_file"
|
||
echo -e " ${RED}✗ Download failed${NC}"
|
||
fi
|
||
|
||
# Also try with curl as fallback
|
||
if [ "$download_success" = false ]; then
|
||
rm -f "$temp_file"
|
||
|
||
if curl -f --connect-timeout 10 --max-time 30 -L -o "$temp_file" "$source_url" 2>/dev/null; then
|
||
# Verify curl created a non-empty file
|
||
if [ -f "$temp_file" ] && [ -s "$temp_file" ]; then
|
||
echo -e " ${GREEN}✓ Download successful via curl ($(du -h "$temp_file" | cut -f1))${NC}"
|
||
download_success=true
|
||
best_source="$source_label"
|
||
break
|
||
else
|
||
rm -f "$temp_file"
|
||
echo -e " ${RED}✗ Curl created empty file (network/firewall issue)${NC}"
|
||
fi
|
||
else
|
||
rm -f "$temp_file"
|
||
echo -e " ${RED}✗ Curl failed${NC}"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
echo ""
|
||
|
||
if [ "$download_success" = false ]; then
|
||
# All sources timed out or failed - check for offline options
|
||
echo -e "${YELLOW}All download sources failed or timed out.${NC}"
|
||
echo ""
|
||
echo "Checking for pre-downloaded archives or system packages..."
|
||
echo ""
|
||
|
||
# Check for pre-cached/pre-downloaded file
|
||
local local_archive=""
|
||
for path in /root/maldetect*.tar.gz /tmp/maldetect*.tar.gz /opt/maldetect*.tar.gz ~/maldetect*.tar.gz; do
|
||
if [ -f "$path" ]; then
|
||
local_archive="$path"
|
||
echo -e "${GREEN}✓ Found archive: $path${NC}"
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ -n "$local_archive" ]; then
|
||
echo ""
|
||
echo "Using pre-downloaded archive: $local_archive"
|
||
# Use archive directly without copying
|
||
temp_file="$local_archive"
|
||
|
||
# Validate the archive
|
||
if [ -f "$temp_file" ] && [ -s "$temp_file" ]; then
|
||
# Quick check: can we read it as tar?
|
||
if tar -tzf "$temp_file" >/dev/null 2>&1; then
|
||
download_success=true
|
||
best_source="offline-archive"
|
||
echo -e "${GREEN}✓ Archive validated and ready for extraction${NC}"
|
||
else
|
||
echo -e "${RED}✗ Archive is corrupted or not a valid tar file${NC}"
|
||
download_success=false
|
||
temp_file=""
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ Archive file is missing or empty${NC}"
|
||
download_success=false
|
||
temp_file=""
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ No local archive found${NC}"
|
||
echo ""
|
||
echo "All sources unreachable. To install Maldet, use one of these methods:"
|
||
echo ""
|
||
echo " METHOD 1 - Download on networked machine and transfer:"
|
||
echo " On another server with internet:"
|
||
echo " wget https://www.rfxn.com/downloads/maldetect-latest.tar.gz"
|
||
echo " Then copy to this server:"
|
||
echo " scp maldetect-latest.tar.gz root@YOUR-SERVER:/root/"
|
||
echo " Then run this installer again"
|
||
echo ""
|
||
echo " METHOD 2 - GitHub source as alternative:"
|
||
echo " wget https://github.com/rfxn/maldet/archive/refs/heads/main.tar.gz -O /root/maldetect.tar.gz"
|
||
echo " (then run this installer again)"
|
||
echo ""
|
||
echo "Once you place the archive in /root/ (or /tmp/ or /opt/), run installer again."
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Extract and install if we have a valid archive
|
||
if [ "$download_success" = true ]; then
|
||
if [ ! -f "$temp_file" ] || [ ! -s "$temp_file" ]; then
|
||
echo -e "${RED}✗ Archive file missing or empty: $temp_file${NC}"
|
||
return 1
|
||
fi
|
||
echo "Installing from $best_source..."
|
||
echo ""
|
||
|
||
# Extract and install
|
||
echo " Extracting archive..."
|
||
if tar xzf "$temp_file" 2>/dev/null; then
|
||
echo " Running installer..."
|
||
# Find the extracted directory (could be maldetect-* or rfxn-linux-malware-detect-*)
|
||
local install_dir
|
||
install_dir=$(find . -maxdepth 1 -type d \( -name "*malware*" -o -name "*maldet*" \) 2>/dev/null | head -1)
|
||
|
||
if [ -n "$install_dir" ] && [ -f "$install_dir/install.sh" ]; then
|
||
if cd "$install_dir" 2>/dev/null && bash install.sh > /tmp/maldet-install.log 2>&1; then
|
||
echo -e " ${GREEN}✓ Maldet installed successfully${NC}"
|
||
|
||
# Update signatures in background
|
||
echo ""
|
||
echo "Updating malware signatures..."
|
||
if command -v maldet &>/dev/null; then
|
||
maldet -u > /dev/null 2>&1 &
|
||
echo " (signatures updating in background)"
|
||
fi
|
||
else
|
||
echo -e " ${RED}✗ Installation failed. Check /tmp/maldet-install.log${NC}"
|
||
fi
|
||
else
|
||
echo -e " ${RED}✗ Could not find install.sh in extracted directory${NC}"
|
||
if [ -z "$install_dir" ]; then
|
||
echo " (Directory not found matching *malware* or *maldet*)"
|
||
fi
|
||
fi
|
||
cd /tmp
|
||
rm -rf maldetect-* rfxn-linux-malware-detect-* maldetect-latest.tar.gz 2>/dev/null || true
|
||
else
|
||
echo -e " ${RED}✗ Failed to extract archive${NC}"
|
||
rm -f "$temp_file"
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ No valid archive available for extraction${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
}
|
||
|
||
install_clamav_only() {
|
||
echo ""
|
||
print_header "Installing ClamAV (Open Source Antivirus)"
|
||
echo ""
|
||
|
||
if is_clamav_installed; then
|
||
echo -e "${GREEN}✓ ClamAV is already installed${NC}"
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
return 0
|
||
fi
|
||
|
||
echo "Installing ClamAV and updating virus definitions..."
|
||
echo ""
|
||
|
||
if command -v yum &>/dev/null; then
|
||
echo "Using yum package manager..."
|
||
yum install -y clamav clamav-daemon clamav-update 2>&1 | tail -5
|
||
elif command -v apt-get &>/dev/null; then
|
||
echo "Using apt package manager..."
|
||
apt-get update > /dev/null 2>&1
|
||
apt-get install -y clamav clamav-daemon 2>&1 | tail -5
|
||
else
|
||
echo -e "${RED}✗ No compatible package manager found${NC}"
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
|
||
echo ""
|
||
if is_clamav_installed; then
|
||
echo -e "${GREEN}✓ ClamAV installed successfully${NC}"
|
||
|
||
# Update signatures
|
||
echo ""
|
||
echo "Updating virus signatures..."
|
||
for freshclam_path in /usr/bin/freshclam /usr/sbin/freshclam /usr/local/bin/freshclam; do
|
||
if [ -x "$freshclam_path" ]; then
|
||
timeout 60 "$freshclam_path" > /dev/null 2>&1 &
|
||
echo " (signatures updating in background)"
|
||
break
|
||
fi
|
||
done
|
||
else
|
||
echo -e "${RED}✗ Installation may have failed${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
}
|
||
|
||
install_rkhunter_only() {
|
||
echo ""
|
||
print_header "Installing RKHunter (Rootkit Detection)"
|
||
echo ""
|
||
|
||
if is_rkhunter_installed; then
|
||
echo -e "${GREEN}✓ RKHunter is already installed${NC}"
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
return 0
|
||
fi
|
||
|
||
echo "Installing RKHunter..."
|
||
echo ""
|
||
|
||
if command -v yum &>/dev/null; then
|
||
echo "Using yum package manager..."
|
||
yum install -y epel-release 2>&1 > /dev/null || true
|
||
yum install -y rkhunter 2>&1 | tail -3
|
||
elif command -v apt-get &>/dev/null; then
|
||
echo "Using apt package manager..."
|
||
apt-get update > /dev/null 2>&1
|
||
apt-get install -y rkhunter 2>&1 | tail -3
|
||
else
|
||
echo -e "${RED}✗ No compatible package manager found${NC}"
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
|
||
echo ""
|
||
if is_rkhunter_installed; then
|
||
echo -e "${GREEN}✓ RKHunter installed successfully${NC}"
|
||
else
|
||
echo -e "${RED}✗ Installation may have failed${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
read -p "Press Enter to continue..." < /dev/tty 2>/dev/null || true
|
||
}
|
||
|
||
# Install all scanners at once
|
||
install_all_scanners() {
|
||
echo ""
|
||
print_header "Install All Malware Scanners"
|
||
|
||
echo "This will install:"
|
||
echo " • ClamAV (free, open source)"
|
||
echo " • Maldet (free, Linux-specific)"
|
||
echo " • ImunifyAV (FREE version)"
|
||
echo " • Rootkit Hunter (free, rootkit detection)"
|
||
echo ""
|
||
echo -e "${YELLOW}Note: ImunifyAV is FREE. Imunify360 is the paid version.${NC}"
|
||
echo ""
|
||
|
||
read -p "Proceed with installation? (yes/no): " confirm
|
||
|
||
if [ "$confirm" != "yes" ]; then
|
||
echo "Cancelled."
|
||
read -p "Press Enter to continue..."
|
||
return 0
|
||
fi
|
||
|
||
echo ""
|
||
echo "=========================================="
|
||
echo "Installing Scanners"
|
||
echo "=========================================="
|
||
echo ""
|
||
|
||
# Install ClamAV
|
||
if ! is_clamav_installed; then
|
||
echo -e "${CYAN}[1/4] Installing ClamAV...${NC}"
|
||
|
||
# Try control panel-specific methods first
|
||
if [ -f "/usr/local/cpanel/cpanel" ]; then
|
||
# cPanel method - use cPanel's package management only
|
||
if rpm -qa 2>/dev/null | grep -q "cpanel-clamav"; then
|
||
echo -e "${GREEN}✓ ClamAV already installed (cPanel)${NC}"
|
||
else
|
||
echo " → Installing via cPanel package manager..."
|
||
|
||
# Check if cPanel scripts exist before using them
|
||
if [ -f "/scripts/update_local_rpm_versions" ] && [ -f "/scripts/check_cpanel_rpms" ]; then
|
||
/scripts/update_local_rpm_versions --edit target_settings.clamav installed 2>/dev/null || true
|
||
if ! /scripts/check_cpanel_rpms --fix --targets=clamav 2>&1 | tail -3; then
|
||
# cPanel scripts failed, fall back to standard yum
|
||
echo " → cPanel package manager unavailable, trying standard yum..."
|
||
yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Installed|already" || echo " (installation in progress)"
|
||
fi
|
||
else
|
||
# cPanel scripts don't exist, fall back to standard yum
|
||
echo " → cPanel tools not available, using standard package manager..."
|
||
yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Installed|already" || echo " (installation in progress)"
|
||
fi
|
||
fi
|
||
# IMPORTANT: Don't fall through to standard yum - cPanel packages conflict!
|
||
elif [ -f "/usr/local/psa/version" ]; then
|
||
# Plesk method - use standard package manager
|
||
echo " → Detected Plesk system, using standard package manager..."
|
||
if command -v yum &>/dev/null; then
|
||
yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Installed|already installed" || echo " (installation may already be complete)"
|
||
elif command -v apt-get &>/dev/null; then
|
||
apt-get update 2>&1 | grep -E "Reading|Building|Hit|Get" | head -3 || true
|
||
apt-get install -y clamav clamav-daemon 2>&1 | grep -E "Setting up|already|newest" || echo " (installation may already be complete)"
|
||
fi
|
||
elif command -v yum &>/dev/null; then
|
||
# RHEL/CentOS based systems (non-cPanel)
|
||
echo " → Installing via yum..."
|
||
yum install -y clamav clamav-update 2>&1 | grep -E "Installing|Installed|already installed" || echo " (installation may already be complete)"
|
||
elif command -v apt-get &>/dev/null; then
|
||
# Debian/Ubuntu: Update package list first, then install ClamAV
|
||
echo " → Updating package list..."
|
||
apt-get update 2>&1 | grep -E "Reading|Building|Hit|Get" | head -3 || true
|
||
echo " → Installing ClamAV..."
|
||
apt-get install -y clamav clamav-daemon 2>&1 | grep -E "Setting up|already|newest" || echo " (installation may already be complete)"
|
||
fi
|
||
|
||
if is_clamav_installed; then
|
||
echo -e "${GREEN}✓ ClamAV installed${NC}"
|
||
|
||
# Find freshclam binary - try standard locations first before using find
|
||
local freshclam_bin=""
|
||
for path in /usr/bin/freshclam /usr/sbin/freshclam \
|
||
/usr/local/bin/freshclam /usr/local/sbin/freshclam \
|
||
/usr/local/cpanel/3rdparty/bin/freshclam; do
|
||
if [ -x "$path" ]; then
|
||
freshclam_bin="$path"
|
||
break
|
||
fi
|
||
done
|
||
# Only use find as last resort if standard paths don't work
|
||
if [ -z "$freshclam_bin" ]; then
|
||
freshclam_bin=$(find /usr/local /usr -name freshclam -type f 2>/dev/null | head -1)
|
||
fi
|
||
|
||
# Update virus signatures immediately
|
||
if [ -n "$freshclam_bin" ]; then
|
||
echo " → Updating virus signatures (timeout 60s)..."
|
||
if timeout 60 "$freshclam_bin" 2>&1 | grep -qE "updated|Downloaded|up-to-date"; then
|
||
echo -e " ${GREEN}✓${NC} Signatures updated"
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} Signature update inconclusive (may still be current)"
|
||
fi
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ ClamAV installation failed${NC}"
|
||
fi
|
||
else
|
||
echo -e "${GREEN}✓ ClamAV already installed${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
|
||
# Install Maldet
|
||
if ! is_maldet_installed; then
|
||
echo -e "${CYAN}[2/4] Installing Maldet...${NC}"
|
||
|
||
(
|
||
cd /tmp || { echo -e "${RED}✗ Cannot access /tmp${NC}"; return 1; }
|
||
|
||
# Download Maldet
|
||
echo " → Downloading Maldet..."
|
||
# Try HTTPS first (more secure), fallback to HTTP if needed
|
||
if ! wget -q https://www.rfxn.com/downloads/maldetect-current.tar.gz 2>/dev/null; then
|
||
if ! wget -q http://www.rfxn.com/downloads/maldetect-current.tar.gz; then
|
||
echo -e "${RED}✗ Download failed - check internet connectivity${NC}"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
if [ -f maldetect-current.tar.gz ]; then
|
||
echo " → Extracting archive..."
|
||
if ! tar -xzf maldetect-current.tar.gz 2>/dev/null; then
|
||
echo -e "${RED}✗ Extraction failed - archive may be corrupted${NC}"
|
||
rm -f maldetect-current.tar.gz
|
||
return 1
|
||
fi
|
||
|
||
# Find the extracted directory
|
||
local maldet_dir=$(find /tmp -maxdepth 1 -type d -name "maldetect-*" 2>/dev/null | head -1)
|
||
if [ -z "$maldet_dir" ]; then
|
||
echo -e "${RED}✗ Cannot find extracted directory${NC}"
|
||
cd /tmp
|
||
rm -rf "maldetect-"*
|
||
return 1
|
||
fi
|
||
|
||
# Change to extracted directory
|
||
if ! cd "$maldet_dir"; then
|
||
echo -e "${RED}✗ Cannot access directory: $maldet_dir${NC}"
|
||
cd /tmp
|
||
rm -rf "maldetect-"*
|
||
return 1
|
||
fi
|
||
|
||
# Run installation with error capture
|
||
echo " → Running installation script..."
|
||
local install_log="/tmp/maldet-install-$$.log"
|
||
if ./install.sh > "$install_log" 2>&1; then
|
||
install_exit=0
|
||
else
|
||
install_exit=$?
|
||
fi
|
||
|
||
# Cleanup
|
||
cd /tmp
|
||
rm -rf "maldetect-"*
|
||
|
||
# Check if installation succeeded
|
||
if is_maldet_installed; then
|
||
# Verify we have version 2.0 or newer
|
||
local maldet_bin=$(command -v maldet || find /usr/local -name maldet -type f 2>/dev/null | head -1)
|
||
local maldet_version=""
|
||
if [ -n "$maldet_bin" ]; then
|
||
maldet_version=$("$maldet_bin" -v 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' | head -1)
|
||
fi
|
||
|
||
# Check version is 2.0 or newer
|
||
if [ -n "$maldet_version" ]; then
|
||
local major_version=$(echo "$maldet_version" | cut -d. -f1)
|
||
if [ "$major_version" -lt 2 ]; then
|
||
echo -e "${YELLOW}⚠ Warning: Maldet version $maldet_version installed (2.0+ recommended for performance)${NC}"
|
||
else
|
||
echo -e "${GREEN}✓${NC} Maldet $maldet_version installed (2.0+ performance optimizations)"
|
||
fi
|
||
else
|
||
echo -e "${GREEN}✓ Maldet installed${NC}"
|
||
fi
|
||
|
||
rm -f "$install_log"
|
||
|
||
# Update malware signatures immediately with timeout
|
||
echo " → Updating malware signatures..."
|
||
if [ -n "$maldet_bin" ]; then
|
||
if timeout 120 "$maldet_bin" -u 2>&1 | grep -qE "update completed|signatures"; then
|
||
echo -e " ${GREEN}✓${NC} Signatures updated"
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} Signature update inconclusive (continuing with current definitions)"
|
||
fi
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ Maldet installation failed${NC}"
|
||
|
||
# Show diagnostic information
|
||
if [ -f "$install_log" ]; then
|
||
echo -e "${YELLOW}Installation output (last 10 lines):${NC}"
|
||
tail -10 "$install_log" | sed 's/^/ /'
|
||
echo ""
|
||
echo -e "${YELLOW}Full log saved to: $install_log${NC}"
|
||
fi
|
||
fi
|
||
return 0
|
||
else
|
||
echo -e "${RED}✗ Download failed - maldetect-current.tar.gz not found${NC}"
|
||
return 0
|
||
fi
|
||
) || true
|
||
else
|
||
echo -e "${GREEN}✓ Maldet already installed${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
|
||
# Install ImunifyAV (FREE version)
|
||
if ! is_imunify_installed; then
|
||
echo -e "${CYAN}[3/4] Installing ImunifyAV (FREE)...${NC}"
|
||
echo " This may take several minutes - please wait..."
|
||
|
||
# ── STANDALONE DETECTION ─────────────────────────────────────────
|
||
# Detect whether this is a standalone system (no cPanel, no Plesk).
|
||
# InterWorx is also treated as standalone for ImunifyAV purposes
|
||
# because imav-deploy.sh does not recognise it as a "panel".
|
||
local imav_is_standalone=0
|
||
if [ ! -f "/usr/local/cpanel/cpanel" ] && [ ! -f "/usr/local/psa/version" ]; then
|
||
imav_is_standalone=1
|
||
fi
|
||
|
||
# ── STANDALONE: INTEGRATION.CONF SETUP ───────────────────────────
|
||
if [ "$imav_is_standalone" -eq 1 ]; then
|
||
echo ""
|
||
echo -e "${YELLOW} ⚠ Standalone system detected (no cPanel or Plesk found)${NC}"
|
||
echo " ImunifyAV requires a web server path for its UI on standalone systems."
|
||
echo ""
|
||
|
||
local imav_conf_dir="/etc/sysconfig/imunify360"
|
||
local imav_conf_file="$imav_conf_dir/integration.conf"
|
||
local imav_ui_path=""
|
||
|
||
# Check if integration.conf already exists with ui_path set
|
||
if [ -f "$imav_conf_file" ] && grep -q "^ui_path" "$imav_conf_file" 2>/dev/null; then
|
||
# Already configured - read existing value for display only
|
||
imav_ui_path=$(grep "^ui_path" "$imav_conf_file" | head -1 | cut -d'=' -f2 | tr -d ' ')
|
||
echo -e " ${GREEN}✓${NC} integration.conf already exists with ui_path: $imav_ui_path"
|
||
echo " Proceeding with existing configuration."
|
||
else
|
||
# Auto-detect web server document root (no prompting)
|
||
local imav_detected_root
|
||
imav_detected_root=$(get_web_root_for_imunify)
|
||
imav_ui_path="$imav_detected_root/imunifyav"
|
||
|
||
echo -e " ${GREEN}✓${NC} Auto-detected web root: $imav_detected_root"
|
||
echo " UI will be deployed to: $imav_ui_path"
|
||
fi
|
||
|
||
# Create config directory if needed
|
||
if [ "$imav_is_standalone" -ne 2 ]; then
|
||
echo " → Creating $imav_conf_dir ..."
|
||
mkdir -p "$imav_conf_dir" || {
|
||
echo -e "${RED} ✗ Cannot create $imav_conf_dir - check permissions. Skipping ImunifyAV.${NC}"
|
||
imav_is_standalone=2
|
||
}
|
||
fi
|
||
|
||
# Write minimal integration.conf (only ui_path is required)
|
||
if [ "$imav_is_standalone" -ne 2 ]; then
|
||
printf '[paths]\nui_path = %s\n' "$imav_ui_path" > "$imav_conf_file" || {
|
||
echo -e "${RED} ✗ Cannot write $imav_conf_file. Skipping ImunifyAV.${NC}"
|
||
imav_is_standalone=2
|
||
}
|
||
fi
|
||
|
||
if [ "$imav_is_standalone" -ne 2 ]; then
|
||
echo -e " ${GREEN}✓${NC} integration.conf written: ui_path = $imav_ui_path"
|
||
fi
|
||
|
||
# SELinux warning for RHEL-family systems
|
||
if [ "$imav_is_standalone" -ne 2 ] && command -v getenforce &>/dev/null; then
|
||
local selinux_status
|
||
selinux_status=$(getenforce 2>/dev/null || echo "Unknown")
|
||
if [ "$selinux_status" = "Enforcing" ]; then
|
||
echo ""
|
||
echo -e " ${YELLOW}⚠ SELinux is Enforcing${NC}"
|
||
echo " After installation, ImunifyAV may need an SELinux policy module."
|
||
echo " If the UI is inaccessible, run:"
|
||
echo " ausearch -c 'imunify' | audit2allow -M imunify && semodule -i imunify.pp"
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
fi
|
||
# ── END STANDALONE SETUP ─────────────────────────────────────────
|
||
|
||
# Only proceed with download/deploy if not cancelled (imav_is_standalone != 2)
|
||
if [ "${imav_is_standalone:-0}" -ne 2 ]; then
|
||
|
||
# Use deployment script method (most reliable)
|
||
cd /tmp
|
||
if [ -f "imav-deploy.sh" ]; then
|
||
rm -f imav-deploy.sh
|
||
fi
|
||
|
||
# Download deployment script with timeout
|
||
if timeout 30 wget -q -O imav-deploy.sh https://repo.imunify360.cloudlinux.com/defence360/imav-deploy.sh 2>/dev/null; then
|
||
if [ ! -f imav-deploy.sh ] || [ ! -s imav-deploy.sh ]; then
|
||
echo -e "${RED} Failed to download installation script (empty file)${NC}"
|
||
else
|
||
# Run deployment script with timeout and capture output
|
||
echo " → Running deployment script..."
|
||
local deploy_log="/tmp/imav-deploy-$$.log"
|
||
if timeout 300 bash imav-deploy.sh > "$deploy_log" 2>&1; then
|
||
# Check if any actual installation happened
|
||
if grep -qiE "installed|complete|success" "$deploy_log"; then
|
||
echo " → Deployment script executed"
|
||
else
|
||
echo " → Deployment script ran (check for errors below)"
|
||
fi
|
||
|
||
# Show any errors from deployment
|
||
if grep -qi "error\|failed\|conflict" "$deploy_log"; then
|
||
echo -e " ${YELLOW}⚠ Warnings detected:${NC}"
|
||
grep -iE "error|failed|conflict" "$deploy_log" | sed 's/^/ /' | head -3
|
||
fi
|
||
else
|
||
echo -e "${YELLOW} ⚠ Deployment script timed out or failed${NC}"
|
||
fi
|
||
rm -f "$deploy_log"
|
||
rm -f imav-deploy.sh
|
||
|
||
# Try to start the service if installed
|
||
if command -v systemctl &>/dev/null && is_imunify_installed; then
|
||
echo " → Starting ImunifyAV service..."
|
||
systemctl start imunify-antivirus 2>/dev/null || true
|
||
fi
|
||
fi
|
||
else
|
||
echo -e "${RED} Failed to download installation script (network error or timeout)${NC}"
|
||
fi
|
||
|
||
if is_imunify_installed; then
|
||
echo -e "${GREEN}✓ ImunifyAV (FREE) installed${NC}"
|
||
echo " No license key required - this is the FREE version"
|
||
|
||
# Find imunify-antivirus binary
|
||
local imunify_bin=$(command -v imunify-antivirus || find /usr -name imunify-antivirus 2>/dev/null | head -1)
|
||
|
||
# Update malware signatures immediately
|
||
if [ -n "$imunify_bin" ]; then
|
||
echo " → Updating malware signatures..."
|
||
if timeout 60 "$imunify_bin" update 2>&1 | grep -qiE "updated|Success|completed"; then
|
||
echo -e " ${GREEN}✓${NC} Signatures updated"
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} Signature update inconclusive (continuing with current definitions)"
|
||
fi
|
||
fi
|
||
|
||
# ── STANDALONE: POST-INSTALL UI URL HINT ─────────────────
|
||
if [ "$imav_is_standalone" -eq 1 ] && [ -n "${imav_ui_path:-}" ]; then
|
||
echo ""
|
||
echo -e " ${CYAN}ImunifyAV UI path:${NC} $imav_ui_path"
|
||
echo " Configure your web server to serve that directory, then"
|
||
echo " access the UI at: http://YOUR-SERVER-IP/<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
|