#!/bin/bash ### Copyright 1999-2022. Plesk International GmbH. All rights reserved. # This script is designed to find suspicious customizations on Plesk servers that may lead to issues. # It should be run on a server with Plesk installed. This script makes no changes on the server. [ -n "$BASH" ] || { echo "This script should be executed using /bin/bash" >&2 exit 3 } detect_vz() { [ -z "$PLESK_VZ_RESULT" ] || return $PLESK_VZ_RESULT PLESK_VZ_RESULT=1 PLESK_VZ=0 PLESK_VE_HW_NODE=0 PLESK_VZ_TYPE= local issue_file="/etc/issue" local vzcheck_file="/proc/self/status" [ -f "$vzcheck_file" ] || return 1 local env_id=`sed -ne 's|^envID\:[[:space:]]*\([[:digit:]]\+\)$|\1|p' "$vzcheck_file"` [ -n "$env_id" ] || return 1 if [ "$env_id" = "0" ]; then # Either VZ/OpenVZ HW node or unjailed CloudLinux PLESK_VE_HW_NODE=1 return 1 fi if grep -q "CloudLinux" "$issue_file" >/dev/null 2>&1 ; then return 1 fi if [ -f "/proc/vz/veredir" ]; then PLESK_VZ_TYPE="vz" elif [ -d "/proc/vz" ]; then PLESK_VZ_TYPE="openvz" fi PLESK_VZ=1 PLESK_VZ_RESULT=0 return 0 } # detects lxc and docker containers detect_lxc() { [ -z "$PLESK_LXC_RESULT" ] || return $PLESK_LXC_RESULT PLESK_LXC_RESULT=1 PLESK_LXC=0 if { [ -f /proc/1/cgroup ] && grep -q 'docker\|lxc' /proc/1/cgroup; } || \ { [ -f /proc/1/environ ] && cat /proc/1/environ | tr \\0 \\n | grep -q "container=lxc"; }; then PLESK_LXC_RESULT=0 PLESK_LXC=1 fi return "$PLESK_LXC_RESULT" } detect_virtualization() { detect_vz detect_lxc local is_docker="`[ -f "/.dockerenv" ] && echo yes || :`" local systemd_detect_virt_ct="`/usr/bin/systemd-detect-virt -c 2>/dev/null | grep -v '^none$' || :`" local systemd_detect_virt_vm="`/usr/bin/systemd-detect-virt -v 2>/dev/null | grep -v '^none$' || :`" local virt_what="`/usr/sbin/virt-what 2>/dev/null | xargs || :`" if [ -n "$is_docker" ]; then echo "docker $virt_what" elif [ "$PLESK_VZ" = "1" ]; then echo "${PLESK_VZ_TYPE:-virtuozzo}" elif [ "$PLESK_LXC" = "1" ]; then echo "lxc $virt_what" elif [ -n "$systemd_detect_virt_ct" ]; then echo "$systemd_detect_virt_ct $systemd_detect_virt_vm" elif [ -n "$virt_what" ]; then echo "$virt_what" elif [ -n "$systemd_detect_virt_vm" ]; then echo "$systemd_detect_virt_vm" fi } # Check that first argument is in array (other arguments) is_in_array() { local needle="$1" shift # everything else is haystack for elem in $@; do [ "$elem" = "$needle" ] && return 0 done return 1 } # A set of functions for work with config variables # $1 is config file name, $2 is variable name, $3 is variable value conf_getvar() { cat $1 | perl -n -e '$p="'$2'"; print $1 if m/^$p\s+(.*?)\s*$/' } plesk_full_version() { # "`plesk_full_version`" = "`plesk_release_version`.`plesk_hotfix_version`" # Beware: this shows information from a file, which changes during plesk-release package upgrade head -n1 /etc/plesk-release | cut -d' ' -f1 } plesk_release_version() { plesk_full_version | cut -d. -f-3 } ### Copyright 1999-2022. Plesk International GmbH. All rights reserved. # --- formatting helpers --- is_fmt_supported() { # Checks if output can be safely formatted # Note that end users, including checks, should use $NO_FMT instead [ -t 1 -a "0`tput colors 2>/dev/null`" -ge 4 ] } esc_seq() { # Outputs escape sequence to format text according to accent [ -z "$NO_FMT" ] || return 0 local accent="$1" case "$accent" in normal) tput sgr0 ;; ok) tput bold ;; good) tput bold; tput setaf 2 ;; sus|warning) tput bold; tput setaf 3 ;; bad|error) tput bold; tput setaf 1 ;; *) tput sgr0; tput setab 1 ;; esac } fmt() { # Outputs text formatted according to accent local accent="$1" shift echo "`esc_seq "$accent"`$*`esc_seq normal`" } strip_fmt() { # Strips escape sequences from piped in input sed "s,\x1B\[[0-9;]*[a-zA-Z],,g" } has_fmt() { # Checks if argument is formatted echo "$1" | grep -q $'\x1B' } join_array() { # Outputs arguments joined with 1-character separator (the first argument) local IFS="$1" shift echo "$*" } # --- result level processing helpers --- # Constants to check result level against LEVEL_ALWAYS=0 LEVEL_WARNINGS=1 LEVEL_ERRORS=2 LEVEL_NEVER=3 result_level() { # Returns check result level. 0 - success. Result is comparable to $LEVEL_* values. local result="$1" case "$result" in good|ok|normal) return 0 ;; sus|warning) return 1 ;; bad|error) return 2 ;; *) return 3 ;; esac } is_result_level_matched() { local level="$1" local target_level="$2" [ "$level" -ge "$target_level" ] } should_show_result_anyway() { [ -n "$SHOW_ALWAYS" -a "$opt_result_level" -lt "$LEVEL_NEVER" ] } fold_result_worst() { # Compares two RESULT values and outputs the worst one local accumulator="$1" local value="$2" local best_to_worst=('' good ok normal sus warning bad error) for i in "${!best_to_worst[@]}"; do if [ "$accumulator" = "${best_to_worst[$i]}" ]; then if is_in_array "$value" "${best_to_worst[@]:$i}"; then accumulator="$value" fi break fi done echo "$accumulator" } # --- console format reporter --- console_report_begin() { ACCUMULATED_RESULT= } console_report_check() { ACCUMULATED_RESULT="`fold_result_worst "$ACCUMULATED_RESULT" "$RESULT"`" [ -n "$DO_SHOW_RESULT" ] || should_show_result_anyway || return 0 has_fmt "$OUTPUT" || OUTPUT="`fmt "$RESULT" "$OUTPUT"`" [ -n "$DO_SHOW_LEGEND" ] || LEGEND= printf "%20s: %s%s\n" "${NAME}" "${OUTPUT}" "${LEGEND:+ ($LEGEND)}" if [ -n "$DO_SHOW_DESCRIPTION" -a -n "$DESCRIPTION" ]; then echo "$DESCRIPTION" | sed 's|^| |' fi } console_report_end() { local executed_checks="$1" local total_checks="$2" if [ "$executed_checks" -ne "$total_checks" ]; then ACCUMULATED_RESULT="`fold_result_worst "$ACCUMULATED_RESULT" "error"`" echo >&2 echo >&2 "Last check requested termination. Use --keep-going to ignore this." fi result_level "$ACCUMULATED_RESULT" } # --- tap format reporter --- tap_report_begin() { local total_checks="$#" echo "1..$total_checks" } tap_report_check() { local check_number="$1" local ok="ok" ! [ -n "$DO_SHOW_RESULT" ] || ok="not ok" [ -n "$DO_SHOW_LEGEND" ] || LEGEND= { printf "%s %d - %s: %s%s\n" "$ok" "$check_number" "$NAME" "$OUTPUT" "${LEGEND:+ ($LEGEND)}" if [ -n "$DO_SHOW_DESCRIPTION" -a -n "$DESCRIPTION" ]; then echo "$DESCRIPTION" | sed 's|^|# |' fi } | strip_fmt } tap_report_end() { local executed_checks="$1" local total_checks="$2" [ "$executed_checks" -eq "$total_checks" ] || echo "Bail out! Last check requested termination." } # --- engine --- result() { # Assigns and accumulates check result (can be called several times in one check) # RESULT is set to the worst one between calls # OUTPUT is concatenated between calls # LEGEND is deduplicated and joined between calls local result="$1" local output="$2" local legend="$3" RESULT="`fold_result_worst "$RESULT" "$result"`" OUTPUT+="$output" if [ -z "$LEGEND" -o -n "${LEGEND##*$legend*}" ]; then LEGEND+="${LEGEND:+, }$legend" fi } run_check() { # Executes one check local check="$1" # Check output parameters NAME= DESCRIPTION= SHOW_ALWAYS= RESULT= OUTPUT= LEGEND= "$check" RETURN_CODE="$?" # Advisory visibility flags DO_SHOW_RESULT= DO_SHOW_LEGEND= DO_SHOW_DESCRIPTION= result_level "$RESULT" local level="$?" ! is_result_level_matched "$level" "$opt_result_level" || DO_SHOW_RESULT="yes" ! is_result_level_matched "$level" "$opt_legend_level" || DO_SHOW_LEGEND="yes" ! is_result_level_matched "$level" "$opt_description_level" || DO_SHOW_DESCRIPTION="yes" } run_checks() { # Executes and reports result of a series of checks local reporter="${1:-console}" local keep_going="$2" shift 2 local NAME DESCRIPTION SHOW_ALWAYS RESULT OUTPUT LEGEND local DO_SHOW_RESULT DO_SHOW_LEGEND DO_SHOW_DESCRIPTION RETURN_CODE local i=0 : console_report_begin console_report_check console_report_end : tap_report_begin tap_report_check tap_report_end "${reporter}_report_begin" "$@" for check in "$@"; do (( ++i )) run_check "$check" "${reporter}_report_check" "$i" "$check" [ "$RETURN_CODE" -eq 0 -o -n "$keep_going" ] || break done "${reporter}_report_end" "$i" "$#" } ### Copyright 1999-2022. Plesk International GmbH. All rights reserved. # --- Plesk-specific helpers --- psa_conf() { local var="$1" local prod_conf_t="/etc/psa/psa.conf" local value="`conf_getvar "$prod_conf_t" "$var"`" [ -n "$value" ] || value="`conf_getvar "$prod_conf_t.default" "$var"`" echo "$value" } ### Copyright 1999-2022. Plesk International GmbH. All rights reserved. # --- general Plesk checks (non-subsystem-specific state) --- check_plesk_psa_conf() { local prod_conf_t="/etc/psa/psa.conf" local default_prod_conf_t="$prod_conf_t.default" local diff="` diff -uw "$default_prod_conf_t" "$prod_conf_t" \ | sed '0,/^@@/ d; /^ / d' | sed -n '/^.[[:alpha:]]/ p' `" local changes="`echo "$diff" | cut -c 2- | awk '{print $1}' | sort -u | xargs`" NAME="psa.conf" DESCRIPTION="Customization of many variables in $prod_conf_t isn't fully supported." if [ -n "$changes" ]; then DESCRIPTION+="${NL}Make sure custom values are valid and do not cause issues." DESCRIPTION+="${NL}Some variables may be customized as a result of Plesk updates." DESCRIPTION+="${NL}Here's the diff (-default setting, +current setting):${NL}$diff" result sus "$changes" "customized" else result ok "default" fi } check_plesk_selinux_module_slow() { NAME="SELinux Plesk module" DESCRIPTION="Policy module 'plesk' is required for correct operation with enforcing SELinux." local state= state="`getenforce 2>/dev/null`" || state="N/A" local issue= case "$state" in Enforcing) issue="bad" ;; Permissive) issue="sus" ;; N/A|Disabled|*) result normal "SELinux is $state" "skipped" return ;; esac # Check Plesk policy if semodule -l | grep -q "^plesk\>"; then result ok "installed" else result "$issue" "`fmt "$issue" not installed`" DESCRIPTION+="${NL}Install it with 'plesk installer add --components selinux', check with 'rpm -q psa-selinux' and 'semodule -l'." fi # Check relocations local HTTPD_VHOSTS_D="`psa_conf HTTPD_VHOSTS_D`" local PLESK_MAILNAMES_D="`psa_conf PLESK_MAILNAMES_D`" [ "$HTTPD_VHOSTS_D" = '/var/www/vhosts' ] || { result "$issue" ", `fmt "$issue" relocated HTTPD_VHOSTS_D`" DESCRIPTION+="${NL}Relocated HTTPD_VHOSTS_D=$HTTPD_VHOSTS_D is not supported with enforcing SELinux." } [ "$PLESK_MAILNAMES_D" = '/var/qmail/mailnames' ] || { result "$issue" ", `fmt "$issue" relocated PLESK_MAILNAMES_D`" DESCRIPTION+="${NL}Relocated PLESK_MAILNAMES_D=$PLESK_MAILNAMES_D is not supported with enforcing SELinux." } } check_plesk_selinux_customizations_slow() { NAME="SELinux policy" DESCRIPTION="Customized SELinux policy may cause issues with enforcing SELinux." DESCRIPTION+="${NL}Typically it can be customized via 'semanage' either by package scripts, tools, or user." local state= state="`getenforce 2>/dev/null`" || state="N/A" case "$state" in Enforcing|Permissive) ;; N/A|Disabled|*) result normal "SELinux is $state" "skipped" return ;; esac # Check customizations (excluding Plesk and typical system ones) local booleans_on=( httpd_can_bind_all_ports httpd_unified httpd_can_network_connect httpd_can_network_connect_db httpd_can_network_relay httpd_can_sendmail httpd_execmem httpd_run_stickshift httpd_tmp_exec ftp_home_dir ftpd_full_access httpd_setrlimit nis_enabled domain_can_mmap_files ftpd_use_passive_mode ftpd_connect_all_unreserved selinuxuser_execheap # bind package on CentOS 7 named_write_master_zones # container-selinux package (Docker) virt_sandbox_use_all_caps virt_use_nfs ) local booleans_any=( ngx_can_access_any_shared_memory ) local custom="` semanage export | grep -Ev \ ' -D$|module -d (courier|qmail)|boolean -m -1 ('"$(IFS="|"; echo "${booleans_on[*]}")"')|boolean -m (-0|-1) ('"$(IFS="|"; echo "${booleans_any[*]}")"')' `" if [ -z "$custom" ]; then result ok "not customized" else result sus "customized facilities: `echo "$custom" | awk '{ print $1 }' | uniq | xargs echo`" DESCRIPTION+="${NL}Relevant 'semanage export' output:${NL}$custom" fi } ### Copyright 1999-2022. Plesk International GmbH. All rights reserved. # --- postfix related checks --- __is_postfix_enabled() { if [ -x "/bin/systemctl" ]; then local is_enabled=$(/bin/systemctl "is-enabled" "postfix.service") if [ "$is_enabled" = "enabled" ]; then return 0 fi fi return 1 } check_postfix_milter_protocol() { # Based on PPS-11186 NAME="milter_protocol" DESCRIPTION="Postfix milter_protocol < 6 is not supported." DESCRIPTION+="${NL}If the wrong protocol is chosen, Plesk mail handlers in SMTP context (DKIM, mail quota, etc.) won't be working." DESCRIPTION+="${NL}Use 'plesk repair mail' or 'postconf milter_protocol=6' to fix this problem." which postconf > /dev/null 2>&1 || { result normal "N/A" "Postfix not installed"; return 0; } __is_postfix_enabled || { result normal "N/A" "Postfix not enabled"; return 0; } local milter_protocol=$(postconf -h milter_protocol) if [ -n "$milter_protocol" ] && [ "$milter_protocol" -lt 6 ]; then result bad "$milter_protocol" "not supported" else result ok "$milter_protocol" "supported" fi } check_postfix_smtputf8_enable() { # Based on PPS-10784 and PPPM-12866 NAME="smtputf8_enable" DESCRIPTION="Only postfix smtputf8_enable=no is supported." DESCRIPTION+="${NL}Value \"yes\" or undefined may lead to problems with delivery" DESCRIPTION+="${NL}from SMTP servers that use utf8 by default." DESCRIPTION+="${NL}Use 'postconf smtputf8_enable=no' to fix this problem." which postconf > /dev/null 2>&1|| { result normal "N/A" "Postfix not installed"; return 0; } __is_postfix_enabled || { result normal "N/A" "Postfix not enabled"; return 0; } local smtputf8_enable=$(postconf -h smtputf8_enable) if [ "$smtputf8_enable" != "no" ]; then result bad "${smtputf8_enable:-undefined}" "not supported" else result ok "$smtputf8_enable" "supported" fi } check_postfix_default_transport_maps() { # Based on PPS-10719 NAME="sender_dependent_default_transport_maps" DESCRIPTION="Postfix configuration sender_dependent_default_transport_maps should be defined." DESCRIPTION+="${NL}If this parameter is undefined, emails may be sent from the wrong IP address." DESCRIPTION+="${NL}Use 'plesk repair mail' to fix this problem." which postconf > /dev/null 2>&1|| { result normal "N/A" "Postfix not installed"; return 0; } __is_postfix_enabled || { result normal "N/A" "Postfix not enabled"; return 0; } local sender_dependent_default_transport_maps=$(postconf -h sender_dependent_default_transport_maps) if [ -z "$sender_dependent_default_transport_maps" ]; then result sus "undefined" "not supported" else result ok "$sender_dependent_default_transport_maps" "supported" fi } check_postfix_authorized_submit_users() { # Based on PPS-9490 NAME="authorized_submit_users" DESCRIPTION="Postfix configuration authorized_submit_users shouldn't be changed." DESCRIPTION+="${NL}Changing of authorized_submit_users may lead to unexpected rejection of outgoing mails." DESCRIPTION+="${NL}New value of the authorized_submit_users should be carefully checked." DESCRIPTION+="${NL}Expected value of authorized_submit_users is 'static:anyone'" which postconf > /dev/null 2>&1|| { result normal "N/A" "Postfix not installed"; return 0; } __is_postfix_enabled || { result normal "N/A" "Postfix not enabled"; return 0; } local authorized_submit_users=$(postconf -h authorized_submit_users) if [ -n "$authorized_submit_users" ] && [ "$authorized_submit_users" != "static:anyone" ]; then result sus "$authorized_submit_users" "customized" else result ok "$authorized_submit_users" "default" fi } check_postfix_transport_maps() { # Based on PPS-6419 NAME="transport_maps" DESCRIPTION="Postfix configuration transport_maps shouldn't be changed." DESCRIPTION+="${NL}Changing of transport_maps may lead to breakage of maillist sending." DESCRIPTION+="${NL}Use 'plesk repair mail' to fix this problem." which postconf > /dev/null 2>&1|| { result normal "N/A" "Postfix not installed"; return 0; } __is_postfix_enabled || { result normal "N/A" "Postfix not enabled"; return 0; } local transport_maps=$(postconf -h transport_maps) if [ -z "$transport_maps" ]; then result sus "undefined" "not supported" elif [ "$transport_maps" != ", hash:/var/spool/postfix/plesk/transport" ]; then result sus "$transport_maps" "customized" else result ok "$transport_maps" "default" fi } check_postfix_master_cf_slow() { NAME="master.cf" DESCRIPTION="Customized Postfix master.cf may result in problems with mail delivery" local chroot= if [ -f "/etc/debian_version" ]; then chroot="y" elif [ -f "/etc/redhat-release" ]; then chroot="n" fi declare -A expected_services_re=( ['smtp\s+inet']="^smtp\s+inet\s+n\s+-\s+$chroot\s+-\s+-\s+smtpd\s*$" ['cleanup\s+unix']="^cleanup\s+unix\s+n\s+-\s+$chroot\s+-\s+0\s+cleanup\s*$" ['tlsmgr\s+unix']="^tlsmgr\s+unix\s+-\s+-\s+$chroot\s+1000\?\s+1\s+tlsmgr\s*$" ['rewrite\s+unix']="^rewrite\s+unix\s+-\s+-\s+$chroot\s+-\s+-\s+trivial-rewrite\s*$" ['bounce\s+unix']="^bounce\s+unix\s+-\s+-\s+$chroot\s+-\s+0\s+bounce\s*$" ['defer\s+unix']="^defer\s+unix\s+-\s+-\s+$chroot\s+-\s+0\s+bounce\s*$" ['trace\s+unix']="^trace\s+unix\s+-\s+-\s+$chroot\s+-\s+0\s+bounce\s*$" ['verify\s+unix']="^verify\s+unix\s+-\s+-\s+$chroot\s+-\s+1\s+verify\s*$" ['flush\s+unix']="^flush\s+unix\s+n\s+-\s+$chroot\s+1000\?\s+0\s+flush\s*$" ['proxymap\s+unix']="^proxymap\s+unix\s+-\s+-\s+n\s+-\s+-\s+proxymap\s*$" ['proxywrite\s+unix']="^proxywrite\s+unix\s+-\s+-\s+n\s+-\s+1\s+proxymap\s*$" ['smtp\s+unix']="^smtp\s+unix\s+-\s+-\s+$chroot\s+-\s+-\s+smtp\s*$" ['relay\s+unix']="^relay\s+unix\s+-\s+-\s+$chroot\s+-\s+-\s+smtp( -o syslog_name=postfix/\\\$service_name)?\s*$" ['showq\s+unix']="^showq\s+unix\s+n\s+-\s+$chroot\s+-\s+-\s+showq\s*$" ['error\s+unix']="^error\s+unix\s+-\s+-\s+$chroot\s+-\s+-\s+error\s*$" ['retry\s+unix']="^retry\s+unix\s+-\s+-\s+$chroot\s+-\s+-\s+error\s*$" ['discard\s+unix']="^discard\s+unix\s+-\s+-\s+$chroot\s+-\s+-\s+discard\s*$" ['local\s+unix']="^local\s+unix\s+-\s+n\s+n\s+-\s+-\s+local\s*$" ['virtual\s+unix']="^virtual\s+unix\s+-\s+n\s+n\s+-\s+-\s+virtual\s*$" ['lmtp\s+unix']="^lmtp\s+unix\s+-\s+-\s+$chroot\s+-\s+-\s+lmtp\s*$" ['anvil\s+unix']="^anvil\s+unix\s+-\s+-\s+$chroot\s+-\s+1\s+anvil\s*$" ['scache\s+unix']="^scache\s+unix\s+-\s+-\s+$chroot\s+-\s+1\s+scache\s*$" ['postlog\s+unix-dgram']="^postlog\s+unix-dgram\s+n\s+-\s+[n$chroot]\s+-\s+1\s+postlogd\s*$" ['plesk_virtual\s+unix']="^plesk_virtual\s+unix\s+-\s+n\s+n\s+-\s+-\s+pipe flags=DORhu user=popuser:popuser argv=/usr/lib(64)?/plesk-9\.0/postfix-local -f \\\${sender} -d \\\${recipient} -p `psa_conf PLESK_MAILNAMES_D` -q \\\${queue_id}\s*$" ['127.0.0.1:12346\s+inet']="^127.0.0.1:12346\s+inet\s+n\s+n\s+n\s+-\s+-\s+spawn user=popuser:popuser argv=/usr/lib(64)?/plesk-9\.0/postfix-srs\s*$" ['mailman\s+unix']="^mailman\s+unix\s+-\s+n\s+n\s+-\s+-\s+pipe flags=R user=(list|mailman):\1 argv=/usr/lib(64)?/plesk-9\.0/postfix-mailman \\\${nexthop} \\\${user} \\\${recipient}\s*$" ['pickup\s+fifo']="^pickup\s+fifo\s+n\s+-\s+$chroot\s+60\s+1\s+pickup\s*$" ['qmgr\s+fifo']="^qmgr\s+fifo\s+n\s+-\s+n\s+1\s+1\s+qmgr\s*$" ['smtps\s+inet']="^smtps\s+inet\s+n\s+-\s+$chroot\s+-\s+-\s+smtpd -o smtpd_tls_wrappermode=yes\s*$" ['plesk_saslauthd\s+unix']="^plesk_saslauthd\s+unix\s+y\s+y\s+$chroot\s+-\s+1\s+plesk_saslauthd status=5 listen=6 dbpath=(/var/spool/postfix)?/plesk/passwd\.db\s*$" ['submission\s+inet']="^submission\s+inet\s+n\s+-\s+$chroot\s+-\s+-\s+smtpd -o smtpd_enforce_tls=yes -o smtpd_tls_security_level=encrypt -o smtpd_sasl_auth_enable=yes -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o smtpd_sender_restrictions= -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject_unauth_destination\s*$" ['plesk(-\S+)?-\S*-\S*\s+unix']="^plesk(-(\S+))?-\S*-\S*\s+unix\s+-\s+-\s+n\s+-\s+-\s+smtp -o smtp_bind_address=\S* -o smtp_bind_address6=\S* -o smtp_address_preference=(any|ipv4|ipv6)( -o smtp_helo_name=\2)?\s*$" ['maildrop\s+unix']="^maildrop\s+unix\s+-\s+n\s+n\s+-\s+-\s+pipe flags=DRX?hu user=vmail argv=/usr/bin/maildrop -d \\\${recipient}\s*$" ['uucp\s+unix']="^uucp\s+unix\s+-\s+n\s+n\s+-\s+-\s+pipe flags=Fqhu user=uucp argv=uux -r -n -z -a\\\$sender - \\\$nexthop!rmail \(\\\$recipient\)\s*$" ['ifmail\s+unix']="^ifmail\s+unix\s+-\s+n\s+n\s+-\s+-\s+pipe flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r \\\$nexthop \(\\\$recipient\)\s*$" ['bsmtp\s+unix']="^bsmtp\s+unix\s+-\s+n\s+n\s+-\s+-\s+pipe flags=Fq\. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t\\\$nexthop -f\\\$sender \\\$recipient\s*$" ['scalemail-backend\s+unix']="^scalemail-backend\s+unix\s+-\s+n\s+n\s+-\s+2\s+pipe flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store \\\${nexthop} \\\${user} \\\${extension}\s*$" ) declare -A optional_services_re=( # Optional Plesk services ['submission\s+inet']=1 ['plesk(-\S+)?-\S*-\S*\s+unix']=1 # Only on Debian >= 10, Ubuntu >= 20.04 ['maildrop\s+unix']=1 ['uucp\s+unix']=1 ['ifmail\s+unix']=1 ['bsmtp\s+unix']=1 ['scalemail-backend\s+unix']=1 ) declare -A services_re=() local master_cf="/etc/postfix/master.cf" which postconf >/dev/null 2>&1 || { result normal "N/A" "Postfix not installed" return 0 } __is_postfix_enabled || { result normal "N/A" "Postfix not enabled" return 0 } for id in "${!expected_services_re[@]}"; do services_re[$id]='-' done # Process logical lines: everything except comment and empty, combine continuation lines while read -r line; do local found= for id in "${!expected_services_re[@]}"; do echo -n "$line" | grep -Eq "^$id\>" || continue found="yes" [ "${services_re[$id]}" != '*' ] || continue if echo -n "$line" | grep -Eq "${expected_services_re[$id]}"; then services_re[$id]=' ' else services_re[$id]='*' fi done if [ -z "$found" ]; then id="`echo -n "$line" | awk '{ print $1 " " $2 }'`" services_re[$id]='+' fi done < <( cat "$master_cf" | sed -e '/^\s*$/ d; /^\s*#/ d' | sed -e ': loop; N; s/\n\s\+/ /; t loop; P; D' ) local sep= local change accent legend for id in "${!services_re[@]}"; do change="${services_re[$id]}" case "$change" in '-') if [ -z "${optional_services_re[$id]}" ]; then accent=bad; legend="`fmt "$accent" "${NO_FMT:+$change}missing services"`" else accent=normal; legend="`fmt "$accent" "optional services"`" fi ;; '*') accent=bad; legend="`fmt "$accent" "${NO_FMT:+$change}changed services"`" ;; '+') accent=sus; legend="`fmt "$accent" "${NO_FMT:+$change}extra services"`" ;; ' ') accent=ok; legend="`fmt "$accent" "present services"`" ;; esac if [ "$accent" = "bad" -o "$accent" = "sus" ]; then result "$accent" "$sep`fmt "$accent" "${NO_FMT:+$change}${id//\\\\s+/ }"`" "$legend" sep=", " fi done [ -n "$sep" ] || result "ok" "default" } ### Copyright 1999-2022. Plesk International GmbH. All rights reserved. # --- system and Plesk summary checks (always visible) --- plesk_version_text() { # Human-readable Plesk version plesk version | sed -n 's|^\s*Product version:\s*|| p' } os_version_text() { # Human-readable OS version and arch (on Plesk Obsidian or Plesk Onyx) { plesk version | sed -n 's|^\s*OS version:\s*|| p' plesk version | sed -n 's|^\s*Architecture:\s*|| p' } | xargs } previous_version() { # Decrements last component of the argument version local ver_head="${1%.*}" local ver_tail="${1##*.}" echo "$ver_head.$(( ver_tail - 1))" } check_summary_plesk_version() { local version_text="`plesk_version_text`" local release_version="`plesk_release_version`" local dev_version='18.0.44' local released_version="`previous_version "$dev_version"`" local prev_released_version="`previous_version "$released_version"`" local supported_versions=("$dev_version" "$released_version" "$prev_released_version" "17.8.11" "17.5.3" "17.0.17") NAME="Plesk release" SHOW_ALWAYS="yes" if [ "$release_version" = "$dev_version" ]; then result normal "$version_text" "dev" elif [ "$release_version" = "$released_version" ]; then result ok "$version_text" "latest" elif is_in_array "$release_version" "${supported_versions[@]}"; then result sus "$version_text" "supported" else result bad "${version_text:-N/A}" "not supported" [ "0${release_version%%.*}" -ge 17 ] || { DESCRIPTION="Many of the next checks expect that Plesk Obsidian or Onyx is installed" return 1 } fi } check_summary_os_version() { local version_text="`os_version_text`" local os_name="`plesk sbin osdetect -vendor`" local os_version="`plesk sbin osdetect -short-version`" local os_arch="`plesk sbin osdetect -arch`" local supported_oses=( {CentOS,RedHat,CloudLinux,VZLinux}-7-x86_64 {RedHat,CloudLinux,AlmaLinux,Rocky}-8-x86_64 Debian-{9,10,11}-x86_64 Ubuntu-{18,20,22}.04-x86_64 ) NAME="OS version" SHOW_ALWAYS="yes" if is_in_array "$os_name-$os_version-$os_arch" "${supported_oses[@]}"; then result ok "$version_text" "supported" else result bad "${version_text:-N/A}" "not supported" return 1 fi } check_summary_virtualization() { local virtualizations=(`detect_virtualization`) # Feel free to add to the lists if something is missing. See also: # https://docs.plesk.com/release-notes/obsidian/software-requirements/#sv # https://people.redhat.com/~rjones/virt-what/virt-what.txt # https://www.freedesktop.org/software/systemd/man/systemd-detect-virt.html local supported=(vmware xen xen-hvm vz openvz virtuozzo parallels kvm microsoft hyperv aws) local limited_support=(docker lxc) NAME="Virtualization" SHOW_ALWAYS="yes" local sep= local accent legend for virt in "${virtualizations[@]}"; do if is_in_array "$virt" "${limited_support[@]}"; then accent=sus legend="`fmt sus "limited support"`" elif is_in_array "$virt" "${supported[@]}"; then accent=ok legend="`fmt ok "supported"`" else accent=bad legend="`fmt bad "not supported"`" fi result "$accent" "$sep`fmt "$accent" "$virt"`" "$legend" sep=" " done } check_summary_plesk_load() { local HTTPD_VHOSTS_D="`psa_conf HTTPD_VHOSTS_D`" local PLESK_MAILNAMES_D="`psa_conf PLESK_MAILNAMES_D`" local doms="`find ${HTTPD_VHOSTS_D:-/_}/system -mindepth 1 -maxdepth 1 -type d | wc -l`" local subs="`find ${HTTPD_VHOSTS_D:-/_} -mindepth 1 -maxdepth 1 -type d | grep -Ev '/(\.skel|chroot|system|default)$' | wc -l`" local mbxs="`{ test -x /usr/bin/msmtp || find ${PLESK_MAILNAMES_D:-/_} -mindepth 3 -maxdepth 3 -name Maildir; } | wc -l`" NAME="Plesk load" SHOW_ALWAYS="yes" threshold() { local value="$1" local sus_threshold="$2" local bad_threshold="$3" if [ -n "$bad_threshold" ] && [ "$value" -ge "$bad_threshold" ]; then echo "bad" elif [ -n "$sus_threshold" ] && [ "$value" -ge "$sus_threshold" ]; then echo "sus" else echo "ok" fi } # Thresholds below are rather arbitrary, feel free to adjust local dom_result="`threshold "$doms" 300 5000`" local sub_result="`threshold "$subs" 300 5000`" local mbx_result="`threshold "$mbxs" 1000`" result "$dom_result" "`fmt $dom_result $doms` domains, " result "$sub_result" "`fmt $sub_result $subs` subscriptions, " result "$mbx_result" "`fmt $mbx_result $mbxs` mailboxes" } check_summary_selinux() { NAME="SELinux" ! which yum >/dev/null 2>&1 || SHOW_ALWAYS="yes" # Check SELinux status local state= state="`getenforce 2>/dev/null`" || state="N/A" case "$state" in N/A|Disabled) result normal "$state" ;; Enforcing|Permissive) result ok "$state" ;; *) result bad "$state" "invalid" DESCRIPTION="getenforce returned invalid result" ;; esac } ### Copyright 1999-2022. Plesk International GmbH. All rights reserved. # --- system checks (general system properties and state) --- check_umask() { local current_umask="`umask`" local expected_umask="0022" NAME="umask" DESCRIPTION="Non-standard umask may result in permission issues (expected: $expected_umask)" if [ "$current_umask" = "$expected_umask" ]; then result ok "$current_umask" else result sus "$current_umask" fi } check_path() { local expected_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" # The following accounts for OSes with merged /usr (maps paths via readlink and deduplicates) local path_diff="` diff -U100 \ <(echo "$expected_path" | tr ':' '\n' | xargs -n1 readlink -f | awk '!a[$0]++') \ <(echo "$PATH" | tr ':' '\n' | xargs -n1 readlink -f | awk '!a[$0]++') \ | sed '0,/^@@/ d' `" local old_IFS="$IFS" local IFS=$'\n' local path_diff_items=($path_diff) IFS="$old_IFS" NAME="PATH" DESCRIPTION="Non-standard \$PATH may result in inaccessible binaries or use of wrong binaries" if [ "${#path_diff_items[@]}" -eq 0 ]; then result ok "$PATH" "`fmt ok "present path"`" return 0 fi local sep= local accent legend local last_path="`readlink -f "${expected_path##*:}"`" local beyond_last= for diff_item in "${path_diff_items[@]}"; do local change="`echo "$diff_item" | cut -c 1`" local path="`echo "$diff_item" | cut -c 2-`" case "$change" in '-') accent=bad; legend="`fmt "$accent" "${NO_FMT:+$change}missing path"`" ;; '+') accent=sus; legend="`fmt "$accent" "${NO_FMT:+$change}extra path"`" if [ -n "$beyond_last" ]; then change='#'; accent=normal; legend="${NO_FMT:+$change}extra trailing path" fi ;; ' ') accent=ok; legend="`fmt "$accent" "present path"`" ;; *) accent=; legend= ;; esac result "$accent" "$sep`fmt "$accent" ${NO_FMT:+$change}"$path"`" "$legend" sep=":" ! [ "$last_path" = "$path" ] || beyond_last="yes" done } check_user_beancounters() { local ubc="/proc/user_beancounters" NAME="user_beancounters" DESCRIPTION="Failures indicate this virtual server attempted to exceed allocated resource limits." DESCRIPTION+="${NL}See https://wiki.openvz.org/Resource_shortage" if [ ! -r "$ubc" ]; then result normal "N/A" return 0 fi # If ran on VZ HW node, this will check all containers # See also: https://wiki.openvz.org/Proc/user_beancounters local failures="`cat "$ubc" | sed -n '3,$ p' | awk '($NF != 0) {print $(NF - 5)}' | sort -u | xargs`" if [ -n "$failures" ]; then result bad "$failures" "failed resources" else result ok "OK" "no failures" fi } ### Copyright 1999-2022. Plesk International GmbH. All rights reserved. # List of all checks CHECKS=( check_summary_plesk_version check_summary_os_version check_summary_virtualization check_summary_selinux check_summary_plesk_load check_umask check_path check_user_beancounters check_plesk_psa_conf check_postfix_milter_protocol check_postfix_smtputf8_enable check_postfix_default_transport_maps check_postfix_authorized_submit_users check_postfix_transport_maps check_postfix_master_cf_slow check_plesk_selinux_module_slow check_plesk_selinux_customizations_slow ) unset GREP_OPTIONS # --- initialization --- init_check_constants() { NL=$'\n' if [ -f "/etc/debian_version" -a -d "/opt/psa" ]; then PRODUCT_ROOT_D="/opt/psa" elif [ -f "/etc/redhat-release" -a -d "/usr/local/psa" ]; then PRODUCT_ROOT_D="/usr/local/psa" fi } # --- parse options --- usage() { if [ -n "$*" ]; then echo "audit: $*" >&2 fi cat << EOT Usage: audit [OPTION ...] audit --list [--include ...] [--exclude ...] curl -fsSL https://autoinstall.plesk.com/audit | bash -s -- [OPTION ...] wget -qO- https://autoinstall.plesk.com/audit | bash -s -- [OPTION ...] Output control: --format Select output format. Default is 'console', which is intended for human use. 'tap' is Test Anything Protocol format, intended for automated use. This option also influences exit code and default verbosity settings. --color Whether to display output in color. Default is 'auto'. -l, --list Instead of running checks, list them. -h, --help Show this help message. Verbosity control: -v, --verbose Be more verbose. Can be specified multiple times. -q, --quiet Be less verbose. Can be specified multiple times. --result WHEN When to show result of checks. Result of some checks is shown always (unless this is 'never'). 'tap' format always shows all results, so instead this option controls which results are treated as failed. --legend WHEN When to show legend for checks. --description WHEN When to show description for checks. WHEN is : 'always' or 'on' show for all checks, regardless of result. 'warnings' show for checks that result in warning or error. 'errors' show for checks that result in error. 'never' or 'off' show for none of the checks, regardless or result. Checks control: -i, --include "regexp ..." Include checks that match listed regular expressions. Multiple space-separated regexps can be specified and the option can be specified multiple times. See the --list output for the list of checks to filter. -e, --exclude "regexp ..." Exclude checks that match listed regular expressions. Works similar to the --include option. Exclude filters are applied after include filters. --keep-going Continue executing checks even if one of them requested termination. EOT exit 3 } parse_level_value() { # Parses level option value into numeric one local option="$1" local value="$2" case "$value" in always|on) return "$LEVEL_ALWAYS" ;; warnings) return "$LEVEL_WARNINGS" ;; errors) return "$LEVEL_ERRORS" ;; never|off) return "$LEVEL_NEVER" ;; *) usage "invalid '$option' option value '$value'" ;; esac } set_default_level_values() { # Assigns default values for $opt_*_level based on $opt_format and $opt_verbosity local result_level legend_level description_level case "$opt_format" in console) result_level=$LEVEL_WARNINGS legend_level=$LEVEL_ALWAYS description_level=$LEVEL_WARNINGS ! [ "$opt_verbosity" -gt 0 ] || result_level=$LEVEL_ALWAYS ! [ "$opt_verbosity" -gt 1 ] || description_level=$LEVEL_ALWAYS ! [ "$opt_verbosity" -lt 0 ] || legend_level=$LEVEL_NEVER ! [ "$opt_verbosity" -lt 0 ] || description_level=$LEVEL_NEVER ! [ "$opt_verbosity" -lt -1 ] || result_level=$LEVEL_ERRORS ! [ "$opt_verbosity" -lt -2 ] || result_level=$LEVEL_NEVER ;; tap) result_level=$LEVEL_WARNINGS legend_level=$LEVEL_NEVER description_level=$LEVEL_ALWAYS ! [ "$opt_verbosity" -gt 0 ] || legend_level=$LEVEL_ALWAYS ! [ "$opt_verbosity" -lt 0 ] || description_level=$LEVEL_NEVER ;; *) usage "invalid '--format' option value '$opt_format'" ;; esac : ${opt_result_level:=$result_level} : ${opt_legend_level:=$legend_level} : ${opt_description_level:=$description_level} } set_color_formatting() { # Initializes $NO_FMT, which defines whether color formatting is disabled case "$opt_color" in auto) [ "$opt_format" = "console" ] && is_fmt_supported && NO_FMT= || NO_FMT="yes" ;; never) NO_FMT="yes" ;; always) NO_FMT= ;; *) usage "invalid '--color' option value '$opt_color'" ;; esac } opt_format="console" opt_color="auto" opt_result_level= opt_legend_level= opt_description_level= opt_verbosity=0 opt_keep_going= opt_include=() opt_exclude=() TEMP=`getopt -o i:e:vqlh \ --long format:,color:,result:,legend:,description:,verbose,quiet,keep-going,include:,exclude:,list,help \ -n audit -- "$@"` || usage eval set -- "$TEMP" while [ "$#" -gt 0 ]; do case "$1" in --format) opt_format="$2" shift 2 ;; --color) opt_color="$2" shift 2 ;; --result) parse_level_value "$1" "$2" opt_result_level="$?" shift 2 ;; --legend) parse_level_value "$1" "$2" opt_legend_level="$?" shift 2 ;; --description) parse_level_value "$1" "$2" opt_description_level="$?" shift 2 ;; -v|--verbose) (( ++opt_verbosity )) || : shift ;; -q|--quiet) (( --opt_verbosity )) || : shift ;; --keep-going) opt_keep_going="yes" shift ;; -i|--include) set -o noglob opt_include+=($2) set +o noglob shift 2 ;; -e|--exclude) set -o noglob opt_exclude+=($2) set +o noglob shift 2 ;; -l|--list) opt_list="yes" shift ;; -h|--help) usage ;; --) shift ;; *) usage "unhandled argument '$1'" ;; esac done set_default_level_values set_color_formatting opt_include="`join_array '|' "${opt_include[@]}"`" opt_exclude="`join_array '|' "${opt_exclude[@]}"`" # --- execute --- [ -z "$opt_include" ] || CHECKS=(`printf "%s\n" "${CHECKS[@]}" | grep -E "$opt_include"`) [ -z "$opt_exclude" ] || CHECKS=(`printf "%s\n" "${CHECKS[@]}" | grep -Ev "$opt_exclude"`) if [ -n "$opt_list" ]; then echo "${CHECKS[@]}" | xargs -n1 -r else init_check_constants run_checks "$opt_format" "$opt_keep_going" "${CHECKS[@]}" fi