File: //etc/fail2ban/action.d/ndn-fail2ban-central.pl
#!/usr/bin/env perl
#
#Nightmare Labs central fail2ban database action.
#
#Writes and Query central fail2ban database.
#
use strict;
use warnings;
use DBI;
use v5.10;
use Getopt::Long qw(GetOptions);
#Global Variables
my ($sql, $hostname, $jail, $ip, $report, $table);
#MySQL related
my $db = 'failcentral';
my $host = '69.163.136.9';
my $user = 'f2b_user';
my $password = 'nightmarelabs';
my $dsn = "DBI:mysql:database=$db;host=$host";
my $dbh = DBI->connect($dsn, $user, $password);
GetOptions (
"help|h" => sub { _help2() },
"report|r=i" => \$report,
) or _help();
#Usage information
sub _help {
print "Tool to log fail2ban actions to a database.
Usage: ndn-fail2ban-central.pl [--help|-h] [--report| -r <seconds> <cluster>] <cluster> <jail name> <IP address>
Options: --help|-h This help page.
--report|-r <interval in seconds> <cluster>
Examples:
ndn-fail2ban-central.pl fail2ban_loadbalancer ssh-bruteforce 1.2.3.4
ndn-fail2ban-central.pl --report|-r 60 (Shows entries from the last 60 seconds) fail2ban_loadbalancer\n";
exit 0;
}
#Running report
if ($report) {
($table) = @ARGV;
query_fail2ban($report, $table);
exit 0;
}
sub query_fail2ban{
($report, $table) = @_;
my $sql = "SELECT date, hostname, jail, ip from $table where date>DATE_ADD(NOW(), interval -$report SECOND)";
my $sth = $dbh->prepare($sql);
$sth->execute();
while (my @row = $sth->fetchrow_array()){
print "@row\n";
}
$sth->finish();
}
sub _insert {
($table, $hostname, $jail, $ip) = @_;
my $sql = "INSERT INTO $table set hostname='$hostname', jail='$jail',ip='$ip', date=NOW()";
my $fail_data = _get_fail_data();
$sql .= ", fail_data='$fail_data'" if $fail_data;
my $sth = $dbh->prepare($sql);
$sth->execute();
$sth->finish();
}
sub _get_fail_data {
my $users_ref = _process_log();
if (!$users_ref) {
my $second_attempt = 1;
$users_ref = _process_log($second_attempt);
return if !$users_ref;
}
my %users = %$users_ref;
my $fail_data;
# TODO: for jails like wp and 418, we probably want to do something else.. figure that out later.
# user with the highest fail count should be first since the list length is max 15 users.
my @sorted_users = sort { $users{$a} <=> $users{$b} } keys %users;
my $i = 0;
for my $user (reverse @sorted_users) {
$fail_data .= "$user:$users{$user},";
$i++;
last if $i > 14; # cutting it off here else the list would be truncated in the db.
}
return $fail_data;
}
sub _process_log {
my ($second_attempt) = @_;
# TODO: we're always going to try a second attempt for the other jails, like ssh, ftp, etc. add those later.
# TODO: put these regex's somewhere?
my $config = {
'dovecot' => {
log_file => '/var/log/auth.log',
fail_regex => 'dovecot.*fail',
user_regex => 'ruser=(.*) rhost',
},
'postfix-sasl' => {
log_file => '/var/log/auth.log',
fail_regex => 'dovecot.*fail',
user_regex => 'ruser=(.*) rhost',
},
};
my $log = $config->{$jail}->{log_file};
$log .= '.1' if $second_attempt;
my $fail_re = $config->{$jail}->{fail_regex};
my $user_re = $config->{$jail}->{user_regex};
if ($log && $fail_re && $user_re) {
open my $fh, '<', $log || return;
my %users;
while (<$fh>) {
my $line = $_;
chomp($line);
next unless $line =~ /$ip/;
next unless $line =~ /$fail_re/;
my ($user) = $line =~ /$user_re/;
next unless $user;
$users{$user}++;
}
close $fh;
return \%users if keys %users > 0;
}
return;
}
sub main {
($table, $jail, $ip) = @ARGV;
$hostname = `hostname`;
#if required values are missing exit
if ($table and $jail and $ip) {
chomp ($table, $hostname, $jail, $ip);
} else {
print "Valid options required\n";
_help();
}
if ($ip =~ m/^(\d\d?\d?)\.(\d\d?\d?)\.(\d\d?\d?)\.(\d\d?\d?)$/) {
} else {
print "Valid IP address required\n";
_help();
}
_insert($table, $hostname, $jail, $ip);
}
main();