Another bug fix.  ssh-ext couldn't ever record geo location.

@load global-ext
@load ssh
@load notice

module SSH;

export {
	const ssh_ext_log = open_log_file("ssh-ext") &raw_output;

	const password_guesses_limit = 30 &redef;
	const authentication_data_size = 5500 &redef;
	const guessing_timeout = 30 mins;

	# Keeps count of how many rejections a host has had
	global password_rejections: table[addr] of count &default=0 &write_expire=guessing_timeout;
	# Keeps track of hosts identified as guessing passwords
	global password_guessers: set[addr] &read_expire=guessing_timeout+1hr;

	type ssh_versions: record {
		client: string &default="";
		server: string &default="";
	};

	# Only monitor SSH connections for up to 15 minutes
	global active_ssh_conns: table[conn_id] of ssh_versions &create_expire=15mins;

	# If you want to lookup and log geoip data in the event of a failed login.
	const log_geodata_on_failure = F &redef;

	# The set of countries for which you'd like to throw notices upon successful login
	#   requires Bro compiled with libGeoIP support
	const watched_countries: set[string] = {"RO"} &redef;

	# Strange/bad host names to originate successful SSH logins
	const strange_hostnames =
			/^d?ns[0-9]*\./ |
			/^smtp[0-9]*\./ |
			/^mail[0-9]*\./ |
			/^pop[0-9]*\./  |
			/^imap[0-9]*\./ |
			/^www[0-9]*\./  |
			/^ftp[0-9]*\./  &redef;

	# This is a table with orig subnet as the key, and subnet as the value.
	const ignore_guessers: table[subnet] of subnet &redef;

	redef enum Notice += {
		SSH_Login,
		SSH_PasswordGuessing,
		SSH_LoginByPasswordGuesser,
		SSH_Login_From_Strange_Hostname,
		SSH_Bytecount_Inconsistency,
	};
}

# Examples for how to handle notices from this script.
#     (define these in a local script)...
#redef notice_policy += {
#	# Send email if a successful ssh login happens from or to a watched country
#	[$pred(n: notice_info) =
#		{ return (n$note == SSH::SSH_Login && n$sub in watched_countries); },
#	 $result = NOTICE_EMAIL],
#
#	# Send email if a password guesser logs in successfully anywhere
#	# To avoid false positives, setting the lower bound for notification to 50 bad password attempts.
#	[$pred(n: notice_info) =
#		{ return (n$note == SSH::SSH_LoginByPasswordGuesser && n$n > 50); },
#	 $result = NOTICE_EMAIL],
#
#	# Send email if a local host is password guessing.
#	[$pred(n: notice_info) =
#		{ return (n$note == SSH::SSH_PasswordGuessing &&
#		          is_local_addr(n$conn$id$orig_h)); },
#	 $result = NOTICE_EMAIL],
#};

# Don't stop processing SSH connections in the default ssh policy script
redef skip_processing_after_handshake = F;

event check_ssh_connection(c: connection, done: bool)
	{
	# If this is no longer a known SSH connection, just return.
	if ( c$id !in active_ssh_conns )
		return;

	# If this is still a live connection and the byte count has not
	# crossed the threshold, just return and let the resheduled check happen later.
	if ( !done && c$resp$size < authentication_data_size )
		return;

	# Make sure the server has sent back more than 50 bytes to filter out
	# hosts that are just port scanning.  Nothing is ever logged if the server
	# doesn't send back at least 50 bytes.
	if (c$resp$size < 50)
		return;

	local versions = active_ssh_conns[c$id];
	local status = "failure";
	local direction = is_local_addr(c$id$orig_h) ? "to" : "from";
	local location: geo_location;
	# Need to give these values defaults in bro.init.
	location$country_code=""; location$region=""; location$city=""; location$latitude=0.0; location$longitude=0.0;

	if ( done && c$resp$size < authentication_data_size )
		{
		# presumed failure

		# Track the number of rejections
		if ( !(c$id$orig_h in ignore_guessers &&
		       c$id$resp_h in ignore_guessers[c$id$orig_h]) )
			password_rejections[c$id$orig_h] += 1;

		if ( password_rejections[c$id$orig_h] > password_guesses_limit &&
		     c$id$orig_h !in password_guessers )
			{
			add password_guessers[c$id$orig_h];
			NOTICE([$note=SSH_PasswordGuessing,
			        $conn=c,
			        $msg=fmt("SSH password guessing by %s", c$id$orig_h),
			        $sub=fmt("%d failed logins", password_rejections[c$id$orig_h]),
			        $n=password_rejections[c$id$orig_h]]);
			}
		}
	# TODO: This is to work around a quasi-bug in Bro which occasionally
	#       causes the byte count to be oversized.
	else if (c$resp$size < 20000000)
		{
		# presumed successful login
		status = "success";

		if ( password_rejections[c$id$orig_h] > password_guesses_limit &&
		     c$id$orig_h !in password_guessers)
			{
			add password_guessers[c$id$orig_h];
			NOTICE([$note=SSH_LoginByPasswordGuesser,
			        $conn=c,
			        $n=password_rejections[c$id$orig_h],
			        $msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h),
			        $sub=fmt("%d failed logins", password_rejections[c$id$orig_h])]);
			}

		local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)",
		              direction, location$country_code, location$region, location$city,
		              location$latitude, location$longitude,
		              numeric_id_string(c$id), c$resp$size);
		# TODO: rewrite the message once a location variable can be put in notices
		NOTICE([$note=SSH_Login,
		        $conn=c,
		        $msg=message,
		        $sub=location$country_code]);

		# Check to see if this login came from a weird hostname (nameserver, mail server, etc.)
		when( local hostname = lookup_addr(c$id$orig_h) )
			{
			if ( strange_hostnames in hostname )
				{
				NOTICE([$note=SSH_Login_From_Strange_Hostname,
				        $conn=c,
				        $msg=fmt("Strange login from %s", hostname),
				        $sub=hostname]);
				}
			}
		}
	else if (c$resp$size >= 20000000)
		{
		NOTICE([$note=SSH_Bytecount_Inconsistency,
		        $conn=c,
		        $msg="During byte counting in extended SSH analysis, an overly large value was seen.",
		        $sub=fmt("%d",c$resp$size)]);
		}

	if ( (log_geodata_on_failure && status == "failure") ||
	     status == "success" )
		{
		location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);
		}

	print ssh_ext_log, cat_sep("\t", "\\N", c$start_time,
				c$id$orig_h, fmt("%d", c$id$orig_p),
				c$id$resp_h, fmt("%d", c$id$resp_p),
				status, direction,
				location$country_code, location$region,
				versions$client, versions$server,
				c$resp$size);

	delete active_ssh_conns[c$id];
	# Stop watching this connection, we don't care about it anymore.
	skip_further_processing(c$id);
	set_record_packets(c$id, F);
	}

event connection_state_remove(c: connection)
	{
	event check_ssh_connection(c, T);
	}

event ssh_watcher(c: connection)
	{
	local id = c$id;
	# don't go any further if this connection is gone already!
	if ( !connection_exists(id) )
		{
		delete active_ssh_conns[id];
		return;
		}

	event check_ssh_connection(c, F);
	if ( c$id in active_ssh_conns )
		schedule +2mins { ssh_watcher(c) };
	}

event ssh_client_version(c: connection, version: string)
	{
	if ( c$id in active_ssh_conns )
		active_ssh_conns[c$id]$client = version;
	}

event ssh_server_version(c: connection, version: string)
	{
	if ( c$id in active_ssh_conns )
		active_ssh_conns[c$id]$server = version;
	}

event protocol_confirmation(c: connection, atype: count, aid: count)
	{
	if ( atype == ANALYZER_SSH )
		{
		local tmp: ssh_versions;
		active_ssh_conns[c$id]=tmp;
		schedule +2mins { ssh_watcher(c) };
		}
	}

@load global-ext
@load ssh
@load notice

module SSH;

export {
	const ssh_ext_log = open_log_file("ssh-ext") &raw_output;

	const password_guesses_limit = 30 &redef;
	const authentication_data_size = 5500 &redef;
	const guessing_timeout = 30 mins;

	# Keeps count of how many rejections a host has had
	global password_rejections: table[addr] of count &default=0 &write_expire=guessing_timeout;
	# Keeps track of hosts identified as guessing passwords
	global password_guessers: set[addr] &read_expire=guessing_timeout+1hr;

	type ssh_versions: record {
		client: string &default="";
		server: string &default="";
	};

	# Only monitor SSH connections for up to 15 minutes
	global active_ssh_conns: table[conn_id] of ssh_versions &create_expire=15mins;

	# If you want to lookup and log geoip data in the event of a failed login.
	const log_geodata_on_failure = F &redef;

	# The set of countries for which you'd like to throw notices upon successful login
	#   requires Bro compiled with libGeoIP support
	const watched_countries: set[string] = {"RO"} &redef;

	# Strange/bad host names to originate successful SSH logins
	const strange_hostnames =
			/^d?ns[0-9]*\./ |
			/^smtp[0-9]*\./ |
			/^mail[0-9]*\./ |
			/^pop[0-9]*\./  |
			/^imap[0-9]*\./ |
			/^www[0-9]*\./  |
			/^ftp[0-9]*\./  &redef;

	# This is a table with orig subnet as the key, and subnet as the value.
	const ignore_guessers: table[subnet] of subnet &redef;

	redef enum Notice += {
		SSH_Login,
		SSH_PasswordGuessing,
		SSH_LoginByPasswordGuesser,
		SSH_Login_From_Strange_Hostname,
		SSH_Bytecount_Inconsistency,
	};
}

# Examples for how to handle notices from this script.
#     (define these in a local script)...
#redef notice_policy += {
#	# Send email if a successful ssh login happens from or to a watched country
#	[$pred(n: notice_info) =
#		{ return (n$note == SSH::SSH_Login && n$sub in watched_countries); },
#	 $result = NOTICE_EMAIL],
#
#	# Send email if a password guesser logs in successfully anywhere
#	# To avoid false positives, setting the lower bound for notification to 50 bad password attempts.
#	[$pred(n: notice_info) =
#		{ return (n$note == SSH::SSH_LoginByPasswordGuesser && n$n > 50); },
#	 $result = NOTICE_EMAIL],
#
#	# Send email if a local host is password guessing.
#	[$pred(n: notice_info) =
#		{ return (n$note == SSH::SSH_PasswordGuessing &&
#		          is_local_addr(n$conn$id$orig_h)); },
#	 $result = NOTICE_EMAIL],
#};

# Don't stop processing SSH connections in the default ssh policy script
redef skip_processing_after_handshake = F;

