#!/usr/local/bin/perl ################################################################### # # Copyright 2001, Brandon Gillespie # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # ################################################################### # VERSION: 1.0 # # Usage: tail -f snortlog/alert | perl pigsentry # # Notes: # # Currently only works on snort full format (-A full), blearg # Grr is my hero, watch Invader Zim on Nick (Awww, I wanted to explode) # use POSIX qw(mktime); ################################################################### ## stuff you can change $trend_threshold_warn = 1.2; # warn on a 120% spike over normal $trend_poll = 300; # how many secs between trend intervals $trend_retention = 3*12; # how many intervals to keep in trend $state_checkpoint_interval = 300; # how often to checkpoint state table $state_expire_time = 86400; # when to expire data from state table $state_file = "/tmp/pigstate"; # state file ### regexp against the msg, to skip $ignore_rx = q/(fragments discarded|source quench)/; ### notify hook, if defined will call this, otherwise will print to STDOUT # $notify_hook = undef; $notify_hook = sub { print ("[" . localtime() . "] " . $_[0] . "\n"); }; ################################################################### ## stuff you shouldn't change $last_state_checkpoint = 0; $last_state_check = 0; %alerts = (); $SIG{INT} = 'store_and_exit'; $SIG{QUIT} = 'store_and_exit'; $SIG{TERM} = 'store_and_exit'; &load_state(); &watch_log(STDIN); ################################################################### # => watch_log(fd): core engine, reads from log # # As time allows, calls check_state which updates trends and # expires entries. Also calls process_alert for each alert sub watch_log { my ($alert) = @_; my $buffer = ""; my $dohead = 0; # fragment checking, incase there was a tail -f while (<$alert>) { chomp; if (/^\[\*\*\]/) { $new = $_; if (length($buffer) && $dohead) { &process_alert($buffer); &check_state(); } $dohead=1; $buffer = $new; } else { $buffer .= "\f$_"; } } &process_alert($buffer); } ############################ # => load_state(): called at initialization to load stored state info # # initialize %alerts dictionary, in all its perly madness sub load_state { if (-f $state_file) { if (!open(STATE, $state_file)) { &error("Cannot open state file '$state_file': $!"); } else { while () { chomp; my @ary; my ($t, @rest) = split(/\t/); if ($t eq "a") { ($last,$dtot,$ltot,$avg,$lavg,$a,@ary) = @rest; $alerts{$a} = {"last" => $last, "dtot" => $dtot, "ltot" => $ltot, "avg" => $avg, "q" => \@ary, "lavg" => $lavg}; } } close(STATE); } } } ############################ # => store_state(): write out %alerts dictionary to disk # sub store_state { if (!open(STATE, ">$state_file")) { &error("Unable to open state file '>$state_file': $!"); } else { for $k (keys %alerts) { print STATE ("a\t" . $alerts{$k}->{"last"} . "\t" . $alerts{$k}->{"dtot"} . "\t" . $alerts{$k}->{"ltot"} . "\t" . $alerts{$k}->{"avg"} . "\t" . $alerts{$k}->{"lavg"} . "\t$k\t"); my $ref = $alerts{$k}->{'q'}; print STATE join("\t", @$ref); print STATE "\n"; } $last_state_checkpoint = time(); close(STATE); } } ############################ # => store_and_exit(): call store_state() then exit # sub store_and_exit { print "Storing state..."; &store_state(); print "goodbye\n"; exit(0); } ############################ # => error(msg): we had a booboo # sub error { print ("ERROR: " . $_[0] . "\n"); } ############################ # => notify(msg): send a notice about an event or trend # sub notify { if ($notify_hook) { &$notify_hook(@_); } else { print ("PigSentry: [" . localtime() . "] " . $_[0] . "\n"); } } ############################ # => check_state(): manages trends, alert expiration and checkpoints # # This is called at random times, and while there are defined minimum # intervals for various things, it is not guaranteed of there are no # new alerts coming out of snort. sub check_state { my $k; if ((time() - $last_state_checkpoint) > $state_checkpoint_interval) { &store_state(); } if ((time() - $last_state_check) < 60) { return; } $last_state_check = time(); for $k (keys (%alerts)) { if ((time() - $alerts{$k}->{'last'}) > $state_expire_time) { ## should it be expired? ¬ify("State Expire: $k"); delete($alerts{$k}); } elsif ((time() - $alerts{$k}->{'lavg'}) > $trend_poll) { ## should we figure a new average? $alerts{$k}->{'lavg'} = time(); $lref = $alerts{$k}->{'q'}; while (($#$lref+1) > ($trend_retention-1)) { shift(@$lref); } my $last = $alerts{$k}->{'dtot'} - $alerts{$k}->{'ltot'}; $alerts{$k}->{'ltot'} = $alerts{$k}->{'dtot'}; push(@$lref, $last); if (($#$lref+1) == $trend_retention) { my $increase = 0; my $median = $alerts{$k}->{'avg'}; if ($last > ($median * $trend_threshold_warn)) { if ($median > 0) { $increase = (int((($last - $median)/$median)*100) . "%"); } else { $increase = $last; } ¬ify("Trend increase of $increase for $k"); } } my $sum = 0; my $p; for $p (@$lref) { $sum += $p; } my $navg = $sum / ($#$lref+1); $alerts{$k}->{'avg'} = $navg; } } } ############################ # => add_to_state(msg,time,date,year): update an alert in the state table # # if the alert does not exist, then create a new data struct for it, # otherwise update the total and last time # requires POSIX:mktime sub add_to_state { my ($msg, $time, $date, $year) = @_; my ($h, $m, $s) = split(/:/, $time); my ($mon, $day) = split(/\//, $date); $t = mktime($s, $m, $h, $day, $mon-1, $year); if (!exists($alerts{$msg})) { ¬ify("New event: $msg"); my @ary = (); $alerts{$msg} = {"last" => $t, "dtot" => 1, "lavg" => 0, "q" => \@ary, "ltot" => 0, "avg" => 0}; } else { $alerts{$msg}->{"last"} = $t; $alerts{$msg}->{"dtot"}++; } } ############################ # => proc__ hooks, called by process_alert for specific known alert types # # mostly these are disregarded, but could be used in the future sub proc__portscan { } # => proc__miscalert: general alert, calls add_to_state() sub proc__miscalert { my ($msg, @rest) = @_; $omsg = $msg; $msg =~ s/\s*\[\*\*\]\s*//g; $msg =~ s/\s*\[([\d:]+)\]\s*//; $msg =~ s/^\s//; $id = $1; if ($msg eq "") { ## An alert fragment, ignore return; } my $cline = ""; my $sline = ""; my $pline = ""; if ($rest[0] =~ /\[Classification:/ || $rest[0] =~ /\[Priority/) { $cline = shift(@rest); } $sline = shift(@rest); $pline = shift(@rest); if ($sline =~ /([\d+\/]+)-([\d:]+)\.(\d+) ([\d:.]+) -> ([\d:.]+)/) { ($date, $time, $subtime, $from, $to) = ($1,$2,$3,$4,$5); } $proto = (split(/\s/, $pline))[0]; if (length($to)) { $detail = "$proto $from -> $to"; $to =~ s/:\d+$//; } &add_to_state($msg, $time, $date, (localtime())[5]); } ############################ # => process_alert(alert): chew on an alert and call the appropriate sub sub process_alert { my ($buf) = @_; my ($msg, @rest) = split(/\f/m, $buf); if ($msg =~ /$ignore_rx/) { # stuff to skip } elsif ($msg =~ /spp_portscan/) { &proc__portscan($msg, @rest); } else { &proc__miscalert($msg, @rest); } }