#!/usr/bin/perl # # 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) # Perl code written by Russell Fulton , # (Thanks Russell!) # & Harry Hoffman of the University of Auckland. # This is a work in progress. # This script is copyrighted under the GPL (Gnu Public License). # If you did not recieve a copy of the GPL with this program please visit # http://www.gnu.org/copyleft/gpl.html # 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 $log_base $domain $time_fmt $wanted_pattern $default_patterns); #Process command line opts -- these override the defaults and those # specified in the -c . getopts("o:m:OMC:c:d:s:vr") 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 $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 = '^/var/log/HOSTS/([^/]+)/([^/]+)/$year/$month/$day'; $default_patterns = "/usr/local/etc/log.patterns"; $log_base = '/var/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 ($mach, $log) = $File::Find::dir =~ m"$wanted_pattern"o; # assumes that log files have numeric data appended to name if( defined $mach and /\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 = ''; # walk the log file heirarchy find($wanted, $log_base); # scan the log files foreach my $m (keys %objects ) { # produce the reports if(defined $objects{$m}->{'proccessed'} ) { do_report( $m, $objects{$m} ) ; } else { delete $objects{$m}; } } foreach my $email ( keys %report ) { # email them 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 "$mach\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(/ANON anonymous: Login successful/) { print; } $class = $proc->($_); if( $class == 0 or $class == 3 ) { $ignored++; next; } unless ( defined $machine->{"report-$class"} ) { $machine->{"report-$class"} = []; } push(@{$machine->{"report-$class"}}, "$_"); } $offset = tell LOG; close( LOG ); $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; 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 my @opts = split(/\s*,\s*|\s*=>\s*|\s*=\s*/, $1); $machine = shift @opts; $objects{$machine} = {}; check_opts(\@opts); %{$objects{$machine}} = @opts; if( defined $opt_m ) { $objects{$machine}->{'email'} = $objects{$machine}->{'alert'} = $opt_m; } $objects{$machine}->{'list'} = []; next; } 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( $objects{$machine}->{'list'} ); use strict 'refs'; print "$machine:\n", $objects{$machine}->{'src'},"\n" if defined $opt_r; eval "\$objects{\$machine}->{'proc'} = ". "sub { \$_ = shift; study \$_; " . "$objects{$machine}->{'src'}; 4}"; if( $@ ) { # something went wrong! print STDERR "Compile errors for $machine: $@\n"; $objects{$machine}->{'proc'} = undef; } } } sub add_rules { my $rules = shift; 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( $objects{$m}->{'list'} )); } else { # its a rule my( $type, $re ) = split(/\t/, $rule); $type =~ tr/a-z/A-Z/; # check type *********** if( $type !~ /^(A|W|I|I\*)(-\d+)?/ ) { warn "unrecognized type '$type' in rule '$rule'"; next; } # we sort on $type to get order of application of matches... my ($t, $o) = split('-', $type); $o = '' unless defined $o; $type = 0 if $t eq 'I*'; $type = 1 if $t eq 'A'; $type = 2 if $t eq 'W'; $type = 3 if $t eq 'I'; push(@code, "$type\t$o\t$re"); } } return @code; } sub write_src_simple { my $list = shift; my $src = ''; foreach my $line (sort &add_rules( $list )) { my ($code, $o, $pat) = split("\t", $line); $src .= "return $code 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 $who; print "$m\n" unless defined $objects{$m}->{'type'}; return if $objects{$m}->{'type'} ne 'host'; $summary .= "\n\n **** summary for $m\n" ; my $message; warn "no email for $m\n" unless exists $machine->{'email'}; if( exists $machine->{'report-1'} ) { # Any alerts ? unshift @{$machine->{'report-1'}}, # insert header at start "\t\t===============\n", "\t\tAlerts for '$m'\n", "\t\t===============\n\n"; if( 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->{'report-1'}}, "\n\n"]; $summary .= sprintf "\t%7d Alerts\n", scalar @{$machine->{'report-1'}}; } $who = $machine->{'email'}; if( !defined $report{$who} ) { $report{$who} = []; } if( exists $machine->{'report-2'} ) { # Any Warnings ? unshift @{$machine->{'report-2'}}, # insert header at start "\t\t===============\n", "\t\t Warnings for '$m'\n", "\t\t===============\n\n"; $report{$who} = [@{$report{$who}}, @{$machine->{'report-2'}}, "\n\n"]; $summary .= sprintf "\t%7d Warnings\n", scalar @{$machine->{'report-2'}}; } if( exists $machine->{'report-4'} ) { # Any unusual logs ? unshift @{$machine->{'report-4'}}, # insert header at start "\t\t======================\n", "\t\tUnusual logs for '$m'\n", "\t\t====================\n\n"; $report{$who} =[@{$report{$who}}, @{$machine->{'report-4'}}, "\n\n"]; $summary .= sprintf "\t%7d Unusual logs\n", scalar @{$machine->{'report-4'}}; } if( defined $machine->{'ignored'} ) { $summary .= sprintf "\t%7d Ignored logs\n", scalar $machine->{'ignored'}; } }