##! FTP brute-forcing detector, triggering when too many rejected usernames or
##! failed passwords have occurred from a single address.

@load base/protocols/ftp
@load base/frameworks/sumstats

@load base/utils/time

module FTP;

export {
	redef enum Notice::Type += {
		## Indicates a host bruteforcing FTP logins by watching for too
		## many rejected usernames or failed passwords.
		Bruteforcing
	};

	## How many rejected usernames or passwords are required before being
	## considered to be bruteforcing.
	const bruteforce_threshold: double = 20 &redef;

	## The time period in which the threshold needs to be crossed before
	## being reset.
	const bruteforce_measurement_interval = 15mins &redef;
}


event zeek_init()
	{
	local r1 = SumStats::Reducer($stream="ftp.failed_auth", $apply=set(SumStats::UNIQUE), $unique_max=double_to_count(bruteforce_threshold+2));
	SumStats::create(SumStats::SumStat($name="ftp-detect-bruteforcing",
		$epoch=bruteforce_measurement_interval,
		$reducers=set(r1),
		$threshold_val(key: SumStats::Key, result: SumStats::Result) =
			{
			return result["ftp.failed_auth"]$num+0.0;
			},
		$threshold=bruteforce_threshold,
		$threshold_crossed(key: SumStats::Key, result: SumStats::Result) =
			{
			local r = result["ftp.failed_auth"];
			local dur = duration_to_mins_secs(r$end-r$begin);
			local plural = r$unique>1 ? "s" : "";
			local message = fmt("%s had %d failed logins on %d FTP server%s in %s", key$host, r$num, r$unique, plural, dur);
			NOTICE(Notice::Info($note=FTP::Bruteforcing,
			                    $src=key$host,
			                    $msg=message,
			                    $identifier=cat(key$host)));
			}));
	}

event ftp_reply(c: connection, code: count, msg: string, cont_resp: bool)
	{
	local cmd = c$ftp$cmdarg$cmd;
	if ( cmd == "USER" || cmd == "PASS" )
		{
		if ( FTP::parse_ftp_reply_code(code)$x == 5 )
			SumStats::observe("ftp.failed_auth", SumStats::Key($host=c$id$orig_h), SumStats::Observation($str=cat(c$id$resp_h)));
		}
	}
