HEX
Server: Apache/2.4.65 (Unix) OpenSSL/1.1.1k
System: Linux vps109042.inmotionhosting.com 4.18.0 #1 SMP Mon Sep 30 15:36:27 MSK 2024 x86_64
User: cisa (1010)
PHP: 8.2.30
Disabled: NONE
Upload Files
File: //opt/cwprads/mspe.sh
#!/usr/bin/env bash
# MSP.sh: MSP-like mail server log parser in Bash
# Nathan P. <me@tchbnl.net>
# 0.2b (Postfix)
set -euo pipefail

# Version and mail server variant
# Right now MSP.sh supports Postfix and has a WIP version for Exim
VERSION='0.2b (Postfix)'

# Nice text formatting options
TEXT_BOLD='\e[1m'
TEXT_RED='\e[31m'
TEXT_GREEN='\e[32m'
TEXT_UNSET='\e[0m'

# Path to the Postfix log file. We do rotation stuff further down.
LOG_FILE='/var/log/maillog'

# The RBLs we check against
RBL_LIST=('b.barracudacentral.org'
          'bl.spamcop.net'
          'dnsbl.sorbs.net'
          'spam.dnsbl.sorbs.net'
          'ips.backscatterer.org'
          'zen.spamhaus.org')

# Help message
# Email Steve to discuss current rates and senior discounts
show_help() {
    cat << EOF
$(echo -e "${TEXT_BOLD}")MSP.sh:$(echo -e "${TEXT_UNSET}") MSP-like mail server log parser in Bash

USAGE: MSP.sh [OPTION]
    --auth              Show mail server stats
    --rotated           Use rotated log files
    --rbl               Check IPs against common RBLs
    --help -h           Show this message
    --version -v        Show version information
EOF
}

# --auth/mail server stats run
auth_check() {
    # There's no reason to run all this if there's no log file...
    if ! [[ -e "${LOG_FILE}" ]]; then
        echo "No Postfix log found. Check for ${LOG_FILE} or update LOG_FILE in MSP.sh."

        return
    fi

    # If --rotated is passed, we fetch the rotated logs as well using find. Not
    # sure if the basename stuff below is smart or really dumb (maybe both?).
    # shellcheck disable=SC2312
    if [[ "${use_rotated}" = true ]]; then
        mapfile -t LOG_FILE< <(find /var/log -maxdepth 1 -type f \( -name "$(basename "${LOG_FILE}")" -o -name "$(basename "${LOG_FILE}")-*" \) | sort)

        echo -e "${TEXT_BOLD}Heads up:${TEXT_UNSET} Using rotated log files. This could take a bit longer."
        echo
    fi

    echo 'Getting cool Postfix facts...'
    echo

    # Dead simple queue size check. Might expand this in the future to alert if
    # the queue size is too high.
    # shellcheck disable=SC2312
    queue_size="$(postqueue -j 2>/dev/null | wc -l)"

    echo -e "📨 ${TEXT_BOLD}Queue Size:${TEXT_UNSET} ${queue_size}"

    echo "There's nothing else to show here. Have a llama: 🦙"
    echo

    # These are senders that have logged in to actual email accounts
    echo -e "🔑 ${TEXT_BOLD}Authenticated Senders${TEXT_UNSET}"

    # First fetch our list of senders into an array
    # shellcheck disable=SC2312
    if grep -q 'sasl_username=' "${LOG_FILE[@]}"; then
        auth_senders="$(grep 'sasl_username=' "${LOG_FILE[@]}" \
            | awk -F 'sasl_username=' '{print $2}' | awk '{print $1}')"
    else
        auth_senders=
    fi

    # Now sort them into the top 10 results
    if [[ -n "${auth_senders[*]}" ]]; then
        # shellcheck disable=SC2312
        for sender in ${auth_senders}; do
            echo "${sender}"
        done | sort | uniq -c | sort -rn | head -n 10
    fi

    # Or report if no results were found
    if [[ -z "${auth_senders[*]}" ]]; then
        echo 'No authenticated senders found.'
    fi

    echo

    # Directories where unauthenticated mail was sent... IF POSTFIX SUPPORTED
    # THIS. Somebody running a shared mail server should definitely be using
    # Exim. Postfix is much too simple, which is great, except for stuff like
    # detailed logging, which is needed for that use case. This is also where
    # I admit that I do not like Postfix.
    #echo -e "📂 ${TEXT_BOLD}Directories${TEXT_UNSET}"
    #echo

    # System users that have sent mail (like with PHP's mail function)
    echo -e "🧔 ${TEXT_BOLD}User Senders${TEXT_UNSET}"

    # First fetch our list of senders into an array
    # shellcheck disable=SC2312
    if grep -q 'uid=' "${LOG_FILE[@]}"; then
        user_senders="$(grep 'uid=' "${LOG_FILE[@]}" \
            | awk -F 'uid=' '{print $2}' | awk '{print $1}')"
    else
        user_senders=
    fi

    # Now sort them into the top 10 results
    if [[ -n "${user_senders[*]}" ]]; then
        # shellcheck disable=SC2312
        for user in ${user_senders}; do
            getent passwd "${user}" | awk -F ':' '{print $1}'
        done | sort | uniq -c | sort -rn | head -n 10
    fi

    # Or report if no results were found
    if [[ -z "${user_senders[*]}" ]]; then
        echo 'No user senders found.'
    fi

    echo

    # Postfix supports logging subjects as well, but it's not enabled in the
    # default configuration. I've written instructions at
    # https://github.com/tchbnl/MSP.sh/LOGGING.md.
    # Yes I named this function so it lines up with the others. I'm like that.
    echo -e "💌 ${TEXT_BOLD}The Usual Subjects™${TEXT_UNSET}"

    # First fetch our list of subjects into an array
    # shellcheck disable=SC2312
    if grep -q 'Subject:' "${LOG_FILE[@]}"; then
        subjects="$(grep 'Subject:' "${LOG_FILE[@]}" \
            | awk -F 'header Subject: ' '{print $2}' \
            | awk -F ' from localhost\\[127.0.0.1\\]' '{print $1}')"
    else
        subjects=
    fi

    # Now sort them into the top 10 results
    if [[ -n "${subjects[*]}" ]]; then
        # shellcheck disable=SC2312
        for subject in "${subjects[@]}"; do
            echo "${subject}"
        done | sort | uniq -c | sort -rn | head -n 10
    fi

    # Or report if no results were found
    if [[ -z "${subjects[*]}" ]]; then
        echo 'No subjects found (or subject logging is disabled in Postfix).'
    fi
}

