#!/usr/bin/perl -w

# $Id: aa-eventd 458 2007-03-20 22:58:38Z jmichael-at-suse-de $
#
# ----------------------------------------------------------------------
#    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;

##########################################################################
# 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 $date_module = "None";

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>) {
            $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", "", "");

    # 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 (<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;
}

##########################################################################

sub process_event ($$) {
    my $dbh    = shift;
    my $logmsg = shift;
    my $sth;

    my ($time, $mesg);
    if ($logmsg =~ /^(?:type=(?:APPARMOR|UNKNOWN\[1500\]) 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 = parsedate($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 {
        chomp $logmsg;
        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';

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