event check_ssh_connection(c: connection, done: bool)
	{
	# If this is no longer a known SSH connection, just return.
	if ( c$id !in active_ssh_conns )
		return;

	# If this is still a live connection and the byte count has not
	# crossed the threshold, just return and let the resheduled check happen later.
	if ( !done && c$resp$size < authentication_data_size )
		return;

	# Make sure the server has sent back more than 50 bytes to filter out
	# hosts that are just port scanning.  Nothing is ever logged if the server
	# doesn't send back at least 50 bytes.
	if (c$resp$size < 50)
		return;

	local versions = active_ssh_conns[c$id];
	local status = "";
	local direction = is_local_addr(c$id$orig_h) ? "to" : "from";
	local location: geo_location;
	# Need to give these values defaults in bro.init.
	location$country_code=""; location$region=""; location$city=""; location$latitude=0.0; location$longitude=0.0;
	if ( (log_geodata_on_failure && status == "failure") ||
	     status == "success" )
		location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h);

	if ( done && c$resp$size < authentication_data_size )
		{
		# presumed failure
		status = "failure";

		# Track the number of rejections
		if ( !(c$id$orig_h in ignore_guessers &&
		       c$id$resp_h in ignore_guessers[c$id$orig_h]) )
			password_rejections[c$id$orig_h] += 1;

		if ( password_rejections[c$id$orig_h] > password_guesses_limit &&
		     c$id$orig_h !in password_guessers )
			{
			add password_guessers[c$id$orig_h];
			NOTICE([$note=SSH_PasswordGuessing,
			        $conn=c,
			        $msg=fmt("SSH password guessing by %s", c$id$orig_h),
			        $sub=fmt("%d failed logins", password_rejections[c$id$orig_h]),
			        $n=password_rejections[c$id$orig_h]]);
			}
		}
	# TODO: This is to work around a quasi-bug in Bro which occasionally
	#       causes the byte count to be oversized.
	else if (c$resp$size < 20000000)
		{
		# presumed successful login
		status = "success";

		if ( password_rejections[c$id$orig_h] > password_guesses_limit &&
		     c$id$orig_h !in password_guessers)
			{
			add password_guessers[c$id$orig_h];
			NOTICE([$note=SSH_LoginByPasswordGuesser,
			        $conn=c,
			        $n=password_rejections[c$id$orig_h],
			        $msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h),
			        $sub=fmt("%d failed logins", password_rejections[c$id$orig_h])]);
			}

		local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)",
		              direction, location$country_code, location$region, location$city,
		              location$latitude, location$longitude,
		              numeric_id_string(c$id), c$resp$size);
		# TODO: rewrite the message once a location variable can be put in notices
		NOTICE([$note=SSH_Login,
		        $conn=c,
		        $msg=message,
		        $sub=location$country_code]);

		# Check to see if this login came from a weird hostname (nameserver, mail server, etc.)
		when( local hostname = lookup_addr(c$id$orig_h) )
			{
			if ( strange_hostnames in hostname )
				{
				NOTICE([$note=SSH_Login_From_Strange_Hostname,
				        $conn=c,
				        $msg=fmt("Strange login from %s", hostname),
				        $sub=hostname]);
				}
			}
		}
	else if (c$resp$size >= 20000000)
		{
		NOTICE([$note=SSH_Bytecount_Inconsistency,
		        $conn=c,
		        $msg="During byte counting in extended SSH analysis, an overly large value was seen.",
		        $sub=fmt("%d",c$resp$size)]);
		}

	print ssh_ext_log, cat_sep("\t", "\\N", c$start_time,
				c$id$orig_h, fmt("%d", c$id$orig_p),
				c$id$resp_h, fmt("%d", c$id$resp_p),
				status, direction,
				location$country_code, location$region,
				versions$client, versions$server,
				c$resp$size);

	delete active_ssh_conns[c$id];
	# Stop watching this connection, we don't care about it anymore.
	skip_further_processing(c$id);
	set_record_packets(c$id, F);
	}

event connection_state_remove(c: connection)
	{
	event check_ssh_connection(c, T);
	}

event ssh_watcher(c: connection)
	{
	local id = c$id;
	# don't go any further if this connection is gone already!
	if ( !connection_exists(id) )
		{
		delete active_ssh_conns[id];
		return;
		}

	event check_ssh_connection(c, F);
	if ( c$id in active_ssh_conns )
		schedule +2mins { ssh_watcher(c) };
	}

event ssh_client_version(c: connection, version: string)
	{
	if ( c$id in active_ssh_conns )
		active_ssh_conns[c$id]$client = version;
	}

event ssh_server_version(c: connection, version: string)
	{
	if ( c$id in active_ssh_conns )
		active_ssh_conns[c$id]$server = version;
	}

event protocol_confirmation(c: connection, atype: count, aid: count)
	{
	if ( atype == ANALYZER_SSH )
		{
		local tmp: ssh_versions;
		active_ssh_conns[c$id]=tmp;
		schedule +2mins { ssh_watcher(c) };
		}
	}


Updating the version check code

Updating the version check code

Previous code calls a function which does not seem to work or exist in bro 2.5.

# This Bro script appends JA3S (JA3 Server) to ssl.log
# Version 1.0 (August 2018)
# This builds a fingerprint for the SSL Server Hello packet based on SSL/TLS version, cipher picked, and extensions used.
# Designed to be used in conjunction with JA3 to fingerprint SSL communication between clients and servers.
#
# Authors: John B. Althouse (jalthouse@salesforce.com) Jeff Atkinson (jatkinson@salesforce.com)
# Copyright (c) 2018, salesforce.com, inc.
# All rights reserved.
# Licensed under the BSD 3-Clause license.
# For full license text, see LICENSE.txt file in the repo root  or https://opensource.org/licenses/BSD-3-Clause
#

module JA3_Server;

export {
redef enum Log::ID += { LOG };
}

type JA3Sstorage: record {
       server_version:      count &default=0 &log;
       server_cipher:      count &default=0 &log;
       server_extensions:   string &default="" &log;
};

redef record connection += {
       ja3sfp: JA3Sstorage &optional;
};

redef record SSL::Info += {
  ja3s:            string &optional &log;
# LOG FIELD VALUES #
#  ja3s_version:  string &optional &log;
#  ja3s_cipher:  string &optional &log;
#  ja3s_extensions: string &optional &log;
};

const sep = "-";
event bro_init() {
    Log::create_stream(JA3_Server::LOG,[$columns=JA3Sstorage, $path="ja3sfp"]);
}

event ssl_extension(c: connection, is_orig: bool, code: count, val: string)
{
if ( ! c?$ja3sfp )
    c$ja3sfp=JA3Sstorage();
    if ( is_orig == F ) {
        if ( c$ja3sfp$server_extensions == "" ) {
            c$ja3sfp$server_extensions = cat(code);
        }
        else {
            c$ja3sfp$server_extensions = string_cat(c$ja3sfp$server_extensions, sep,cat(code));
        }
    }
}

@if ( ( Version::number >= 20600 ) || ( Version::number == 20500 && Version::info$commit >= 944 ) )
event ssl_server_hello(c: connection, version: count, record_version: count, possible_ts: time, server_random: string, session_id: string, cipher: count, comp_method: count) &priority=1
@else
event ssl_server_hello(c: connection, version: count, possible_ts: time, server_random: string, session_id: string, cipher: count, comp_method: count) &priority=1
@endif
{
    if ( !c?$ja3sfp )
    c$ja3sfp=JA3Sstorage();
    c$ja3sfp$server_version = version;
    c$ja3sfp$server_cipher = cipher;
    local sep2 = ",";
    local ja3s_string = string_cat(cat(c$ja3sfp$server_version),sep2,cat(c$ja3sfp$server_cipher),sep2,c$ja3sfp$server_extensions);
    local ja3sfp_1 = md5_hash(ja3s_string);
    c$ssl$ja3s = ja3sfp_1;

# LOG FIELD VALUES #
#c$ssl$ja3s_version = cat(c$ja3sfp$server_version);
#c$ssl$ja3s_cipher = cat(c$ja3sfp$server_cipher);
#c$ssl$ja3s_extensions = c$ja3sfp$server_extensions;
#
# FOR DEBUGGING #
#print "JA3S: "+ja3sfp_1+" Fingerprint String: "+ja3s_string;

}

# This Bro script appends JA3S (JA3 Server) to ssl.log
# Version 1.0 (August 2018)
# This builds a fingerprint for the SSL Server Hello packet based on SSL/TLS version, cipher picked, and extensions used.
# Designed to be used in conjunction with JA3 to fingerprint SSL communication between clients and servers.
#
# Authors: John B. Althouse (jalthouse@salesforce.com) Jeff Atkinson (jatkinson@salesforce.com)
# Copyright (c) 2018, salesforce.com, inc.
# All rights reserved.
# Licensed under the BSD 3-Clause license.
# For full license text, see LICENSE.txt file in the repo root  or https://opensource.org/licenses/BSD-3-Clause
#

module JA3_Server;

export {
redef enum Log::ID += { LOG };
}

type JA3Sstorage: record {
       server_version:      count &default=0 &log;
       server_cipher:      count &default=0 &log;
       server_extensions:   string &default="" &log;
};

redef record connection += {
       ja3sfp: JA3Sstorage &optional;
};

redef record SSL::Info += {
  ja3s:            string &optional &log;
# LOG FIELD VALUES #
#  ja3s_version:  string &optional &log;
#  ja3s_cipher:  string &optional &log;
#  ja3s_extensions: string &optional &log;
};

const sep = "-";
event bro_init() {
    Log::create_stream(JA3_Server::LOG,[$columns=JA3Sstorage, $path="ja3sfp"]);
}

event ssl_extension(c: connection, is_orig: bool, code: count, val: string)
{
if ( ! c?$ja3sfp )
    c$ja3sfp=JA3Sstorage();
    if ( is_orig == F ) {
        if ( c$ja3sfp$server_extensions == "" ) {
            c$ja3sfp$server_extensions = cat(code);
        }
        else {
            c$ja3sfp$server_extensions = string_cat(c$ja3sfp$server_extensions, sep,cat(code));
        }
    }
}

@if ( Version::at_least("2.6") || ( Version::number == 20500 && Version::info$commit >= 944 ) )
event ssl_server_hello(c: connection, version: count, record_version: count, possible_ts: time, server_random: string, session_id: string, cipher: count, comp_method: count) &priority=1
@else
event ssl_server_hello(c: connection, version: count, possible_ts: time, server_random: string, session_id: string, cipher: count, comp_method: count) &priority=1
@endif
{
    if ( !c?$ja3sfp )
    c$ja3sfp=JA3Sstorage();
    c$ja3sfp$server_version = version;
    c$ja3sfp$server_cipher = cipher;
    local sep2 = ",";
    local ja3s_string = string_cat(cat(c$ja3sfp$server_version),sep2,cat(c$ja3sfp$server_cipher),sep2,c$ja3sfp$server_extensions);
    local ja3sfp_1 = md5_hash(ja3s_string);
    c$ssl$ja3s = ja3sfp_1;

# LOG FIELD VALUES #
#c$ssl$ja3s_version = cat(c$ja3sfp$server_version);
#c$ssl$ja3s_cipher = cat(c$ja3sfp$server_cipher);
#c$ssl$ja3s_extensions = c$ja3sfp$server_extensions;
#
# FOR DEBUGGING #
#print "JA3S: "+ja3sfp_1+" Fingerprint String: "+ja3s_string;

}


phishing counter table should be create_expire

@load global-ext
@load smtp-ext

module PHISH;

global smtp_password_conns: set[conn_id] &read_expire=2mins;

export {
    redef enum Notice += {
        SMTP_PossiblePWPhish,
        SMTP_PossiblePWPhishReply,
    };
    global phishing_counter: table[string] of count &default=0 &create_expire=1hr &synchronized;
    global phishing_reply_tos: set[string] &synchronized &redef;
    global phishing_ignore_froms: set[string] &redef;
    global phishing_threshold = 50;

    const phish_keywords =
          /[pP][aA][sS][sS][wW][oO][rR][dD]/
        | /[uU][sS][eE][rR].?[nN][aA][mM][eE]/
        | /[nN][eE][tT][iI][dD]/ &redef;
}

event bro_init()
{
    LOG::create_logs("password-mail", All, F, T);
    LOG::define_header("password-mail", cat_sep("\t", "",
                                          "ts",
                                          "orig_h", "orig_p",
                                          "resp_h", "resp_p",
                                          "helo", "message-id", "in-reply-to",
                                          "mailfrom", "rcptto",
                                          "date", "from", "reply_to", "to", "subject",
                                          "files", "last_reply", "x-originating-ip",
                                          "path", "is_webmail", "agent"));
}

event bro_done()
{
    print "Counter";
    print phishing_counter;
    print "bad reply-tos";
    print phishing_reply_tos;
}

event smtp_data(c: connection, is_orig: bool, data: string)
{
    if(is_local_addr(c$id$orig_h))
        return;
    # look for 'password'
    if(phish_keywords in data)
        add smtp_password_conns[c$id];
}

