#!/usr/bin/perl -w

# ----------------------------------------------------------------------
#    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 DBI;
use Fcntl;
use File::Temp qw(tempfile);
use Getopt::Long;
use POSIX 'setsid';
use Time::Local;
use File::Tail;

use Immunix::Severity;
require LibAppArmor;

##########################################################################
# 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 @debug_buffer;

my @verbose_buffer;
my @summary_buffer;
my @terse_buffer;

my $date_module = "None";

my %templates = (
    "path"                => "(time,counter,type,op,profile,sdmode,mode_req,mode_deny,resource,prog,pid,severity) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
    "link"                => "(time,counter,type,op,profile,sdmode,resource,target,prog,pid,severity)    VALUES(?,?,?,?,?,?,?,?,?,?,?)",
    "chattr"              => "(time,counter,type,op,profile,sdmode,resource,mode_req,mode_deny,prog,pid,severity) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
    "capability"          => "(time,counter,type,op,profile,sdmode,resource,prog,pid,severity)           VALUES(?,?,?,?,?,?,?,?,?,?)",
    "capable"             => "(time,counter,type,op,prog,pid,profile)                                    VALUES(?,?,?,?,?,?,?)",
    "unknown_hat"         => "(time,counter,type,op,profile,sdmode,resource,pid)                         VALUES(?,?,?,?,?,?,?,?)",
    "fork"                => "(time,counter,type,op,profile,sdmode,pid,resource)                         VALUES(?,?,?,?,?,?,?,?)",
    "changing_profile"    => "(time,counter,type,op,profile,sdmode,pid)                                  VALUES(?,?,?,?,?,?,?)",
    "profile_replacement" => "(time,counter,type,op,profile,sdmode,prog,pid,severity)                    VALUES(?,?,?,?,?,?,?,?,?)",
	"net"                 => "(time,counter,type,op,net_family,net_socktype,net_proto,pid,profile)       VALUES(?,?,?,?,?,?,?,?,?)",
    "removed"             => "(time,counter,type,op,severity)                                            VALUES(?,?,?,?,?)",
    "initialized"         => "(time,counter,type,op,resource,severity)                                   VALUES(?,?,?,?,?,?)",
    "ctrl_var"            => "(time,counter,type,op,resource,mode_req,mode_deny,severity)                         VALUES(?,?,?,?,?,?,?,?)",
	"profile_load"        => "(time,counter,type,op,resource,prog,pid)                                   VALUES(?,?,?,?,?,?,?)",
);

##########################################################################
# 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>) {
            $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: $!";
}

sub parsedate ($) {
    my $time      = shift;
    my $timestamp = 0;
    if ($date_module eq 'TimeDate') {
        $timestamp = Date::Parse::str2time($time);
    } elsif ($date_module eq 'DateManip') {
        $timestamp = Date::Manip::UnixDate(Date::Manip::ParseDateString($time), '%s');
    } else {
        errlog "No date module found, exiing";
        kill HUP => -$$;
    }

    return $timestamp;
}

##########################################################################
# database handling functions

