Overview
Configuration Crontab Environment
Written: Jan-2025

maillog-summary.bsh script

So I needed/wanted to investigate what was going on in my /var/log/maillog file on plesk, so this is my bash script, running on ubuntu via cron

bash
\#!/bin/bash

logfile="/var/log/maillog"

\# Extract date range
earliest=$(awk 'NR==1 {print $1, $2, $3}' "$logfile")
latest=$(awk 'END {print $1, $2, $3}' "$logfile")

echo "# maillog dates from $earliest - $latest"

echo
echo "# Time Period Summary of Failed Attempts #"
echo


\# Process logs for Plesk's smtpd and extract unique IPs and login IDs with counts
awk '
function parse_timestamp(month, day, time) {
    split(time, t, ":");
    months["Jan"]=1; months["Feb"]=2; months["Mar"]=3; months["Apr"]=4;
    months["May"]=5; months["Jun"]=6; months["Jul"]=7; months["Aug"]=8;
    months["Sep"]=9; months["Oct"]=10; months["Nov"]=11; months["Dec"]=12;
    return mktime("2025 " months[month] " " day " " t[1] " " t[2] " 0");
}



function format_interval(timestamp, minutes) {
    # Default to 10 minutes if no minutes argument is provided
    if (minutes == "") {
        minutes = 10;
    }

    split(strftime("%Y-%m-%d %H:%M", timestamp), time_parts, ":");
    min_block = int(time_parts[2] / minutes) * minutes;
    return time_parts[1] ":" (min_block < 10 ? "0" min_block : min_block);
}


function sort_and_print(arr, label, min) {
    n = asorti(arr, sorted_arr, "@val_num_desc");
    if (n > 0) {
        seen=0;
        for (i = 1; i <= n; i++) {
            if (arr[sorted_arr[i]] >= min) {
                if ( seen==0 ) print "    " label ":";
                seen=1;
                print "     " arr[sorted_arr[i]] " attempts : " sorted_arr[i];
            }
        }
        print "";
        return 1;
    }
    return 0;
}



function reset_block() {
    ips=0;
    logins=0;

    \# ip_attempts[ip]=count
    \# login_attempts[login]=count

    for (i in ip_attempts) {
        if (ip_attempts[i] >= min_count) ips++;
    }
    for (i in login_attempts) {
        if (login_attempts[i] >= min_count) logins++;
    }

    if(current_interval != "" && (ips >= 1 || logins >= 1)) {

        print "";
        print "";
        print "Summary for " current_interval;
        print "";

        ip_reported = sort_and_print(ip_attempts, "Unique suspect IP Addresses ", min_count_ip);
        login_reported = sort_and_print(login_attempts, "Unique failed Login IDs ", min_count_login);

        ip_reported = login_reported = 0;

        seen_matches = 0;
        if (length(seen_entries) > 0) {
            for (i in seen_entries) {

                \# Loop through ip_attempts to check for the existence of seen_entries[i]
                found_in_ip = 0;
                for (ip in ip_attempts) {
                    if (ip_attempts[ip] >= min_count_ip && index(seen_entries[i], ip) > 0) {
                        found_in_ip = 1;
                        break;  # Exit the loop if a match is found
                    }
                }

                \# Loop through login_attempts to check for the existence of seen_entries[i]
                found_in_login = 0;
                for (login in login_attempts) {
                    if (login_attempts[login]>=min_count_login && index(seen_entries[i], login) > 0) {
                        found_in_login = 1;
                        break;  # Exit the loop if a match is found
                    }
                }

                \# If found in either ip_attempts or login_attempts
                if (found_in_ip || found_in_login) {
                    if (seen_matches == 0) {
                        print "   Referenced Entries:";
                    }

                    print "     > " seen_entries[i];
                    seen_matches = 1;
                }
            }
            print "";
        }

    }
    delete ip_attempts;
    delete login_attempts;
    delete seen_entries;
    for (i in seen_entries) {
        delete seen_entries[i];
    }
}


BEGIN {
    current_interval = "";
    min_count_ip = 2;
    min_count_login = 1;
    gap_minutes = 30;

    delete ip_attempts;
    delete login_attempts;
    delete seen_entries;
}

