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