sub connect_database ($) {
    my $dbdir = shift;

    my $dbh = DBI->connect("dbi:SQLite:dbname=$dbdir/events.db", "", "", {RaiseError=>1});

    # 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.2");
        $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,
                op,
                pid,
                sdmode,
                type,
                mode_deny,
                mode_req,
                resource,
                target,
                profile,
                prog,
                name_alt,
                attr,
                parent,
                active_hat,
                net_family,
                net_proto,
                net_socktype,
                severity INTEGER
             )"
        );

        # set up the indexes we want
        #my @indexes = qw(time type sdmode mode resource profile prog severity);
        my @indexes = qw(time type op sdmode mode_req mode_deny 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 (<V>) {
            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 (<V>) {
            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 (<V>) {
            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;
}

##########################################################################
# Parse event record into key-value pairs
sub parseEvent($) {

    my %ev = ();
    my $msg = shift;
	chomp($msg);

    my $event = LibAppArmor::parse_record($msg);

    # resource is an alternate term for 'name1' below
    # mode is an alternate term for 'mode_deny' below
    $ev{'time'} = LibAppArmor::aa_log_record::swig_epoch_get($event);
    $ev{'op'} = LibAppArmor::aa_log_record::swig_operation_get($event);
    $ev{'pid'} = LibAppArmor::aa_log_record::swig_pid_get($event);
    $ev{'mode_deny'} = LibAppArmor::aa_log_record::swig_denied_mask_get($event);
    $ev{'mode_req'} = LibAppArmor::aa_log_record::swig_requested_mask_get($event);
    $ev{'profile'}= LibAppArmor::aa_log_record::swig_profile_get($event);
    $ev{'prog'} = LibAppArmor::aa_log_record::swig_name_get($event);
    $ev{'name2'} = LibAppArmor::aa_log_record::swig_name2_get($event);
    $ev{'attr'} = LibAppArmor::aa_log_record::swig_attribute_get($event);
    $ev{'parent'} = LibAppArmor::aa_log_record::swig_parent_get($event);
    $ev{'magic_token'} = LibAppArmor::aa_log_record::swig_magic_token_get($event);
    $ev{'resource'} = LibAppArmor::aa_log_record::swig_info_get($event);
    $ev{'active_hat'} = LibAppArmor::aa_log_record::swig_active_hat_get($event);
    $ev{'sdmode'} = LibAppArmor::aa_log_record::swig_event_get($event);

	# NetDomain 
	if ( $ev{'op'} && $ev{'op'} =~ /socket/ ) {
		next if $ev{'op'} =~ /create/;
		$ev{'net_family'} = LibAppArmor::aa_log_record::swig_net_family_get($event);
		$ev{'net_proto'} = LibAppArmor::aa_log_record::swig_net_protocol_get($event);
		$ev{'net_socktype'} = LibAppArmor::aa_log_record::swig_net_sock_type_get($event);
	}

    LibAppArmor::free_record($event);

    if ( ! $ev{'time'} ) { $ev{'time'} = time; }

    # remove null responses
    for (keys(%ev)) {
		if ( ! $ev{$_} || $ev{$_} !~ /\w+/) {delete($ev{$_}); }
		#errlog "EVENT:  $_ is $ev{$_}";
    }

    if ( $ev{'sdmode'} ) {
        #0 = invalid, 1 = error, 2 = AUDIT, 3 = ALLOW/PERMIT,
        #4 = DENIED/REJECTED, 5 = HINT, 6 = STATUS/config change
        if    ( $ev{'sdmode'} == 2 ) { $ev{'sdmode'} = "AUDITING"; }
        elsif ( $ev{'sdmode'} == 3 ) { $ev{'sdmode'} = "PERMITING"; }
        elsif ( $ev{'sdmode'} == 4 ) { $ev{'sdmode'} = "REJECTING"; }
        else  { delete($ev{'sdmode'}); }
    }

    return \%ev;
}

sub process_event ($$) {

    my $dbh    = shift;
    my $logmsg = shift;
    my $sth;
    my $severity = "";
	my @eventList = ();
	my $type = undef;
	my $time = undef;

	return unless $logmsg && $logmsg =~ /APPARMOR/;
	my $ev = parseEvent($logmsg);

    # skip logprof hints
    if ( ! $ev->{'op'} || $ev->{'op'} eq 'clone') { return; }

	$time = time;		# XXX - do we want current time or $ev->{'time'}?

    if ($time ne $lasttime) {
        $counter   = 0;
        $timestamp = $time;
        $lasttime  = $time;
    }

    $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;
    }

    if ( $ev->{'sdmode'} && $ev->{'sdmode'} eq "REJECTING") {
        $severity = $sevdb->rank($ev->{'prog'}, $ev->{'mode_req'});
		if ( ! $severity ) { $severity = "-1"; }

        # 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",
					$ev->{'prog'}, $ev->{'mode_req'}, $ev->{'resource'} ];
            }
        }

        if ($config->{terse_freq}) {
            if (   ($severity >= $config->{terse_level})
                || (($severity == -1) && $config->{terse_unknown}))
            {
                push @terse_buffer, [ $timestamp, $counter, "dummy" ];
            }
        }

    }

	unless ( $ev->{'op'} ) {
		my $errmsg = "ERROR: No operation found: ";
		for my $k (sort keys(%$ev)) {
			$errmsg .= "$k is $ev->{$k}, ";
		}
		errlog("$errmsg\n");
		return;
	}

    # Format the message to match the db template 
    if ($ev->{'op'} eq 'link' ) {
		$type = 'link';
        push(@eventList, [$time,$counter,$type,$ev->{'profile'},$ev->{'sdmode'},
			$ev->{'resource'},$ev->{'target'},$ev->{'prog'},$ev->{'pid'},$severity]); 
    } elsif ($ev->{'op'} eq 'attribute') {
		$type = 'chattr';
        push(@eventList, []);
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'profile'},$ev->{'sdmode'},
			$ev->{'resource'},$ev->{'mode_req'},$ev->{'mode_deny'},$ev->{'prog'},
			$ev->{'pid'},$severity]); 
    } elsif ($ev->{'op'} eq 'capability') {
		$type = 'capability';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'profile'},$ev->{'sdmode'},
			$ev->{'resource'},$ev->{'prog'},$ev->{'pid'},$severity]); 
    } elsif ($ev->{'op'} eq 'capable') {
		$type = 'capable';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'prog'},
			$ev->{'profile'},$ev->{'pid'}]); 
    } elsif ($ev->{'op'} =~ /ontrol variable/ ) {
        $type = 'ctrl_var';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'resource'},
			$ev->{'mode_req'},$ev->{'mode_deny'},$severity]); 
	} elsif ($ev->{'op'} eq 'unknown_hat') {
		$type = 'unknown_hat';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'profile'},$ev->{'sdmode'},
			$ev->{'resource'},$ev->{'pid'},$severity]); 
	} elsif ($ev->{'op'} eq 'fork') {
		$type = 'fork';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'profile'},$ev->{'sdmode'},
			$ev->{'pid'},$ev->{'resource'}]); 
	} elsif ($ev->{'op'} eq 'changing_profile') {
		$type = 'changing_profile';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'profile'},$ev->{'sdmode'},
			$ev->{'pid'}]); 
	} elsif ($ev->{'op'} eq 'profile_load') {
		$type = 'profile_load';
		push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'resource'},
			$ev->{'prog'},$ev->{'pid'}]);
	} elsif ($ev->{'op'} eq 'profile_replace') {
		$type = 'profile_replacement';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'profile'},$ev->{'sdmode'},
			$ev->{'prog'},$ev->{'pid'},$severity]); 
	} elsif ($ev->{'op'} eq 'removed') {
	    $type = 'removed';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$severity]); 
	} elsif ($ev->{'op'} eq 'initialized') {
		$type = 'initialized';
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'resource'},$severity]); 
	} elsif ( $ev->{'op'} =~ /socket/) {
		$type = 'net';
		push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'net_family'},
			$ev->{'net_sock_type'},$ev->{'net_proto'},$ev->{'pid'},$ev->{'profile'}]);
    } else {
        $type = 'path';
		if ( ! $ev->{'prog'} ) { $ev->{'prog'} = "NIL"; }
        push(@eventList, [$time,$counter,$type,$ev->{'op'},$ev->{'profile'},
			$ev->{'sdmode'},$ev->{'mode_req'},$ev->{'mode_deny'},$ev->{'resource'},
			$ev->{'prog'},$ev->{'pid'},$severity]); 
    }

    push(@commit_buffer, @eventList);
    $inserts++;

}

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;
            my $type  = $event[2];

		    eval {
		        if ($type ne $last_prepare) {
		            $sth          = $dbh->prepare("INSERT INTO events $templates{$type}");
		            $last_prepare = $type;
		        }

		        $sth->execute(@event);
		    };

		    if ($@) {
		        print ERRLOG "DBI Execution failed: $DBI::errstr\n";
		    }

            #$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);

$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';

# Sigh, portable dates in perl sucks
eval "use Date::Parse";
if (!$@) {
    $date_module = 'TimeDate';
} else {
    eval "use Date::Manip";
    if (!$@) {
        $date_module = 'DateManip';
    } else {
        errlog "Unable to load Date module; use either TimeDate or Date::Manip";
        $finished = 1;
    }
}

# 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,
    tail              => -1,
    interval          => 1,
    maxinterval       => 5,
    adjustafter       => 20,
    errmode           => "return",
    ignore_noexistant => 1
);
my $syslog = File::Tail->new(
    name              => $syslogfile,
    debug             => 1,
    tail              => -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;
