# For this script to work, you must define a local_mail table # listing all of your known mail sending hosts. # i.e. const local_mail: set[subnet] = { 1.2.3.4/32 }; @load conn-id @load smtp #global smtp_rejects_log = open_log_file("smtp_rejects"); #global smtp_remote_rejects_log = open_log_file("smtp_remote_rejects"); #global smtp_local_rejects_log = open_log_file("smtp_local_rejects"); #global smtp_known_mailserver_log = open_log_file("smtp_known_mailservers_rejects"); module SMTP; export { # The idea for this is that if a host makes more than spam_threshold # smtp connections per hour of which at least spam_percent of those are # rejected and the host is not a known mail sending host, then it's likely # sending spam or viruses. # const spam_threshold = 300 &redef; const spam_percent = 30 &redef; const spam_inspect_period = 60 mins &redef; # These are smtp status codes that are considered "rejected". const smtp_reject_codes: set[count] = { 550, # Requested action not taken: mailbox unavailable 551, # User not local; please try 553, # Requested action not taken: mailbox name not allowed 554, # Transaction failed }; redef enum Notice += { SMTP_PossibleSpam, # Host sending mail *to* internal hosts is suspicious SMTP_PossibleInternalSpam, # Internal host seems to be spamming SMTP_StrangeRejectBehavior, # Local mail server is getting high numbers of rejects }; } type conn_set: set[conn_id]; type smtp_counter: record { rejects: count; total: count; connects: conn_set; rej_conns: conn_set; recipients: string; }; global smtp_status_comparison: table[addr] of smtp_counter &create_expire=spam_inspect_period; event periodic_smtp_checker() { for( host in smtp_status_comparison ) { local hostsmtp = smtp_status_comparison[host]; if (hostsmtp$total >= spam_threshold) { local percent = (hostsmtp$rejects*100) / hostsmtp$total; if (percent >= spam_percent) { if ( host in local_mail ) { NOTICE([$note=SMTP_StrangeRejectBehavior, $msg=fmt("%s - sent: %d rejected: %d percent", host, hostsmtp$total, percent)]); #print smtp_known_mailserver_log, fmt("%s had %d / %d ( %d percent) rejected - sent to: %s", sender, foo$rejects, foo$total, percent); } else if ( is_local_addr(host) ) { NOTICE([$note=SMTP_PossibleInternalSpam, $msg=fmt("%s - sent: %d rejected: %d percent mailto: %s", host, hostsmtp$total, percent, hostsmtp$recipients)]); #print smtp_local_rejects_log, fmt("%s had %d / %d ( %d percent) rejected - sent to: %s", sender, foo$rejects, foo$total, percent); } else { NOTICE([$note=SMTP_PossibleSpam, $msg=fmt("%s - sent: %d rejected: %d percent", host, hostsmtp$total, percent)]); #print smtp_remote_rejects_log, fmt("%s had %d / %d ( %d percent) rejected - sent to: %s", sender, foo$rejects, foo$total, percent); } } } } schedule spam_inspect_period { periodic_smtp_checker() }; } event bro_init() { schedule spam_inspect_period { periodic_smtp_checker() }; } event smtp_reply(c: connection, is_orig: bool, code: count, cmd: string, msg: string, cont_resp: bool) { if ( c$id$orig_h !in smtp_status_comparison ) { local bar: set[conn_id]; local blarg: set[conn_id]; smtp_status_comparison[c$id$orig_h] = [$rejects=0, $total=0, $connects=bar, $rej_conns=blarg, $recipients=""]; } # Set the smtp_counter to the local var "foo" local foo = smtp_status_comparison[c$id$orig_h]; if ( code in smtp_reject_codes && c$id !in foo$rej_conns ) { ++foo$rejects; local session = smtp_sessions[c$id]; if( foo$recipients == "" ) { foo$recipients = session$recipients; } else if (session$recipients != "") { foo$recipients = cat(foo$recipients, ",", session$recipients); } add foo$rej_conns[c$id]; #print smtp_rejects_log, fmt("%f %s - %d - %s", c$start_time, id_string(c$id), code, msg); } if ( c$id !in foo$connects ) { ++foo$total; add foo$connects[c$id]; } }