mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 16:35:02 +01:00

perl utilities to the deprecated to directory; a couple of perl utilities remain, but they are still useful and do not depend on the Immunix module (just the LibAppArmor perl module).
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;
|