# LogAgent 4.0 Freeware and Open Source version
# Now includes the ablility to monitor, convert to ascii and centralize the Event Viewer logs
# Changes since 3.0 : Ability to log date and time
#########################################################################################
# LogAgent 4.0 Free, Open Source version						#
# by Floydman  floydian_99@yahoo.com							#
# Copyright 2002-2003 SecurIT Informatique Inc.  http://securit.iquebec.com		#
#											#
# This program gets its configuration from the file config.txt, and the list of files or#
# directories to be monitored from the file mondir.txt.  These two files have to be in	#
# the same directory as LogAgent.  The config file lets you specify if you want to 	#
# include the IP of the machine, the hostname, the username, date and time in the log 	#
# files, in these cases where the software generating the log doesn't provide these 	#
# credentials.  You can also specify to display entries captured by LogAgent on the 	#
# console or not.  Then, the program starts the monitoring threads for each directory 	#
# entry in mondir.txt, and then enters in an infinite-loop, waiting for signals from the#
# monitoring threads.  When a signal is trigerred (ie: a file as changed in the directo-#
# ry you are monitoring), it gets the appended lines from the log file, and sends it to #
# the specified outputs.  Output dirs can be remote or local, and as many as you want.	#
#											#
#########################################################################################

#########################################################################################
# LICENSE										#
# This software is Open Source.  This means that its source code is open, free and avai-#
# lable for anyone to look into, make modifications, correct bugs (let me know, please) #
# and use for their personal use.  The binary version of this file is also available	#
# and is free to use for anyone.  Business users may want to look into the features 	#
# included in LogAgent 4.0 Pro for better ease-of-use and the ability to run as a 	#
# service (eg. run in the background without a visible console), along with some more 	#
# features.These files can be found by looking at my website http://securit.iquebec.com/#
#########################################################################################

#########################################################################################
# Main Program										#
# This is the main structure of LogAgent.						#
# This procedure takes note of the machine credentials,	LogAgent's configuration and	#
# the list of directories and files to monitor.	 It also gets the linecount of the 	#
# files to monitor.									#
# Then, we start a thread for each entry in mondir.txt and we enter in the main loop.	#
# This loop waits for signals from the threads, and when a signal is received, it 	#
# captures the last lines of the modified log file.  These linee are then sent to the 	#
# various outputs specified in config.txt.						#
# At the end of the loop (CTRL-C) we destroy our threads and memory objects, for clean 	#
# programming purposes.									#
#########################################################################################

# Using Win32::AdvNotify 
# By Amine Moulay Ramdane <aminer@generation.net>
# Website: http://www.generation.net/~aminer/Perl/ 
# This module is now available at CPAN.org
# This Perl module is the core engine of LogAgent.  This module contains all the funtionalities
# For monitoring the changes made to files and folders on the system
# You will also need to install the Win32 API Perl module in order to use AdvNotify

#perl2exe_include Win32::AdvNotify
use Win32::AdvNotify qw(FILE_NAME SIZE INFINITE Yes No 
                        All %ActionName %ActionColor);

# Declaration of needed components for machine identification
use Socket;
use Sys::Hostname;
use File::Basename;
use Win32::EventLog;
my $element;
@buffer=();

# Creation of the AdvNotify object
my $obj  = new Win32::AdvNotify()|| die "Can't create object\n";

# Filelocking of the config files
lockconfig();

# Creation of machine ID table
@id = getid();

# Creation of config table
my @config = getconfig();

# Creation of file table
my @filelist = getfilelist();

# Creation of line count table
my @linecount = getlinecount (@filelist);

# Creation of mondir table
my @mondir = getmondir();

#Event Viewer variables initialization
$eventdir = $ENV{SystemRoot}."/system32/config/";
push @mondir, $eventdir;

$system=Win32::EventLog->new("System", $ENV{ComputerName})
        or die "Can't open System EventLog\n";
$system->GetNumber($sysrecs)
        or die "Can't get number of EventLog records\n";

$security=Win32::EventLog->new("Security", $ENV{ComputerName})
        or die "Can't open Security EventLog\n";
$security->GetNumber($secrecs)
        or die "Can't get number of EventLog records\n";

$application=Win32::EventLog->new("Application", $ENV{ComputerName})
        or die "Can't open Application EventLog\n";
$application->GetNumber($apprecs)
        or die "Can't get number of EventLog records\n";

