#!/usr/bin/perl -w # # Tool to scan log file for anomolies. # This script will scan the directory where your logfiles reside and report # everything that it finds with the exception of the those expressions found # in the ignore file, scanlog.ignore. # This script has been produced because I wanted to merge the stuff from # psionic.com (logcheck-1.1.1) and Nate Campi's newlogcheck.sh (www.campin.net) # use warnings; use strict; #use diagnostics; use Sys::Hostname; use POSIX qw(strftime); use File::Find; use Getopt::Std; use vars qw( %objects %report @history $opt_o $opt_m $opt_O $opt_M $opt_c $opt_C $opt_d $opt_s $opt_v $opt_r $summary $opt_h $log_base $domain $time_fmt $wanted_pattern $default_patterns %code); sub IGNORE {0} sub PROCESS {1} sub COUNT {2}; sub ALERT {3}; sub WARN {4}; sub count {5}; sub ignore {6}; sub UNUSUAL {7}; my @labels = ( 'Ignore', 'Process', 'Count', 'Alerts', 'Warnings', 'count', '', 'Unusual logs' ); #Process command line opts -- these override the defaults and those # specified in the -c . getopts("o:m:OMC:c:d:s:vrh:") or die "Invalid options"; # -o -- append to offset filename # -O (oh) -- do not update offset files # -m -- send all mail here over riding config file # -M -- do not send mail at all - print to stdout. # -c -- override default values of configuration vars # -C {simple|alt} # -d ago # -s -- send summary to (default->email) # -v -- verbose # -r -- print rules # -h -- just run for this host $opt_o = '' unless defined $opt_o; $opt_d = 0 unless defined $opt_d; $opt_C = 'simple' unless defined $opt_C; $ENV{'PATH'} = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/ucb:/usr/local/bin'; my @time = localtime(time - $opt_d*24*3600); my $year = strftime("%Y", @time); my $month = strftime("%m", @time); my $day = strftime("%d", @time); ################################################################## # Configuration section -- all these vars can be overridden # in the -c # # email addresses are relative to this domain $domain = 'ip-solutions.net'; # Times printed in reports will use this strftime format $time_fmt = "%T on %d/%m"; # select files to process in the logging heirarchy (see wanted function) $wanted_pattern = '^/srv/log/HOSTS/([^/]+)/([^/]+)/$year/$month/$day'; $default_patterns = "/usr/local/etc/scanlogs/config"; $log_base = '/srv/log/HOSTS'; # in addition you can specify your own 'wanted' function for a the # default one see defintion of wanted_default() below. # #################################################################### # my $display_time = strftime($time_fmt, @time); my $sysname = hostname(); my $write_src = defined $opt_C ? "write_src_$opt_C": "write_src_simple"; # read configuration file require $opt_c if defined $opt_c; $wanted_pattern =~ s/(\$\w+)/$1/eeg; # perform var substitution # # default wanted function gets called by find, # calls process_log_file() if file is to be processed # # this can be replaced by defining a new sub wanted{} in the # config file # sub wanted_default{ my $want = 0; my $base = "$File::Find::dir/$_"; # return if $File::Find::dir eq "$log_base"; return if /^\./; #print "*****$base\n"; if( $base =~ / /) { # spaces in file name $File::Find::prune = 1; return; } my ($mach) = $base =~ m'^/srv/log/HOSTS/([^/]+)'o; if( ! defined $mach or (defined $opt_h and $mach ne $opt_h ) ) { $File::Find::prune =1; return; } my ($log, $rest) = $base =~ m'^/srv/log/HOSTS/[^/]+/([^/]+)/?(.*)'; return unless defined $log and defined $rest and $rest; #print "^^^^^\n"; if( $rest =~ m'^(\d{4})$' ) { $File::Find::prune = $1 ne $year; #print "prune year $1 $File::Find::prune\n"; return; } elsif ( $rest =~ m'^\d{4}/(\d\d)$' ){ $File::Find::prune = $1 ne $month; #print "prune month $1 $File::Find::prune\n"; return; } elsif ( $rest =~ m'^\d{4}/\d\d/(\d\d)$') { $File::Find::prune = $1 ne $day; #print "prune day $1 $File::Find::prune\n"; return; } if( /\d+$/ ) { # ignore offset files process_log_file($mach, $log, "$File::Find::dir/$_"); } } # which wanted function do we use? my $wanted = defined &wanted ? \&wanted : \&wanted_default; print "date:time $display_time\n" if $opt_v; # push default pattern file into @ARGV if none specified push @ARGV, $default_patterns unless scalar @ARGV; foreach my $patterns ( @ARGV ) { read_patterns( $patterns ); } my $sum_email = defined $opt_s? $opt_s : $objects{'default'}->{'email'}; $summary = ''; if ( ! -d $log_base ) { die "'$log_base' is not a directory"; } # walk the log file heirarchy find($wanted, $log_base); # scan the log files foreach my $m (sort keys %objects ) { # produce the reports if(defined $objects{$m}->{'proccessed'} ) { do_report( $m, $objects{$m} ) ; } else { delete $objects{$m}; } } # send out the reports foreach my $email ( sort keys %report ) { # email them print STDERR "email to $email\n"; sendmail ( "$email\@$domain", undef, undef, "Logs from logchecker at $display_time", $report{$email} ) } # send the summary sendmail ( "$sum_email\@$domain", undef, undef, "Summary from logchecker at $display_time", $summary ); exit; #################################################################### # # process_log_file gets called by find to process selected file # performs the actual processing of the log file. sub process_log_file { my ($mach, $type, $file) = @_; # my (@log, @hack, @warn); my ($proc, $class, $inode, $offset); my $machine; my $ignored = 0; if( $objects{$mach} ) { # have a entry for this one $machine = $objects{$mach}; warn "No rules set for $mach, using default!" unless defined $machine->{'proc'}; } else { # no entry -- use default $machine = $objects{$mach} = {}; $machine->{'type'} = 'host'; $machine->{'email'} = $objects{'default'}->{'email'}; $machine->{'alert'} = $objects{'default'}->{'alert'}; $machine->{'proc'} = $objects{'default'}->{'proc'}; } $proc = $machine->{'proc'} ; $machine->{'proccessed'} = 1; if( !ref $proc ) { print STDERR "$mach -- no proc\n"; } # Get starting position from offset file if ( open(OFFSET, "$file-offset$opt_o") ) { ($inode, $offset) = split(/\t/, ); close(OFFSET); } else { $inode = 0; $offset = 0; } if( !open(LOG, $file) ) { warn "Process_log_file can't open '$file':$!"; return; } my @stat = stat LOG; if( $inode and $inode != $stat[1] ) { # Inode has changed! push(@{$machine->{"report-1"}}, "********************** Warning!! ***************************"); push(@{$machine->{"report-1"}}, "Inode for $file has changed\n"); push(@{$machine->{"report-1"}}, "********************** Warning!! ***************************"); } if( $offset and $offset > $stat[7] ) { # file has shrunk! push(@{$machine->{"report-1"}}, "********************** Warning!! ***************************"); push(@{$machine->{"report-1"}}, "$file has shrunk!\n"); push(@{$machine->{"report-1"}}, "********************** Warning!! ***************************"); $offset = 0; # reset and report everthing } $inode = $stat[1]; seek(LOG, $offset, 0); while(){ if( defined $machine->{'sec-proc'} ){ $machine->{'sec-proc'}->(0, $_); } my $ret = $proc->($_); my ( $class, $msg ) = split('-', $ret); if( defined $msg ) { if( $class == PROCESS ) { unless ( defined $machine->{"process"} ) { $machine->{"process"} = {}; } $machine->{"process"}->{$msg}++; } else { unless ( defined $machine->{"count-$msg"} ) { $machine->{"count-$msg"} = 0; } $machine->{"count-$msg"}++; } } if( $class == &IGNORE or $class == &ignore ) { $ignored++; next; } unless ( defined $machine->{"report-$class"} ) { $machine->{"report-$class"} = []; } push(@{$machine->{"report-$class"}}, "$_"); } $offset = tell LOG; close( LOG ); if( defined $machine->{'sec-proc'} ){ $machine->{"report-special"} = $machine->{'sec-proc'}->(1); } $machine->{'ignored'} += $ignored; return if defined $opt_O; # skip writing offset file... # write offset file if ( open(OFFSET, ">$file-offset$opt_o") ) { print OFFSET "$inode\t$offset"; close(OFFSET); } else { print STDERR "Can't write offset file '$file-offsetx'$!\n"; } } sub sendmail{ my($to, $cc, $bcc, $subject, $message) = @_; my $m = ref $message eq 'ARRAY' ? join('', @$message): $message; if( defined $opt_M ) { print "mail to '$to'\n\n"; print $m; return; } open(SENDMAIL, "|/usr/lib/sendmail -t -f root\@ip-solutions.net"); print SENDMAIL "To: $to\n"; print SENDMAIL "From: logcheck\@ip-solutions.net\n"; print SENDMAIL "Subject: $subject\n"; print SENDMAIL "Cc: $cc\n" if defined $cc; print SENDMAIL "BCc: $bcc\n" if defined $bcc; print SENDMAIL "\n"; print SENDMAIL $m; print SENDMAIL "\n\n"; close SENDMAIL; return $? >> 8; } sub check_opts { my $opts = shift; my @valid_opts = qw ( email type alert ); my $o; my $no = scalar @$opts; for( my $i=0; $i<$no; $i+=2 ){ check: { foreach $o (@valid_opts) { last check if $opts->[$i] eq $o; } warn "$_\noption '$opts->[$i]' is not valid"; } } } sub read_patterns { my $config = shift; my $code = ''; my $type; open(CONFIG, $config) or die "Can't open config file '$config'$!"; my $machine = 'default'; # $objects{$machine} = {}; # $objects{$machine}->{'list'} = []; # first pass: read the config file and store configs in %objects while() { next if /^\s*#/; s/(?!\\)#.*$//; chomp; if( /^\s*\[\s*([^\]]+)]/ ) { # its a header line or separator if( defined $type and $type eq 'code' ) { # last object was code print $code if $opt_r; eval "\$code{\$machine} = $code "; if( $@ ) { # something went wrong! print STDERR "Compile errors for $machine: $@\n"; $code{$machine}->{'proc'} = undef; } } my @opts = split(/\s*,\s*|\s*=>\s*|\s*=\s*/, $1); $machine = shift @opts; if( /type\s*=>\s*code/ ) { $code = ''; $type = 'code'; } else { if( $objects{$machine} ) { warn "redefining object '$machine'"; } $objects{$machine} = {}; check_opts(\@opts); %{$objects{$machine}} = @opts; $type =$objects{$machine}->{'type'}; if( defined $opt_m ) { $objects{$machine}->{'email'} = $objects{$machine}->{'alert'} = $opt_m; } $objects{$machine}->{'list'} = []; } } else { if( $type eq 'code' ) { $code .= "$_\n"; } else { push(@{$objects{$machine}->{'list'}}, $_ ); } } } # second pass: process includes and then compile matching routines if( !defined $objects{'default'} ) { die "no default, giving up!"; } foreach my $machine ( keys %objects ) { next if($objects{$machine}->{'type'} eq 'service' ); undef @history; push(@history, $machine); $objects{$machine}->{'email'} = $objects{'default'}->{'email'} unless defined $objects{$machine}->{'email'}; $objects{$machine}->{'alert'} = $objects{'default'}->{'alert'} unless defined $objects{$machine}->{'alert'}; no strict 'refs'; $objects{$machine}->{'src'} = &$write_src( $machine, $objects{$machine}->{'list'} ); use strict 'refs'; print "$machine:\nsub { \$_ = shift; study \$_; " . ", $objects{$machine}->{'src'} return \"". UNUSUAL() ."\$tag\"}" if defined $opt_r; eval "\$objects{\$machine}->{'proc'} = ". "sub { \$_ = shift; study \$_; " . "$objects{$machine}->{'src'} return \"". UNUSUAL ."\$tag\"}"; if( $@ ) { # something went wrong! print STDERR "Compile errors for $machine: $@\n"; $objects{$machine}->{'proc'} = undef; } } } sub add_rules { my ($mach, $rules) = @_; my @code ; foreach my $rule ( @{$rules} ) { if( $rule =~ /^include/ ) { my ($m) = $rule =~ /^include\s+(\S+)/; if( ! defined $m ) { print STDERR "could not find machine name '$rule'\n"; next; } if( ! exists $objects{$m} ) { print STDERR "include '$m' unknown service\n"; next; } if( grep(/^$m$/, @history) ) { print STDERR "recursive include of '$m' in $history[$#history]\n"; next; } push(@history, $m); push(@code, add_rules($m, $objects{$m}->{'list'} )); pop(@history); } else { # its a rule my( $type, $rest ) = split(/\t/, $rule, 2); $type =~ tr/a-z/A-Z/; # check type *********** if( $type !~ /^(A|W|I|I\*|C|CI|P)(-\d+)?/ ) { warn "unrecognized type '$type' in rule '$rule'"; next; } if($type eq 'CI' or $type eq 'C') { my ($pat, $threshold, $message) = split(/\t/, $rest); if(! defined $message ) { warn "Problems with rule '$rule':"; warn " format is C|CI / threshold "; } if( $threshold !~ /^\d+$/ ) { warn "threshold '$threshold' in rule '$rule' contains non digits"; next; } $rest = "$pat\t$message"; if( !defined $objects{$mach}->{'thres'} ) { $objects{$mach}->{'thres'} = {}; } $objects{$mach}->{'thres'}->{$message} = $threshold; } # we sort on $type to get order of application of matches... my ($t, $o) = split('-', $type); $o = '' unless defined $o; $type = IGNORE if $t eq 'I*'; $type = COUNT if $t eq 'CI'; $type = ALERT if $t eq 'A'; $type = WARN if $t eq 'W'; $type = COUNT if $t eq 'C'; $type = PROCESS if $t eq 'P'; $type = ignore if $t eq 'I'; push(@code, "$type\t$o\t$rest"); } } return @code; } sub write_src_simple { my ($m, $list) = @_; my $src = "my \$tag = '';\n"; foreach my $line (sort &add_rules( $m, $list )) { my ($code, $o, $pat, $msg ) = split("\t", $line, 4); if( defined $msg ) { # counts or process if( $code == COUNT ) { #tag but continue matching $src .= "\$tag = \"-$msg\" if $pat"."o;\n"; } elsif( $code == PROCESS ) { $src .= "if( $pat"."o ) { return \"". PROCESS . "-$msg\" if( \$code{$msg}->(\$_) ) }\n"; } else { $src .= "return $code-$msg if $pat"."o;\n" if defined $pat; } } else { $src .= "return \"$code\$tag\" if $pat"."o;\n" if defined $pat; } } return $src; } sub write_src_alt { # do alternation within patterns my $list = shift; my $last_code = 0; my %src; my $src = ''; foreach my $line (sort &add_rules( $list )) { my ($code, $o, $pat) = split("\t", $line); my $mod; ($pat, $mod) = $pat =~ m"^/(.+)/(\w*)"; if( $code ne $last_code ) { foreach my $k (keys %src ){ my ($t, $mod) = split(' ', $k); $src .= defined $mod ? "return $last_code if /$src{$k}/o$mod;\n": "return $last_code if /$src{$k}/o;\n"; } $last_code = $code; undef %src; } $mod = '' unless defined $mod; if( defined $src{"src $mod"} ) { $src{"src $mod"} .= "|(?:$pat)"; } else { $src{"src $mod"} = "(?:$pat)"; } } # handle last batch foreach my $k (keys %src ){ my ($t, $mod) = split(' ', $k); $src .= defined $mod ? "return $last_code if /$src{$k}/0$mod;\n": "return $last_code if /$src{$k}/o;\n"; } return $src; } sub write_src_or { # do ors: return 1 if re || re || re.... my $list = shift; my $last_code = 0; my $s; my $src = ''; foreach my $line (sort &add_rules( $list )) { my ($code, $o, $pat) = split("\t", $line); if( $code ne $last_code ) { $src .= "return $last_code if $s;" if $s; $last_code = $code; undef $s; } if( defined $s ) { $s .= "||$pat"."o"; } else { $s = "$pat"."o"; } } # handle last batch $src .= "return $last_code if $s;"; return $src; } sub do_report { my($m, $machine) = @_; my $summ; my $who; print "$m\n" unless defined $objects{$m}->{'type'}; return if $objects{$m}->{'type'} ne 'host'; my $message; warn "no email for $m\n" unless exists $machine->{'email'}; foreach my $report ( ALERT, WARN, UNUSUAL ) { my $index = "report-$report"; if( exists $machine->{$index} ) { my $h = "$labels[$report] for '$m'"; my $l = length($h); unshift @{$machine->{$index}}, # insert header at start "\t\t" . '=' x $l . "\n", "\t\t$h\n", "\t\t" . '=' x $l . "\n\n"; if( $report == ALERT && defined $machine->{'alert'} ) { $who = $machine->{'alert'}; } else { $who = $machine->{'email'}; } if( defined $opt_m ) { $who = $opt_m; } if( !defined $report{$who} ) { $report{$who} = []; } $report{$who} = [@{$report{$who}}, @{$machine->{$index}}, "\n\n"]; if( scalar @{$machine->{$index}} ) { $summ .= "\n\n **** summary for $m\n" unless $summ; $summ .= sprintf "\t%7d %s\n", scalar @{$machine->{$index}}, $labels[$report]; } } $who = $machine->{'email'}; } foreach my $key ( grep /^count/, keys %$machine ) { $summ .= "\n\n **** summary for $m\n" unless $summ; $summ .= "$key $machine->{$key}\n" if $key; } if( defined $machine->{'process'} ) { $summ .= "\n\n **** summary for $m\n" unless $summ; foreach my $key ( keys %{$machine->{'process'}} ) { $summ .= $code{$key}->(); } } if( defined $machine->{'ignored'} && scalar $machine->{'ignored'} ) { $summ .= "\n\n **** summary for $m\n" unless $summ; $summ .= sprintf "\t%7d Ignored logs\n", scalar $machine->{'ignored'}; } $summary .= $summ if defined $summ ; }