All the files

This commit is contained in:
2024-03-09 15:36:42 +01:00
parent 58e88da2d8
commit 08b6b503a6
22 changed files with 1721 additions and 0 deletions

51
bin/parserfilter Executable file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env perl
use strict;
use warnings;
use diagnostics;
use sigtrap qw/handler signal_handler normal-signals/;
use Tie::Syslog;
use My::Savepid qw(savepid);
use My::parser::parser;
use Glib;
my $config_file = '/usr/local/etc/parserfilter.conf';
my $pidfile = '/var/run/parserfilter.pid';
my $program = $0;
unless(-f $config_file) {
die "Configuration file $config_file does not exist";
}
my $x;
my $loop = Glib::MainLoop->new;
my $parser = My::parser::parser->new($config_file);
my $time = Glib::Timeout->add(1000, \&loopsie);
if(my $tp = fork) {
exit 0;
} else {
my $pid = $$;
$x = tie *STDERR, 'Tie::Syslog', 'local0.err',$program,'pid','unix';
$x->ExtendedSTDERR();
&savepid($pid,$pidfile);
close STDIN;
$parser->load_parsers;
$loop->run;
}
sub loopsie {
my $result = $parser->parse_all;
return 1 if($result);
return 0;
}
sub signal_handler {
my $signal = shift;
if($signal eq 'HUP') {
$parser->{'config'}->{'logger'}->reload_log;
$parser->{'config'}->{'logger'}->log('Log reloaded');
} else {
$loop->quit;
$parser->{'config'}->{'logger'}->log("$program stopped");
}
}

46
bin/parserfilter-tester Executable file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use My::parser::stats;
my $module;
my @modules = ('ssh','dovecot','exim','apache','gitea_ssh');
my $program = $0;
GetOptions ("module=s" => \$module); #Only test one module
unless($module) {
print 'No module specified, use argument --module=(ssh|dovecot|exim|apache|gitea_ssh)'."\n";
exit;
}
my @matches = grep { /$module/ } @modules;
unless(@matches) {
print 'Unsupported module '.$module.' specified, use argument --module=(ssh|dovecot|exim|apache|gitea_ssh)'."\n";
exit;
}
print "Please paste a line to parse here:\n";
my $frompipe = <STDIN>;
chomp($frompipe); #One line is fine for us, user may fuck up, but we're good..
my $parser = &load($module);
my $result = $parser->parser($frompipe);
if($result->{'retval'}) {
print 'Parser said: '.$result->{'retmsg'}."\n";
print 'Regarded as a hostile action'."\n" if($result->{'hostile'});
print 'Host: '.$result->{'host'}."\n";
} else {
print 'Parser found no match'."\n";
}
sub load {
my $parser = shift;
my $filename = 'My/parser/'.$parser.'_parser.pm';
my $newclass;
eval {
require $filename;
my $classname = 'My::parser::'.$parser.'_parser';
$newclass = $classname->new || die "Failed to load parser for $parser";
} or do {
my $e = $@;
print 'Could not load parser '.$module.' from file '.$filename.': '.$e."\n";
exit;
};
return $newclass;
}

65
lib/addtolist.pm Normal file
View File