# Creation of threads table.  Threads are started, and then launched, this is the way the AdvNotify module works
my $index=0;
foreach $element (@mondir) 
	{
	$threads[$index] = $obj->StartThread(Directory    => $mondir[$index],  
                             Filter       =>  All ,
                             WatchSubtree =>  No ) || die "Can't start thread\n";
	$threads[$index]->EnableWatch() || die "Problem starting EnableWatch()\n"; 
	$index++;
	}

# Print header
print "Log Agent 4.0 Free, Open Source version, brought to you by Floydman\n";
print "Copyright 2003 SecurIT Informatique Inc.\n";
print "http://securit.iquebec.com\n";
print "\nPress CTRL-C to quit\n";

logevt ("LogAgent started successfully");
getevent();
# Enters the main monitoring loop
startmonitoringloop();

logevt("LogAgent terminated.");
getevent();
closeagent();
# End of program#

#########################################################################################
# procedure lockconfig()								#
# This procedure locks the config files to prevent tampering while LogAgent runs.	#
#########################################################################################
sub lockconfig
{
open(CONFIGFILE,"<config.txt") || die "Can't open config.txt";
open(MONDIRFILE,"<mondir.txt") || die "Can't open mondir.txt";
}

#########################################################################################
# procedure unlockconfig()								#
# This procedure unlocks the config files.						#
#########################################################################################
# Unlocking of the config files
sub unlockconfig
{
close (CONFIGFILE) || die "Can't close config.txt";
close (MONDIRFILE) || die "Can't close mondir.txt";
}


#########################################################################################
# procedure getid() 									#
# This procedure gets the IP address, the host name and the username of the machine.	#
#########################################################################################

sub getid
{
# Define username, IP address and hostname of the local machine
my $addr = inet_ntoa(scalar(gethostbyname($name)) || 'localhost');
my $host = hostname() || "hostname not defined";
my $login = getlogin || getpwuid($<) || "not logged";
my @id_table = ($addr, $host, $login);
return (@id_table);

}

#########################################################################################
# procedure getconfig() 								#
# This procedure gets the configuration file config.txt.				#
#########################################################################################

sub getconfig
{
my @configtable;
my @dirtable;
$j = 0;
$numarg = 0;

$logip = <CONFIGFILE> || die "Can't read  logip from config.txt";
($logip=~m/LOGIP/i) || die "LOGIP entry missing in config.txt";

$loghost = <CONFIGFILE> || die "Can't read loghost from config.txt";
($loghost=~m/LOGHOST/i) || die "LOGHOST entry missing in config.txt";

$loguser = <CONFIGFILE> || die "Can't read loguser from config.txt";
($loguser=~m/LOGUSER/i) || die "LOGUSER entry missing in config.txt";

$logdate = <CONFIGFILE> || die "Can't read logdate from config.txt";
($logdate=~m/LOGDATE/i) || die "LOGDATE entry missing in config.txt";

$logtime = <CONFIGFILE> || die "Can't read logtime from config.txt";
($logtime=~m/LOGTIME/i) || die "LOGTIME entry missing in config.txt";

$showconsole = <CONFIGFILE> || die "Can't showconsole read from config.txt";
($showconsole=~m/SHOWCONSOLE/i) || die "SHOWCONSOLE entry missing in config.txt";

$lp = <CONFIGFILE> || die "Can't read lineprint from config.txt";
($lp=~m/LINEPRINT/i) || die "LINEPRINT entry missing in config.txt";

$lpbuffer = <CONFIGFILE> || die "Can't read lineprint buffer from config.txt";
($lpbuffer=~m/LINEPRINT BUFFER/i) || die "LINEPRINT BUFFER entry missing in config.txt";

while (defined($dir = <CONFIGFILE>))
	{
	 $dirtable[$j]=$dir;
	 $j++;
	}
($j==0) && die "No destination directory specifed in config.txt.";


@configtable = ($logip, $loghost, $loguser, $logdate, $logtime, $showconsole, $lp, $lpbuffer, @dirtable);
@configtable = parse(@configtable);

(($numarg=@configtable)<9) && die "Not enough parameters in config.txt.  Check file for errors.";

# Tranformation of the first 7 lines of configtable to boolean value
$configtable[0]=$configtable[0]=~m/Y/i;
$configtable[1]=$configtable[1]=~m/Y/i;
$configtable[2]=$configtable[2]=~m/Y/i;
$configtable[3]=$configtable[3]=~m/Y/i;
$configtable[4]=$configtable[4]=~m/Y/i;
$configtable[5]=$configtable[5]=~m/Y/i;
$configtable[6]=$configtable[6]=~m/Y/i;

# Get the lineprint buffer
@element=split(/=/, $configtable[7]);
$configtable[7]=$element[1]; ($configtable[7]>=0) || die "LINEPRINT BUFFER entry must be 0 or a positive number in config.txt";
return (@configtable);
}