event smtp_ext(id: conn_id, si: smtp_ext_session_info)
{
    if(is_local_addr(id$orig_h)) {
        for (to in si$rcptto){
            if(to in phishing_reply_tos){
                NOTICE([$note=SMTP_PossiblePWPhishReply,
                        $msg=fmt("%s replied to %s", si$mailfrom, to),
                        $sub=si$mailfrom
                      ]);
            }
        }
    } else {
        if (id !in smtp_password_conns)
            return;
        if(si$mailfrom in phishing_ignore_froms)
            return;
        if(++(phishing_counter[si$mailfrom]) > phishing_threshold){
            local to_add ="";
            if(si$reply_to != "")
                to_add = si$reply_to;
            else
                to_add = si$mailfrom;
            if(to_add !in phishing_reply_tos){
                add phishing_reply_tos[to_add];
                NOTICE([$note=SMTP_PossiblePWPhish,
                        $msg=fmt("%s(%s) may be phishing - %s", si$mailfrom, si$reply_to, si$subject),
                        $sub=si$mailfrom
                      ]);
            }
        }

        local log = LOG::get_file_by_id("password-mail", id, F);
        print log, cat_sep("\t", "\\N",
                    network_time(),
                    id$orig_h, port_to_count(id$orig_p), id$resp_h, port_to_count(id$resp_p),
                    si$helo,
                    si$msg_id,
                    si$in_reply_to,
                    si$mailfrom,
                    fmt_str_set(si$rcptto, /["'<>]|([[:blank:]].*$)/),
                    si$date,
                    si$from,
                    si$reply_to,
                    fmt_str_set(si$to, /["']/),
                    si$subject,
                    fmt_str_set(si$files, /["']/),
                    si$last_reply,
                    si$x_originating_ip,
                    si$path,
                    si$is_webmail,
                    si$agent);
    }

}

@load global-ext
@load smtp-ext

module PHISH;

global smtp_password_conns: set[conn_id] &read_expire=2mins;

export {
    redef enum Notice += {
        SMTP_PossiblePWPhish,
        SMTP_PossiblePWPhishReply,
    };
    global phishing_counter: table[string] of count &default=0 &write_expire=1hr &synchronized;
    global phishing_reply_tos: set[string] &synchronized &redef;
    global phishing_ignore_froms: set[string] &redef;
    global phishing_threshold = 50;

    const phish_keywords =
          /[pP][aA][sS][sS][wW][oO][rR][dD]/
        | /[uU][sS][eE][rR].?[nN][aA][mM][eE]/
        | /[nN][eE][tT][iI][dD]/ &redef;
}

event bro_init()
{
    LOG::create_logs("password-mail", All, F, T);
    LOG::define_header("password-mail", cat_sep("\t", "",
                                          "ts",
                                          "orig_h", "orig_p",
                                          "resp_h", "resp_p",
                                          "helo", "message-id", "in-reply-to",
                                          "mailfrom", "rcptto",
                                          "date", "from", "reply_to", "to", "subject",
                                          "files", "last_reply", "x-originating-ip",
                                          "path", "is_webmail", "agent"));
}

event bro_done()
{
    print "Counter";
    print phishing_counter;
    print "bad reply-tos";
    print phishing_reply_tos;
}

event smtp_data(c: connection, is_orig: bool, data: string)
{
    if(is_local_addr(c$id$orig_h))
        return;
    # look for 'password'
    if(phish_keywords in data)
        add smtp_password_conns[c$id];
}

event smtp_ext(id: conn_id, si: smtp_ext_session_info)
{
    if(is_local_addr(id$orig_h)) {
        for (to in si$rcptto){
            if(to in phishing_reply_tos){
                NOTICE([$note=SMTP_PossiblePWPhishReply,
                        $msg=fmt("%s replied to %s", si$mailfrom, to),
                        $sub=si$mailfrom
                      ]);
            }
        }
    } else {
        if (id !in smtp_password_conns)
            return;
        if(si$mailfrom in phishing_ignore_froms)
            return;
        if(++(phishing_counter[si$mailfrom]) > phishing_threshold){
            local to_add ="";
            if(si$reply_to != "")
                to_add = si$reply_to;
            else
                to_add = si$mailfrom;
            if(to_add !in phishing_reply_tos){
                add phishing_reply_tos[to_add];
                NOTICE([$note=SMTP_PossiblePWPhish,
                        $msg=fmt("%s(%s) may be phishing - %s", si$mailfrom, si$reply_to, si$subject),
                        $sub=si$mailfrom
                      ]);
            }
        }

        local log = LOG::get_file_by_id("password-mail", id, F);
        print log, cat_sep("\t", "\\N",
                    network_time(),
                    id$orig_h, port_to_count(id$orig_p), id$resp_h, port_to_count(id$resp_p),
                    si$helo,
                    si$msg_id,
                    si$in_reply_to,
                    si$mailfrom,
                    fmt_str_set(si$rcptto, /["'<>]|([[:blank:]].*$)/),
                    si$date,
                    si$from,
                    si$reply_to,
                    fmt_str_set(si$to, /["']/),
                    si$subject,
                    fmt_str_set(si$files, /["']/),
                    si$last_reply,
                    si$x_originating_ip,
                    si$path,
                    si$is_webmail,
                    si$agent);
    }

}


filter mdns log in bro

filter mdns log in bro

##! Local site policy. Customize as appropriate.
##!
##! This file will not be overwritten when upgrading or reinstalling!

redef ignore_checksums = T;
redef SSL::disable_analyzer_after_detection = F;

#@load site/sqlite.bro

# This script logs which scripts were loaded during each run.
@load misc/loaded-scripts

# Apply the default tuning scripts for common tuning settings.
@load tuning/defaults
@load tuning/json-logs

# Load the scan detection script.
@load misc/scan

# Log some information about web applications being used by users
# on your network.
@load misc/app-stats

# Detect traceroute being run on the network.
@load misc/detect-traceroute

# Generate notices when vulnerable versions of software are discovered.
# The default is to only monitor software found in the address space defined
# as "local".  Refer to the software framework's documentation for more
# information.
@load frameworks/software/vulnerable

# Detect software changing (e.g. attacker installing hacked SSHD).
@load frameworks/software/version-changes
@load frameworks/software/windows-version-detection

# This adds signatures to detect cleartext forward and reverse windows shells.
@load-sigs frameworks/signatures/detect-windows-shells

# Load all of the scripts that detect software in various protocols.
@load protocols/ftp/software
@load protocols/smtp/software
@load protocols/ssh/software
@load protocols/http/software
# The detect-webapps script could possibly cause performance trouble when
# running on live traffic.  Enable it cautiously.
#@load protocols/http/detect-webapps

# This script detects DNS results pointing toward your Site::local_nets
# where the name is not part of your local DNS zone and is being hosted
# externally.  Requires that the Site::local_zones variable is defined.
@load protocols/dns/detect-external-names

# Script to detect various activity in FTP sessions.
@load protocols/ftp/detect

# Scripts that do asset tracking.
@load protocols/conn/known-hosts
@load protocols/conn/known-services
@load protocols/ssl/known-certs

# This script enables SSL/TLS certificate validation.
@load protocols/ssl/validate-certs

# This script prevents the logging of SSL CA certificates in x509.log
@load protocols/ssl/log-hostcerts-only

# Uncomment the following line to check each SSL certificate hash against the ICSI
# certificate notary service; see http://notary.icsi.berkeley.edu .
# @load protocols/ssl/notary

# If you have libGeoIP support built in, do some geographic detections and
# logging for SSH traffic.
@load protocols/ssh/geo-data
# Detect hosts doing SSH bruteforce attacks.
@load protocols/ssh/detect-bruteforcing
# Detect logins using "interesting" hostnames.
@load protocols/ssh/interesting-hostnames

# Detect SQL injection attacks.
@load protocols/http/detect-sqli

#### Network File Handling ####

# Enable MD5 and SHA1 hashing for all files.
@load frameworks/files/hash-all-files

# Detect SHA1 sums in Team Cymru's Malware Hash Registry.
@load frameworks/files/detect-MHR

# Uncomment the following line to enable detection of the heartbleed attack. Enabling
# this might impact performance a bit.
@load policy/protocols/ssl/heartbleed

# enable link-layer address information to connection logs
#@load policy/protocols/conn/mac-logging

redef restrict_filters += [["not-mdns"] = "not port 5353"];

redef SSL::disable_analyzer_after_detection = F;
redef Communication::listen_interface = 127.0.0.1;

@load base/protocols/dhcp

##! Local site policy. Customize as appropriate.
##!
##! This file will not be overwritten when upgrading or reinstalling!

redef ignore_checksums = T;
redef SSL::disable_analyzer_after_detection = F;

#@load site/sqlite.bro

# This script logs which scripts were loaded during each run.
@load misc/loaded-scripts

# Apply the default tuning scripts for common tuning settings.
@load tuning/defaults
@load tuning/json-logs

# Load the scan detection script.
@load misc/scan

# Log some information about web applications being used by users
# on your network.
@load misc/app-stats

# Detect traceroute being run on the network.
@load misc/detect-traceroute

# Generate notices when vulnerable versions of software are discovered.
# The default is to only monitor software found in the address space defined
# as "local".  Refer to the software framework's documentation for more
# information.
@load frameworks/software/vulnerable

# Detect software changing (e.g. attacker installing hacked SSHD).
@load frameworks/software/version-changes
@load frameworks/software/windows-version-detection

# This adds signatures to detect cleartext forward and reverse windows shells.
@load-sigs frameworks/signatures/detect-windows-shells

# Load all of the scripts that detect software in various protocols.
@load protocols/ftp/software
@load protocols/smtp/software
@load protocols/ssh/software
@load protocols/http/software
# The detect-webapps script could possibly cause performance trouble when
# running on live traffic.  Enable it cautiously.
#@load protocols/http/detect-webapps

# This script detects DNS results pointing toward your Site::local_nets
# where the name is not part of your local DNS zone and is being hosted
# externally.  Requires that the Site::local_zones variable is defined.
@load protocols/dns/detect-external-names

# Script to detect various activity in FTP sessions.
@load protocols/ftp/detect

# Scripts that do asset tracking.
@load protocols/conn/known-hosts
@load protocols/conn/known-services
@load protocols/ssl/known-certs

# This script enables SSL/TLS certificate validation.
@load protocols/ssl/validate-certs

# This script prevents the logging of SSL CA certificates in x509.log
@load protocols/ssl/log-hostcerts-only

# Uncomment the following line to check each SSL certificate hash against the ICSI
# certificate notary service; see http://notary.icsi.berkeley.edu .
# @load protocols/ssl/notary

# If you have libGeoIP support built in, do some geographic detections and
# logging for SSH traffic.
@load protocols/ssh/geo-data
# Detect hosts doing SSH bruteforce attacks.
@load protocols/ssh/detect-bruteforcing
# Detect logins using "interesting" hostnames.
@load protocols/ssh/interesting-hostnames

# Detect SQL injection attacks.
@load protocols/http/detect-sqli

#### Network File Handling ####

# Enable MD5 and SHA1 hashing for all files.
@load frameworks/files/hash-all-files

# Detect SHA1 sums in Team Cymru's Malware Hash Registry.
@load frameworks/files/detect-MHR

# Uncomment the following line to enable detection of the heartbleed attack. Enabling
# this might impact performance a bit.
@load policy/protocols/ssl/heartbleed

# enable link-layer address information to connection logs
#@load policy/protocols/conn/mac-logging

redef SSL::disable_analyzer_after_detection = F;
redef Communication::listen_interface = 127.0.0.1;

@load base/protocols/dhcp


Change bro_init to zeek_init

Change bro_init to zeek_init

@load base/protocols/conn
@load base/utils/time

# This is probably not so great to reach into the Conn namespace..
module Conn;

export {
function set_conn_log_data_hack(c: connection)
	{
	Conn::set_conn(c, T);
	}
}

# Now onto the actual code for this script...

module LongConnection;

export {
	redef enum Log::ID += { LOG };

	redef enum Notice::Type += {
		## Notice for when a long connection is found.
		## The `sub` field in the notice represents the number
		## of seconds the connection has currently been alive.
		LongConnection::found
	};

	## Aliasing vector of interval values as
	## "Durations"
	type Durations: vector of interval;

	## The default duration that you are locally
	## considering a connection to be "long".
	const default_durations = Durations(10min, 30min, 1hr, 12hr, 24hrs, 3days) &redef;

	## These are special cases for particular hosts or subnets
	## that you may want to watch for longer or shorter
	## durations than the default.
	const special_cases: table[subnet] of Durations = {} &redef;
}

redef record connection += {
	## Offset of the currently watched connection duration by the long-connections script.
	long_conn_offset: count &default=0;
};

event zeek_init() &priority=5
	{
	Log::create_stream(LOG, [$columns=Conn::Info, $path="conn_long"]);
	}

function get_durations(c: connection): Durations
	{
	local check_it: Durations;
	if ( c$id$orig_h in special_cases )
		check_it = special_cases[c$id$orig_h];
	else if ( c$id$resp_h in special_cases )
		check_it = special_cases[c$id$resp_h];
	else
		check_it = default_durations;

	return check_it;
	}

function long_callback(c: connection, cnt: count): interval
	{
	local check_it = get_durations(c);

	if ( c$long_conn_offset < |check_it| && c$duration >= check_it[c$long_conn_offset] )
		{
		Conn::set_conn_log_data_hack(c);
		Log::write(LongConnection::LOG, c$conn);

		local message = fmt("%s -> %s:%s remained alive for longer than %s",
		                    c$id$orig_h, c$id$resp_h, c$id$resp_p, duration_to_mins_secs(c$duration));
		NOTICE([$note=LongConnection::found,
		        $msg=message,
		        $sub=fmt("%.2f", c$duration),
		        $conn=c]);

		++c$long_conn_offset;
		}

	# Keep watching if there are potentially more thresholds.
	if ( c$long_conn_offset < |check_it| )
		return check_it[c$long_conn_offset];
	else
		return -1sec;
	}

event connection_established(c: connection)
	{
	local check = get_durations(c);
	if ( |check| > 0 )
		{
		ConnPolling::watch(c, long_callback, 1, check[0]);
		}
	}

@load base/protocols/conn
@load base/utils/time

# This is probably not so great to reach into the Conn namespace..
module Conn;

export {
function set_conn_log_data_hack(c: connection)
	{
	Conn::set_conn(c, T);
	}
}

# Now onto the actual code for this script...

module LongConnection;

export {
	redef enum Log::ID += { LOG };

	redef enum Notice::Type += {
		## Notice for when a long connection is found.
		## The `sub` field in the notice represents the number
		## of seconds the connection has currently been alive.
		LongConnection::found
	};

	## Aliasing vector of interval values as
	## "Durations"
	type Durations: vector of interval;

	## The default duration that you are locally
	## considering a connection to be "long".
	const default_durations = Durations(10min, 30min, 1hr, 12hr, 24hrs, 3days) &redef;

	## These are special cases for particular hosts or subnets
	## that you may want to watch for longer or shorter
	## durations than the default.
	const special_cases: table[subnet] of Durations = {} &redef;
}

redef record connection += {
	## Offset of the currently watched connection duration by the long-connections script.
	long_conn_offset: count &default=0;
};

event bro_init() &priority=5
	{
	Log::create_stream(LOG, [$columns=Conn::Info, $path="conn_long"]);
	}

function get_durations(c: connection): Durations
	{
	local check_it: Durations;
	if ( c$id$orig_h in special_cases )
		check_it = special_cases[c$id$orig_h];
	else if ( c$id$resp_h in special_cases )
		check_it = special_cases[c$id$resp_h];
	else
		check_it = default_durations;

	return check_it;
	}

function long_callback(c: connection, cnt: count): interval
	{
	local check_it = get_durations(c);

	if ( c$long_conn_offset < |check_it| && c$duration >= check_it[c$long_conn_offset] )
		{
		Conn::set_conn_log_data_hack(c);
		Log::write(LongConnection::LOG, c$conn);

		local message = fmt("%s -> %s:%s remained alive for longer than %s",
		                    c$id$orig_h, c$id$resp_h, c$id$resp_p, duration_to_mins_secs(c$duration));
		NOTICE([$note=LongConnection::found,
		        $msg=message,
		        $sub=fmt("%.2f", c$duration),
		        $conn=c]);

		++c$long_conn_offset;
		}

	# Keep watching if there are potentially more thresholds.
	if ( c$long_conn_offset < |check_it| )
		return check_it[c$long_conn_offset];
	else
		return -1sec;
	}

event connection_established(c: connection)
	{
	local check = get_durations(c);
	if ( |check| > 0 )
		{
		ConnPolling::watch(c, long_callback, 1, check[0]);
		}
	}


Turn off tagged json for now

Turn off tagged json for now

## Setup Kafka output
@load Bro/Kafka/logs-to-kafka

redef Kafka::topic_name = "bro_raw";
redef Kafka::json_timestamps = JSON::TS_ISO8601;
redef Kafka::tag_json = F;

## Setup event extension to include sensor and probe name
type Extension: record {
    ## The name of the system that wrote this log. This
    ## is defined in the  const so that
    ## a system running lots of processes can give the
    ## same value for any process that writes a log.
    system:   string &log;
    ## The name of the process that wrote the log. In
    ## clusters, this will typically be the name of the
    ## worker that wrote the log.
    proc:     string &log;
};

function add_log_extension(path: string): Extension
{
    return Extension($system = ROCK::sensor_id,
                     $proc   = peer_description);
}

redef Log::default_ext_func   = add_log_extension;
redef Log::default_ext_prefix = "@";
redef Log::default_scope_sep  = "_";

## Setup Kafka output
@load Bro/Kafka/logs-to-kafka

redef Kafka::topic_name = "bro_raw";
redef Kafka::json_timestamps = JSON::TS_ISO8601;
redef Kafka::tag_json = T;

## Setup event extension to include sensor and probe name
type Extension: record {
    ## The name of the system that wrote this log. This
    ## is defined in the  const so that
    ## a system running lots of processes can give the
    ## same value for any process that writes a log.
    system:   string &log;
    ## The name of the process that wrote the log. In
    ## clusters, this will typically be the name of the
    ## worker that wrote the log.
    proc:     string &log;
};

function add_log_extension(path: string): Extension
{
    return Extension($system = ROCK::sensor_id,
                     $proc   = peer_description);
}

redef Log::default_ext_func   = add_log_extension;
redef Log::default_ext_prefix = "@";
redef Log::default_scope_sep  = "_";


Fixed a bug with the notice generation.

Fixed a bug with the notice generation.

@load ./bin-list

module CreditCardExposure;

export {
	redef enum Log::ID += { LOG };

	redef enum Notice::Type += {
		Found
	};

	type Info: record {
		## When the SSN was seen.
		ts:   time    &log;
		## Unique ID for the connection.
		uid:  string  &log;
		## Connection details.
		id:   conn_id &log;
		## Credit card number that was discovered.
		cc:   string  &log &optional;
		## Bank Indentification number information
		bank: Bank    &log &optional;
		## Data that was received when the credit card was discovered.
		data: string  &log;
	};

	## Logs are redacted by default.  If you want to see the credit card
	## numbers in the log, redef this value to F.
	## Notices are automatically and unchangeably redacted.
	const redact_log = T &redef;

	## The character used for redaction to replace all numbers.
	const redaction_char = "X" &redef;

	## The number of bytes around the discovered credit card number that is used
	## as a summary in notices.
	const summary_length = 200 &redef;

	const cc_regex = /(^|[^0-9\-])\x00?[3-9](\x00?[0-9]){3}([[:blank:]\-\.]?\x00?[0-9]{4}){3}([^0-9\-]|$)/ &redef;

	const cc_separators = /\.([0-9]*\.){2}/ |
	                      /\-([0-9]*\-){2}/ |
	                      /[:blank:]([0-9]*[:blank:]){2}/ &redef;
}

const luhn_vector = vector(0,2,4,6,8,1,3,5,7,9);
function luhn_check(val: string): bool
	{
	local sum = 0;
	local odd = T;
	for ( char in gsub(val, /[^0-9]/, "") )
		{
		odd = !odd;
		local digit = to_count(char);
		sum += (odd ? digit : luhn_vector[digit]);
		}
	return sum % 10 == 0;
	}

event bro_init() &priority=5
	{
	Log::create_stream(CreditCardExposure::LOG, [$columns=Info]);
	}

function check_cards(c: connection, data: string): bool
	{
	local ccps = find_all(data, cc_regex);

	for ( ccp in ccps )
		{
		# Remove non digit characters from the beginning and end of string.
		ccp = sub(ccp, /^[^0-9]*/, "");
		ccp = sub(ccp, /[^0-9]*$/, "");
		# Remove any null bytes.
		ccp = gsub(ccp, /\x00/, "");
		if ( cc_separators in ccp && luhn_check(ccp) )
			{
			# we've got a match
			local cc_parts = split_all(data, cc_regex);
			for ( i in cc_parts )
				{
				if ( i % 2 == 0 )
					{
					# Redact all matches
					local cc_match = cc_parts[i];
					cc_parts[i] = gsub(cc_parts[i], /[0-9]/, redaction_char);
					}
				}
			local redacted_data = join_string_array("", cc_parts);

			# Trim the data
			local begin = 0;
			local cc_location = strstr(data, ccp);
			if ( cc_location > (summary_length/2) )
				begin = cc_location - (summary_length/2);

			local byte_count = summary_length;
			if ( begin + summary_length > |redacted_data| )
				byte_count = |redacted_data| - begin;

			local trimmed_redacted_data = sub_bytes(redacted_data, begin, byte_count);

			NOTICE([$note=Found,
			        $conn=c,
			        $msg=fmt("Redacted excerpt of disclosed credit card session: %s", trimmed_redacted_data),
			        $identifier=cat(c$id$orig_h,c$id$resp_h)]);

			local log: Info = [$ts=network_time(),
			                   $uid=c$uid, $id=c$id,
			                   $cc=(redact_log ? gsub(ccp, /[0-9]/, redaction_char) : ccp),
			                   $data=(redact_log ? trimmed_redacted_data : sub_bytes(data, begin, byte_count))];

			local bin_number = to_count(sub_bytes(gsub(ccp, /[^0-9]/, ""), 1, 6));
			if ( bin_number in bin_list )
				log$bank = bin_list[bin_number];

			Log::write(CreditCardExposure::LOG, log);
			return T;
			}
		}
	return F;
	}

event http_entity_data(c: connection, is_orig: bool, length: count, data: string)
	{
	if ( c$start_time > network_time()-20secs )
		check_cards(c, data);
	}

event mime_segment_data(c: connection, length: count, data: string)
	{
	if ( c$start_time > network_time()-20secs )
		check_cards(c, data);
	}

# This is used if the signature based technique is in use
function validate_credit_card_match(state: signature_state, data: string): bool
	{
	# TODO: Don't handle HTTP data this way.
	if ( /^GET/ in data )
		return F;

	return check_cards(state$conn, data);
	}

@load ./bin-list

module CreditCardExposure;

export {
	redef enum Log::ID += { LOG };

	redef enum Notice::Type += {
		Found
	};

	type Info: record {
		## When the SSN was seen.
		ts:   time    &log;
		## Unique ID for the connection.
		uid:  string  &log;
		## Connection details.
		id:   conn_id &log;
		## Credit card number that was discovered.
		cc:   string  &log &optional;
		## Bank Indentification number information
		bank: Bank    &log &optional;
		## Data that was received when the credit card was discovered.
		data: string  &log;
	};

	## Logs are redacted by default.  If you want to see the credit card
	## numbers in the log, redef this value to F.
	## Notices are automatically and unchangeably redacted.
	const redact_log = T &redef;

	## The character used for redaction to replace all numbers.
	const redaction_char = "X" &redef;

	## The number of bytes around the discovered credit card number that is used
	## as a summary in notices.
	const summary_length = 200 &redef;

	const cc_regex = /(^|[^0-9\-])\x00?[3-9](\x00?[0-9]){3}([[:blank:]\-\.]?\x00?[0-9]{4}){3}([^0-9\-]|$)/ &redef;

	const cc_separators = /\.([0-9]*\.){2}/ |
	                      /\-([0-9]*\-){2}/ |
	                      /[:blank:]([0-9]*[:blank:]){2}/ &redef;
}

const luhn_vector = vector(0,2,4,6,8,1,3,5,7,9);
function luhn_check(val: string): bool
	{
	local sum = 0;
	local odd = T;
	for ( char in gsub(val, /[^0-9]/, "") )
		{
		odd = !odd;
		local digit = to_count(char);
		sum += (odd ? digit : luhn_vector[digit]);
		}
	return sum % 10 == 0;
	}

event bro_init() &priority=5
	{
	Log::create_stream(CreditCardExposure::LOG, [$columns=Info]);
	}

function check_cards(c: connection, data: string): bool
	{
	local ccps = find_all(data, cc_regex);

	for ( ccp in ccps )
		{
		# Remove non digit characters from the beginning and end of string.
		ccp = sub(ccp, /^[^0-9]*/, "");
		ccp = sub(ccp, /[^0-9]*$/, "");
		# Remove any null bytes.
		ccp = gsub(ccp, /\x00/, "");
		if ( cc_separators in ccp && luhn_check(ccp) )
			{
			# we've got a match
			local cc_parts = split_all(data, cc_regex);
			for ( i in cc_parts )
				{
				if ( i % 2 == 0 )
					{
					# Redact all matches
					local cc_match = cc_parts[i];
					cc_parts[i] = gsub(cc_parts[i], /[0-9]/, redaction_char);
					}
				}
			local redacted_data = join_string_array("", cc_parts);

			# Trim the data
			local begin = 0;
			local cc_location = strstr(data, ccp);
			if ( cc_location > (summary_length/2) )
				begin = cc_location - (summary_length/2);

			local byte_count = summary_length;
			if ( begin + summary_length > |redacted_data| )
				byte_count = |redacted_data| - begin;

			local trimmed_redacted_data = sub_bytes(redacted_data, begin, byte_count);

			NOTICE([$note=Found,
			        $conn=c,
			        $msg=fmt("Redacted excerpt of disclosed credit card session: %s", trimmed_redacted_data),
			        $identity=cat(c$id$orig_h,c$id$resp_h)]);

			local log: Info = [$ts=network_time(),
			                   $uid=c$uid, $id=c$id,
			                   $cc=(redact_log ? gsub(ccp, /[0-9]/, redaction_char) : ccp),
			                   $data=(redact_log ? trimmed_redacted_data : sub_bytes(data, begin, byte_count))];

			local bin_number = to_count(sub_bytes(gsub(ccp, /[^0-9]/, ""), 1, 6));
			if ( bin_number in bin_list )
				log$bank = bin_list[bin_number];

			Log::write(CreditCardExposure::LOG, log);
			return T;
			}
		}
	return F;
	}

event http_entity_data(c: connection, is_orig: bool, length: count, data: string)
	{
	if ( c$start_time > network_time()-20secs )
		check_cards(c, data);
	}

event mime_segment_data(c: connection, length: count, data: string)
	{
	if ( c$start_time > network_time()-20secs )
		check_cards(c, data);
	}

# This is used if the signature based technique is in use
function validate_credit_card_match(state: signature_state, data: string): bool
	{
	# TODO: Don't handle HTTP data this way.
	if ( /^GET/ in data )
		return F;

	return check_cards(state$conn, data);
	}


The problem of header printing multiple times is showing up again.  Hopefully this fixes it.

module LOG;

export {
	# The record type to store logging information.
	type log_info: record {
		dh:           Directions_and_Hosts &default=All;
		split:        bool                 &default=F;
		raw_output:   bool                 &default=F;
		header:       string               &default="";
		files:        table[string] of file;
		tags:         set[string];
	};

	# Where the data for knowing how to log is stored.
	const logs: table[string] of log_info &redef;

	# Utility functions
	global get_file_by_addr: function(a: string, ip: addr, force_log: bool): file;
	global get_file_by_id: function(a: string, id: conn_id, force_log: bool): file;
	global print_log_by_addr: function(a: string, ip: addr, force_log: bool, tags: set[string], line: string): count;
	global print_log_by_id: function(a: string, id: conn_id, force_log: bool, tags: set[string], line: string): count;
	global open_log_files: function(a: string, tag: string);
	global create_logs: function(a: string, d: Directions_and_Hosts, split: bool, raw: bool);
	global define_header: function(a: string, h: string);
	global define_tag: function(a: string, tag: string);
	global buffer: function(a: string, value: bool);

	# This is dumb, but it helps avoid needing to duplicate code on the
	# printing side.
	const null_file: file = open_log_file("null");
}

function get_file_by_addr(a: string, ip: addr, force_log: bool): file
	{
	local i = logs[a];
	if ( !force_log && !addr_matches_hosts(ip, i$dh) )
		return LOG::null_file;

	if ( i$split && |local_nets| > 0 )
		{
		if ( is_local_addr(ip) )
			return i$files["split1-log"];
		else
			return i$files["split2-log"];
		}
	else
		{
		return i$files["combined-log"];
		}
	}

function get_file_by_id(a: string, id: conn_id, force_log: bool): file
	{
	local i = logs[a];
	if ( !force_log && !id_matches_directions(id, i$dh) )
		return LOG::null_file;

	if ( i$split && |local_nets| > 0 )
		{
		if ( is_local_addr(id$resp_h) )
			return i$files["split1-log"];
		else
			return i$files["split2-log"];
		}
	else
		{
		return i$files["combined-log"];
		}
	}

function print_log_by_addr(a: string, ip: addr, force_log: bool, tags: set[string], line: string): count
	{
	local log = get_file_by_addr(a, ip, force_log);
	print log, line;

	local lines_printed=0;
	if ( get_file_name(log) != "null.log" ) ++lines_printed;

	local i = logs[a];
	for ( tag in tags )
		{
		local prefix = cat(a,"-",tag,"-");  # http-ext-identified-files
		if ( prefix in logs )
			i = logs[prefix];

		if ( i$split && |local_nets| > 0 )
			{
			if ( is_local_addr(ip) )
				log = i$files["split1-log"];
			else
				log = i$files["split2-log"];
			}
		else
			{
			log = i$files["combined-log"];
			}
		print log, line;
		++lines_printed;
		}
	return lines_printed;
	}

function print_log_by_id(a: string, id: conn_id, force_log: bool, tags: set[string], line: string): count
	{
	local log = get_file_by_id(a, id, force_log);
	print log, line;

	local lines_printed=0;
	if ( get_file_name(log) != "null.log" ) ++lines_printed;

	local i = logs[a];
	for ( tag in tags )
		{
		local prefix = cat(a,"-",tag);  # http-ext-identified-files
		if ( prefix in logs )
			i = logs[prefix];

		if ( i$split && |local_nets| > 0 )
			{
			if ( is_local_addr(id$resp_h) )
				log = i$files["split1-log"];
			else
				log = i$files["split2-log"];
			}
		else
			{
			log = i$files["combined-log"];
			}
		print log, line;
		++lines_printed;
		}
	return lines_printed;
	}

function buffer(a: string, value: bool)
	{
	local i = logs[a];

	for ( f in i$files )
		{
		set_buf(i$files[f], value);
		}
	}

function open_log_files(a: string, tag: string)
	{
	local i = logs[a];

	if ( i$split && |local_nets| == 0 )
		print fmt("WARNING: Output log splitting requested for %s, but no networks defined in local_nets", a);

	local fname=a;
	if ( tag != "" )
		{
		fname = cat(a,"-",tag);
		if ( fname in logs )
			i = logs[fname];
		}

	if ( i$split && |local_nets| > 0 )
		{
		# Find if this log is determined by HOSTS or DIRECTIONS
		if ( i$dh in DIRECTIONS )
			{
			i$files["split1-log"] = open_log_file(cat(fname,"-inbound"));
			i$files["split2-log"] = open_log_file(cat(fname,"-outbound"));
			}
		else
			{
			i$files["split1-log"] = open_log_file(cat(fname,"-localhosts"));
			i$files["split2-log"] = open_log_file(cat(fname,"-remotehosts"));
			}
		}
	else
		{
		i$files["combined-log"] = open_log_file(fname);
		}
	}

function create_logs(a: string, d: Directions_and_Hosts, split: bool, raw: bool)
	{
	local x: table[string] of file = table();
	local y: set[string] = set();
	logs[a] = [$dh=d, $split=split, $raw_output=raw, $files=x, $tags=y];
	}

function define_header(a: string, h: string)
	{
	local i = logs[a];
	i$header = h;
	}

function define_tag(a: string, tag: string)
	{
	if ( a !in logs )
		{
		print fmt("WARNING: log type '%s' must be defined before adding tag '%s'.", a, tag);
		return;
		}
	# TODO: Validate this is being called during bro_init

	local i = logs[a];
	logs[cat(a,"-",tag)] = copy(i);
	add i$tags[tag];
	}

event file_opened(f: file) &priority=10
	{
	# Only do any of this for files opened locally.
	if ( is_remote_event() ) return;

	local filename = get_file_name(f);
	# TODO: make this not depend on the file extension being .log
	local log_type = gsub(filename, /(-(((in|out)bound)|(local|remote)hosts))?\.log$/, "");
	if ( log_type in logs )
		{
		local i = logs[log_type];
		if ( i$raw_output )
			enable_raw_output(f);
		if ( i$header != "" )
			print f, i$header;
		}
	else
		{
		# TODO: This needs to be handled.
		}
	}

# This is a hack.  The null file is used as /dev/null by all
# scripts using the logging framework.  The file needs to be
# closed so that nothing is ever written to it.
# TODO: change this when a better method for not printing
#       is created.
event bro_init()
	{
	close(null_file);
	}

# Open the appropriate log files.
event bro_init() &priority=-10
	{
	for ( lt in logs )
		{
		open_log_files(lt, "");
		# Open up all of the tagged log files if they exist.
		for ( tag in logs[lt]$tags )
			open_log_files(lt, tag);
		}
	}

module LOG;

export {
	# The record type to store logging information.
	type log_info: record {
		dh:           Directions_and_Hosts &default=All;
		split:        bool                 &default=F;
		raw_output:   bool                 &default=F;
		header:       string               &default="";
		files:        table[string] of file;
		tags:         set[string];
	};

	# Where the data for knowing how to log is stored.
	const logs: table[string] of log_info &redef;

	# Utility functions
	global get_file_by_addr: function(a: string, ip: addr, force_log: bool): file;
	global get_file_by_id: function(a: string, id: conn_id, force_log: bool): file;
	global print_log_by_addr: function(a: string, ip: addr, force_log: bool, tags: set[string], line: string): count;
	global print_log_by_id: function(a: string, id: conn_id, force_log: bool, tags: set[string], line: string): count;
	global open_log_files: function(a: string, tag: string);
	global create_logs: function(a: string, d: Directions_and_Hosts, split: bool, raw: bool);
	global define_header: function(a: string, h: string);
	global define_tag: function(a: string, tag: string);
	global buffer: function(a: string, value: bool);

	# This is dumb, but it helps avoid needing to duplicate code on the
	# printing side.
	const null_file: file = open_log_file("null");
}

function get_file_by_addr(a: string, ip: addr, force_log: bool): file
	{
	local i = logs[a];
	if ( !force_log && !addr_matches_hosts(ip, i$dh) )
		return LOG::null_file;

	if ( i$split && |local_nets| > 0 )
		{
		if ( is_local_addr(ip) )
			return i$files["split1-log"];
		else
			return i$files["split2-log"];
		}
	else
		{
		return i$files["combined-log"];
		}
	}

function get_file_by_id(a: string, id: conn_id, force_log: bool): file
	{
	local i = logs[a];
	if ( !force_log && !id_matches_directions(id, i$dh) )
		return LOG::null_file;

	if ( i$split && |local_nets| > 0 )
		{
		if ( is_local_addr(id$resp_h) )
			return i$files["split1-log"];
		else
			return i$files["split2-log"];
		}
	else
		{
		return i$files["combined-log"];
		}
	}

function print_log_by_addr(a: string, ip: addr, force_log: bool, tags: set[string], line: string): count
	{
	local log = get_file_by_addr(a, ip, force_log);
	print log, line;

	local lines_printed=0;
	if ( get_file_name(log) != "null.log" ) ++lines_printed;

	local i = logs[a];
	for ( tag in tags )
		{
		local prefix = cat(a,"-",tag,"-");  # http-ext-identified-files
		if ( prefix in logs )
			i = logs[prefix];

		if ( i$split && |local_nets| > 0 )
			{
			if ( is_local_addr(ip) )
				log = i$files["split1-log"];
			else
				log = i$files["split2-log"];
			}
		else
			{
			log = i$files["combined-log"];
			}
		print log, line;
		++lines_printed;
		}
	return lines_printed;
	}

function print_log_by_id(a: string, id: conn_id, force_log: bool, tags: set[string], line: string): count
	{
	local log = get_file_by_id(a, id, force_log);
	print log, line;

	local lines_printed=0;
	if ( get_file_name(log) != "null.log" ) ++lines_printed;

	local i = logs[a];
	for ( tag in tags )
		{
		local prefix = cat(a,"-",tag);  # http-ext-identified-files
		if ( prefix in logs )
			i = logs[prefix];

		if ( i$split && |local_nets| > 0 )
			{
			if ( is_local_addr(id$resp_h) )
				log = i$files["split1-log"];
			else
				log = i$files["split2-log"];
			}
		else
			{
			log = i$files["combined-log"];
			}
		print log, line;
		++lines_printed;
		}
	return lines_printed;
	}

function buffer(a: string, value: bool)
	{
	local i = logs[a];

	for ( f in i$files )
		{
		set_buf(i$files[f], value);
		}
	}

function open_log_files(a: string, tag: string)
	{
	local i = logs[a];

	if ( i$split && |local_nets| == 0 )
		print fmt("WARNING: Output log splitting requested for %s, but no networks defined in local_nets", a);

	local fname=a;
	if ( tag != "" )
		{
		fname = cat(a,"-",tag);
		if ( fname in logs )
			i = logs[fname];
		}

	if ( i$split && |local_nets| > 0 )
		{
		# Find if this log is determined by HOSTS or DIRECTIONS
		if ( i$dh in DIRECTIONS )
			{
			i$files["split1-log"] = open_log_file(cat(fname,"-inbound"));
			i$files["split2-log"] = open_log_file(cat(fname,"-outbound"));
			}
		else
			{
			i$files["split1-log"] = open_log_file(cat(fname,"-localhosts"));
			i$files["split2-log"] = open_log_file(cat(fname,"-remotehosts"));
			}
		}
	else
		{
		i$files["combined-log"] = open_log_file(fname);
		}
	}

function create_logs(a: string, d: Directions_and_Hosts, split: bool, raw: bool)
	{
	local x: table[string] of file = table();
	local y: set[string] = set();
	logs[a] = [$dh=d, $split=split, $raw_output=raw, $files=x, $tags=y];
	}

function define_header(a: string, h: string)
	{
	local i = logs[a];
	i$header = h;
	}

function define_tag(a: string, tag: string)
	{
	if ( a !in logs )
		{
		print fmt("WARNING: log type '%s' must be defined before adding tag '%s'.", a, tag);
		return;
		}
	# TODO: Validate this is being called during bro_init

	local i = logs[a];
	logs[cat(a,"-",tag)] = copy(i);
	add i$tags[tag];
	}

event file_opened(f: file) &priority=10
	{
	local filename = get_file_name(f);
	# TODO: make this not depend on the file extension being .log
	local log_type = gsub(filename, /(-(((in|out)bound)|(local|remote)hosts))?\.log$/, "");
	if ( log_type in logs )
		{
		local i = logs[log_type];
		if ( i$raw_output )
			enable_raw_output(f);
		if ( !is_remote_event() && i$header != "" )
			print f, i$header;
		}
	else
		{
		# TODO: This needs to be handled.
		}
	}

# This is a hack.  The null file is used as /dev/null by all
# scripts using the logging framework.  The file needs to be
# closed so that nothing is ever written to it.
# TODO: change this when a better method for not printing
#       is created.
event bro_init()
	{
	close(null_file);
	}

# Open the appropriate log files.
event bro_init() &priority=-10
	{
	for ( lt in logs )
		{
		open_log_files(lt, "");
		# Open up all of the tagged log files if they exist.
		for ( tag in logs[lt]$tags )
			open_log_files(lt, tag);
		}
	}


datatypes for logging

datatypes for logging

module Dio_access;

export {
  redef enum Log::ID += { LOG };

  type Info: record {
    ts: time &log;
    dst_ip: addr &log;
    dst_port: port &log;
    src_hostname: string &log;
    src_ip: addr &log;
    src_port: port &log;
    transport: string &log;
    protocol: string &log;
    connector_id: string &log;
  };
}

event bro_init() &priority=5 {
  Log::create_stream(Dio_access::LOG, [$columns=Info, $path="dionaea_access"]);
}

module Dio_access;

export {
  redef enum Log::ID += { LOG };

  type Info: record {
    ts: time &log;
    dst_ip: time &log;
    dst_port: string &log;
    src_hostname: addr &log;
    src_ip: port &log;
    src_port: addr &log;
    remote_port: port &log;
    transport: string &log;
    protocol: string &log;
    connector_id: string &log;
  };
}

event bro_init() &priority=5 {
  Log::create_stream(Dio_access::LOG, [$columns=Info, $path="dionaea_access"]);
}


count the total number of recipients rather than just one per email

@load global-ext
@load smtp-ext

module PHISH;

global smtp_password_conns: set[conn_id] &read_expire=2mins;

export {
    redef enum Notice += {
        SMTP_PossiblePWPhish,
        SMTP_PossiblePWPhishReply,
    };
    global phishing_counter: table[string] of count &default=0 &create_expire=1hr &synchronized;
    global phishing_reply_tos: set[string] &synchronized &redef;
    global phishing_ignore_froms: set[string] &redef;
    global phishing_threshold = 50;

    const phish_keywords =
          /[pP][aA][sS][sS][wW][oO][rR][dD]/
        | /[uU][sS][eE][rR].?[nN][aA][mM][eE]/
        | /[nN][eE][tT][iI][dD]/ &redef;
}

event bro_init()
{
    LOG::create_logs("password-mail", All, F, T);
    LOG::define_header("password-mail", cat_sep("\t", "",
                                          "ts",
                                          "orig_h", "orig_p",
                                          "resp_h", "resp_p",
                                          "helo", "message-id", "in-reply-to",
                                          "mailfrom", "rcptto",
                                          "date", "from", "reply_to", "to", "subject",
                                          "files", "last_reply", "x-originating-ip",
                                          "path", "is_webmail", "agent"));
}

event bro_done()
{
    print "Counter";
    print phishing_counter;
    print "bad reply-tos";
    print phishing_reply_tos;
}

event smtp_data(c: connection, is_orig: bool, data: string)
{
    if(is_local_addr(c$id$orig_h))
        return;
    # look for 'password'
    if(phish_keywords in data)
        add smtp_password_conns[c$id];
}

event smtp_ext(id: conn_id, si: smtp_ext_session_info)
{
    if(is_local_addr(id$orig_h)) {
        for (to in si$rcptto){
            if(to in phishing_reply_tos){
                NOTICE([$note=SMTP_PossiblePWPhishReply,
                        $msg=fmt("%s replied to %s - %s", si$mailfrom, to, si$subject),
                        $sub=si$mailfrom
                      ]);
            }
        }
    } else {
        if (id !in smtp_password_conns)
            return;
        if(si$mailfrom in phishing_ignore_froms)
            return;
        phishing_counter[si$mailfrom] += |si$rcptto|;
        if(phishing_counter[si$mailfrom] > phishing_threshold){
            local to_add ="";
            if(si$reply_to != "")
                to_add = si$reply_to;
            else
                to_add = si$mailfrom;
            if(to_add !in phishing_reply_tos){
                add phishing_reply_tos[to_add];
                NOTICE([$note=SMTP_PossiblePWPhish,
                        $msg=fmt("%s(%s) may be phishing - %s", si$mailfrom, si$reply_to, si$subject),
                        $sub=si$mailfrom
                      ]);
            }
        }

        local log = LOG::get_file_by_id("password-mail", id, F);
        print log, cat_sep("\t", "\\N",
                    network_time(),
                    id$orig_h, port_to_count(id$orig_p), id$resp_h, port_to_count(id$resp_p),
                    si$helo,
                    si$msg_id,
                    si$in_reply_to,
                    si$mailfrom,
                    fmt_str_set(si$rcptto, /["'<>]|([[:blank:]].*$)/),
                    si$date,
                    si$from,
                    si$reply_to,
                    fmt_str_set(si$to, /["']/),
                    si$subject,
                    fmt_str_set(si$files, /["']/),
                    si$last_reply,
                    si$x_originating_ip,
                    si$path,
                    si$is_webmail,
                    si$agent);
    }

}

@load global-ext
@load smtp-ext

module PHISH;

global smtp_password_conns: set[conn_id] &read_expire=2mins;

export {
    redef enum Notice += {
        SMTP_PossiblePWPhish,
        SMTP_PossiblePWPhishReply,
    };
    global phishing_counter: table[string] of count &default=0 &create_expire=1hr &synchronized;
    global phishing_reply_tos: set[string] &synchronized &redef;
    global phishing_ignore_froms: set[string] &redef;
    global phishing_threshold = 50;

    const phish_keywords =
          /[pP][aA][sS][sS][wW][oO][rR][dD]/
        | /[uU][sS][eE][rR].?[nN][aA][mM][eE]/
        | /[nN][eE][tT][iI][dD]/ &redef;
}

event bro_init()
{
    LOG::create_logs("password-mail", All, F, T);
    LOG::define_header("password-mail", cat_sep("\t", "",
                                          "ts",
                                          "orig_h", "orig_p",
                                          "resp_h", "resp_p",
                                          "helo", "message-id", "in-reply-to",
                                          "mailfrom", "rcptto",
                                          "date", "from", "reply_to", "to", "subject",
                                          "files", "last_reply", "x-originating-ip",
                                          "path", "is_webmail", "agent"));
}

event bro_done()
{
    print "Counter";
    print phishing_counter;
    print "bad reply-tos";
    print phishing_reply_tos;
}

event smtp_data(c: connection, is_orig: bool, data: string)
{
    if(is_local_addr(c$id$orig_h))
        return;
    # look for 'password'
    if(phish_keywords in data)
        add smtp_password_conns[c$id];
}

event smtp_ext(id: conn_id, si: smtp_ext_session_info)
{
    if(is_local_addr(id$orig_h)) {
        for (to in si$rcptto){
            if(to in phishing_reply_tos){
                NOTICE([$note=SMTP_PossiblePWPhishReply,
                        $msg=fmt("%s replied to %s - %s", si$mailfrom, to, si$subject),
                        $sub=si$mailfrom
                      ]);
            }
        }
    } else {
        if (id !in smtp_password_conns)
            return;
        if(si$mailfrom in phishing_ignore_froms)
            return;
        if(++(phishing_counter[si$mailfrom]) > phishing_threshold){
            local to_add ="";
            if(si$reply_to != "")
                to_add = si$reply_to;
            else
                to_add = si$mailfrom;
            if(to_add !in phishing_reply_tos){
                add phishing_reply_tos[to_add];
                NOTICE([$note=SMTP_PossiblePWPhish,
                        $msg=fmt("%s(%s) may be phishing - %s", si$mailfrom, si$reply_to, si$subject),
                        $sub=si$mailfrom
                      ]);
            }
        }

        local log = LOG::get_file_by_id("password-mail", id, F);
        print log, cat_sep("\t", "\\N",
                    network_time(),
                    id$orig_h, port_to_count(id$orig_p), id$resp_h, port_to_count(id$resp_p),
                    si$helo,
                    si$msg_id,
                    si$in_reply_to,
                    si$mailfrom,
                    fmt_str_set(si$rcptto, /["'<>]|([[:blank:]].*$)/),
                    si$date,
                    si$from,
                    si$reply_to,
                    fmt_str_set(si$to, /["']/),
                    si$subject,
                    fmt_str_set(si$files, /["']/),
                    si$last_reply,
                    si$x_originating_ip,
                    si$path,
                    si$is_webmail,
                    si$agent);
    }

}


Delete listen-clear.bro

Delete listen-clear.bro

# $Id: listen-clear.bro 416 2004-09-17 03:52:28Z vern $
#
# Listen for other Bros (non-SSL).

@load remote

# On which port to listen.
const listen_port_clear = Remote::default_port_clear &redef;

# On which IP to bind (0.0.0.0 for any interface)
const listen_if_clear = 0.0.0.0 &redef;

event bro_init()
	{
	listen(listen_if_clear, listen_port_clear, F);
	}


Adding test for failure to reuse the same event for two on-clauses.

#
# @TEST-EXEC: bro -r ${TRACES}/ssh-single-conn.trace ./ssh-cond.evt %INPUT >output
# @TEST-EXEC: btest-diff output
#
# @TEST-KNOWN-FAILURE: The Bro interface can't handle two "on .." clauses
#                      raising the same event.

event ssh::banner(i: int, software: string)
	{
	print i, software;
	}

# @TEST-START-FILE ssh-cond.evt

grammar ssh.pac2;

protocol analyzer pac2::SSH over TCP:
    parse with SSH::Banner,
    port 22/tcp,
    replaces SSH;

on SSH::Banner -> event ssh::banner(1, self.software);
on SSH::Banner -> event ssh::banner(2, self.software);

# @TEST-END-FILE

error: pathspec 'bro/tests/pac2/double-event.bro' did not match any file(s) known to git


Rebalance for new connections:

Rebalance for new connections:

store registered connectors, regardless of available slaves. Rebalance
whenever new slaves come in. Auto-balance (no rebalance required) when
new connectors come in.

@load ./dio_log.bro
@load ./bro_log.bro
@load ./dio_mysql_log.bro
const broker_port: port = 9999/tcp &redef;
redef exit_only_after_terminate = T;
redef Broker::endpoint_name = "bro_receiver";
global dionaea_access: event(timestamp: time, dst_ip: addr, dst_port: count, src_hostname: string, src_ip: addr, src_port: count, transport: string, protocol: string, connector_id: string);
global dionaea_mysql: event(timestamp: time, id: string, local_ip: addr, local_port: count, remote_ip: addr, remote_port: count, transport: string, args: string, connector_id: string);
global get_protocol: function(proto_str: string) : transport_proto;
global log_bro: function(msg: string);
global slaves: table[string] of count;
global connectors: opaque of Broker::Handle;
global add_to_balance: function(peer_name: string);
global remove_from_balance: function(peer_name: string);

event bro_init() {
    log_bro("bro_receiver.bro: bro_init()");
    Broker::enable([$auto_publish=T]);

    Broker::listen(broker_port, "0.0.0.0");

    Broker::subscribe_to_events("bro/forwarder");

    ## create a distributed datastore for the connector to link against:
    connectors = Broker::create_master("connectors");

    log_bro("bro_receiver.bro: bro_init() done");
}
event bro_done() {
    log_bro("bro_receiver.bro: bro_done()");
}
event dionaea_access(timestamp: time, dst_ip: addr, dst_port: count, src_hostname: string, src_ip: addr, src_port: count, transport: string, protocol: string, connector_id: string) {
    local sport: port = count_to_port(src_port, get_protocol(transport));
    local dport: port = count_to_port(dst_port, get_protocol(transport));
    print fmt("dionaea_access: timestamp=%s, dst_ip=%s, dst_port=%s, src_hostname=%s, src_ip=%s, src_port=%s, transport=%s, protocol=%s, connector_id=%s", timestamp, dst_ip, dst_port, src_hostname, src_ip, src_port, transport, protocol, connector_id);
    local rec: Dio_access::Info = [$ts=timestamp, $dst_ip=dst_ip, $dst_port=dport, $src_hostname=src_hostname, $src_ip=src_ip, $src_port=sport, $transport=transport, $protocol=protocol, $connector_id=connector_id];

    Log::write(Dio_access::LOG, rec);
}
event dionaea_mysql(timestamp: time, id: string, local_ip: addr, local_port: count, remote_ip: addr, remote_port: count, transport: string, args: string, connector_id: string) {
    local lport: port = count_to_port(local_port, get_protocol(transport));
    local rport: port = count_to_port(remote_port, get_protocol(transport));
    print fmt("dionaea_mysql: timestamp=%s, id=%s, local_ip=%s, local_port=%s, remote_ip=%s, remote_port=%s, transport=%s, args=%s, connector_id=%s", timestamp, id, local_ip, local_port, remote_ip, remote_port, transport, args, connector_id);
    local rec: Dio_mysql::Info = [$ts=timestamp, $id=id, $local_ip=local_ip, $local_port=lport, $remote_ip=remote_ip, $remote_port=rport, $transport=transport, $args=args, $connector_id=connector_id];
    Log::write(Dio_mysql::LOG, rec);
}
event Broker::incoming_connection_established(peer_name: string) {
    log_bro("Incoming connection extablished " + peer_name);
    add_to_balance(peer_name);
}
event Broker::incoming_connection_broken(peer_name: string) {
    log_bro("Incoming connection broken for " + peer_name);
    remove_from_balance(peer_name);
}
function get_protocol(proto_str: string) : transport_proto {
    # https://www.bro.org/sphinx/scripts/base/init-bare.bro.html#type-transport_proto
    if (proto_str == "tcp") {
        return tcp;
    }
    if (proto_str == "udp") {
        return udp;
    }
    if (proto_str == "icmp") {
        return icmp;
    }
    return unknown_transport;
}

function log_bro(msg:string) {
    local rec: Brolog::Info = [$msg=msg];
    Log::write(Brolog::LOG, rec);
}

function add_to_balance(peer_name: string) {
    if(/bro-slave-/ in peer_name) {
        print "Registering new slave ", peer_name;
        log_bro("Registering new slave " + peer_name);
        slaves[peer_name] = 0;
        local total_slaves = |slaves|;
        local slave_vector = vector();
        local i = 0;
        for (slave in slaves) {
            slave_vector[i] = slave;
            ++i;
        }
        i = 0;
        when (local keys = Broker::keys(connectors)) {
            local connector_vector = vector();
            local total_connectors = Broker::vector_size(keys$result);
            while (i < total_connectors) {
                connector_vector[i] = Broker::vector_lookup(keys$result, i);
                ++i;
            }

            while (total_slaves > 0 && total_connectors > 0) {
                local balance_amount = total_connectors / total_slaves;
                if (total_connectors % total_slaves > 0) {
                    balance_amount = total_connectors / total_slaves + 1;
                }
                --total_slaves;
                local balanced_to = slave_vector[total_slaves];
                slaves[balanced_to] = balance_amount;
                local balance_index = total_connectors - balance_amount; # do this once, do this here!
                while (total_connectors > balance_index) {
                    --total_connectors;
                    local rebalanced_conn = Broker::refine_to_string(connector_vector[total_connectors]);
                    Broker::insert(connectors, Broker::data(rebalanced_conn), Broker::data(balanced_to));
                    print "Rebalanced connector", rebalanced_conn, "to slave", balanced_to;
                    #log_bro("Rebalanced connector " + rebalanced_conn + " to slave " + balanced_to);
                }
            }
        }
        timeout 100msec {
            log_bro("ERROR: Unable to query keys in 'connectors-data-store' within 100ms, timeout");
        }

    }
    if(/beemaster-connector-/ in peer_name) {
        print "Registering new connector ", peer_name;
        log_bro("Registering new connector " + peer_name);
        local min_count_conn = 100000;
        local best_slave = "";

        for(slave in slaves) {
            local count_conn = slaves[slave];
            if (count_conn < min_count_conn) {
                best_slave = slave;
                min_count_conn = count_conn;
            }
        }
        # add the connector, regardless if any slave is ready. Thus, at least a reference is stored, that will get rebalanced once a new slave registers.
        Broker::insert(connectors, Broker::data(peer_name), Broker::data(best_slave));
        if (best_slave != "") {
            ++slaves[best_slave];
            print "Registered connector", peer_name, "and balanced to", best_slave;
            log_bro("Registered connector " + peer_name + " and balanced to " + best_slave);
        }
        else {
            print "Could not register connector", peer_name;
            log_bro("Could not register connector " + peer_name);
        }

    }
}

function remove_from_balance(peer_name: string) {
    if(/bro-slave-/ in peer_name) {
        log_bro("Unregistering old slave " + peer_name);
        delete slaves[peer_name];
        # TODO rebalance
    }
    if(/beemaster-connector-/ in peer_name) {
        when(local cs = Broker::lookup(connectors, Broker::data(peer_name))) {
            local connected_slave = Broker::refine_to_string(cs$result);
            Broker::erase(connectors, Broker::data(peer_name));
            if (connected_slave == "") {
                # connector was registered, but no slave was there to handle it. If it now goes away, OK!
                print "Unregistered old connector", peer_name, "no connected slave found";
                log_bro("Unregistered old connector " + peer_name + " no connected slave found");
                return;
            }
            local count_conn = slaves[connected_slave];
            if (count_conn > 0) {
                slaves[connected_slave] = count_conn - 1;
                print "Unregistered old connector", peer_name, "from slave", connected_slave;
                log_bro("Unregistered old connector " + peer_name + " from slave " + connected_slave);
            }
        }
        timeout 100msec {
            print "Timeout unregistering connector", peer_name;
            log_bro("Timeout unregistering connector " + peer_name);
        }
    }
}

@load ./dio_log.bro
@load ./bro_log.bro
@load ./dio_mysql_log.bro
const broker_port: port = 9999/tcp &redef;
redef exit_only_after_terminate = T;
redef Broker::endpoint_name = "bro_receiver";
global dionaea_access: event(timestamp: time, dst_ip: addr, dst_port: count, src_hostname: string, src_ip: addr, src_port: count, transport: string, protocol: string, connector_id: string);
global dionaea_mysql: event(timestamp: time, id: string, local_ip: addr, local_port: count, remote_ip: addr, remote_port: count, transport: string, args: string, connector_id: string);
global get_protocol: function(proto_str: string) : transport_proto;
global log_bro: function(msg: string);
global slaves: table[string] of count;
global connectors: opaque of Broker::Handle;
global add_to_balance: function(peer_name: string);
global remove_from_balance: function(peer_name: string);

event bro_init() {
    log_bro("bro_receiver.bro: bro_init()");
    Broker::enable([$auto_publish=T]);

    Broker::listen(broker_port, "0.0.0.0");

    Broker::subscribe_to_events("bro/forwarder");

    ## create a distributed datastore for the connector to link against:
    connectors = Broker::create_master("connectors");

    log_bro("bro_receiver.bro: bro_init() done");
}
event bro_done() {
    log_bro("bro_receiver.bro: bro_done()");
}
event dionaea_access(timestamp: time, dst_ip: addr, dst_port: count, src_hostname: string, src_ip: addr, src_port: count, transport: string, protocol: string, connector_id: string) {
    log_bro("Bro Master log access event!");
    local sport: port = count_to_port(src_port, get_protocol(transport));
    local dport: port = count_to_port(dst_port, get_protocol(transport));
    print fmt("dionaea_access: timestamp=%s, dst_ip=%s, dst_port=%s, src_hostname=%s, src_ip=%s, src_port=%s, transport=%s, protocol=%s, connector_id=%s", timestamp, dst_ip, dst_port, src_hostname, src_ip, src_port, transport, protocol, connector_id);
    local rec: Dio_access::Info = [$ts=timestamp, $dst_ip=dst_ip, $dst_port=dport, $src_hostname=src_hostname, $src_ip=src_ip, $src_port=sport, $transport=transport, $protocol=protocol, $connector_id=connector_id];

    Log::write(Dio_access::LOG, rec);
}
event dionaea_mysql(timestamp: time, id: string, local_ip: addr, local_port: count, remote_ip: addr, remote_port: count, transport: string, args: string, connector_id: string) {
    log_bro("Bro Master log mysql event!");
    local lport: port = count_to_port(local_port, get_protocol(transport));
    local rport: port = count_to_port(remote_port, get_protocol(transport));
    print fmt("dionaea_mysql: timestamp=%s, id=%s, local_ip=%s, local_port=%s, remote_ip=%s, remote_port=%s, transport=%s, args=%s, connector_id=%s", timestamp, id, local_ip, local_port, remote_ip, remote_port, transport, args, connector_id);
    local rec: Dio_mysql::Info = [$ts=timestamp, $id=id, $local_ip=local_ip, $local_port=lport, $remote_ip=remote_ip, $remote_port=rport, $transport=transport, $args=args, $connector_id=connector_id];
    Log::write(Dio_mysql::LOG, rec);
}
event Broker::incoming_connection_established(peer_name: string) {
    local inc: string = "Incoming connection extablished " + peer_name;
    log_bro(inc);
    add_to_balance(peer_name);
}
event Broker::incoming_connection_broken(peer_name: string) {
    local inc: string = "Incoming connection broken for " + peer_name;
    log_bro(inc);
    remove_from_balance(peer_name);
}
function get_protocol(proto_str: string) : transport_proto {
    # https://www.bro.org/sphinx/scripts/base/init-bare.bro.html#type-transport_proto
    if (proto_str == "tcp") {
        return tcp;
    }
    if (proto_str == "udp") {
        return udp;
    }
    if (proto_str == "icmp") {
        return icmp;
    }
    return unknown_transport;
}

function log_bro(msg:string) {
    local rec: Brolog::Info = [$msg=msg];
    Log::write(Brolog::LOG, rec);
}

function add_to_balance(peer_name: string) {
    if(/bro-slave-/ in peer_name) {
        log_bro("Registering new slave " + peer_name);
        slaves[peer_name] = 0;
        local total_slaves = |slaves|;
        local total_connectors = 0;
        local slave_table = vector of string;
        local i = 0;
        for (slave in slaves) {
            total_connectors += slaves[slave];
            slave_table[i] = slave;
            ++i;
        }
        i = 0;
        when (local keys = Broker::keys(connectors)) {
            local connector_table = vector of string;
            while (i < total_connectors) {
                connector_table[i] = Broker::vector_lookup(keys$result, i);
            }

            print "Starting add slave to balance:";
            print "total_slaves", total_slaves, "total_connectors", total_connectors;
            while (total_slaves > 0) {
                local balance_amount = balance_amount = total_connectors;
                if (total_connectors % total_slaves > 0) {
                    balance_amount = total_connectors / total_slaves + 1;
                }
                --total_slaves;
                total_connectors -= balance_amount;
                print "total_slaves", total_slaves, "total_connectors", total_connectors;
                # TODO: set balanceamount as value for one slave
                # TODO: set for all those connectors that one slave
            }

        }
        timeout 100msec {
            print "aww";
        }

    }
    if(/beemaster-connector-/ in peer_name) {
        log_bro("Registering new connector " + peer_name);
        local min_count_conn = 100000;
        local best_slave = "";

        local size = |slaves|;

        if (size <= 0) {
            log_bro("No slaves are registered, cannot register connector " + peer_name);
            print "should return";
            return;
        }
        for(slave in slaves) {
            local count_conn = slaves[slave];
            if (count_conn < min_count_conn) {
                best_slave = slave;
                min_count_conn = count_conn;
            }
        }
        if (best_slave != "") {
            Broker::insert(connectors, Broker::data(peer_name), Broker::data(best_slave));
            ++slaves[best_slave];
            print "Registered connector", peer_name, "and balanced to", best_slave;
            log_bro("Registered connector " + peer_name + " and balanced to " + best_slave);
        }
        else {
            print "Could not register connector ", peer_name;
            log_bro("Could not register connector " + peer_name);
        }

    }
}

function remove_from_balance(peer_name: string) {
    if(/bro-slave-/ in peer_name) {
        log_bro("Unregistering old slave " + peer_name);
        delete slaves[peer_name];
        # TODO rebalance
    }
    if(/beemaster-connector-/ in peer_name) {
        when(local cs = Broker::lookup(connectors, Broker::data(peer_name))) {
            local connected_slave = Broker::refine_to_string(cs$result);
            local count_conn = slaves[connected_slave];
            if (count_conn > 0) {
                slaves[connected_slave] = count_conn - 1;
            }
            Broker::erase(connectors, Broker::data(peer_name));
            print "Unregistered old connector", peer_name, "leaving slave table", slaves;
            log_bro("Unregistered old connector " + peer_name);
        }
        timeout 100msec {
            print "Timeout unregistering connector", peer_name;
            log_bro("Timeout unregistering connector " + peer_name);
        }
    }
}


Now counts rejects during the MAIL FROM command. More rewording and slight reworking.

# For the SMTP_StrangeRejectBehavior notice 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 smtp

module SMTP;

type smtp_counter: record {
	rejects: count &default=0;
	total: count &default=0;
};

export {
	# The idea for this is that if a host makes more than reject_threshold
	# smtp connections per hour of which at least reject_percent of those are
	# rejected and the host is not a known mail sending host, then it's likely
	# sending spam or viruses.
	#
	const reject_threshold = 100 &redef;
	const reject_percent = 30 &redef;

	# These are smtp status codes that are considered "rejected".
	const bad_address_reject_codes: set[count] = {
		501, # Bad sender address syntax
		550, # Requested action not taken: mailbox unavailable
		551, # User not local; please try <forward-path>
		553, # Requested action not taken: mailbox name not allowed
		550, # Rejected
	};

	redef enum Notice += {
		SMTP_PossibleSpam, # Host sending mail *to* internal hosts is suspicious
		SMTP_StrangeRejectBehavior, # Local mail server is getting high numbers of rejects
	};

	# This variable keeps track of the number of rejected and accepted
	# RCPT TO's a host has per hour.
	global reject_counter: table[addr] of smtp_counter &create_expire=1hr &redef;

	# Reduce the volume of notices raised by filtering out host that have
	# already been detected as having too many rejected RCPT TOs.
	global notified_reject_spammers: set[addr] &create_expire=1hr &redef;
}

event smtp_reply(c: connection, is_orig: bool, code: count, cmd: string,
                 msg: string, cont_resp: bool)
	{
	# If this is a continued response, it could be something like
	# the multiline rejections that gmail gives.  We only want to count
	# the first rejection in that case.
	if ( cont_resp ) return;

	if ( c$id$orig_h !in reject_counter )
		{
		local t: smtp_counter;
		reject_counter[c$id$orig_h] = t;
		}
	# Set the smtp_counter to the local var "sc"
	local sc = reject_counter[c$id$orig_h];

	# Whenever a "RCPT TO" is done, we add that to the total.
	if ( /^([rR][cC][pP][tT]|[mM][aA][iI][lL])/ in cmd )
		{
		++sc$total;
		if ( code in bad_address_reject_codes )
			++sc$rejects;

		if ( sc$total >= reject_threshold )
			{
			local percent = (sc$rejects*100) / sc$total;
			local host = c$id$orig_h;
			if ( percent >= reject_percent &&
				 host !in notified_reject_spammers )
				{
				local notice_type = SMTP_PossibleSpam;
@ifdef ( local_mail )
				if ( host in local_mail )
					notice_type = SMTP_StrangeRejectBehavior;
@endif
				NOTICE([$note=notice_type,
				        $msg=fmt("%s is having a large number of attempted recipients rejected", host),
				        $sub=fmt("attempted: %d rejected: %d percent",
				        sc$total, percent),
				        $conn=c]);

				add notified_reject_spammers[host];
				}
			}
		}
	}

# For the SMTP_StrangeRejectBehavior notice 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 smtp

module SMTP;

type smtp_counter: record {
	rejects: count &default=0;
	total: count &default=0;
};

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;

	# These are smtp status codes that are considered "rejected".
	const bad_address_reject_codes: set[count] = {
		501, # Bad sender address syntax
		550, # Requested action not taken: mailbox unavailable
		551, # User not local; please try <forward-path>
		553, # Requested action not taken: mailbox name not allowed
	};

	redef enum Notice += {
		SMTP_PossibleSpam, # Host sending mail *to* internal hosts is suspicious
		SMTP_StrangeRejectBehavior, # Local mail server is getting high numbers of rejects
	};

	# This variable keeps track of the number of rejected and accepted
	# RCPT TO's a host has per hour.
	global reject_counter: table[addr] of smtp_counter &create_expire=1hr &redef;

	# Reduce the volume of notices raised by filtering out host that have
	# already been detected as having too many rejected RCPT TOs.
	global notified_reject_spammers: set[addr] &create_expire=1hr &redef;
}

event smtp_reply(c: connection, is_orig: bool, code: count, cmd: string,
                 msg: string, cont_resp: bool)
	{
	# If this is a continued response, it could be something like
	# the multiline rejections that gmail gives.  We only want to count
	# the first rejection in that case.
	if ( cont_resp ) return;

	if ( c$id$orig_h !in reject_counter )
		{
		local t: smtp_counter;
		reject_counter[c$id$orig_h] = t;
		}
	# Set the smtp_counter to the local var "sc"
	local sc = reject_counter[c$id$orig_h];

	# Whenever a "RCPT TO" is done, we add that to the total.
	if ( /^[rR][cC][pP][tT]/ in cmd )
		{
		++sc$total;
		if ( code in bad_address_reject_codes )
			++sc$rejects;
		}

	if ( sc$total >= spam_threshold )
		{
		local percent = (sc$rejects*100) / sc$total;
		local host = c$id$orig_h;
		if ( percent >= spam_percent &&
			 host !in notified_reject_spammers )
			{
			local notice_type = SMTP_PossibleSpam;
@ifdef ( local_mail )
			if ( host in local_mail )
				notice_type = SMTP_StrangeRejectBehavior;
@endif
			NOTICE([$note=notice_type,
			        $msg=fmt("%s is having a large number of attempted recipients rejected", host),
			        $sub=fmt("attempted: %d rejected: %d percent",
			        sc$total, percent),
			        $conn=c]);

			add notified_reject_spammers[host];
			}
		}
	}


only keep track of individual connections for 1 minute.

A machine spamming wouldn't have the same connection open for very long.
Even if it does, if we see more rejects after 1 minute, it doesn't hurt to
count them.  This would esentially count '1 rejection per connection per
minute', instead of 'at most 1 rejection per connection'

# 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 };

@ifdef ( local_mail )

@load conn-id
@load smtp

module SMTP;

type smtp_counter: record {
 rejects: count;
 total: count;
 connects: set[conn_id];
 rej_conns: set[conn_id];
};

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;

 # These are smtp status codes that are considered "rejected".
 const smtp_reject_codes: set[count] = {
   501, # Bad sender address syntax
   550, # Requested action not taken: mailbox unavailable
   551, # User not local; please try <forward-path>
   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
 };

 global smtp_status_comparison: table[addr] of smtp_counter &create_expire=1hr &redef;
}

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] &create_expire=1m;
   local blarg: set[conn_id] &create_expire=1m;
   smtp_status_comparison[c$id$orig_h] = [$rejects=0, $total=0, $connects=bar, $rej_conns=blarg];
 }

 # 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];
   add foo$rej_conns[c$id];
 }

 if ( c$id !in foo$connects )
 {
   ++foo$total;
   add foo$connects[c$id];
 }

 local host = c$id$orig_h;
 if ( foo$total >= spam_threshold ) {
   local percent = (foo$rejects*100) / foo$total;
   if ( percent >= spam_percent ) {
     if ( host in local_mail )
       NOTICE([$note=SMTP_StrangeRejectBehavior, $msg=fmt("a high percentage of mail from %s is being rejected", host), $sub=fmt("sent: %d rejected: %d percent", foo$total, percent), $conn=c]);
     else if ( is_local_addr(host) )
       NOTICE([$note=SMTP_PossibleInternalSpam, $msg=fmt("%s appears to be spamming", host), $sub=fmt("sent: %d rejected: %d percent", foo$total, percent), $conn=c]);
     else
       NOTICE([$note=SMTP_PossibleSpam, $msg=fmt("%s appears to be spamming", host), $sub=fmt("sent: %d rejected: %d percent", foo$total, percent), $conn=c]);
   }
 }
}

@endif

# 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 };

@ifdef ( local_mail )

@load conn-id
@load smtp

module SMTP;

type smtp_counter: record {
 rejects: count;
 total: count;
 connects: set[conn_id];
 rej_conns: set[conn_id];
};

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;

 # These are smtp status codes that are considered "rejected".
 const smtp_reject_codes: set[count] = {
   501, # Bad sender address syntax
   550, # Requested action not taken: mailbox unavailable
   551, # User not local; please try <forward-path>
   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
 };

 global smtp_status_comparison: table[addr] of smtp_counter &create_expire=1hr &redef;
}

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];
 }

 # 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];
   add foo$rej_conns[c$id];
 }

 if ( c$id !in foo$connects )
 {
   ++foo$total;
   add foo$connects[c$id];
 }

 local host = c$id$orig_h;
 if ( foo$total >= spam_threshold ) {
   local percent = (foo$rejects*100) / foo$total;
   if ( percent >= spam_percent ) {
     if ( host in local_mail )
       NOTICE([$note=SMTP_StrangeRejectBehavior, $msg=fmt("a high percentage of mail from %s is being rejected", host), $sub=fmt("sent: %d rejected: %d percent", foo$total, percent), $conn=c]);
     else if ( is_local_addr(host) )
       NOTICE([$note=SMTP_PossibleInternalSpam, $msg=fmt("%s appears to be spamming", host), $sub=fmt("sent: %d rejected: %d percent", foo$total, percent), $conn=c]);
     else
       NOTICE([$note=SMTP_PossibleSpam, $msg=fmt("%s appears to be spamming", host), $sub=fmt("sent: %d rejected: %d percent", foo$total, percent), $conn=c]);
   }
 }
}

@endif


Fixed path resolution.

Fixed path resolution.

# Copyright (C) 2016, Missouri Cyber Team
# All Rights Reserved
# See the file "LICENSE" in the main distribution directory for details

# Load integration with Snort on ROCK
@load ./frameworks/files/unified2-integration

# Copyright (C) 2016, Missouri Cyber Team
# All Rights Reserved
# See the file "LICENSE" in the main distribution directory for details

# Load integration with Snort on ROCK
@load frameworks/files/unified2-integration