#!/usr/bin/perl -w # # SEC (Simple Event Correlator) 2.4.2 - sec.pl # Copyright (C) 2000-2008 Risto Vaarandi # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # package main::SEC; # Parameters: par1 - perl code to be evaluated # par2 - if set to 0, the code will be evaluated in scalar # context; if 1, list context is used for evaluation # Action: calls eval() for the perl code par1, and returns an array with # the eval() return value(s). The first element of the array # indicates whether the code was evaluated successfully (i.e., # the compilation didn't fail). If code evaluation fails, the # first element of the return array contains the error string. sub call_eval { my($code) = $_[0]; my($listcontext) = $_[1]; my($ok, @result); $ok = 1; if ($listcontext) { @result = eval $code; } else { $result[0] = eval $code; } if ($@) { $ok = 0; chomp($result[0] = $@); } return ($ok, @result); } ###################################################################### package main; use strict; ##### List of global variables ##### use vars qw( $blocksize $bufpos $bufsize @calendar $check_timeout %children $cleantime @conffilepat @conffiles %config_ltimes %config_mtimes %configuration %context_list %corr_list $debuglevel $detach $dumpdata $dumpfile @events $evstoresize $fromstart $help @inputfilepat @inputfiles %inputsrc @input_buffer @input_sources $input_timeout $intcontexts $intevents %int_contexts $lastcleanuptime $lastconfigload $logfile $openlog @pending_events $pidfile $poll_timeout $processedlines $quoting $rcfile_status @readbuffer $refresh $reopen_timeout $SEC_COPYRIGHT $SEC_LICENSE $SEC_USAGE $SEC_VERSION $SYSLOGAVAIL $sec_options $softrefresh $startuptime $syslogf $tail $terminate $testonly $timeout_script %variables $version $WIN32 ); ##### Load modules and set some global variables ##### use Getopt::Long; use POSIX qw(:errno_h :sys_wait_h SEEK_SET SEEK_CUR SEEK_END setsid); use Fcntl; use IO::Handle; # check if Sys::Syslog is available $SYSLOGAVAIL = eval { require Sys::Syslog }; # check if the platform is win32 $WIN32 = ($^O =~ /win/i && $^O !~ /cygwin/i && $^O !~ /darwin/i); # set version and usage variables $SEC_VERSION = "SEC (Simple Event Correlator) 2.4.2"; $SEC_COPYRIGHT = "Copyright (C) 2000-2008 Risto Vaarandi"; $SEC_USAGE = qq!Usage: $0 [options] Options: -conf= ... -input=[=] ... -input_timeout= -timeout_script= -reopen_timeout= -check_timeout= -poll_timeout= -blocksize= -bufsize= -evstoresize= -cleantime= -log= -syslog= -debug= -pid= -dump= -quoting, -noquoting -tail, -notail -fromstart, -nofromstart -detach, -nodetach -intevents, -nointevents -intcontexts, -nointcontexts -testonly, -notestonly -help, -? -version !; $SEC_LICENSE = q! This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. !; ##### List of internal constants ##### use constant CONFIG_KEYWORDS => { type => 1, continue => 1, ptype => 1, pattern => 1, context => 1, desc => 1, action => 1, window => 1, thresh => 1, continue2 => 1, ptype2 => 1, pattern2 => 1, context2 => 1, desc2 => 1, action2 => 1, window2 => 1, thresh2 => 1, time => 1, script => 1, rem => 1 }; use constant INVALIDVALUE => -1; use constant SINGLE => 0; use constant SINGLE_W_SUPPRESS => 1; use constant SINGLE_W_SCRIPT => 2; use constant PAIR => 3; use constant PAIR_W_WINDOW => 4; use constant SINGLE_W_THRESHOLD => 5; use constant SINGLE_W_2_THRESHOLDS => 6; use constant SUPPRESS => 7; use constant CALENDAR => 8; use constant SUBSTR => 0; use constant REGEXP => 1; use constant PERLFUNC => 2; use constant NSUBSTR => 3; use constant NREGEXP => 4; use constant NPERLFUNC => 5; use constant TVALUE => 6; use constant DONTCONT => 0; use constant TAKENEXT => 1; use constant NONE => 0; use constant LOGONLY => 1; use constant WRITE => 2; use constant SHELLCOMMAND => 3; use constant SPAWN => 4; use constant PIPE => 5; use constant CREATECONTEXT => 6; use constant DELETECONTEXT => 7; use constant OBSOLETECONTEXT => 8; use constant SETCONTEXT => 9; use constant ALIAS => 10; use constant UNALIAS => 11; use constant ADD => 12; use constant FILL => 13; use constant REPORT => 14; use constant COPYCONTEXT => 15; use constant EMPTYCONTEXT => 16; use constant EVENT => 17; use constant TEVENT => 18; use constant RESET => 19; use constant ASSIGN => 20; use constant EVAL => 21; use constant CALL => 22; use constant OPERAND => 0; use constant NEGATION => 1; use constant AND => 2; use constant OR => 3; use constant EXPRESSION => 4; use constant ECODE => 5; use constant CCODE => 6; use constant EXPRSYMBOL => "\0"; use constant LOG_CRIT => 1; use constant LOG_ERR => 2; use constant LOG_WARN => 3; use constant LOG_NOTICE => 4; use constant LOG_INFO => 5; use constant LOG_DEBUG => 6; use constant SYSLOG_LEVELS => { 1 => "crit", 2 => "err", 3 => "warning", 4 => "notice", 5 => "info", 6 => "debug" }; use constant SEPARATOR => " | "; use constant TERMTIMEOUT => 3; ############################################################### # ------------------------- FUNCTIONS ------------------------- ############################################################### ############################## # Functions related to logging ############################## # Parameters: par1 - name of the logfile # Action: logfile will be opened. Filehandle of the logfile will be # saved to the global filehandle LOGFILE. sub open_logfile { my($logfile) = $_[0]; if (open(LOGFILE, ">>$logfile")) { select LOGFILE; $| = 1; select STDOUT; } else { if (-t STDERR || -f STDERR) { print STDERR "Can't open logfile $logfile ($!), exiting!\n"; } child_cleanup(); exit(1); } } # Parameters: par1 - syslog facility # Action: open connection to the system logger with the facility par1. sub open_syslog { my($facility) = $_[0]; my($progname); if (!$SYSLOGAVAIL) { if (-t STDERR || -f STDERR) { print STDERR "Can't connect to syslog (no Sys::Syslog), exiting!\n"; } child_cleanup(); exit(1); } $progname = $0; $progname =~ s/.*\///; eval { Sys::Syslog::openlog($progname, "pid", $facility) }; if ($@) { if (-t STDERR || -f STDERR) { print STDERR "Can't connect to syslog ($@), exiting!\n"; } child_cleanup(); exit(1); } } # Parameters: par1 - severity of the log message # par2, par3, ... - strings to be logged # Action: if par1 is smaller or equal to the current logging level (i.e., # the message must be logged), then strings par2, par3, ... # will be equipped with timestamp and written to LOGFILE and/or # forwarded to the system logger as a single line. If STDERR is # connected to terminal, message will also be written there. sub log_msg { my($level) = shift(@_); my($ltime, $msg); if ($debuglevel < $level) { return; } if (!$logfile && !$syslogf && ! -t STDERR) { return; } $msg = join(" ", @_); if (-t STDERR) { print STDERR "$msg\n"; } if ($logfile) { $ltime = localtime(time()); print LOGFILE "$ltime: $msg\n"; } if ($syslogf) { $msg =~ s/%/%%/g; eval { Sys::Syslog::syslog(SYSLOG_LEVELS->{$level}, $msg) }; } } ####################################################### # Functions related to configuration file(s) processing ####################################################### # Parameters: par1, par2, .. - strings # Action: All 2-byte substrings in par1, par2, .. that denote special # symbols ("\n", "\t", ..) will be replaced with corresponding # special symbols sub subst_specchar { my(%specchar, $string); $specchar{"0"} = ""; $specchar{"n"} = "\n"; $specchar{"r"} = "\r"; $specchar{"s"} = " "; $specchar{"t"} = "\t"; $specchar{"\\"} = "\\"; foreach $string (@_) { $string =~ s/\\(0|n|r|s|t|\\)/$specchar{$1}/g; } } # Parameters: par1 - expression # par2 - reference to an array # Action: parentheses and their contents will be replaced with special # symbols EXPRSYMBOL in par 1. The expressions inside parentheses # will be returned in par2. Previous content of the array par2 # is erased. If par1 was parsed successfully, the modified par1 # will be returned, otherwise undef is returned. sub replace_subexpr { my($expression) = $_[0]; my($expr_ref) = $_[1]; my($i, $j, $l, $pos); my($char, $prev); @{$expr_ref} = (); $i = 0; $j = 0; $l = length($expression); $pos = undef; $prev = ""; while ($i < $l) { # process expression par1 from the start and inspect every symbol, # adding 1 to $j for every '(' and subtracting 1 for every ')'; # if a parenthesis is masked with a backslash, it is ignored $char = substr($expression, $i, 1); if ($prev ne "\\") { if ($char eq "(") { ++$j; } elsif ($char eq ")") { --$j; } } # After observing first '(' save its position to $pos; # after observing its counterpart ')' replace everything # from '(' to ')' with EXPRSYMBOL (including possible nested # expressions), and save the content of parentheses; # if at some point $j becomes negative, the parentheses must # be unbalanced if ($j == 1 && !defined($pos)) { $pos = $i; } elsif ($j == 0 && defined($pos)) { # take symbols starting from position $pos+1 (next symbol after # '(') up to position $i-1 (the symbol before ')'), and save # the symbols to array push @{$expr_ref}, substr($expression, $pos + 1, $i - $pos - 1); # replace both the parentheses and the symbols between them # with EXPRSYMBOL substr($expression, $pos, $i - $pos + 1) = EXPRSYMBOL; # set the variables according to changes in expression $i = $pos; $l = length($expression); $pos = undef; $char = ""; } elsif ($j < 0) { return undef; } # extra ')' was found $prev = $char; ++$i; } # if the parsing ended with non-zero $j, the parentheses were unbalanced if ($j == 0) { return $expression; } else { return undef; } } # Parameters: par1 - continue value (string) # par2 - the name of the configuration file # par3 - line number in configuration file # Action: par1 will be analyzed and the integer continue value will be # returned. If errors are found when analyzing par1, error message # about improper line par3 in configuration file will be logged. sub analyze_continue { my($continue) = $_[0]; my($conffile) = $_[1]; my($lineno) = $_[2]; if (uc($continue) eq "TAKENEXT") { return TAKENEXT; } elsif (uc($continue) eq "DONTCONT") { return DONTCONT; } log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid continue value '$continue'"); return INVALIDVALUE; } # Parameters: par1 - pattern type (string) # par2 - pattern # par3 - the name of the configuration file # par4 - line number in configuration file # par5 - if we are dealing with the second pattern of Pair* # rule, par5 contains the type of the first pattern # Action: par1 and par2 will be analyzed and tuple of integers # (pattern type, line count, compiled pattern) will be returned # (line count shows how many lines the pattern is designed to match). # If errors are found when analyzing par1 and par2, error message # about improper line par4 in configuration file will be logged. sub analyze_pattern { my($pattype) = $_[0]; my($pat) = $_[1]; my($conffile) = $_[2]; my($lineno) = $_[3]; my($negate, $lines); my($evalok, $retval); if ($pattype =~ /^(n?)regexp(\d*)$/i) { if (length($1)) { $negate = 1; } else { $negate = 0; } if (length($2)) { $lines = $2; } else { $lines = 1; } if ($lines > $bufsize || $lines < 1) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid linecount $lines in '$pattype'"); return (INVALIDVALUE, INVALIDVALUE, INVALIDVALUE); } eval { "" =~ /$pat/; }; if ($@) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid regular expression '$pat'"); return (INVALIDVALUE, INVALIDVALUE, INVALIDVALUE); } if (!defined($_[4]) || $_[4] == TVALUE || $_[4] == SUBSTR || $_[4] == NSUBSTR) { $pat = qr/$pat/; } if ($negate) { return (NREGEXP, $lines, $pat); } else { return (REGEXP, $lines, $pat); } } elsif ($pattype =~ /^(n?)substr(\d*)$/i) { if (length($1)) { $negate = 1; } else { $negate = 0; } if (length($2)) { $lines = $2; } else { $lines = 1; } if ($lines > $bufsize || $lines < 1) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid linecount $lines in '$pattype'"); return (INVALIDVALUE, INVALIDVALUE, INVALIDVALUE); } subst_specchar($pat); if ($negate) { return (NSUBSTR, $lines, $pat); } else { return (SUBSTR, $lines, $pat); } } elsif ($pattype =~ /^(n?)perlfunc(\d*)$/i) { if (length($1)) { $negate = 1; } else { $negate = 0; } if (length($2)) { $lines = $2; } else { $lines = 1; } if ($lines > $bufsize || $lines < 1) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid linecount $lines in '$pattype'"); return (INVALIDVALUE, INVALIDVALUE, INVALIDVALUE); } ($evalok, $retval) = SEC::call_eval($pat, 0); if (!$evalok || !defined($retval) || ref($retval) ne "CODE") { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid function '$pat'", defined($retval)?"($retval)":""); return (INVALIDVALUE, INVALIDVALUE, INVALIDVALUE); } if ($negate) { return (NPERLFUNC, $lines, $retval); } else { return (PERLFUNC, $lines, $retval); } } elsif ($pattype =~ /^tvalue$/i) { if (uc($pat) ne "TRUE" && uc($pat) ne "FALSE") { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid truth value '$pat'"); return (INVALIDVALUE, INVALIDVALUE, INVALIDVALUE); } return (TVALUE, 1, uc($pat) eq "TRUE"); } log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid pattern type '$pattype'"); return (INVALIDVALUE, INVALIDVALUE, INVALIDVALUE); } # Parameters: par1 - action # par2 - the name of the configuration file # par3 - line number in configuration file # par4 - rule ID # Action: par1 will be analyzed and pair of integers # (action type, action description) will be returned. If errors # are found when analyzing par1, error message about improper # line par3 in configuration file will be logged. sub analyze_action { my($action) = $_[0]; my($conffile) = $_[1]; my($lineno) = $_[2]; my($ruleid) = $_[3]; my($file, $cmdline, $progname); my($sign, $rule); my($actionlist, @action); my($createafter, $event); my($lifetime, $context, $alias); my($variable, $value, $code, $codeptr, $params); if ($action =~ /^none$/i) { return NONE; } elsif ($action =~ /^logonly\b\s*(.*)/i) { $event = $1; # strip outer parentheses if they exist if ($event =~ /^\s*\(\s*(.*)\)\s*$/) { $event = $1; } # remove backslashes in front of the parentheses $event =~ s/\\([\(\)])/$1/g; if (!length($event)) { $event = "%s"; } return (LOGONLY, $event); } elsif ($action =~ /^write\s+(\S+)\s*(.*)/i) { $file = $1; $event = $2; # strip outer parentheses if they exist if ($file =~ /^\s*\(\s*(.*)\)\s*$/) { $file = $1; } if ($event =~ /^\s*\(\s*(.*)\)\s*$/) { $event = $1; } # remove backslashes in front of the parentheses $file =~ s/\\([\(\)])/$1/g; $event =~ s/\\([\(\)])/$1/g; if (!length($event)) { $event = "%s"; } return (WRITE, $file, $event); } elsif ($action =~ /^shellcmd\s+(.*\S)/i) { $cmdline = $1; # strip outer parentheses if they exist if ($cmdline =~ /^\s*\(\s*(.*)\)\s*$/) { $cmdline = $1; } # remove backslashes in front of the parentheses $cmdline =~ s/\\([\(\)])/$1/g; $progname = (split(' ', $cmdline))[0]; if (! -f $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - could not find '$progname'"); } elsif (! -x $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - '$progname' is not executable"); } return (SHELLCOMMAND, $cmdline); } elsif ($action =~ /^spawn\s+(.*\S)/i) { if ($WIN32) { log_msg(LOG_ERR, "'spawn' action is not supported on Win32"); return INVALIDVALUE; } $cmdline = $1; # strip outer parentheses if they exist if ($cmdline =~ /^\s*\(\s*(.*)\)\s*$/) { $cmdline = $1; } # remove backslashes in front of the parentheses $cmdline =~ s/\\([\(\)])/$1/g; $progname = (split(' ', $cmdline))[0]; if (! -f $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - could not find '$progname'"); } elsif (! -x $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - '$progname' is not executable"); } return (SPAWN, $cmdline); } elsif ($action =~ /^pipe\s+'([^']*)'\s*(.*)/i) { $event = $1; $cmdline = $2; # strip outer parentheses if they exist if ($event =~ /^\s*\(\s*(.*)\)\s*$/) { $event = $1; } if ($cmdline =~ /^\s*\(\s*(.*)\)\s*$/) { $cmdline = $1; } # remove backslashes in front of the parentheses $event =~ s/\\([\(\)])/$1/g; $cmdline =~ s/\\([\(\)])/$1/g; if (!length($event)) { $event = "%s"; } if (length($cmdline)) { $progname = (split(' ', $cmdline))[0]; if (! -f $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - could not find '$progname'"); } elsif (! -x $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - '$progname' is not executable"); } } return (PIPE, $event, $cmdline); } elsif ($action =~ /^create\b\s*(\S*)\s*(\S*)\s*(.*)/i) { $context = $1; $lifetime = $2; $actionlist = $3; # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } if ($lifetime =~ /^\s*\(\s*(.*)\)\s*$/) { $lifetime = $1; } if ($actionlist =~ /^\s*\(\s*(.*)\)\s*$/) { $actionlist = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; $lifetime =~ s/\\([\(\)])/$1/g; if (!length($context)) { $context = "%s"; } if (!length($lifetime)) { $lifetime = 0; } if ($lifetime =~ /^0+$/ && length($actionlist)) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Context '$context' has infinite lifetime,", "ignoring actionlist '$actionlist'"); $actionlist = ""; } if (length($actionlist)) { if (!analyze_actionlist($actionlist, \@action, $conffile, $lineno, $ruleid)) { return INVALIDVALUE; } return (CREATECONTEXT, $context, $lifetime, [ @action ]); } return (CREATECONTEXT, $context, $lifetime, []); } elsif ($action =~ /^delete\b\s*(\S*)\s*$/i) { $context = $1; # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; if (!length($context)) { $context = "%s"; } return (DELETECONTEXT, $context); } elsif ($action =~ /^obsolete\b\s*(\S*)\s*$/i) { $context = $1; # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; if (!length($context)) { $context = "%s"; } return (OBSOLETECONTEXT, $context); } elsif ($action =~ /^set\s+(\S+)\s+(\S+)\s*(.*)/i) { $context = $1; $lifetime = $2; $actionlist = $3; # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } if ($lifetime =~ /^\s*\(\s*(.*)\)\s*$/) { $lifetime = $1; } if ($actionlist =~ /^\s*\(\s*(.*)\)\s*$/) { $actionlist = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; $lifetime =~ s/\\([\(\)])/$1/g; if ($lifetime =~ /^0+$/ && length($actionlist)) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Context '$context' has infinite lifetime,", "ignoring actionlist '$actionlist'"); $actionlist = ""; } if (length($actionlist)) { if (!analyze_actionlist($actionlist, \@action, $conffile, $lineno, $ruleid)) { return INVALIDVALUE; } return (SETCONTEXT, $context, $lifetime, [ @action ]); } return (SETCONTEXT, $context, $lifetime, []); } elsif ($action =~ /^alias\s+(\S+)\s*(\S*)\s*$/i) { $context = $1; $alias = $2; # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } if ($alias =~ /^\s*\(\s*(.*)\)\s*$/) { $alias = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; $alias =~ s/\\([\(\)])/$1/g; if (!length($alias)) { $alias = "%s"; } return (ALIAS, $context, $alias); } elsif ($action =~ /^unalias\b\s*(\S*)\s*$/i) { $alias = $1; # strip outer parentheses if they exist if ($alias =~ /^\s*\(\s*(.*)\)\s*$/) { $alias = $1; } # remove backslashes in front of the parentheses $alias =~ s/\\([\(\)])/$1/g; if (!length($alias)) { $alias = "%s"; } return (UNALIAS, $alias); } elsif ($action =~ /^add\s+(\S+)\s*(.*)/i) { $context = $1; $event = $2; # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } if ($event =~ /^\s*\(\s*(.*)\)\s*$/) { $event = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; $event =~ s/\\([\(\)])/$1/g; if (!length($event)) { $event = "%s"; } return (ADD, $context, $event); } elsif ($action =~ /^fill\s+(\S+)\s*(.*)/i) { $context = $1; $event = $2; # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } if ($event =~ /^\s*\(\s*(.*)\)\s*$/) { $event = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; $event =~ s/\\([\(\)])/$1/g; if (!length($event)) { $event = "%s"; } return (FILL, $context, $event); } elsif ($action =~ /^report\s+(\S+)\s*(.*)/i) { $context = $1; $cmdline = $2; # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } if ($cmdline =~ /^\s*\(\s*(.*)\)\s*$/) { $cmdline = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; $cmdline =~ s/\\([\(\)])/$1/g; if (length($cmdline)) { $progname = (split(' ', $cmdline))[0]; if (! -f $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - could not find '$progname'"); } elsif (! -x $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - '$progname' is not executable"); } } return (REPORT, $context, $cmdline); } elsif ($action =~ /^copy\s+(\S+)\s+(\S+)\s*$/i) { $context = $1; $variable = $2; if ($variable !~ /^%[A-Za-z][A-Za-z0-9_]*$/) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Variable $variable does not have the form", "%[||]..."); return INVALIDVALUE; } # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; return (COPYCONTEXT, $context, substr($variable, 1)); } elsif ($action =~ /^empty\s+(\S+)\s*(\S*)\s*$/i) { $context = $1; $variable = $2; if (length($variable) && $variable !~ /^%[A-Za-z][A-Za-z0-9_]*$/) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Variable $variable does not have the form", "%[||]..."); return INVALIDVALUE; } # strip outer parentheses if they exist if ($context =~ /^\s*\(\s*(.*)\)\s*$/) { $context = $1; } # remove backslashes in front of the parentheses $context =~ s/\\([\(\)])/$1/g; if (!length($variable)) { return (EMPTYCONTEXT, $context, ""); } return (EMPTYCONTEXT, $context, substr($variable, 1)); } elsif ($action =~ /^event\b\s*(\d*)\b\s*(.*)/i) { $createafter = $1; $event = $2; # strip outer parentheses if they exist if ($event =~ /^\s*\(\s*(.*)\)\s*$/) { $event = $1; } # remove backslashes in front of the parentheses $event =~ s/\\([\(\)])/$1/g; if (!length($createafter)) { $createafter = 0; } if (!length($event)) { $event = "%s"; } return (EVENT, $createafter, $event); } elsif ($action =~ /^tevent\s+(\S+)\s*(.*)/i) { $createafter = $1; $event = $2; # strip outer parentheses if they exist if ($createafter =~ /^\s*\(\s*(.*)\)\s*$/) { $createafter = $1; } if ($event =~ /^\s*\(\s*(.*)\)\s*$/) { $event = $1; } # remove backslashes in front of the parentheses $createafter =~ s/\\([\(\)])/$1/g; $event =~ s/\\([\(\)])/$1/g; if (!length($event)) { $event = "%s"; } return (TEVENT, $createafter, $event); } elsif ($action =~ /^reset\b\s*([\+-]?)(\d*)\b\s*(.*)/i) { $sign = $1; $rule = $2; $event = $3; if (length($rule)) { if ($sign eq "+") { $rule = $ruleid + $rule; } elsif ($sign eq "-") { $rule = $ruleid - $rule; } elsif (!$rule) { $rule = $ruleid; } else { --$rule; } } else { $rule = ""; } # strip outer parentheses if they exist if ($event =~ /^\s*\(\s*(.*)\)\s*$/) { $event = $1; } # remove backslashes in front of the parentheses $event =~ s/\\([\(\)])/$1/g; if (!length($event)) { $event = "%s"; } return (RESET, $conffile, $rule, $event); } elsif ($action =~ /^assign\s+(\S+)\s*(.*)/i) { $variable = $1; $value = $2; if ($variable !~ /^%[A-Za-z][A-Za-z0-9_]*$/) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Variable $variable does not have the form", "%[||]..."); return INVALIDVALUE; } # strip outer parentheses if they exist if ($value =~ /^\s*\(\s*(.*)\)\s*$/) { $value = $1; } # remove backslashes in front of the parentheses $value =~ s/\\([\(\)])/$1/g; if (!length($value)) { $value = "%s"; } return (ASSIGN, substr($variable, 1), $value); } elsif ($action =~ /^eval\s+(\S+)\s+(.*\S)/i) { $variable = $1; $code = $2; if ($variable !~ /^%[A-Za-z][A-Za-z0-9_]*$/) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Variable $variable does not have the form", "%[||]..."); return INVALIDVALUE; } # strip outer parentheses if they exist if ($code =~ /^\s*\(\s*(.*)\)\s*$/) { $code = $1; } # remove backslashes in front of the parentheses $code =~ s/\\([\(\)])/$1/g; return (EVAL, substr($variable, 1), $code); } elsif ($action =~ /^call\s+(\S+)\s+(\S+)\s*(.*)/i) { $variable = $1; $codeptr = $2; $params = $3; if ($variable !~ /^%[A-Za-z][A-Za-z0-9_]*$/) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Variable $variable does not have the form", "%[||]..."); return INVALIDVALUE; } if ($codeptr !~ /^%[A-Za-z][A-Za-z0-9_]*$/) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Variable $codeptr does not have the form", "%[||]..."); return INVALIDVALUE; } # strip outer parentheses if they exist if ($params =~ /^\s*\(\s*(.*)\)\s*$/) { $params = $1; } # remove backslashes in front of the parentheses $params =~ s/\\([\(\)])/$1/g; return (CALL, substr($variable, 1), substr($codeptr, 1), [ split(' ', $params) ]); } log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action '$action'"); return INVALIDVALUE; } # Parameters: par1 - action list separated by semicolons # par2 - reference to an array # par3 - the name of the configuration file # par4 - line number in configuration file # par5 - rule ID # Action: par1 will be split to parts, each part is analyzed and saved # to array @{$par2}. Previous content of the array is erased. # Parameters par3..par5 will be passed to the analyze_action() # function for logging purposes. Return 0 if an invalid action # was detected in the list par1, otherwise return 1. sub analyze_actionlist { my($actionlist) = $_[0]; my($arrayref) = $_[1]; my($conffile) = $_[2]; my($lineno) = $_[3]; my($ruleid) = $_[4]; my(@parts, $part); my($actiontype, @action); my($newactionlist, @list, $expr); my($pos, $l); @{$arrayref} = (); # replace the actions that are in parentheses with special symbols # and save the actions to @list $newactionlist = replace_subexpr($actionlist, \@list); if (!defined($newactionlist)) { return 0; } @parts = split(/\s*;\s*/, $newactionlist); $l = length(EXPRSYMBOL); foreach $part (@parts) { # substitute special symbols with expressions # that were removed previously for (;;) { $pos = index($part, EXPRSYMBOL); if ($pos == -1) { last; } $expr = shift @list; substr($part, $pos, $l) = "(" . $expr . ")"; } # analyze the action list part ($actiontype, @action) = analyze_action($part, $conffile, $lineno, $ruleid); if ($actiontype == INVALIDVALUE) { return 0; } push @{$arrayref}, $actiontype, @action; } return 1; } # Parameters: par1 - context expression # par2 - reference to an array # Action: par1 will be analyzed and saved to array par2 in reverse # polish notation form (it is assumed that par1 does not contain # expressions in parentheses). Previous content of the array par2 # is erased. If errors are found when analyzing par1, 0 will be # returned, otherwise 1 will be returned. sub analyze_context_expr { my($context) = $_[0]; my($result) = $_[1]; my($pos, $op1, $op2); my(@side1, @side2); my($evalok, $retval); # if we are parsing '&&' and '||' operators that take 2 operands, # process the context expression from the end with rindex(), in order # to get "from left to right" processing for AND and OR at runtime $pos = rindex($context, "||"); if ($pos != -1) { $op1 = substr($context, 0, $pos); $op2 = substr($context, $pos + 2); if (!analyze_context_expr($op1, \@side1)) { return 0; } if (!analyze_context_expr($op2, \@side2)) { return 0; } @{$result} = ( @side1, @side2, OR ); return 1; } $pos = rindex($context, "&&"); if ($pos != -1) { $op1 = substr($context, 0, $pos); $op2 = substr($context, $pos + 2); if (!analyze_context_expr($op1, \@side1)) { return 0; } if (!analyze_context_expr($op2, \@side2)) { return 0; } @{$result} = ( @side1, @side2, AND ); return 1; } # check for possible typos for '!' operator (any preceding illegal symbols) $pos = index($context, "!"); if ($pos != -1) { $op1 = substr($context, 0, $pos); $op2 = substr($context, $pos + 1); if ($op1 !~ /^\s*$/) { return 0; } if (!analyze_context_expr($op2, \@side2)) { return 0; } @{$result} = ( @side2, NEGATION ); return 1; } # since CCODE, ECODE and OPERAND are terminals, make sure that any # leading and trailing whitespace is removed from their parameters # (rest of the code relies on that); also, remove backslashes in front # of the parentheses if ($context =~ /^\s*(.*?)\s*->\s*(.*\S)/) { $op1 = $1; $op2 = $2; if ($op1 ne EXPRSYMBOL) { $op1 =~ s/\\([\(\)])/$1/g; $op1 = [ split(' ', $op1) ]; } if ($op2 ne EXPRSYMBOL) { $op2 =~ s/\\([\(\)])/$1/g; ($evalok, $retval) = SEC::call_eval($op2, 0); if (!$evalok || !defined($retval) || ref($retval) ne "CODE") { log_msg(LOG_ERR, "Eval '$op2' didn't return a code reference:", defined($retval)?$retval:"undef"); return 0; } $op2 = $retval; } @{$result} = ( CCODE, $op1, $op2 ); return 1; } if ($context =~ /^\s*=\s*(.*\S)/) { $op1 = $1; if ($op1 ne EXPRSYMBOL) { $op1 =~ s/\\([\(\)])/$1/g; } @{$result} = ( ECODE, $op1 ); return 1; } if ($context =~ /^\s*(.*\S)/) { $op1 = $1; if ($op1 ne EXPRSYMBOL) { $op1 =~ s/\\([\(\)])/$1/g; } @{$result} = ( OPERAND, $op1 ); return 1; } return 0; } # Parameters: par1 - context description # par2 - reference to an array # Action: par1 will be analyzed and saved to array par2 in reverse polish # notation form. Previous content of the array par2 is erased. # If errors are found when analyzing par1, 0 will be returned, # otherwise 1 will be returned. sub analyze_context { my($context) = $_[0]; my($result) = $_[1]; my($newcontext, $i, $j); my($params, $code, $evalok, $retval); my($subexpr, @expr); # replace upper level expressions in parentheses with special symbol # and save the expressions to @expr (i.e. !(a && (b || c )) || d # becomes !specialsymbol || d, and "a && (b || c )" is saved to @expr); # if context was not parsed successfully, exit $newcontext = replace_subexpr($context, \@expr); if (!defined($newcontext)) { return 0; } # convert the context to reverse polish notation, and if there # were no parenthesized subexpressions found in the context during # previous step, exit if (!analyze_context_expr($newcontext, $result)) { return 0; } if ($newcontext eq $context) { return 1; } # If the context contains parenthesized subexpressions, analyze and # convert these expressions recursively, attaching the results to # the current context. If a parenthesized expression is a Perl code, # it will not be analyzed recursively but rather treated as a terminal # (backslashes in front of the parentheses are removed) $i = 0; $j = scalar(@{$result}); while ($i < $j) { if ($result->[$i] == OPERAND) { if ($result->[$i+1] eq EXPRSYMBOL) { $result->[$i] = EXPRESSION; $result->[$i+1] = []; $subexpr = shift @expr; if (!analyze_context($subexpr, $result->[$i+1])) { return 0; } } $i += 2; } elsif ($result->[$i] == ECODE) { if ($result->[$i+1] eq EXPRSYMBOL) { $code = shift @expr; $code =~ s/\\([\(\)])/$1/g; $result->[$i+1] = $code; } $i += 2; } elsif ($result->[$i] == CCODE) { if ($result->[$i+1] eq EXPRSYMBOL) { $params = shift @expr; $params =~ s/\\([\(\)])/$1/g; $result->[$i+1] = [ split(' ', $params) ]; } if ($result->[$i+2] eq EXPRSYMBOL) { $code = shift @expr; $code =~ s/\\([\(\)])/$1/g; ($evalok, $retval) = SEC::call_eval($code, 0); if (!$evalok || !defined($retval) || ref($retval) ne "CODE") { log_msg(LOG_ERR, "Eval '$code' didn't return a code reference:", defined($retval)?$retval:"undef"); return 0; } $result->[$i+2] = $retval; } $i += 3; } else { ++$i; } } return 1; } # Parameters: par1 - context description # Action: if par1 is surrounded by [] brackets, the brackets will be # removed and 1 will be returned, otherwise 0 will be returned. sub check_context_preeval { if ($_[0] =~ /^\s*\[(.*)\]\s*$/) { $_[0] = $1; return 1; } else { return 0; } } # Parameters: par1 - list of the time values # par2 - minimum possible value for time # par3 - maximum possible value for time # par4 - offset that must be added to every list value # par5 - reference to a hash where every list value is added # Action: take the list definition and find the time values that belong # to the list (list definition is given in crontab-style). # After the values have been calculated, add an element to par5 with # the key that equals to the calculated value + offset. Leading zeros # are removed from keys (rest of the code relies on that). E.g., if # offset is 0, then "02,5-07" becomes 2,5,6,7; if offset is -1, min # is 1, and max is 12, then "2,5-7,11-" becomes 1,4,5,6,10,11. Before # adding elements to par5, its previous content is erased. If par1 is # specified incorrectly, return value is 0, otherwise 1 is returned. sub eval_timelist { my($spec) = $_[0]; my($min) = $_[1]; my($max) = $_[2]; my($offset) = $_[3]; my($ref) = $_[4]; my(@parts, $part); my($pos, $range1, $range2); my($i, $j); # split time specification into parts (by comma) and look what # ranges or individual numbers every part defines @parts = split(/,/, $spec); if (!scalar(@parts)) { return 0; } %{$ref} = (); foreach $part (@parts) { # if part is empty, skip it and take the next part if (!length($part)) { next; } # if part equals to '*', assume that it defines the range min..max if ($part eq "*") { # add offset (this also forces numeric context, so "05" becomes "5") # and save values to the hash $i = $min + $offset; $j = $max + $offset; while ($i <= $j) { $ref->{$i++} = 1; } next; } # if part is not empty and not '*', check if it contains '-' $pos = index($part, "-"); if ($pos == -1) { # if part does not contain '-', assume it defines a single number if ($part =~ /^0*(\d+)$/) { $part = $1; } else { return 0; } if ($part < $min || $part > $max) { return 0; } # add offset and save value to the hash $part += $offset; $ref->{$part} = 1; } else { # if part does contain '-', assume it defines a range $range1 = substr($part, 0, $pos); $range2 = substr($part, $pos + 1); # if left side of the range is missing, assume minimum for the value; # if right side of the range is missing, assume maximum for the value; # offset is then added to the left and right side of the range if (length($range1)) { if ($range1 =~ /^0*(\d+)$/) { $range1 = $1; } else { return 0; } if ($range1 < $min || $range1 > $max) { return 0; } $i = $range1 + $offset; } else { $i = $min + $offset; } if (length($range2)) { if ($range2 =~ /^0*(\d+)$/) { $range2 = $1; } else { return 0; } if ($range2 < $min || $range2 > $max) { return 0; } $j = $range2 + $offset; } else { $j = $max + $offset; } # save values to the hash while ($i <= $j) { $ref->{$i++} = 1; } } } return 1; } # Parameters: par1 - time specification # par2..par6 - references to the hashes of minutes, hours, # days, months and weekdays # par7 - the name of the configuration file # par8 - line number in configuration file # Action: par1 will be split to parts, every part is analyzed and # results are saved into hashes par2..par6. # Previous content of the hashes is erased. If errors # are found when analyzing par1, 0 is returned, otherwise 1 # will be return value. sub analyze_timespec { my($timespec) = $_[0]; my($minref) = $_[1]; my($hourref) = $_[2]; my($dayref) = $_[3]; my($monthref) = $_[4]; my($wdayref) = $_[5]; my($conffile) = $_[6]; my($lineno) = $_[7]; my(@parts); # split time specification into parts by whitespace (like with # split(/\s+/, ...)), but leading whitespace will be ignored @parts = split(' ', $timespec); if (scalar(@parts) != 5) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Wrong number of elements in time specification"); return 0; } # evaluate minute specification (range 0..59, offset 0) if (!eval_timelist($parts[0], 0, 59, 0, $minref)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid minute specification '$parts[0]'"); return 0; } # evaluate hour specification (range 0..23, offset 0) if (!eval_timelist($parts[1], 0, 23, 0, $hourref)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid hour specification '$parts[1]'"); return 0; } # evaluate day specification (range 0..31, offset 0) # 0 denotes the last day of a month if (!eval_timelist($parts[2], 0, 31, 0, $dayref)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid day specification '$parts[2]'"); return 0; } # evaluate month specification (range 1..12, offset -1) if (!eval_timelist($parts[3], 1, 12, -1, $monthref)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid month specification '$parts[3]'"); return 0; } # evaluate weekday specification (range 0..7, offset 0) if (!eval_timelist($parts[4], 0, 7, 0, $wdayref)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid weekday specification '$parts[4]'"); return 0; } # if 7 was specified as a weekday, also define 0, # since perl uses only 0 for Sunday if (exists($wdayref->{"7"})) { $wdayref->{"0"} = 1; } return 1; } # Parameters: par1 - reference to a hash containing the rule # par2 - list of required keywords for the rule # par3 - the type of the rule # par4 - the name of the configuration file # par5 - line number in configuration file the rule begins at # Action: check if all required keywords are present in the rule par1 and # return 0 if they are, otherwise return 1. sub missing_keywords { my($ref) = $_[0]; my($keylist) = $_[1]; my($type) = $_[2]; my($conffile) = $_[3]; my($lineno) = $_[4]; my($key, $error); $error = 0; foreach $key (@{$keylist}) { if (!exists($ref->{$key})) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Keyword '$key' missing (needed for the rule type $type)"); $error = 1; } } return $error; } # Parameters: par1 - reference to a hash containing the rule # par2 - name of the configuration file # par3 - line number in configuration file the rule begins at # par4 - rule ID # Action: check the rule par1 for correctness and save it to # global array $configuration{par2} if it is well-defined; # if the rule was correctly defined, return 1, otherwise return 0 sub check_rule { my($ref) = $_[0]; my($conffile) = $_[1]; my($lineno) = $_[2]; my($number) = $_[3]; my($config, @keywords); my($type, $progname); my($whatnext, $pattype, $patlines, $pattern, $contpreeval); my($whatnext2, $pattype2, $patlines2, $pattern2, $contpreeval2); my(@context, @action, @context2, @action2); my(%minutes, %hours, %days, %months, %weekdays); $config = $configuration{$conffile}; if (!exists($ref->{"type"})) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Keyword 'type' missing"); return 0; } $type = uc($ref->{"type"}); # ------------------------------------------------------------ # SINGLE rule # ------------------------------------------------------------ if ($type eq "SINGLE") { @keywords = ("ptype", "pattern", "desc", "action"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } if (!exists($ref->{"continue"})) { $whatnext = DONTCONT; } else { $whatnext = analyze_continue($ref->{"continue"}, $conffile, $lineno); } if ($whatnext == INVALIDVALUE) { return 0; } ($pattype, $patlines, $pattern) = analyze_pattern($ref->{"ptype"}, $ref->{"pattern"}, $conffile, $lineno); if ($pattype == INVALIDVALUE) { return 0; } if (!analyze_actionlist($ref->{"action"}, \@action, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action"}, "'"); return 0; } if (exists($ref->{"context"})) { $contpreeval = check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); $contpreeval = 0; } $config->[$number] = { "ID" => $number, "Type" => SINGLE, "WhatNext" => $whatnext, "PatType" => $pattype, "Pattern" => $pattern, "PatLines" => $patlines, "Context" => [ @context ], "ContPreEval" => $contpreeval, "Desc" => $ref->{"desc"}, "Action" => [ @action ], "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # SINGLE_W_SCRIPT rule # ------------------------------------------------------------ elsif ($type eq "SINGLEWITHSCRIPT") { @keywords = ("ptype", "pattern", "script", "desc", "action"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } if (!exists($ref->{"continue"})) { $whatnext = DONTCONT; } else { $whatnext = analyze_continue($ref->{"continue"}, $conffile, $lineno); } if ($whatnext == INVALIDVALUE) { return 0; } ($pattype, $patlines, $pattern) = analyze_pattern($ref->{"ptype"}, $ref->{"pattern"}, $conffile, $lineno); if ($pattype == INVALIDVALUE) { return 0; } $progname = (split(' ', $ref->{"script"}))[0]; if (! -f $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - could not find '$progname'"); } elsif (! -x $progname) { log_msg(LOG_WARN, "Rule in $conffile at line $lineno:", "Warning - '$progname' is not executable"); } if (!analyze_actionlist($ref->{"action"}, \@action, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action"}, "'"); return 0; } if (exists($ref->{"action2"})) { if (!analyze_actionlist($ref->{"action2"}, \@action2, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action2"}, "'"); return 0; } } else { @action2 = (); } if (exists($ref->{"context"})) { $contpreeval = check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); $contpreeval = 0; } $config->[$number] = { "ID" => $number, "Type" => SINGLE_W_SCRIPT, "WhatNext" => $whatnext, "PatType" => $pattype, "Pattern" => $pattern, "PatLines" => $patlines, "Context" => [ @context ], "ContPreEval" => $contpreeval, "Script" => $ref->{"script"}, "Desc" => $ref->{"desc"}, "Action" => [ @action ], "Action2" => [ @action2 ], "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # SINGLE_W_SUPPRESS rule # ------------------------------------------------------------ elsif ($type eq "SINGLEWITHSUPPRESS") { @keywords = ("ptype", "pattern", "desc", "action", "window"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } if (!exists($ref->{"continue"})) { $whatnext = DONTCONT; } else { $whatnext = analyze_continue($ref->{"continue"}, $conffile, $lineno); } if ($whatnext == INVALIDVALUE) { return 0; } ($pattype, $patlines, $pattern) = analyze_pattern($ref->{"ptype"}, $ref->{"pattern"}, $conffile, $lineno); if ($pattype == INVALIDVALUE) { return 0; } if (!analyze_actionlist($ref->{"action"}, \@action, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action"}, "'"); return 0; } if ($ref->{"window"} !~ /^0*(\d+)$/ || $1 == 0) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid time window '", $ref->{"window"}, "'"); return 0; } else { $ref->{"window"} = $1; } if (exists($ref->{"context"})) { $contpreeval = check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); $contpreeval = 0; } $config->[$number] = { "ID" => $number, "Type" => SINGLE_W_SUPPRESS, "WhatNext" => $whatnext, "PatType" => $pattype, "Pattern" => $pattern, "PatLines" => $patlines, "Context" => [ @context ], "ContPreEval" => $contpreeval, "Desc" => $ref->{"desc"}, "Action" => [ @action ], "Window" => $ref->{"window"}, "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # PAIR rule # ------------------------------------------------------------ elsif ($type eq "PAIR") { @keywords = ("ptype", "pattern", "desc", "action", "ptype2", "pattern2", "desc2", "action2"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } if (!exists($ref->{"continue"})) { $whatnext = DONTCONT; } else { $whatnext = analyze_continue($ref->{"continue"}, $conffile, $lineno); } if ($whatnext == INVALIDVALUE) { return 0; } ($pattype, $patlines, $pattern) = analyze_pattern($ref->{"ptype"}, $ref->{"pattern"}, $conffile, $lineno); if ($pattype == INVALIDVALUE) { return 0; } if (!analyze_actionlist($ref->{"action"}, \@action, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action"}, "'"); return 0; } if (!exists($ref->{"continue2"})) { $whatnext2 = DONTCONT; } else { $whatnext2 = analyze_continue($ref->{"continue2"}, $conffile, $lineno); } if ($whatnext2 == INVALIDVALUE) { return 0; } ($pattype2, $patlines2, $pattern2) = analyze_pattern($ref->{"ptype2"}, $ref->{"pattern2"}, $conffile, $lineno, $pattype); if ($pattype2 == INVALIDVALUE) { return 0; } if (!analyze_actionlist($ref->{"action2"}, \@action2, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action2"}, "'"); return 0; } if (!exists($ref->{"window"})) { $ref->{"window"} = 0; } elsif ($ref->{"window"} =~ /^0*(\d+)$/) { $ref->{"window"} = $1; } else { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid time window '", $ref->{"window"}, "'"); return 0; } if (exists($ref->{"context"})) { $contpreeval = check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid 1st context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); $contpreeval = 0; } if (exists($ref->{"context2"})) { $contpreeval2 = check_context_preeval($ref->{"context2"}); if (!analyze_context($ref->{"context2"}, \@context2)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid 2nd context specification '", $ref->{"context2"}, "'"); return 0; } } else { @context2 = (); $contpreeval2 = 0; } $config->[$number] = { "ID" => $number, "Type" => PAIR, "WhatNext" => $whatnext, "PatType" => $pattype, "Pattern" => $pattern, "PatLines" => $patlines, "Context" => [ @context ], "ContPreEval" => $contpreeval, "Desc" => $ref->{"desc"}, "Action" => [ @action ], "WhatNext2" => $whatnext2, "PatType2" => $pattype2, "Pattern2" => $pattern2, "PatLines2" => $patlines2, "Context2" => [ @context2 ], "ContPreEval2" => $contpreeval2, "Desc2" => $ref->{"desc2"}, "Action2" => [ @action2 ], "Window" => $ref->{"window"}, "Operations" => {}, "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # PAIR_W_WINDOW rule # ------------------------------------------------------------ elsif ($type eq "PAIRWITHWINDOW") { @keywords = ("ptype", "pattern", "desc", "action", "ptype2", "pattern2", "desc2", "action2", "window"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } if (!exists($ref->{"continue"})) { $whatnext = DONTCONT; } else { $whatnext = analyze_continue($ref->{"continue"}, $conffile, $lineno); } if ($whatnext == INVALIDVALUE) { return 0; } ($pattype, $patlines, $pattern) = analyze_pattern($ref->{"ptype"}, $ref->{"pattern"}, $conffile, $lineno); if ($pattype == INVALIDVALUE) { return 0; } if (!analyze_actionlist($ref->{"action"}, \@action, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action"}, "'"); return 0; } if (!exists($ref->{"continue2"})) { $whatnext2 = DONTCONT; } else { $whatnext2 = analyze_continue($ref->{"continue2"}, $conffile, $lineno); } if ($whatnext2 == INVALIDVALUE) { return 0; } ($pattype2, $patlines2, $pattern2) = analyze_pattern($ref->{"ptype2"}, $ref->{"pattern2"}, $conffile, $lineno, $pattype); if ($pattype2 == INVALIDVALUE) { return 0; } if (!analyze_actionlist($ref->{"action2"}, \@action2, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action2"}, "'"); return 0; } if ($ref->{"window"} !~ /^0*(\d+)$/ || $1 == 0) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid time window '", $ref->{"window"}, "'"); return 0; } else { $ref->{"window"} = $1; } if (exists($ref->{"context"})) { $contpreeval = check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid 1st context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); $contpreeval = 0; } if (exists($ref->{"context2"})) { $contpreeval2 = check_context_preeval($ref->{"context2"}); if (!analyze_context($ref->{"context2"}, \@context2)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid 2nd context specification '", $ref->{"context2"}, "'"); return 0; } } else { @context2 = (); $contpreeval2 = 0; } $config->[$number] = { "ID" => $number, "Type" => PAIR_W_WINDOW, "WhatNext" => $whatnext, "PatType" => $pattype, "Pattern" => $pattern, "PatLines" => $patlines, "Context" => [ @context ], "ContPreEval" => $contpreeval, "Desc" => $ref->{"desc"}, "Action" => [ @action ], "WhatNext2" => $whatnext2, "PatType2" => $pattype2, "Pattern2" => $pattern2, "PatLines2" => $patlines2, "Context2" => [ @context2 ], "ContPreEval2" => $contpreeval2, "Desc2" => $ref->{"desc2"}, "Action2" => [ @action2 ], "Window" => $ref->{"window"}, "Operations" => {}, "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # SINGLE_W_THRESHOLD rule # ------------------------------------------------------------ elsif ($type eq "SINGLEWITHTHRESHOLD") { @keywords = ("ptype", "pattern", "desc", "action", "window", "thresh"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } if (!exists($ref->{"continue"})) { $whatnext = DONTCONT; } else { $whatnext = analyze_continue($ref->{"continue"}, $conffile, $lineno); } if ($whatnext == INVALIDVALUE) { return 0; } ($pattype, $patlines, $pattern) = analyze_pattern($ref->{"ptype"}, $ref->{"pattern"}, $conffile, $lineno); if ($pattype == INVALIDVALUE) { return 0; } if (!analyze_actionlist($ref->{"action"}, \@action, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action"}, "'"); return 0; } if (exists($ref->{"action2"})) { if (!analyze_actionlist($ref->{"action2"}, \@action2, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action2"}, "'"); return 0; } } else { @action2 = (); } if ($ref->{"window"} !~ /^0*(\d+)$/ || $1 == 0) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid time window '", $ref->{"window"}, "'"); return 0; } else { $ref->{"window"} = $1; } if ($ref->{"thresh"} !~ /^0*(\d+)$/ || $1 == 0) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid threshold '", $ref->{"thresh"}, "'"); return 0; } else { $ref->{"thresh"} = $1; } if (exists($ref->{"context"})) { $contpreeval = check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); $contpreeval = 0; } $config->[$number] = { "ID" => $number, "Type" => SINGLE_W_THRESHOLD, "WhatNext" => $whatnext, "PatType" => $pattype, "Pattern" => $pattern, "PatLines" => $patlines, "Context" => [ @context ], "ContPreEval" => $contpreeval, "Desc" => $ref->{"desc"}, "Action" => [ @action ], "Action2" => [ @action2 ], "Window" => $ref->{"window"}, "Threshold" => $ref->{"thresh"}, "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # SINGLE_W_2_THRESHOLDS rule # ------------------------------------------------------------ elsif ($type eq "SINGLEWITH2THRESHOLDS") { @keywords = ("ptype", "pattern", "desc", "action", "window", "thresh", "desc2", "action2", "window2", "thresh2"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } if (!exists($ref->{"continue"})) { $whatnext = DONTCONT; } else { $whatnext = analyze_continue($ref->{"continue"}, $conffile, $lineno); } if ($whatnext == INVALIDVALUE) { return 0; } ($pattype, $patlines, $pattern) = analyze_pattern($ref->{"ptype"}, $ref->{"pattern"}, $conffile, $lineno); if ($pattype == INVALIDVALUE) { return 0; } if (!analyze_actionlist($ref->{"action"}, \@action, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action"}, "'"); return 0; } if ($ref->{"window"} !~ /^0*(\d+)$/ || $1 == 0) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid 1st time window '", $ref->{"window"}, "'"); return 0; } else { $ref->{"window"} = $1; } if ($ref->{"thresh"} !~ /^0*(\d+)$/ || $1 == 0) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid 1st threshold '", $ref->{"thresh"}, "'"); return 0; } else { $ref->{"thresh"} = $1; } if (!analyze_actionlist($ref->{"action2"}, \@action2, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action2"}, "'"); return 0; } if ($ref->{"window2"} !~ /^0*(\d+)$/ || $1 == 0) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid 2nd time window '", $ref->{"window2"}, "'"); return 0; } else { $ref->{"window2"} = $1; } if ($ref->{"thresh2"} =~ /^0*(\d+)$/) { $ref->{"thresh2"} = $1; } else { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid 2nd threshold '", $ref->{"thresh2"}, "'"); return 0; } if (exists($ref->{"context"})) { $contpreeval = check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); $contpreeval = 0; } $config->[$number] = { "ID" => $number, "Type" => SINGLE_W_2_THRESHOLDS, "WhatNext" => $whatnext, "PatType" => $pattype, "Pattern" => $pattern, "PatLines" => $patlines, "Context" => [ @context ], "ContPreEval" => $contpreeval, "Desc" => $ref->{"desc"}, "Action" => [ @action ], "Window" => $ref->{"window"}, "Threshold" => $ref->{"thresh"}, "Desc2" => $ref->{"desc2"}, "Action2" => [ @action2 ], "Window2" => $ref->{"window2"}, "Threshold2" => $ref->{"thresh2"}, "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # SUPPRESS rule # ------------------------------------------------------------ elsif ($type eq "SUPPRESS") { @keywords = ("ptype", "pattern"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } ($pattype, $patlines, $pattern) = analyze_pattern($ref->{"ptype"}, $ref->{"pattern"}, $conffile, $lineno); if ($pattype == INVALIDVALUE) { return 0; } if (exists($ref->{"context"})) { $contpreeval = check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); $contpreeval = 0; } if (!exists($ref->{"desc"})) { if ($pattype == REGEXP || $pattype == SUBSTR || $pattype == PERLFUNC) { $ref->{"desc"} = "Suppress rule with pattern: $pattern"; } elsif ($pattype == NREGEXP || $pattype == NSUBSTR || $pattype == NPERLFUNC) { $ref->{"desc"} = "Suppress rule with negative pattern: $pattern"; } else { $ref->{"desc"} = "Suppress rule with pattern: " . ($pattern?"TRUE":"FALSE"); } } $config->[$number] = { "ID" => $number, "Type" => SUPPRESS, "PatType" => $pattype, "Pattern" => $pattern, "PatLines" => $patlines, "Context" => [ @context ], "ContPreEval" => $contpreeval, "Desc" => $ref->{"desc"}, "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # CALENDAR rule # ------------------------------------------------------------ elsif ($type eq "CALENDAR") { @keywords = ("time", "desc", "action"); if (missing_keywords($ref, \@keywords, $type, $conffile, $lineno)) { return 0; } if (!analyze_timespec($ref->{"time"}, \%minutes, \%hours, \%days, \%months, \%weekdays, $conffile, $lineno)) { return 0; } if (!analyze_actionlist($ref->{"action"}, \@action, $conffile, $lineno, $number)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid action list '", $ref->{"action"}, "'"); return 0; } if (exists($ref->{"context"})) { # since for Calendar rule []-operator has no meaning, # just remove [] brackets if they exist check_context_preeval($ref->{"context"}); if (!analyze_context($ref->{"context"}, \@context)) { log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid context specification '", $ref->{"context"}, "'"); return 0; } } else { @context = (); } $config->[$number] = { "ID" => $number, "Type" => CALENDAR, "Minutes" => { %minutes }, "Hours" => { %hours }, "Days" => { %days }, "Months" => { %months }, "Weekdays" => { %weekdays }, "LastMinute" => 0, "LastHour" => 0, "LastDay" => 0, "LastMonth" => 0, "LastWeekday" => 0, "Context" => [ @context ], "Desc" => $ref->{"desc"}, "Action" => [ @action ], "MatchCount" => 0, "LineNo" => $lineno }; return 1; } # ------------------------------------------------------------ # end of rule processing # ------------------------------------------------------------ log_msg(LOG_ERR, "Rule in $conffile at line $lineno:", "Invalid rule type $type"); return 0; } # Parameters: par1 - name of the configuration file # Action: read in rules from configuration file par1, so that leading # and trailing whitespace is removed both from keywords and values # of rule definions, and then call check_rule() for every rule; # if all rules in the file were correctly defined, return 1, # otherwise return 0 sub read_configfile { my($conffile) = $_[0]; my($linebuf, $line, $i, $cont, $rulestart); my($keyword, $value, $file_status); my(%rule); $file_status = 1; # start with the assumption that all rules # are correctly defined log_msg(LOG_NOTICE, "Reading configuration from $conffile"); if (!open(CONFFILE, "$conffile")) { log_msg(LOG_ERR, "Can't open configuration file $conffile ($!)"); return 0; } $i = 0; $cont = 0; %rule = (); $rulestart = 1; for (;;) { # read next line from file $linebuf = ; # check if the line belongs to previous line; if it does, form a # single line from them and start the loop again (i.e. we will # concatenate lines until we read a line that does not end with '\') if (defined($linebuf)) { chomp($linebuf); if ($cont) { $line .= $linebuf; } else { $line = $linebuf; } # remove whitespaces from line beginnings and ends; # if line is all-whitespace, set it to empty string if ($line =~ /^\s*(.*\S)/) { $line = $1; } else { $line = ""; } # check if line ends with '\'; if it does, remove '\', set $cont # to 1 and jump at the start of loop to read next line, otherwise # set $cont to 0 if (substr($line, length($line) - 1) eq '\\') { chop($line); $cont = 1; next; } else { $cont = 0; } } # if the line constructed during previous loop is empty, starting # with #-symbol, or if we have reached EOF, consider that as the end # of current rule. Check the rule and set $rulestart to the next line. # If we have reached EOF, quit the loop, otherwise take the next line. if (!defined($linebuf) || !length($line) || index($line, '#') == 0) { if (scalar(%rule)) { if (check_rule(\%rule, $conffile, $rulestart, $i)) { ++$i; } else { $file_status = 0; } %rule = (); } $rulestart = $. + 1; if (defined($linebuf)) { next; } else { last; } } # split line into keyword and value if ($line =~ /^\s*([A-Za-z0-9]+)\s*=\s*(.*\S)/) { $keyword = $1; $value = $2; } else { log_msg(LOG_ERR, "$conffile line $. ($line):", "Line not in keyword=value format or non-alphanumeric keyword"); $file_status = 0; next; } # check if the keyword is valid and save it to hash %rule if it is if (!exists(CONFIG_KEYWORDS->{$keyword})) { log_msg(LOG_ERR, "$conffile line $.:", "Invalid keyword $keyword"); $file_status = 0; next; } $rule{$keyword} = $value; } if (!$i) { log_msg(LOG_WARN, "No valid rules found in configuration file $conffile"); } else { log_msg(LOG_DEBUG, "$i rules loaded from $conffile"); } close(CONFFILE); return $file_status; } # Parameters: - # Action: evaluate the conffile patterns given in commandline, form the # list of configuration files and save it to global array # @conffiles, and read in rules from the configuration files sub read_config { my($pattern, $conffile, $ret); my(@stat, @rules); # Initialize global arrays %configuration, %config_ltimes, %config_mtimes, # @calendar and @conffiles (the keys for %configuration, %config_ltimes # and %config_mtimes are members of the global array @conffiles), and set # the $lastconfigload variable to reflect the current time $lastconfigload = time(); %configuration = (); %config_ltimes = (); %config_mtimes = (); @calendar = (); @conffiles = (); # Form the list of configuration files and save it to a global array foreach $pattern (@conffilepat) { push @conffiles, glob($pattern); } # Read the configuration from rule files and store it to the global # array %configuration; also, store mtimes of rule files to the global # array %config_mtimes and Calendar rules to the global array @calendar $ret = 1; foreach $conffile (@conffiles) { $configuration{$conffile} = []; $config_ltimes{$conffile} = $lastconfigload; @stat = stat($conffile); $config_mtimes{$conffile} = scalar(@stat)?$stat[9]:0; if (!read_configfile($conffile)) { $ret = 0; } @rules = grep($_->{"Type"} == CALENDAR, @{$configuration{$conffile}}); push @calendar, @rules; } return $ret; } # Parameters: par1 - reference to an array where the names of modified # and removed configuration files will be stored # Action: evaluate the conffile patterns given in commandline, form the # list of configuration files and save it to global array # @conffiles; read in rules from the configuration files that are # either new or have been modified since the last configuration # load; also store to the array par1 the names of configuration # files that have been modified or removed since the last # configuration load. sub soft_read_config { my($file_list) = $_[0]; my($pattern, $conffile); my(%old_config, %old_ltimes, %old_mtimes); my(@old_conffiles, @stat, @rules); # Back up global arrays %configuration, %config_ltimes, %config_mtimes, # and @conffiles %old_config = %configuration; %old_ltimes = %config_ltimes; %old_mtimes = %config_mtimes; @old_conffiles = @conffiles; # Initialize global arrays %configuration, %config_ltimes, %config_mtimes, # @calendar and @conffiles (the keys for %configuration, %config_ltimes # and %config_mtimes are members of the global array @conffiles), and set # the $lastconfigload variable to reflect the current time $lastconfigload = time(); %configuration = (); %config_ltimes = (); %config_mtimes = (); @calendar = (); @conffiles = (); # Form the list of configuration files and save it to a global array foreach $pattern (@conffilepat) { push @conffiles, glob($pattern); } # Read the configuration from the rule files that are new or have been # modified and store it to the global array %configuration; store mtimes # of rule files to the global array %config_mtimes; store file load times # to the global array %config_ltimes; store Calendar rules to the global # array @calendar; also, store the names of modified configuration files # to the array par1 @{$file_list} = (); foreach $conffile (@conffiles) { @stat = stat($conffile); $config_mtimes{$conffile} = scalar(@stat)?$stat[9]:0; if (!exists($old_config{$conffile})) { $configuration{$conffile} = []; read_configfile($conffile); $config_ltimes{$conffile} = $lastconfigload; } elsif ($old_mtimes{$conffile} != $config_mtimes{$conffile}) { $configuration{$conffile} = []; read_configfile($conffile); $config_ltimes{$conffile} = $lastconfigload; push @{$file_list}, $conffile; } else { $configuration{$conffile} = $old_config{$conffile}; $config_ltimes{$conffile} = $old_ltimes{$conffile}; } @rules = grep($_->{"Type"} == CALENDAR, @{$configuration{$conffile}}); push @calendar, @rules; } # store the names of removed configuration files to the array par1 push @{$file_list}, grep(!exists($configuration{$_}), @old_conffiles); } ################################################ # Functions related to execution of action lists ################################################ # Parameters: par1 - string # par2 - string # Action: all %-variables in string par1 will be replaced with their values sub substitute_var { if (index($_[0], "%") == -1) { return; } $variables{"u"} = time(); $variables{"t"} = localtime($variables{"u"}); $variables{"s"} = $_[1]; $variables{"%"} = "%"; # variable will not be substituted if it doesn't exist or its value is undef $_[0] =~ s/(\%\{([A-Za-z][A-Za-z0-9_]*)\}| \%([A-Za-z][A-Za-z0-9_]*|\%))/ defined($variables{$+})?$variables{$+}:$1/egx; } # Parameters: par1 - shell command # par2 - 'collect output' flag # Action: par1 will be executed as a shell command in a child # process. After process has been created, subroutine creates an # entry in the %children hash, and returns the pid of the child # process. If process creation failed, undef is returned. After the # command has completed, the child process terminates and returns # command's exit code as its own exit value. # If par2 is defined and non-zero, command's standard output is # returned to the main process through a pipe. sub shell_cmd { my($cmd) = $_[0]; my($collect_output) = $_[1]; my($pid); local *READ_FH; # we need to use 'local *', since each time we enter # this procedure a new filehandle must be created that # will be returned from this procedure for external use # set up a pipe before calling fork() if ($collect_output && !pipe(READ_FH, WRITE_FH)) { log_msg(LOG_ERR, "Could not create pipe for command '$cmd' ($!)"); return undef; } # try to create a child process and return undef, if fork failed; # if fork was successful and we are in parent process, return the # pid of the child process $pid = fork(); if (!defined($pid)) { if ($collect_output) { close(READ_FH); close(WRITE_FH); } log_msg(LOG_ERR, "Could not fork command '$cmd' ($!)"); return undef; } elsif ($pid) { $children{$pid} = { "cmd" => $cmd, "fh" => undef, "open" => 0, "buffer" => "", "Desc" => undef, "Action" => undef, "Action2" => undef }; if ($collect_output) { close(WRITE_FH); $children{$pid}->{"fh"} = *READ_FH; $children{$pid}->{"open"} = 1; } log_msg(LOG_DEBUG, "Child $pid created for command '$cmd'"); return $pid; } # we are in the child process now... if ($collect_output) { # connect the standard output of the child process to the pipe # and make the standard output unbuffered close(READ_FH); if (!open(STDOUT, ">&WRITE_FH")) { exit(1); } select(STDOUT); $| = 1; close(WRITE_FH); } # if we have received SIGTERM, exit if ($terminate) { exit(0); } # execute the command inside the child process; if exec() fails, exit exec("$cmd"); exit(1); } # Parameters: par1 - shell command for reporting # par2 - reference to a hash or an array # Action: par1 will be executed as a shell command in a child process, and # contents of array par2 (or keys of hash par2) are fed to its # standard input. After process has been created, subroutine creates # an entry in the %children hash, and returns the pid of the child # process. If process creation failed, undef is returned. # After the command has completed, the child process # terminates and returns command's exit code as its own exit value. sub pipe_cmd { my($cmd) = $_[0]; my($ref) = $_[1]; my($pid, $elem); # try to create a child process and return undef, if fork failed; # if fork was successful and we are in parent process, return the # pid of the child process $pid = fork(); if (!defined($pid)) { log_msg(LOG_ERR, "Could not fork command '$cmd' ($!)"); return undef; } elsif ($pid) { $children{$pid} = { "cmd" => $cmd, "fh" => undef, "open" => 0, "buffer" => "", "Desc" => undef, "Action" => undef, "Action2" => undef }; log_msg(LOG_DEBUG, "Child $pid created for command '$cmd'"); return $pid; } # we are in the child process now... # if we have received SIGTERM, exit; otherwise fork the command if ($terminate) { exit(0); } else { $pid = open(CMDPIPE, "| $cmd"); } if (defined($pid)) { # if the main SEC process has sent us SIGTERM meanwhile, send SIGTERM # to the command and exit; otherwise set the signal handler for SIGTERM if ($terminate) { kill('TERM', $pid); exit(0); } else { $SIG{TERM} = sub { kill('TERM', $pid); exit(0); }; } # ignore SIGPIPE if the command has died or has closed the pipe $SIG{PIPE} = 'IGNORE'; # write data to pipe select CMDPIPE; $| = 1; if (ref($ref) eq "HASH") { while ($elem = each(%{$ref})) { print CMDPIPE $elem, "\n"; } } else { foreach $elem (@{$ref}) { print CMDPIPE $elem, "\n"; } } # In some perl versions the close() function is buggy, and although # SIGPIPE is ignored, close() still sets $? variable to signal an # error, if the forked command does not read its stdin. To overcome # this problem, IO::Handle->flush() must be called before close(), # since this forces the close() function to set $? correctly CMDPIPE->flush(); # note that close() does not return until the command has completed close(CMDPIPE); exit($? >> 8); } exit(1); } # Parameters: par1 - reference to a list of actions # par2 - event description text # Action: execute actions in a given action list sub execute_actionlist { my($actionlist) = $_[0]; my($text) = $_[1]; my($text2, $i, $j, $nbytes); my($file, $cmdline, $context, $lifetime, $list); my($createafter, $conffile, $ruleid); my($event, @event, $alias, @aliases, @params); my($variable, $value, $code, @retval, $evalok); my($key, $ref); $i = 0; $j = scalar(@{$actionlist}); while ($i < $j) { if ($actionlist->[$i] == NONE) { ++$i; } elsif ($actionlist->[$i] == LOGONLY) { $event = $actionlist->[$i+1]; substitute_var($event, $text); log_msg(LOG_NOTICE, $event); $i += 2; } elsif ($actionlist->[$i] == WRITE) { $file = $actionlist->[$i+1]; $event = $actionlist->[$i+2]; substitute_var($file, $text); substitute_var($event, $text); log_msg(LOG_DEBUG, "Writing event '$event' to file $file"); if ($file eq "-") { select(STDOUT); $| = 1; print STDOUT "$event\n"; } elsif (-e $file && ! -f $file && ! -p $file) { log_msg(LOG_WARN, "Can't write event '$event' to file $file!", "(not a regular file or pipe)"); } elsif (-p $file) { if (sysopen(WRITEFILE, $file, O_WRONLY | O_NONBLOCK)) { $nbytes = syswrite(WRITEFILE, "$event\n"); close(WRITEFILE); if (!defined($nbytes) || $nbytes != length($event) + 1) { log_msg(LOG_WARN, "Error when writing event '$event' to pipe $file!"); } } else { log_msg(LOG_WARN, "Can't open pipe $file for writing event '$event'!"); } } else { if (open(WRITEFILE, ">>$file")) { print WRITEFILE "$event\n"; close(WRITEFILE); } else { log_msg(LOG_WARN, "Can't open file $file for writing event '$event'!"); } } $i += 3; } elsif ($actionlist->[$i] == SHELLCOMMAND) { $cmdline = $actionlist->[$i+1]; $text2 = $text; # if -quoting flag was specified, mask apostrophes in $text2 # and put $text2 inside apostrophes if ($quoting) { $text2 =~ s/'/'\\''/g; $text2 = "'" . $text2 . "'"; } substitute_var($cmdline, $text2); log_msg(LOG_INFO, "Executing shell command '$cmdline'"); shell_cmd($cmdline); $i += 2; } elsif ($actionlist->[$i] == SPAWN) { $cmdline = $actionlist->[$i+1]; $text2 = $text; # if -quoting flag was specified, mask apostrophes in $text2 # and put $text2 inside apostrophes if ($quoting) { $text2 =~ s/'/'\\''/g; $text2 = "'" . $text2 . "'"; } substitute_var($cmdline, $text2); log_msg(LOG_INFO, "Spawning shell command '$cmdline'"); shell_cmd($cmdline, 1); $i += 2; } elsif ($actionlist->[$i] == PIPE) { $event = $actionlist->[$i+1]; $cmdline = $actionlist->[$i+2]; substitute_var($event, $text); substitute_var($cmdline, $text); log_msg(LOG_INFO, "Feeding event '$event' to shell command '$cmdline'"); if (length($cmdline)) { pipe_cmd($cmdline, [ $event ]); } else { select(STDOUT); $| = 1; print STDOUT "$event\n"; } $i += 3; } elsif ($actionlist->[$i] == CREATECONTEXT) { $context = $actionlist->[$i+1]; $lifetime = $actionlist->[$i+2]; $list = $actionlist->[$i+3]; substitute_var($context, $text); substitute_var($lifetime, $text); log_msg(LOG_DEBUG, "Creating context '$context'"); if ($lifetime =~ /^\s*0*(\d+)\s*$/) { $lifetime = $1; if (exists($context_list{$context})) { $context_list{$context}->{"Time"} = time(); $context_list{$context}->{"Window"} = $lifetime; $context_list{$context}->{"Buffer"} = []; $context_list{$context}->{"Action"} = $list; $context_list{$context}->{"Desc"} = $text; } else { $context_list{$context} = { "Time" => time(), "Window" => $lifetime, "Buffer" => [], "Action" => $list, "Desc" => $text, "Aliases" => [ $context ] }; } } else { log_msg(LOG_WARN, "Invalid lifetime '$lifetime' for context '$context', can't create"); } $i += 4; } elsif ($actionlist->[$i] == DELETECONTEXT) { $context = $actionlist->[$i+1]; substitute_var($context, $text); log_msg(LOG_DEBUG, "Deleting context '$context'"); if (exists($context_list{$context}) && !exists($context_list{$context}->{"DeleteInProgress"})) { @aliases = @{$context_list{$context}->{"Aliases"}}; foreach $alias (@aliases) { delete $context_list{$alias}; log_msg(LOG_DEBUG, "Context '$alias' deleted"); } } else { log_msg(LOG_WARN, "Context '$context' does not exist or is going through deletion, can't delete"); } $i += 2; } elsif ($actionlist->[$i] == OBSOLETECONTEXT) { $context = $actionlist->[$i+1]; substitute_var($context, $text); log_msg(LOG_DEBUG, "Obsoleting context '$context'"); if (exists($context_list{$context}) && !exists($context_list{$context}->{"DeleteInProgress"})) { $context_list{$context}->{"Window"} = -1; valid_context($context); } else { log_msg(LOG_WARN, "Context '$context' does not exist or is going through deletion, can't obsolete"); } $i += 2; } elsif ($actionlist->[$i] == SETCONTEXT) { $context = $actionlist->[$i+1]; $lifetime = $actionlist->[$i+2]; $list = $actionlist->[$i+3]; substitute_var($context, $text); substitute_var($lifetime, $text); log_msg(LOG_DEBUG, "Changing settings for context '$context'"); if ($lifetime =~ /^\s*0*(\d+)\s*$/) { $lifetime = $1; if (exists($context_list{$context})) { $context_list{$context}->{"Time"} = time(); $context_list{$context}->{"Window"} = $lifetime; $context_list{$context}->{"Action"} = $list; $context_list{$context}->{"Desc"} = $text; } else { log_msg(LOG_WARN, "Context '$context' does not exist, can't change settings"); } } else { log_msg(LOG_WARN, "Invalid lifetime '$lifetime' for context '$context', can't change settings"); } $i += 4; } elsif ($actionlist->[$i] == ALIAS) { $context = $actionlist->[$i+1]; $alias = $actionlist->[$i+2]; substitute_var($context, $text); substitute_var($alias, $text); log_msg(LOG_DEBUG, "Creating alias '$alias' for context '$context'"); if (!exists($context_list{$context})) { log_msg(LOG_WARN, "Context '$context' does not exist, can't create alias"); } elsif (exists($context_list{$alias})) { log_msg(LOG_WARN, "Alias '$alias' already exists"); } else { push @{$context_list{$context}->{"Aliases"}}, $alias; $context_list{$alias} = $context_list{$context}; } $i += 3; } elsif ($actionlist->[$i] == UNALIAS) { $alias = $actionlist->[$i+1]; substitute_var($alias, $text); log_msg(LOG_DEBUG, "Removing alias '$alias'"); if (exists($context_list{$alias}) && !exists($context_list{$alias}->{"DeleteInProgress"})) { @aliases = grep($_ ne $alias, @{$context_list{$alias}->{"Aliases"}}); if (scalar(@aliases)) { $context_list{$alias}->{"Aliases"} = [ @aliases ]; } else { log_msg(LOG_DEBUG, "Alias '$alias' was the last reference to a context"); } delete $context_list{$alias}; } else { log_msg(LOG_WARN, "Alias '$alias' does not exist or its context is going through deletion, can't remove"); } $i += 2; } elsif ($actionlist->[$i] == ADD) { $context = $actionlist->[$i+1]; $event = $actionlist->[$i+2]; substitute_var($context, $text); substitute_var($event, $text); log_msg(LOG_DEBUG, "Adding event '$event' to context '$context'"); if (!exists($context_list{$context})) { $context_list{$context} = { "Time" => time(), "Window" => 0, "Buffer" => [], "Action" => [], "Desc" => "", "Aliases" => [ $context ] }; } @event = split(/\n/, $event); if (!$evstoresize || scalar(@{$context_list{$context}->{"Buffer"}}) + scalar(@event) <= $evstoresize) { push @{$context_list{$context}->{"Buffer"}}, @event; } else { log_msg(LOG_WARN, "Can't add event '$event' to context '$context', store full"); } $i += 3; } elsif ($actionlist->[$i] == FILL) { $context = $actionlist->[$i+1]; $event = $actionlist->[$i+2]; substitute_var($context, $text); substitute_var($event, $text); log_msg(LOG_DEBUG, "Filling context '$context' with event '$event'"); if (!exists($context_list{$context})) { $context_list{$context} = { "Time" => time(), "Window" => 0, "Buffer" => [], "Action" => [], "Desc" => "", "Aliases" => [ $context ] }; } @event = split(/\n/, $event); if (!$evstoresize || scalar(@event) <= $evstoresize) { $context_list{$context}->{"Buffer"} = [ @event ]; } else { log_msg(LOG_WARN, "Can't fill context '$context' with event '$event', store full"); } $i += 3; } elsif ($actionlist->[$i] == REPORT) { $context = $actionlist->[$i+1]; $cmdline = $actionlist->[$i+2]; substitute_var($context, $text); substitute_var($cmdline, $text); log_msg(LOG_INFO, "Reporting the event store of context '$context' through shell command '$cmdline'"); if (!exists($context_list{$context})) { log_msg(LOG_WARN, "Context '$context' does not exist, can't report"); } elsif (!scalar(@{$context_list{$context}->{"Buffer"}})) { log_msg(LOG_WARN, "Event store of context '$context' is empty, can't report"); } else { if (length($cmdline)) { pipe_cmd($cmdline, $context_list{$context}->{"Buffer"}); } else { select(STDOUT); $| = 1; foreach $event (@{$context_list{$context}->{"Buffer"}}) { print STDOUT "$event\n"; } } } $i += 3; } elsif ($actionlist->[$i] == COPYCONTEXT) { $context = $actionlist->[$i+1]; $variable = $actionlist->[$i+2]; substitute_var($context, $text); log_msg(LOG_DEBUG, "Copying context '$context' to variable '%$variable'"); if (exists($context_list{$context})) { $value = join("\n", @{$context_list{$context}->{"Buffer"}}); $variables{$variable} = $value; log_msg(LOG_DEBUG, "Variable '%$variable' set to '$value'"); } else { log_msg(LOG_WARN, "Context '$context' does not exist, can't copy"); } $i += 3; } elsif ($actionlist->[$i] == EMPTYCONTEXT) { $context = $actionlist->[$i+1]; $variable = $actionlist->[$i+2]; substitute_var($context, $text); log_msg(LOG_DEBUG, "Emptying the event store of context '$context'"); if (exists($context_list{$context})) { if (length($variable)) { $value = join("\n", @{$context_list{$context}->{"Buffer"}}); $variables{$variable} = $value; log_msg(LOG_DEBUG, "Variable '%$variable' set to '$value'"); } $context_list{$context}->{"Buffer"} = []; } else { log_msg(LOG_WARN, "Context '$context' does not exist, can't empty"); } $i += 3; } elsif ($actionlist->[$i] == EVENT) { $createafter = $actionlist->[$i+1]; $event = $actionlist->[$i+2]; substitute_var($event, $text); @event = split(/\n/, $event); if ($createafter) { foreach $event (@event) { push @pending_events, [ time() + $createafter, $event ]; } } else { log_msg(LOG_DEBUG, "Creating event '$event'"); push @events, @event; } $i += 3; } elsif ($actionlist->[$i] == TEVENT) { $createafter = $actionlist->[$i+1]; $event = $actionlist->[$i+2]; substitute_var($createafter, $text); substitute_var($event, $text); @event = split(/\n/, $event); if ($createafter =~ /^\s*0*(\d+)\s*$/) { $createafter = $1; if ($createafter) { foreach $event (@event) { push @pending_events, [ time() + $createafter, $event ]; } } else { log_msg(LOG_DEBUG, "Creating event '$event'"); push @events, @event; } } else { log_msg(LOG_WARN, "Can't create event '$event' after '$createafter' seconds"); } $i += 3; } elsif ($actionlist->[$i] == RESET) { $conffile = $actionlist->[$i+1]; $ruleid = $actionlist->[$i+2]; $event = $actionlist->[$i+3]; substitute_var($event, $text); if (length($ruleid)) { $key = gen_key($conffile, $ruleid, $event); log_msg(LOG_DEBUG, "Cancelling the correlation operation with key '$key'"); $ref = $configuration{$conffile}->[$ruleid]; if (exists($ref->{"Operations"})) { delete $ref->{"Operations"}->{$key}; } delete $corr_list{$key}; } else { log_msg(LOG_DEBUG, "Cancelling all correlation operations started by rules from", $conffile, "to detect composite event '$event'"); foreach $ref (@{$configuration{$conffile}}) { $key = gen_key($conffile, $ref->{"ID"}, $event); if (exists($ref->{"Operations"})) { delete $ref->{"Operations"}->{$key}; } delete $corr_list{$key}; } } $i += 4; } elsif ($actionlist->[$i] == ASSIGN) { $variable = $actionlist->[$i+1]; $value = $actionlist->[$i+2]; substitute_var($value, $text); log_msg(LOG_DEBUG, "Assigning '$value' to variable '%$variable'"); $variables{$variable} = $value; $i += 3; } elsif ($actionlist->[$i] == EVAL) { $variable = $actionlist->[$i+1]; $code = $actionlist->[$i+2]; substitute_var($code, $text); log_msg(LOG_DEBUG, "Evaluating code '$code' and setting variable '%$variable'"); @retval = SEC::call_eval($code, 1); $evalok = shift @retval; foreach $value (@retval) { if (!defined($value)) { $value = ""; } } if ($evalok) { if (scalar(@retval) > 1) { $value = join("\n", @retval); $variables{$variable} = $value; log_msg(LOG_DEBUG, "Variable '%$variable' set to '$value'"); } elsif (scalar(@retval) == 1) { $variables{$variable} = $retval[0]; log_msg(LOG_DEBUG, "Variable '%$variable' set to '$retval[0]'"); } else { log_msg(LOG_DEBUG, "No value received for variable '%$variable'"); } } else { log_msg(LOG_ERR, "Error evaluating code '$code':", $retval[0]); } $i += 3; } elsif ($actionlist->[$i] == CALL) { $variable = $actionlist->[$i+1]; $code = $actionlist->[$i+2]; @params = @{$actionlist->[$i+3]}; log_msg(LOG_DEBUG, "Calling code '%$code->()' and setting variable '%$variable'"); if (ref($variables{$code}) eq "CODE") { foreach $value (@params) { substitute_var($value, $text); } @retval = eval { $variables{$code}->(@params) }; foreach $value (@retval) { if (!defined($value)) { $value = ""; } } if ($@) { log_msg(LOG_ERR, "Code '%$code->()' runtime error:", $@); } else { if (scalar(@retval) > 1) { $value = join("\n", @retval); $variables{$variable} = $value; log_msg(LOG_DEBUG, "Variable '%$variable' set to '$value'"); } elsif (scalar(@retval) == 1) { $variables{$variable} = $retval[0]; log_msg(LOG_DEBUG, "Variable '%$variable' set to '$retval[0]'"); } else { log_msg(LOG_DEBUG, "No value received for variable '%$variable'"); } } } else { log_msg(LOG_WARN, "Variable '%$code' is not a code reference"); } $i += 4; } } } ##################################################### # Functions related to processing of lists at runtime ##################################################### # Parameters: par1 - context # Action: check if context "par1" is valid at the moment and return 1 # if it is, otherwise return 0. If context "par1" is found to # be stale but is still present in the context list, it will be # removed from there, and if it has an action list, the action # list will be executed. sub valid_context { my($context) = $_[0]; my($alias, @aliases); if (exists($context_list{$context})) { # if the context has infinite lifetime or if its lifetime is not # exceeded, it is valid (TRUE) and return 1 if (!$context_list{$context}->{"Window"}) { return 1; } if (time() - $context_list{$context}->{"Time"} <= $context_list{$context}->{"Window"}) { return 1; } # if the deletion of the context is already in progress (a previous # invocation of valid_context(CONTEXT) has called execute_actionlist() # for the context CONTEXT, which has called valid_context(CONTEXT) # again), then don't call execute_actionlist() for the second time # but return 0 instead. if (exists($context_list{$context}->{"DeleteInProgress"})) { return 0; } # if the context is stale and its action-list-on-delete has not been # executed yet, execute it now log_msg(LOG_DEBUG, "Deleting stale context '$context'"); # execute action-list-on-delete if (scalar(@{$context_list{$context}->{"Action"}})) { $context_list{$context}->{"DeleteInProgress"} = 1; execute_actionlist($context_list{$context}->{"Action"}, $context_list{$context}->{"Desc"}); } # remove all names of the context from the list of contexts @aliases = @{$context_list{$context}->{"Aliases"}}; foreach $alias (@aliases) { delete $context_list{$alias}; log_msg(LOG_DEBUG, "Stale context '$alias' deleted"); } } return 0; } # Parameters: par1 - reference to a context formula # Action: calculate the truth value of the context formula par1; return 1 # if it is TRUE, and return 0 if it is FALSE. sub valid_formula { my($ref) = $_[0]; my($i, $j, $left, @right); my($evalresult, $evalok, $retval); my($code, $func, $args); $i = 0; $j = scalar(@{$ref}); $left = undef; @right = (); while ($i < $j) { if ($ref->[$i] == EXPRESSION) { if (defined($left)) { push @right, EXPRESSION; push @right, $ref->[$i+1]; } else { $left = valid_formula($ref->[$i+1]); } $i += 2; } elsif ($ref->[$i] == ECODE) { if (defined($left)) { push @right, ECODE; push @right, $ref->[$i+1]; } else { # if eval() for $code failed or returned false in boolean context # (undef, "", or 0), set $left to 0, otherwise set $left to 1 $code = $ref->[$i+1]; ($evalok, $evalresult) = SEC::call_eval($code, 0); if (!$evalok) { log_msg(LOG_ERR, "Error evaluating code '$code': $evalresult"); $left = 0; } else { $left = $evalresult?1:0; } } $i += 2; } elsif ($ref->[$i] == CCODE) { if (defined($left)) { push @right, CCODE; push @right, $ref->[$i+1]; push @right, $ref->[$i+2]; } else { $args = $ref->[$i+1]; $func = $ref->[$i+2]; # don't call $func->($args), since the valid_formula() function # could be called for the original context expression definition # (e.g., if the rule type is Calendar or if the context expression # is in []-brackets), and passing $args to the end user would allow # the user to modify the original context definition $retval = eval { $func->( ( @{$args} ) ) }; # if function call failed or returned false in boolean context # (undef, "", or 0), set $left to 0, otherwise set $left to 1 if ($@) { log_msg(LOG_ERR, "Context expression runtime error:", $@); $left = 0; } else { $left = $retval?1:0; } } $i += 3; } elsif ($ref->[$i] == OPERAND) { if (defined($left)) { push @right, OPERAND; push @right, $ref->[$i+1]; } else { $left = valid_context($ref->[$i+1]); } $i += 2; } elsif ($ref->[$i] == NEGATION) { # if the second operand is present, negation belongs to it, # otherwise negate the value of the first operand if (scalar(@right)) { push @right, NEGATION; } else { $left = $left?0:1; } ++$i; } elsif ($ref->[$i] == AND) { # the && operator has the short-circuiting capability and returns # the value of the last evaluated operand which is either 0 or 1 $left = $left && valid_formula(\@right); @right = (); ++$i; } elsif ($ref->[$i] == OR) { # the || operator has the short-circuiting capability and returns # the value of the last evaluated operand which is either 0 or 1 $left = $left || valid_formula(\@right); @right = (); ++$i; } } return $left; } # Parameters: par1 - number of lines that pattern was designed to match # par2 - pattern (string type) # Action: take par1 last lines from input buffer and concatenate them to # form a single string. Check if par2 is a substring in the formed # string (both par1 and par2 can contain newlines), and return 1 # if it is, otherwise return 0. sub match_substr { my($linecount) = $_[0]; my($substr) = $_[1]; my($line); $line = join("\n", @input_buffer[$bufpos - $linecount + 1 .. $bufpos]); return (index($line, $substr) != -1); } # Parameters: par1 - number of lines that pattern was designed to match # par2 - pattern (regular expression type) # par3 - reference to an array, where backreference values # $1, $2, .. will be saved. First element of an array will # be $0 that equals to line(s) that were found matching # Action: take par1 last lines from input buffer and concatenate them to # form a single string. Match the formed string with regular # expression par2, and if par2 contains bracketing constructs, # save backreference values $1, $2, .. to array par3. If formed # string matched regular expression, return 1, otherwise return 0 sub match_regexp { my($linecount) = $_[0]; my($regexp) = $_[1]; my($subst_ref) = $_[2]; my($line); $line = join("\n", @input_buffer[$bufpos - $linecount + 1 .. $bufpos]); if (@{$subst_ref} = ($line =~ /$regexp/)) { unshift @{$subst_ref}, $line; # create $0 that equals to $line return 1; } else { @{$subst_ref} = ( $line ); # create $0 that equals to $line return 0; } } # Parameters: par1 - number of lines that pattern was designed to match # par2 - pattern (perl function type) # par3 - reference to an array, where return values # $1, $2, .. will be saved. First element of an array will # be $0 that equals to line(s) that were found matching # Action: take par1 last lines from input buffer with corresponding source # names, and pass them to the perl function par2->(). # If the function returned value(s), save them as values $1, $2, .. # to array par3. If function returned an empty list or returned # a single value FALSE, return 0, otherwise return 1 sub match_perlfunc { my($linecount) = $_[0]; my($codeptr) = $_[1]; my($subst_ref) = $_[2]; my($line, @lines, @sources); my($size, $match); $line = join("\n", @input_buffer[$bufpos - $linecount + 1 .. $bufpos]); @lines = @input_buffer[$bufpos - $linecount + 1 .. $bufpos]; @sources = @input_sources[$bufpos - $linecount + 1 .. $bufpos]; @{$subst_ref} = eval { $codeptr->(@lines, @sources) }; if ($@) { log_msg(LOG_ERR, "(N)PerlFunc pattern runtime error:", $@); @{$subst_ref} = (); } $size = scalar(@{$subst_ref}); $match = $size > 1 || ($size == 1 && $subst_ref->[0]); unshift @{$subst_ref}, $line; # create $0 that equals to $line return $match; } # Parameters: par1 - reference to a source action list # par2 - reference to a destination action list # Action: action list par1 will be copied to par2 sub copy_actionlist { my($src_ref) = $_[0]; my($dest_ref) = $_[1]; my($i, $j); @{$dest_ref} = (); $i = 0; $j = scalar(@{$src_ref}); while ($i < $j) { if ($src_ref->[$i] == NONE) { push @{$dest_ref}, NONE; ++$i; } elsif ($src_ref->[$i] == LOGONLY) { push @{$dest_ref}, LOGONLY; push @{$dest_ref}, $src_ref->[$i+1]; $i += 2; } elsif ($src_ref->[$i] == WRITE) { push @{$dest_ref}, WRITE; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == SHELLCOMMAND) { push @{$dest_ref}, SHELLCOMMAND; push @{$dest_ref}, $src_ref->[$i+1]; $i += 2; } elsif ($src_ref->[$i] == SPAWN) { push @{$dest_ref}, SPAWN; push @{$dest_ref}, $src_ref->[$i+1]; $i += 2; } elsif ($src_ref->[$i] == PIPE) { push @{$dest_ref}, PIPE; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == CREATECONTEXT) { push @{$dest_ref}, CREATECONTEXT; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; push @{$dest_ref}, []; copy_actionlist($src_ref->[$i+3], $dest_ref->[$i+3]); $i += 4; } elsif ($src_ref->[$i] == DELETECONTEXT) { push @{$dest_ref}, DELETECONTEXT; push @{$dest_ref}, $src_ref->[$i+1]; $i += 2; } elsif ($src_ref->[$i] == OBSOLETECONTEXT) { push @{$dest_ref}, OBSOLETECONTEXT; push @{$dest_ref}, $src_ref->[$i+1]; $i += 2; } elsif ($src_ref->[$i] == SETCONTEXT) { push @{$dest_ref}, SETCONTEXT; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; push @{$dest_ref}, []; copy_actionlist($src_ref->[$i+3], $dest_ref->[$i+3]); $i += 4; } elsif ($src_ref->[$i] == ALIAS) { push @{$dest_ref}, ALIAS; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == UNALIAS) { push @{$dest_ref}, UNALIAS; push @{$dest_ref}, $src_ref->[$i+1]; $i += 2; } elsif ($src_ref->[$i] == ADD) { push @{$dest_ref}, ADD; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == FILL) { push @{$dest_ref}, FILL; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == REPORT) { push @{$dest_ref}, REPORT; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == COPYCONTEXT) { push @{$dest_ref}, COPYCONTEXT; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == EMPTYCONTEXT) { push @{$dest_ref}, EMPTYCONTEXT; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == EVENT) { push @{$dest_ref}, EVENT; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == TEVENT) { push @{$dest_ref}, TEVENT; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == RESET) { push @{$dest_ref}, RESET; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; push @{$dest_ref}, $src_ref->[$i+3]; $i += 4; } elsif ($src_ref->[$i] == ASSIGN) { push @{$dest_ref}, ASSIGN; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == EVAL) { push @{$dest_ref}, EVAL; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } elsif ($src_ref->[$i] == CALL) { push @{$dest_ref}, CALL; push @{$dest_ref}, $src_ref->[$i+1]; push @{$dest_ref}, $src_ref->[$i+2]; push @{$dest_ref}, [ @{$src_ref->[$i+3]} ]; $i += 4; } } } # Parameters: par1 - reference to a source context # par2 - reference to a destination context # Action: context par1 will be copied to par2 sub copy_context { my($src_ref) = $_[0]; my($dest_ref) = $_[1]; my($i, $j); @{$dest_ref} = (); $i = 0; $j = scalar(@{$src_ref}); while ($i < $j) { if ($src_ref->[$i] == OPERAND) { push @{$dest_ref}, OPERAND; push @{$dest_ref}, $src_ref->[$i+1]; $i += 2; } elsif ($src_ref->[$i] == EXPRESSION) { push @{$dest_ref}, EXPRESSION; push @{$dest_ref}, []; copy_context($src_ref->[$i+1], $dest_ref->[$i+1]); $i += 2; } elsif ($src_ref->[$i] == ECODE) { push @{$dest_ref}, ECODE; push @{$dest_ref}, $src_ref->[$i+1]; $i += 2; } elsif ($src_ref->[$i] == CCODE) { push @{$dest_ref}, CCODE; push @{$dest_ref}, [ @{$src_ref->[$i+1]} ]; push @{$dest_ref}, $src_ref->[$i+2]; $i += 3; } else { push @{$dest_ref}, $src_ref->[$i]; ++$i; } } } # Parameters: par1 - reference to the array of replacements # par2, par3, .. - strings that will go through replacement # procedure # par n - token that special variables start with # Action: Strings par2, par3, .. will be searched for special variables # (like $0, $1, $2, ..) that will be replaced with 1st, 2nd, .. # element from array par1. If the token symbol is followed by # another token symbol, they will be replaced by a single token # (e.g., $$ -> $). sub subst_string { my($subst_ref) = shift @_; my($token) = pop @_; my($token2, $msg); # variable will not be substituted if it doesn't exist or its value is undef $token2 = quotemeta($token); foreach $msg (@_) { if (index($msg, $token) == -1) { next; } $msg =~ s/$token2(\d+|$token2)/ ($1 eq $token)?$token: (defined($subst_ref->[$1])?$subst_ref->[$1]:"$token$1")/egx; } } # Parameters: par1 - reference to the array of replacements # par2, par3, .. - regular expressions that will go through # replacement procedure # par n - token that special variables start with # Action: Regular expressions par2, par3, .. will be searched for special # variables (like $1, $2, ..) that will be replaced with 1st, # 2nd, .. element from array par1 sub subst_regexp { my($subst_ref) = shift @_; my($token) = pop @_; my($subst, @subst_modified); @subst_modified = @{$subst_ref}; foreach $subst (@subst_modified) { if (defined($subst)) { $subst = quotemeta($subst); } } subst_string(\@subst_modified, @_, $token); } # Parameters: par1 - reference to the array of replacements # par2 - reference to a context formula # par3 - token that special variables start with # Action: Context formula par2 will be searched for special variables # (like $1, $2, ..) that will be replaced with 1st, 2nd, .. element # from array par1 sub subst_context { my($subst_ref) = $_[0]; my($ref) = $_[1]; my($token) = $_[2]; my($i, $j); $i = 0; $j = scalar(@{$ref}); while ($i < $j) { if ($ref->[$i] == OPERAND) { subst_string($subst_ref, $ref->[$i+1], $token); $i += 2; } elsif ($ref->[$i] == EXPRESSION) { subst_context($subst_ref, $ref->[$i+1], $token); $i += 2; } elsif ($ref->[$i] == ECODE) { subst_string($subst_ref, $ref->[$i+1], $token); $i += 2; } elsif ($ref->[$i] == CCODE) { subst_string($subst_ref, @{$ref->[$i+1]}, $token); $i += 3; } else { ++$i; } } } # Parameters: par1 - reference to the array of replacements # par2 - reference to action list # par3 - token that special variables start with # Action: action list par2 will be searched for special variables # (like $1, $2, ..) that will be replaced with 1st, 2nd, .. # element from array par1 sub subst_actionlist { my($subst_ref) = $_[0]; my($actionlist) = $_[1]; my($token) = $_[2]; my($subst, @subst_modified); my($i, $j); # mask %-signs in substitutions, in order to prevent incorrect # %-variable interpretations @subst_modified = @{$subst_ref}; foreach $subst (@subst_modified) { if (defined($subst)) { $subst =~ s/%/%%/g; } } # process the action list $i = 0; $j = scalar(@{$actionlist}); while ($i < $j) { if ($actionlist->[$i] == NONE) { ++$i; } elsif ($actionlist->[$i] == LOGONLY) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); $i += 2; } elsif ($actionlist->[$i] == WRITE) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == SHELLCOMMAND) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); $i += 2; } elsif ($actionlist->[$i] == SPAWN) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); $i += 2; } elsif ($actionlist->[$i] == PIPE) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == CREATECONTEXT) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); subst_actionlist($subst_ref, $actionlist->[$i+3], $token); $i += 4; } elsif ($actionlist->[$i] == DELETECONTEXT) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); $i += 2; } elsif ($actionlist->[$i] == OBSOLETECONTEXT) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); $i += 2; } elsif ($actionlist->[$i] == SETCONTEXT) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); subst_actionlist($subst_ref, $actionlist->[$i+3], $token); $i += 4; } elsif ($actionlist->[$i] == ALIAS) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == UNALIAS) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); $i += 2; } elsif ($actionlist->[$i] == ADD) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == FILL) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == REPORT) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == COPYCONTEXT) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); $i += 3; } elsif ($actionlist->[$i] == EMPTYCONTEXT) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); $i += 3; } elsif ($actionlist->[$i] == EVENT) { subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == TEVENT) { subst_string(\@subst_modified, $actionlist->[$i+1], $token); subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == RESET) { subst_string(\@subst_modified, $actionlist->[$i+3], $token); $i += 4; } elsif ($actionlist->[$i] == ASSIGN) { subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == EVAL) { subst_string(\@subst_modified, $actionlist->[$i+2], $token); $i += 3; } elsif ($actionlist->[$i] == CALL) { subst_string(\@subst_modified, @{$actionlist->[$i+3]}, $token); $i += 4; } } } # Parameters: par1 - reference to an element from list %corr_list # par2 - time # Action: search event-time list that is associated with element par1, # and remove those elements that are obsolete by time par2 sub update_times { my($ref) = $_[0]; my($time) = $_[1]; while (scalar(@{$ref->{"Times"}})) { if ($time - $ref->{"Times"}->[0] <= $ref->{"Window"}) { last; } shift @{$ref->{"Times"}}; } if (scalar(@{$ref->{"Times"}})) { $ref->{"Time"} = $ref->{"Times"}->[0]; } else { $ref->{"Time"} = 0; } } # Parameters: par1, par2, .. - strings # Action: calculate unique key for strings par1, par2, .. that will be # used in correlation lists to distinguish between differents events sub gen_key { return join(SEPARATOR, @_); } # Parameters: par1 - name of the configuration file # Action: search the rules from configuration file par1 and check, if # there is a matching rule for the current content of input buffer. # If matching rule is found, new element (that corresponds to # an event correlation operation) will be added to the list # %corr_list. Key for new element is calculated by calling gen_key # function: # gen_key(file name, rule number, textual description of event) sub process_rules { my($conffile) = $_[0]; my($key, $ref, $ref2); my($time, $match_found, $i); my($desc, $pattern2, $desc2); my($pid, $script); my($below_threshold, $inside_window); my($subst, @subst); my($context, $context2); my($action, $action2); foreach $ref (@{$configuration{$conffile}}) { # skip CALENDAR rule if ($ref->{"Type"} == CALENDAR) { next; } # check if the rule context expression must be evaluated before # comparing input line(s) with the pattern if ($ref->{"ContPreEval"}) { # if the value of the context expression is FALSE and the rule is # of type Pair*, look also for all active correlation operations # associated with the current rule and check if 2nd pattern matches if (!valid_formula($ref->{"Context"})) { if ( ($ref->{"Type"} == PAIR || $ref->{"Type"} == PAIR_W_WINDOW) && scalar(%{$ref->{"Operations"}}) ) { if (process_rules2($ref) && $ref->{"WhatNext2"} == DONTCONT) { return 1; } } next; } $context = $ref->{"Context"}; } # Check if last N lines of input buffer match the pattern # specified by rule (value of N is also specified by rule) # If match was found, set $match_found to 1 # If the pattern returned any values, assign them to @subst, # otherwise leave @subst empty if ($ref->{"PatType"} == REGEXP) { $match_found = match_regexp($ref->{"PatLines"}, $ref->{"Pattern"}, \@subst); } elsif ($ref->{"PatType"} == SUBSTR) { $match_found = match_substr($ref->{"PatLines"}, $ref->{"Pattern"}); @subst = (); } elsif ($ref->{"PatType"} == PERLFUNC) { $match_found = match_perlfunc($ref->{"PatLines"}, $ref->{"Pattern"}, \@subst); } elsif ($ref->{"PatType"} == NREGEXP) { $match_found = !match_regexp($ref->{"PatLines"}, $ref->{"Pattern"}, \@subst); } elsif ($ref->{"PatType"} == NSUBSTR) { $match_found = !match_substr($ref->{"PatLines"}, $ref->{"Pattern"}); @subst = (); } elsif ($ref->{"PatType"} == NPERLFUNC) { $match_found = !match_perlfunc($ref->{"PatLines"}, $ref->{"Pattern"}, \@subst); } elsif ($ref->{"PatType"} == TVALUE) { $match_found = $ref->{"Pattern"}; @subst = (); } # If match was found, process the event if ($match_found) { # Evaluate the context expression of the rule if (!scalar(@{$ref->{"Context"}})) { $context = []; } elsif (!$ref->{"ContPreEval"}) { if (scalar(@subst)) { $context = []; copy_context($ref->{"Context"}, $context); subst_context(\@subst, $context, '$'); } else { $context = $ref->{"Context"}; } # if the value of the context expression is FALSE and the rule is # of type Pair*, look also for all active correlation operations # associated with the current rule and check if 2nd pattern matches if (!valid_formula($context)) { if ( ($ref->{"Type"} == PAIR || $ref->{"Type"} == PAIR_W_WINDOW) && scalar(%{$ref->{"Operations"}}) ) { if (process_rules2($ref) && $ref->{"WhatNext2"} == DONTCONT) { return 1; } } next; } } # increment the counter that reflects the rule usage # (just for statistical purposes) ++$ref->{"MatchCount"}; # ------------------------------------------------------------ # SINGLE rule # ------------------------------------------------------------ if ($ref->{"Type"} == SINGLE) { $desc = $ref->{"Desc"}; if (scalar(@subst)) { $action = []; copy_actionlist($ref->{"Action"}, $action); subst_actionlist(\@subst, $action, '$'); subst_string(\@subst, $desc, '$'); } else { $action = $ref->{"Action"}; } execute_actionlist($action, $desc); } # ------------------------------------------------------------ # SINGLE_W_SCRIPT rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == SINGLE_W_SCRIPT) { $desc = $ref->{"Desc"}; $script = $ref->{"Script"}; if (scalar(@subst)) { $action = []; $action2 = []; copy_actionlist($ref->{"Action"}, $action); copy_actionlist($ref->{"Action2"}, $action2); subst_actionlist(\@subst, $action, '$'); subst_actionlist(\@subst, $action2, '$'); subst_string(\@subst, $desc, $script, '$'); } else { $action = $ref->{"Action"}; $action2 = $ref->{"Action2"}; } $pid = pipe_cmd($script, \%context_list); if (defined($pid)) { $children{$pid}->{"Desc"} = $desc; $children{$pid}->{"Action"} = $action; $children{$pid}->{"Action2"} = $action2; } } # ------------------------------------------------------------ # SINGLE_W_SUPPRESS rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == SINGLE_W_SUPPRESS) { $desc = $ref->{"Desc"}; if (scalar(@subst)) { subst_string(\@subst, $desc, '$'); } $key = gen_key($conffile, $ref->{"ID"}, $desc); $time = time(); # if there is no event correlation operation for the key, or # the operation with the key has expired, start the new operation if (!exists($corr_list{$key}) || $time - $corr_list{$key}->{"Time"} > $ref->{"Window"}) { if (scalar(@subst)) { $action = []; copy_actionlist($ref->{"Action"}, $action); subst_actionlist(\@subst, $action, '$'); } else { $action = $ref->{"Action"}; } $corr_list{$key} = { "Time" => $time, "Type" => $ref->{"Type"}, "File" => $conffile, "ID" => $ref->{"ID"}, "Window" => $ref->{"Window"}, "Context" => $context, "Desc" => $desc, "Action" => $action }; execute_actionlist($action, $desc); } } # ------------------------------------------------------------ # PAIR rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == PAIR) { $desc = $ref->{"Desc"}; if (scalar(@subst)) { subst_string(\@subst, $desc, '$'); } $key = gen_key($conffile, $ref->{"ID"}, $desc); $time = time(); # if there is no event correlation operation for the key, or # the operation with the key has expired, start the new operation if ( !exists($corr_list{$key}) || ($ref->{"Window"} && $time - $corr_list{$key}->{"Time"} > $ref->{"Window"}) ) { $pattern2 = $ref->{"Pattern2"}; $desc2 = $ref->{"Desc2"}; if (scalar(@subst)) { $action = []; copy_actionlist($ref->{"Action"}, $action); subst_actionlist(\@subst, $action, '$'); $action2 = []; copy_actionlist($ref->{"Action2"}, $action2); $context2 = []; copy_context($ref->{"Context2"}, $context2); if ($ref->{"PatType2"} == REGEXP || $ref->{"PatType2"} == NREGEXP) { subst_regexp(\@subst, $pattern2, '$'); $pattern2 = qr/$pattern2/; # mask all $-symbols in substitutions, in order to prevent # false interpretations when the second pattern matches foreach $subst (@subst) { if (defined($subst)) { $subst =~ s/\$/\$\$/g; } } subst_string(\@subst, $desc2, '%'); subst_actionlist(\@subst, $action2, '%'); subst_context(\@subst, $context2, '%'); } elsif ($ref->{"PatType2"} == PERLFUNC || $ref->{"PatType2"} == NPERLFUNC) { # mask all $-symbols in substitutions, in order to prevent # false interpretations when the second pattern matches foreach $subst (@subst) { if (defined($subst)) { $subst =~ s/\$/\$\$/g; } } subst_string(\@subst, $desc2, '%'); subst_actionlist(\@subst, $action2, '%'); subst_context(\@subst, $context2, '%'); } elsif ($ref->{"PatType2"} == SUBSTR || $ref->{"PatType2"} == NSUBSTR) { subst_string(\@subst, $pattern2, $desc2, '$'); subst_actionlist(\@subst, $action2, '$'); subst_context(\@subst, $context2, '$'); } else { subst_string(\@subst, $desc2, '$'); subst_actionlist(\@subst, $action2, '$'); subst_context(\@subst, $context2, '$'); } } else { $action = $ref->{"Action"}; $action2 = $ref->{"Action2"}; $context2 = $ref->{"Context2"}; } $corr_list{$key} = { "Time" => $time, "Type" => $ref->{"Type"}, "File" => $conffile, "ID" => $ref->{"ID"}, "Window" => $ref->{"Window"}, "Context" => $context, "Desc" => $desc, "Action" => $action, "Pattern2" => $pattern2, "Context2" => $context2, "Desc2" => $desc2, "Action2" => $action2 }; $ref->{"Operations"}->{$key} = $corr_list{$key}; execute_actionlist($action, $desc); } } # ------------------------------------------------------------ # PAIR_W_WINDOW rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == PAIR_W_WINDOW) { $desc = $ref->{"Desc"}; if (scalar(@subst)) { subst_string(\@subst, $desc, '$'); } $key = gen_key($conffile, $ref->{"ID"}, $desc); $time = time(); # if there is an event correlation operation for the key and # the operation has expired, execute the first action list and # terminate the operation if (exists($corr_list{$key}) && $time - $corr_list{$key}->{"Time"} > $ref->{"Window"}) { execute_actionlist($corr_list{$key}->{"Action"}, $desc); delete $corr_list{$key}; delete $ref->{"Operations"}->{$key}; } # if there is no event correlation operation for the key, # start the new operation if (!exists($corr_list{$key})) { $pattern2 = $ref->{"Pattern2"}; $desc2 = $ref->{"Desc2"}; if (scalar(@subst)) { $action = []; copy_actionlist($ref->{"Action"}, $action); subst_actionlist(\@subst, $action, '$'); $action2 = []; copy_actionlist($ref->{"Action2"}, $action2); $context2 = []; copy_context($ref->{"Context2"}, $context2); if ($ref->{"PatType2"} == REGEXP || $ref->{"PatType2"} == NREGEXP) { subst_regexp(\@subst, $pattern2, '$'); $pattern2 = qr/$pattern2/; # mask all $-symbols in substitutions, in order to prevent # false interpretations when the second pattern matches foreach $subst (@subst) { if (defined($subst)) { $subst =~ s/\$/\$\$/g; } } subst_string(\@subst, $desc2, '%'); subst_actionlist(\@subst, $action2, '%'); subst_context(\@subst, $context2, '%'); } elsif ($ref->{"PatType2"} == PERLFUNC || $ref->{"PatType2"} == NPERLFUNC) { # mask all $-symbols in substitutions, in order to prevent # false interpretations when the second pattern matches foreach $subst (@subst) { if (defined($subst)) { $subst =~ s/\$/\$\$/g; } } subst_string(\@subst, $desc2, '%'); subst_actionlist(\@subst, $action2, '%'); subst_context(\@subst, $context2, '%'); } elsif ($ref->{"PatType2"} == SUBSTR || $ref->{"PatType2"} == NSUBSTR) { subst_string(\@subst, $pattern2, $desc2, '$'); subst_actionlist(\@subst, $action2, '$'); subst_context(\@subst, $context2, '$'); } else { subst_string(\@subst, $desc2, '$'); subst_actionlist(\@subst, $action2, '$'); subst_context(\@subst, $context2, '$'); } } else { $action = $ref->{"Action"}; $action2 = $ref->{"Action2"}; $context2 = $ref->{"Context2"}; } $corr_list{$key} = { "Time" => $time, "Type" => $ref->{"Type"}, "File" => $conffile, "ID" => $ref->{"ID"}, "Window" => $ref->{"Window"}, "Context" => $context, "Desc" => $desc, "Action" => $action, "Pattern2" => $pattern2, "Context2" => $context2, "Desc2" => $desc2, "Action2" => $action2 }; $ref->{"Operations"}->{$key} = $corr_list{$key}; } } # ------------------------------------------------------------ # SINGLE_W_THRESHOLD rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == SINGLE_W_THRESHOLD) { $desc = $ref->{"Desc"}; if (scalar(@subst)) { subst_string(\@subst, $desc, '$'); } $key = gen_key($conffile, $ref->{"ID"}, $desc); $time = time(); # if there is no event correlation operation for the key, # start the new operation if (!exists($corr_list{$key})) { if (scalar(@subst)) { $action = []; $action2 = []; copy_actionlist($ref->{"Action"}, $action); copy_actionlist($ref->{"Action2"}, $action2); subst_actionlist(\@subst, $action, '$'); subst_actionlist(\@subst, $action2, '$'); } else { $action = $ref->{"Action"}; $action2 = $ref->{"Action2"}; } $corr_list{$key} = { "Time" => $time, "Type" => $ref->{"Type"}, "File" => $conffile, "ID" => $ref->{"ID"}, "Times" => [], "Window" => $ref->{"Window"}, "Context" => $context, "Desc" => $desc, "Action" => $action, "Action2" => $action2, "Threshold" => $ref->{"Threshold"} }; } $ref2 = $corr_list{$key}; # inside_window - TRUE if we are still in time window # below_threshold - TRUE if we were below threshold before this event $inside_window = ($time - $ref2->{"Time"} <= $ref->{"Window"}); $below_threshold = (scalar(@{$ref2->{"Times"}}) < $ref->{"Threshold"}); if ($inside_window && $below_threshold) { # if we are inside time window and below threshold, increase # the counter, and if new value of the counter equals to threshold, # execute the action list push @{$ref2->{"Times"}}, $time; if (scalar(@{$ref2->{"Times"}}) == $ref->{"Threshold"}) { execute_actionlist($ref2->{"Action"}, $desc); } } elsif ($below_threshold) { # if we are already outside time window but still below # threshold, slide the window forward push @{$ref2->{"Times"}}, $time; update_times($ref2, $time); } elsif (!$inside_window) { # if we are both outside time window and above threshold, then # the 1st action list was executed in the past and this event # correlation operation has been suppressing post-action events; # since the operation has expired, execute its 2nd action list # and start the new operation, because the event we have received # matches the rule. execute_actionlist($ref2->{"Action2"}, $desc); if (scalar(@subst)) { $action = []; $action2 = []; copy_actionlist($ref->{"Action"}, $action); copy_actionlist($ref->{"Action2"}, $action2); subst_actionlist(\@subst, $action, '$'); subst_actionlist(\@subst, $action2, '$'); } else { $action = $ref->{"Action"}; $action2 = $ref->{"Action2"}; } $corr_list{$key} = { "Time" => $time, "Type" => $ref->{"Type"}, "File" => $conffile, "ID" => $ref->{"ID"}, "Times" => [ $time ], "Window" => $ref->{"Window"}, "Context" => $context, "Desc" => $desc, "Action" => $action, "Action2" => $action2, "Threshold" => $ref->{"Threshold"} }; if ($ref->{"Threshold"} == 1) { execute_actionlist($action, $desc); } } } # ------------------------------------------------------------ # SINGLE_W_2_THRESHOLDS rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == SINGLE_W_2_THRESHOLDS) { $desc = $ref->{"Desc"}; if (scalar(@subst)) { subst_string(\@subst, $desc, '$'); } $key = gen_key($conffile, $ref->{"ID"}, $desc); $time = time(); # if there is no event correlation operation for the key, # start the new operation if (!exists($corr_list{$key})) { $desc2 = $ref->{"Desc2"}; if (scalar(@subst)) { $action = []; $action2 = []; copy_actionlist($ref->{"Action"}, $action); copy_actionlist($ref->{"Action2"}, $action2); subst_actionlist(\@subst, $action, '$'); subst_actionlist(\@subst, $action2, '$'); subst_string(\@subst, $desc2, '$'); } else { $action = $ref->{"Action"}; $action2 = $ref->{"Action2"}; } $corr_list{$key} = { "Time" => $time, "Type" => $ref->{"Type"}, "File" => $conffile, "ID" => $ref->{"ID"}, "Times" => [], "Window" => $ref->{"Window"}, "Context" => $context, "Desc" => $desc, "Action" => $action, "Threshold" => $ref->{"Threshold"}, "2ndPass" => 0, "Window2" => $ref->{"Window2"}, "Threshold2" => $ref->{"Threshold2"}, "Desc2" => $desc2, "Action2" => $action2 }; } $ref2 = $corr_list{$key}; # the 1st round of counting with a rising threshold if (!$ref2->{"2ndPass"}) { # inside_window - TRUE if we are still in time window # below_threshold - TRUE if we were below threshold before this event $inside_window = ($time - $ref2->{"Time"} <= $ref->{"Window"}); $below_threshold = (scalar(@{$ref2->{"Times"}}) < $ref->{"Threshold"}); if ($inside_window) { # if we are inside time window, increase the counter, and # if new value of the counter equals to threshold, execute # the action list and start to check 2nd threshold push @{$ref2->{"Times"}}, $time; if (scalar(@{$ref2->{"Times"}}) == $ref->{"Threshold"}) { $ref2->{"Time"} = $time; $ref2->{"2ndPass"} = 1; $ref2->{"Times"} = []; execute_actionlist($ref2->{"Action"}, $desc); } } elsif ($below_threshold) { # if we are already outside time window but still below # threshold, slide the window forward push @{$ref2->{"Times"}}, $time; update_times($ref2, $time); } # the 2nd round of counting with a falling threshold } else { # inside_window - TRUE if we are still in time window # below_threshold - TRUE if we were below threshold before this event $inside_window = ($time - $ref2->{"Time"} <= $ref->{"Window2"}); $below_threshold = (scalar(@{$ref2->{"Times"}}) < $ref->{"Threshold2"}); if ($inside_window && $below_threshold) { # if we are both inside time window and below threshold, # we can increase the counter (this threshold is considered # as crossed if counter > threshold, counter == threshold # is still permitted). push @{$ref2->{"Times"}}, $time; } elsif ($inside_window) { # if we are inside the time window and below_threshold == FALSE # then together with current event we have crossed the threshold # (counter > threshold). So we have to slide the window. if ($ref->{"Threshold2"}) { shift @{$ref2->{"Times"}}; push @{$ref2->{"Times"}}, $time; $ref2->{"Time"} = $ref2->{"Times"}->[0]; } else { $ref2->{"Time"} = $time; } } else { # if we have reached here, we must be outside time window # and also below threshold, since threshold crossing would # have already been detected by previous code block. # So we can execute the action list. execute_actionlist($ref2->{"Action2"}, $ref2->{"Desc2"}); # since action was just executed we can terminate this event # correlation operation and start the new one, because the event # we have received matches the rule. $desc2 = $ref->{"Desc2"}; if (scalar(@subst)) { $action = []; $action2 = []; copy_actionlist($ref->{"Action"}, $action); copy_actionlist($ref->{"Action2"}, $action2); subst_actionlist(\@subst, $action, '$'); subst_actionlist(\@subst, $action2, '$'); subst_string(\@subst, $desc2, '$'); } else { $action = $ref->{"Action"}; $action2 = $ref->{"Action2"}; } $corr_list{$key} = { "Time" => $time, "Type" => $ref->{"Type"}, "File" => $conffile, "ID" => $ref->{"ID"}, "Times" => [ $time ], "Window" => $ref->{"Window"}, "Context" => $context, "Desc" => $desc, "Action" => $action, "Threshold" => $ref->{"Threshold"}, "2ndPass" => 0, "Window2" => $ref->{"Window2"}, "Threshold2" => $ref->{"Threshold2"}, "Desc2" => $desc2, "Action2" => $action2 }; if ($ref->{"Threshold"} == 1) { $corr_list{$key}->{"2ndPass"} = 1; $corr_list{$key}->{"Times"} = []; execute_actionlist($action, $desc); } } } } # ------------------------------------------------------------ # SUPPRESS rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == SUPPRESS) { return 1; } # ------------------------------------------------------------ # if match was found and rule's continue-parameter # is set to DontCont, return 1, otherwise return 0 if ($ref->{"WhatNext"} == DONTCONT) { return 1; } } else { # if match was not found and rule is of type Pair*, look also for # all active correlation operations associated with the current # rule and check if 2nd pattern matches if ( ($ref->{"Type"} == PAIR || $ref->{"Type"} == PAIR_W_WINDOW) && scalar(%{$ref->{"Operations"}}) ) { if (process_rules2($ref) && $ref->{"WhatNext2"} == DONTCONT) { return 1; } } } } return 0; } # Parameters: par1 - reference to a rule # Action: search the event correlation operations associated with Pair* # rules and check, if there is a matching event for the current # content of input buffer. If there were 1 or more matches found, # return 1, otherwise return 0 sub process_rules2 { my($elem) = $_[0]; my($key, $ref, $ret); my($match_found, @subst); my($type, $window); my($pattype2, $patlines2, $desc2); my($context2, $action2); $ret = 0; # shows if matches were found $type = $elem->{"Type"}; $pattype2 = $elem->{"PatType2"}; $patlines2 = $elem->{"PatLines2"}; $window = $elem->{"Window"}; foreach $key (keys %{$elem->{"Operations"}}) { if (!exists($elem->{"Operations"}->{$key})) { next; } $ref = $elem->{"Operations"}->{$key}; # check if the rule context expression must be evaluated before # comparing input line(s) with the pattern if ($elem->{"ContPreEval2"}) { if (!valid_formula($ref->{"Context2"})) { next; } } # Check if last N lines of input buffer match the pattern # If match was found, set $match_found to 1 # If the pattern returned any values, assign them to @subst, # otherwise leave @subst empty if ($pattype2 == REGEXP) { $match_found = match_regexp($patlines2, $ref->{"Pattern2"}, \@subst); } elsif ($pattype2 == SUBSTR) { $match_found = match_substr($patlines2, $ref->{"Pattern2"}); @subst = (); } elsif ($pattype2 == PERLFUNC) { $match_found = match_perlfunc($patlines2, $ref->{"Pattern2"}, \@subst); } elsif ($pattype2 == NREGEXP) { $match_found = !match_regexp($patlines2, $ref->{"Pattern2"}, \@subst); } elsif ($pattype2 == NSUBSTR) { $match_found = !match_substr($patlines2, $ref->{"Pattern2"}); @subst = (); } elsif ($pattype2 == NPERLFUNC) { $match_found = !match_perlfunc($patlines2, $ref->{"Pattern2"}, \@subst); } elsif ($pattype2 == TVALUE) { $match_found = $ref->{"Pattern2"}; @subst = (); } # If match was found, process the event if ($match_found) { # Evaluate the context expression of the rule if (scalar(@{$ref->{"Context2"}}) && !$elem->{"ContPreEval2"}) { if (scalar(@subst)) { $context2 = []; copy_context($ref->{"Context2"}, $context2); subst_context(\@subst, $context2, '$'); } else { $context2 = $ref->{"Context2"}; } if (!valid_formula($context2)) { next; } } # processing for PAIR rule if ($type == PAIR) { # if we are inside time window, execute 2nd action list if (!$window || time() - $ref->{"Time"} <= $window) { $ret = 1; ++$elem->{"MatchCount"}; $desc2 = $ref->{"Desc2"}; if (scalar(@subst)) { $action2 = []; copy_actionlist($ref->{"Action2"}, $action2); subst_actionlist(\@subst, $action2, '$'); subst_string(\@subst, $desc2, '$'); } else { $action2 = $ref->{"Action2"}; } execute_actionlist($action2, $desc2); } # now we can terminate this event correlation operation, # since we have seen the event that matches the second pattern delete $corr_list{$key}; delete $elem->{"Operations"}->{$key}; } # processing for PAIR_W_WINDOW rule elsif ($type == PAIR_W_WINDOW) { # we can terminate this event correlation operation, # since we have seen the event that matches the second pattern # (in order to achieve good event ordering, execute 2nd action # list without checking the window) $ret = 1; ++$elem->{"MatchCount"}; $desc2 = $ref->{"Desc2"}; if (scalar(@subst)) { $action2 = []; copy_actionlist($ref->{"Action2"}, $action2); subst_actionlist(\@subst, $action2, '$'); subst_string(\@subst, $desc2, '$'); } else { $action2 = $ref->{"Action2"}; } execute_actionlist($action2, $desc2); delete $corr_list{$key}; delete $elem->{"Operations"}->{$key}; } } } # if there were 1 or more matches found, return 1, otherwise return 0 return $ret; } # Parameters: - # Action: search lists %corr_list, %context_list, @calendar and # @pending_events, performing timed tasks that are associated # with elements and removing obsolete elements sub process_lists { my($key, $ref, $config); my($time, $diff, $lastdayofmonth); my(@time, $event, @buffer); my($minute, $hour, $day, $month, $weekday); # remove obsolete elements from %context_list foreach $key (keys %context_list) { valid_context($key); } # move pending events that have become relevant from # @pending_events list to @events list if (scalar(@pending_events)) { @buffer = (); foreach $ref (@pending_events) { if (time() >= $ref->[0]) { $event = $ref->[1]; log_msg(LOG_DEBUG, "Creating event '$event'"); push @events, $event; } else { push @buffer, $ref; } } @pending_events = @buffer; } # process CALENDAR rules @time = localtime(time()); $minute = $time[1]; $hour = $time[2]; $day = $time[3]; $month = $time[4]; $weekday = $time[6]; $lastdayofmonth = ((localtime(time()+86400))[3] == 1); foreach $ref (@calendar) { # if we have already performed this task in current minute, skip if ($minute == $ref->{"LastMinute"} && $hour == $ref->{"LastHour"} && $day == $ref->{"LastDay"} && $month == $ref->{"LastMonth"} && $weekday == $ref->{"LastWeekday"}) { next; } # if one of the time conditions does not hold, skip if (!exists($ref->{"Minutes"}->{$minute})) { next; } if (!exists($ref->{"Hours"}->{$hour})) { next; } if (!exists($ref->{"Days"}->{$day}) && !($lastdayofmonth && exists($ref->{"Days"}->{"0"}))) { next; } if (!exists($ref->{"Months"}->{$month})) { next; } if (!exists($ref->{"Weekdays"}->{$weekday})) { next; } # check the context expression of the rule if (scalar(@{$ref->{"Context"}})) { if (!valid_formula($ref->{"Context"})) { next; } } # execute the action list of the calendar event # and save current time execute_actionlist($ref->{"Action"}, $ref->{"Desc"}); $ref->{"LastMinute"} = $minute; $ref->{"LastHour"} = $hour; $ref->{"LastDay"} = $day; $ref->{"LastMonth"} = $month; $ref->{"LastWeekday"} = $weekday; ++$ref->{"MatchCount"}; } # perform timed tasks that are associated with elements of # %corr_list and remove obsolete elements foreach $key (keys %corr_list) { if (!exists($corr_list{$key})) { next; } $ref = $corr_list{$key}; $time = time(); $diff = $time - $ref->{"Time"}; $config = $configuration{$ref->{"File"}}->[$ref->{"ID"}]; # ------------------------------------------------------------ # SINGLE_W_SUPPRESS rule # ------------------------------------------------------------ if ($ref->{"Type"} == SINGLE_W_SUPPRESS) { # if we are outside time window, list element is obsolete # and can be removed if ($diff > $ref->{"Window"}) { delete $corr_list{$key}; } } # ------------------------------------------------------------ # PAIR rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == PAIR) { # if we are outside time window, list elements are obsolete # and can be removed if ($ref->{"Window"} && $diff > $ref->{"Window"}) { delete $corr_list{$key}; delete $config->{"Operations"}->{$key}; } } # ------------------------------------------------------------ # PAIR_W_WINDOW rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == PAIR_W_WINDOW) { # if we are outside time window, 1st action must be executed; # after that the list elements are obsolete and can be removed if ($diff > $ref->{"Window"}) { execute_actionlist($ref->{"Action"}, $ref->{"Desc"}); delete $corr_list{$key}; delete $config->{"Operations"}->{$key}; } } # ------------------------------------------------------------ # SINGLE_W_THRESHOLD rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == SINGLE_W_THRESHOLD) { if ($diff > $ref->{"Window"}) { if (scalar(@{$ref->{"Times"}}) < $ref->{"Threshold"}) { # If we are outside time window and threshold is not exceeded, # try to slide the window. If all events are gone after sliding, # remove the list element as obsolete. update_times($ref, $time); if (!scalar(@{$ref->{"Times"}})) { delete $corr_list{$key}; } } else { # If we are outside time window and threshold is exceeded, # execute the 2nd action and remove the list element as obsolete. execute_actionlist($ref->{"Action2"}, $ref->{"Desc"}); delete $corr_list{$key}; } } } # ------------------------------------------------------------ # SINGLE_W_2_THRESHOLDS rule # ------------------------------------------------------------ elsif ($ref->{"Type"} == SINGLE_W_2_THRESHOLDS) { if (!$ref->{"2ndPass"}) { # If we are outside 1st time window, try to slide the window. # If all events are gone after sliding, remove the list element # as obsolete if ($diff > $ref->{"Window"}) { update_times($ref, $time); if (!scalar(@{$ref->{"Times"}})) { delete $corr_list{$key}; } } } else { # If we are outside 2nd time window and list element # has not been removed, we can conclude that 2nd threshold was # not exceeded, and so 2nd action can be executed. # After that the list element can be removed as obsolete. if ($diff > $ref->{"Window2"}) { execute_actionlist($ref->{"Action2"}, $ref->{"Desc2"}); delete $corr_list{$key}; } } } } } ################################################# # Functions related to reporting and data dumping ################################################# # Parameters: par1 - reference to a action list # Action: convert action list to a string representation sub actionlist2str { my($actionlist) = $_[0]; my($i, $j); my($result); $i = 0; $j = scalar(@{$actionlist}); $result = ""; while ($i < $j) { if ($actionlist->[$i] == NONE) { $result .= "none"; ++$i; } elsif ($actionlist->[$i] == LOGONLY) { $result .= "logonly " . $actionlist->[$i+1]; $i += 2; } elsif ($actionlist->[$i] == WRITE) { $result .= "write " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == SHELLCOMMAND) { $result .= "shellcmd " . $actionlist->[$i+1]; $i += 2; } elsif ($actionlist->[$i] == SPAWN) { $result .= "spawn " . $actionlist->[$i+1]; $i += 2; } elsif ($actionlist->[$i] == PIPE) { $result .= "pipe " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == CREATECONTEXT) { $result .= "create " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; if (scalar(@{$actionlist->[$i+3]})) { $result .= " (" . actionlist2str($actionlist->[$i+3]) . ")"; } $i += 4; } elsif ($actionlist->[$i] == DELETECONTEXT) { $result .= "delete " . $actionlist->[$i+1]; $i += 2; } elsif ($actionlist->[$i] == OBSOLETECONTEXT) { $result .= "obsolete " . $actionlist->[$i+1]; $i += 2; } elsif ($actionlist->[$i] == SETCONTEXT) { $result .= "set " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; if (scalar(@{$actionlist->[$i+3]})) { $result .= " (" . actionlist2str($actionlist->[$i+3]) . ")"; } $i += 4; } elsif ($actionlist->[$i] == ALIAS) { $result .= "alias " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == UNALIAS) { $result .= "unalias " . $actionlist->[$i+1]; $i += 2; } elsif ($actionlist->[$i] == ADD) { $result .= "add " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == FILL) { $result .= "fill " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == REPORT) { $result .= "report " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == COPYCONTEXT) { $result .= "copy " . $actionlist->[$i+1] . " %" . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == EMPTYCONTEXT) { if (length($actionlist->[$i+2])) { $result .= "empty " . $actionlist->[$i+1] . " %" . $actionlist->[$i+2]; } else { $result .= "empty " . $actionlist->[$i+1]; } $i += 3; } elsif ($actionlist->[$i] == EVENT) { $result .= "event " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == TEVENT) { $result .= "tevent " . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == RESET) { $result .= "reset " . $actionlist->[$i+2] . " " . $actionlist->[$i+3]; $i += 4; } elsif ($actionlist->[$i] == ASSIGN) { $result .= "assign %" . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == EVAL) { $result .= "eval %" . $actionlist->[$i+1] . " " . $actionlist->[$i+2]; $i += 3; } elsif ($actionlist->[$i] == CALL) { $result .= "call %" . $actionlist->[$i+1] . " %" . $actionlist->[$i+2] . " " . join(" ", @{$actionlist->[$i+3]}); $i += 4; } else { $result .= "unknown action type"; } $result .= "; "; } return $result; } # Parameters: par1 - pattern type # par2 - pattern lines # par3 - pattern # Action: convert pattern to a printable representation sub pattern2str { my($type) = $_[0]; my($lines) = $_[1]; my($pattern) = $_[2]; if ($type == SUBSTR) { return "substring for $lines line(s): $pattern"; } elsif ($type == REGEXP) { return "regexp for $lines line(s): $pattern"; } elsif ($type == PERLFUNC) { return "perlfunc for $lines line(s): $pattern"; } elsif ($type == NSUBSTR) { return "negative substring for $lines line(s): $pattern"; } elsif ($type == NREGEXP) { return "negative regexp for $lines line(s): $pattern"; } elsif ($type == NPERLFUNC) { return "negative perlfunc for $lines line(s): $pattern"; } elsif ($type == TVALUE) { return "truth value: " . ($pattern?"TRUE":"FALSE"); } else { return "Unknown pattern type"; } } # Parameters: par1 - reference to a context formula # Action: convert given context to a printable representation sub context2str { my($ref) = $_[0]; my($i, $j, $op1, $op2); my(@stack, $result); $i = 0; $j = scalar(@{$ref}); @stack = (); while ($i < $j) { if ($ref->[$i] == EXPRESSION) { $op1 = $ref->[$i+1]; push @stack, "(" . context2str($op1) . ")"; $i += 2; } elsif ($ref->[$i] == ECODE) { $op1 = $ref->[$i+1]; push @stack, "=( " . $op1 . " )"; $i += 2; } elsif ($ref->[$i] == CCODE) { $op1 = $ref->[$i+1]; $op2 = $ref->[$i+2]; push @stack, join(" ", @{$op1}) . " -> " . $op2; $i += 3; } elsif ($ref->[$i] == OPERAND) { $op1 = $ref->[$i+1]; push @stack, $op1; $i += 2; } elsif ($ref->[$i] == NEGATION) { $op1 = pop @stack; push @stack, "!" . $op1; ++$i; } elsif ($ref->[$i] == AND) { $op2 = pop @stack; $op1 = pop @stack; push @stack, $op1 . " && " . $op2; ++$i; } elsif ($ref->[$i] == OR) { $op2 = pop @stack; $op1 = pop @stack; push @stack, $op1 . " || " . $op2; ++$i; } } $result = pop @stack; if (!defined($result)) { $result = ""; } return $result; } # Parameters: par1 - filehandle # par2 - list element key # par3 - reference to list element # Action: print given list element to the filehandle sub print_element { my($handle) = $_[0]; my($key) = $_[1]; my($ref) = $_[2]; my($config, $conffile, $id, $time); print $handle "Key:\t\t\t\t", $key, "\n"; print $handle "Start of correlation operation:\t", scalar(localtime($ref->{"Time"})), "\n"; $conffile = $ref->{"File"}; $id = $ref->{"ID"}; $config = $configuration{$conffile}->[$id]; print $handle "Configuration file:\t\t", $conffile, "\n"; print $handle "Rule number:\t\t\t", $id+1, "\n"; print $handle "Rule internal ID:\t\t", $id, "\n"; if ($ref->{"Type"} == SINGLE_W_SUPPRESS) { print $handle "Type:\t\t\t\t"; print $handle "SingleWithSuppress\n"; if ($config->{"WhatNext"} == DONTCONT) { print $handle "Behaviour after match:\t\t", "don't continue\n"; } else { print $handle "Behaviour after match:\t\t", "take next\n"; } print $handle "Pattern:\t\t\t"; print $handle pattern2str($config->{"PatType"}, $config->{"PatLines"}, $config->{"Pattern"}); print $handle "\n"; print $handle "Context:\t\t\t"; print $handle context2str($ref->{"Context"}); print $handle "\n"; print $handle "Event:\t\t\t\t", $ref->{"Desc"}, "\n"; print $handle "Action:\t\t\t\t"; print $handle actionlist2str($ref->{"Action"}); print $handle "\n"; print $handle "Window:\t\t\t\t", $ref->{"Window"}, " seconds\n"; print $handle "\n"; } elsif ($ref->{"Type"} == PAIR) { print $handle "Type:\t\t\t\t"; print $handle "Pair\n"; if ($config->{"WhatNext"} == DONTCONT) { print $handle "Behaviour after 1st match:\t", "don't continue\n"; } else { print $handle "Behaviour after 1st match:\t", "take next\n"; } print $handle "1st Pattern:\t\t\t"; print $handle pattern2str($config->{"PatType"}, $config->{"PatLines"}, $config->{"Pattern"}); print $handle "\n"; print $handle "1st Context:\t\t\t"; print $handle context2str($ref->{"Context"}); print $handle "\n"; print $handle "1st Event:\t\t\t", $ref->{"Desc"}, "\n"; print $handle "1st Action:\t\t\t"; print $handle actionlist2str($ref->{"Action"}); print $handle "\n"; if ($config->{"WhatNext2"} == DONTCONT) { print $handle "Behaviour after 2nd match:\t", "don't continue\n"; } else { print $handle "Behaviour after 2nd match:\t", "take next\n"; } print $handle "2nd Pattern:\t\t\t"; print $handle pattern2str($config->{"PatType2"}, $config->{"PatLines2"}, $ref->{"Pattern2"}); print $handle "\n"; print $handle "2nd Context:\t\t\t"; print $handle context2str($ref->{"Context2"}); print $handle "\n"; print $handle "2nd Event:\t\t\t", $ref->{"Desc2"}, "\n"; print $handle "2nd Action:\t\t\t"; print $handle actionlist2str($ref->{"Action2"}); print $handle "\n"; if ($ref->{"Window"}) { print $handle "Window:\t\t\t\t", $ref->{"Window"}, " seconds\n"; } else { print $handle "Window:\t\t\t\t", "infinite\n"; } print $handle "\n"; } elsif ($ref->{"Type"} == PAIR_W_WINDOW) { print $handle "Type:\t\t\t\t"; print $handle "PairWithWindow\n"; if ($config->{"WhatNext"} == DONTCONT) { print $handle "Behaviour after 1st match:\t", "don't continue\n"; } else { print $handle "Behaviour after 1st match:\t", "take next\n"; } print $handle "1st Pattern:\t\t\t"; print $handle pattern2str($config->{"PatType"}, $config->{"PatLines"}, $config->{"Pattern"}); print $handle "\n"; print $handle "Context:\t\t\t"; print $handle context2str($ref->{"Context"}); print $handle "\n"; print $handle "1st Event:\t\t\t", $ref->{"Desc"}, "\n"; print $handle "1st Action:\t\t\t"; print $handle actionlist2str($ref->{"Action"}); print $handle "\n"; if ($config->{"WhatNext2"} == DONTCONT) { print $handle "Behaviour after 2nd match:\t", "don't continue\n"; } else { print $handle "Behaviour after 2nd match:\t", "take next\n"; } print $handle "2nd Pattern:\t\t\t"; print $handle pattern2str($config->{"PatType2"}, $config->{"PatLines2"}, $ref->{"Pattern2"}); print $handle "\n"; print $handle "2nd Context:\t\t\t"; print $handle context2str($ref->{"Context2"}); print $handle "\n"; print $handle "2nd Event:\t\t\t", $ref->{"Desc2"}, "\n"; print $handle "2nd Action:\t\t\t"; print $handle actionlist2str($ref->{"Action2"}); print $handle "\n"; print $handle "Window:\t\t\t\t", $ref->{"Window"}, " seconds\n"; print $handle "\n"; } elsif ($ref->{"Type"} == SINGLE_W_THRESHOLD) { print $handle "Type:\t\t\t\t"; print $handle "SingleWithThreshold\n"; if ($config->{"WhatNext"} == DONTCONT) { print $handle "Behaviour after match:\t\t", "don't continue\n"; } else { print $handle "Behaviour after match:\t\t", "take next\n"; } print $handle "Pattern:\t\t\t"; print $handle pattern2str($config->{"PatType"}, $config->{"PatLines"}, $config->{"Pattern"}); print $handle "\n"; print $handle "Context:\t\t\t"; print $handle context2str($ref->{"Context"}); print $handle "\n"; print $handle "Event:\t\t\t\t", $ref->{"Desc"}, "\n"; print $handle "1st Action:\t\t\t"; print $handle actionlist2str($ref->{"Action"}); print $handle "\n"; print $handle "2nd Action:\t\t\t"; print $handle actionlist2str($ref->{"Action2"}); print $handle "\n"; print $handle "Window:\t\t\t\t", $ref->{"Window"}, " seconds\n"; print $handle "Threshold:\t\t\t", $ref->{"Threshold"}, "\n"; print $handle scalar(@{$ref->{"Times"}}), " events observed at:\n"; foreach $time (@{$ref->{"Times"}}) { print $handle scalar(localtime($time)), "\n"; } print $handle "\n"; } elsif ($ref->{"Type"} == SINGLE_W_2_THRESHOLDS) { print $handle "Type:\t\t\t\t"; print $handle "SingleWith2Thresholds\n"; if ($config->{"WhatNext"} == DONTCONT) { print $handle "Behaviour after match:\t\t", "don't continue\n"; } else { print $handle "Behaviour after match:\t\t", "take next\n"; } print $handle "Pattern:\t\t\t"; print $handle pattern2str($config->{"PatType"}, $config->{"PatLines"}, $config->{"Pattern"}); print $handle "\n"; print $handle "Context:\t\t\t"; print $handle context2str($ref->{"Context"}); print $handle "\n"; print $handle "1st Event:\t\t\t", $ref->{"Desc"}, "\n"; print $handle "1st Action:\t\t\t"; print $handle actionlist2str($ref->{"Action"}); print $handle "\n"; print $handle "1st Window:\t\t\t", $ref->{"Window"}, " seconds\n"; print $handle "1st Threshold:\t\t\t", $ref->{"Threshold"}, "\n"; print $handle "2nd Event:\t\t\t", $ref->{"Desc2"}, "\n"; print $handle "2nd Action:\t\t\t"; print $handle actionlist2str($ref->{"Action2"}); print $handle "\n"; print $handle "2nd Window:\t\t\t", $ref->{"Window2"}, " seconds\n"; print $handle "2nd Threshold:\t\t\t", $ref->{"Threshold2"}, "\n"; print $handle scalar(@{$ref->{"Times"}}), " events observed at "; if ($ref->{"2ndPass"}) { print $handle "(checking 2nd threshold):\n"; } else { print $handle "(checking 1st threshold):\n"; } foreach $time (@{$ref->{"Times"}}) { print $handle scalar(localtime($time)), "\n"; } print $handle "\n"; } } # Parameters: - # Action: save some information about the current state of the program # to dump file. sub dump_data { my($i, $line, $key, $ref, $file, $event); my($time, $user, $system, $cuser, $csystem); my($name, %reported_names); # verify that dumpfile does not exist and open it if (-e $dumpfile) { log_msg(LOG_ERR, "Can't write to dumpfile: $dumpfile exists"); return; } if (!open(DUMPFILE, ">$dumpfile")) { log_msg(LOG_ERR, "Can't open dumpfile $dumpfile ($!)"); return; } $time = time(); # print program info print DUMPFILE "Program information:\n"; print DUMPFILE '=' x 60, "\n"; print DUMPFILE "Program version: ", $SEC_VERSION, "\n"; print DUMPFILE "Time of the start: ", scalar(localtime($startuptime)), "\n"; print DUMPFILE "Time of the last configuration load: ", scalar(localtime($lastconfigload)), "\n"; print DUMPFILE "Time of the dump: ", scalar(localtime($time)), "\n"; print DUMPFILE "Program resource file: ", $rcfile_status, "\n"; print DUMPFILE "Program options: ", $sec_options, "\n"; print DUMPFILE "\n"; # print environment info print DUMPFILE "Environment:\n"; print DUMPFILE '=' x 60, "\n"; foreach $key (sort(keys %ENV)) { print DUMPFILE "$key=", $ENV{$key}, "\n"; } print DUMPFILE "\n"; # print performance statistics print DUMPFILE "Performance statistics:\n"; print DUMPFILE '=' x 60, "\n"; ($user, $system, $cuser, $csystem) = times(); print DUMPFILE "Run time: ", $time - $startuptime, " seconds\n"; print DUMPFILE "User time: $user seconds\n"; print DUMPFILE "System time: $system seconds\n"; print DUMPFILE "Child user time: $cuser seconds\n"; print DUMPFILE "Child system time: $csystem seconds\n"; print DUMPFILE "Processed input lines: $processedlines\n"; print DUMPFILE "\n"; # print rule usage statistics print DUMPFILE "Rule usage statistics:\n"; print DUMPFILE '=' x 60, "\n"; foreach $file (@conffiles) { $i = 1; print DUMPFILE "\nStatistics for the rules from $file\n"; print DUMPFILE "(loaded at ", scalar(localtime($config_ltimes{$file})), ")\n"; print DUMPFILE '-' x 60, "\n"; foreach $ref (@{$configuration{$file}}) { print DUMPFILE "Rule $i at line ", $ref->{"LineNo"}, " (", $ref->{"Desc"}, ") has matched ", $ref->{"MatchCount"}, " events\n"; ++$i; } } print DUMPFILE "\n"; # print input sources print DUMPFILE "Input sources:\n"; print DUMPFILE '=' x 60, "\n"; foreach $file (@inputfiles) { print DUMPFILE $file, " "; if ($inputsrc{$file}->{"open"}) { print DUMPFILE "(status: Open, "; } else { print DUMPFILE "(status: Closed, "; } print DUMPFILE "received data: ", $inputsrc{$file}->{"lines"}, " lines, "; if ($intcontexts) { print DUMPFILE "context: ", $inputsrc{$file}->{"context"}; } else { print DUMPFILE "no context set"; } print DUMPFILE ")\n"; } print DUMPFILE "\n"; # print content of input buffer print DUMPFILE "Content of input buffer (last $bufsize input lines):\n"; print DUMPFILE '-' x 60, "\n"; for ($i = $bufpos - $bufsize + 1; $i <= $bufpos; ++$i) { print DUMPFILE $input_buffer[$i], "\n"; } print DUMPFILE '-' x 60, "\n"; print DUMPFILE "\n"; # print last $bufsize input sources print DUMPFILE "Last $bufsize input sources:\n"; print DUMPFILE '-' x 60, "\n"; for ($i = $bufpos - $bufsize + 1; $i <= $bufpos; ++$i) { if (defined($input_sources[$i])) { print DUMPFILE $input_sources[$i], "\n"; } else { print DUMPFILE "SEC 'event' action\n"; } } print DUMPFILE '-' x 60, "\n"; print DUMPFILE "\n"; # print content of pending event buffer $i = 0; print DUMPFILE "Pending events:\n"; print DUMPFILE '=' x 60, "\n"; foreach $ref (@pending_events) { print DUMPFILE "Event: ", $ref->[1], "\n"; print DUMPFILE "Will be created at: ", scalar(localtime($ref->[0])), "\n"; print DUMPFILE "\n"; ++$i; } print DUMPFILE "Total: $i elements\n\n"; # print the list of active event correlation operations $i = 0; print DUMPFILE "List of event correlation operations:\n"; print DUMPFILE '=' x 60, "\n"; while (($key, $ref) = each(%corr_list)) { print_element(*DUMPFILE, $key, $ref); print DUMPFILE '-' x 60, "\n"; ++$i; } print DUMPFILE "Total: $i elements\n\n"; # print the list of active contexts $i = 0; %reported_names = (); print DUMPFILE "List of contexts:\n"; print DUMPFILE '=' x 60, "\n"; while (($key, $ref) = each(%context_list)) { if (exists($reported_names{$key})) { next; } foreach $name (@{$ref->{"Aliases"}}) { print DUMPFILE "Context Name: ", $name, "\n"; $reported_names{$name} = 1; } print DUMPFILE "Creation Time: ", scalar(localtime($ref->{"Time"})), "\n"; if ($ref->{"Window"}) { print DUMPFILE "Lifetime: ", $ref->{"Window"}, " seconds\n"; } else { print DUMPFILE "Lifetime: infinite\n"; } if (scalar(@{$ref->{"Action"}})) { print DUMPFILE "Action on delete: ", actionlist2str($ref->{"Action"}); print DUMPFILE " (%s = ", $ref->{"Desc"}, ")\n"; } if (scalar(@{$ref->{"Buffer"}})) { print DUMPFILE scalar(@{$ref->{"Buffer"}}), " events associated with context:\n"; foreach $event (@{$ref->{"Buffer"}}) { print DUMPFILE $event, "\n"; } } print DUMPFILE '-' x 60, "\n"; ++$i; } print DUMPFILE "Total: $i elements\n\n"; # print the list of running children $i = 0; print DUMPFILE "Child processes:\n"; print DUMPFILE '=' x 60, "\n"; while (($key, $ref) = each(%children)) { print DUMPFILE "Child PID: ", $key, "\n"; print DUMPFILE "Commandline started by child: ", $ref->{"cmd"}, "\n"; print DUMPFILE '-' x 60, "\n"; ++$i; } print DUMPFILE "Total: $i elements\n\n"; # print the values of user-defined variables $i = 0; print DUMPFILE "User-defined variables:\n"; print DUMPFILE '=' x 60, "\n"; foreach $key (sort(keys %variables)) { if (defined($variables{$key})) { print DUMPFILE "%$key = '", $variables{$key}, "'\n"; } else { print DUMPFILE "%$key = undef\n"; } ++$i; } print DUMPFILE "Total: $i elements\n\n"; close(DUMPFILE); } ################################################################# # Functions related to input handling and input buffer management ################################################################# # Parameters: - # Action: if the current size of the input buffer is different from # $bufsize, change the size of the input buffer to $bufsize # and set the global variable $bufpos accordingly sub resize_input_buffer { my($cursize) = scalar(@input_buffer); my(@buf, $i, $diff); if ($cursize > $bufsize) { @input_buffer = @input_buffer[$bufpos - $bufsize + 1 .. $bufpos]; @input_sources = @input_sources[$bufpos - $bufsize + 1 .. $bufpos]; $bufpos = $bufsize - 1; } elsif ($cursize < $bufsize) { $diff = $bufsize - $cursize; for ($i = 0; $i < $diff; ++$i) { $buf[$i] = ""; } @input_buffer = (@buf, @input_buffer[$bufpos - $cursize + 1 .. $bufpos]); @input_sources = (@buf, @input_sources[$bufpos - $cursize + 1 .. $bufpos]); $bufpos = $bufsize - 1; } } # Parameters: par1 - text of the SEC internal event # Action: insert the SEC internal event par1 into the event buffer # and match it against the rulebase. sub internal_event { my($text) = $_[0]; my($context, $conffile); $context = "SEC_INTERNAL_EVENT"; log_msg(LOG_INFO, "Creating SEC internal context '$context'"); $context_list{$context} = { "Time" => time(), "Window" => 0, "Buffer" => [], "Action" => [], "Desc" => "SEC internal", "Aliases" => [ $context ] }; log_msg(LOG_INFO, "Creating SEC internal event '$text'"); $bufpos = ($bufpos + 1) % $bufsize; $input_buffer[$bufpos] = $text; $input_sources[$bufpos] = undef; foreach $conffile (@conffiles) { process_rules($conffile); } ++$processedlines; log_msg(LOG_INFO, "Deleting SEC internal context '$context'"); delete $context_list{$context}; } # Parameters: par1 - process ID # Action: read available data from process par1 and create events. sub consume_pipe { my($pid) = $_[0]; my($rin, $ret, $pos, $nbytes, $event); for (;;) { # poll the pipe with select() $rin = ''; vec($rin, fileno($children{$pid}->{"fh"}), 1) = 1; $ret = select($rin, undef, undef, 0); # if select() failed because of the caught signal, try again, # otherwise close the pipe and quit the read-loop; # if select() returned 0, no data is available, so quit the read-loop if (!defined($ret) || $ret < 0) { if ($! == EINTR) { next; } log_msg(LOG_ERR, "Process $pid pipe select error ($!), closing the pipe"); close($children{$pid}->{"fh"}); $children{$pid}->{"open"} = 0; last; } elsif ($ret == 0) { last; } # try to read from the pipe $nbytes = sysread($children{$pid}->{"fh"}, $children{$pid}->{"buffer"}, $blocksize, length($children{$pid}->{"buffer"})); # if sysread() failed and the reason was other than a caught signal, # close the pipe and quit the read-loop; # if sysread() failed because of a caught signal, continue (posix # allows read(2) to be interrupted by a signal and return -1, with # some bytes already been read into read buffer); # if sysread() returned 0, the other end has closed the pipe, so close # our end of the pipe and quit the read-loop if (!defined($nbytes)) { if ($! != EINTR) { log_msg(LOG_ERR, "Process $pid pipe IO error ($!), closing the pipe"); close($children{$pid}->{"fh"}); $children{$pid}->{"open"} = 0; last; } } elsif ($nbytes == 0) { close($children{$pid}->{"fh"}); $children{$pid}->{"open"} = 0; last; } # create all lines of pipe buffer as events, except the last one # which could be a partial line with its 2nd part still not written for (;;) { $pos = index($children{$pid}->{"buffer"}, "\n"); if ($pos == -1) { last; } $event = substr($children{$pid}->{"buffer"}, 0, $pos); substr($children{$pid}->{"buffer"}, 0, $pos + 1) = ""; log_msg(LOG_DEBUG, "Creating event '$event' (received from child $pid)"); push @events, $event; } } # if the child pipe has been closed but the pipe buffer still contains # data (bytes with no terminating newline), create an event from this data if (!$children{$pid}->{"open"} && length($children{$pid}->{"buffer"})) { $event = $children{$pid}->{"buffer"}; log_msg(LOG_DEBUG, "Creating event '$event' (received from child $pid)"); push @events, $event; } } # Parameters: - # Action: check the status of SEC child processes and process their output sub check_children { my($pid, $exitcode); # if the child was started by 'spawn' action, gather the child # standard output and create events (if child has more than PIPE_BUF # bytes to write, we must start reading from pipe before child # termination, otherwise child would block) while ($pid = each(%children)) { if ($children{$pid}->{"open"}) { consume_pipe($pid); } } # get the exit status of every terminated child process. for (;;) { # get the exit status of next terminated child process and # quit the loop if there are no more deceased children # waitpid will return -1 if there are no deceased children (or no # children at all) at the moment; on some platforms, 0 means that # there are children, but none of them is deceased at the moment. # Process ID can be a positive (UNIX) or negative (windows) integer. $pid = waitpid(-1, &WNOHANG); if ($pid == -1 || $pid == 0) { last; } # check if the child process has really exited (and not just stopped). # This check will be skipped on Windows which does not have a valid # implementation of WIFEXITED macro. if ($WIN32 || WIFEXITED($?) || WIFSIGNALED($?)) { # find the child exit code $exitcode = $? >> 8; # if the terminated child was started as a part of 'spawn' # action and its pipe has not been emptied yet, do it now if ($children{$pid}->{"open"}) { consume_pipe($pid); } # if the child exit code is zero and the child was started as # a part of SINGLE_W_SCRIPT rule, execute action list 'Action' if (!$exitcode && defined($children{$pid}->{"Desc"})) { log_msg(LOG_DEBUG, "Child $pid terminated with exitcode 0"); execute_actionlist($children{$pid}->{"Action"}, $children{$pid}->{"Desc"}); # if the child exit code is non-zero and the child was started as # a part of SINGLE_W_SCRIPT rule, execute action list 'Action2' } elsif ($exitcode && defined($children{$pid}->{"Desc"})) { log_msg(LOG_DEBUG, "Child $pid terminated with non-zero exitcode $exitcode"); execute_actionlist($children{$pid}->{"Action2"}, $children{$pid}->{"Desc"}); # if the child exit code is non-zero, log a message } elsif ($exitcode) { log_msg(LOG_WARN, "Child $pid terminated with non-zero exitcode $exitcode (", $children{$pid}->{"cmd"}, ")"); } delete $children{$pid}; } } } # Parameters: par1 - name of the input file # par2 - file position # Action: Input file will be opened and file position will be moved to # position par2 (-1 means "seek EOF" and 0 means "don't seek at all"). # Return the filehandle of the input file, or 'undef' if open failed. sub open_input_file { my($file) = $_[0]; my($fpos) = $_[1]; my($flags); local *INPUT; # we need to use 'local *', since each time we enter # this procedure a new filehandle must be created, that # will be returned from this procedure for external use # if input is stdin, duplicate it if ($file eq "-") { if ($WIN32) { log_msg(LOG_ERR, "Stdin is not supported as input on Win32"); return undef; } while (!open(INPUT, "<&STDIN")) { if ($! == EINTR) { next; } log_msg(LOG_ERR, "Can't dup stdin ($!)"); return undef; } } # if input file is a regular file, open it for reading elsif (-f $file) { while (!sysopen(INPUT, $file, O_RDONLY)) { if ($! == EINTR) { next; } log_msg(LOG_ERR, "Can't open input file $file ($!)"); return undef; } } # if input file is a named pipe, open it both for reading and writing # (the open would block if there are no writers at the moment, so the # process pretends to be a writer) elsif (-p $file) { if ($WIN32) { log_msg(LOG_ERR, "Named pipe is not supported as input on Win32"); return undef; } while (!sysopen(INPUT, $file, O_RDWR)) { if ($! == EINTR) { next; } log_msg(LOG_ERR, "Can't open input file $file ($!)"); return undef; } } # if input file does not exist, log a debug message if -reopen_timeout # option was given, otherwise log an error message elsif (! -e $file) { if ($reopen_timeout) { log_msg(LOG_DEBUG, "Input file $file has not been created yet"); } else { log_msg(LOG_ERR, "Input file $file does not exist!"); } return undef; } # input file is of unsupported type else { log_msg(LOG_ERR, "Input file $file is of unsupported type!"); return undef; } # if INPUT filehandle is connected to a regular file # and $fpos == -1 or $fpos > 0, seek the given position in the file if (-f INPUT) { if ($fpos == -1) { while (!sysseek(INPUT, 0, SEEK_END)) { if ($! == EINTR) { next; } log_msg(LOG_ERR, "Can't seek EOF in input file $file ($!)"); close(INPUT); return undef; } } elsif ($fpos > 0) { while (!sysseek(INPUT, $fpos, SEEK_SET)) { if ($! == EINTR) { next; } log_msg(LOG_ERR, "Can't seek position $fpos in input file $file ($!)"); close(INPUT); return undef; } } } return *INPUT; } # Parameters: par1 - file position # Action: evaluate the inputfile patterns given in commandline, form the # list of inputfiles and save it to global array @inputfiles. Each # input file will then be opened and file position will be moved to # position par1 (-1 means "seek EOF" and 0 means "don't seek at all"). # If -intcontexts option is active, also set up internal contexts. sub open_input { my($fpos) = $_[0]; my($filepat, $pattern, $cmdline_context, $context); my($inputfile, @files, $time, $fh); # Initialize (or clean) global arrays %inputsrc and @inputfiles # (the keys for %inputsrc are members of global array @inputfiles) %inputsrc = (); @inputfiles = (); # Initialize (or clean) the read buffer @readbuffer = (); # Form the list of configuration files, save it to global array # @inputfiles, and open the files $time = time(); foreach $filepat (@inputfilepat) { # check if the input file pattern has a context associated with it, # and if it does, force the -intcontexts option if ($filepat =~ /^(.+)=(\S+)$/) { $pattern = $1; $cmdline_context = $2; $intcontexts = 1; } else { $pattern = $filepat; $cmdline_context = undef; } # interpret the pattern, and open the files that correspond to a pattern @files = glob($pattern); foreach $inputfile (@files) { $fh = open_input_file($inputfile, $fpos); if (defined($cmdline_context)) { $context = $cmdline_context; } else { $context = "_FILE_EVENT_$inputfile"; } $inputsrc{$inputfile} = { "fh" => $fh, "open" => defined($fh), "buffer" => "", "scriptexec" => 0, "checktime" => 0, "lastopen" => $time, "lastread" => $time, "lines" => 0, "context" => $context }; if (!defined($fh) && $inputfile ne "-" && ! -e $inputfile) { $inputsrc{$inputfile}->{"read_from_start"} = 1; } } push @inputfiles, @files; } # if -intcontexts option is active, set up internal contexts if ($intcontexts) { %int_contexts = (); foreach $inputfile (@inputfiles) { $context = $inputsrc{$inputfile}->{"context"}; if (exists($int_contexts{$context})) { next; } $int_contexts{$context} = { "Time" => $time, "Window" => 0, "Buffer" => [], "Action" => [], "Desc" => "SEC internal", "Aliases" => [ $context ] }; } $context = "_INTERNAL_EVENT"; $int_contexts{$context} = { "Time" => $time, "Window" => 0, "Buffer" => [], "Action" => [], "Desc" => "SEC internal", "Aliases" => [ $context ] }; } } # Parameters: par1 - name of the input file # Action: check if input file has been removed, recreated or truncated. # Return 1 if input file has changed and should be reopened; # return 0 if the file has not changed or should not be # reopened right now. If system calls of this procedure # are interrupted by a signal, return 0 also. If system call # on the input file fails, close the file and return undef. sub input_shuffled { my($file) = $_[0]; my(@oldstat, @newstat, $fpos); # standard input is always intact (it can't be recreated or truncated) if ($file eq "-") { return 0; } # stat the input filehandle and exit if stat fails @oldstat = stat($inputsrc{$file}->{"fh"}); if (!scalar(@oldstat)) { if ($! == EINTR) { return 0; } log_msg(LOG_ERR, "Can't stat filehandle of input file $file ($!), closing the file"); close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"open"} = 0; return undef; } # stat the input file and return 0 if stat fails (e.g., input file has # been removed and not recreated yet, so we can't reopen it now) @newstat = stat($file); if (!scalar(@newstat)) { return 0; } # check if i-node numbers of filehandle and input file are different # (this check will be skipped on Windows). if (!$WIN32 && ($oldstat[0] != $newstat[0] || $oldstat[1] != $newstat[1])) { log_msg(LOG_NOTICE, "Input file $file has been recreated"); return 1; } # Check if file size has decreased if (-f $inputsrc{$file}->{"fh"}) { $fpos = sysseek($inputsrc{$file}->{"fh"}, 0, SEEK_CUR); if (!defined($fpos)) { if ($! == EINTR) { return 0; } log_msg(LOG_ERR, "Can't seek filehandle of input file $file ($!), closing the file"); close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"open"} = 0; return undef; } if ($fpos > $newstat[7]) { log_msg(LOG_NOTICE, "Input file $file has been truncated"); return 1; } } return 0; } # Parameters: par1 - name of the input file # Action: read next line from the input file and return it (without '\n' at # the end of the line). If the file has no complete line available, # undef is returned. If read system call fails, or returns EOF and # -notail mode is active, the file is closed and undef is returned. sub read_line_from_file { my($file) = $_[0]; my($pos, $line, $rin, $ret, $nbytes); # if there is a complete line in the read buffer of the file (i.e., the # read buffer contains at least one newline symbol), read line from there $pos = index($inputsrc{$file}->{"buffer"}, "\n"); if ($pos != -1) { $line = substr($inputsrc{$file}->{"buffer"}, 0, $pos); substr($inputsrc{$file}->{"buffer"}, 0, $pos + 1) = ""; return $line; } if (-f $inputsrc{$file}->{"fh"}) { # try to read data from a regular file $nbytes = sysread($inputsrc{$file}->{"fh"}, $inputsrc{$file}->{"buffer"}, $blocksize, length($inputsrc{$file}->{"buffer"})); # check the exit value from sysread() that was saved to $nbytes: # if $nbytes == undef, sysread() failed; # if $nbytes == 0, we have reached EOF (no more data available); # otherwise ($nbytes > 0) sysread() succeeded if (!defined($nbytes)) { # check if sysread() failed because of the caught signal (posix # allows read(2) to be interrupted by a signal and return -1, with # some bytes already been read into read buffer); if sysread() failed # because of some other reason, close the file and return undef if ($! != EINTR) { log_msg(LOG_ERR, "Input file $file IO error ($!), closing the file"); close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"open"} = 0; return undef; } } elsif ($nbytes == 0) { # if we have reached EOF and -tail mode is set, return undef; if # -notail mode is active, close the file, and if the file buffer is not # empty, return its content (bytes between the last newline in the file # and EOF), otherwise return undef if ($tail) { return undef; } close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"open"} = 0; $line = $inputsrc{$file}->{"buffer"}; $inputsrc{$file}->{"buffer"} = ""; if (length($line)) { return $line; } else { return undef; } } } else { # poll the input pipe for new data with select() $rin = ''; vec($rin, fileno($inputsrc{$file}->{"fh"}), 1) = 1; $ret = select($rin, undef, undef, 0); if (!defined($ret) || $ret < 0) { # if select() failed because of the caught signal, return undef, # otherwise close the file and return undef if ($! == EINTR) { return undef; } log_msg(LOG_ERR, "Input file $file select error ($!), closing the file"); close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"open"} = 0; return undef; } elsif ($ret == 0) { # if we have reached EOF and -tail mode is set, return undef; if # -notail mode is active, close the file, and if the file buffer is not # empty, return its content (bytes between the last newline in the file # and EOF), otherwise return undef if ($tail) { return undef; } close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"open"} = 0; $line = $inputsrc{$file}->{"buffer"}; $inputsrc{$file}->{"buffer"} = ""; if (length($line)) { return $line; } else { return undef; } } # try to read from the pipe $nbytes = sysread($inputsrc{$file}->{"fh"}, $inputsrc{$file}->{"buffer"}, $blocksize, length($inputsrc{$file}->{"buffer"})); # check the exit value from sysread() that was saved to $nbytes: # if $nbytes == undef, sysread() failed; # if $nbytes == 0, we have reached EOF (no more data available); # otherwise ($nbytes > 0) sysread() succeeded if (!defined($nbytes)) { # check if sysread() failed because of the caught signal (posix # allows read(2) to be interrupted by a signal and return -1, with # some bytes already been read into read buffer); if sysread() failed # because of some other reason, log an error message and return undef if ($! != EINTR) { log_msg(LOG_ERR, "Input file $file IO error ($!), closing the file"); close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"open"} = 0; return undef; } } elsif ($nbytes == 0) { # if sysread() returns 0, that signals that there are no writers # on the pipe anymore, and from now on select() always claims that # there is some data (EOF) to be read (with named pipe we should # never reach that condition, since we have opened it in RW-mode) log_msg(LOG_ERR, "Input file $file IO error (unknown pipe error), closing the file"); close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"open"} = 0; return undef; } } # if the read buffer contains a newline, cut the first line from the # read buffer and return it, otherwise return undef (even if there are # some bytes in the buffer) $pos = index($inputsrc{$file}->{"buffer"}, "\n"); if ($pos != -1) { $line = substr($inputsrc{$file}->{"buffer"}, 0, $pos); substr($inputsrc{$file}->{"buffer"}, 0, $pos + 1) = ""; return $line; } return undef; } # Parameters: par1 - variable where the input line is saved # par2 - variable where the input file name is saved # Action: attempt to read next line from each input file, and store the # received lines with corresponding input file names to the read # buffer. Return the first line from the read buffer, with par1 set # to line and par2 set to file name. If there were no new lines in # input files, par1 is set to undef but par2 reflects the status of # input files: value 1 means that at least one of the input files has # new data available (although no complete line), value 0 means that # no data were added to any of the input files since the last poll. sub read_line { my($line, $file); my($time, $len, $newdata); # check all input files and store new data to the read buffer $newdata = 0; $time = time(); foreach $file (@inputfiles) { # if the check timer for the file has not expired yet, skip the file if ($check_timeout && $time < $inputsrc{$file}->{"checktime"}) { next; } # before reading, memorize the number of bytes in the read cache $len = length($inputsrc{$file}->{"buffer"}); # if the input file is open, read a line from it; if the input file # is closed, treat it as an open file with no new data available if ($inputsrc{$file}->{"open"}) { $line = read_line_from_file($file); } else { $line = undef; } if (defined($line)) { # if we received a new line, write the line to the read buffer; also # update time-related variables and call external script, if necessary push @readbuffer, $line; push @readbuffer, $file; if ($input_timeout) { $inputsrc{$file}->{"lastread"} = $time; } if ($inputsrc{$file}->{"scriptexec"}) { log_msg(LOG_INFO, "Input received, executing script $timeout_script 0 $file"); shell_cmd("$timeout_script 0 $file"); $inputsrc{$file}->{"scriptexec"} = 0; } } else { # if we were unable to obtain a complete line from the file but # new bytes were stored to the read cache, don't set the check # timer and skip shuffle and timeout checks if ($len < length($inputsrc{$file}->{"buffer"})) { $newdata = 1; next; } # if there were no new bytes in the file and -notail mode is active, # don't set the check timer and skip shuffle and timeout checks (i.e., # -input_timeout, -timeout_script, -reopen_timeout, and -check_timeout # options are ignored when -notail is set) if (!$tail) { next; } # if -check_timeout is set, poll the file after $check_timeout seconds if ($check_timeout) { $inputsrc{$file}->{"checktime"} = $time + $check_timeout; } # if there were no new bytes in the file and it has been shuffled, # reopen the file and start to process it from the beginning if ($inputsrc{$file}->{"open"} && input_shuffled($file)) { log_msg(LOG_NOTICE, "Shuffled $file, reopening and processing from the start"); close($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"fh"} = open_input_file($file, 0); $inputsrc{$file}->{"open"} = defined($inputsrc{$file}->{"fh"}); if ($reopen_timeout) { $inputsrc{$file}->{"lastopen"} = $time; } } # if we have waited for new bytes for more than $input_timeout # seconds, execute external script $timeout_script with commandline # parameters "1 " if ($input_timeout && !$inputsrc{$file}->{"scriptexec"} && $time - $inputsrc{$file}->{"lastread"} >= $input_timeout) { log_msg(LOG_INFO, "No input, executing script $timeout_script 1 $file"); shell_cmd("$timeout_script 1 $file"); $inputsrc{$file}->{"scriptexec"} = 1; } # if we have waited for new bytes for more than $reopen_timeout # seconds, reopen the input file if ($reopen_timeout && !$inputsrc{$file}->{"open"} && $time - $inputsrc{$file}->{"lastopen"} >= $reopen_timeout) { log_msg(LOG_DEBUG, "Attempting to (re)open $file"); if (exists($inputsrc{$file}->{"read_from_start"})) { $inputsrc{$file}->{"fh"} = open_input_file($file, 0); if (defined($inputsrc{$file}->{"fh"})) { delete $inputsrc{$file}->{"read_from_start"}; } } else { $inputsrc{$file}->{"fh"} = open_input_file($file, -1); } $inputsrc{$file}->{"open"} = defined($inputsrc{$file}->{"fh"}); $inputsrc{$file}->{"lastopen"} = $time; } } } # if we succeeded to read new data and write it to the read buffer, # return the first line from the buffer; otherwise return undef if (scalar(@readbuffer)) { $_[0] = shift @readbuffer; $_[1] = shift @readbuffer; } else { $_[0] = undef; $_[1] = $newdata; } } ################################################### # Functions related to signal reception and sending ################################################### # Parameters: - # Action: check whether signals have arrived and process them sub check_signals { my($file, @file_list); my(@allkeys, @keys); # if SIGHUP has arrived, do a full restart of SEC if ($refresh) { log_msg(LOG_NOTICE, "SIGHUP received: full restart of SEC"); # terminate child processes child_cleanup(); # clear correlation operations, contexts and user-defined variables %corr_list = (); %context_list = (); %variables = (); # clear pending events @pending_events = (); # close input sources foreach $file (@inputfiles) { if ($inputsrc{$file}->{"open"}) { close($inputsrc{$file}->{"fh"}); } } # close the logfile and connection to the system logger if ($logfile) { close(LOGFILE); } if ($syslogf) { eval { Sys::Syslog::closelog() }; } # now the SEC internal state has been cleared, input sources and log # handles closed - re-read SEC command line and resource file options read_options(); # open the logfile and connection to the system logger if ($logfile) { open_logfile($logfile); } if ($syslogf) { open_syslog($syslogf); } # read configuration from SEC rule files read_config(); # open input sources and resize the input buffer open_input(-1); resize_input_buffer(); # if -intevents flag was specified, generate the SEC_RESTART event if ($intevents) { internal_event("SEC_RESTART"); } # set the signal flag back to zero $refresh = 0; } # if SIGABRT has arrived, do a soft restart of SEC if ($softrefresh) { log_msg(LOG_NOTICE, "SIGABRT received: soft restart of SEC"); # close input sources foreach $file (@inputfiles) { if ($inputsrc{$file}->{"open"}) { close($inputsrc{$file}->{"fh"}); } } # close the logfile and connection to the system logger if ($logfile) { close(LOGFILE); } if ($syslogf) { eval { Sys::Syslog::closelog() }; } # now input sources and log handles have been closed - # re-read SEC command line and resource file options read_options(); # open the logfile and connection to the system logger if ($logfile) { open_logfile($logfile); } if ($syslogf) { open_syslog($syslogf); } # read configuration from SEC rule files that are either new or # have been modified, and store to the array @file_list the names # of files that have been modified or removed soft_read_config(\@file_list); # clear event correlation operations related to the modified and # removed configuration files @allkeys = keys %corr_list; foreach $file (@file_list) { @keys = grep($corr_list{$_}->{"File"} eq $file, @allkeys); delete @corr_list{@keys}; } # open input sources and resize the input buffer open_input(-1); resize_input_buffer(); # if -intevents flag was specified, generate the SEC_SOFTRESTART event if ($intevents) { internal_event("SEC_SOFTRESTART"); } # set the signal flag back to zero $softrefresh = 0; } # if SIGUSR1 has arrived, create the dump file if ($dumpdata) { log_msg(LOG_NOTICE, "SIGUSR1 received: dumping data to $dumpfile"); # write info about SEC state to the dump file dump_data(); # set the signal flag back to zero $dumpdata = 0; } # if SIGUSR2 has arrived, restart logging if ($openlog) { log_msg(LOG_NOTICE, "SIGUSR2 received: restarting logging"); # reopen the logfile and connection to the system logger if ($logfile) { close(LOGFILE); open_logfile($logfile); } if ($syslogf) { eval { Sys::Syslog::closelog() }; open_syslog($syslogf); } # set the signal flag back to zero $openlog = 0; } # if SIGTERM has arrived, shutdown SEC if ($terminate) { log_msg(LOG_NOTICE, "SIGTERM received: shutting down SEC"); # If -intevents flag was specified, generate the SEC_SHUTDOWN event. # Note that the $terminate flag will be set back to zero, as if # SEC_SHUTDOWN event was generated before SIGTERM under normal circum- # stances (when $terminate is set, SEC does not fork any new processes). # Note also, that after generating SEC_SHUTDOWN event, SEC will sleep for # TERMTIMEOUT seconds, so that child processes that were triggered by # SEC_SHUTDOWN have time to create a signal handler for SIGTERM if needed. if ($intevents) { $terminate = 0; internal_event("SEC_SHUTDOWN"); sleep(TERMTIMEOUT); } # final shutdown procedures child_cleanup(); exit(0); } } # Parameters: - # Action: terminate child processes sub child_cleanup { my($pid); while($pid = each(%children)) { log_msg(LOG_NOTICE, "Sending SIGTERM to process $pid"); kill('TERM', $pid); } } # Parameters: - # Action: on arrival of SIGHUP set flag $refresh sub hup_handler { $SIG{HUP} = \&hup_handler; $refresh = 1; } # Parameters: - # Action: on arrival of SIGABRT set flag $softrefresh sub abrt_handler { $SIG{ABRT} = \&abrt_handler; $softrefresh = 1; } # Parameters: - # Action: on arrival of SIGUSR1 set flag $dumpdata sub usr1_handler { $SIG{USR1} = \&usr1_handler; $dumpdata = 1; } # Parameters: - # Action: on arrival of SIGUSR2 set flag $openlog sub usr2_handler { $SIG{USR2} = \&usr2_handler; $openlog = 1; } # Parameters: - # Action: on arrival of SIGTERM clean things up and exit sub term_handler { $SIG{TERM} = \&term_handler; $terminate = 1; } ########################################################## # Functions related to daemonization and option processing ########################################################## # Parameters: - # Action: daemonize the process sub daemonize { local $SIG{HUP} = 'IGNORE'; # ignore SIGHUP inside this function my($pid); # -detach is not supported on Windows if ($WIN32) { log_msg(LOG_CRIT, "'-detach' option is not supported on Win32"); exit(1); } # if stdin was specified as input, we can't become a daemon if (grep($_ eq "-", @inputfiles)) { log_msg(LOG_CRIT, "Can't become a daemon (stdin is specified as input), exiting!"); exit(1); } # fork a new copy of the process and exit from the parent $pid = fork(); if (!defined($pid)) { log_msg(LOG_CRIT, "Can't fork a new process for daemonization ($!), exiting!"); exit(1); } if ($pid) { exit(0); } # create a new session and process group if (!POSIX::setsid()) { log_msg(LOG_CRIT, "Can't start a new session ($!), exiting!"); exit(1); } # fork a second copy of the process and exit from the parent - the parent # as a session leader might deliver the SIGHUP signal to child when it # exits, but SIGHUP is ignored inside this function $pid = fork(); if (!defined($pid)) { log_msg(LOG_CRIT, "Can't fork a new process for daemonization ($!), exiting!"); exit(1); } if ($pid) { exit(0); } # connect stdin, stdout, and stderr to /dev/null if (!open(STDIN, '/dev/null')) { log_msg(LOG_CRIT, "Can't connect stdin to /dev/null ($!), exiting!"); exit(1); } if (!open(STDOUT, '>/dev/null')) { log_msg(LOG_CRIT, "Can't connect stdout to /dev/null ($!), exiting!"); exit(1); } if (!open(STDERR, '>&STDOUT')) { log_msg(LOG_CRIT, "Can't connect stderr to stdout with dup ($!), exiting!"); exit(1); } log_msg(LOG_DEBUG, "Daemonization complete"); } # Parameters: - # Action: read and process options from command line and resource file sub read_options { my(@argv_backup, $option); # back up the @ARGV array @argv_backup = @ARGV; # open the file pointed by the SECRC environment variable and # read options from that file; empty lines and lines starting # with the #-symbol are ignored, rest of the lines are treated # as SEC command line options and pushed into @ARGV with # leading and trailing whitespace removed if (exists($ENV{"SECRC"})) { if (open(SECRC, $ENV{"SECRC"})) { while () { if (/^\s*(.*\S)/) { $option = $1; if (index($option, '#') == 0) { next; } push @ARGV, $option; } } close(SECRC); $rcfile_status = $ENV{"SECRC"}; } else { $rcfile_status = $ENV{"SECRC"} . " - open failed ($!)"; } } else { $rcfile_status = "none"; } # set the $sec_options global variable $sec_options = join(" ", @ARGV); # (re)set option variables to default values @conffilepat = (); @inputfilepat = (); $input_timeout = 0; $timeout_script = ""; $reopen_timeout = 0; $check_timeout = 0; $poll_timeout = 0.1; $blocksize = 1024; $bufsize = 10; $evstoresize = 0; $cleantime = 1; $logfile = ""; $syslogf = ""; $debuglevel = 6; $pidfile = ""; $dumpfile = "/tmp/sec.dump"; $quoting = 0; $tail = 1; $fromstart = 0; $detach = 0; $intevents = 0; $intcontexts = 0; $testonly = 0; $help = 0; $version = 0; # parse the options given in command line and in SEC resource file GetOptions( "conf=s" => \@conffilepat, "input=s" => \@inputfilepat, "input_timeout=i" => \$input_timeout, "timeout_script=s" => \$timeout_script, "reopen_timeout=i" => \$reopen_timeout, "check_timeout=i" => \$check_timeout, "poll_timeout=f" => \$poll_timeout, "blocksize=i" => \$blocksize, "bufsize=i" => \$bufsize, "evstoresize=i" => \$evstoresize, "cleantime=i" => \$cleantime, "log=s" => \$logfile, "syslog=s" => \$syslogf, "debug=i", \$debuglevel, "pid=s" => \$pidfile, "dump=s" => \$dumpfile, "quoting!" => \$quoting, "tail!" => \$tail, "fromstart!" => \$fromstart, "detach!" => \$detach, "intevents!" => \$intevents, "intcontexts!" => \$intcontexts, "testonly!" => \$testonly, "help|?" => \$help, "version" => \$version ); # check the values received from command line and resource file # and set option variables back to defaults, if necessary if (!$timeout_script || $input_timeout < 0) { $input_timeout = 0; } if ($reopen_timeout < 0) { $reopen_timeout = 0; } if ($check_timeout < 0) { $check_timeout = 0; } if ($poll_timeout < 0) { $poll_timeout = 0.1; } if ($blocksize <= 0) { $blocksize = 1024; } if ($bufsize <= 0) { $bufsize = 10; } if ($evstoresize < 0) { $evstoresize = 0; } if ($cleantime < 0) { $cleantime = 1; } if ($debuglevel < 1 || $debuglevel > 6) { $debuglevel = 6; } # restore the @ARGV array @ARGV = @argv_backup; } ################################################################## # ------------------------- MAIN PROGRAM ------------------------- ################################################################## ### Read and process SEC options from command line and resource file read_options(); ### If requested, print usage/version info and exit if ($help) { print $SEC_USAGE; exit(0); } if ($version) { print $SEC_VERSION, "\n"; print $SEC_COPYRIGHT, "\n"; print $SEC_LICENSE; exit(0); } ### Open logfile if ($logfile) { open_logfile($logfile); } if ($syslogf) { open_syslog($syslogf); } log_msg(LOG_NOTICE, "$SEC_VERSION"); # If -detach flag was specified, chdir to / for not disturbing future # unmount of current filesystem. Must be done before read_config() to # receive error messages about scripts that would not be found at runtime if ($detach) { log_msg(LOG_NOTICE, "Changing working directory to /"); chdir('/'); } ### Read in configuration my $config_ok = read_config(); if ($testonly) { if ($config_ok) { exit(0); } else { exit(1); } } ### Open input sources if ($fromstart) { open_input(0); } elsif ($tail) { open_input(-1); } else { open_input(0); } ### Daemonize the process, if -detach flag was specified if ($detach) { daemonize(); } ### Create pidfile - must be done after daemonization if ($pidfile) { if (open(PIDFILE, ">$pidfile")) { print PIDFILE "$$\n"; close(PIDFILE); } else { log_msg(LOG_CRIT, "Can't open pidfile $pidfile for writing ($!), exiting!"); exit(1); } } ### Set signal handlers $refresh = 0; $SIG{HUP} = \&hup_handler; $softrefresh = 0; $SIG{ABRT} = \&abrt_handler; $dumpdata = 0; $SIG{USR1} = \&usr1_handler; $openlog = 0; $SIG{USR2} = \&usr2_handler; $terminate = 0; $SIG{TERM} = \&term_handler; ### Set various global variables $lastcleanuptime = $startuptime = time(); $processedlines = 0; ### Initialize input buffer for (my $i = 0; $i < $bufsize; ++$i) { $input_buffer[$i] = ""; $input_sources[$i] = ""; } $bufpos = $bufsize - 1; ### Initialize correlation list, context list, ### buffer list, and child process list %corr_list = (); %context_list = (); %children = (); ### Initialize event buffers @events = (); @pending_events = (); ### If -intevents flag was specified, create generate the SEC_STARTUP event if ($intevents) { internal_event("SEC_STARTUP"); } ### The main loop - read lines from input stream and process them for (;;) { my($line, $file, $ret); my($context, $conffile); # if there are pending events in the event buffer or the read buffer, # read new line from there, otherwise read new line from input stream. if (scalar(@events)) { $line = shift @events; $file = undef; } elsif (scalar(@readbuffer)) { $line = shift @readbuffer; $file = shift @readbuffer; } else { read_line($line, $file); } if (defined($line)) { if ($intcontexts) { if (defined($file)) { $context = $inputsrc{$file}->{"context"}; } else { $context = "_INTERNAL_EVENT"; } $context_list{$context} = $int_contexts{$context}; } # update input buffer (it is implemented as a circular buffer, since # according to benchmarks an array queue using shift and push is slower) $bufpos = ($bufpos + 1) % $bufsize; $input_buffer[$bufpos] = $line; $input_sources[$bufpos] = $file; # process rules from configuration files foreach $conffile (@conffiles) { process_rules($conffile); } if ($intcontexts) { delete $context_list{$context}; } if (defined($file)) { ++$inputsrc{$file}->{"lines"}; } ++$processedlines; } elsif (!$file) { # if we didn't get new data and -tail option was specified, sleep # for $poll_timeout seconds; if -notail option is active and all # input files have been closed, exit if ($tail) { # sleep with select() $ret = select(undef, undef, undef, $poll_timeout); if ((!defined($ret) || $ret < 0) && $! != EINTR) { log_msg(LOG_CRIT, "Select error ($!), exiting!"); child_cleanup(); exit(1); } } elsif (!grep($inputsrc{$_}->{"open"}, @inputfiles)) { # after generating SEC_SHUTDOWN event, SEC will sleep for TERMTIMEOUT # seconds, so that child processes that were triggered by SEC_SHUTDOWN # have time to create a signal handler for SIGTERM if they wish if ($intevents) { internal_event("SEC_SHUTDOWN"); sleep(TERMTIMEOUT); } child_cleanup(); exit(0); } } # search all lists, performing timed tasks associated with elements # and removing obsolete elements if (time() - $lastcleanuptime >= $cleantime) { process_lists(); $lastcleanuptime = time(); } # manage child processes if (scalar(%children)) { check_children(); } # check signal flags check_signals(); }