#########################################################################################
# procedure getfilelist() 								#
# This procedure gets the list of files to check for in mondir.txt. 			#
#########################################################################################

sub getfilelist
{
my @filetable;
my $k = 0;
@temptable;

while (defined($file = <MONDIRFILE>)) 
	{
	 $temptable[$k]=$file;
	 $k++;
	}
($k==0) && die "No file to monitor in mondir.txt.";

@temptable = parse(@temptable);
$k=0;
foreach $file (@temptable) 
{ if ($file=~m/.*\w+\.\w{0,3}/) {$filetable[$k]=$file; $k++; } }

return (@filetable);
}

#########################################################################################
# procedure getlinecount (@filelist)							#
# This procedure gets the list of directories to watch in mondir.txt.			#
#########################################################################################
sub getlinecount
{
my (@table) = @_;
my @linecount;
my @temp;
$z = 0;

foreach $file (@table)
  {
   open (FILE, $file) || die "Can't open ".$file." for line count.";
   @temp = <FILE>;
   close (FILE);
   @temp = parse(@temp);
   $linecount[$z] = @temp;
   $z++;
  }

return (@linecount);
}

#########################################################################################
# procedure getmondir() 								#
# This procedure gets the list of directories to watch in mondir.txt.			#
#########################################################################################

sub getmondir
{
my @dirtable =(), @temptable;
my $w = 0;

sysseek MONDIRFILE,0,0;
while (defined($dir = <MONDIRFILE>)) 
	{
	 $temptable[$w]=$dir;
	 $w++;
	}
($w==0) && die "No directory to monitor in mondir.txt.";


@temptable = parse(@temptable);

$w=0;

if (lc($temptable[0])ne"null") {
  foreach $entry (@temptable)
   { 
    $entry = dirname($entry)."/";
  TAG:  for ($t=0; $t <= $w; $t++)
          {
           if ($entry eq $dirtable[$t]) { last TAG;}
           if ($t >= $w) { $dirtable[$w] = $entry; $w++; last TAG;}
          }
    }
  return (@dirtable);} else {return ();}
}


#########################################################################################
# procedure parse(table_file) 								#
# This procedure cleans the files from non-valid and blank characters that could be	#
# placed in the config files.  The procedure returns the file as a table.		#
#########################################################################################

