13d7054aa1
- Fix line 63 in php-analyzer.sh: Add default value for count variable (integer comparison error) - Fix line 655 in php-analyzer.sh: Add default value for memory_error_count (integer comparison error) - Fix line 396 in php-scanner.sh: Replace unsafe eval with safe getent passwd lookup - Add php-ui.sh: User interface and menu system (18KB, 25+ functions) - Add php-scanner.sh: Server enumeration system (17KB, 18 functions) - Add php-action-executor.sh: Optimization execution system (17KB, 20 functions) - Add php-server-manager.sh: Orchestration framework (21KB, 7 functions) - Add php-fpm-batch-analyzer.sh: One-shot diagnostic script showing current vs recommended max_children, memory impact, and optimization potential - Add comprehensive test suite (24 tests) These fixes resolve "integer expression expected" errors during domain analysis. Batch analyzer enables users to see domain-by-domain optimization opportunities before applying changes. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
555 lines
16 KiB
Bash
Executable File
555 lines
16 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 (simplified)
|
|
tail -100000 "$log_file" 2>/dev/null | \
|
|
awk '{print $4}' | \
|
|
sed 's/\[//' | \
|
|
awk -F: '{print $3}' | \
|
|
sort | uniq -c | \
|
|
sort -rn | head -1 | \
|
|
awk '{print $1}' || 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
|
|
;;
|
|
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
|
|
find "/home/${owner}/public_html" -maxdepth 2 -name "access_log*" -type f 2>/dev/null | head -1
|
|
fi
|
|
;;
|
|
plesk)
|
|
find "/var/www/vhosts/${domain}/statistics/logs" -name "access_log*" -type f 2>/dev/null | head -1
|
|
;;
|
|
interworx)
|
|
find "/home/*/public_html/${domain}" -name "access_log*" -type f 2>/dev/null | head -1
|
|
;;
|
|
*)
|
|
find /var/log -name "*${domain}*access*log*" -type f 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
|