@ -0,0 +1,65 @@
package My::parser::addtolist;
use strict;
use warnings;
use DBI;
sub new {
my $class = shift;
my $config = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
$self->{'block_table'} = 'badhosts';
return $self;
}
sub add {
my $self = shift;
my $params = shift;
die "No params" unless $params;
$self->{'dbh'} = $self->{'config'}->get_dbh unless($self->{'dbh'});
my $return = {};
$return = $self->addtoblacklist($params) if($params->{'list'} eq 'black');
$return = $self->addtorejectlist($params) if($params->{'list'} eq 'reject');
return $return;
}
sub addtoblacklist {
my $self = shift;
my $work = shift;
my $host = $work->{'host'};
my $asn = $work->{'asn'} || 0;
my $iso = $work->{'iso'} || '';
return { retval => 0, retmsg => "No host specified" } unless($host);
my $sth = $self->{'dbh'}->prepare("
SELECT COUNT(*)
FROM reject_iptables
WHERE ip = ? AND
time > DATE_SUB(NOW(), INTERVAL 5 MINUTE)");
$sth->execute($host);
my $rows = $sth->fetchrow_arrayref->[0];
unless($rows) {
my $sth = $self->{'dbh'}->prepare("INSERT INTO reject_iptables(ip,asn,iso) VALUES(?,?,?)") || return { retval => 0, retmsg => 'Failed to prepare statement in addtoblacklist '.DBI::errstr };
$sth->execute($host,$asn,$iso) || return { retval => 0, retmsg => 'Failed to execute statement in addtoblacklist '.DBI::errstr };
system "/sbin/pfctl -q -t $self->{'block_table'} -T add $host";
system "/sbin/pfctl -q -k $host";
return { retval => 1, retmsg => "Added pf rule" };
} else {
return { retval => 1, retmsg => "Recently got $rows entries" };
}
}
sub addtorejectlist {
my $self = shift;
my $work = shift;
my $host = $work->{'host'};
my $service = $work->{'service'} || '';
my $asn = $work->{'asn'} || 0;
my $iso = $work->{'iso'} || '';
return { retval => 0, retmsg => "No host specified" } unless($host);
my $sth = $self->{'dbh'}->prepare("INSERT INTO reject(ip,service,asn,iso) VALUES(?,?,?,?)") || return { retval => 0, retmsg => 'Failed to prepare statement in addtorejectlist '.DBI::errstr };
$sth->execute($host,$service,$asn,$iso) || return { retval => 0, retmsg => 'Failed to executute statement in addtorejectlist '.DBI::errstr };
return { retval => 1, retmsg => "Added to reject list" };
}
1;

45
lib/apache.pm Normal file
View File

@ -0,0 +1,45 @@
package My::parser::apache;
use strict;
use warnings;
use File::Tail 0.91;
use My::parser::apache_parser;
sub new {
my $class = shift;
my $config = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
$self->{'parser'} = My::parser::apache_parser->new();
return $self;
}
sub parse {
my $self = shift;
my @result;
while(my $string = $self->fetch) {
last unless($string);
if (my $line = $self->{'parser'}->parser($string)) {
push(@result,$line);
}
}
return { retval => 0 } unless(scalar(@result)); # nothing to say, nothing to report
return { retval => 1, retmsg => 'Here comes the results', lines => \@result };
}
sub fetch {
my $self = shift;
my $fetcher = $self->{'config'}->get_fetcher('apache');
die "Fetcher for apache went away?" unless($fetcher);
my $line;
my ($nfound,$timeleft,@pending) = File::Tail::select(undef,undef,undef,1,$fetcher);
foreach (@pending) {
$line = $_->read;
chomp($line);
}
return 0 unless($line);
return $line;
}
1;

81
lib/apache_parser.pm Normal file
View File

@ -0,0 +1,81 @@
package My::parser::apache_parser;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless ($self, $class);
return $self;
}
sub parser {
my $self = shift;
my $string = shift;
my ($reply,$hostile,$host) = ("No match for $string",0,'');
my $re_host = qr/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/;
if($string =~ m/proxy_fcgi:error.*Got error 'Primary script unknown'/) {
$_ = $string;
$reply = 'Request for unknown fcgi-script';
$hostile = 1;
PARSE:
m/(\[client\ ($re_host)\:0\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/script not found or unable to stat/) {
$_ = $string;
$reply = 'Script not found';
$hostile = 1;
PARSE:
m/(\[client\ ($re_host)\:0\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/ap_pass_brigade failed in handle_request_ipc function/) {
$_ = $string;
$hostile = 1;
$reply = 'Connection closed early';
PARSE:
m/(\[client\ ($re_host)\:0\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/Invalid URI in request/) {
$_ = $string;
$hostile = 1;
$reply = 'Invalid URI';
PARSE:
m/(\[client\ ($re_host)\:0\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/invalid URI path /) {
$_ = $string;
$hostile = 1;
$reply = 'Invalid URI';
PARSE:
m/(\[client\ ($re_host)\:0\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/\(63\)File name too long: /) {
$_ = $string;
$hostile = 1;
$reply = 'File name too long';
PARSE:
m/(\[client\ ($re_host)\:0\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/ AH00135: Invalid method in request /) {
$_ = $string;
$hostile = 1;
$reply = 'Invalid request method';
PARSE:
m/(\[client\ ($re_host)\:0\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/mod_fcgid: cleanup zombie process/) {
$reply = 'fcgi process killed';
} elsif($string =~ m/scoreboard already in used/) {
$reply = 'Scoreboard already in use';
}
return { retval => 1, retmsg => $reply, hostile => $hostile, host => $host, string => $string };
}
return 1;

78
lib/block.pm Normal file
View File

@ -0,0 +1,78 @@
package My::parser::block;
use strict;
use warnings;
use My::parser::addtolist;
use My::parser::stats;
use POSIX qw(ceil floor);
sub new {
my $class = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = shift;
$self->{'addtolist'} = My::parser::addtolist->new($self->{'config'});
$self->{'stats'} = My::parser::stats->new($self->{'config'});
return $self;
}
sub blocklogic {
my $self = shift;
my $result = shift;
return { retval => 0, retmsg => 'Too few arguments to blocklogic' } unless($result);
my $host = $result->{'host'};
my $service = $result->{'service'};
my $geoip = $result->{'geoip'};
my $short = $self->{'config'}->get_as_single_val('config','short');
my $long = $self->{'config'}->get_as_single_val('config','long');
my $retstr;
my $stats;
my $logline;
my $modifier = 0;
my $fromreject = $self->{'addtolist'}->add( { list => 'reject', host => $host, service => $service, asn => $geoip->{'asn'}, iso => $geoip->{'iso'} } );
return { retval => 0, hostile => 0, retmsg => $fromreject->{'retmsg'} } unless($fromreject->{'retval'});
if ($stats = $self->{'stats'}->checker({ host => $host, asn => $geoip->{'asn'}, iso => $geoip->{'iso'} })) {
$self->{'config'}->{'logger'}->log($stats->{'retmsg'}) unless($stats->{'retval'});
my $hrs = $stats->{$short}->{'reject'} || 0;
my $ars = $stats->{$short}->{'asn'} || 0;
my $irs = $stats->{$short}->{'iso'} || 0;
my $hbs = $stats->{$short}->{'blocks'} || 0;
my $abs = $stats->{$short}->{'block_asn'} || 0;
my $ibs = $stats->{$short}->{'block_iso'} || 0;
my $hrl = $stats->{$long}->{'reject'} || 0;
my $arl = $stats->{$long}->{'asn'} || 0;
my $irl = $stats->{$long}->{'iso'} || 0;
my $hbl = $stats->{$long}->{'blocks'} || 0;
my $abl = $stats->{$long}->{'block_asn'} || 0;
my $ibl = $stats->{$long}->{'block_iso'} || 0;
my $hostile = 0;
my $shortrejectpoints = ($hrs * 100) + ($ars * 50) + ($irs * 25);
my $shortblockpoints = ($hbs * 750) + ($abs * 250) + ($ibs * 50);
my $longrejectpoints = ($hrl * 30) + ($arl * 10) + ($irl);
my $longblockpoints = ($hbl * 150) + ($abl * 75) + ($ibl * 10);
my $points = $shortrejectpoints + $shortblockpoints + $longrejectpoints + $longblockpoints;
if (my $recent_hostile = $self->{'stats'}->recent_hostile()) {
unless($recent_hostile->{'retval'}) {
$self->{'config'}->{'logger'}->log($recent_hostile->{'retmsg'})
} else {
my $rh = $recent_hostile->{'rows'} - 1;
if($rh > 0) {
$modifier = 1 + ($rh / 5);
$points = floor($points * $modifier);
}
}
}
$hostile++ if($points > 999);
my $fromblack = $self->{'addtolist'}->add( { list => 'black', host => $host, asn => $geoip->{'asn'}, iso => $geoip->{'iso'} } ) if($hostile);
$self->{'config'}->{'logger'}->log($fromblack->{'retmsg'}) unless($fromblack->{'retval'});
$logline .= $fromblack->{'retmsg'}.', ' if($fromblack->{'retmsg'});
$logline .= "Points: $points";
$logline .= '(mod='.$modifier.')' if($modifier);
return { retval => 1, hostile => $hostile, retmsg => $logline };
} else {
return { retval => 0, hostile => 0, retmsg => 'Blocklogic failed, no idea why:)' };
}
}
1;

209
lib/config.pm Normal file
View File

@ -0,0 +1,209 @@
package My::parser::config;
use strict;
use warnings;
use Getopt::Long;
sub new {
my $class = shift;
my $file = shift;
my $self = {};
bless ($self, $class);
my $test = 0;
my $parse;
GetOptions(
"cf=s" => \$file, #string, config file from options
"test=i" => \$test, #integer, test mode (no writing to db, no blacklisting)
"parse=s" => \$parse, #string, parser testing. Parse only the submitted line, test=1 needed
);
$self->{'file'} = $file;
$self->parse if($self->load);
$self->{'config'}->{'test'} = $test;
$self->{'config'}->{'parse'} = $parse;
return $self;
}
sub load {
my $self = shift;
my $file = $self->{'file'};
open(FH, "<", "$file") || die "Could not open $file for reading: ".$!;
while(<FH>) {
my $line = $_;
chomp($line);
next unless($line =~ m/\S/);
push(@{$self->{'conf_file'}},$line) unless($line =~ m/^\#/);
}
close(FH);
return 1;
}
sub parse {
my $self = shift;
foreach(@{$self->{'conf_file'}}) {
my ($opt,$arg) = split("=");
$opt =~ s/^\s+//;
$opt =~ s/\s+$//;
$arg =~ s/^\s+//;
$arg =~ s/\s+$//;
my @argarr = split("','",$arg);
foreach my $argie(@argarr) {
$argie =~ s/^\'//;
$argie =~ s/\'$//;
push(@{$self->{'config'}->{$opt}},$argie);
}
}
delete $self->{'conf_file'};
return 1;
}
sub get_modules {
my $self = shift;
my $type = $self->{'config'}->{'modules'};
return $type;
}
sub get_parsers {
my $self = shift;
my $return = $self->{'parsers'};
return $return;
}
sub get_fetchers {
my $self = shift;
my $return = $self->{'fetchers'};
return $return;
}
sub get_parser_info {
my $self = shift;
my $m = shift;
my $toret = {};
if(defined($self->{'config'}->{$m})) {
for(my $i = 0; $i < @{$self->{'config'}->{$m}}; $i++) {
my $val;
$val = 'type' if($i == 0);
$val = 'source' if($i == 1);
$val = 'program' if($i == 2);
$toret->{$val} = @{$self->{'config'}->{$m}}[$i]
}
} else {
return;
}
return $toret;
}
sub get_fetcher {
my $self = shift;
my $m = shift;
my $toret;
if(defined($self->get_parser_info($m))) {
my $type = $self->get_parser_info($m)->{'type'};
if ($toret = $self->{'config'}->{'fetchers'}->{$type}->{$m}) {
return $toret;
}
}
return;
}
sub get_fetcher_module {
my $self = shift;
my $m = shift;
my $toret;
if(defined($self->{'fetcher'}->{$m})) {
my $toret = $self->{'fetcher'}->{$m};
return $toret;
} else {
return;
}
}
sub get_dbh {
my $self = shift;
my $dbh = $self->{'config'}->{'fetchers'}->{'db'}->{'dbh'};
return $dbh;
}
sub set_dbh {
my $self = shift;
my $dbh = shift;
$self->{'config'}->{'fetchers'}->{'db'}->{'dbh'} = $dbh;
return 1;
}
sub set_fetcher {
my $self = shift;
my $type = shift; ## Type of source
my $m = shift; ## m for module? name of service
my $fetcher = shift; ## hash for reaching fetcher
if($type && $m) {
$self->{'config'}->{'fetchers'}->{$type}->{$m} = $fetcher;
#print localtime(time).": Fetcher set to $type($fetcher) for service $m\n"; FIXME add debug option in config?
return 1;
}
return 0;
}
sub set {
my $self = shift;
my $node = shift || undef;
my $key = shift || undef;
my $value = shift || undef;
if(defined($node) && defined($key) && defined($value)) {
$self->{$node}->{$key} = $value;
return 1;
} elsif (defined($node) && defined($key)) {
$self->{$node} = $key;
return 1;
}
return 0;
}
sub get {
my $self = shift;
my $node = shift || undef;
my $key = shift || undef;
if(defined($node) && defined($key)) {
my $toret = $self->{$node}->{$key};
return $toret;
} elsif(defined($node)) {
my $toret = $self->{$node};
return $toret;
}
return 0;
}
sub get_as_single_val {
my $self = shift;
my $node = shift || undef;
my $key = shift || undef;
my $toret;
if(defined($node) && defined($key)) {
$toret = exists $self->{$node}->{$key} ? $self->{$node}->{$key} : undef;
} elsif(defined($node)) {
$toret = exists $self->{$key} ? $self->{$key} : undef;
}
if($toret) {
if(scalar(@{$toret})) {
$toret = join('',@{$toret});
}
return $toret;
}
return;
}
sub print {
my $self = shift;
foreach my $key(keys %{$self->{'config'}}) {
$self->{'config'}->{'logger'}->log("$key: ");
my $tolog;
foreach my $arg(@{$self->{'config'}->{$key}}) {
$tolog .= "$arg ";
}
$self->{'config'}->{'logger'}->log($tolog);
}
return 1;
}
1;

48
lib/db.pm Normal file
View File

@ -0,0 +1,48 @@
package My::parser::db;
use strict;
use warnings;
use DBI;
#use Scalar::Util qw(weaken);
sub new {
my $class = shift;
my $config = shift;
my $rest = shift;
return \$class if($rest);
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
return $self;
}
sub init {
my $self = shift;
my $parser = shift; ## not needed for this module as the db-connection is for all modules
$self->connect || die "Could not connect to db";
return 1;
}
sub connect {
my $self = shift;
my $config = $self->{'config'};
my $usr = $config->get_as_single_val('config','dbusr');
my $pwd = $config->get_as_single_val('config','dbpwd');
my $host = $config->get_as_single_val('config','dbhost');
my $db = $config->get_as_single_val('config','db');
my $dbh;
if($usr && $pwd && $host && $db) {
while(1) {
last if($dbh = DBI->connect("DBI:mysql:database=$db;host=$host",$usr,$pwd, { PrintError => 1, mysql_auto_reconnect=>1, AutoCommit => 1 }));
sleep(10);
}
$config->set_dbh($dbh);
#$self->{'config'}->{'logger'}->log("Sucsessfully connected to db"); FIXME add debug to config?
return 1;
} else {
$self->{'config'}->{'logger'}->log("Unable to connect to db, not enough parameters");
return 0;
}
}
1;

43
lib/dovecot.pm Normal file
View File

@ -0,0 +1,43 @@
package My::parser::dovecot;
use strict;
use warnings;
use My::parser::dovecot_parser;
sub new {
my $class = shift;
my $config = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
$self->{'parser'} = My::parser::dovecot_parser->new();
return $self;
}
sub parse {
my $self = shift;
my @result;
while(my $string = $self->fetch) {
last unless($string);
if (my $line = $self->{'parser'}->parser($string)) {
push(@result,$line);
}
}
return { retval => 0 } unless(scalar(@result));
return { retval => 1, retmsg => 'Here comes the results', lines => \@result };
}
sub fetch {
my $self = shift;
my $fetcher = $self->{'config'}->get_fetcher('dovecot');
my $line;
my ($nfound,$timeleft,@pending) = File::Tail::select(undef,undef,undef,1,$fetcher);
foreach (@pending) {
$line = $_->read;
chomp($line);
}
return 0 unless($line);
return $line;
}
1;

113
lib/dovecot_parser.pm Normal file
View File

@ -0,0 +1,113 @@
package My::parser::dovecot_parser;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless ($self, $class);
return $self;
}
sub parser {
my $self = shift;
my $string = shift;
my ($reply,$hostile,$host) = ("No match for $string",0,'');
my $re_host = qr/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/;
my $re_usr = qr/[a-zA-Z0-9_-]*/;
my $re_pid = qr/[0-9]*/;
my $re_uid = qr/[a-zA-Z0-9_\-+:\/]*/;
my $re_info = qr/\($re_usr\)\<$re_pid\>\<$re_uid\>/;
if($string =~ m/ Info: Disconnected: Logged out /) {
$reply = 'Normal logout';
} elsif($string =~ m/ imap-login: (Info: |)Login: /) {
$reply = 'Normal login';
} elsif($string =~ m/ indexer-worker$re_info: Info: /) {
$reply = 'Indexer worker message';
} elsif($string =~ m/ Disconnected (in IDLE|for inactivity) /) {
$reply = 'Idle disconnect';
} elsif($string =~ m/imap($re_info): (Info: |Disconnected: |)Connection closed/) {
$reply = 'Normal connection closed';
} elsif($string =~ m/ imap$re_info: (Info: |)Disconnected: Logged out /) {
$reply = 'Normal log out';
} elsif($string =~ m/ master: Warning: Time moved /) {
$reply = 'Clock adjustment';
} elsif($string =~ m/auth failed/) {
$_ = $string;
$reply = 'Auth failure';
$hostile = 1;
PARSER:
m/ (rip=($re_host)) /gcix && do {
$host = $2;
};
} elsif($string =~ m/(Authentication error (\(Password mismatch\?\)|unknown user))/) {
$_ = $string;
$reply = 'Unknown user or auth error';
$hostile = 1;
PARSER:
m/(([a-zA-Z0-9@._-]*),($re_host),.*\): pam_authenticate\(\) failed: (Authentication error \(Password mismatch\?\)|unknown user))/gi && do {
$host = $3;
};
} elsif($string =~ m/unknown user/) {
$_ = $string;
$reply = 'Unknown user';
$hostile = 1;
PARSER:
m/(((Info: |)conn unix:auth-worker \(uid=([0-9]{1,9})\): auth-worker<([0-9]{1,9}>: pam\(([a-zA-Z0-9@._-]*),($re_host),<($re_uid)>\): unknown user)))/gi && do {
$host = $7;
};
} elsif($string =~ m/ imap-login: (Info: |)Disconnected/) {
if($string =~ m/Connection closed/) {
$hostile = 0;
$reply = 'Disconnecting is legit';
} elsif($string =~ m/ TLS handshaking: /) {
$reply = 'TLS error';
$hostile = 1;
} elsif($string =~ m/Too many invalid commands/) {
$reply = 'Too many invalid commands';
$hostile = 1;
} elsif($string =~ m/TLS: Disconnected,/) {
$reply = 'Likely a sleeping android';
$hostile = 0;
} elsif($string =~ m/client didn't finish SASL auth/) {
$reply = 'Timeout waiting for SASL auth';
$hostile = 1;
} elsif($string =~ m/no auth attempts in/) {
if($string =~ m/, secured/) {
$reply = 'Secured Disconnect during auth, either sleeping phone or attack on webmail';
$hostile = 0;
} else {
$reply = 'Non-secure disconnect during auth';
$hostile = 1;
}
}
if($hostile) {
$_ = $string;
PARSER:
m/\ (rip=($re_host))/gcix && do {
$host = $2;
};
}
} elsif($string =~ m/ imap-login: (Info: |) Aborted login /) {
$_ = $string;
$reply = 'Aborted login';
$hostile = 1;
PARSER:
m/ (rip=($re_host)) /gcix && do {
$host = $2;
};
} elsif($string =~ m/unknown user$/) {
$_ = $string;
$hostile = 1;
$reply = 'Unknown user';
PARSER:
m/Info: pam\([a-zA-Z0-9@._-]*,($re_host),\<.*\>\): unknown user/gi && do {
$host = $1;
};
} else {
$reply = 'No match for '.$string;
}
return { retval => 1, retmsg => $reply, hostile => $hostile, host => $host, string => $string };
}
1;

45
lib/exim.pm Normal file
View File

@ -0,0 +1,45 @@
package My::parser::exim;
use strict;
use warnings;
use File::Tail 0.91;
use My::parser::exim_parser;
sub new {
my $class = shift;
my $config = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
$self->{'parser'} = My::parser::exim_parser->new();
return $self;
}
sub parse {
my $self = shift;
my @result;
while(my $string = $self->fetch) {
last unless($string);
if (my $line = $self->{'parser'}->parser($string)) {
push(@result,$line);
}
}
return { retval => 0 } unless(scalar(@result)); # nothing to say, nothing to report
return { retval => 1, retmsg => 'Here comes the results', lines => \@result };
}
sub fetch {
my $self = shift;
my $fetcher = $self->{'config'}->get_fetcher('exim');
die "Fetcher for exim went away?" unless($fetcher);
my $line;
my ($nfound,$timeleft,@pending) = File::Tail::select(undef,undef,undef,1,$fetcher);
foreach (@pending) {
$line = $_->read;
chomp($line);
}
return 0 unless($line);
return $line;
}
1;

173
lib/exim_parser.pm Normal file
View File

@ -0,0 +1,173 @@
package My::parser::exim_parser;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless ($self, $class);
return $self;
}
sub parser {
my $self = shift;
my $string = shift;
my ($reply,$hostile,$host) = ("No match for $string",0,'');
my $re_host = qr/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/;
my $re_msgid = qr/[0-9a-z]{6}-[0-9a-z]{11}-[0-9a-z]{4}/i;
my $saslauth = qr/saslauthd_server authenticator failed/i;
if($string =~ m/ ($re_msgid) /) {
$reply = 'MSG ID, normal delivery';
} elsif($string =~ m/(Start|End) queue run/) {
$reply = 'Queue run';
} elsif($string =~ m/1 accept\(\) failure/) {
$reply = 'Connection aborted by software';
} elsif($string =~ m/SSL verify error: /) {
if($string =~ m/self signed certificate in certificate chain/) {
$reply = 'Self signed certificate in chain';
} elsif($string =~ m/error=self signed certificate cert=/) {
$reply = 'Self signed certificate';
} else {
$reply = 'Other SSL verify errors';
}
} elsif($string =~ m/Temporary DNS error while checking SPF record/) {
$reply = 'Temporary DNS error while checking SPF record';
} elsif($string =~ m/$saslauth /) {
$_ = $string;
$hostile = 1;
$reply = 'saslauth error';
PARSER:
m/(\ \[($re_host)\]:)/gcix && do {
$host = $2;
};
} elsif($string =~ m/> rejected (after DATA|RCPT)/) {
$_ = $string;
$reply = 'Rejected';
$hostile = 1;
PARSER:
m/(\ \[($re_host)\]\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/\ rejected (AUTH|MAIL)\ /) {
$_ = $string;
$reply = 'Rejected MAIL or AUTH';
$hostile = 1;
PARSER:
m/(\ \[($re_host)\]\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/ too many (nonmail|syntax or protocol errors|unrecognized commands) /) {
$_ = $string;
$reply = 'protocol violations';
$hostile = 1;
PARSER:
m/(\ \[($re_host)\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/ SMTP protocol synchronization error /) {
$_ = $string;
$reply = 'protocol errors';
$hostile = 1;
PARSE:
m/(\[($re_host)\]\ ) /gcix && do {
$host = $2;
};
} elsif($string =~ m/ TLS error on connection /) {
$_ = $string;
$reply = 'TLS error';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\]\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/ TLS error \(SSL_read\)\: on connection /) {
$_ = $string;
$reply = 'TLS error';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\]\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/ no IP address found for host /) {
$_ = $string;
$reply = 'Problems resolving hostname';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/ no host name found for IP address /) {
$_ = $string;
$reply = 'No hostname';
$hostile = 0;
} elsif($string =~ m/unexpected disconnection while reading SMTP command from/) {
$_ = $string;
$reply = 'SMTP error, disconnected';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\]\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/ sender verify fail for /) {
$_ = $string;
$reply = 'Sender verify failure';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\]\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/ rejected (EH|HE)LO from /) {
$_ = $string;
$reply = 'HELO/EHLO error';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\](:|)\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/ You are not me /) {
$_ = $string;
$reply = 'Forged HELO';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\]\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/ rejected after DATA /) {
$_ = $string;
$reply = 'Parser error';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\]\ )/gcix && do {
$host = $2;
};
} elsif($string =~ m/ (login_saslauthd_server|LOGIN|PLAIN) authenticator failed for /) {
$_ = $string;
$reply = 'Auth error';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/ SMTP command timeout /) {
$_ = $string;
$reply = 'SMTP timeout';
$hostile = 1;
PARSE:
m/(\ \[($re_host)\])/gcix && do {
$host = $2;
};
} elsif($string =~ m/SSL_write: /) {
unless($string =~ m/syscall: Broken pipe/) { #If we get a broken pipe, it's most likely because we blocked the ip earlier, and this is just the pipe timing out
$_ = $string;
$reply = 'SSL error';
$hostile = 1;
PARSE:
m/(\ \[$re_host\]\))/gcix && do {
$host = $2;
};
}
}
return { retval => 1, retmsg => $reply, hostile => $hostile, host => $host, string => $string };
}
return 1;

72
lib/file.pm Normal file
View File

@ -0,0 +1,72 @@
package My::parser::file;
use strict;
use warnings;
use File::Tail 0.91;
sub new {
my $class = shift;
my $config = shift;
my $rest = shift;
return \$class if($rest);
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
$self->{'config'}->{'fetcher'}->{'file'} = $self;
return $self;
}
sub init {
my $self = shift;
my $parser = shift;
if($parser) {
my $hash = $self->{'config'}->get_parser_info($parser);
if($hash->{'type'} eq 'file') {
my %opts = %{ &set_defaults };
my $filename = $hash->{'source'};
my $maxinterval = $self->{'config'}->get_as_single_val('config','maxinterval');
my $interval = $self->{'config'}->get_as_single_val('config','interval');
my $adjustafter = $self->{'config'}->get_as_single_val('config','adjustafter');
my $resetafter = $self->{'config'}->get_as_single_val('config','resetafter');
my $maxbuf = $self->{'config'}->get_as_single_val('config','maxbuf');
my $nowait = $self->{'config'}->get_as_single_val('config','nowait');
my $ignore_nonexistant = $self->{'config'}->get_as_single_val('config','ignore_nonexistant');
my $tail = $self->{'config'}->get_as_single_val('config','tail');
my $reset_tail = $self->{'config'}->get_as_single_val('config','reset_tail');
$opts{'name'} = $filename;
$opts{'maxinterval'} = $maxinterval if(defined($maxinterval));
$opts{'interval'} = $interval if(defined($interval));
$opts{'adjustafter'} = $adjustafter if(defined($adjustafter));
$opts{'resetafter'} = $resetafter if(defined($resetafter));
$opts{'maxbuf'} = $maxbuf if(defined($maxbuf));
$opts{'nowait'} = $nowait if(defined($nowait));
$opts{'ignore_nonexistant'} = $ignore_nonexistant if(defined($ignore_nonexistant));
$opts{'tail'} = $tail if(defined($tail));
$opts{'reset_tail'} = $reset_tail if(defined($reset_tail));
$self->{'config'}->{'logger'}->log("Initializing fetcher file from parser $parser with file $filename");
my $newfetcher = File::Tail->new(%opts);
$self->{'config'}->set_fetcher('file',$parser,$newfetcher);
return 1;
} else {
return 0;
}
} else {
return 0;
}
}
sub set_defaults {
my $opts;
$opts->{'maxinterval'} = 60; #max time spent sleeping between checks
$opts->{'interval'} = 10; #initial time before first check
$opts->{'adjustafter'} = 10; #number of times $interval passes before adjusting the check interval
$opts->{'resetafter'} = $opts->{'interval'} * $opts->{'adjustafter'}; #Number of seconds after last change to file before reopening the file
$opts->{'maxbuf'} = 16384;
$opts->{'nowait'} = 0; #Does not block on read, but returns an empty string if there is nothing to read.
$opts->{'ignore_nonexistant'} = 0; #Do not complain if the file doesn't exist when it is first opened or when it is to be reopened.
$opts->{'tail'} = 0; #When first started, read and return C<n> lines from the file. If C<n> is zero, start at the end of file. If C<n> is negative, return the whole file.
$opts->{'reset_tail'} = 0; #Same as tail, but for when $resetafter has gone by without any changes to the file
return $opts;
}
1;

40
lib/geoip.pm Normal file
View File

@ -0,0 +1,40 @@
package My::parser::geoip;
use strict;
use warnings;
use IO::Socket::INET;
sub new {
my $class = shift;
my $self = {};
bless ($self,$class);
$self->{'config'} = shift;
my $config = $self->{'config'};
$config->{'logger'}->log('Loaded My::parser::geoip');
return $self;
}
sub parse {
my $self = shift;
my $host = shift;
return unless($host =~ m/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/);
$| = 1;
my $socket = new IO::Socket::INET (
PeerHost => '10.0.0.100',
PeerPort => '7777',
Proto => 'tcp'
);
$self->{'config'}->{'logger'}->log("Failed to look up GeoIP for $host, could not connect to GeoIP resolver") unless $socket;
return { asn => 0, iso => '' } unless $socket;
my $size = $socket->send($host."\n");
my $response;
$socket->recv($response, 1024);
shutdown($socket,1);
$socket->close;
my ($asn,$iso) = split (",", $response);
my $r = {};
$r->{'asn'} = $asn;
$r->{'iso'} = $iso;
return $r;
}
1;

43
lib/gitea.pm Normal file
View File

@ -0,0 +1,43 @@
package My::parser::gitea;
use strict;
use warnings;
use My::parser::gitea_parser;
sub new {
my $class = shift;
my $config = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
$self->{'parser'} = My::parser::gitea_parser->new();
return $self;
}
sub parse {
my $self = shift;
my @result;
while(my $string = $self->fetch) {
last unless($string);
if (my $line = $self->{'parser'}->parser($string)) {
push(@result,$line);
}
}
return { retval => 0 } unless(scalar(@result));
return { retval => 1, retmsg => 'Here comes the results', lines => \@result };
}
sub fetch {
my $self = shift;
my $fetcher = $self->{'config'}->get_fetcher('gitea');
my $line;
my ($nfound,$timeleft,@pending) = File::Tail::select(undef,undef,undef,1,$fetcher);
foreach (@pending) {
$line = $_->read;
chomp($line);
}
return 0 unless($line);
return $line;
}
1;

41
lib/gitea_parser.pm Normal file
View File

@ -0,0 +1,41 @@
package My::parser::gitea_parser;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless ($self, $class);
return $self;
}
sub parser {
my $self = shift;
my $string = shift;
my ($reply,$hostile,$host) = ("No match for $string",0,'');
my $re_host = qr/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/;
if($string =~ m/sshConnectionFailed/) {
if($string =~ m/Failed connection from /) {
$_ = $string;
$reply = 'Failed connection';
$hostile = 1;
PARSE:
m/($re_host)\:[0-9]{1,6} /gcix && do {
$host = $1;
};
} elsif($string =~ m/Failed authentication attempt from /) {
$_ = $string;
$reply = 'Failed auth';
$hostile = 1;
PARSE:
m/($re_host)\:[0-9]{1,6}/gcix && do {
$host = $1;
};
}
} else {
$reply = 'non-ssh lines not supported yet';
}
return { retval => 1, retmsg => $reply, hostile => $hostile, host => $host, string => $string };
}
return 1;

24
lib/localcheck.pm Normal file
View File

@ -0,0 +1,24 @@
package My::parser::localcheck;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless ($self, $class);
return $self;
}
sub islocal {
my $self = shift;
my $host = shift;
my @local_nets = ('127\.','10\.','192\.168\.','172\.((1[6-9])|(2[0-9])|(3[0-1]))\.','213\.236\.200\.(7|10)'); # array of regexes matching networks considered local and to be ignored
my $local = 0;
foreach my $test(@local_nets) {
$local = 1 if($host =~ m/^$test/);
last if $local;
}
return $local;
}
return 1;

35
lib/logger.pm Normal file
View File

@ -0,0 +1,35 @@
package My::parser::logger;
use strict;
use warnings;
sub new {
my $class = shift;
my $config = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
return $self;
}
sub log {
my $self = shift;
my $str = shift;
my $fh = $self->{'logfh'};
return 0 unless($str);
print $fh localtime(time).': '.$str."\n";
return 1;
}
sub reload_log {
my $self = shift;
my $file = $self->{'config'}->get_as_single_val('config','logfile');
die 'No output logfile' unless($file);
close($self->{'logfh'}) if($self->{'logfh'});
my $fh = $self->{'logfh'};
open($fh, '>>', "$file") || die 'Could not open logfile for appending: '.$!;
select $fh; $| = 1;
$self->{'logfh'} = $fh;
return 1;
}
1;

142
lib/parser.pm Normal file
View File

@ -0,0 +1,142 @@
package My::parser::parser;
use strict;
use warnings;
use My::parser::logger;
use My::parser::config;
use My::parser::geoip;
use My::parser::localcheck;
use My::parser::block;
sub new {
my $class = shift;
my $config_file = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = My::parser::config->new($config_file) || die "Could not load config file $config_file";
$self->{'config'}->{'logger'} = My::parser::logger->new($self->{'config'});
$self->{'config'}->{'logger'}->reload_log;
$self->{'geoip'} = My::parser::geoip->new($self->{'config'});
$self->{'localcheck'} = My::parser::localcheck->new;
$self->{'block'} = My::parser::block->new($self->{'config'});
return $self;
}
sub parse_all {
my $self = shift;
my $parsers = $self->{'config'}->get_parsers;
foreach my $service (keys %{$parsers}) {
my $fromparser = $parsers->{$service}->parse;
if($fromparser->{'retval'} == 1) {
foreach my $line (@{$fromparser->{'lines'}}) {
my $msg = $line->{'retmsg'};
my $host = $line->{'host'};
my $hostile = $line->{'hostile'};
if ($msg && $hostile && $host) {
unless($self->{'localcheck'}->islocal($host)) {
my $tolog = $host."($service";
my $geoip = $self->{'geoip'}->parse($host);
if ($geoip->{'asn'} && $geoip->{'iso'}) {
$tolog .= ",$geoip->{'asn'},$geoip->{'iso'}): ";
} else {
$geoip->{'asn'} = 0;
$geoip->{'iso'} = '';
$tolog .= '): ';
}
if(my $fromblock = $self->{'block'}->blocklogic({host => $host, geoip => $geoip, service => $service})) {
my $blmsg = $fromblock->{'retmsg'};
$tolog .= $blmsg;
}
$tolog .= ", $msg";
$self->{'config'}->{'logger'}->log($tolog);
} else {
$self->{'config'}->{'logger'}->log("$host is local");
}
} elsif ($msg =~ m/^No match/) {
$self->{'config'}->{'logger'}->log("$service said: $msg");
} elsif ($hostile) {
$self->{'config'}->{'logger'}->log("Parser error, $service reported hostile activity, but no host given. Message from parser was: $msg. String passed to parser: ".$line->{'string'});
}
}
}
}
return 1;
}
sub load_parsers {
my $self = shift;
my $config = $self->{'config'};
die 'No modules defined in config' unless(scalar(@{$config->get_modules}));
foreach my $parser(@{$config->get_modules}) {
if(my $newparser = $self->load($parser)) {
$config->set('parsers',$parser,$newparser);
my $module_info = $config->get_parser_info($parser);
my $fetcher_needed = $module_info->{'type'};
my $isloaded = $config->{'loadedparsers'}->{$fetcher_needed};
unless(defined($isloaded)) {
if(my $fetcher_loaded = $self->load($fetcher_needed)) {
$self->dyninit($fetcher_loaded,$fetcher_needed,$parser);
#$self->{'config'}->{'logger'}->log("Loaded dependency from $parser; $fetcher_needed"); FIXME add debug in config?
} else {
die "Failed to load $fetcher_needed, needed by $parser";
}
} else {
my $toinit = $config->get_fetcher_module($fetcher_needed);
if(defined($toinit)) {
unless($self->dyninit($toinit,$fetcher_needed,$parser)) {
$self->{'config'}->{'logger'}->log("Dyninit failed for $fetcher_needed for $parser");
}
} else {
$self->{'config'}->{'logger'}->log('No dyninit needed for '.$fetcher_needed.' for '.$parser);
}
}
} else {
$self->{'config'}->{'logger'}->log("Failed to load parser for $parser");
}
}
return 1;
}
sub dyninit {
my $self = shift;
my $fetcher_loaded = shift;
my $fetcher_needed = shift;
my $parser = shift;
if(my $initr = $self->init($fetcher_loaded,$parser)) {
$self->{'config'}->set('fetchers',$fetcher_needed,$fetcher_loaded);
#$self->{'config'}->{'logger'}->log("Dependency $fetcher_needed initialized"); FIXME add debug in config?
return 1;
}
return 0;
}
sub load {
my $self = shift;
my $config = $self->{'config'};
my $parser = shift;
my $filename = 'My/parser/'.$parser.'.pm';
my $newclass;
eval {
require $filename;
my $classname = 'My::parser::'.$parser;
$newclass = $classname->new($config) || die "Failed to load parser for $parser";
$self->{'config'}->{'logger'}->log("Loaded $classname");
} or do {
my $e = $@;
$self->{'config'}->{'logger'}->log("Failed to load $filename: $e");
return 0;
};
$config->{'loadedparsers'}->{$parser} = 'loaded';
return $newclass;
}
sub init {
my $self = shift;
my $fetcher = shift;
my $parser = shift;
return 1 if($fetcher->init($parser));
return 0;
}
1;

59
lib/ssh.pm Normal file
View File

@ -0,0 +1,59 @@
package My::parser::ssh;
use strict;
use DBI;
use warnings;
use My::parser::ssh_parser;
sub new {
my $class = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = shift;
$self->{'parser'} = My::parser::ssh_parser->new();
return $self;
}
sub fetch {
my $self = shift;
$self->{'dbh'} = $self->{'config'}->get_dbh unless($self->{'dbh'});
my $seq = $self->{'seq'} || 0;
my $retmsg;
my @toreturn;
unless($seq) {
my $seqsth = $self->{'dbh'}->prepare("SELECT seq FROM logs WHERE program = 'sshd' ORDER BY seq DESC LIMIT 1") or $retmsg = DBI::errstr;
$seqsth->execute or $retmsg = DBI::errstr unless($retmsg);
$seq = $seqsth->fetchrow_arrayref->[0] or $retmsg = DBI::errstr unless($retmsg);
}
return { retval => 0, retmsg => $retmsg, error => 1 } if($retmsg);
my $sth = $self->{'dbh'}->prepare("SELECT msg,seq FROM logs WHERE program = 'sshd' AND seq > $seq") or $retmsg = DBI::errstr unless($retmsg);
$sth->execute or $retmsg = DBI::errstr unless($retmsg);
while(my $ref = $sth->fetchrow_hashref) {
my $string = $$ref{'msg'};
$seq = $$ref{'seq'};
push(@toreturn,$string);
}
$self->{'seq'} = $seq;
return { retval => 0, retmsg => $retmsg, error => 1 } if($retmsg);
return { retval => 0, retmsg => 'Nothing to return' } unless(scalar(@toreturn));
return { retval => 1, retmsg => 'Here comes the results', lines => \@toreturn };
}
sub parse {
my $self = shift;
my @result;
my $string = $self->fetch;
if($string->{'retval'}) {
foreach my $str(@{$string->{'lines'}}) {
my $r = $self->{'parser'}->parser($str);
if($r->{'retval'}) {
delete $r->{'retval'};
push(@result,$r);
}
}
return { retval => 1, retmsg => 'Here comes the results', lines => \@result };
}
return { retval => 0, retmsg => "ssh-fetcher returned an error: $string->{'retmsg'}" } if($string->{'error'});
return { retval => 0 }; # nothing to return, nothing to say:)
}
1;

178
lib/ssh_parser.pm Normal file
View File

@ -0,0 +1,178 @@
package My::parser::ssh_parser;
use strict;
use warnings;
sub new {
my $class = shift;
my $self = {};
bless ($self, $class);
return $self;
}
sub parser {
my $self = shift;
my $string = shift;
my ($reply,$hostile,$host) = ('',0,'');
my $re_host = qr/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/;
my $re_user = qr/[\w\d_\-.]*/;
if($string =~ m/Accepted publickey for/) {
$reply = 'Accept publickey';
} elsif($string =~ m/Received disconnect from $re_host port [0-9]{1,5}:11: disconnected by user/) {
$reply = 'Normal disconnect';
} elsif($string =~ m/Failed unknown for (invalid user |)$re_user from $re_host port [0-9]{1,5} ssh2/) {
$reply = 'Log spam, also logged as a failure';
} elsif($string =~ m/Disconnected from user $re_user $re_host port [0-9]{1,5}$/) {
$reply = 'Normal disconnect';
} elsif($string =~ m/Did not receive identification string from /) {
$_ = $string;
$reply = 'No identification string';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/User $re_user from $re_host not allowed because /) {
$_ = $string;
$reply = 'Blocked user';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/(i|I)nvalid user .* from $re_host port/) {
$_ = $string;
$reply = 'Invalid user';
$hostile = 1;
PARSER:
m/(from\ ($re_host)) /gcix && do {
$host = $2;
};
} elsif($string =~ m/(Disconnecting|Received disconnect from|Disconnected from|Connection closed by|Connection reset by) (authenticating |invalid |)(user .* |)$re_host port [0-9]{1,6}.*\[preauth\]/) {
$_ = $string;
$reply = 'Received disconnect';
$hostile = 1;
PARSER:
m/\ ($re_host)\ /gcix && do {
$host = $1;
};
} elsif($string =~ m/refused connect from .* \($re_host\)/) {
$_ = $string;
$reply = 'Blocked by tcpwrappers';
$hostile = 1;
PARSER:
m/ \(($re_host)\) /gcix && do {
$host = $1;
};
} elsif($string =~ m/error: maximum authentication attempts exceeded for (invalid user |)$re_user from ($re_host) port [0-9]{1,6} (ssh2 |)\[preauth\]/) {
$_ = $string;
$reply = 'Auth attempt limit exceeded';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/fatal: Unable to negotiate with/) {
$_ = $string;
$reply = 'Unable to negotiate';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/Bad protocol version identification/) {
$_ = $string;
$reply = 'Bad protocol';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/Could not write ident string to/) {
$_ = $string;
$reply = 'Could not write ident string';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/Disconnecting (authenticating|invalid) user.*Change of username or service not allowed/) {
$_ = $string;
$reply = "Change of username or service";
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/Failed publickey for $re_user from $re_host port [\d]{1,6}.*/) {
$_ = $string;
$reply = 'Failed publickey';
$hostile = 1;
PARSER:
m/\ ($re_host)\ /gcix && do {
$host = $1;
};
} elsif($string =~ m/ssh_dispatch_run_fatal/) {
$_ = $string;
$reply = 'ssh dispatch fatal';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/Unable to negotiate with $re_host port/) {
$_ = $string;
$reply = 'Unable to negotiate. Weak key exchange';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/Protocol major versions differ for/) {
$_ = $string;
$reply = 'Protocol major versions differ';
$hostile = 1;
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/Unable to negotiate with .* no matching MAC found/) {
$host = '';
$reply = 'no matching MAC, ancient client trying to connect';
} elsif($string =~ m/\/etc\/hosts\.allow/) {
$host = '';
} elsif($string =~ m/Disconnecting: Too many authentication failures/) {
$host = '';
} elsif($string =~ m/input_userauth_request: invalid user.*\[preauth\]/) {
$host = '';
} elsif($string =~ m/user $re_user login class/) {
$host = '';
$reply = 'Useless log info';
} elsif($string =~ m/(Disconnected|Connection closed) (from|by) (invalid|) user $re_user $re_host port [0-9]{1,6} \[preauth\]/) {
$host = '';
$reply = 'Log info';
} elsif($string =~ m/Fssh_kex_exchange_identification/) {
$hostile = 0;
$reply = 'kex exchange identification problem';
} elsif($string =~ m/fatal: Timeout before authentication for $re_host port [0-9]{1,6}/) {
$_ = $string;
$hostile = 1;
$reply = 'Timeout before auth';
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} elsif($string =~ m/banner exchange: Connection from $re_host port [0-9]{1,6}: invalid format/) {
$_ = $string;
$hostile = 1;
$reply = 'Invalid format during banner exchange';
PARSER:
m/ ($re_host) /gcix && do {
$host = $1;
};
} else {
$reply = 'No match for '.$string;
}
return { retval => 1, retmsg => $reply, hostile => $hostile, host => $host, string => $string };
}
1;

90
lib/stats.pm Normal file
View File

@ -0,0 +1,90 @@
package My::parser::stats;
use strict;
use warnings;
use DBI;
sub new {
my $class = shift;
my $config = shift;
my $self = {};
bless ($self, $class);
$self->{'config'} = $config;
my $short_time = $config->get_as_single_val('config','short') || die "Failed to get short value";
my $long_time = $config->get_as_single_val('config','long') || die "Failed to get long value";
$self->{'short'} = $short_time;
$self->{'long'} = $long_time;
return $self;
}
sub recent_hostile {
my $self = shift;
$self->{'dbh'} = $self->{'config'}->get_dbh unless($self->{'dbh'});
my $sth = $self->{'dbh'}->prepare("
SELECT COUNT(1)
FROM reject
WHERE time > DATE_SUB(NOW(), INTERVAL 5 MINUTE)")
|| return { retval => 0, retmsg => 'Failed checking for rejects last 5 minutes: '.DBI::errstr };
$sth->execute() || return { retval => 0, retmsg => 'Failed execute on checking for rejects last 5 minutes: '.DBI::errstr };
my $rows = $sth->fetchrow_arrayref->[0];
return { retval => 1, rows => $rows, retmsg => $rows.' the last 5 minutes' };
}
sub checker {
my $self = shift;
my $tocheck = shift;
my $host = $tocheck->{'host'};
my $asn = $tocheck->{'asn'};
my $iso = $tocheck->{'iso'};
unless($self && $host) {
return { retval => 0, retmsg => "Too few variables to run My::parser::stats->checker, got host($host), asn($asn), iso($iso)" };
}
my $retval;
my $rows;
my $msg;
my $return = { retval => 1 };
my @times = ($self->{'short'},$self->{'long'});
#### Mapping between checks and tables in db
my %checks = ('reject' => 'reject','blocks' => 'reject_iptables','iso' => 'reject','asn' => 'reject', 'block_iso' => 'reject_iptables', 'block_asn' => 'reject_iptables');
#### Mapping between checks and column in db
my %values = ('reject' => 'ip','blocks' => 'ip', 'iso' => 'iso', 'asn' => 'asn', 'block_iso' => 'iso', 'block_asn' => 'asn');
my %keys = ('reject' => $host, 'blocks' => $host, 'iso' => $iso, 'block_iso' => $iso, 'asn' => $asn, 'block_asn' => $asn);
$self->{'dbh'} = $self->{'config'}->get_dbh unless($self->{'dbh'});
foreach my $time(@times) {
foreach my $c(keys %checks) {
my $fromcheck = $self->check($keys{$c},$time,$values{$c},$checks{$c});
if($fromcheck->{'retval'}) {
my $temp = $fromcheck->{'rows'};
$return->{$time}->{$c} = $temp;
# } else {
# $self->{'config'}->{'logger'}->log("Check failed: $fromcheck->{'retmsg'}");
}
}
}
return $return;
}
sub check {
my $self = shift;
my $host = shift;
my $allowed_time = shift;
my $value = shift;
my $table = shift;
my $retval;
my $rows;
my $msg;
if($host && $allowed_time && $value && $table) {
my $sth = $self->{'dbh'}->prepare("
SELECT COUNT(1)
FROM $table
WHERE $value = ? AND
time > DATE_SUB(NOW(), INTERVAL ? MINUTE)")
|| return { retval => 0, retmsg => "Checking $host in $table for $value in the last $allowed_time minutes failed: ".DBI::errstr };
$sth->execute($host,$allowed_time) || return { retval => 0, retmsg => "Execute failed on $table, checking for $host in $allowed_time: ".DBI::errstr };
my $rows = $sth->fetchrow_arrayref->[0];
return { retval => 1, rows => $rows, retmsg => "$rows in $allowed_time minutes" };
} else {
return { retval => 0, retmsg => "Check called with too few arguments, we got host: $host, allowed_time: $allowed_time, value: $value, table: $table" };
}
}
1;