#!/usr/bin/perl -w ################################################################### # evlogsys.pl # # 2001-08-10 # Author: hal@enteract.com # hal@vailsys.com # Version: 1.0 # License: BSD-type ################################################################### ################################################################### # # scan NT Event Log periodically and copy new entries to syslog # # usage: evlogsys.pl [-c config-file] [-d] [-p] # -d enables debug mode # -p will log start of each scan ("pulse") # # run as a service with admin rights # ################################################################### ################################################################### # # Configuration file is evlogsys.cfg. # # Its default location is C:/EvLogSys/ # # Blank lines and lines beginning with # are ignored. # Other entries are # log_host = xxxxxx (IP addr or hostname of syslog server) # scan_interval_sec = xxxx (number of seconds between event log scans) # source = xxxx... (one or more of "system", "security", "application") # Values are case-insensitive. If there is more than one source spec, # all specs are combined. # ################################################################### use strict; use Getopt::Std; use vars qw/ $opt_c $opt_d /; use Win32::EventLog; use Win32::NetAdmin; ################################################################### # # usage () # # no args; no return # ################################################################### sub usage () { print "Usage: evlogsys.pl [-c configfile] [-d]\n"; exit 1; } use Socket; use constant SYSLOG_PORT_NO => 514; use constant LOG_EMERG => 0 ; # system is unusable use constant LOG_ALERT => 1 ; # action must be taken immediately use constant LOG_CRIT => 2 ; # critical conditions use constant LOG_ERR => 3 ; # error conditions use constant LOG_WARNING => 4 ; # warning conditions use constant LOG_NOTICE => 5 ; # normal but significant condition use constant LOG_INFO => 6 ; # informational use constant LOG_DEBUG => 7 ; # debug-level messages use constant LOG_KERN => (0<<3) ; # kernel messages use constant LOG_USER => (1<<3) ; # random user-level messages use constant LOG_MAIL => (2<<3) ; # mail system use constant LOG_DAEMON => (3<<3) ; # system daemons use constant LOG_AUTH => (4<<3) ; # security/authorization messages use constant LOG_SYSLOG => (5<<3) ; # messages generated internally by syslogd use constant LOG_LPR => (6<<3) ; # line printer subsystem use constant LOG_NEWS => (7<<3) ; # network news subsystem use constant LOG_UUCP => (8<<3) ; # UUCP subsystem use constant LOG_CRON => (9<<3) ; # clock daemon use constant LOG_AUTHPRIV => (10<<3) ; # security/authorization messages (private) use constant LOG_FTP => (11<<3) ; # ftp daemon use constant LOG_LOCAL0 => (16<<3) ; # reserved for local use use constant LOG_LOCAL1 => (17<<3) ; # reserved for local use use constant LOG_LOCAL2 => (18<<3) ; # reserved for local use use constant LOG_LOCAL3 => (19<<3) ; # reserved for local use use constant LOG_LOCAL4 => (20<<3) ; # reserved for local use use constant LOG_LOCAL5 => (21<<3) ; # reserved for local use use constant LOG_LOCAL6 => (22<<3) ; # reserved for local use use constant LOG_LOCAL7 => (23<<3) ; # reserved for local use # default to LOCAL3; may override in config file my $facility = LOG_LOCAL3; my $SockHandle; my $portaddr; ################################# # syslog_open($server) ################################# sub syslog_open ($) { my $server = shift; socket (SockHandle, PF_INET, SOCK_DGRAM, getprotobyname("udp")) or return 0; my $ipaddr = inet_aton($server); $portaddr = sockaddr_in(SYSLOG_PORT_NO, $ipaddr); return 1; } ################################################################### # # logit($message, $host, $facility) # # send a string to syslog about a certain host # Use timestamp from computer running this program. # # Globals: SockHandle, $portaddr, LOG constants. # ################################################################### sub logit ($$$) { my $msg = shift; my $host = shift; my $facility = shift; my $log_level = LOG_DEBUG | $facility; # strip day of week and year from localtime my $timestamp = substr scalar(localtime), 4, 15; my $txt = sprintf "<%d>%s %s \[NTLOG\]: %s", $log_level, $timestamp, $host, $msg; return send(SockHandle, $txt, 0, $portaddr) == length($txt); } ################################################################### # # process_config_file ($config_file_name, %$hashref) # # exit on failure # ################################################################### sub process_config_file ($$) { my $config_file_name = shift; my $hashref = shift; my $parse_err_ct = 0; my @sources = (); my @evhosts = (); my %omithosts = (); %$hashref = (); open CONFIG, $config_file_name or die "Can't open config file: $!"; my $line_num = 0; while () { # count lines for error reporting $line_num++; # skip blank lines next if (/^\s*$/); # skip comment lines next if (/^\s*\#.*$/); chomp; if (/syslog_host\s*=\s*(\w+)/i) { $$hashref{'syslog_host'} = lc $1; } elsif (/domain\s*=\s*(\w+)/i) { $$hashref{'domain'} = uc $1; } elsif (/scan_interval_sec\s*=\s*(\d+)\s*/i) { $$hashref{'scan_interval_sec'} = $1; } elsif (/source\s*=\s*(.*)/i) { my @s = split /\s+/, lc $1; push @sources, @s; } elsif (/event_log_host\s*=\s*(.*)/i) { my @s = split /\s+/, uc $1; push @evhosts, @s; } elsif (/omit_host\s*=\s*(.*)/i) { my @s = split /\s+/, uc $1; # add a null value for each host key in %omithosts @omithosts{@s} = (); } elsif (/facility\s*=\s*(local[0-7])/i) { $$hashref{'facility'} = eval ('LOG_'.uc $1); } else { print "Error at line $line_num: $_\n"; $parse_err_ct++; } } close CONFIG; # validity checks on config file contents unless (exists $$hashref{'syslog_host'}) { $parse_err_ct++; print "no syslog host given\n"; } unless (exists $$hashref{'scan_interval_sec'}) { $parse_err_ct++; print "no scan interval given\n"; } if (@sources == 0) { $parse_err_ct++; print "no event log sources given\n"; } print "sources: @sources\n" if (defined $opt_d); $$hashref{'sources'} = \@sources; if (@evhosts == 0 && ! exists $$hashref{'domain'}) { $parse_err_ct++; print "no event log hosts given\n"; } print "event log hosts: @evhosts\n" if (defined $opt_d); $$hashref{'evhosts'} = \@evhosts; $$hashref{'omithosts'} = \%omithosts if %omithosts; if ($parse_err_ct) { print "Aborting - $parse_err_ct error(s) in $config_file_name\n"; exit 1; } } ################################################################### # # scan_log ($source, $host, $time_limit, $facility) # # Send events to syslog. # # No useful return value, unfortunately. # # $time_limit is a value in time() format; scan_log will not # report events older than $time_limit based on the timestamp # in the remote NT host's event log # ################################################################### sub scan_log ($$$$) { my ($source, $host, $time_limit, $facility) = @_; my $hashref; # "new" method doesn't return valid error status my $NT_log = Win32::EventLog->new($source, $host); logit "open log apparently failed", $host, $facility unless defined $NT_log; my $num; unless ($NT_log->GetNumber($num)) { logit "can't get $source record count", $host, $facility; goto SCAN_DONE; } my $oldest; unless ($NT_log->GetOldest($oldest)) { logit "can't get oldest record # for $source", $host, $facility; goto SCAN_DONE; } my $flag = EVENTLOG_BACKWARDS_READ | EVENTLOG_SEQUENTIAL_READ; my %hash; READ_LOG: while ($NT_log->Read($flag, 0, \%hash)) { # event attributes my ($time_gen, $priority, $pri_str, $event_id, $msg, $esource); $time_gen = $hash{TimeGenerated}; unless (defined $time_gen) { logit "no TimeGenerated on event", $host, $facility; next READ_LOG; } logit (("event time ".localtime($time_gen)), $host, $facility) if defined $opt_d; last READ_LOG unless $time_limit < $time_gen; if( $hash{EventType} == EVENTLOG_ERROR_TYPE ) { $priority = LOG_ERR; $pri_str = "ERROR"; } elsif( $hash{EventType} == EVENTLOG_WARNING_TYPE ) { $priority = LOG_WARNING; $pri_str = "WARNING"; } elsif( $hash{EventType} == EVENTLOG_INFORMATION_TYPE ) { $priority = LOG_INFO; $pri_str = "INFO"; } else { $priority = LOG_DEBUG; $pri_str = "OTHER"; } $event_id = $hash{EventID} & 0xffff; $event_id = 'XXXX' unless defined $event_id && $event_id >= 0; $esource = "-" unless $esource = $hash{Source}; # Get text and strip out MS newline braindamage. $msg = $hash{Message}; $msg = $hash{Strings} unless $msg; $msg = '-' unless $msg; # event log Strings has embedded null bytes - nasty! $msg =~ s/\0/ /g; $msg =~ s/\s+/ /g; my $timestr = "" . localtime($time_gen); my $timestamp = substr $timestr, 4, 15; my $log_level = $priority | $facility; my $txt = sprintf "<%d>", $log_level; $txt .= "$timestamp $host \[$pri_str\]: <$event_id> $esource $msg"; print "$txt\n" if defined $opt_d; send(SockHandle, $txt, 0, $portaddr); } SCAN_DONE: # Closing event log explicitly caused leaks and crashes. } ################################################################### # # main program # ################################################################### # put text into Message key of event hash $Win32::EventLog::GetMessageText = 1; # parse the command line usage unless getopts('c:dp'); # process config file my $this_host = Win32::NodeName; my $config_file_name = "c:/evlogsys/evlogsys.cfg"; my %config_settings; $config_file_name = $opt_c if (defined $opt_c); process_config_file($config_file_name, \%config_settings); my $NT_host; my @sources = @{$config_settings{'sources'}}; my $source; my $syslog_host = $config_settings{'syslog_host'}; my $scan_interval_sec = $config_settings{'scan_interval_sec'}; $facility = $config_settings{'facility'} if (exists($config_settings{'facility'})); # if a domain is specified in config file, poll all hosts in that domain my @NT_hosts; if (exists $config_settings{'domain'}) { my $domain = $config_settings{'domain'}; Win32::NetAdmin::GetServers( '', $domain, SV_TYPE_NT, \@NT_hosts ); } else { @NT_hosts = @{$config_settings{'evhosts'}}; } # delete hosts to be omitted from list of all NT systems in domain my @scan_hosts; if (exists $config_settings{'omithosts'}) { my $omits = $config_settings{'omithosts'}; foreach $NT_host (sort @NT_hosts) { push @scan_hosts, $NT_host unless exists $$omits{lc $NT_host}; } } else { @scan_hosts = sort @NT_hosts; } syslog_open($syslog_host) or die "Can't open syslog to $syslog_host: $!"; logit "scanning NT logs every $scan_interval_sec sec", $this_host, $facility; # scan all NT hosts forever for (;;) { # only care about events occurring within latest scan interval my $start_scan_time = time; my $time_limit = $start_scan_time - $scan_interval_sec; # need something logged every pass just to show we're alive logit ("start scan ".localtime, $this_host, $facility); foreach $NT_host (@scan_hosts) { SOURCE: foreach $source (@sources) { logit ("scanning $source back to ". localtime($time_limit), $NT_host, $facility) if defined $opt_d; scan_log($source, $NT_host, $time_limit, $facility); } } my $sleep_secs = ($start_scan_time + $scan_interval_sec) - time; sleep $sleep_secs if $sleep_secs > 0; } # done exit 0;