# --rbl/RBL check
# TODO: Add support for IPv6 addresses. Oh God.
rbl_check() {
    if ! command -v dig >/dev/null; then
        echo 'dig not found. MSP.sh requires dig to check IPs against RBLs.'
        exit
    fi

    # Get our list of public IPs
    # shellcheck disable=SC2312
    server_ips="$(hostname -I | xargs -n 1 \
        | grep -Ev '^10.0.0|^127.0.0.1|^172.16.0|^192.168.0|^169.254.0|::')"

    # Not sure when this'd happen, but just in case...
    if [[ -z "${server_ips}" ]]; then
        echo 'No IP addresses found. MSP.sh checks public IPv4 addresses.'

        return
    fi

    echo 'Running RBL checks...'

    # And loop through each one
    for ip in ${server_ips}; do
        # We need to reverse the IP order for checks to "work"
        # This is a quick and dirty solution and will need to be replaced when
        # I add IPv6 support. I'm not looking forward to that.
        reversed_ip="$(echo "${ip}" | awk -F '.' '{print $4 "." $3 "." $2 "." $1}')"

        echo
        echo -e "${TEXT_BOLD}${ip}${TEXT_UNSET}"

        # Now we loop through each RBL inside the IP loop
        for rbl in "${RBL_LIST[@]}"; do
            # These two RBLs are too short for only one tab :(
            if [[ "${rbl}" = 'bl.spamcop.net' || "${rbl}" = 'dnsbl.sorbs.net' ]]; then
                echo -ne "\t${rbl}\t\t"
            else
                echo -ne "\t${rbl}\t"
            fi

            # We dig our reversed IP against each RBL. This might not work for
            # all RBLs, but I haven't been able to fully test it.
            rbl_result="$(dig "${reversed_ip}"."${rbl}" +short)"

            # And now we return the block result depending on what the response
            # from dig was (none = good, something = bad)
            if [[ -n "${rbl_result}" ]]; then
                echo -e "${TEXT_BOLD}${TEXT_RED}LISTED${TEXT_UNSET}"
            else
                echo -e "${TEXT_BOLD}${TEXT_GREEN}GOOD${TEXT_UNSET}"
            fi
        done
    done
}

# Command options
while [[ "${#}" -gt 0 ]]; do
    case "${1}" in
        --auth)
            shift 1

            # New! Support for rotated log files. If --rotated is passed with
            # --auth, we round up the rotated logs as well into LOG_FILE... in
            # the function above. No nutso stuff in the while loop.
            if [[ "${#}" -gt 0 && "${1}" = '--rotated' ]]; then
                use_rotated=true
            else
                use_rotated=
            fi

            auth_check
            exit
            ;;

        # MSP requires --all to check all RBLs. We don't, but I figured I
        # should/might as well add this.
        --rbl | '--rbl --all')
            rbl_check
            exit
            ;;

        --help | -h)
            show_help
            exit
            ;;

        --version | -v)
            echo -e "${TEXT_BOLD}MSP.sh${TEXT_UNSET} ${VERSION}"
            exit
            ;;

        -*)
            echo -e "Not sure what ${1} is supposed to mean..."
            echo
            show_help
            exit
            ;;

        *)
            break
            ;;
    esac
done

# If no options are passed, we show the help message. I might renege on this
# and default to running auth_check.
show_help