mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 08:24:42 +01:00
940 lines
29 KiB
Perl
Executable file
940 lines
29 KiB
Perl
Executable file
#!/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;
|