#!/usr/local/bin/perl # # # filelogger # # Program for monitoring files # # Copyright (c) 2001 by Ardan Patwardhan. All rights reserved. # # DISCLAIMER # This software is provided on an ``as is'' and any express or implied # warranties, including, but not limited to, the implied warranties of # merchantability and fitness for a particular purpose are disclaimed. # In no event shall the copyright owner be liable for any direct, # indirect, incidental, special, exemplary, or consequential damages # (including, but not limited to, procurement of substitute goods or # services; loss of use, data, or profits; or business interruption) # however caused and on any theory of liability, whether in contract, # strict liability, or tort (including negligence or otherwise) arising # in any way out of the use of this software, even if advised of the # possibility of such damage. # # $Header: /usr/users/ardan/cvsroot/perl/filelogger,v 1.4 2002/01/04 08:49:58 ardan Exp $ # # Program version my $VERSION = '$Revision: 1.4 $'; ######################################################################## # # # COMMAND LINE PROCESSING # # # ######################################################################## use Getopt::Std; getopt('c',\%opts); # Configuration file die "No config file specified, stopped" unless ($configMod = $opts{c}); die "Unable to read config file, stopped" unless (do $configMod); # The following lines up to the main block should be cut and paste into # a separate resource file. The file is specified for inclusion using the # -c option. The lines with "##" need to be uncommented and modified. ######################################################################## # # # CONFIGURATION # # # ######################################################################## # Used to identify configuration in log file ##$reportId = "Ardan Patwardhan"; # Log file to contain state of monitored directories # Meant only to be used internally by the program ##$lastLog = "lastLog.log"; # Base log file - All info about changes to monitored files will be # appended to this file. If it does not exist, it will be created. ##$baseLog = "baseLog.log"; # Directories to monitor ##@dirList = ("/f03/image/tbsv", ## "/f07/image/tbsv", ## "/f08/ardan", ## "/f10/ardan", ## "/f11/Image/ardan", ## "/f11/Image/marc", ## "/f13/ardan", ## "/f14/ardan", ## "/f15/ardan", ## "/f15/mount", ## "/f15/patches", ## "/wolf1/ardan", ## "/wolf9/ardan", ## "/disk1/ardan", ## "/disk1/Image/ardan", ## "/usr/users/ardan"); # List of patterns specifying files and directories that should be # excluded. Use Perl regular expression syntax ##@excludeList = ('~$', # Emacs old files ## '.saves-', # Emacs .saves files ## '#\w+(\.\w+)*#$', # Emacs backup files ## '/core$', # Core dumps ## 'junk', # Junk files and directories ## ); ######################################################################## # # # MAIN BODY # # # ######################################################################## # USE statements use File::Find; # Variables and Data structures %hashOfFiles = (); # keys = all files and directories to be monitored; values = (mode, user, group, size, mtime) %hashOfLastFiles = (); # keys = all files and direcories in lastLog file; values = (mode, user, group, size, mtime) @failDir = (); # Directories that could not be accessed @modeChngFiles = (); # List of files/dir for which the mode has changed @userChngFiles = (); # List of files/dir for which the user has changed @groupChngFiles = (); # List of files/dir for which the group has changed @modifiedFiles = (); # List of files that have been modified @deletedFiles = (); # List of files/dir that have been deleted @createdFiles = (); # List of files/dir that have been created $lastLogExists = 0; # Assume false to start with $lastLogRead = 0; # Assume false to start with $sourceSize = 0; # Total size of all the files monitored # Create list of all files in monitored directories foreach my $dir (@dirList) { if(! -d $dir) { push @failDir, ($dir); next; } find { wanted => sub { my @fileStatus = (); if(-l $File::Find::name) { @fileStatus = lstat($File::Find::name); } else { @fileStatus = stat($File::Find::name); } my $mode = $fileStatus[2]; my $user = getpwuid($fileStatus[4]); my $group = getgrgid($fileStatus[5]); my $size = $fileStatus[7]; my $mtime = $fileStatus[9]; my @fileInfo = ($mode, $user, $group, $size, $mtime); $sourceSize += $fileInfo[3]; $hashOfFiles{$File::Find::name} = [ @fileInfo ]; }, follow => 0}, "$dir"; } # Exclude files and directories that match exclude patterns foreach my $pat (@excludeList) { foreach my $file (keys %hashOfFiles) { if($file =~ m/$pat/) { $sourceSize -= @{$hashOfFiles{$file}}[3]; delete $hashOfFiles{$file}; } } } # Read data from lastLog file if (-f $lastLog && -r $lastLog) { $lastLogExists = 1; open(LASTLOG, "< $lastLog") || die "can't open $lastLog: $!"; while (my $line = ) { chomp($line); my $mode = oct(cleanWhitespace(substr($line,0,7))); my $mtime = cleanWhitespace(substr($line,8,15)); my $size = cleanWhitespace(substr($line,24,19)); my $user = cleanWhitespace(substr($line,44,10)); my $group = cleanWhitespace(substr($line,55,10)); my $file = cleanWhitespace(substr($line,66)); $hashOfLastFiles{$file} = [ $mode, $user, $group, $size, $mtime ]; } $lastLogRead = 1; close LASTLOG; } # Check which files have been deleted or modified if($lastLogRead) { for my $file (sort keys %hashOfLastFiles) { if ($hashOfFiles{$file}=="") { push @deletedFiles, ($file); } else { my ($mode1, $user1, $group1, $size1, $mtime1) = @{$hashOfLastFiles{$file}}; my ($mode2, $user2, $group2, $size2, $mtime2) = @{$hashOfFiles{$file}}; if ($mode1 != $mode2) { push @modeChngFiles, ($file); } if ($user1 ne $user2) { push @userChngFiles, ($file); } if ($group1 ne $group2) { push @groupChngFiles, ($file); } if ($mtime1 != $mtime2) { # This case is not interesting if this is a directory if (! -d $file) { push @modifiedFiles, ($file); } } } } } # Check which files have been created if($lastLogRead) { foreach my $file (sort keys %hashOfFiles) { if ($hashOfLastFiles{$file}=="") { push @createdFiles, ($file); } } } # If nothing has happened since last time just exit if($lastLogRead && !(@createdFiles||@modeChngFiles||@userChngFiles||@groupChngFiles||@modifiedFiles||@deletedFiles||@failDir)) { exit; } # Append activity to base log file open(BASELOG, ">> $baseLog") || die "can't open $baseLog: $!"; printf(BASELOG "\n"); printf(BASELOG "FILELOGGER REPORT BEGIN\n"); @timeList = niceTime(time); printf(BASELOG "Prepared for %s on %d\/%d\/%d at %d:%02d:%02d \n", $reportId,$timeList[5],$timeList[4],$timeList[3],$timeList[2],$timeList[1],$timeList[0]); printf(BASELOG "Source size(bytes) : %d\n\n", $sourceSize); if(!$lastLogRead) { # First time $txt = "Dump of all monitored files"; printFileList([keys %hashOfFiles],\%hashOfFiles,\*BASELOG,\$txt); } else { $txt = "New files"; printFileList(\@createdFiles,\%hashOfFiles,\*BASELOG,\$txt); $txt = "Modified files"; printFileList(\@modifiedFiles,\%hashOfFiles,\*BASELOG,\$txt); $txt = "Deleted files"; printFileList(\@deletedFiles,\%hashOfLastFiles,\*BASELOG,\$txt ); # Output list of files that have had mode changes if(@modeChngFiles) { printf(BASELOG "File-mode change: \n\n"); foreach my $file (sort @modeChngFiles) { my $mode1 = @{$hashOfLastFiles{$file}}[0]; my $mode2 = @{$hashOfFiles{$file}}[0]; my $modeText1 = niceMode($mode1); my $modeText2 = niceMode($mode2); printf(BASELOG "%s -> %s : %s\n",$modeText1,$modeText2,$file); } printf(BASELOG "\n\n\n"); } # Output list of files for which ownership has changed if(@userChngFiles) { printf(BASELOG "File-owner change: \n\n"); foreach my $file (sort @userChngFiles) { my $user1 = @{$hashOfLastFiles{$file}}[1]; my $user2 = @{$hashOfFiles{$file}}[1]; printf(BASELOG "(%s) -> (%s) : %s\n",$user1,$user2,$file); } printf(BASELOG "\n\n\n"); } # Output list of files for which group has changed if(@groupChngFiles) { printf(BASELOG "File-group change: \n\n"); foreach my $file (sort @groupChngFiles) { my $group1 = @{$hashOfLastFiles{$file}}[2]; my $group2 = @{$hashOfFiles{$file}}[2]; printf(BASELOG "(%s) -> (%s) : %s\n",$group1,$group2,$file); } printf(BASELOG "\n\n\n"); } } # Output list of directories not accessed if(@failDir) { printf(BASELOG "Unable to access following directories: \n\n"); foreach my $dir (sort @failDir) { printf(BASELOG "%s\n",$dir); } printf(BASELOG "\n\n\n"); } printf(BASELOG "FILELOGGER REPORT END\n\n\n"); close BASELOG; # Output monitored files to lastLog file open(LASTLOG, "> $lastLog") || die "can't open $lastLog: $!"; foreach my $file (sort keys %hashOfFiles) { my @fileInfo = @{$hashOfFiles{$file}}; my ($mode, $user, $group, $size, $mtime) = @fileInfo; printf(LASTLOG "%07o %15d %19d %10s %10s %s\n",$mode,$mtime,$size,$user,$group,$file); } close LASTLOG; ######################################################################## # # # SUBROUTINES # # # ######################################################################## # Subroutine for deleting leading and trailing whitespace sub cleanWhitespace { my ($inStr) = @_; for ($outStr = $inStr) { s/^\s+//; s/\s+$//; } return $outStr; } # Get current time into nice format sub niceTime { my ($time) = @_; my @timeList = localtime($time); $timeList[4] += 1; # Month 1->12 $timeList[5] += 1900; # Year return @timeList; } # Get mode into nice format sub niceMode { use Fcntl ':mode'; my ($mode) = @_; my @modeList = (); $modeList[0] = ($mode & S_IXUSR)?'x':'-'; $modeList[1] = ($mode & S_IWUSR)?'w':'-'; $modeList[2] = ($mode & S_IRUSR)?'r':'-'; $modeList[3] = ($mode & S_IXGRP)?'x':'-'; $modeList[4] = ($mode & S_IWGRP)?'w':'-'; $modeList[5] = ($mode & S_IRGRP)?'r':'-'; $modeList[6] = ($mode & S_IXOTH)?'x':'-'; $modeList[7] = ($mode & S_IWOTH)?'w':'-'; $modeList[8] = ($mode & S_IROTH)?'r':'-'; if (S_ISDIR($mode)) { $modeList[9] = 'd'; } elsif (S_ISLNK($mode)) { $modeList[9] = 'l'; } elsif (S_ISBLK($mode)) { $modeList[9] = 'b'; } elsif (S_ISCHR($mode)) { $modeList[9] = 'c'; } elsif (S_ISFIFO($mode)) { $modeList[9] = 'p'; } elsif (S_ISSOCK($mode)) { $modeList[9] = 's'; } elsif (S_ISREG($mode)) { $modeList[9] = '-'; } else { $modeList[9] = '?'; } my $modeText = sprintf("%s%s%s%s%s%s%s%s%s%s",$modeList[9],$modeList[2],$modeList[1],$modeList[0],$modeList[5],$modeList[4],$modeList[3],$modeList[8],$modeList[7],$modeList[6]); return $modeText; } # Output list of files sub printFileList { my ($fileList, $hashOfFiles,$FD, $txt) = @_; if(@$fileList) { my $nTotal = 0; my $totSize = 0; foreach my $file (@$fileList) { my @fileInfo = @{$$hashOfFiles{$file}}; my ($mode, $user, $group, $size, $mtime) = @fileInfo; $nTotal++; $totSize += $size; } printf($FD "%s:\n\n", $$txt); printf($FD "Number : %d\n", $nTotal); printf($FD "Size (bytes): %d\n\n",$totSize); printf($FD " Mode Date Time Size Owner Group File-name\n"); printf($FD "==========================================================================================\n"); # drwxr-xr-x 20011210 16:17:05 512 ardan system /f14/ardan foreach $file (sort @$fileList) { my @fileInfo = @{$$hashOfFiles{$file}}; my ($mode, $user, $group, $size, $mtime) = @fileInfo; my @timeList = niceTime($mtime); my $modeText = niceMode($mode); printf($FD "%s %d%02d%02d %02d:%02d:%02d %19d %10s %10s %s\n",$modeText,$timeList[5],$timeList[4],$timeList[3],$timeList[2],$timeList[1],$timeList[0],$size,$user,$group,$file); } printf($FD "\n\n\n"); } } ######################################################################## # # # POD # # # ######################################################################## =head1 NAME filelogger =head1 SYNOPSIS filelogger -c config.rc >& error.log =head1 DESCRIPTION This program analyses changes in files and directories in monitored directory trees (@dirList) between consecutive runs of the program and appends a report to a file specified by $baseLog. It does so by storing the state of the files and directories in @dirList in a file specified by $lastLog, each time the program runs. Whereas the file specified by $baseLog is meant to be examined by the user, the file specified by $lastLog is for internal consumption only. The program reports which files and directories have been created, deleted or modified. Changes to the following attributes of files are monitored: mode, user, group, size, and mtime. =head2 Configuration Configuration information is provided in a separate file that must be specified using the -c command-line option, e.g. filelogger -c filelogger.rc The following is a sample configuration file: ######################################################################## # # # CONFIGURATION # # # ######################################################################## # Used to identify configuration in log file $reportId = "Ardan Patwardhan"; # Log file to contain state of monitored directories # Meant only to be used internally by the program $lastLog = "lastLog.log"; # Base log file - All info about changes to monitored files will be # appended to this file. If it does not exist, it will be created. $baseLog = "baseLog.log"; # Directories to monitor @dirList = ("/f03/image/tbsv", "/f07/image/tbsv", "/f08/ardan", "/f10/ardan", "/f11/Image/ardan", "/f11/Image/marc", "/f13/ardan", "/f14/ardan", "/f15/ardan", "/f15/mount", "/f15/patches", "/wolf1/ardan", "/wolf9/ardan", "/disk1/ardan", "/disk1/Image/ardan", "/usr/users/ardan"); # List of patterns specifying files and directories that should be # excluded. Use Perl regular expression syntax @excludeList = ('~$', # Emacs old files '.saves-', # Emacs .saves files '#\w+(\.\w+)*#$', # Emacs backup files '/core$', # Core dumps 'junk', # Junk files and directories ); =head2 crontab The best way to run this script is to have it run periodically by adding a crontab entry. The following is an example crontab entry that runs every night at 1:10: 10 1 * * * /usr/users/ardan/perl/filelogger -c/usr/users/ardan/filelogger.rc >& /usr/users/ardan/error.log =head2 Sample output The following is a sample from a base log file: FILELOGGER REPORT BEGIN Prepared for Ardan Patwardhan on 2002/1/3 at 15:09:53 Source size(bytes) : 23907240 Modified files: Number : 2 Size (bytes): 35215 Mode Date Time Size Owner Group File-name ========================================================================================== -rw-r--r-- 20020103 15:09:45 21017 ardan users /usr/users/ardan/history.log -rwxr-xr-x 20020103 15:09:25 14198 ardan users /usr/users/ardan/perl/filelogger Deleted files: Number : 1 Size (bytes): 512 Mode Date Time Size Owner Group File-name ========================================================================================== drwxr-xr-x 20020103 12:25:11 512 ardan users /usr/users/ardan/tmp1 FILELOGGER REPORT END =head1 BUGS =head1 SEE ALSO =head1 COPYRIGHT Copyright 2001-2002 by Ardan Patwardhan. All Rights Reserved. =head1 DISCLAIMER This software is provided on an 'as is' basis and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. =head1 README Script for monitoring specified directory trees for changes in files and directories. =head1 PREREQUISITES File::Find =head1 OSNAMES dec_osf (tested) Any Unix or Linux OS (not tested) =head1 SCRIPT CATEGORIES Unix/System_administration =cut