From 0eadb5f3168e2271de68f44a3311145589e7c965 Mon Sep 17 00:00:00 2001 From: cschantz Date: Tue, 11 Nov 2025 18:43:31 -0500 Subject: [PATCH] Add comprehensive malware scanner module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Multi-scanner support: ImunifyAV, ClamAV, Maldet (LMD) - Scan scopes: Entire server, specific user, domain, or custom path - Auto-detect control panel (cPanel, Plesk, Interworx) - Smart docroot detection with subdirectory filtering - Memory safety checks before large scans - Organized scan logging and result viewing - Integrates with user-manager and reference database Menu path: Security & Threat Analysis → Analysis & Troubleshooting → Web Application Analysis → Malware Scanner Based on provided malware scanning code with toolkit standardization. --- modules/security/malware-scanner.sh | 622 ++++++++++++++++++++++++++++ 1 file changed, 622 insertions(+) create mode 100755 modules/security/malware-scanner.sh diff --git a/modules/security/malware-scanner.sh b/modules/security/malware-scanner.sh new file mode 100755 index 0000000..9070885 --- /dev/null +++ b/modules/security/malware-scanner.sh @@ -0,0 +1,622 @@ +#!/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 +################################################################################ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$SCRIPT_DIR/lib/common-functions.sh" 2>/dev/null || true +source "$SCRIPT_DIR/lib/system-detect.sh" 2>/dev/null || true +source "$SCRIPT_DIR/lib/user-manager.sh" 2>/dev/null || true + +# Arrays for docroots and scanners +declare -a docroot_array +declare -a sanitized_docroot +declare -a remove_docroot +declare -a available_scanners + +# Scanner detection +detect_scanners() { + available_scanners=() + + if command -v imunify-antivirus &>/dev/null; then + available_scanners+=("imunify") + fi + + if command -v clamscan &>/dev/null; then + available_scanners+=("clamav") + fi + + if command -v maldet &>/dev/null; then + available_scanners+=("maldet") + fi + + if [ ${#available_scanners[@]} -eq 0 ]; then + echo -e "${RED}No malware scanners detected!${NC}" + echo "" + echo "Available scanners to install:" + echo " • ImunifyAV - Commercial, real-time protection" + echo " • ClamAV - Open source antivirus" + echo " • Maldet (LMD) - Linux Malware Detect" + echo "" + return 1 + fi + + return 0 +} + +# Detect control panel and gather docroots +detect_control_panel() { + docroot_array=() + + # Detect cPanel + if [ -f "/etc/userdatadomains" ]; then + CONTROL_PANEL="cpanel" + export PATH=/usr/local/cpanel/3rdparty/bin/:$PATH + + while IFS= read -r docroot; do + [ -n "$docroot" ] && docroot_array+=("$docroot") + done < <(cut -d= -f5 /etc/userdatadomains | sed 's/==/=/g' | sort -u) + + # Detect Plesk + elif [ -f "/usr/local/psa/version" ]; then + CONTROL_PANEL="plesk" + + 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) + + # Detect Interworx + elif [ -d "/usr/local/interworx/" ]; then + CONTROL_PANEL="interworx" + + while IFS= read -r docroot; do + [ -n "$docroot" ] && docroot_array+=("$docroot") + done < <(grep -rh "DocumentRoot" /etc/httpd/conf* 2>/dev/null | grep -Ev '^\s*#|/var/www/html($|$)' | sed 's/DocumentRoot//g' | tr -d " " | 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 +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 + break + fi + done + + if [ $skip -eq 0 ]; then + sanitized_docroot+=("$docroot") + 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 docroot; do + [ -n "$docroot" ] && user_docroots+=("$docroot") + done < <(grep "^${username}:" /etc/userdatadomains | cut -d= -f5 | sed 's/==/=/g' | sort -u) + else + echo -e "${RED}User-specific scanning only supported on cPanel${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 + domain_docroot=$(grep "^${domain}:" /etc/userdatadomains | cut -d= -f5 | sed 's/==/=/g') + elif [ "$CONTROL_PANEL" = "plesk" ]; then + domain_docroot=$(plesk bin site -i "$domain" 2>/dev/null | grep "WWW-Root" | awk '{print $2}') + else + echo -e "${RED}Domain lookup only supported on cPanel/Plesk${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 +scan_imunify() { + local scan_paths=("$@") + + if ! command -v imunify-antivirus &>/dev/null; then + echo -e "${RED}ImunifyAV not installed${NC}" + return 1 + fi + + echo -e "${CYAN}Starting ImunifyAV scan...${NC}" + echo "" + + # Update signatures + echo "→ Updating signatures..." + imunify-antivirus update 2>/dev/null + + # Queue scan paths + for path in "${scan_paths[@]}"; do + if [ -d "$path" ]; then + echo "→ Queuing: $path" + imunify-antivirus malware on-demand queue put "$path" 2>/dev/null + fi + done + + echo "" + echo -e "${GREEN}✓ Scan queued${NC}" + echo "" + echo "Monitor progress:" + echo " imunify-antivirus malware on-demand list" +} + +# ClamAV scanner +scan_clamav() { + local scan_paths=("$@") + + if ! command -v clamscan &>/dev/null; then + echo -e "${RED}ClamAV not installed${NC}" + return 1 + fi + + # Create log directory + local log_dir="$SCRIPT_DIR/logs/malware-scans" + mkdir -p "$log_dir" + + local log_file="$log_dir/clamav_$(date +%Y%m%d_%H%M%S).log" + + echo -e "${CYAN}Starting ClamAV scan...${NC}" + echo "" + + # Update signatures + if command -v freshclam &>/dev/null; then + echo "→ Updating signatures..." + freshclam 2>/dev/null || true + fi + + echo "→ Scanning paths..." + echo "" + + # Log scan details + { + echo "ClamAV Malware Scan" + echo "Date: $(date)" + echo "Paths:" + printf '%s\n' "${scan_paths[@]}" + echo "" + echo "Results:" + echo "========================================" + } > "$log_file" + + # Run scan + clamscan --infected --recursive "${scan_paths[@]}" >> "$log_file" 2>&1 & + local scan_pid=$! + + echo "Scan running in background (PID: $scan_pid)" + echo "Log file: $log_file" + echo "" + echo "Monitor with: tail -f $log_file" + + # Store scan info in reference DB + store_reference "malware_scan_clamav_latest" "$log_file" +} + +# Maldet scanner +scan_maldet() { + local scan_paths=("$@") + + if ! command -v maldet &>/dev/null; then + echo -e "${RED}Maldet not installed${NC}" + return 1 + fi + + # Create temp file with paths + local path_file="/tmp/maldet_paths_$$.txt" + printf '%s\n' "${scan_paths[@]}" > "$path_file" + + echo -e "${CYAN}Starting Maldet scan...${NC}" + echo "" + + # Update signatures + echo "→ Updating signatures..." + maldet -u 2>/dev/null || true + + echo "→ Starting scan..." + maldet -b -f "$path_file" + + rm -f "$path_file" + + echo "" + echo "View results:" + echo " maldet -l" +} + +# Main scan menu +show_scan_menu() { + while true; do + print_banner "Malware Scanner" + + echo "Available Scanners:" + for scanner in "${available_scanners[@]}"; do + echo " • ${scanner^}" + done + echo "" + + echo "Scan Scope:" + echo " 1. Scan entire server" + echo " 2. Scan specific user" + echo " 3. Scan specific domain" + echo " 4. Scan custom path" + echo "" + echo " 5. View scan results" + echo " 6. Scanner settings" + echo "" + echo " 0. Back to main menu" + echo "" + + read -p "Select option: " choice + + case $choice in + 1) scan_entire_server ;; + 2) scan_user_account ;; + 3) scan_domain ;; + 4) scan_custom_path ;; + 5) view_scan_results ;; + 6) scanner_settings ;; + 0) return 0 ;; + *) echo -e "${RED}Invalid option${NC}"; sleep 1 ;; + esac + done +} + +# Scan entire server +scan_entire_server() { + echo "" + print_header "Full Server Scan" + + if ! detect_control_panel; then + read -p "Press Enter to continue..." + return 1 + fi + + echo "Control Panel: ${CONTROL_PANEL^}" + echo "Docroots found: ${#sanitized_docroot[@]}" + echo "" + + if ! check_memory; then + read -p "Press Enter to continue..." + return 1 + fi + + echo "Select scanner:" + local i=1 + for scanner in "${available_scanners[@]}"; do + echo " $i. ${scanner^}" + ((i++)) + done + echo "" + + read -p "Scanner: " scanner_choice + + if [ "$scanner_choice" -lt 1 ] || [ "$scanner_choice" -gt ${#available_scanners[@]} ]; then + echo -e "${RED}Invalid choice${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + local selected_scanner="${available_scanners[$((scanner_choice-1))]}" + + case "$selected_scanner" in + imunify) scan_imunify "${sanitized_docroot[@]}" ;; + clamav) scan_clamav "${sanitized_docroot[@]}" ;; + maldet) scan_maldet "${sanitized_docroot[@]}" ;; + esac + + echo "" + read -p "Press Enter to continue..." +} + +# Scan user account +scan_user_account() { + echo "" + print_header "Scan User Account" + + if ! detect_control_panel; then + read -p "Press Enter to continue..." + return 1 + fi + + # Use user manager to select user + select_user_interactive "Select user to scan" + + if [ -z "$SELECTED_USER" ]; then + echo "No user selected" + read -p "Press Enter to continue..." + return 1 + fi + + echo "" + echo "Getting docroots for: $SELECTED_USER" + + local user_paths=($(get_user_docroots "$SELECTED_USER")) + + if [ ${#user_paths[@]} -eq 0 ]; then + echo -e "${RED}No docroots found for user${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + echo "Paths to scan: ${#user_paths[@]}" + printf ' %s\n' "${user_paths[@]}" + echo "" + + echo "Select scanner:" + local i=1 + for scanner in "${available_scanners[@]}"; do + echo " $i. ${scanner^}" + ((i++)) + done + echo "" + + read -p "Scanner: " scanner_choice + + if [ "$scanner_choice" -lt 1 ] || [ "$scanner_choice" -gt ${#available_scanners[@]} ]; then + echo -e "${RED}Invalid choice${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + local selected_scanner="${available_scanners[$((scanner_choice-1))]}" + + case "$selected_scanner" in + imunify) scan_imunify "${user_paths[@]}" ;; + clamav) scan_clamav "${user_paths[@]}" ;; + maldet) scan_maldet "${user_paths[@]}" ;; + esac + + echo "" + read -p "Press Enter to continue..." +} + +# Scan domain +scan_domain() { + echo "" + print_header "Scan Domain" + + if ! detect_control_panel; then + read -p "Press Enter to continue..." + return 1 + fi + + read -p "Enter domain name: " domain + + if [ -z "$domain" ]; then + echo "No domain entered" + read -p "Press Enter to continue..." + return 1 + fi + + local domain_path=$(get_domain_docroot "$domain") + + if [ -z "$domain_path" ] || [ ! -d "$domain_path" ]; then + echo -e "${RED}Domain not found or docroot doesn't exist${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + echo "Docroot: $domain_path" + echo "" + + echo "Select scanner:" + local i=1 + for scanner in "${available_scanners[@]}"; do + echo " $i. ${scanner^}" + ((i++)) + done + echo "" + + read -p "Scanner: " scanner_choice + + if [ "$scanner_choice" -lt 1 ] || [ "$scanner_choice" -gt ${#available_scanners[@]} ]; then + echo -e "${RED}Invalid choice${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + local selected_scanner="${available_scanners[$((scanner_choice-1))]}" + + case "$selected_scanner" in + imunify) scan_imunify "$domain_path" ;; + clamav) scan_clamav "$domain_path" ;; + maldet) scan_maldet "$domain_path" ;; + esac + + echo "" + read -p "Press Enter to continue..." +} + +# Scan custom path +scan_custom_path() { + echo "" + print_header "Scan Custom Path" + + read -p "Enter path to scan: " custom_path + + if [ -z "$custom_path" ] || [ ! -d "$custom_path" ]; then + echo -e "${RED}Path doesn't exist${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + echo "Path: $custom_path" + echo "" + + echo "Select scanner:" + local i=1 + for scanner in "${available_scanners[@]}"; do + echo " $i. ${scanner^}" + ((i++)) + done + echo "" + + read -p "Scanner: " scanner_choice + + if [ "$scanner_choice" -lt 1 ] || [ "$scanner_choice" -gt ${#available_scanners[@]} ]; then + echo -e "${RED}Invalid choice${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + local selected_scanner="${available_scanners[$((scanner_choice-1))]}" + + case "$selected_scanner" in + imunify) scan_imunify "$custom_path" ;; + clamav) scan_clamav "$custom_path" ;; + maldet) scan_maldet "$custom_path" ;; + esac + + echo "" + read -p "Press Enter to continue..." +} + +# View scan results +view_scan_results() { + echo "" + print_header "Scan Results" + + 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 + + if [ "$scanner_choice" -lt 1 ] || [ "$scanner_choice" -gt ${#available_scanners[@]} ]; then + echo -e "${RED}Invalid choice${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 + + 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..." +} + +# Main execution +main() { + if ! detect_scanners; then + exit 1 + fi + + show_scan_menu +} + +# Run if executed directly +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + main "$@" +fi