{
    timestamp = parse_timestamp($1, $2, $3);
    timestamp_str = $1 " " $2 " " $3;
    interval = format_interval(timestamp, gap_minutes);

    if ( current_interval == "" ) current_interval = interval;


    if (interval != current_interval) {
        reset_block();

        current_interval = interval;
        start_datestr = interval ":00";
    }

    \# Extract IPs from "warning: unknown"
    if ($0 ~ /warning: unknown\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\]/) {
        match($0, /warning: unknown\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\]/, ip_match);
        if (ip_match[1] != "") ip_attempts[ip_match[1]]++;
        if (ip_match[1] != "") seen_entries[NR] = $0;
    }

    \# Extract login IDs from "No such user"
    if ($0 ~ /failed mail authentication attempt for user/) {
        match($0, / authentication attempt for user \x27([^\x27]+)\x27/, login_match);
        if (login_match[1] != "") login_attempts[login_match[1]]++;
        if (login_match[1] != "") seen_entries[NR] = $0;
    }
}

END {
    reset_block();
    print "";
    print "End of log summary.";
}' "$logfile"

The output for it will be a summary of the number of failed logins or IPs that have occured during a given period within the log.

stdout
\# maillog dates from Jan 25 10:41:52 - Jan 25 16:49:52

\# Time Period Summary of Failed Attempts #



Summary for 2025-01-25 10:30

    Unique suspect IP Addresses :
     2 attempts : 87.120.121.102
     2 attempts : 45.151.99.245
     2 attempts : 194.0.234.135
     2 attempts : 193.32.162.89

    Unique failed Login IDs :
     2 attempts : test@abc.com
     1 attempts : test3@zzz.com
     1 attempts : test3@fff.com
     1 attempts : soporte@xyz.com

   Referenced Entries:0 1
     > Jan 25 10:42:19 xxx plesk_saslauthd[156912]: failed mail authentication attempt for user 'test@xyz.com' (password len=10)
     > Jan 25 10:42:19 xxx postfix/smtpd[143596]: warning: unknown[87.120.121.102]: SASL LOGIN authentication failed: authentication failure
     > Jan 25 10:43:22 xxx plesk_saslauthd[170949]: failed mail authentication attempt for user 'iva' (password len=4)
     > Jan 25 10:43:37 xxx plesk_saslauthd[170949]: failed mail authentication attempt for user 'nataliag' (password len=9)
     > Jan 25 10:44:23 xxx plesk_saslauthd[184481]: failed mail authentication attempt for user 'news@xyz.com' (password len=13)
     > Jan 25 10:44:23 xxx postfix/smtpd[143596]: warning: unknown[45.151.99.245]: SASL LOGIN authentication failed: authentication failure
     > Jan 25 10:44:32 xxx plesk_saslauthd[184481]: failed mail authentication attempt for user 'mail@xyz.com' (password len=13)
     > Jan 25 10:44:32 xxx postfix/smtpd[143596]: warning: unknown[194.0.234.135]: SASL LOGIN authentication failed: authentication failure
     > Jan 25 10:50:05 xxx plesk_saslauthd[262997]: failed mail authentication attempt for user 'rachel' (password len=7)



Summary for 2025-01-25 11:00

    Unique suspect IP Addresses :
     2 attempts : 193.32.162.23

    Unique failed Login IDs :
     1 attempts : user


And so on.

Configuration

Adjust the size of the time periods being chunked out of the logfile, then change gap_minutes in the script.To change the number of repeats in the log for an IP or a login-name during the period, then change min_count_ip and/or min_count_login

Crontab

To put this into crontab to email yourself, use the command line:

echo -e "Subject:.....\nFrom:noreply@......com\nTo:someone@.....com\n\n$(/path-name/maillog-summary.bsh)" | sendmail -t -f noreply@....com

(Requires the script to be saved as maillog-summary.bsh, and the noreply@... com email address to exist).

Environment

NOTE - this script will not protect you from login attempts - you should consider installing fail2ban on plesk to provide that.

square