#!/bin/bash
### Copyright 1999-2021. 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+(.*)/'
}

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-2021. 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-2021. 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-2021. 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
}

### Copyright 1999-2021. 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
}
### Copyright 1999-2021. 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.38'
	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
		{CentOS,RedHat,CloudLinux,AlmaLinux}-8-x86_64
		Debian-{9,10}-x86_64
		Ubuntu-{16,18,20}.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)
	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"
}

### Copyright 1999-2021. 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-2021. Plesk International GmbH. All rights reserved.

# List of all checks
CHECKS=(
	check_summary_plesk_version
	check_summary_os_version
	check_summary_virtualization
	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
)

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 <console|tap>      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 <auto|never|always> 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|on|warnings|errors|never|off>:
        '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