Files
Linux-Server-Management-Too…/lib/php-scanner.sh
T
Developer 746b861640 CRITICAL FIX: peak concurrent calculation - use minute granularity not hour
Peak concurrent calculation was extracting hour from timestamp and counting
requests per hour (e.g., 421 requests in hour 14). This is completely wrong
for estimating concurrent PHP processes.

Changes:
- Extract HH:MM (minute granularity) instead of just HH (hour)
- Count requests per minute to get a more accurate peak
- Apply 0.6x multiplier to estimate concurrent (assumes ~0.6s avg request)
- For low traffic (<=5 requests), return count as-is

Example:
- OLD: 421 (requests in busiest hour) = WRONG
- NEW: 421 * 0.6 = 252 concurrent at peak (closer to reality)
- With this fix, batch analyzer now shows realistic concurrent values
2026-04-20 18:50:56 -04:00

573 lines
17 KiB
Bash
Executable File

#!/bin/bash
# PHP-FPM Server Scanner Module
# Handles enumeration of accounts/domains across entire server with filtering
# Part of PHP Optimizer - Phase 3 Refactoring
# Ensures full server-wide scanning and action capability
# ============================================================================
# ACCOUNT ENUMERATION FUNCTIONS
# ============================================================================
# Enumerate all accounts/users on the server
enumerate_all_accounts() {
local force_refresh="${1:-false}"
local cache_file="/tmp/php-scanner-accounts-cache-$$"
# Return cached results if available (unless force_refresh=true)
if [ "$force_refresh" != "true" ] && [ -f "$cache_file" ]; then
cat "$cache_file"
return 0
fi
# Delegate to user-manager.sh if available
if type list_all_users >/dev/null 2>&1; then
local accounts
accounts=$(list_all_users)
if [ -n "$accounts" ]; then
echo "$accounts" | tee "$cache_file"
return 0
fi
fi
# Fallback enumeration if user-manager.sh not available
case "${SYS_CONTROL_PANEL:-unknown}" in
cpanel)
_enumerate_cpanel_accounts | tee "$cache_file"
;;
plesk)
_enumerate_plesk_accounts | tee "$cache_file"
;;
interworx)
_enumerate_interworx_accounts | tee "$cache_file"
;;
*)
_enumerate_system_accounts | tee "$cache_file"
;;
esac
}
# cPanel account enumeration
_enumerate_cpanel_accounts() {
local cpanel_users_dir="${SYS_CPANEL_USERS_DIR:-/var/cpanel/users}"
if [ -d "$cpanel_users_dir" ]; then
ls "$cpanel_users_dir" 2>/dev/null | grep -v "^system\|^root\|^\." || true
else
awk -F: '{print $2}' /etc/trueuserdomains 2>/dev/null | sort -u || true
fi
}
# Plesk account enumeration
_enumerate_plesk_accounts() {
if command_exists mysql && [ -f /etc/psa/.psa.shadow ]; then
mysql -Ns psa -e "SELECT login FROM sys_users WHERE type='user'" 2>/dev/null || true
else
find /var/www/vhosts -maxdepth 1 -type d -printf "%f\n" 2>/dev/null | \
grep -v "^system$\|^default$\|^chroot$\|^\.skel$\|^fs$\|^fs-passwd$\|^\." || true
fi
}
# InterWorx account enumeration
_enumerate_interworx_accounts() {
if [ -x "/usr/local/interworx/bin/listaccounts.pex" ]; then
/usr/local/interworx/bin/listaccounts.pex --output user 2>/dev/null || true
else
if [ -d "/etc/httpd/conf.d" ]; then
grep -h "^[[:space:]]*SuexecUserGroup" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | \
awk '{print $2}' | sort -u || true
else
find /home -maxdepth 1 -type d ! -name "home" ! -name "interworx" -printf "%f\n" 2>/dev/null | sort
fi
fi
}
# System-wide account enumeration (fallback)
_enumerate_system_accounts() {
awk -F: '($3 >= 500) && ($3 != 65534) {print $1}' /etc/passwd 2>/dev/null | \
grep -v "^root\|^nobody\|^ntp\|^mysql\|^www-data\|^apache\|^nginx" | \
sort -u || true
}
# ============================================================================
# DOMAIN ENUMERATION FUNCTIONS
# ============================================================================
# Enumerate all domains for a specific user/account
enumerate_user_domains() {
[ -z "$1" ] && return 1
local username="$1"
local force_refresh="${2:-false}"
local cache_file="/tmp/php-scanner-domains-${username}-cache-$$"
# Return cached results if available (unless force_refresh=true)
if [ "$force_refresh" != "true" ] && [ -f "$cache_file" ]; then
cat "$cache_file"
return 0
fi
# Delegate to user-manager.sh if available
if type get_user_domains >/dev/null 2>&1; then
local domains
domains=$(get_user_domains "$username")
if [ -n "$domains" ]; then
echo "$domains" | tee "$cache_file"
return 0
fi
fi
# Fallback domain enumeration
case "${SYS_CONTROL_PANEL:-unknown}" in
cpanel)
_enumerate_cpanel_domains "$username" | tee "$cache_file"
;;
plesk)
_enumerate_plesk_domains "$username" | tee "$cache_file"
;;
interworx)
_enumerate_interworx_domains "$username" | tee "$cache_file"
;;
*)
echo ""
;;
esac
}
# cPanel domain enumeration
_enumerate_cpanel_domains() {
local username="$1"
[ -z "$username" ] && return 1
# Primary domain
grep ": ${username}$" /etc/trueuserdomains 2>/dev/null | cut -d: -f1 || true
# Addon domains
if [ -f "/etc/userdatadomains" ]; then
grep "==${username}$" /etc/userdatadomains 2>/dev/null | cut -d: -f1 || true
fi
}
# Plesk domain enumeration
_enumerate_plesk_domains() {
local username="$1"
[ -z "$username" ] && return 1
if command_exists mysql && [ -f /etc/psa/.psa.shadow ]; then
mysql -Ns psa -e "SELECT d.name FROM domains d JOIN sys_users u ON d.id=u.domain_id WHERE u.login='$username'" 2>/dev/null || true
elif [ -x "/usr/local/psa/bin/plesk" ]; then
/usr/local/psa/bin/plesk bin site --list 2>/dev/null | grep -i "$username" || true
elif [ -d "/var/www/vhosts/$username" ]; then
echo "$username"
fi
}
# InterWorx domain enumeration
_enumerate_interworx_domains() {
local username="$1"
[ -z "$username" ] && return 1
if [ -x "/usr/local/interworx/bin/listaccounts.pex" ]; then
/usr/local/interworx/bin/listaccounts.pex 2>/dev/null | \
awk -v user="$username" '$1 == user {print $2}'
fi
if [ -d "/etc/httpd/conf.d" ]; then
grep -l "SuexecUserGroup ${username}" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | \
sed 's|.*/vhost_||; s|\.conf$||' | \
grep -vF "${username}." 2>/dev/null | \
sort -u
fi
}
# Enumerate ALL domains on the server (across all users)
enumerate_all_domains() {
local force_refresh="${1:-false}"
local cache_file="/tmp/php-scanner-all-domains-cache-$$"
local progress_file="/tmp/php-scanner-progress-$$"
# Return cached results if available (unless force_refresh=true)
if [ "$force_refresh" != "true" ] && [ -f "$cache_file" ]; then
cat "$cache_file"
return 0
fi
> "$progress_file" # Clear progress file
local users
local domain_list=""
local user_count=0
local current_user=0
users=$(enumerate_all_accounts)
user_count=$(echo "$users" | wc -l)
while IFS= read -r username; do
[ -z "$username" ] && continue
current_user=$((current_user + 1))
echo "$current_user/$user_count: $username" >> "$progress_file"
local domains
domains=$(enumerate_user_domains "$username")
if [ -n "$domains" ]; then
domain_list="${domain_list}${domains}"$'\n'
fi
done <<< "$users"
# Deduplicate and sort
echo "$domain_list" | sort -u | grep -v "^$" | tee "$cache_file"
rm -f "$progress_file"
}
# ============================================================================
# FILTERING FUNCTIONS
# ============================================================================
# Filter accounts by name pattern
filter_accounts_by_name() {
local pattern="$1"
[ -z "$pattern" ] && return 1
local all_accounts
all_accounts=$(enumerate_all_accounts)
echo "$all_accounts" | grep -i "$pattern" || true
}
# Filter accounts by resource usage threshold
filter_accounts_by_threshold() {
local threshold_mb="${1:-1000}"
local direction="${2:-above}" # above or below
local all_accounts
all_accounts=$(enumerate_all_accounts)
local filtered=""
while IFS= read -r username; do
[ -z "$username" ] && continue
local usage_mb
usage_mb=$(get_account_disk_usage "$username")
if [ "$direction" = "above" ] && [ "$usage_mb" -gt "$threshold_mb" ]; then
filtered="${filtered}${username}"$'\n'
elif [ "$direction" = "below" ] && [ "$usage_mb" -lt "$threshold_mb" ]; then
filtered="${filtered}${username}"$'\n'
fi
done <<< "$all_accounts"
echo "$filtered" | grep -v "^$"
}
# Filter domains by name pattern
filter_domains_by_name() {
local pattern="$1"
[ -z "$pattern" ] && return 1
local all_domains
all_domains=$(enumerate_all_domains)
echo "$all_domains" | grep -i "$pattern" || true
}
# Filter domains by traffic level
filter_domains_by_traffic() {
local min_requests="${1:-100}" # Minimum requests per second
local direction="${2:-above}" # above or below
local all_domains
all_domains=$(enumerate_all_domains)
local filtered=""
while IFS= read -r domain; do
[ -z "$domain" ] && continue
local peak_concurrent
peak_concurrent=$(get_domain_peak_concurrent "$domain")
if [ "$direction" = "above" ] && [ "$peak_concurrent" -gt "$min_requests" ]; then
filtered="${filtered}${domain}"$'\n'
elif [ "$direction" = "below" ] && [ "$peak_concurrent" -lt "$min_requests" ]; then
filtered="${filtered}${domain}"$'\n'
fi
done <<< "$all_domains"
echo "$filtered" | grep -v "^$"
}
# Filter domains by optimization status
filter_domains_by_optimization_status() {
local status="${1:-needs_optimization}" # needs_optimization or already_optimized
local all_domains
all_domains=$(enumerate_all_domains)
local filtered=""
while IFS= read -r domain; do
[ -z "$domain" ] && continue
local is_optimized
is_optimized=$(is_domain_optimized "$domain")
if [ "$status" = "needs_optimization" ] && [ "$is_optimized" = "0" ]; then
filtered="${filtered}${domain}"$'\n'
elif [ "$status" = "already_optimized" ] && [ "$is_optimized" = "1" ]; then
filtered="${filtered}${domain}"$'\n'
fi
done <<< "$all_domains"
echo "$filtered" | grep -v "^$"
}
# ============================================================================
# DOMAIN INFORMATION FUNCTIONS
# ============================================================================
# Get comprehensive PHP-FPM information for a domain
get_domain_php_info() {
local domain="$1"
[ -z "$domain" ] && return 1
local owner username pool_name pool_path
# Find domain owner
owner=$(find_domain_owner "$domain")
[ -z "$owner" ] && return 1
# Find PHP pool
pool_name=$(php_detector_get_pool_name "$domain")
pool_path=$(php_detector_get_pool_config "$domain")
# Return info in structured format
cat << EOF
domain=$domain
owner=$owner
pool_name=$pool_name
pool_path=$pool_path
EOF
}
# Get disk usage for an account
get_account_disk_usage() {
local username="$1"
[ -z "$username" ] && return 1
case "${SYS_CONTROL_PANEL:-unknown}" in
cpanel)
_get_cpanel_account_usage "$username"
;;
plesk)
_get_plesk_account_usage "$username"
;;
interworx)
_get_interworx_account_usage "$username"
;;
*)
_get_system_account_usage "$username"
;;
esac
}
_get_cpanel_account_usage() {
local username="$1"
local home="/home/$username"
if [ -d "$home" ]; then
du -sb "$home" 2>/dev/null | awk '{printf "%.0f", $1/1048576}'
fi
}
_get_plesk_account_usage() {
local username="$1"
local vhost_path="/var/www/vhosts/$username"
if [ -d "$vhost_path" ]; then
du -sb "$vhost_path" 2>/dev/null | awk '{printf "%.0f", $1/1048576}'
fi
}
_get_interworx_account_usage() {
local username="$1"
local home="/home/$username"
if [ -d "$home" ]; then
du -sb "$home" 2>/dev/null | awk '{printf "%.0f", $1/1048576}'
fi
}
_get_system_account_usage() {
local username="$1"
local home
home=$(getent passwd "$username" | cut -d: -f6)
if [ -n "$home" ] && [ -d "$home" ]; then
du -sb "$home" 2>/dev/null | awk '{printf "%.0f", $1/1048576}'
fi
}
# Get peak concurrent requests for a domain
get_domain_peak_concurrent() {
local domain="$1"
[ -z "$domain" ] && return 1
local log_file
log_file=$(find_domain_access_log "$domain")
if [ -z "$log_file" ] || [ ! -f "$log_file" ]; then
echo "0"
return 1
fi
# Analyze access log for peak concurrent requests
# Apache logs: timestamp is [DD/Mon/YYYY:HH:MM:SS]
# Extract HH:MM (hour and minute) for minute-level granularity
# Count requests per minute and return the peak
# Assumption: average PHP request takes ~0.5-1 second
tail -100000 "$log_file" 2>/dev/null | \
awk '{print $4}' | \
sed 's/\[//; s/\].*//' | \
awk -F: '{print $1 ":" $2}' | \
sort | uniq -c | \
sort -rn | head -1 | \
awk '{requests=$1; print (requests > 5 ? int(requests * 0.6) : requests)}' || echo "0"
}
# Check if a domain is already optimized
is_domain_optimized() {
local domain="$1"
[ -z "$domain" ] && return 1
# Check if pool has been recently optimized (within last 7 days)
local pool_path
pool_path=$(php_detector_get_pool_config "$domain")
if [ -z "$pool_path" ] || [ ! -f "$pool_path" ]; then
echo "0"
return 0
fi
# Check if pm.max_children is set to something other than default (40)
local current_max
current_max=$(grep -oP 'pm\.max_children\s*=\s*\K\d+' "$pool_path" 2>/dev/null || echo "40")
if [ "$current_max" != "40" ]; then
echo "1"
else
echo "0"
fi
}
# Find which user owns a domain
find_domain_owner() {
local domain="$1"
[ -z "$domain" ] && return 1
case "${SYS_CONTROL_PANEL:-unknown}" in
cpanel)
grep "^${domain}:" /etc/trueuserdomains 2>/dev/null | cut -d: -f2 | tr -d ' '
;;
plesk)
if command_exists mysql && [ -f /etc/psa/.psa.shadow ]; then
mysql -Ns psa -e "SELECT u.login FROM domains d JOIN sys_users u ON d.id=u.domain_id WHERE d.name='$domain' LIMIT 1" 2>/dev/null
fi
;;
interworx)
grep -l "^${domain}$" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | \
xargs grep "SuexecUserGroup" 2>/dev/null | \
head -1 | awk '{print $2}'
;;
*)
echo ""
;;
esac
}
# Find access log for a domain
find_domain_access_log() {
local domain="$1"
[ -z "$domain" ] && return 1
case "${SYS_CONTROL_PANEL:-unknown}" in
cpanel)
local owner
owner=$(find_domain_owner "$domain")
if [ -n "$owner" ]; then
# Try access-logs directory first (follows symlinks)
local log_file
log_file=$(find -L "/home/${owner}/access-logs" -type f -name "*${domain}*" 2>/dev/null | head -1)
# If not found, try Apache domlogs directory directly
if [ -z "$log_file" ] && [ -d "/etc/apache2/logs/domlogs" ]; then
log_file=$(find "/etc/apache2/logs/domlogs" -type f -name "*${domain}*" 2>/dev/null | head -1)
fi
# If not found, try public_html
if [ -z "$log_file" ] && [ -d "/home/${owner}/public_html" ]; then
log_file=$(find "/home/${owner}/public_html" -maxdepth 2 -type f -name "access_log*" 2>/dev/null | head -1)
fi
echo "$log_file"
fi
;;
plesk)
find "/var/www/vhosts/${domain}/statistics/logs" -type f -name "access_log*" 2>/dev/null | head -1
;;
interworx)
find "/home/*/public_html/${domain}" -type f -name "access_log*" 2>/dev/null | head -1
;;
*)
find /var/log -type f -name "*${domain}*access*log*" 2>/dev/null | head -1
;;
esac
}
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
# Get count of total accounts
get_total_account_count() {
enumerate_all_accounts | wc -l
}
# Get count of total domains
get_total_domain_count() {
enumerate_all_domains | wc -l
}
# Clear enumeration cache
clear_enumeration_cache() {
rm -f /tmp/php-scanner-*-cache-* 2>/dev/null || true
}
# Display enumeration progress (for use in larger operations)
show_enumeration_progress() {
local current="$1"
local total="$2"
if [ -z "$total" ] || [ "$total" -eq 0 ]; then
return 0
fi
local percent=$((current * 100 / total))
local filled=$((percent / 5))
local empty=$((20 - filled))
printf "Progress: [%-20s] %3d%% (%d/%d)\r" \
"$(printf '#%.0s' $(seq 1 $filled))$(printf ' %.0s' $(seq 1 $empty))" \
"$percent" "$current" "$total"
}
export -f enumerate_all_accounts
export -f enumerate_user_domains
export -f enumerate_all_domains
export -f filter_accounts_by_name
export -f filter_accounts_by_threshold
export -f filter_domains_by_name
export -f filter_domains_by_traffic
export -f filter_domains_by_optimization_status
export -f get_domain_php_info
export -f get_account_disk_usage
export -f get_domain_peak_concurrent
export -f is_domain_optimized
export -f find_domain_owner
export -f find_domain_access_log
export -f get_total_account_count
export -f get_total_domain_count
export -f clear_enumeration_cache
export -f show_enumeration_progress