use Win32::Daemon; use Win32::TieRegistry ( Delimiter=>"/", ArrayValues=>1, SplitMultis => 1, AllowLoad => 1, qw( :REG_ KEY_READ KEY_WRITE KEY_ALL_ACCESS )); use win32::Process ("DETACHED_PROCESS"); use Win32::Console; use Win32; use Win32::NetResource; use Win32::FileSecurity; use Getopt::Long; use Win32::Lanman; use IO::Handle; #use strict; my ($VERSION)= sprintf("%d.%d", q$Revision: 1.40 $ =~ /(\d+)\.(\d+)/); my ($me) = $0; $me =~ s/.*(\\|\/)([a-zA-Z0-9\.]+)$/$2/; my ($banner) = " $me - version $VERSION\n"; my ($sysmonkey) = "/LMachine/System/CurrentControlSet/Services/sysmon/"; my ($paramkey) = $sysmonkey . 'Parameters/'; my ($schedkey) = $paramkey . 'Schedule Dirs/'; my ($jobskey) = $paramkey . 'Code/'; my ($debug,$rkey); my %Config = ( service => 'sysmon', display => 'System Monitoring Service', account => '', password => '', ); &Configure(\%Config); if ( $Config{help} || scalar @ARGV ) { open (MAN,"pod2text $0|"); while () {print " $_";} close MAN; exit 0; }elsif( $Config{install} ) { print "Installing service\n"; InstallService(); exit(); }elsif( $Config{remove} ) { RemoveService(); exit(); }elsif( $Config{reset} ) { SetRegistry(); exit(); }elsif( defined($Config{debug}) ) { if ( $rkey = $Registry->{$sysmonkey} ) { if ($Config{debug} >= 1) { $rkey->{"Parameters/"} = { "Debug" => [ 1 , "REG_SZ" ], }; print "set debug on OK\n"; }else{ $rkey->{"Parameters/"} = { "Debug" => [ 0 , "REG_SZ" ], }; print "set debug off OK\n"; } }else{ print "\n Failed to open registry key $sysmonkey\n"; } exit(); } # Check registry entries are set before starting service unless ( $rkey = $Registry->{$sysmonkey} ) { print <<"EOT"; Registry entries not set for sysmon. The service must be installed before being run. $useage EOT exit(); } my %exec = ( 1 => 'datetime stamp of script has changed', 2 => 'no record of previously executing', 3 => 'scheduled', 4 => 'registry set to force run' ); my (%params,%sched,%scripts,%sparams,$State,$script,$value); ReadRegSettings(); # Start the service...; Win32::Daemon::StartService() || exit(); my ($NewControls) = SERVICE_ACCEPT_STOP || SERVICE_ACCEPT_PAUSE_CONTINUE || SERVICE_ACCEPT_SHUTDOWN || SERVICE_ACCEPT_PARAMCHANGE || SERVICE_ACCEPT_NETBINDCHANGE; # Win32::Daemon::AcceptedControls( [$NewControls] ); my($PrevState) = SERVICE_START_PENDING; # Register the service Win32::Daemon::ShowService(); my($Buffer) = new Win32::Console(); $Buffer->Display(); $Buffer->Size(80, 120); $Buffer->Window(1, 0, 0, 80, 50); $Buffer->Title("system monitoring service"); $Buffer->Attr( eval($params{Console_FG}) | eval($params{Console_BG}) ); $Buffer->Cls( eval($params{Console_FG}) | eval($params{Console_BG}) ); Write("Console Opened\n"); &StartLog; &refresh; if ( $params{"Console"} eq "1" ) { #Write(" showing service...\n"); #Win32::Daemon::ShowService(); }elsif ( $params{"Console"} eq "0" ) { #Win32::Daemon::HideService(); }else{ Write("Registry setting for service show/hide is ambigious\n"); } my ($SERVICE_SLEEP_TIME) = $params{"Sleep Time"}; # How much time do we sleep between polling? my ($REFRESH_COUNT) = $params{"Refresh Count"}; # How often (in cycles) do we call refresh subroutine my ($TRUNC_LOG_COUNT) = $params{"TruncLogCount"}; # How often (in cycles) do we call log truncate subroutine my ($refresh_counter) = 1; # Initial Value - force to cycle immediatly my ($PROCESS_COUNT) = $params{"Process Count"}; # How often (in cycles) do we process schedule scripts.. my ($process_counter) = 1; # Initial Value - force to cycle immediatly my ($trunc_counter) = 1; # Initial Value - to truncate log file if ($params{Debug} > 0 ) { Write ("DEBUG: Sleep Time : $SERVICE_SLEEP_TIME\n"); Write ("DEBUG: Refresh Count : $REFRESH_COUNT\n"); Write ("DEBUG: Truncate Log Count : $TRUNC_LOG_COUNT\n"); foreach (keys %params) { Write( "DEBUG: Param: $_ Val: $params{$_}\n"); } foreach (keys %sched) { Write( "DEBUG: Sched: $_ Val: $sched{$_}\n"); } foreach (keys %scripts) { Write( "DEBUG: Script: $_ Val: $scripts{$_}\n"); } } $debug = $params{Debug}; my(%pr); if ($Config{test}) { Write("called with -test switch - calling main process once only\n"); &MainCall; # Run main loop once Write("quitting after running with -test switch - finished main process call\n"); }else{ while ( SERVICE_STOPPED != ( $State = Win32::Daemon::State() ) ){ if( SERVICE_START_PENDING == $State ){ # Initialization code $refresh_counter = 1; # Initial Value - force to cycle immediatly $process_counter = 1; # Initial Value - force to cycle immediatly Win32::Daemon::State( SERVICE_RUNNING ); Write($banner); Write( "Service Started\n" ); $PrevState = SERVICE_RUNNING; }elsif( SERVICE_PAUSE_PENDING == $State ){ # "Pausing..."; Win32::Daemon::State( SERVICE_PAUSED ); Write( "Service Paused\n" ); $PrevState = SERVICE_PAUSED; next; }elsif( SERVICE_CONTINUE_PENDING == $State ){ # "Resuming..."; $refresh_counter = 1; # Initial Value - force to cycle immediatly $process_counter = 1; # Initial Value - force to cycle immediatly Win32::Daemon::State( SERVICE_RUNNING ); Write( "Service Resumed\n" ); $PrevState = SERVICE_RUNNING; next; }elsif( SERVICE_STOP_PENDING == $State ){ Write( "Service Stop Requested\n" ); &KillSpawnedProcesses; # Kill all spawned processes..... Win32::Daemon::State( SERVICE_STOPPED ); $PrevState = SERVICE_STOPPED; Write( "Service Stopped\n" ); next; }elsif( SERVICE_CONTROL_SHUTDOWN == $State ){ # Request 10 seconds to shutdown... Write( "Service Control Shutdown Requested\n" ); Win32::Daemon::State( SERVICE_STOP_PENDING, 10000 ); &KillSpawnedProcesses; # Kill all spawned processes..... Win32::Daemon::State( SERVICE_STOPPED ); Write( "Service Stopped\n" ); $PrevState = SERVICE_STOPPED; }elsif( SERVICE_RUNNING == $State ){ &MainCall; }else{ # Got an unhandled control message Write ("Unhandled Control Message\n"); Win32::Daemon::State( $PrevState ); } # Check for any outstanding commands. Pass in a non zero value # and it resets the Last Message to SERVICE_CONTROL_NONE. if( SERVICE_CONTROL_NONE != ( my $Message = Win32::Daemon::QueryLastMessage( 1 ) ) ){ if( SERVICE_CONTROL_INTERROGATE == $Message ){ # Got here if the Service Control Manager is requesting # the current state of the service. This can happen for # a variety of reasons. Report the last state we set. Write ("Recieved Message SERVICE_CONTROL_INTERROGATE\n"); Win32::Daemon::State( $PrevState ); }elsif( SERVICE_CONTROL_SHUTDOWN == $Message ){ # Yikes! The system is shutting down. We had better clean up # and stop. # Tell the SCM that we are preparing to shutdown and that we expect # it to take 10 seconds (so don't terminate us for at least 10 seconds)... Write( "Service Control Shutdown Requested\n" ); Win32::Daemon::State( SERVICE_STOP_PENDING, 10000 ); &KillSpawnedProcesses; # Kill all spawned processes..... Win32::Daemon::State( SERVICE_STOPPED ); Write( "Service Stopped\n" ); $PrevState = SERVICE_STOPPED; }else{ Write("Message Recieved: $Message\n"); } } sleep( $SERVICE_SLEEP_TIME ); } } exit 0; sub MainCall { # Normal running .... Write ("DEBUG: Main Run routine called\n") if ($params{Debug} > 0 ); # Test to see if its time to truncate the logfile... $trunc_counter --; if ($trunc_counter < 1) { $trunc_counter = $TRUNC_LOG_COUNT; &truncate_file; } # Test to see if its time to refresh... $refresh_counter --; if ($refresh_counter < 1) { &refresh; # refresh service parameters from registry, re-read schedule jobs... &ReadRegSettings() ; # re-read registry if ($SERVICE_SLEEP_TIME != $params{"Sleep Time"}) { # How much time do we sleep between polling? if ( $params{"Sleep Time"} ) { # If value exists in registry Write(sprintf "changing Service Sleep Time to %s seconds\n",$params{"Sleep Time"}); $SERVICE_SLEEP_TIME = $params{"Sleep Time"}; }else{ Write("failed to read service \"Sleep Time\" from registry\n"); } } if ($REFRESH_COUNT != $params{"Refresh Count"}) { # How often (in cycles) do we call refresh subroutine if ( $params{"Refresh Count"} ) { # If value exists in registry Write(sprintf "changing refresh count to %s cycles before re-reading registry etc.\n",$params{"Refresh Count"}); $REFRESH_COUNT = $params{"Refresh Count"}; }else{ Write("failed to read service \"Refresh Count\" from registry\n"); } } if ($TRUNC_LOG_COUNT != $params{"TruncLogCount"}) { # How often (in cycles) do we call refresh subroutine if ( $params{"TruncLogCount"} ) { # If value exists in registry Write(sprintf "changing truncate log count to %s cycles before re-reading registry etc.\n",$params{"TruncLogCount"}); $TRUNC_LOG_COUNT = $params{"TruncLogCount"}; }else{ Write("failed to read service \"TruncLogCount\" from registry\n"); } } if ($PROCESS_COUNT != $params{"Process Count"}) { # How often (in cycles) do we process schedule scripts.. if ( $params{"Process Count"} ) { # If value exists in registry Write(sprintf "changing Process count to %s cycles before re-reading registry etc.\n",$params{"Process Count"}); $PROCESS_COUNT = $params{"Process Count"}; }else{ Write("failed to read service \"Process Count\" from registry\n"); } } if ($debug != $params{"Debug"}) { # Debug value change.. if ( $params{"Debug"} ) { # If value exists in registry Write(sprintf "changing debug to %s \n",$params{"Debug"}); $debug = $params{"Debug"}; } } $refresh_counter = $REFRESH_COUNT; } $process_counter --; Write("DEBUG Process Count: $process_counter\n") if ($params{Debug} > 0 ); if ($process_counter < 1) { $process_counter = $PROCESS_COUNT; # Check on status of existing spawned processes..... if (%pr) { while (my($script,$ref)=each %pr) { my($ExitCode); my($msg) = " - $script"; $ref->GetExitCode($ExitCode); if ( $ExitCode != 259 ) { Write ("$msg terminated with exit code $ExitCode\n"); delete $pr{$script}; }else{ if (my $pid = $ref->GetProcessID()) { Write ("$msg is still running (PID: $pid)\n"); }else{ Write ("$msg is still running (PID: unable to find PID)\n"); } } } } Write("DEBUG: finished checking existing processes\n") if ($params{Debug} > 0 ); ChkScripts: foreach $script (keys %scripts) { my($sched,$ptime,$time,$runtime,$pmtime,$timestamp,$process); Write("DEBUG: Testing if $script is to be executed now\n") if ($params{Debug} > 0 ); my ($exe) = 0; # Set execute flag to zero........ # Open registry key to script values area. my $key = $jobskey . "$script/"; if ($rkey = $Registry->{$key}) { undef %sparams; foreach $value ($rkey->ValueNames) { my( $valueString, $valueType )= $rkey->GetValue($value); $sparams{$value} = $valueString; } }else{ Write("\n Failed to open registry key $key\n"); next ChkScripts; } # Determine DateTimeStamp - and execute script if it has changed my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks); if ( ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($scripts{$script}) ) { if ( $mtime != hex($sparams{DateTimeStamp}) ) { Write ("DEBUG: DTS of $script has changed\n") if ($params{Debug} > 0 ); $exe = 1; # Set to execute - as datetime stamp of script has changed } unless ( $sparams{LastRunTime} ) { $exe = 2; # Set to execute - as no record of previously executing }else{ $ptime= $sparams{LastRunTime}; $time = hex($ptime); $runtime = localtime($time); #Write(" last executed $runtime - SchedDir $sparams{\"Sched Dir\"}\n"); # Find the schedule directory..... $sched = $sparams{"Sched Dir"}; Write(sprintf "DEBUG: %20s %12s %8s %12s\n",$script,$sched,time-$time,$sched{$sched}) if ($params{Debug} > 0 ); if ( (time - $time) > $sched{$sched} ) { $exe = 3 # Scheduled to run now.... } } }else{ Write("Unable to stat $scripts{$script} - removing from schedule list\n"); undef $scripts{$script}; } if ( $sparams{ForceRun} && ( $sparams{ForceRun} >= 1 ) ) { $exe = 4; # ForceRun flag set for this programe - so set execute on } if ( $exe >= 1 ) { Write("DEBUG: checking no instance of $script is running\n") if ($params{Debug} > 0 ); if ( defined $pr{$script} ) { Write("DEBUG: reference exists in \%pr hash for $script\n") if ($params{Debug} > 0 ); if (my $pid = $pr{$script}->GetProcessID()) { Write ("unable to spawn $script - a previously launched copy is running (PID: $pid)\n"); }else{ Write ("unable to spawn $script - a previously launched copy running (unable to identify PID)\n"); } }else{ Write("DEBUG: launching $script\n") if ($params{Debug} > 0 ); my($args) = "perl $params{Root}/perl/perlcaller.pl $sparams{Log} $scripts{$script}"; my($app) = $^X; $args= Win32::ExpandEnvironmentStrings($args); # - in case the Log has an ENV to be expanded # Spawn the perlcaller.pl script here. This opens the log file and calls the script Write("DEBUG: spawning now...\n") if ($params{Debug} > 0 ); if ( Win32::Process::Create( $process, $app, $args, 1, DETACHED_PROCESS, $sparams{"Sched Dir"} ) ) { Write("DEBUG: spawned OK...\n") if ($params{Debug} > 0 ); my ($pid); unless ( $pid = $process->GetProcessID() ) { Write("DEBUG: can't determine pid\n") if ($params{Debug} > 0 ); } Write("spawned $script PID:$pid ($exec{$exe})\n"); $pr{$script} = $process; # Add process reference to hash of process references if ($params{Debug} > 0 ) { Write(" app : $app\n"); Write(" args: $args\n"); Write(" dir : " . $sparams{"Sched Dir"} . "\n" ); } # Identify and record run time in registry $time = time; $ptime = pack("L",$time); $pmtime = pack("L",$mtime); $runtime = localtime($time); $timestamp = localtime($mtime); # Record the rundate as a value $rkey->SetValue( "LastRunTime" , pack("L",$time) , "REG_DWORD" ); $rkey->SetValue( "Last Run Time" , $runtime , "REG_SZ" ); $rkey->SetValue( "DateTimeStamp" , pack("L",$mtime) , "REG_DWORD" ); $rkey->SetValue( "Date Time Stamp", $timestamp , "REG_SZ" ); # Set ForceRun to 0 if it was set if ( $sparams{ForceRun} ) { $rkey->SetValue( "ForceRun" , 0 , "REG_SZ" ); } # Don't spawn more than one script at a time....wait for next cycle if ( $params{"Max Processes"} ) { my(@KEYS) = keys %pr; # create array of keys of %pr - to identify number of elements last ChkScripts if ($#KEYS >= ($params{"Max Processes"}-1) ); # launch no more processes if Max Processes exceeded (incl this one) }else{ Write("Max Processes value missing from registry\n"); last ChkScripts; } }else{ Write("unable to spawn $script $args\n"); } } } } } } sub GetServiceConfig { my $ScriptPath = join( "", Win32::GetFullPathName( $0 ) ); my %Hash = ( name => $Config{service}, display => $Config{display}, path => $^X, user => $Config{account}, password => $Config{password}, parameters => "\"$ScriptPath\"", description=> "sysmon is a schedule framework that runs as a service, calling system monitoring tasks.", ); $Hash{parameters} .= " -debug" if( $Config{debug} ); $Hash{parameters} .= " -console" if( $Config{console} ); $Hash{parameters} .= " -nopage" if( $Config{nopage} ); return( \%Hash ); } sub InstallService { my @SidList; my @accounts = ( $Config{account} ); #print "Finding SID for $Config{account}\n"; if ( Win32::Lanman::LsaLookupNames( "",\@accounts,\@SidList ) ) { foreach my $Sid ( @SidList ) { my @privileges; if ( Win32::Lanman::LsaEnumerateAccountRights("", $Sid->{sid}, \@privileges)) { push (@privileges,SE_SERVICE_LOGON_NAME,SE_INTERACTIVE_LOGON_NAME); #print "Privs: @privileges\n"; if (Win32::Lanman::LsaAddAccountRights( "",$Sid->{sid},\@privileges ) ) { print "Added \n \"Logon As Service\" and \"Interactive Logon\"\n rights for $Config{account}\n"; }else{ print "Failed to add \n \"Logon As Service\" and \"Interactive Logon\"\n rights for $Config{account}\n"; } }else{ print "Failed to enumerate privileges for $Config{account}\n"; } } }else{ print "Unable to find SID for $Config{account}\n"; } my $ServiceConfig = GetServiceConfig(); if( Win32::Daemon::CreateService( $ServiceConfig ) ) { print "The $ServiceConfig->{display} was successfully installed.\n"; } else { print "Failed to add the $ServiceConfig->{display} service.\nError: " . GetError() . "\n"; } SetRegistry(); } sub SetRegistry { if ( $rkey = $Registry->{$sysmonkey} ) { my($sysmonroot) = Win32::ExpandEnvironmentStrings("%SystemDrive%/sysmon"); my($sysmonlogs) = Win32::ExpandEnvironmentStrings("%SystemDrive%/sysmon-logs"); $rkey->{"Parameters/"} = { "Root" => [ $sysmonroot , "REG_SZ" ], "Logs" => [ $sysmonlogs , "REG_SZ" ], "SysMonLogLines"=> [ "10000" , "REG_SZ" ], "SysMonLogAge" => [ "100" , "REG_SZ" ], "Sleep Time" => [ "5" , "REG_SZ" ], # Max time before we respond to svc manager "Refresh Count" => [ "100" , "REG_SZ" ], # "Process Count" => [ "12" , "REG_SZ" ], "TruncLogCount" => [ "8000" , "REG_SZ" ], "Debug" => [ $debug , "REG_SZ" ], "Max Processes" => [ "3" , "REG_SZ" ], # Max no of processes sysmon is allowed "Console_BG" => [ "BACKGROUND_BLUE", "REG_SZ" ], # Console Background Colour "Console_FG" => [ "\$FG_YELLOW" , "REG_SZ" ], # Console Foreground Colour "Console" => [ "1" , "REG_SZ" ], # Console (1 = Visible, 0 = Not) "Schedule Dirs/" => { "allways" => ["1" , "REG_SZ" ], "daily" => ["86400" , "REG_SZ" ], "6hourly" => ["21960" , "REG_SZ" ], "hourly" => ["3660" , "REG_SZ" ], "weekly" => ["604800" , "REG_SZ" ], "oneoff" => ["999999999", "REG_SZ" ], }, "Code/" => { }, }; $rkey->SetValue( "Type" , "0x110" , "REG_DWORD" ); # 0x110 - display console, else 0x10 print "sysmon service configured OK\n"; }else{ print "\n Failed to open registry key $sysmonkey \n - check that sysmon is installed as a service\n"; } } sub RemoveService { my $ServiceConfig = GetServiceConfig(); if( Win32::Daemon::DeleteService( $ServiceConfig->{name} ) ) { print "The $ServiceConfig->{display} was successfully removed.\n"; } else { print "Failed to remove the $ServiceConfig->{display} service.\nError: " . GetError() . "\n"; } } sub GetError { return( Win32::FormatMessage( Win32::Daemon::GetLastError() ) ); } sub Write { my( $Message ) = @_; $Message = "[" . scalar( localtime() ) . "] $Message"; if (defined $Buffer) { $Buffer->Write($Message); } print $Message; } sub StartLog { # Verify the log directory exists if ( $params{Logs} && ( ! -d $params{Logs} ) ) { if ( mkdir ($params{Logs},0777) ) { Write("Created log directory $params{Logs}\n"); }else{ Write("Unable to create log directory $params{Logs}\n"); } } if ( $params{Logs} && ( -d $params{Logs} ) ) { # Set directory permissions my($dir_sec) = Win32::FileSecurity::MakeMask(qw(FULL GENERIC_ALL)); my(%hash); $hash{"Administrator"} = $dir_sec; $hash{"Administrators"} = $dir_sec; Win32::FileSecurity::Set($params{Logs}, \%hash); # Share the log directory as sysmon$ (allows remote collection of logs etc.) my($sh) = "sysmon\$"; my($shpath,$ShareInfo); if (Win32::NetResource::NetShareGetInfo( $sh, $ShareInfo )){ $shpath = $$ShareInfo{path}; $shpath =~ s/\\/\//g; } if ( $shpath ne $params{Logs} ) { Write("Need to do something about the share\n"); Write("Current Path $shpath\n"); Write("Required Path $params{Logs}\n"); # Delete old share (if it exists) if ( $shpath ) { if (Win32::NetResource::NetShareDel( $sh )) { Write("Removed incorrect sysmon log share $sh\n"); }else{ Write("Failed to delete sysmon log share $sh\n"); } } # Add the share...... my($ShareInfo) = { 'path' => $params{Logs}, 'netname' => $sh, 'remark' => "Sysmon Log Directory", 'passwd' => "", 'current-users' => 0, 'permissions' => 0, 'maxusers' => -1, 'type' => 0, }; my($parm); if (Win32::NetResource::NetShareAdd( $ShareInfo,$parm )) { Write("Added sysmon log share OK as $sh\n"); }else{ Write("Failed to add share for sysmon log directory ( $parm ) \n"); } } } # Divert STDOUT and STDERR to log file if running as a service my ( $DB_FILE ) = "$params{Logs}/sysmon.log"; open(LOG,">> $DB_FILE") || Err("can't append to log_file \"$DB_FILE\": $!");# && exit 1; #open(STDOUT,">&LOG") || Err("can't redirect stdout: $!");# && exit 1; select LOG; open(STDERR,">&LOG") || Err("can't redirect stderr: $!");# && exit 1; # Enable Autoflush LOG->autoflush(1); STDOUT->autoflush(1); STDERR->autoflush(1); # if( open( LOG, ">> $DB_FILE" ) ) # { # Write("Opened log $DB_FILE\n"); # select LOG; # $|=1; # }else{ # Write("Failed to open log $DB_FILE\n"); # } } sub ReadRegSettings { # read sysmon parameters from registry if ($rkey = $Registry->{$paramkey} ) { undef %params; foreach $value ($rkey->ValueNames) { my( $valueString, $valueType )= $rkey->GetValue($value); $params{$value} = $valueString; } }else{ &Err("Failed to open registry key $paramkey\n"); } } sub refresh { my $schedule; if ( $params{"Root"} ) { Write("DEBUG: finding executable files in schedule\n") if ($params{Debug} > 0 ); undef %scripts; $schedule = $params{"Root"} . '/schedule'; $schedule =~ s/\/\//\//; # sub a // for / } # read script dirs from registry if ($rkey = $Registry->{$schedkey} ) { undef %sched; foreach $value ($rkey->ValueNames) { my( $valueString, $valueType )= $rkey->GetValue($value); $sched{$schedule . '/' . $value} = $valueString; } }else{ &Err( "Failed to open registry key $schedkey\nThis is required for the sysmon service\nSetting default registry entries\n"); SetRegistry(); return; } # find executable files in the schedule file structure undef %scripts; foreach my $dir (keys %sched) { #Write("** searching $dir\n"); if ( $rkey = $Registry->{$jobskey} ) { if ( chdir $dir ) { foreach (<*.*>) { next unless m/\.(pl|cmd|bat)$/i; # Ignore all but perl or batch files $scripts{$_} = $dir . '/' . $_; my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($_); my($pmtime) = pack("L",$mtime); # Create log dir entry... my ($log) = $params{'Logs'} . '/' . $_; $log =~ s/\.([a-zA-Z0-9]+)$/\.log/; $rkey->{"$_/"} = { "DateTimeStamp" => [ $pmtime , "REG_DWORD" ], "Sched Dir" => [ $dir , "REG_SZ"], "Log" => [ $log , "REG_SZ"] }; } } else{ Write ("unable to chdir to $dir\n"); } } else{ print "Failed to open registry key $jobskey\n"; } } } sub Err{ my ($message) = @_; $message = "ERROR: $message"; Write($message); sleep 1; } sub Configure { my( $Config ) = @_; my $WarnSub = $SIG{__WARN__}; #undef $SIG{__WARN__}; Getopt::Long::Configure( "prefix_pattern=(-|\/)" ); GetOptions( $Config, qw( install remove reset account=s password=s debug=s nodebug help test ) ); $SIG{__WARN__} = $WarnSub; } sub KillSpawnedProcesses { # Kill all spawned processes..... if (%pr) { while (my($script,$ref)=each %pr) { my($ExitCode); my($msg) = "child $script"; $ref->GetExitCode($ExitCode); if ( $ExitCode != 259 ) { Write ("$msg (terminated with exit code $ExitCode)\n"); delete $pr{$script}; }else{ # Kill remaining processes if (my $pid = $ref->GetProcessID()) { if ( $ref->Kill( 0 ) ) { $ref->GetExitCode($ExitCode); Write ("$msg (PID: $pid) killed (exit code $ExitCode)\n"); delete $pr{$script}; }else{ Write ("$msg (PID: $pid) failed to kill !\n"); } }else{ Write ("$msg killed (PID: unable to find PID)\n"); } } } } } sub truncate_file { my($line, $lines); my @array = (); my ($size) = $params{SysMonLogLines}; # Max number of lines in the log file my ($age) = $params{SysMonLogAge}; # Max age of records in the log file (in days). To be implemented.... my ( $DB_FILE ) = "$params{Logs}/sysmon.log"; #my ( $T_LOG ) = $DB_FILE; #$T_LOG =~ s/\.log/\.tmp\.log/; #if( open( TLOG, ">> $T_LOG" ) ) #{ # Write("Opened log $T_LOG\n"); # select TLOG; # $|=1; #}else{ # Write("Failed to open temp log $T_LOG\n"); #} #close LOG; my($err) = 0; if( open( FILE, "+>> $DB_FILE" ) ) { # Read file into array while () { push(@array,$_) } close(FILE); # Truncate array and write back to file if ( ($lines = @array) > $size ) { if (open(FILE,"> $DB_FILE")) { for($line = $lines - $size ; $line < $lines; $line++) { print FILE $array[$line]; } $line = $lines - $size ; close(FILE); }else { $err =1; } } }else { $err = 3; } #close TLOG; #&StartLog; if ( $err == 0 ) { if ( $lines > $size ) { Write("Truncated Log File from $lines to $size lines (starting at $line)\n"); } }elsif( $err == 1 ) { Err("Truncate Log File - Can't write to file \"$DB_FILE\": $! \n"); }elsif( $err == 2 ) { Err("Truncate Log File - ????: $!\n"); }elsif( $err == 3 ) { Err("Truncate Log File - Can't open file \"$DB_FILE\": $!\n"); }elsif( $err == 4 ) { Err("Truncate Log File - ????: $!\n"); }else{ Err("Truncate Log File - unknown file truncate error\n"); } } 1; __END__ =head1 NAME sysmon.pl =head1 SYNOPSIS sysmon.pl [-install [-account=XX] [-password=XX] | -remove | -reset ] [-help] [-debug=X] [-test] =head1 DESCRIPTION This script controls the System Monitoring Service (SysMon). SysMon is an NT service that executes various perl scripts and command files using a non-deterministic schedule. SysMon allows scripts to be added and removed as it executes, and creates a log of each scripts execution, as well as a history of previous executions. SysMon is configured significantly from registry settings - whose defaults are configured when the service is installed. If the service is installed as System, and allowed to interact with the desktop, a console is generated for monitoring purposes. The SysMon service also creates a circular log file for recording its actions. =head1 OPTIONS =over 4 =item -install install sysmon as a service (this can also be used with the account and password =item -account the account name the service is to run with (the default option is to use the System account) =item -password the password for the account =item -remove remove sysmon service =item -reset set default registry settings for the service. =item -debug set verbose (or debug) option (0=off, 1 =on). This can be executed with service running, and will eventually be re-read by the service after which verbose output will be switched on =item -help show short help message =back =head1 REGISTRY SETTINGS The SysMon is broardly configured through its registry settings. These are all located at HKLM/System/CurrentControlSet/Services/sysmon/Parameters =over 4 =item Root (REG_SZ) The location of the SysMon directory structure - %SystemDrive%/sysmon by default =item Logs (REG_SZ) The location of the SysMon service logs - %SystemDrive%/sysmon-logs by default =item SysMonLogLines (REG_SZ) The number of lines to be maintain in the SysMon log file - deafult 10000 =item SysMonLogAge (REG_SZ) The maximum age in days of any entry in the SysMon log file - default 100 (not yet implemenetd =item Sleep Time (REG_SZ) The maximum time interval in seconds before the service responds to the service manager =item Refresh Count (REG_SZ) The number of cycles after which SysMon re-reads its registry settings - default 100. (so using default settings the registry will be re-read every 500 seconds) =item Process Count (REG_SZ) The number of cycles after which SysMon service will check to determine if any further processes should be spawned, and to report on existing child processes. - default 12 =item TruncLogCount (REG_SZ) default 8000 =item Debug (REG_SZ) Set to 0 (default) for normal output, and 1 for verbose. =item Max Processes (REG_SZ) The maximum number of processes the SysMon service will generate. If a script is scheduled or readyto be spawned, and this limit is reached, it will wait until the number of processes reduces below the macximum - default 3. =item Console_BG (REG_SZ) The console background - set to BACKGROUND_BLUE by default =item Console_FG (REG_SZ) The console foreground (text) colour - set to $FG_YELLOW by default =item Console (REG_SZ) A flag to set the console visible (default 1) or hidden (0) =back The HKLM/System/CurrentControlSet/Services/sysmon/Parameters/Schedule Dirs key contains the names of directories that contain scripts to be executed to a given routine. The actual directory locations are under the "schedule" directory in the SysMon service Root. The values given below are the default locations, with the scheduled time between script execution given in seconds. allways => 1 daily => 86400 6hourly => 21960 hourly => 3660 weekly => 604800 oneoff => 999999999 Additional values for other directories can be added. The HKLM/System/CurrentControlSet/Services/sysmon/Parameters/Code key will have a sub-key created for each script oidentified in a schedule directory. The SysMon service will maintain key data for each script within this sub-key. This includes the following values:- =over 4 =item Date Time Stamp (REG_SZ) The date/time of the script - in human readable form =item DateTimeStamp (REG_DWORD) The date/time stamp of the script. If this is seen to have changed (scripts are checked at every Process Count) the script will be re-executed as soon a permitted. =item Last Run Time (REG_SZ) The last date/time when the script was executed by the SysMon service - in human readable form =item LastRunTime (REG_SZ) The last date/time when the script was executed by the SysMon service. This, together with the schedule period identified by the scedule directory in which the script is located, is used to determine the next execution time. =item Log (REG_SZ) The log file used to record the STDOUT and STDERR from the script execution. This is derived by the service from the script name and the Logs value =item Sched Dir (REG_SZ) The schedule directory in which the script is located =back =head1 REQUIRED MODULES Win32::Daemon Win32::TieRegistry win32::Process Win32::Console Win32 Win32::NetResource Win32::FileSecurity Getopt::Long Win32::Lanman IO::Handle =head1 SEE ALSO =head1 EXAMPLES =head1 TO DO =head1 AUTHOR Dave Roberts =head1 SUPPORT You can send bug reports and suggestions for improvements on this module to me at DaveRoberts@iname.com. However, I can't promise to offer any other support for this script. =head1 COPYRIGHT This script is Copyright © 2000, 2001 Dave Roberts. All rights reserved. This script is free software; you can redistribute it and/or modify it under the same terms as Perl itself. This script 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. The copyright holder of this script can not be held liable for any general, special, incidental or consequential damages arising out of the use of the script. =head1 CHANGE HISTORY $Log: sysmon.pl $ Revision 1.40 2001/12/18 18:25:44 Dave.Roberts corrected service management errors Revision 1.38 2001/12/14 11:35:51 Dave.Roberts added pod, and used this for the -help option =cut