diff --git a/modules/security/malware-scanner.sh b/modules/security/malware-scanner.sh index b351911..4f1bf2d 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 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # Source required libraries (warn if missing, but allow graceful degradation) @@ -233,8 +235,11 @@ install_all_scanners() { # Update virus signatures immediately if [ -n "$freshclam_bin" ]; then echo " → Updating virus signatures (this may take a moment)..." - $freshclam_bin 2>&1 | grep -E "updated|Downloaded|up-to-date" || $freshclam_bin &>/dev/null - echo -e " ${GREEN}✓${NC} Signatures updated" + if "$freshclam_bin" 2>&1 | grep -qE "updated|Downloaded|up-to-date"; then + echo -e " ${GREEN}✓${NC} Signatures updated" + else + echo -e " ${YELLOW}⚠${NC} Signature update status unclear (may still be current)" + fi fi else echo -e "${RED}✗ ClamAV installation failed${NC}" @@ -305,8 +310,11 @@ install_all_scanners() { # Update malware signatures immediately echo " → Updating malware signatures..." - maldet -u 2>&1 | grep -E "update completed|signatures" || maldet -u &>/dev/null - echo -e " ${GREEN}✓${NC} Signatures updated" + if maldet -u 2>&1 | grep -qE "update completed|signatures"; then + echo -e " ${GREEN}✓${NC} Signatures updated" + else + echo -e " ${YELLOW}⚠${NC} Signature update status unclear (continuing with current definitions)" + fi else echo -e "${RED}✗ Maldet installation failed${NC}" @@ -382,8 +390,11 @@ install_all_scanners() { # Update malware signatures immediately if [ -n "$imunify_bin" ]; then echo " → Updating malware signatures..." - $imunify_bin update 2>&1 | grep -E "updated|Success|completed" || $imunify_bin update &>/dev/null - echo -e " ${GREEN}✓${NC} Signatures updated" + if "$imunify_bin" update 2>&1 | grep -qE "updated|Success|completed"; then + echo -e " ${GREEN}✓${NC} Signatures updated" + else + echo -e " ${YELLOW}⚠${NC} Signature update status unclear (continuing with current definitions)" + fi fi else echo -e "${RED}✗ ImunifyAV installation failed${NC}" @@ -398,17 +409,24 @@ install_all_scanners() { if ! is_rkhunter_installed; then echo -e "${CYAN}[4/4] Installing Rootkit Hunter...${NC}" - # Ensure EPEL repo is enabled + # Ensure repo is enabled (OS-specific) if command -v yum &>/dev/null; then - if ! rpm -qa | grep -q epel-release; then + # RHEL/CentOS - EPEL repo (only on RHEL-based systems that have rpm) + if ! rpm -qa 2>/dev/null | grep -q epel-release; then echo " → Installing EPEL repository..." - yum install -y epel-release 2>&1 | grep -E "Installing|Installed|already installed" + yum install -y epel-release 2>&1 | grep -E "Installing|Installed|already installed" || true fi # Install rkhunter - yum install -y rkhunter 2>&1 | grep -E "Installing|Installed|already installed" + yum install -y rkhunter 2>&1 | grep -E "Installing|Installed|already installed" || true elif command -v apt-get &>/dev/null; then - apt-get update && apt-get install -y rkhunter + # Debian/Ubuntu - universe repo (rkhunter is in universe) + echo " → Ensuring universe repository is enabled..." + grep -q "universe" /etc/apt/sources.list 2>/dev/null || \ + sed -i 's/^deb http/deb http universe\ndeb http/' /etc/apt/sources.list 2>/dev/null || true + + apt-get update 2>&1 | grep -E "Hit|Get|Reading|Building" | head -3 || true + apt-get install -y rkhunter 2>&1 | grep -E "Setting up|already|newest" || true fi if is_rkhunter_installed; then @@ -500,38 +518,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 } @@ -545,7 +551,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") @@ -570,13 +576,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 -F for literal matching (safe from regex injection) + domain_docroot=$(grep -F "^${domain}:" /etc/userdatadomains | cut -d= -f5 | sed 's/==/=/g') elif [ "$CONTROL_PANEL" = "plesk" ]; then domain_docroot=$(plesk bin site -i "$domain" 2>/dev/null | grep "WWW-Root" | awk '{print $2}') elif [ "$CONTROL_PANEL" = "interworx" ]; then # Find which user owns this domain using vhost configs - 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" @@ -1036,8 +1050,8 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do if [ -f "$LOG_DIR/clamav.log" ]; then current_size=$(stat -c%s "$LOG_DIR/clamav.log" 2>/dev/null || echo 0) - # 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 (optimized - avoid UUOC) + current_file=$(sed -n 's/^.*\(\/.* \).*/\1/p' "$LOG_DIR/clamav.log" 2>/dev/null | tail -1) if [ -n "$current_file" ]; then filename=$(basename "$current_file" 2>/dev/null || echo "...") @@ -1089,13 +1103,13 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do fi # Extract infected files - grep "FOUND" "$LOG_DIR/clamav.log" | cut -d: -f1 >> "$INFECTED_LIST" 2>/dev/null + 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}') + # Get scan stats from log (with validation) + FILES_SCANNED=$(grep "Scanned files:" "$LOG_DIR/clamav.log" 2>/dev/null | tail -1 | awk '{print $3}' || echo "0") CLAM_INFECTED=$(grep -c "FOUND" "$LOG_DIR/clamav.log" 2>/dev/null || echo 0) - # Validate numbers + # Validate numbers (ensure they're numeric) if ! [[ "$FILES_SCANNED" =~ ^[0-9]+$ ]]; then FILES_SCANNED=0 fi @@ -1174,9 +1188,18 @@ 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 (different systems may have different paths) + local event_log="/usr/local/maldetect/logs/event_log" + [ -f "$event_log" ] || event_log=$(find /usr -name "event_log" -type f 2>/dev/null | head -1) + + FILES_SCANNED="0" + MALDET_HITS="0" + + if [ -f "$event_log" ]; then + # Use -E instead of -P for portability (BSD grep doesn't support -P) + FILES_SCANNED=$(grep "scan completed" "$event_log" 2>/dev/null | tail -1 | grep -oE 'files [0-9]+' | awk '{print $2}' || echo "0") + MALDET_HITS=$(grep "scan completed" "$event_log" 2>/dev/null | tail -1 | grep -oE 'malware hits [0-9]+' | awk '{print $3}' || echo "0") + fi # Validate numbers if ! [[ "$FILES_SCANNED" =~ ^[0-9]+$ ]]; then