diff --git a/modules/security/malware-scanner.sh b/modules/security/malware-scanner.sh index 1645f61..a72d68c 100755 --- a/modules/security/malware-scanner.sh +++ b/modules/security/malware-scanner.sh @@ -8,6 +8,8 @@ # 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' @@ -61,6 +63,71 @@ 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" ] @@ -69,7 +136,8 @@ is_imunify_installed() { is_clamav_installed() { command -v clamscan &>/dev/null || \ [ -f "/usr/local/cpanel/3rdparty/bin/clamscan" ] || \ - rpm -qa | grep -q "cpanel-clamav" + (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() { @@ -106,7 +174,7 @@ detect_scanners() { return 0 } -# Show installation instructions for missing scanners +# Show installation instructions for missing scanners (DEPRECATED - menu always shows now) show_scanner_installation_guide() { echo -e "${YELLOW}Available Malware Scanners:${NC}" echo "" @@ -183,6 +251,255 @@ show_scanner_installation_guide() { 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 "" @@ -352,13 +669,29 @@ install_all_scanners() { # Check if installation succeeded if is_maldet_installed; then - echo -e "${GREEN}✓ Maldet installed${NC}" + # 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..." - # Try to find maldet binary (might not be in PATH yet) - local maldet_bin=$(command -v maldet || find /usr/local -name maldet -type f 2>/dev/null | head -1) 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" @@ -394,69 +727,166 @@ install_all_scanners() { echo -e "${CYAN}[3/4] Installing ImunifyAV (FREE)...${NC}" echo " This may take several minutes - please wait..." - # Use deployment script method (most reliable) - cd /tmp - if [ -f "imav-deploy.sh" ]; then - rm -f imav-deploy.sh + # ── 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 - # 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}" + # ── 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 - # 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 + # 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" - # 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}" + 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 - rm -f "$deploy_log" + 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 - - # 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" + # 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 - echo -e " ${YELLOW}⚠${NC} Signature update inconclusive (continuing with current definitions)" + # Run deployment script with timeout and capture output + echo " → Running deployment script..." + local deploy_log="/tmp/imav-deploy-$$.log" + if timeout 300 bash imav-deploy.sh > "$deploy_log" 2>&1; then + # Check if any actual installation happened + if grep -qiE "installed|complete|success" "$deploy_log"; then + echo " → Deployment script executed" + else + echo " → Deployment script ran (check for errors below)" + fi + + # Show any errors from deployment + if grep -qi "error\|failed\|conflict" "$deploy_log"; then + echo -e " ${YELLOW}⚠ Warnings detected:${NC}" + grep -iE "error|failed|conflict" "$deploy_log" | sed 's/^/ /' | head -3 + fi + else + echo -e "${YELLOW} ⚠ Deployment script timed out or failed${NC}" + fi + rm -f "$deploy_log" + rm -f imav-deploy.sh + + # Try to start the service if installed + if command -v systemctl &>/dev/null && is_imunify_installed; then + echo " → Starting ImunifyAV service..." + systemctl start imunify-antivirus 2>/dev/null || true + fi + fi + else + echo -e "${RED} Failed to download installation script (network error or timeout)${NC}" + fi + + if is_imunify_installed; then + echo -e "${GREEN}✓ ImunifyAV (FREE) installed${NC}" + echo " No license key required - this is the FREE version" + + # Find imunify-antivirus binary + local imunify_bin=$(command -v imunify-antivirus || find /usr -name imunify-antivirus 2>/dev/null | head -1) + + # Update malware signatures immediately + if [ -n "$imunify_bin" ]; then + echo " → Updating malware signatures..." + if timeout 60 "$imunify_bin" update 2>&1 | grep -qiE "updated|Success|completed"; then + echo -e " ${GREEN}✓${NC} Signatures updated" + else + echo -e " ${YELLOW}⚠${NC} Signature update inconclusive (continuing with current definitions)" + fi + fi + + # ── STANDALONE: POST-INSTALL UI URL HINT ───────────────── + if [ "$imav_is_standalone" -eq 1 ] && [ -n "${imav_ui_path:-}" ]; then + echo "" + echo -e " ${CYAN}ImunifyAV UI path:${NC} $imav_ui_path" + echo " Configure your web server to serve that directory, then" + echo " access the UI at: http://YOUR-SERVER-IP//" + echo " (Replace with the last component of the path above)" + fi + # ── END POST-INSTALL HINT ───────────────────────────────── + + else + echo -e "${RED}✗ ImunifyAV installation failed${NC}" + if [ "$imav_is_standalone" -eq 1 ]; then + echo -e "${YELLOW} Note: Verify integration.conf at $imav_conf_file is correct${NC}" + echo -e "${YELLOW} and that $imav_ui_path is accessible by your web server.${NC}" + else + echo -e "${YELLOW} Note: ImunifyAV FREE is primarily supported on CloudLinux, cPanel, and Plesk systems${NC}" fi fi - else - echo -e "${RED}✗ ImunifyAV installation failed${NC}" - echo -e "${YELLOW} Note: ImunifyAV FREE is primarily supported on CloudLinux, cPanel, and Plesk systems${NC}" + fi + # ── END CANCELLED GUARD ─────────────────────────────────────────── + else echo -e "${GREEN}✓ ImunifyAV already installed${NC}" fi @@ -492,6 +922,7 @@ install_all_scanners() { 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 @@ -590,38 +1021,26 @@ detect_control_panel() { return 0 } -# Remove subdirectory docroots from array +# Remove subdirectory docroots from array (optimized single-pass algorithm) sanitize_docroots() { - remove_docroot=() - - for search_value in "${docroot_array[@]}"; do - # Count how many paths contain this value - count=$(printf '%s\n' "${docroot_array[@]}" | grep -c "$search_value" || true) - - if [ "$count" -gt 1 ]; then - # Find subdirectories and mark for removal - while IFS= read -r subdir; do - if [ "$subdir" != "$search_value" ]; then - remove_docroot+=("$subdir") - fi - done < <(printf '%s\n' "${docroot_array[@]}" | grep "$search_value") - fi - done - - # Build sanitized array sanitized_docroot=() - for docroot in "${docroot_array[@]}"; do - # Check if this docroot is in remove list - skip=0 - for remove in "${remove_docroot[@]}"; do - if [ "$docroot" = "$remove" ]; then - skip=1 + + # 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 - if [ "${skip:-0}" -eq 0 ]; then - sanitized_docroot+=("$docroot") + # Only add if it's NOT a subdirectory of another path + if [ $is_subdir -eq 0 ]; then + sanitized_docroot+=("$current") fi done } @@ -635,7 +1054,7 @@ get_user_docroots() { while IFS= read -r line; do docroot=$(awk -F'==' '{print $5}' <<< "$line") [ -n "$docroot" ] && [ -d "$docroot" ] && user_docroots+=("$docroot") - done < <(grep ":.*${username}==" /etc/userdatadomains | cut -d: -f2- | sort -u) + 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") @@ -660,13 +1079,21 @@ get_domain_docroot() { local domain_docroot="" if [ "$CONTROL_PANEL" = "cpanel" ]; then - domain_docroot=$(grep "^${domain}:" /etc/userdatadomains | cut -d= -f5 | sed 's/==/=/g') + # 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 - local username=$(grep -l "ServerName ${domain}" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | head -1 | \ - xargs grep "SuexecUserGroup" 2>/dev/null | awk '{print $2}') + # 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" @@ -731,6 +1158,7 @@ generate_standalone_scanner() { # Create standalone scan script cat > "$session_dir/scan.sh" << 'STANDALONE_EOF' #!/bin/bash +set -o pipefail ################################################################################ # Standalone Malware Scanner @@ -744,13 +1172,50 @@ 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" @@ -761,6 +1226,14 @@ log_message() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$SESSION_LOG" } +# Confirm user action (y/n prompt) +confirm() { + local prompt="$1" + local response + read -t 10 -p "$prompt (y/n): " response /dev/null || response="n" + [[ "$response" =~ ^[Yy]$ ]] +} + # Activity spinner for long-running scans show_spinner() { local pid=$1 @@ -768,12 +1241,12 @@ show_spinner() { local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' local i=0 - while kill -0 $pid 2>/dev/null; do + while kill -0 "$pid" 2>/dev/null; do i=$(( (i+1) % 10 )) - printf "\r ⏳ $message ${spin:$i:1} " + printf "\r ⏳ %s %s " "$message" "${spin:$i:1}" sleep 0.2 done - printf "\r ✓ $message - Complete\n" + printf "\r ✓ %s - Complete\n" "$message" } # Format elapsed time @@ -831,7 +1304,7 @@ cleanup_on_exit() { fi # Save interrupted status (only if summary file directory exists) - if [ $exit_code -ne 0 ] && [ -d "$RESULTS_DIR" ]; then + if [ "$exit_code" -ne 0 ] && [ -d "$RESULTS_DIR" ]; then { echo "" echo "SCAN INTERRUPTED" @@ -892,24 +1365,75 @@ else echo "→ Installing Rootkit Hunter (temporary, will be removed after scan)..." if command -v yum &>/dev/null; then - # Ensure EPEL is available + # Ensure EPEL is available for RHEL-based systems if ! rpm -qa | grep -q epel-release; then - yum install -y epel-release &>/dev/null + log_message "RKHunter: Installing EPEL repository..." + yum install -y epel-release &>/dev/null || log_message "WARNING: EPEL install failed" fi - # Install rkhunter - yum install -y rkhunter &>/dev/null - - if command -v rkhunter &>/dev/null; then + # Install rkhunter via yum + log_message "RKHunter: Installing via yum..." + if yum install -y rkhunter &>/dev/null; then # Update definitions and initialize baseline - rkhunter --update &>/dev/null - rkhunter --propupd &>/dev/null + 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 @@ -1005,7 +1529,7 @@ if [ "$AVAILABLE_MB" -lt 100 ]; then 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 + read -t 10 -p "Continue anyway? (y/N): " continue_scan /dev/null || continue_scan="n" if [[ ! "$continue_scan" =~ ^[Yy]$ ]]; then log_message "Scan cancelled due to low disk space" echo "Scan cancelled." @@ -1085,7 +1609,6 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do echo "" IMUNIFY_INFECTED=0 - FILES_SCANNED=0 # Run ImunifyAV scan with timeout (2 hours max) # Use --output-format to make parsing easier, with --timeout to prevent hanging @@ -1099,13 +1622,18 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do echo " Scanning: $path" # Run scan with timeout to prevent hanging on status checks - # ImunifyAV scan - output to log file - timeout 7200 imunify-antivirus malware on-demand scan --path="$path" &>> "$LOG_DIR/imunify.log" & + # 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) - wait $IMUNIFY_PID - IMUNIFY_EXIT=$? + 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" @@ -1117,15 +1645,38 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do done # Try to get count of malicious files from malicious list (if available) - # Run once with timeout (60s) - capture output and exit code together - IMUNIFY_INFECTED=$(timeout 60 imunify-antivirus malware malicious list 2>/dev/null | tail -n +2 | wc -l) + # FIXED Issue 4B: Defensive header detection + malicious_output=$(timeout 60 imunify-antivirus malware malicious list 2>/dev/null) IMUNIFY_MALICIOUS_EXIT=$? - # Validate the count - if [ "$IMUNIFY_MALICIOUS_EXIT" -ne 0 ] || ! [[ "$IMUNIFY_INFECTED" =~ ^[0-9]+$ ]]; then - IMUNIFY_INFECTED=0 - log_message "WARNING: Failed to get ImunifyAV malicious count (exit: $IMUNIFY_MALICIOUS_EXIT)" - fi + 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)) @@ -1148,7 +1699,7 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do log_message "ClamAV: Starting scan with activity monitoring" echo "" - echo " 📁 Scanning path(s): ${SCAN_PATHS[*]}" + echo " 📁 Scanning path(s): ${SCAN_PATHS[@]}" echo " ⏳ Scanner: ClamAV (comprehensive virus scan...)" echo "" @@ -1162,13 +1713,17 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do last_filename="" stall_counter=0 - while kill -0 $CLAM_PID 2>/dev/null; do + 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 - current_size=$(stat -c%s "$LOG_DIR/clamav.log" 2>/dev/null || echo 0) + # 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 - current_file=$(tail -1 "$LOG_DIR/clamav.log" 2>/dev/null | grep -o '/[^:]*' | head -1) + # 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 "...") @@ -1176,7 +1731,7 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do if [ "$filename" != "$last_filename" ]; then elapsed=$(($(date +%s) - SCAN_START)) printf "\r Scanning: %s | Elapsed: %s " \ - "${filename:0:50}" "$(format_time $elapsed)" + "${filename:0:50}" "$(format_time "$elapsed")" last_filename="$filename" fi fi @@ -1184,7 +1739,7 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do # 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 + 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 @@ -1197,8 +1752,13 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do done # Wait for scan to complete and get exit code - wait $CLAM_PID - CLAM_EXIT=$? + 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 @@ -1219,21 +1779,21 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do continue fi - # Extract infected files - grep "FOUND" "$LOG_DIR/clamav.log" | cut -d: -f1 >> "$INFECTED_LIST" 2>/dev/null + # 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 - FILES_SCANNED=$(grep "Scanned files:" "$LOG_DIR/clamav.log" | tail -1 | awk '{print $3}') - CLAM_INFECTED=$(grep -c "FOUND" "$LOG_DIR/clamav.log" 2>/dev/null || echo 0) + # 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 - if ! [[ "$FILES_SCANNED" =~ ^[0-9]+$ ]]; then - FILES_SCANNED=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 $FILES_SCANNED files" + 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" @@ -1251,7 +1811,7 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do log_message "Maldet: Starting scan with live progress" echo "" - echo " 📁 Scanning path(s): ${SCAN_PATHS[*]}" + echo " 📁 Scanning path(s): ${SCAN_PATHS[@]}" echo " ⏳ Scanner: Maldet/LMD (Linux-specific malware detection...)" echo "" @@ -1259,8 +1819,7 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do # 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 - TOTAL_MALDET_FILES=0 - TOTAL_MALDET_HITS=0 + MALDET_PIDS=() for path in "${SCAN_PATHS[@]}"; do if [ ! -d "$path" ]; then @@ -1271,16 +1830,24 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do log_message "Maldet: Scanning $path with -a (all files)" # Run with -a (scan-all) for comprehensive scanning - # Timeout after 2 hours per path - timeout 7200 maldet -b -a "$path" &>> "$LOG_DIR/maldet.log" - exit_code=$? + # 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+=($!) - if [ "$exit_code" -ne 0 ]; then - MALDET_EXIT=$exit_code + # 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 - - # Give scan a moment to complete - sleep 2 done echo "" # New line after progress @@ -1305,13 +1872,46 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do # Extract scan results from event log (more reliable than parsing output) # Maldet logs to /usr/local/maldetect/logs/event_log - # Use proper fallback: assign first, then check if empty and fallback to 0 - FILES_SCANNED=$(grep "scan completed" /usr/local/maldetect/logs/event_log 2>/dev/null | tail -1 | grep -oP 'files \K[0-9]+') || FILES_SCANNED="0" - MALDET_HITS=$(grep "scan completed" /usr/local/maldetect/logs/event_log 2>/dev/null | tail -1 | grep -oP 'malware hits \K[0-9]+') || MALDET_HITS="0" + # 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 ! [[ "$FILES_SCANNED" =~ ^[0-9]+$ ]]; then - FILES_SCANNED=0 + if ! [[ "$MALDET_FILES_SCANNED" =~ ^[0-9]+$ ]]; then + MALDET_FILES_SCANNED=0 fi if ! [[ "$MALDET_HITS" =~ ^[0-9]+$ ]]; then MALDET_HITS=0 @@ -1319,7 +1919,7 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do SCAN_END=$(date +%s) DURATION=$((SCAN_END - SCAN_START)) - echo " ✓ Scanned $FILES_SCANNED files" + 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" @@ -1343,18 +1943,21 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do # Run with timeout (30 minutes, RKHunter is usually fast) # Show test names as they run - timeout 1800 rkhunter --check --skip-keypress --report-warnings-only 2>&1 | tee -a "$LOG_DIR/rkhunter.log" | \ + # 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]}" - printf "\r → %-60s" "${test_name:0:60}" + test_name="${BASH_REMATCH[1]:-}" + [ -n "$test_name" ] && printf "\r → %-60s" "${test_name:0:60}" elif [[ "$line" =~ ^Scanning\ (.+)$ ]]; then - scan_item="${BASH_REMATCH[1]}" - printf "\r → Scanning: %-50s" "${scan_item:0:50}" + scan_item="${BASH_REMATCH[1]:-}" + [ -n "$scan_item" ] && printf "\r → Scanning: %-50s" "${scan_item:0:50}" fi - done - RKH_EXIT=$? + done < <(echo "$output" | tee -a "$LOG_DIR/rkhunter.log") echo "" # New line after test display if [ "$RKH_EXIT" -eq 124 ]; then @@ -1370,11 +1973,14 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do echo " ⚠️ Scan completed with warnings (exit code: $RKH_EXIT)" fi - # Extract warnings - RKH_WARNINGS=$(grep -c "Warning:" "$LOG_DIR/rkhunter.log" 2>/dev/null || echo 0) + # 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 - grep "Rootkit" "$LOG_DIR/rkhunter.log" | grep -i "found" >> "$INFECTED_LIST" 2>/dev/null + # 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)) @@ -1390,7 +1996,7 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do ((SCANNERS_COMPLETED++)) # Wait between scanners - if [ ${SCANNERS_COMPLETED:-0} -lt $TOTAL_SCANNERS ]; then + if [ "${SCANNERS_COMPLETED:-0}" -lt "$TOTAL_SCANNERS" ]; then echo "Waiting 3 seconds before next scanner..." sleep 3 fi @@ -1442,87 +2048,16 @@ done echo "ACTION REQUIRED: Review and quarantine/remove infected files" echo "" - # IP Reputation Integration: Flag IPs that uploaded malware - echo "────────────────────────────────────────" - echo "Analyzing upload sources..." - 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 - # Correlate infected files with Apache logs to find uploading IPs - flagged_ips=0 - while read -r infected_file; do - # Extract file path components - filename=$(basename "$infected_file") - filepath=$(dirname "$infected_file") - - # Try to find corresponding Apache access logs - # Look for POST requests to the directory containing the infected file - - # Use system-detected log directory with control panel-specific search - if [ "$CONTROL_PANEL" = "interworx" ]; then - # InterWorx: Search /home/*/var/*/logs/transfer.log (VERIFIED: uses 'transfer.log') - # Search last 7 days of logs for POST requests to this path - find /home/*/var/*/logs -type f -name 'transfer.log' 2>/dev/null | while read -r logfile; do - # Check if this log corresponds to the domain/user - grep -h "POST.*${filepath}" "$logfile" 2>/dev/null | tail -20 | while read -r logline; do - # Extract IP from Apache log line - ip=$(awk '{print $1}' <<< "$logline") - if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - # Flag this IP in reputation database - if type flag_ip_attack &>/dev/null; then - flag_ip_attack "$ip" "RCE" 25 "Malware scanner: Uploaded $filename" >/dev/null 2>&1 - echo " → Flagged IP: $ip (uploaded to $filepath)" >> "$LOG_DIR/flagged_ips.log" - ((flagged_ips++)) - fi - fi - done - done - elif [ "$CONTROL_PANEL" = "plesk" ]; then - # Plesk: Search /var/www/vhosts/*/logs/access*log - # Plesk stores logs in /var/www/vhosts/domain.com/logs/access_log or access_ssl_log - find /var/www/vhosts/*/logs -type f \( -name 'access_log' -o -name 'access_ssl_log' \) 2>/dev/null | while read -r logfile; do - # Check if this log corresponds to the domain/user - grep -h "POST.*${filepath}" "$logfile" 2>/dev/null | tail -20 | while read -r logline; do - # Extract IP from Apache log line - ip=$(awk '{print $1}' <<< "$logline") - if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - # Flag this IP in reputation database - if type flag_ip_attack &>/dev/null; then - flag_ip_attack "$ip" "RCE" 25 "Malware scanner: Uploaded $filename" >/dev/null 2>&1 - echo " → Flagged IP: $ip (uploaded to $filepath)" >> "$LOG_DIR/flagged_ips.log" - ((flagged_ips++)) - fi - fi - done - done - elif [ "$CONTROL_PANEL" = "cpanel" ]; then - # cPanel: Search domlogs directory - # cPanel stores logs as domain.com, domain.net, etc. in /var/log/apache2/domlogs/ - if [ -n "$SYS_LOG_DIR" ] && [ -d "$SYS_LOG_DIR" ]; then - find "$SYS_LOG_DIR" -type f \( -name '*.com' -o -name '*.net' -o -name '*.org' -o -name '*.info' -o -name '*.biz' \) 2>/dev/null | while read -r logfile; do - # Check if this log corresponds to the domain/user - grep -h "POST.*${filepath}" "$logfile" 2>/dev/null | tail -20 | while read -r logline; do - # Extract IP from Apache log line - ip=$(awk '{print $1}' <<< "$logline") - if [ -n "$ip" ] && [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - # Flag this IP in reputation database - if type flag_ip_attack &>/dev/null; then - flag_ip_attack "$ip" "RCE" 25 "Malware scanner: Uploaded $filename" >/dev/null 2>&1 - echo " → Flagged IP: $ip (uploaded to $filepath)" >> "$LOG_DIR/flagged_ips.log" - ((flagged_ips++)) - fi - fi - done - done - fi - fi - done < <(sort -u "$INFECTED_LIST" | head -20) # Limit to first 20 files to avoid long processing - - if [ "${flagged_ips:-0}" -gt 0 ]; then - echo "✓ Flagged $flagged_ips IPs in reputation database" - echo " (See $LOG_DIR/flagged_ips.log for details)" - else - echo " No upload IPs identified (files may be older than log retention)" - fi echo "" else echo "✓ No infected files detected by automated scan." @@ -1600,9 +2135,9 @@ else # Inline client report generation for standalone scripts client_report_file="$RESULTS_DIR/client_report.txt" - # Extract scan info - 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 "/home") + # 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 @@ -1710,7 +2245,7 @@ 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 "" +read -t 30 -p "" /dev/null || true # Keep screen session alive with an interactive shell echo "" @@ -1730,7 +2265,7 @@ STANDALONE_EOF # Escape special characters for sed (handle /, \, &, |, $) # CRITICAL FIX: Must escape the delimiter (|) as well since we use it in the sed command - local escaped_paths=$(printf '%s\n' "$paths_declaration" | sed -e 's/[\/&|]/\\&/g') + 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}" @@ -1836,8 +2371,8 @@ STANDALONE_EOF sleep 1 - # Verify screen started - if screen -list | grep -q "$session_id"; then + # 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 "" @@ -1986,27 +2521,52 @@ launch_standalone_scanner_menu() { echo "" echo "Scanning all user home directories..." - # Determine user base directory based on control panel - local user_base_dir + # Determine user directories based on control panel + # Each panel has different home directory structures + scan_paths=() case "$CONTROL_PANEL" in plesk) - user_base_dir="/var/www/vhosts" + # 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|interworx|standalone) - user_base_dir="/home" + 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" ;; *) - user_base_dir="/home" + # 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 - # Add the user base directory to scan paths - scan_paths=("$user_base_dir") - scan_description="all user accounts in $user_base_dir" + # 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 directory: $user_base_dir" - echo "Scan scope: All user home directories" + echo "User directories found: ${#scan_paths[@]}" + echo "Scan scope: $scan_description" ;; 3) @@ -2021,9 +2581,10 @@ launch_standalone_scanner_menu() { return 1 fi - # Get user's docroots + # Get user's docroots (FIXED: more specific path matching to avoid false matches like 'test' matching 'username_test') for docroot in "${sanitized_docroot[@]}"; do - if [[ "$docroot" == *"/$SELECTED_USER/"* ]]; then + # 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 @@ -2049,9 +2610,10 @@ launch_standalone_scanner_menu() { return 1 fi - # Find docroot for domain + # Find docroot for domain (FIXED: more specific matching to distinguish 'example' from 'example-prod' or 'prefix_example') for docroot in "${sanitized_docroot[@]}"; do - if [[ "$docroot" == *"/$domain"* ]] || [[ "$docroot" == *"/$domain/"* ]]; then + # 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 @@ -2311,6 +2873,172 @@ delete_standalone_sessions() { } # 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 @@ -2318,7 +3046,7 @@ show_scan_menu() { return 1 fi - # Build reference database once for the entire menu session + # 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 @@ -2341,46 +3069,57 @@ show_scan_menu() { fi echo "" - echo -e "${CYAN}Create New Scan:${NC}" - echo -e " ${CYAN}1.${NC} Scan entire server (ClamAV, Maldet, RKHunter)" - echo -e " ${CYAN}2.${NC} Scan all user accounts (All scanners - recommended)" - echo -e " ${CYAN}3.${NC} Scan specific user account (All scanners)" - echo -e " ${CYAN}4.${NC} Scan specific domain (All scanners)" - echo -e " ${CYAN}5.${NC} Scan custom path (All scanners)" + echo -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}6.${NC} Check scan status" - echo -e " ${CYAN}7.${NC} View scan results" - echo -e " ${CYAN}8.${NC} Delete scan sessions" + 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}9.${NC} Install all scanners" - echo -e " ${CYAN}10.${NC} Scanner settings" + 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-10): " choice + read -p "Select option (0-14): " choice - if ! [[ "$choice" =~ ^([0-9]|10)$ ]]; then + if ! [[ "$choice" =~ ^([0-9]|1[0-4])$ ]]; then echo -e "${RED}Invalid option${NC}" sleep 1 continue fi case $choice in - 1) launch_standalone_scanner_menu "server"; break ;; - 2) launch_standalone_scanner_menu "all_users"; break ;; - 3) launch_standalone_scanner_menu "user"; break ;; - 4) launch_standalone_scanner_menu "domain"; break ;; - 5) launch_standalone_scanner_menu "custom"; break ;; - 6) check_standalone_status; break ;; - 7) view_scan_results; break ;; - 8) delete_standalone_sessions; break ;; - 9) install_all_scanners; break ;; - 10) scanner_settings; break ;; + 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