File: //opt/cpanel-security.sh
#!/bin/bash
#
# cPanel / WHM Linux Security Audit
# Version: 1.0
#
# PURPOSE:
# Read-only security audit for cPanel/WHM Linux servers.
#
# IMPORTANT:
# - No service restarts
# - No configuration changes
# - No package installation/update
# - No firewall changes
# - No remediation
# - The only intentional filesystem write is the HTML report.
#
set +e
SCRIPT_VERSION="1.0"
REPORT_DATE="$(date '+%Y-%m-%d')"
REPORT_TIME="$(date '+%Y-%m-%d %H:%M:%S %Z')"
DEFAULT_REPORT="/usr/local/apache/htdocs/cpanel-security-audit-${REPORT_DATE}.html"
REPORT_FILE="${1:-$DEFAULT_REPORT}"
if [ "$(id -u)" -ne 0 ]; then
echo "[FAIL] This audit must be run as root."
exit 1
fi
REPORT_DIR="$(dirname "$REPORT_FILE")"
if [ ! -d "$REPORT_DIR" ]; then
echo "[FAIL] Report directory does not exist: $REPORT_DIR"
echo "[INFO] No directory will be created by this read-only audit."
exit 1
fi
if [ ! -w "$REPORT_DIR" ]; then
echo "[FAIL] Report directory is not writable: $REPORT_DIR"
exit 1
fi
# ============================================================
# Counters
# ============================================================
PASS_COUNT=0
WARN_COUNT=0
FAIL_COUNT=0
INFO_COUNT=0
# ============================================================
# Helper functions
# ============================================================
html_escape() {
printf '%s' "$1" |
sed \
-e 's/&/\&/g' \
-e 's/</\</g' \
-e 's/>/\>/g' \
-e 's/"/\"/g' \
-e "s/'/\'/g"
}
status_class() {
case "$1" in
PASS) echo "pass" ;;
WARN) echo "warn" ;;
FAIL) echo "fail" ;;
INFO) echo "info" ;;
*) echo "info" ;;
esac
}
print_terminal_status() {
local status="$1"
local title="$2"
local detail="$3"
printf '[%-4s] %-32s %s\n' "$status" "$title" "$detail"
case "$status" in
PASS) PASS_COUNT=$((PASS_COUNT + 1)) ;;
WARN) WARN_COUNT=$((WARN_COUNT + 1)) ;;
FAIL) FAIL_COUNT=$((FAIL_COUNT + 1)) ;;
INFO) INFO_COUNT=$((INFO_COUNT + 1)) ;;
esac
}
add_result() {
local title="$1"
local status="$2"
local detail="$3"
local recommendation="$4"
local class
class="$(status_class "$status")"
print_terminal_status "$status" "$title" "$detail"
cat >> "$REPORT_FILE" <<EOF
<div class="result ${class}">
<div class="result-head">
<span class="badge ${class}">$(html_escape "$status")</span>
<strong>$(html_escape "$title")</strong>
</div>
<div class="detail">$(html_escape "$detail")</div>
<div class="recommendation"><b>Recommendation:</b> $(html_escape "$recommendation")</div>
</div>
EOF
}
section_start() {
local title="$1"
echo
echo "============================================================"
echo "$title"
echo "============================================================"
cat >> "$REPORT_FILE" <<EOF
<section>
<h2>$(html_escape "$title")</h2>
EOF
}
section_end() {
printf '</section>\n' >> "$REPORT_FILE"
}
add_table_header() {
cat >> "$REPORT_FILE" <<EOF
<table>
<tr>
EOF
}
add_table_cell() {
printf '<td>%s</td>\n' "$(html_escape "$1")" >> "$REPORT_FILE"
}
add_table_header_cell() {
printf '<th>%s</th>\n' "$(html_escape "$1")" >> "$REPORT_FILE"
}
# ============================================================
# HTML Header
# ============================================================
cat > "$REPORT_FILE" <<EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cPanel Security Audit - $(html_escape "$(hostname -s 2>/dev/null)") - ${REPORT_DATE}</title>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 0;
background: #f4f6f8;
color: #202124;
}
.container {
max-width: 1400px;
margin: auto;
padding: 24px;
}
header {
background: #ffffff;
padding: 24px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 1px 4px rgba(0,0,0,.08);
}
h1 {
margin: 0 0 8px 0;
}
h2 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 2px solid #ddd;
}
h3 {
margin-top: 24px;
}
section {
background: #ffffff;
padding: 20px;
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.summary {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 18px;
}
.summary-box {
min-width: 120px;
padding: 14px;
border-radius: 8px;
background: #f1f3f4;
}
.summary-box b {
display: block;
font-size: 24px;
margin-bottom: 4px;
}
.result {
border-left: 5px solid #777;
padding: 12px 15px;
margin: 10px 0;
background: #fafafa;
border-radius: 5px;
}
.result.pass { border-left-color: #198754; }
.result.warn { border-left-color: #f0ad00; }
.result.fail { border-left-color: #dc3545; }
.result.info { border-left-color: #0d6efd; }
.result-head {
display: flex;
gap: 10px;
align-items: center;
}
.badge {
display: inline-block;
min-width: 48px;
text-align: center;
padding: 4px 7px;
border-radius: 4px;
color: #fff;
font-size: 12px;
font-weight: bold;
}
.badge.pass { background: #198754; }
.badge.warn { background: #f0ad00; }
.badge.fail { background: #dc3545; }
.badge.info { background: #0d6efd; }
.detail {
margin-top: 8px;
white-space: pre-wrap;
word-break: break-word;
}
.recommendation {
margin-top: 7px;
color: #555;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
font-size: 14px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
vertical-align: top;
}
th {
background: #f1f3f4;
}
pre {
white-space: pre-wrap;
word-break: break-word;
background: #f6f8fa;
padding: 12px;
border-radius: 6px;
}
footer {
color: #666;
text-align: center;
padding: 20px;
font-size: 12px;
}
.small {
color: #666;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>cPanel / WHM Linux Security Audit</h1>
<div class="small">Audit Date: $(html_escape "$REPORT_TIME")</div>
<div class="small">Hostname: $(html_escape "$(hostname -f 2>/dev/null)")</div>
<div class="small">Script Version: $(html_escape "$SCRIPT_VERSION")</div>
<div class="small">Mode: Read-only audit; HTML report generation is the only intended filesystem write.</div>
<div class="summary">
<div class="summary-box"><b id="pass-count">0</b>PASS</div>
<div class="summary-box"><b id="warn-count">0</b>WARN</div>
<div class="summary-box"><b id="fail-count">0</b>FAIL</div>
<div class="summary-box"><b id="info-count">0</b>INFO</div>
</div>
</header>
EOF
echo "[INFO] Starting cPanel security audit..."
echo "[INFO] Report: $REPORT_FILE"
echo
# ============================================================
# 1. Server Information
# ============================================================
section_start "1. Server Information"
HOSTNAME_FQDN="$(hostname -f 2>/dev/null)"
OS_NAME="$(grep '^PRETTY_NAME=' /etc/os-release 2>/dev/null | cut -d= -f2- | tr -d '"')"
KERNEL="$(uname -r 2>/dev/null)"
ARCH="$(uname -m 2>/dev/null)"
UPTIME="$(uptime -p 2>/dev/null)"
CPANEL_VERSION="$(cat /usr/local/cpanel/version 2>/dev/null)"
add_table_header
add_table_header_cell "Item"
add_table_header_cell "Value"
printf '</tr>\n' >> "$REPORT_FILE"
for item in \
"Hostname|$HOSTNAME_FQDN" \
"Operating System|$OS_NAME" \
"Kernel|$KERNEL" \
"Architecture|$ARCH" \
"Uptime|$UPTIME" \
"cPanel Version|$CPANEL_VERSION"
do
IFS='|' read -r key value <<< "$item"
printf '<tr>' >> "$REPORT_FILE"
add_table_cell "$key"
add_table_cell "$value"
printf '</tr>\n' >> "$REPORT_FILE"
done
printf '</table>\n' >> "$REPORT_FILE"
section_end
# ============================================================
# 2. SSH Security
# ============================================================
section_start "2. SSH Security"
SSH_PORT=""
PERMIT_ROOT=""
PASSWORD_AUTH=""
PUBKEY_AUTH=""
if command -v sshd >/dev/null 2>&1; then
SSHD_T="$(sshd -T 2>/dev/null)"
SSH_PORT="$(printf '%s\n' "$SSHD_T" | awk '$1=="port"{print $2; exit}')"
PERMIT_ROOT="$(printf '%s\n' "$SSHD_T" | awk '$1=="permitrootlogin"{print $2; exit}')"
PASSWORD_AUTH="$(printf '%s\n' "$SSHD_T" | awk '$1=="passwordauthentication"{print $2; exit}')"
PUBKEY_AUTH="$(printf '%s\n' "$SSHD_T" | awk '$1=="pubkeyauthentication"{print $2; exit}')"
fi
if [ -z "$SSH_PORT" ]; then
SSH_PORT="$(grep -Ei '^[[:space:]]*Port[[:space:]]+' /etc/ssh/sshd_config 2>/dev/null |
tail -1 | awk '{print $2}')"
fi
if [ -z "$PERMIT_ROOT" ]; then
PERMIT_ROOT="$(grep -Ei '^[[:space:]]*PermitRootLogin[[:space:]]+' /etc/ssh/sshd_config 2>/dev/null |
tail -1 | awk '{print tolower($2)}')"
fi
if [ -z "$PASSWORD_AUTH" ]; then
PASSWORD_AUTH="$(grep -Ei '^[[:space:]]*PasswordAuthentication[[:space:]]+' /etc/ssh/sshd_config 2>/dev/null |
tail -1 | awk '{print tolower($2)}')"
fi
if [ -z "$PUBKEY_AUTH" ]; then
PUBKEY_AUTH="$(grep -Ei '^[[:space:]]*PubkeyAuthentication[[:space:]]+' /etc/ssh/sshd_config 2>/dev/null |
tail -1 | awk '{print tolower($2)}')"
fi
if [ "$PERMIT_ROOT" = "no" ]; then
add_result "Direct Root SSH Authentication" "PASS" \
"PermitRootLogin is disabled." \
"Keep direct root SSH login disabled and use controlled administrative accounts."
elif [ -n "$PERMIT_ROOT" ]; then
add_result "Direct Root SSH Authentication" "WARN" \
"PermitRootLogin is set to $PERMIT_ROOT." \
"Disable direct root SSH login where operationally possible."
else
add_result "Direct Root SSH Authentication" "INFO" \
"Unable to determine PermitRootLogin." \
"Review the effective SSH configuration."
fi
if [ "$PUBKEY_AUTH" = "yes" ]; then
add_result "SSH Key Authentication" "INFO" \
"SSH public-key authentication is enabled." \
"Review authorized keys and remove unused keys."
elif [ "$PUBKEY_AUTH" = "no" ]; then
add_result "SSH Key Authentication" "WARN" \
"SSH public-key authentication is disabled." \
"Consider using SSH keys for administrative access."
else
add_result "SSH Key Authentication" "INFO" \
"Unable to determine SSH public-key authentication state." \
"Review the effective SSH configuration."
fi
if [ "$PASSWORD_AUTH" = "no" ]; then
add_result "SSH Password Authentication" "PASS" \
"SSH password authentication is disabled." \
"Continue using key-based authentication where practical."
elif [ "$PASSWORD_AUTH" = "yes" ]; then
add_result "SSH Password Authentication" "WARN" \
"SSH password authentication is enabled." \
"Consider disabling SSH password authentication after validating key-based access."
else
add_result "SSH Password Authentication" "INFO" \
"Unable to determine SSH password authentication state." \
"Review the effective SSH configuration."
fi
if [ "$SSH_PORT" = "22" ]; then
add_result "SSH Port" "WARN" \
"SSH is listening on the default port 22." \
"Consider using an alternate SSH port as part of the server hardening baseline."
elif [ -n "$SSH_PORT" ]; then
add_result "SSH Port" "PASS" \
"SSH configured port: $SSH_PORT." \
"Keep the SSH port documented and restricted to trusted sources where possible."
else
add_result "SSH Port" "INFO" \
"Unable to determine SSH port." \
"Review the effective SSH configuration."
fi
section_end
# ============================================================
# 3. PHP Security
# ============================================================
section_start "3. PHP Security"
PHP_FOUND=0
for PHP_BIN in /opt/cpanel/ea-php*/root/usr/bin/php; do
[ -x "$PHP_BIN" ] || continue
PHP_FOUND=1
PHP_VERSION="$("$PHP_BIN" -r 'echo PHP_VERSION;' 2>/dev/null)"
PHP_INI="$("$PHP_BIN" --ini 2>/dev/null | awk -F': ' '/Loaded Configuration File/{print $2}')"
DISABLE_FUNCTIONS="$("$PHP_BIN" -r 'echo ini_get("disable_functions");' 2>/dev/null)"
ALLOW_URL_INCLUDE="$("$PHP_BIN" -r 'echo ini_get("allow_url_include");' 2>/dev/null)"
EXPOSE_PHP="$("$PHP_BIN" -r 'echo ini_get("expose_php");' 2>/dev/null)"
if [ -n "$DISABLE_FUNCTIONS" ]; then
add_result "PHP $PHP_VERSION - disable_functions" "PASS" \
"disable_functions is configured: $DISABLE_FUNCTIONS" \
"Review the list periodically and ensure dangerous functions are restricted according to the hosting security baseline."
else
add_result "PHP $PHP_VERSION - disable_functions" "WARN" \
"disable_functions is empty." \
"Review whether dangerous PHP functions should be disabled for this hosting environment."
fi
if [ "$ALLOW_URL_INCLUDE" = "0" ] || [ "$ALLOW_URL_INCLUDE" = "Off" ]; then
add_result "PHP $PHP_VERSION - allow_url_include" "PASS" \
"allow_url_include is disabled." \
"Keep allow_url_include disabled unless a documented application requirement exists."
else
add_result "PHP $PHP_VERSION - allow_url_include" "WARN" \
"allow_url_include is $ALLOW_URL_INCLUDE." \
"Disable allow_url_include unless specifically required."
fi
if [ "$EXPOSE_PHP" = "0" ] || [ "$EXPOSE_PHP" = "Off" ]; then
add_result "PHP $PHP_VERSION - expose_php" "PASS" \
"expose_php is disabled." \
"Keep PHP version exposure disabled."
else
add_result "PHP $PHP_VERSION - expose_php" "WARN" \
"expose_php is $EXPOSE_PHP." \
"Disable expose_php to reduce unnecessary version disclosure."
fi
cat >> "$REPORT_FILE" <<EOF
<div class="small">PHP binary: $(html_escape "$PHP_BIN") | Loaded php.ini: $(html_escape "$PHP_INI")</div>
EOF
done
if [ "$PHP_FOUND" -eq 0 ]; then
add_result "EA-PHP Versions" "INFO" \
"No EasyApache PHP binaries were detected under /opt/cpanel." \
"Verify PHP installation manually if PHP is expected on this server."
fi
section_end
# ============================================================
# 4. cPHulk
# ============================================================
section_start "4. cPHulk"
CPHULK_STATUS=""
if command -v whmapi1 >/dev/null 2>&1; then
CPHULK_RAW="$(whmapi1 --output=json cphulk_status 2>/dev/null)"
CPHULK_STATUS="$(printf '%s\n' "$CPHULK_RAW" |
grep -oE '"is_enabled"[[:space:]]*:[[:space:]]*(0|1|true|false)' |
head -1 |
sed -E 's/.*:[[:space:]]*//')"
fi
case "$CPHULK_STATUS" in
1|true)
add_result "cPHulk" "PASS" \
"cPHulk appears to be enabled." \
"Keep cPHulk enabled and review its protection settings."
;;
0|false)
add_result "cPHulk" "WARN" \
"cPHulk appears to be disabled." \
"Enable cPHulk after validating compatibility with the server's access requirements."
;;
*)
add_result "cPHulk" "INFO" \
"Unable to determine cPHulk state from the available WHM API output." \
"Verify cPHulk status in WHM."
;;
esac
section_end
# ============================================================
# 5. Shell Fork Bomb Protection
# ============================================================
section_start "5. Shell Fork Bomb Protection"
FORK_LIMIT_PROFILE=""
if [ -d /var/cpanel/login_profile/limits ] ||
[ -f /var/cpanel/login_profile/limits ] ||
[ -d /usr/local/cpanel/etc/login_profile/limits ] ||
[ -f /usr/local/cpanel/etc/login_profile/limits ]; then
FORK_LIMIT_PROFILE="present"
fi
if [ "$FORK_LIMIT_PROFILE" = "present" ]; then
add_result "Shell Fork Bomb Protection" "PASS" \
"cPanel login profile limits configuration was detected." \
"Keep the cPanel shell limits profile enabled and periodically verify its configuration."
else
add_result "Shell Fork Bomb Protection" "WARN" \
"The cPanel login profile limits configuration was not detected in the expected locations." \
"Verify the Shell Fork Bomb Protection / limits login profile in cPanel."
fi
section_end
# ============================================================
# 6. SMTP Security
# ============================================================
section_start "6. SMTP Security"
SMTP_RESTRICTION=""
if [ -f /var/cpanel/cpanel.config ]; then
SMTP_RESTRICTION="$(grep -Ei '^smtpmailgidonly=' /var/cpanel/cpanel.config 2>/dev/null |
tail -1 | cut -d= -f2)"
fi
if [ "$SMTP_RESTRICTION" = "1" ]; then
add_result "SMTP Restrictions" "PASS" \
"SMTP mail gid restriction is enabled." \
"Keep SMTP restrictions enabled to limit direct outbound SMTP abuse."
elif [ "$SMTP_RESTRICTION" = "0" ]; then
add_result "SMTP Restrictions" "WARN" \
"SMTP mail gid restriction is disabled." \
"Review the SMTP restriction setting in WHM."
else
add_result "SMTP Restrictions" "INFO" \
"Unable to determine the SMTP restriction setting." \
"Review SMTP Restrictions in WHM."
fi
section_end
# ============================================================
# 7. WHM Password Security
# ============================================================
section_start "7. WHM Password Security"
MIN_PW_STRENGTH=""
if [ -f /var/cpanel/cpanel.config ]; then
MIN_PW_STRENGTH="$(grep -Ei '^minpwstrength=' /var/cpanel/cpanel.config 2>/dev/null |
tail -1 | cut -d= -f2)"
fi
if [[ "$MIN_PW_STRENGTH" =~ ^[0-9]+$ ]]; then
if [ "$MIN_PW_STRENGTH" -ge 50 ]; then
add_result "Minimum Password Strength" "PASS" \
"Configured minimum password strength: $MIN_PW_STRENGTH." \
"Maintain a strong password-strength baseline and review it periodically."
else
add_result "Minimum Password Strength" "WARN" \
"Configured minimum password strength: $MIN_PW_STRENGTH." \
"Consider increasing the minimum password-strength requirement."
fi
else
add_result "Minimum Password Strength" "INFO" \
"Unable to determine minimum password strength." \
"Review Password Strength Configuration in WHM."
fi
section_end
# ============================================================
# 8. Compiler Access
# ============================================================
section_start "8. Compiler Security"
GCC_PATH="/usr/bin/gcc"
if [ -e "$GCC_PATH" ]; then
GCC_OWNER="$(stat -c '%U:%G' "$GCC_PATH" 2>/dev/null)"
GCC_MODE="$(stat -c '%a' "$GCC_PATH" 2>/dev/null)"
if [ "$GCC_OWNER" = "root:compiler" ] && [ "$GCC_MODE" = "750" ]; then
add_result "Compiler Access" "PASS" \
"gcc ownership/mode is $GCC_OWNER $GCC_MODE." \
"Keep compiler access restricted to authorized users/groups."
else
add_result "Compiler Access" "WARN" \
"gcc exists with ownership/mode $GCC_OWNER $GCC_MODE." \
"Review compiler access against the organization's shared-hosting security baseline."
fi
else
add_result "Compiler Access" "PASS" \
"gcc was not found at /usr/bin/gcc." \
"No compiler was detected at the standard path."
fi
section_end
# ============================================================
# 9. /tmp Security
# ============================================================
section_start "9. /tmp Security"
if [ -e /var/cpanel/disabled/securetmp ]; then
add_result "Secure /tmp" "WARN" \
"The cPanel securetmp disabled marker was detected." \
"Verify the securetmp configuration and mount protection in WHM."
else
TMP_MOUNT="$(mount 2>/dev/null | grep -E ' on /tmp( |$)')"
VARTMP_MOUNT="$(mount 2>/dev/null | grep -E ' on /var/tmp( |$)')"
if printf '%s\n' "$TMP_MOUNT" | grep -q '/usr/tmpDSK' &&
printf '%s\n' "$VARTMP_MOUNT" | grep -q '/usr/tmpDSK'; then
add_result "Secure /tmp" "PASS" \
"Both /tmp and /var/tmp appear to use the cPanel tmp disk mount." \
"Keep temporary directories protected with appropriate mount options."
else
add_result "Secure /tmp" "INFO" \
"Securetmp marker is absent, but expected tmpDSK mounts were not both detected." \
"Review /tmp and /var/tmp mounts and their security options."
fi
fi
section_end
# ============================================================
# 10. ModSecurity
# ============================================================
section_start "10. ModSecurity"
MODSECURITY_MODULE=""
if command -v httpd >/dev/null 2>&1; then
MODSECURITY_MODULE="$(httpd -M 2>/dev/null | grep -i 'security2_module')"
fi
if [ -n "$MODSECURITY_MODULE" ]; then
add_result "ModSecurity" "PASS" \
"Apache security2_module is loaded." \
"Keep ModSecurity enabled and maintain current rules."
else
add_result "ModSecurity" "WARN" \
"Apache security2_module was not detected." \
"Review ModSecurity configuration in WHM."
fi
section_end
# ============================================================
# 11. Imunify Security
# ============================================================
section_start "11. Imunify Security"
IMUNIFY_FOUND=0
if command -v imunify-antivirus >/dev/null 2>&1; then
IMUNIFY_FOUND=1
ACTIVE_SERVICE=""
for service in imunify-antivirus imunify-antivirus.service; do
if systemctl is-active --quiet "$service" 2>/dev/null; then
ACTIVE_SERVICE="$service"
break
fi
done
if [ -n "$ACTIVE_SERVICE" ]; then
add_result "ImunifyAV" "PASS" \
"ImunifyAV is installed and its service appears active ($ACTIVE_SERVICE)." \
"Keep malware protection active and review malware detections regularly."
else
add_result "ImunifyAV" "WARN" \
"ImunifyAV is installed, but an active service was not detected." \
"Verify the ImunifyAV service status."
fi
MALWARE_OUTPUT="$(imunify-antivirus malware malicious list --limit 100 2>/dev/null)"
INFECTED_OUTPUT="$(imunify-antivirus malware infected-domains --limit 100 2>/dev/null)"
MALWARE_DATA="$(printf '%s\n' "$MALWARE_OUTPUT" |
grep -vE '^[[:space:]]*$|^[[:space:]]*(ID|ID[[:space:]]+|No data|Nothing|Total|Showing|Malicious|Domain|Error|Usage)' |
grep -vE '^[[:space:]]*-+[[:space:]]*$' |
head -20)"
INFECTED_DATA="$(printf '%s\n' "$INFECTED_OUTPUT" |
grep -vE '^[[:space:]]*$|^[[:space:]]*(ID|ID[[:space:]]+|No data|Nothing|Total|Showing|Infected|Domain|Error|Usage)' |
grep -vE '^[[:space:]]*-+[[:space:]]*$' |
head -20)"
if [ -n "$MALWARE_DATA" ]; then
add_result "ImunifyAV Malware Results" "WARN" \
"The ImunifyAV CLI returned data that may contain malware detections. Review the detailed CLI output below." \
"Investigate each detection and follow the approved malware-remediation process."
printf '<h3>ImunifyAV Malware CLI Output</h3><pre>%s</pre>\n' \
"$(html_escape "$MALWARE_OUTPUT")" >> "$REPORT_FILE"
else
add_result "ImunifyAV Malware Results" "PASS" \
"No malware result rows were detected in the CLI output." \
"Continue monitoring ImunifyAV malware detections."
fi
if [ -n "$INFECTED_DATA" ]; then
add_result "ImunifyAV Infected Domains" "WARN" \
"The ImunifyAV CLI returned infected-domain data. Review the detailed output below." \
"Investigate affected domains using the approved malware-remediation process."
printf '<h3>ImunifyAV Infected Domains CLI Output</h3><pre>%s</pre>\n' \
"$(html_escape "$INFECTED_OUTPUT")" >> "$REPORT_FILE"
else
add_result "ImunifyAV Infected Domains" "PASS" \
"No infected-domain result rows were detected in the CLI output." \
"Continue monitoring infected-domain results."
fi
fi
if command -v imunify360-agent >/dev/null 2>&1; then
IMUNIFY_FOUND=1
ACTIVE_SERVICE=""
for service in imunify-antivirus imunify360 imunify360-agent imunify360-agent.service; do
if systemctl is-active --quiet "$service" 2>/dev/null; then
ACTIVE_SERVICE="$service"
break
fi
done
if [ -n "$ACTIVE_SERVICE" ]; then
add_result "Imunify360" "PASS" \
"Imunify360 is installed and an active service was detected ($ACTIVE_SERVICE)." \
"Keep Imunify360 active and review malware detections regularly."
else
add_result "Imunify360" "WARN" \
"Imunify360 is installed, but an active service was not detected." \
"Verify the Imunify360 service status."
fi
MALWARE_OUTPUT="$(imunify360-agent malware malicious list --limit 100 2>/dev/null)"
INFECTED_OUTPUT="$(imunify360-agent malware infected-domains --limit 100 2>/dev/null)"
MALWARE_DATA="$(printf '%s\n' "$MALWARE_OUTPUT" |
grep -vE '^[[:space:]]*$|^[[:space:]]*(ID|ID[[:space:]]+|No data|Nothing|Total|Showing|Malicious|Domain|Error|Usage)' |
grep -vE '^[[:space:]]*-+[[:space:]]*$' |
head -20)"
INFECTED_DATA="$(printf '%s\n' "$INFECTED_OUTPUT" |
grep -vE '^[[:space:]]*$|^[[:space:]]*(ID|ID[[:space:]]+|No data|Nothing|Total|Showing|Infected|Domain|Error|Usage)' |
grep -vE '^[[:space:]]*-+[[:space:]]*$' |
head -20)"
if [ -n "$MALWARE_DATA" ]; then
add_result "Imunify360 Malware Results" "WARN" \
"The Imunify360 CLI returned data that may contain malware detections. Review the detailed CLI output below." \
"Investigate each detection and follow the approved malware-remediation process."
printf '<h3>Imunify360 Malware CLI Output</h3><pre>%s</pre>\n' \
"$(html_escape "$MALWARE_OUTPUT")" >> "$REPORT_FILE"
else
add_result "Imunify360 Malware Results" "PASS" \
"No malware result rows were detected in the CLI output." \
"Continue monitoring Imunify360 malware detections."
fi
if [ -n "$INFECTED_DATA" ]; then
add_result "Imunify360 Infected Domains" "WARN" \
"The Imunify360 CLI returned infected-domain data. Review the detailed output below." \
"Investigate affected domains using the approved malware-remediation process."
printf '<h3>Imunify360 Infected Domains CLI Output</h3><pre>%s</pre>\n' \
"$(html_escape "$INFECTED_OUTPUT")" >> "$REPORT_FILE"
else
add_result "Imunify360 Infected Domains" "PASS" \
"No infected-domain result rows were detected in the CLI output." \
"Continue monitoring infected-domain results."
fi
fi
if [ "$IMUNIFY_FOUND" -eq 0 ]; then
add_result "Imunify Security" "INFO" \
"Neither ImunifyAV nor Imunify360 CLI was detected." \
"Verify whether malware protection is provided by another approved security product."
fi
section_end
# ============================================================
# 12. PHP-FPM
# ============================================================
section_start "12. PHP-FPM"
PHPFPM_FOUND=0
for FPM_BIN in /opt/cpanel/ea-php*/root/usr/sbin/php-fpm*; do
[ -x "$FPM_BIN" ] || continue
PHPFPM_FOUND=1
break
done
if [ "$PHPFPM_FOUND" -eq 1 ]; then
add_result "PHP-FPM" "PASS" \
"EasyApache PHP-FPM binaries were detected." \
"Keep PHP-FPM versions maintained and review pool resource limits."
else
add_result "PHP-FPM" "INFO" \
"EasyApache PHP-FPM binaries were not detected." \
"Verify PHP-FPM requirements for hosted applications."
fi
section_end
# ============================================================
# 13. Apache Security
# ============================================================
section_start "13. Apache Security"
HTTPD_CONF_DIR=""
if [ -d /etc/apache2 ]; then
HTTPD_CONF_DIR="/etc/apache2"
elif [ -d /etc/httpd ]; then
HTTPD_CONF_DIR="/etc/httpd"
fi
SERVER_TOKENS=""
SERVER_SIGNATURE=""
TRACE_ENABLE=""
if [ -n "$HTTPD_CONF_DIR" ]; then
SERVER_TOKENS="$(grep -RHiE '^[[:space:]]*ServerTokens[[:space:]]+' "$HTTPD_CONF_DIR" 2>/dev/null |
tail -1 | awk '{print $2}')"
SERVER_SIGNATURE="$(grep -RHiE '^[[:space:]]*ServerSignature[[:space:]]+' "$HTTPD_CONF_DIR" 2>/dev/null |
tail -1 | awk '{print $2}')"
TRACE_ENABLE="$(grep -RHiE '^[[:space:]]*TraceEnable[[:space:]]+' "$HTTPD_CONF_DIR" 2>/dev/null |
tail -1 | awk '{print $2}')"
fi
case "$(echo "$SERVER_TOKENS" | tr '[:upper:]' '[:lower:]')" in
prod)
add_result "Apache ServerTokens" "PASS" \
"ServerTokens is configured as Prod." \
"Keep Apache version/product disclosure minimized."
;;
"")
add_result "Apache ServerTokens" "INFO" \
"No explicit ServerTokens directive was detected." \
"Review the effective Apache configuration and consider a minimal disclosure setting."
;;
*)
add_result "Apache ServerTokens" "WARN" \
"ServerTokens is configured as $SERVER_TOKENS." \
"Consider using ServerTokens Prod for reduced information disclosure."
;;
esac
case "$(echo "$SERVER_SIGNATURE" | tr '[:upper:]' '[:lower:]')" in
off)
add_result "Apache ServerSignature" "PASS" \
"ServerSignature is disabled." \
"Keep ServerSignature disabled."
;;
"")
add_result "Apache ServerSignature" "INFO" \
"No explicit ServerSignature directive was detected." \
"Review the effective Apache configuration."
;;
*)
add_result "Apache ServerSignature" "WARN" \
"ServerSignature is configured as $SERVER_SIGNATURE." \
"Disable ServerSignature."
;;
esac
case "$(echo "$TRACE_ENABLE" | tr '[:upper:]' '[:lower:]')" in
off)
add_result "Apache TraceEnable" "PASS" \
"TraceEnable is disabled." \
"Keep HTTP TRACE disabled unless specifically required."
;;
"")
add_result "Apache TraceEnable" "INFO" \
"No explicit TraceEnable directive was detected." \
"Review the effective Apache configuration."
;;
*)
add_result "Apache TraceEnable" "WARN" \
"TraceEnable is configured as $TRACE_ENABLE." \
"Disable HTTP TRACE unless specifically required."
;;
esac
HEADERS_MODULE=""
if command -v httpd >/dev/null 2>&1; then
HEADERS_MODULE="$(httpd -M 2>/dev/null | grep -i 'headers_module')"
fi
if [ -n "$HEADERS_MODULE" ]; then
add_result "Apache mod_headers" "PASS" \
"headers_module is loaded." \
"Keep required security headers managed through the approved Apache configuration."
else
add_result "Apache mod_headers" "INFO" \
"headers_module was not detected." \
"Verify whether mod_headers is required for the site's security-header baseline."
fi
section_end
# ============================================================
# 14. CSF
# ============================================================
section_start "14. CSF"
CSF_INSTALLED=0
CSF_RUNNING=0
LFD_RUNNING=0
if [ -x /usr/sbin/csf ] || [ -x /usr/local/cpanel/whostmgr/docroot/cgi/configserver/csf.cgi ]; then
CSF_INSTALLED=1
fi
if command -v csf >/dev/null 2>&1; then
CSF_INSTALLED=1
fi
if systemctl is-active --quiet lfd 2>/dev/null ||
systemctl is-active --quiet lfd.service 2>/dev/null; then
LFD_RUNNING=1
fi
if command -v csf >/dev/null 2>&1; then
if csf -l >/dev/null 2>&1; then
CSF_RUNNING=1
fi
fi
if [ "$CSF_INSTALLED" -eq 1 ]; then
if [ "$CSF_RUNNING" -eq 1 ] || [ "$LFD_RUNNING" -eq 1 ]; then
add_result "CSF Firewall" "PASS" \
"CSF/LFD installation was detected and firewall/daemon activity appears present." \
"Keep CSF/LFD maintained and review firewall rules periodically."
else
add_result "CSF Firewall" "WARN" \
"CSF is installed but active firewall/daemon status was not confirmed." \
"Verify CSF/LFD service status in the server."
fi
else
add_result "CSF Firewall" "INFO" \
"CSF was not detected." \
"Verify which approved firewall provider protects the server."
fi
section_end
# ============================================================
# 15. Firewall Provider
# ============================================================
section_start "15. Firewall Provider"
FIREWALL_PROVIDER=""
if [ "$CSF_INSTALLED" -eq 1 ]; then
FIREWALL_PROVIDER="CSF"
fi
if systemctl is-active --quiet firewalld 2>/dev/null; then
if [ -n "$FIREWALL_PROVIDER" ]; then
FIREWALL_PROVIDER="$FIREWALL_PROVIDER + firewalld"
else
FIREWALL_PROVIDER="firewalld"
fi
fi
if command -v nft >/dev/null 2>&1; then
if nft list ruleset >/dev/null 2>&1; then
if [ -n "$FIREWALL_PROVIDER" ]; then
FIREWALL_PROVIDER="$FIREWALL_PROVIDER + nftables"
else
FIREWALL_PROVIDER="nftables"
fi
fi
fi
if command -v iptables >/dev/null 2>&1; then
if iptables -L -n >/dev/null 2>&1; then
if [ -n "$FIREWALL_PROVIDER" ]; then
FIREWALL_PROVIDER="$FIREWALL_PROVIDER + iptables"
else
FIREWALL_PROVIDER="iptables"
fi
fi
fi
if [ -n "$FIREWALL_PROVIDER" ]; then
add_result "Firewall Provider" "INFO" \
"Detected firewall components: $FIREWALL_PROVIDER." \
"Document the authoritative firewall provider and avoid overlapping firewall management."
else
add_result "Firewall Provider" "WARN" \
"No active firewall provider could be confirmed." \
"Verify the server firewall configuration."
fi
section_end
# ============================================================
# 16. Kernel Review
# ============================================================
section_start "16. Kernel Review"
RUNNING_KERNEL="$(uname -r 2>/dev/null)"
INSTALLED_KERNELS=""
if command -v rpm >/dev/null 2>&1; then
INSTALLED_KERNELS="$(rpm -q kernel 2>/dev/null | sort -V)"
fi
if [ -n "$INSTALLED_KERNELS" ]; then
LATEST_KERNEL_PACKAGE="$(printf '%s\n' "$INSTALLED_KERNELS" | tail -1)"
LATEST_KERNEL_VERSION="$(printf '%s\n' "$LATEST_KERNEL_PACKAGE" |
sed 's/^kernel-//')"
if printf '%s\n' "$INSTALLED_KERNELS" | grep -Fq "$RUNNING_KERNEL"; then
add_result "Kernel Packages" "INFO" \
"Installed kernel package(s) were detected. Running kernel: $RUNNING_KERNEL. Latest installed package: $LATEST_KERNEL_PACKAGE." \
"Review installed kernels and reboot when an approved newer kernel is installed and scheduled."
else
add_result "Kernel Packages" "WARN" \
"The running kernel ($RUNNING_KERNEL) was not directly matched to the installed kernel package list." \
"Review installed kernels and reboot into the approved current kernel when appropriate."
fi
printf '<h3>Installed Kernel Packages</h3><pre>%s</pre>\n' \
"$(html_escape "$INSTALLED_KERNELS")" >> "$REPORT_FILE"
else
add_result "Kernel Packages" "INFO" \
"Unable to retrieve installed kernel packages with rpm." \
"Review installed kernel packages manually."
fi
section_end
# ============================================================
# 17. Privileged & Shell Access Review
# ============================================================
section_start "17. Privileged & Shell Access Review"
ROOT_UID_USERS="$(awk -F: '$3 == 0 {print $1}' /etc/passwd 2>/dev/null)"
if [ -n "$ROOT_UID_USERS" ]; then
add_result "UID 0 Accounts" "INFO" \
"UID 0 accounts: $ROOT_UID_USERS" \
"Ensure every UID 0 account is authorized and required."
else
add_result "UID 0 Accounts" "WARN" \
"Unable to determine UID 0 accounts." \
"Review /etc/passwd manually."
fi
WHEEL_USERS=""
if getent group wheel >/dev/null 2>&1; then
WHEEL_USERS="$(getent group wheel | awk -F: '{print $4}')"
fi
if [ -n "$WHEEL_USERS" ]; then
add_result "Wheel Group Users" "WARN" \
"wheel members: $WHEEL_USERS" \
"Review all wheel users and remove unnecessary privileged access."
else
add_result "Wheel Group Users" "PASS" \
"No wheel group members were detected." \
"No action required."
fi
SHELL_USERS=""
while IFS=: read -r USERNAME PASSWORD UID GID GECOS HOME SHELL; do
[ -n "$USERNAME" ] || continue
case "$SHELL" in
/sbin/nologin|/usr/sbin/nologin|/bin/false|/usr/bin/false|nologin|false|"")
continue
;;
esac
if [ -f /etc/shells ]; then
if ! grep -Fxq "$SHELL" /etc/shells 2>/dev/null; then
continue
fi
fi
SHELL_USERS="${SHELL_USERS}${USERNAME}|${UID}|${HOME}|${SHELL}
"
done < /etc/passwd
if [ -n "$SHELL_USERS" ]; then
SHELL_USER_COUNT="$(printf '%s\n' "$SHELL_USERS" | grep -c '|' 2>/dev/null)"
add_result "Users With Shell Access" "WARN" \
"$SHELL_USER_COUNT local user(s) have an interactive shell." \
"Review every shell-enabled account and disable shell access where it is not required."
else
add_result "Users With Shell Access" "PASS" \
"No local users with interactive shell access were detected." \
"No action required."
fi
printf '<h3>Detailed Shell Access Review</h3>\n' >> "$REPORT_FILE"
add_table_header
add_table_header_cell "Username"
add_table_header_cell "UID"
add_table_header_cell "Home Directory"
add_table_header_cell "Shell"
add_table_header_cell "Shell Access"
add_table_header_cell "cPanel Account"
add_table_header_cell "Wheel"
add_table_header_cell "UID 0"
printf '</tr>\n' >> "$REPORT_FILE"
# Build a cPanel account lookup from /etc/userdomains.
CPANEL_USERS_FILE="/etc/userdomains"
is_cpanel_account() {
local user="$1"
if [ -f "$CPANEL_USERS_FILE" ]; then
if grep -Eq "^[^:]+:[[:space:]]*$user$" "$CPANEL_USERS_FILE" 2>/dev/null; then
echo "YES"
return
fi
fi
echo "NO"
}
is_wheel_user() {
local user="$1"
if getent group wheel 2>/dev/null |
awk -F: -v u="$user" '
{
n=split($4,a,",")
for(i=1;i<=n;i++)
if(a[i]==u)
found=1
}
END {
if(found) print "YES"
else print "NO"
}'; then
return
fi
echo "NO"
}
while IFS='|' read -r USERNAME UID HOME SHELL; do
[ -n "$USERNAME" ] || continue
CPANEL_ACCOUNT="$(is_cpanel_account "$USERNAME")"
WHEEL="$(is_wheel_user "$USERNAME")"
if [ "$UID" = "0" ]; then
UID_ZERO="YES"
else
UID_ZERO="NO"
fi
printf '<tr>' >> "$REPORT_FILE"
add_table_cell "$USERNAME"
add_table_cell "$UID"
add_table_cell "$HOME"
add_table_cell "$SHELL"
add_table_cell "YES"
add_table_cell "$CPANEL_ACCOUNT"
add_table_cell "$WHEEL"
add_table_cell "$UID_ZERO"
printf '</tr>\n' >> "$REPORT_FILE"
done <<EOF
$SHELL_USERS
EOF
printf '</table>\n' >> "$REPORT_FILE"
NO_SHELL_COUNT=0
while IFS=: read -r USERNAME PASSWORD UID GID GECOS HOME SHELL; do
[ -n "$USERNAME" ] || continue
case "$SHELL" in
/sbin/nologin|/usr/sbin/nologin|/bin/false|/usr/bin/false)
NO_SHELL_COUNT=$((NO_SHELL_COUNT + 1))
;;
esac
done < /etc/passwd
add_result "Users Without Interactive Shell" "INFO" \
"$NO_SHELL_COUNT local account(s) use nologin/false shells." \
"Periodically review service and system accounts."
section_end
# ============================================================
# 18. Network Exposure
# ============================================================
section_start "18. Network Exposure"
NETSTAT_OUTPUT=""
if command -v netstat >/dev/null 2>&1; then
NETSTAT_OUTPUT="$(netstat -ntlp 2>/dev/null)"
elif command -v ss >/dev/null 2>&1; then
NETSTAT_OUTPUT="$(ss -ntlp 2>/dev/null)"
fi
if [ -n "$NETSTAT_OUTPUT" ]; then
add_result "Listening TCP Services" "INFO" \
"Listening TCP sockets were collected for review." \
"Review every externally reachable service and close unnecessary listeners."
printf '<h3>Listening TCP Services</h3><pre>%s</pre>\n' \
"$(html_escape "$NETSTAT_OUTPUT")" >> "$REPORT_FILE"
else
add_result "Listening TCP Services" "INFO" \
"Unable to collect listening TCP sockets." \
"Review listening services manually with ss or netstat."
fi
section_end
# ============================================================
# 19. Service Review
# ============================================================
section_start "19. Service Review"
check_service() {
local title="$1"
shift
local found=""
local service
local state
for service in "$@"; do
if systemctl list-unit-files 2>/dev/null |
awk '{print $1}' |
grep -Fxq "$service" 2>/dev/null; then
found="$service"
break
fi
done
if [ -n "$found" ]; then
state="$(systemctl is-active "$found" 2>/dev/null)"
if [ "$state" = "active" ]; then
add_result "$title" "PASS" \
"$found is active." \
"Continue monitoring the service and keep it maintained."
elif [ "$state" = "inactive" ] || [ "$state" = "failed" ]; then
add_result "$title" "WARN" \
"$found is $state." \
"Confirm whether the service is expected to be running."
else
add_result "$title" "INFO" \
"$found state: $state." \
"Review the service status if required."
fi
else
add_result "$title" "INFO" \
"No matching systemd service was detected." \
"Verify whether this service is installed or managed by another mechanism."
fi
}
check_service "SSH Service" sshd.service ssh.service
check_service "Apache Service" httpd.service apache2.service
check_service "cPanel Service" cpanel.service cpanel
check_service "cPHulk Service" cphulkd.service cphulkd
check_service "Cron Service" crond.service cron.service
if systemctl list-unit-files 2>/dev/null |
awk '{print $1}' |
grep -Eq '^(mariadb|mysqld|mysql)\.service$'; then
DB_SERVICE="$(systemctl list-unit-files 2>/dev/null |
awk '{print $1}' |
grep -E '^(mariadb|mysqld|mysql)\.service$' |
head -1)"
DB_STATE="$(systemctl is-active "$DB_SERVICE" 2>/dev/null)"
if [ "$DB_STATE" = "active" ]; then
add_result "MySQL / MariaDB Service" "PASS" \
"$DB_SERVICE is active." \
"Keep the database service maintained and monitor resource usage."
else
add_result "MySQL / MariaDB Service" "WARN" \
"$DB_SERVICE is $DB_STATE." \
"Confirm whether the database service is expected to be running."
fi
else
add_result "MySQL / MariaDB Service" "INFO" \
"No MySQL/MariaDB systemd service was detected." \
"Verify database service status if a local database is expected."
fi
section_end
# ============================================================
# 20. CloudLinux Security
# ============================================================
section_start "20. CloudLinux Security"
CLOUDLINUX="no"
if rpm -q cloudlinux-release >/dev/null 2>&1 ||
[ -f /etc/sysconfig/cloudlinux ] ||
[ -e /proc/lve/list ]; then
CLOUDLINUX="yes"
fi
if [ "$CLOUDLINUX" = "no" ]; then
add_result "CloudLinux Detection" "INFO" \
"CloudLinux was not detected. CloudLinux-specific checks were skipped." \
"No CloudLinux action is required on a non-CloudLinux server."
else
add_result "CloudLinux Detection" "PASS" \
"CloudLinux appears to be installed." \
"Continue monitoring CloudLinux isolation and resource-management features."
CLOUDLINUX_VERSION=""
if command -v cldetect >/dev/null 2>&1; then
CLOUDLINUX_VERSION="$(cldetect --get-version 2>/dev/null | head -1)"
fi
if [ -z "$CLOUDLINUX_VERSION" ] && rpm -q cloudlinux-release >/dev/null 2>&1; then
CLOUDLINUX_VERSION="$(rpm -q cloudlinux-release 2>/dev/null)"
fi
add_result "CloudLinux Version" "INFO" \
"${CLOUDLINUX_VERSION:-Unable to determine CloudLinux version.}" \
"Keep CloudLinux updated through the approved maintenance process."
add_result "CloudLinux Kernel" "INFO" \
"$(uname -r)" \
"Ensure the running CloudLinux kernel is within the approved patch baseline."
if command -v cagefsctl >/dev/null 2>&1; then
CAGEFS_STATUS="$(cagefsctl --cagefs-status 2>/dev/null)"
if printf '%s\n' "$CAGEFS_STATUS" | grep -qiE 'enabled|started|mounted'; then
add_result "CageFS" "PASS" \
"CageFS status indicates an enabled/active configuration." \
"Keep CageFS enabled for hosted-user isolation."
else
add_result "CageFS" "WARN" \
"CageFS status did not clearly indicate an enabled/active state." \
"Review CageFS status in CloudLinux."
fi
printf '<h3>CageFS Status</h3><pre>%s</pre>\n' \
"$(html_escape "$CAGEFS_STATUS")" >> "$REPORT_FILE"
else
add_result "CageFS" "WARN" \
"cagefsctl was not found." \
"Verify CageFS installation and configuration."
fi
if [ -e /proc/lve/list ]; then
LVE_COUNT="$(grep -c '^' /proc/lve/list 2>/dev/null)"
add_result "LVE" "PASS" \
"The CloudLinux LVE interface /proc/lve/list is available (${LVE_COUNT} line(s))." \
"Continue monitoring LVE resource controls and account limits."
else
add_result "LVE" "WARN" \
"CloudLinux LVE interface /proc/lve/list was not detected." \
"Verify CloudLinux LVE functionality."
fi
ALT_PHP_FOUND=0
for ALT_PHP_INI in /opt/alt/php*/etc/php.ini; do
[ -f "$ALT_PHP_INI" ] || continue
ALT_PHP_FOUND=1
ALT_PHP_VERSION="$(basename "$(dirname "$(dirname "$ALT_PHP_INI")")")"
ALT_DISABLE_FUNCTIONS="$(grep -Ei '^[[:space:]]*disable_functions[[:space:]]*=' "$ALT_PHP_INI" 2>/dev/null |
tail -1 | cut -d= -f2- | sed 's/^[[:space:]]*//')"
if [ -n "$ALT_DISABLE_FUNCTIONS" ]; then
add_result "Alt-PHP $ALT_PHP_VERSION - disable_functions" "PASS" \
"disable_functions is configured." \
"Review Alt-PHP restrictions periodically."
else
add_result "Alt-PHP $ALT_PHP_VERSION - disable_functions" "WARN" \
"disable_functions is empty or not explicitly configured." \
"Review Alt-PHP PHP security settings."
fi
done
if [ "$ALT_PHP_FOUND" -eq 0 ]; then
add_result "Alt-PHP" "INFO" \
"No Alt-PHP php.ini files were detected." \
"Verify PHP Selector/Alt-PHP installation if expected."
fi
if command -v selectorctl >/dev/null 2>&1; then
PHP_SELECTOR_OUTPUT="$(selectorctl --interpreter=php --list 2>/dev/null)"
if [ -n "$PHP_SELECTOR_OUTPUT" ]; then
add_result "PHP Selector" "PASS" \
"PHP Selector information was available through selectorctl." \
"Keep supported PHP versions maintained and remove obsolete versions where appropriate."
printf '<h3>PHP Selector</h3><pre>%s</pre>\n' \
"$(html_escape "$PHP_SELECTOR_OUTPUT")" >> "$REPORT_FILE"
else
add_result "PHP Selector" "INFO" \
"selectorctl is present but returned no PHP version listing." \
"Review PHP Selector configuration."
fi
else
add_result "PHP Selector" "INFO" \
"selectorctl was not detected." \
"Verify whether PHP Selector is installed/required."
fi
if command -v dbctl >/dev/null 2>&1 ||
command -v dbtop >/dev/null 2>&1 ||
rpm -q governor-mysql >/dev/null 2>&1; then
add_result "MySQL Governor" "INFO" \
"CloudLinux MySQL Governor components were detected." \
"Review MySQL Governor resource controls and database workload monitoring."
else
add_result "MySQL Governor" "INFO" \
"MySQL Governor components were not detected." \
"Verify whether MySQL Governor is installed for this CloudLinux edition."
fi
MOD_LSAPI=""
if command -v httpd >/dev/null 2>&1; then
MOD_LSAPI="$(httpd -M 2>/dev/null | grep -i 'lsapi_module')"
fi
if [ -n "$MOD_LSAPI" ]; then
add_result "mod_lsapi" "PASS" \
"CloudLinux mod_lsapi appears to be loaded." \
"Keep the approved PHP handler configuration maintained."
else
add_result "mod_lsapi" "INFO" \
"mod_lsapi was not detected in the Apache loaded module list." \
"Verify the intended PHP handler configuration."
fi
PROC_HIDE_PID=""
if mount 2>/dev/null | grep -qE 'proc on /proc .*hidepid='; then
PROC_HIDE_PID="$(mount 2>/dev/null | grep -E 'proc on /proc .*hidepid=' | head -1)"
fi
if [ -n "$PROC_HIDE_PID" ]; then
add_result "proc hidepid" "PASS" \
"A hidepid option was detected on the /proc mount." \
"Keep process visibility restricted according to the CloudLinux security baseline."
else
add_result "proc hidepid" "INFO" \
"No hidepid option was detected on the /proc mount." \
"Review process visibility requirements for the server."
fi
if systemctl list-unit-files 2>/dev/null |
awk '{print $1}' |
grep -Eq '^(lvestats|lvestats.service)$'; then
LVE_STATS_SERVICE="$(systemctl list-unit-files 2>/dev/null |
awk '{print $1}' |
grep -E '^(lvestats|lvestats.service)$' |
head -1)"
LVE_STATS_STATE="$(systemctl is-active "$LVE_STATS_SERVICE" 2>/dev/null)"
if [ "$LVE_STATS_STATE" = "active" ]; then
add_result "LVE Stats Service" "PASS" \
"$LVE_STATS_SERVICE is active." \
"Keep CloudLinux resource statistics available for monitoring."
else
add_result "LVE Stats Service" "WARN" \
"$LVE_STATS_SERVICE is $LVE_STATS_STATE." \
"Verify whether LVE statistics service should be running."
fi
else
add_result "LVE Stats Service" "INFO" \
"LVE statistics service was not identified through systemd." \
"Verify CloudLinux statistics configuration if required."
fi
if command -v cldiag >/dev/null 2>&1; then
add_result "CloudLinux Diagnostics" "PASS" \
"cldiag is available." \
"Use cldiag during approved troubleshooting and maintenance."
else
add_result "CloudLinux Diagnostics" "INFO" \
"cldiag was not detected." \
"Verify CloudLinux diagnostic tooling if required."
fi
fi
section_end
# ============================================================
# Summary
# ============================================================
cat >> "$REPORT_FILE" <<EOF
</div>
<footer>
cPanel / WHM Linux Security Audit v$(html_escape "$SCRIPT_VERSION")<br>
Generated: $(html_escape "$REPORT_TIME")<br>
This report is an audit baseline. It does not perform remediation.
</footer>
<script>
document.getElementById("pass-count").textContent = "$(html_escape "$PASS_COUNT")";
document.getElementById("warn-count").textContent = "$(html_escape "$WARN_COUNT")";
document.getElementById("fail-count").textContent = "$(html_escape "$FAIL_COUNT")";
document.getElementById("info-count").textContent = "$(html_escape "$INFO_COUNT")";
</script>
</body>
</html>
EOF
echo
echo "============================================================"
echo "AUDIT COMPLETED"
echo "============================================================"
echo "PASS : $PASS_COUNT"
echo "WARN : $WARN_COUNT"
echo "FAIL : $FAIL_COUNT"
echo "INFO : $INFO_COUNT"
echo
echo "HTML Report:"
echo "$REPORT_FILE"
echo
echo "No remediation or service/configuration changes were performed."