sub parse
{ my (@table) = @_;

#check for invalid characters in table_file
chomp @table;

foreach $element (@table)
 {
  $element=~s%^\s+%%;
  @char = split (//, $element);

  foreach $char (@char)
    { $char=~s%\\%/%; }
  $element = join ('',@char);
 }

my @tabletemp;
my $x = 0;

foreach $element (@table)
   {
    if ($element ne '') {
	$tabletemp[$x]=$element;
	$x++;}
   }
@table = @tabletemp;
return (@table);
}


#########################################################################################
# procedure startmonitoringloop()							#
# This procedure is the main monitoring loop.  When a change is detected in a file	#
# located in a monitored directory (preferably ASCII files), the procedure calls 	#
# getlastline() with the name of the modified file.  The captured line is then sent	#
# via the procedure sendoutput(), along with LogAgent's configuration table.		#
#########################################################################################

sub startmonitoringloop
{
while($threads[0]->Wait(INFINITE))# exit with [Ctrl-C] signal 
  {
   while($threads[0]->Read(\@data))# exit when the list is empty
    {
     for($i=0;$i<=$#data;$i++)
      { 
	if ($data[$i]->{Directory} eq $eventdir) {getevent();}
	else {getlastline($data[$i]);}
      }
    }
  }
}

#########################################################################################
# procedure getevent(filename)								#
# This procedure determines which event log is reporting and gets the latests entries	#
#########################################################################################

sub getevent
{
$oldsysrecs = $sysrecs; 
$oldsecrecs = $secrecs;
$oldapprecs = $apprecs;

$system->GetNumber($sysrecs) or die "Can't get number of EventLog records\n";
$security->GetNumber($secrecs) or die "Can't get number of EventLog records\n";
$application->GetNumber($apprecs) or die "Can't get number of EventLog records\n";

if ($oldsysrecs > $sysrecs) {   $system->Close();
				$system=Win32::EventLog->new("System", $ENV{ComputerName})
        or die "Can't open System EventLog\n";
				$system->GetOldest($oldsysrecs);}

if ($oldsecrecs > $secrecs) {	$security->Close();
				$security=Win32::EventLog->new("Security", $ENV{ComputerName}) or die "Can't open Security EventLog\n";
				$security->GetOldest($oldsecrecs);				}

if ($oldapprecs > $apprecs) {   $application->Close();
				$application=Win32::EventLog->new("Application", $ENV{ComputerName}) or die "Can't open Application EventLog\n";
				$application->GetOldest($oldapprecs);}

while ($oldsysrecs < $sysrecs) { $system->Read(EVENTLOG_FORWARDS_READ|EVENTLOG_SEEK_READ,
				      $oldsysrecs+1,
				      $hashRef)
				      or die "Can't read EventLog entry #$oldsysrecs\n";
			Win32::EventLog::GetMessageText($hashRef);
			@message = split('\r',$hashRef->{Message}); chomp (@message);
			$message = join ('', @message);
			$message = "Time Generated : ".localtime($hashRef->{TimeGenerated})."\nTime Written : ".localtime($hashRef->{Timewritten})."\nComputer: ".$hashRef->{Computer}."\nEvent Type: ".$hashRef->{EventType}."    Event ID: ".$hashRef->{EventID}."   Category: ".$hashRef->{Category}."\nSource: ".$hashRef->{Source}."\n".$message."\#\#";
			sendoutput("SysEvent.log", "System Event Entry $oldsysrecs:\n$message\n",@config);
			$oldsysrecs++;
			}

while ($oldsecrecs < $secrecs) { $security->Read(EVENTLOG_FORWARDS_READ|EVENTLOG_SEEK_READ,
					$oldsecrecs+1,
					$hashRef)
					or die "Can't read EventLog entry #$oldsecrecs\n";

			Win32::EventLog::GetMessageText($hashRef);
			@message = split('\r',$hashRef->{Message}); chomp (@message);
			$message = join ('', @message);
			$message = "Time Generated : ".localtime($hashRef->{TimeGenerated})."\nTime Written : ".localtime($hashRef->{Timewritten})."\nComputer: ".$hashRef->{Computer}."\nEvent Type: ".$hashRef->{EventType}."    Event ID: ".$hashRef->{EventID}."   Category: ".$hashRef->{Category}."\nSource: ".$hashRef->{Source}."\n".$message."\#\#";
			sendoutput("SecEvent.log", "Security Event Entry $oldsecrecs:\n$message\n",@config);
			$oldsecrecs++;
			}

while ($oldapprecs < $apprecs) { $application->Read(EVENTLOG_FORWARDS_READ|EVENTLOG_SEEK_READ,
					   $oldapprecs+1,
					   $hashRef)
					   or die "Can't read EventLog entry #$oldapprecs\n";
			Win32::EventLog::GetMessageText($hashRef);
			@message = split('\r',$hashRef->{Message}); chomp (@message);
			$message = join ('', @message);
			$message = "Time Generated : ".localtime($hashRef->{TimeGenerated})."\nTime Written : ".localtime($hashRef->{Timewritten})."\nComputer: ".$hashRef->{Computer}."\nEvent Type: ".$hashRef->{EventType}."    Event ID: ".$hashRef->{EventID}."   Category: ".$hashRef->{Category}."\nSource: ".$hashRef->{Source}."\n".$message."\#\#";

#print "$hashref\n";
#print "$hashref->{Data}\n$hashref->{Strings}\n";
#print "\nWaiting...";$a=<STDIN>;

			sendoutput("AppEvent.log", "Application Event Entry $oldapprecs:\n$message\n",@config);
			$oldapprecs++;
			}

}

#########################################################################################
# procedure getlastline(filename)							#
# This procedure gets the last line (non-blank) of the file received as the argument.	#
# It returns the filename (whitout path) and the last line of the file.			#
#########################################################################################

sub getlastline
{
my ($data) = @_;
my $y = 0;
if (-T $data->{Directory}.$data->{FileName})
   {
	open (LOGFILE, $data->{Directory}.$data->{FileName}) or die "Can't open log file ".$data->{Directory}.$data->{FileName};
	flock (LOGFILE, 1) or die "Can't lock file";
	@lines = <LOGFILE>;
	close (LOGFILE) or die "Can't close file"; # To unlock the file as fast as possible for new entries

	@lines = parse(@lines);
	$filename = lc($data->{Directory}.$data->{FileName});

	COUNTER: foreach $file (@filelist) 
	         {  $file = lc($file);
	            if ($file eq $filename) {last COUNTER;} else {$y++;}
	         }
	if ($filelist[$y] eq '') {$filelist[$y] = $filename;}
	if ($linecount[$y] > @lines) { $linecount[$y]=0; }

	for ($toto=$linecount[$y]; $toto < @lines; $toto++ )
	 { 
	  $lastline = $lines[$linecount[$y]];
	  $linecount[$y]++;
	  sendoutput($data->{FileName}, $lastline,@config);
	 }
   }
}

#########################################################################################
# procedure sendoutput(filename, line, config)						#
# This procedure receives as arguments: the name of the modified file, the last line	#
# of the logfile, and then the config table (LOGIP, LOGHOST, LOGUSER, SHOWCONSOLE, and 	#
# the various destination directories).  The procedure checks the configuration to see	#
# if it has to append any information to the original line or not.  If SHOWCONSOLE in 	#
# enabled, then the line is printed on the screen, if not it simply passes to the next	#
# step which is to forward this line to all mentionned destinations in config.txt.	#
#########################################################################################

sub sendoutput
{ my ($filename, $lines, $logip, $loghost, $loguser, $logdate, $logtime, $showconsole, $lp, $lpbuffer, @dest) = @_;

my $line = '';
my @newlines = split (/\n/,$lines);
foreach $line (@newlines)
 {
 my $newline=""; 

 if ($logip) {$newline=$newline.$id[0].",";}
 if ($loghost) {$newline=$newline.$id[1].",";}
 if ($loguser) {$newline=$newline.$id[2].",";}
 if ($logdate || $logtime) {($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);} 
 if ($logdate) {$newline=$newline.($year+=1900)."/".($mon+1)."/".$mday.",";}
 if ($logtime) {$newline=$newline.$hour.":".$min.":".$sec.",";}

 $newline=$newline.$line."\n";

 if ($showconsole) {print $newline;}

 if (lc($dest[0])ne"null") 
   {
   foreach $destdir (@dest)
      {
       $destination=$destdir.$filename;
       open (DEST, ">>".$destination) || die "Can't open master log file $destination";
       flock (DEST, 2) || die "Can't lock file for writing";
       print DEST $newline || die "Can't write to file";
       close (DEST) || die "Can't close master log file";
      }
   }


 if ($lp) { push @buffer, $newline; $buffer=@buffer;
	    if ($buffer>=$lpbuffer+1) { 
				       open(LINEPRINT,">LPT1") || die "Can't open pipe to LPT1";
				       foreach $line (@buffer){print LINEPRINT $line;}
				       close (LINEPRINT) || die "Can't close pipe to LPT1";
				       @buffer=();
				       } 
           }
 }
}

#########################################################################################
# procedure logevt (evt)								#
# This procedure receives a LogAgent event message that needs to be sent to the Event	#
# Viewer.										#
#########################################################################################

sub logevt
{ my ($evt) = @_;

my %h =(Computer=>	$ENV{ComputerName},
	Source	=>	$evt,
	EventType=>	EVENTLOG_INFORMATION_TYPE,
	Category=>	'',
	EventID	=>	'',
	Data	=>	$evt,
	Strings	=>	$evt,);

$application->Report(\%h);

}

#########################################################################################
# procedure closeagent									#
# This procedure closes any handles and destroy any objects that need to be		#
# closed before shuting down LogAgent.							#
#########################################################################################

sub closeagent
{ 

# Empty the lines buffer if LINEPRINT is on
if ($lp && @buffer) { 
		     open(LINEPRINT,">LPT1") || die "Can't open pipe to LPT1";
			     foreach $line (@buffer){
				print LINEPRINT $line;}
			     close (LINEPRINT) || die "Can't close pipe to LPT1";
			     @buffer=();
		    }

# Closing of Event Viewer handles
$application->Close();
$system->Close();
$security->Close();

# Unlocking of the config files
unlockconfig();

# termination of the threads. 
for ($a; $a<$index; $a++)
   { $threads[$a]->Terminate(); }

# destruction of the object
undef $obj;
exit;
}
#EOF
