#!/usr/bin/perl -w # $Id$ # # ---------------------------------------------------------------------- # Copyright (c) 2005 Novell, Inc. All Rights Reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of version 2 of the GNU General Public # License as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, contact Novell, Inc. # # To contact Novell about this file by physical or electronic mail, # you may find current contact information at www.novell.com. # ---------------------------------------------------------------------- use strict; use Data::Dumper; use Date::Parse; use DBI; use Fcntl; use File::Temp qw(tempfile); use Getopt::Long; use POSIX 'setsid'; use Time::Local; use File::Tail; use Immunix::Severity; ########################################################################## # locations my $productname = "apparmor"; my $cfgdir = "/etc/$productname"; my $dbdir = "/var/log/$productname"; my $cfgfile = "$cfgdir/notify.cfg"; my $errlog = "$dbdir/event-dispatch.log"; my $logfile = "/var/log/audit/audit.log"; my $syslogfile = "/var/log/messages"; ########################################################################## # options variables my $pidfile = ''; GetOptions( 'pidfile|p=s' => \$pidfile ); my $DEBUG = 0; my $config; my $verbose = { last_notify => 0 }; my $summary = { last_notify => 0 }; my $terse = { last_notify => 0 }; # we don't want to call str2time on every line and also batch up event dbs # a month at a time, so we need to keep track of a few extra things my $timestamp = 0; my $lasttime = ""; my $counter = 0; my $thismonth = 0; my $nextmonth = 0; # pop open a connection to the severity database my $sevdb = new Immunix::Severity("$cfgdir/severity.db", -1); my $REdate = '\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}'; my $last_inserted_time; my $last_inserted_counter; ########################################################################## # commit how often? my $timeout = 5; # keep track of when we commited last my $last_flush_time = 0; # keep track of some statistics my $max = 0; my $inserts = 0; my $total = 0; my @commit_buffer; my @verbose_buffer; my @summary_buffer; my @terse_buffer; my %templates = ( "path" => "(time,counter,type,profile,sdmode,mode,resource,prog,pid,severity) VALUES(?,?,'path',?,?,?,?,?,?,?)", "link" => "(time,counter,type,profile,sdmode,resource,target,prog,pid,severity) VALUES(?,?,'link',?,?,?,?,?,?,?)", "chattr" => "(time,counter,type,profile,sdmode,resource,mode,prog,pid,severity) VALUES(?,?,'chattr',?,?,?,?,?,?,?)", "capability" => "(time,counter,type,profile,sdmode,resource,prog,pid,severity) VALUES(?,?,'capability',?,?,?,?,?,?)", "unknown_hat" => "(time,counter,type,profile,sdmode,resource,pid) VALUES(?,?,'unknown_hat',?,?,?,?)", "fork" => "(time,counter,type,profile,sdmode,pid,resource) VALUES(?,?,'fork',?,?,?,?)", "changing_profile" => "(time,counter,type,profile,sdmode,pid) VALUES(?,?,'changing_profile',?,?,?)", "profile_replacement" => "(time,counter,type,profile,sdmode,prog,pid,severity) VALUES(?,?,'profile_replacement',?,?,?,?,?)", "removed" => "(time,counter,type,severity) VALUES(?,?,'removed',?)", "initialized" => "(time,counter,type,resource,severity) VALUES(?,?,'initialized',?,?)", "ctrl_var" => "(time,counter,type,resource,mode,severity) VALUES(?,?,'ctrl_var',?,?,?)", ); ########################################################################## # generic functions sub errlog ($) { my $mesg = shift; my $localtime = localtime(time); print ERRLOG "[$localtime] $mesg\n"; } sub readconfig () { my $cfg = { }; # record when we read the config file $cfg->{load_time} = time; if(open(CFG, $cfgfile)) { # yank in the values we need while () { $cfg->{$1} = $2 if /^(\S+)\s+(.+)\s*$/; } close(CFG); } return $cfg; } sub daemonize { chdir '/' or die "Can't chdir to /: $!"; open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!"; defined(my $pid = fork) or die "Can't fork: $!"; exit if $pid; setsid or die "Can't start a new session: $!"; open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; } ########################################################################## # database handling functions sub connect_database ($) { my $dbdir = shift; my $dbh = DBI->connect("dbi:SQLite:dbname=$dbdir/events.db","",""); # we'll do the commits ourselves so performance doesn't suck $dbh->{AutoCommit} = 0; # bump up our cache size a little $dbh->do("PRAGMA cache_size = 20000;"); # figure out if the tables already exist or not my %existing_tables; my $sth = $dbh->prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"); $sth->execute; while(my @row = $sth->fetchrow_array) { $existing_tables{$row[0]} = 1; } $sth->finish; # create the info table and fill in the appropriate values for this db unless($existing_tables{info}) { my $host = `hostname -f`; chomp $host; $dbh->do("CREATE TABLE info (name,value)"); $sth = $dbh->prepare("INSERT INTO info(name,value) VALUES(?,?)"); $sth->execute("version", "0.1"); $sth->execute("host", "$host"); } # create the events table unless($existing_tables{events}) { $dbh->do("CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, time INTEGER NOT NULL, counter INTEGER NOT NULL, pid, sdmode, type, mode, resource, target, profile, prog, severity INTEGER )"); # set up the indexes we want my @indexes = qw(time type sdmode mode resource profile prog severity); for my $index (@indexes) { $dbh->do("CREATE INDEX " . $index . "_idx ON events($index)"); } } # make sure our changes actually get saved $dbh->commit || errlog "Error commiting changes: $!"; # mark the db as up to date as of now $last_flush_time = time; return $dbh; } ########################################################################## sub verbose_notify_handler { my ($email, $file, $last, $level, $unknown) = @_; $last = localtime($last); my $now = time; my $host = `hostname -f`; chomp $host; my $subj = "Verbose Security Report for $host."; my $mesg = "The following security events occured since $last:\n\n"; my @events; if(open(V, $file)) { while() { chomp; if(/^(\d+) (\d+) (.+)$/) { my ($timestamp, $counter, $logmsg) = ($1, $2, $3); push @events, [ $timestamp, $counter ]; $mesg .= "$logmsg\n"; } } close(V); if(@events) { if($DEBUG) { my $count = scalar @events; errlog "[$count events] sending verbose notification to $email."; } # actually send out the notification... open(MAIL, "| sendmail -F 'AppArmor Security Notification' $email"); print MAIL "To: $email\n"; print MAIL "Subject: $subj\n\n"; print MAIL "$mesg\n"; print MAIL ".\n"; close(MAIL); } # delete the verbose notification logfile once we've processed it unlink($file); } } sub summary_notify_handler { my ($email, $file, $last, $level, $unknown) = @_; $last = localtime($last); my $now = time; my $host = `hostname -f`; chomp $host; my $subj = "Summary Security Report for $host."; my $mesg = "The following security events occured since $last:\n\n"; my @events; if(open(V, $file)) { while() { chomp; if(/^(\d+) (\d+) (.+)$/) { my ($timestamp, $counter, $logmsg) = ($1, $2, $3); push @events, [ $timestamp, $counter ]; $mesg .= "$logmsg\n"; } } close(V); if(@events) { if($DEBUG) { my $count = scalar @events; errlog "[$count events] sending summary notification to $email."; } # actually send out the notification... open(MAIL, "| sendmail -F 'AppArmor Security Notification' $email"); print MAIL "To: $email\n"; print MAIL "Subject: $subj\n\n"; print MAIL "$mesg\n"; print MAIL ".\n"; close(MAIL); } # delete the verbose notification logfile once we've processed it unlink($file); } } sub terse_notify_handler { my ($email, $file, $last, $level, $unknown) = @_; $last = localtime($last); my $now = time; my $host = `hostname -f`; chomp $host; my @events; my $count = 0; if(open(V, $file)) { while() { chomp; if(/^(\d+) (\d+) (.+)$/) { my ($timestamp, $counter, $logmsg) = ($1, $2, $3); push @events, [ $timestamp, $counter ]; $count++; } } close(V); if($count) { if($DEBUG) { errlog "[$count events] sending terse notification to $email."; } my $subj = "Security Report for $host."; my $mesg = "$host has had $count security events since $last."; # actually send out the notification... open(MAIL, "| sendmail -F 'AppArmor Security Notification' $email"); print MAIL "To: $email\n"; print MAIL "Subject: $subj\n\n"; print MAIL "$mesg\n"; print MAIL ".\n"; close(MAIL); } # delete the terse notification logfile once we've processed it unlink($file); } } sub fork_into_background { my ($name, $func, @args) = @_; my $pid = fork; if(not defined $pid) { # something bad happened, just log it... errlog "couldn't fork for \"$name\": $!" } elsif($pid == 0) { # we're in the child process now... # set our process name $0 = $name; # call our subroutine my $ret = &$func(@args); exit($ret); } return $pid; } ########################################################################## sub process_event ($$) { my $dbh = shift; my $logmsg = shift; my $sth; my ($time, $mesg); if($logmsg =~ /^(?:type=APPARMOR msg=|$REdate\s+\S+\s+(?:kernel:\s+)*)audit\((\d+).\d+:\d+\): (.+)$/) { ($time, $mesg) = ($1, $2); # have we rolled over to another second yet? if($time ne $lasttime) { $counter = 0; $timestamp = $time; $lasttime = $time; } } elsif ($logmsg =~ /^\s*($REdate)\s+\S+\s+(?:kernel:\s+)*(SubDomain|AppArmor):\s+(.+)$/) { ($time, $mesg) = ($1, $3); # have we rolled over to another second yet? if($time ne $lasttime) { $counter = 0; $timestamp = str2time($time); $lasttime = $time; } } else { # not one of ours, just return return; } $counter++; # some statistics... $max = $counter if $counter > $max; # if we already have events in the db, make sure we don't try to re-enter # duplicates if we start up again and parse the same logfile over again if($last_inserted_time) { return if $timestamp < $last_inserted_time; if($timestamp == $last_inserted_time) { return if $counter <= $last_inserted_counter; } $last_inserted_time = undef; } # workaround for syslog uglyness. if($mesg =~ s/(PERMITTING|REJECTING|AUDITING)-SYSLOGFIX/$1/) { $mesg =~ s/%%/%/g; } if($mesg =~ /(PERMITTING|REJECTING|AUDITING) (\S+) access to (.+?) \((\S+)\((\d+)\) profile (\S+) active (\S+)\)/) { my ($sdmode, $mode, $resource, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7); $profile .= "^$hat" if $profile ne $hat; my $severity = ""; if($sdmode eq "REJECTING") { $severity = $sevdb->rank($resource, $mode); # we only do notification for enforce mode events if($config->{verbose_freq}) { if(($severity >= $config->{verbose_level}) || (($severity == -1) && $config->{verbose_unknown})) { push @verbose_buffer, [ $timestamp, $counter, $logmsg ]; } } if($config->{summary_freq}) { if(($severity >= $config->{summary_level}) || (($severity == -1) && $config->{summary_unknown})) { push @summary_buffer, [ $timestamp, $counter, "path", $prog, $mode, $resource ]; } } if($config->{terse_freq}) { if(($severity >= $config->{terse_level}) || (($severity == -1) && $config->{terse_unknown})) { push @terse_buffer, [ $timestamp, $counter, "dummy" ]; } } } push @commit_buffer, [ "path", $timestamp, $counter, $profile, $sdmode, $mode, $resource, $prog, $pid, $severity ]; $inserts++; } elsif($mesg =~ /(PERMITTING|REJECTING|AUDITING) link access from (.+?) to (.+?) \((\S+)\((\d+)\) profile (\S+) active (\S+)\)/) { my ($sdmode, $link, $target, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7); $profile .= "^$hat" if $profile ne $hat; my $severity = ""; if($sdmode eq "REJECTING") { $severity = $sevdb->rank($target, "l"); # we only do notification for enforce mode events if($config->{verbose_freq}) { if(($severity >= $config->{verbose_level}) || (($severity == -1) && $config->{verbose_unknown})) { push @verbose_buffer, [ $timestamp, $counter, $logmsg ]; } } if($config->{summary_freq}) { if(($severity >= $config->{summary_level}) || (($severity == -1) && $config->{summary_unknown})) { push @summary_buffer, [ $timestamp, $counter, "link", $prog, $link, $target ]; } } if($config->{terse_freq}) { if(($severity >= $config->{terse_level}) || (($severity == -1) && $config->{terse_unknown})) { push @terse_buffer, [ $timestamp, $counter ]; } } } push @commit_buffer, [ "link", $timestamp, $counter, $profile, $sdmode, $link, $target, $prog, $pid, $severity ]; $inserts++; } elsif($mesg =~ /(PERMITTING|REJECTING|AUDITING) attribute \((\S*)\) change to (.+)? \((\S+)\((\d+)\) profile (\S+) active (\S+)\)/) { my ($sdmode, $attrch, $resource, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7); $profile .= "^$hat" if $profile ne $hat; my $severity = ""; if($sdmode eq "REJECTING") { $severity = $sevdb->rank($resource, "w"); # we only do notification for enforce mode events if($config->{verbose_freq}) { if(($severity >= $config->{verbose_level}) || (($severity == -1) && $config->{verbose_unknown})) { push @verbose_buffer, [ $timestamp, $counter, $logmsg ]; } } if($config->{summary_freq}) { if(($severity >= $config->{summary_level}) || (($severity == -1) && $config->{summary_unknown})) { push @summary_buffer, [ $timestamp, $counter, "attrch", $prog, $resource, $attrch ]; } } if($config->{terse_freq}) { if(($severity >= $config->{terse_level}) || (($severity == -1) && $config->{terse_unknown})) { push @terse_buffer, [ $timestamp, $counter ]; } } } push @commit_buffer, [ "chattr", $timestamp, $counter, $profile, $sdmode, $resource, $attrch, $prog, $pid, $severity ]; $inserts++; } elsif(m/(PERMITTING|REJECTING) (?:mk|rm)dir on (.+) \((\S+)\((\d+)\) profile (\S+) active (\S+)\)/) { my ($sdmode, $resource, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6); $profile .= "^$hat" if $profile ne $hat; my $mode = "w"; my $severity = ""; if($sdmode eq "REJECTING") { $severity = $sevdb->rank($resource, $mode); # we only do notification for enforce mode events if($config->{verbose_freq}) { if(($severity >= $config->{verbose_level}) || (($severity == -1) && $config->{verbose_unknown})) { push @verbose_buffer, [ $timestamp, $counter, $logmsg ]; } } if($config->{summary_freq}) { if(($severity >= $config->{summary_level}) || (($severity == -1) && $config->{summary_unknown})) { push @summary_buffer, [ $timestamp, $counter, "path", $prog, $mode, $resource ]; } } if($config->{terse_freq}) { if(($severity >= $config->{terse_level}) || (($severity == -1) && $config->{terse_unknown})) { push @terse_buffer, [ $timestamp, $counter, "dummy" ]; } } } push @commit_buffer, [ "path", $timestamp, $counter, $profile, $sdmode, $mode, $resource, $prog, $pid, $severity ]; $inserts++; } elsif(/(PERMITTING|REJECTING) xattr (\S+) on (.+) \((\S+)\((\d+)\) profile (\S+) active (\S+)\)/) { my ($sdmode, $xattr_op, $resource, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7); $profile .= "^$hat" if $profile ne $hat; my $mode; if($xattr_op eq "get" || $xattr_op eq "list") { $mode = "r"; } elsif($xattr_op eq "set" || $xattr_op eq "remove") { $mode = "w"; } my $severity = ""; if($sdmode eq "REJECTING") { $severity = $sevdb->rank($resource, $mode); # we only do notification for enforce mode events if($config->{verbose_freq}) { if(($severity >= $config->{verbose_level}) || (($severity == -1) && $config->{verbose_unknown})) { push @verbose_buffer, [ $timestamp, $counter, $logmsg ]; } } if($config->{summary_freq}) { if(($severity >= $config->{summary_level}) || (($severity == -1) && $config->{summary_unknown})) { push @summary_buffer, [ $timestamp, $counter, "path", $prog, $mode, $resource ]; } } if($config->{terse_freq}) { if(($severity >= $config->{terse_level}) || (($severity == -1) && $config->{terse_unknown})) { push @terse_buffer, [ $timestamp, $counter, "dummy" ]; } } } push @commit_buffer, [ "path", $timestamp, $counter, $profile, $sdmode, $mode, $resource, $prog, $pid, $severity ]; $inserts++; } elsif($mesg =~ /(PERMITTING|REJECTING|AUDITING) access to capability '(.+?)' \((\S+)\((\d+)\) profile (\S+) active (\S+)\)/) { my ($sdmode, $capability, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7); $profile .= "^$hat" if $profile ne $hat; my $severity = ""; if($sdmode eq "REJECTING") { $severity = $sevdb->rank(uc("cap_$capability")); # we only do notification for enforce mode events if($config->{verbose_freq}) { if(($severity >= $config->{verbose_level}) || (($severity == -1) && $config->{verbose_unknown})) { push @verbose_buffer, [ $timestamp, $counter, $logmsg ]; } } if($config->{summary_freq}) { if(($severity >= $config->{summary_level}) || (($severity == -1) && $config->{summary_unknown})) { push @summary_buffer, [ $timestamp, $counter, "capability", $prog, $capability ]; } } if($config->{terse_freq}) { if(($severity >= $config->{terse_level}) || (($severity == -1) && $config->{terse_unknown})) { push @terse_buffer, [ $timestamp, $counter ]; } } } push @commit_buffer, [ "capability", $timestamp, $counter, $profile, $sdmode, $capability, $prog, $pid, $severity ]; $inserts++; } elsif($mesg =~ /LOGPROF-HINT unknown_hat (\S+) pid=(\d+) profile=(\S+) active=(\S+)/) { my ($uhat, $pid, $profile, $hat) = ($1, $2, $3, $4); $profile .= "^$hat" if $profile ne $hat; push @commit_buffer, [ "unknown_hat", $timestamp, $counter, $profile, "PERMITTING", $uhat, $pid ]; $inserts++; } elsif($mesg =~ /LOGPROF-HINT fork pid=(\d+) child=(\d+) profile=(\S+) active=(\S+)/) { my ($pid, $child, $profile, $hat) = ($1, $2, $3, $4); $profile .= "^$hat" if $profile ne $hat; push @commit_buffer, [ "fork", $timestamp, $counter, $profile, "PERMITTING", $pid, $child ]; $inserts++; } elsif($mesg =~ /LOGPROF-HINT changing_profile pid=(\d+) newprofile=(\S+)/) { my ($pid, $newprofile) = ($1, $2); push @commit_buffer, [ "changing_profile", $timestamp, $counter, $newprofile, "PERMITTING", $pid ]; $inserts++; } elsif($mesg =~ /LOGPROF-HINT fork pid=(\d+) child=(\d+)/) { my ($pid, $child) = ($1, $2); push @commit_buffer, [ "fork", $timestamp, $counter, "null-complain-profile", "PERMITTING", $pid, $child ]; $inserts++; } elsif($mesg =~ /LOGPROF-HINT changing_profile pid=(\d+)/) { my $pid = $1; push @commit_buffer, [ "changing_profile", $timestamp, $counter, "null-complain-profile", "PERMITTING", $pid ]; $inserts++; } elsif($mesg =~ /(PERMITTING|REJECTING|AUDITING) access to profile replacement \((\S+)\((\d+)\) profile (\S+) active (\S+)\)/) { my ($sdmode, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7); $profile .= "^$hat" if $profile ne $hat; my $severity = 10; # we only do notification for enforce mode events if($config->{verbose_freq}) { if(($severity >= $config->{verbose_level}) || (($severity == -1) && $config->{verbose_unknown})) { push @verbose_buffer, [ $timestamp, $counter, $logmsg ]; } } if($config->{summary_freq}) { if(($severity >= $config->{summary_level}) || (($severity == -1) && $config->{summary_unknown})) { push @summary_buffer, [ $timestamp, $counter, "profile_replacement", $prog ]; } } if($config->{terse_freq}) { if(($severity >= $config->{terse_level}) || (($severity == -1) && $config->{terse_unknown})) { push @terse_buffer, [ $timestamp, $counter ]; } } push @commit_buffer, [ "profile_replacement", $timestamp, $counter, $profile, $sdmode, $prog, $pid, $severity ]; $inserts++; } elsif($mesg =~ /(SubDomain|AppArmor) protection removed/) { push @commit_buffer, [ "removed", $timestamp, $counter, 10 ]; $inserts++; } elsif($mesg =~ /(SubDomain|AppArmor) \(version (\S+)\) initialized/) { my $version = $1; push @commit_buffer, [ "initialized", $timestamp, $counter, $version, 10 ]; $inserts++; } elsif($mesg =~ /Control variable '(\S+)' changed to (\S+)/) { my ($variable, $value) = ($1, $2); push @commit_buffer, [ "ctrl_var", $timestamp, $counter, $variable, $value, 10 ]; $inserts++; } else { errlog "Unhandled log message: $logmsg"; } } sub dump_events { my ($which, @events) = @_; if($DEBUG) { my $count = scalar @events; errlog "dumping $count events to $which db."; } if(open(F, ">>$dbdir/$which.db")) { for my $event (@events) { my @event = @$event; print F "@event\n"; } close(F); } else { errlog "can't write to $dbdir/$which.db: $!"; } } sub check_timers ($) { my $dbh = shift; # what time is it right... NOW my $now = time; # make sure we commit periodically if(($inserts > 10000) || ($now >= ($last_flush_time + $timeout))) { my $last_prepare = ""; my $sth; for my $event ( sort { $a->[0] cmp $b->[0] } @commit_buffer ) { my @event = @{$event}; my $type = shift @event; if($type ne $last_prepare) { $sth = $dbh->prepare("INSERT INTO events $templates{$type};"); $last_prepare = $type; } $sth->execute(@event); } $dbh->commit || errlog "Error commiting changes: $!"; # need to get the time again to include how much time it takes to # actually write all this crap to the db $now = time; if($DEBUG && $inserts) { $total += $inserts; my $delta = $now - $last_flush_time; my $rate = int($inserts / $delta); errlog "$rate/s $inserts in ${delta}s total=$total max=$max"; } $last_flush_time = $now; @commit_buffer = ( ); $max = 0; $inserts = 0; if(@verbose_buffer) { # if we've got verbose events, dump them dump_events("verbose", @verbose_buffer); # and clear out our buffer @verbose_buffer = ( ); } if(@terse_buffer) { # if we've got terse events, dump them dump_events("terse", @terse_buffer); # and clear out our buffer @terse_buffer = ( ); } # bail out if we don't have notification configured return unless -f $cfgfile; # what time did we last read the config file? my $load_time = $config->{load_time}; # check when the config file was last modified... my $mtime = (stat($cfgfile))[9]; # if it's been changed since we last read the config file, we need to # load the new settings if($load_time < $mtime) { errlog "Reloading changed config file."; $config = readconfig(); } } # bail out if we don't have notification configured return unless -f $cfgfile; if($config->{terse_freq}) { if(($terse->{last_notify} + $config->{terse_freq}) <= $now) { if(-f "$dbdir/terse.db") { $DEBUG && errlog "doing terse notification..."; # get a temporary filename... my ($fh, $filename) = tempfile("terseXXXXXX", DIR => $dbdir); # overwrite the temp file we just created... rename("$dbdir/terse.db", $filename); if($DEBUG) { errlog "terse file is $filename"; } # do the actual notification in the background fork_into_background("terse-notification", \&terse_notify_handler, $config->{terse_email}, $filename, $terse->{last_notify}, $config->{terse_level}, $config->{terse_unknown}); # ...keep track of when we last sent out a notify $terse->{last_notify} = $now; } } } if($config->{summary_freq}) { if(($summary->{last_notify} + $config->{summary_freq}) <= $now) { if(-f "$dbdir/summary.db") { $DEBUG && errlog "doing summary notification..."; # get a temporary filename... my ($fh, $filename) = tempfile("summaryXXXXXX", DIR => $dbdir); # overwrite the temp file we just created... rename("$dbdir/summary.db", $filename); # do the actual notification in the background fork_into_background("summary-notification", \&summary_notify_handler, $config->{summary_email}, $filename, $summary->{last_notify}, $config->{summary_level}, $config->{summary_unknown}); # ...keep track of when we last sent out a notify $summary->{last_notify} = $now; } } } if($config->{verbose_freq}) { if(($verbose->{last_notify} + $config->{verbose_freq}) <= $now) { if(-f "$dbdir/verbose.db") { $DEBUG && errlog "doing verbose notification..."; # get a temporary filename... my ($fh, $filename) = tempfile("verboseXXXXXX", DIR => $dbdir); # overwrite the temp file we just created... rename("$dbdir/verbose.db", $filename); if($DEBUG) { errlog "verbose file is $filename"; } # do the actual notification in the background fork_into_background("verbose-notification", \&verbose_notify_handler, $config->{verbose_email}, $filename, $verbose->{last_notify}, $config->{verbose_level}, $config->{verbose_unknown}); # ...keep track of when we last sent out a notify $verbose->{last_notify} = $now; } } } } sub get_last_event { my $dbh = shift; my ($time, $counter); # get the oldest timestamp... my $sth = $dbh->prepare('SELECT MAX(time) FROM events'); $sth->execute; my @row = $sth->fetchrow_array || ( 0 ); $time = $row[0]; if($time) { # get the highest counter for this timestamp... $sth = $dbh->prepare("SELECT MAX(counter) FROM events WHERE time = $time"); $sth->execute; @row = $sth->fetchrow_array || ( 0 ); $counter = $row[0]; } return ($time, $counter); } ########################################################################## # start the real magic... my $finished; # make sure we exit if someone sends us the right signal sub sig_handler { my $signame = shift; errlog("Caught signal '$signame'. Exiting..."); $finished = 1; }; # set up our error log without buffering open(ERRLOG, ">>$dbdir/event-dispatch.log"); my $oldfd = select(ERRLOG); $| = 1; select($oldfd); errlog "Starting..."; $config = readconfig(); # fork off into the background. we need to do this before we connect to # the db, otherwise, we'll get an ugly error about rolling back a # connection that's being destroyed daemonize; # automagically reap child processes $SIG{INT} = \&sig_handler; $SIG{TERM} = \&sig_handler; $SIG{CHLD} = 'IGNORE'; # if they want us to write a pid, do it if($pidfile) { if(open(PIDFILE, ">$pidfile")) { print PIDFILE "$$\n"; close(PIDFILE); } } my $dbh = connect_database($dbdir); ($last_inserted_time, $last_inserted_counter) = get_last_event($dbh); my $auditlog=File::Tail->new(name=>$logfile, debug=>1, interval=>1, maxinterval=>5, adjustafter=>20, errmode=>"return", ignore_noexistant=>1); my $syslog=File::Tail->new(name=>$syslogfile, debug=>1, interval=>1, maxinterval=>5, adjustafter=>20, errmode=>"return", ignore_noexistant=>1); my $line = ''; # process complete lines from the buffer... while (not $finished) { my ($nfound, $timeleft, @pending) = File::Tail::select(undef, undef, undef, $timeout, ($auditlog, $syslog)); foreach(@pending) { process_event($dbh, $_->read); } # see if we should flush pending entries to disk and/or do notification check_timers($dbh); }; # make sure we don't exit with any pending events not written to the db $dbh->commit || errlog "Error commiting changes: $!"; $dbh->disconnect || errlog "Error disconnecting from db: $!"; # close our error/debugging log file close(ERRLOG); unlink($pidfile) if $pidfile; exit 0;