I borrowed your code and have been using it to keep track of my BSD system. Unfortunately, it fell down behind the book-rack where the dog got it and ripped it into little tiny shreds. I got it put back together again with plenty of tape and glue but it's not the same program it once was. All the data access got rolled up into their own loops, well.. most of them, and some time when I wasn't looking the CHECKED field totally wandered off but I did find a hash key to take it's place. I did manage to push most of the flow control out of the loops and push the data access inside of the subroutines so it should be more suited to converting to OO; something I would like to tackle when the round-toit I requisitioned is finally delivered.
#!/usr/bin/perl -w
#
# syschk.pl - given a list of directories, compare the hack of the fil
+es
# by cianoz
#
# 24 Jan 2008 bet - Added program name to die strings.
# 25 Jan 2008 bet - Added creation of db directory if not exists.
# Print mode in hex.
# 11 Feb 2008 bet - Loopified stat checking.
# 19 Feb 2008 bet - Added sanity checks for -f arg
# 06 Apr 2008 bet - resequence
# 11 Apr 2008 bet - CHECKED field no longer needed
#
# If we parallel the files we have the benefit
# of hashing them sequentially while the
# routine is still in cache, also the
# fetches or writes
# to the DB will be consecutive,,
# note: no difference noted.
#
# In an object world the db objects
# ( file refs ; would check themselves,,
#
use warnings;
use strict;
use vars qw(%opt %DB $VERSION $NAME $DEFRC $DEFDB $DEFNM $DEFLIB $DEBU
+G);
use subs qw( initialize check );
use diagnostics;
$DEBUG = 0;
$VERSION = '0.9.2';
$0 =~ m|/|g;
$NAME = $';
$DEFNM = 'sum.db';
$DEFLIB = "/var/db/syschk";
$DEFDB = "$DEFLIB/$DEFNM";
$DEFRC = '/etc/syschk.rc';
use File::Find 'find';
use Digest::MD5;
use Getopt::Std;
use DB_File;
use English '-no_match_vars';
getopts('hvidtf:c:', \%opt);
if( $opt{h} ) {
print(<<EOF);
$NAME $VERSION
USAGE: $NAME [-hvi] [-f checksum_file] [-c config_file]
DESCRIPTION:
This program checks files and directories in your system,
reporting if they were modified (by checking an md5,sha1 sum)
created or deleted since last time you initialized it.
I use it to check my /etc /sbin /lib /bin /usr/bin and /usr/sbin
You can run it from a cron job or manually
(it is safe to store a copy of the checksum database outside the syste
+m).
It takes about 2 minutes to scan all relevant files on my (chinoz) sys
+tems.
Although it works for me it is all but perfect:
I would appreciate some hints to make it better.
Options:
-h this (poor) help
-i create (initialize) a new checksum database
-f <checksum_file> use another checksum file instead of
$DEFDB
-c <config_file> use another config file instead of
$DEFRC ( - for STDIN)
-v verbose output (not yet implemented :)
EOF
exit 0;
} # end -h option
my $rcfile = $opt{c} || $DEFRC;
my $dbfile = $opt{f} || $DEFDB;
$DEBUG= ($opt{d})?1:0;
if( $dbfile =~ m<^([\-.\w/]+)$> ) {
$dbfile = $1; # Sanitized
} else {
die "$NAME reports invalid file path. $!\n";
}
( $EUID==0 ) or die "$NAME must run as root.\n";
if( $opt{i} ) {
unless (-e $DEFLIB) {
print "Creating $DEFLIB\n";
mkdir $DEFLIB, 0644 or die "No syscheck subdir and cannot crea
+te one.";
}
if (-e $dbfile) {
unlink( $dbfile ) or die "$NAME cannot unlink $dbfile: $!\n";
} else {
print "$NAME reports $dbfile missing.\n","Creating $dbfile\n";
}
}
# point to the particular find() handler we're going to use.
my $process = ( $opt{i} ) ?\&initialize :\✓
# end options processing
tie %DB, 'DB_File', $dbfile or die "$NAME cannot tie $dbfile\n";
print header();
# read in the rcfile
# Three argument open is more secure here.
open( RC, "<", $rcfile ) or die "$NAME cannot open $rcfile: $!\n";
my @directories = grep { chomp; ( not ( /^\s*[#\$]/ or /^\s*$/ )) } <R
+C>;
close RC;
my %filenames;
# Convert a list of directories into files and store in a HoH
find \&get_files, @directories;
for ( keys %filenames ) {
if ( $filenames{$_}->{openp} ) {
$filenames{$_}->{hash} = hashit($_);
$filenames{$_}->{stat} = statit($_);
&$process ( $_ );
close $filenames{$_}->{handle};
} else {
print "- unopened file: $_\n";
}
}
# Post processing
deletions() unless $opt{i};
totals() if $opt{t};
print footer();
untie %DB;
exit 0;
#
# End of MAIN
#
################################################################
################################################################
#
# Subroutines
#
sub get_files { # callback from find()
# modifies global %filenames
if( -f $_ ) { # Only process plain files.
my $fname = $File::Find::name;
print "Open: $fname\n" if $DEBUG;
# Three argument open is more secure here.
my $openp = open( my $file, '<', $_ );
$file = $file || 0;
$filenames{$fname} = +{ handle=>$file,
openp=>$openp };
}
}
# hashit ( filenmame )
#
sub hashit {
my $md5 = new Digest::MD5;
$md5->addfile( $filenames{$_[0]}->{handle} );
return $md5->hexdigest;
}
# statit ( filename )
sub statit {
my( undef, undef, $mode, undef,
$uid, $gid, undef, undef,
undef, $mtime, undef, undef, undef,
) = stat ( $filenames{$_[0]}->{handle} );
return { mode => $mode,
uid => $uid,
gid => $gid,
time => $mtime,
};
}
# initialize ( filename ) - aliased to process() if opt{i}
# Write HoH to database
sub initialize {
my $fname = $_[0];
# record it
# "$digest:$mode:$uid:$gid:$mtime";
$DB{$fname} =join ':', ( $filenames{$fname}->{hash},
$filenames{$fname}{stat}{mode},
$filenames{$fname}{stat}{uid},
$filenames{$fname}{stat}{gid},
$filenames{$fname}{stat}{time}, );
}
# check ( filename ) - aliased to process() if not opt{i}
# Get the DB record an compare to the HoH
sub check{
my $fname = $_[0];
print "Check $fname\n" if ($DEBUG);
if( exists $DB{$fname} ) {
my( $digest, $mode, $uid, $gid, $mtime) = split( ':', $DB{$fna
+me});
my @these = ( # List of data to be compared according to the
+format:
# was--------is-------------------------------type
[ $digest, "$filenames{$fname}->{hash}", 'digest' ],
[ $uid, $filenames{$fname}{stat}{uid}, 'uid' ],
[ $gid, $filenames{$fname}{stat}{gid}, 'gid' ],
[ sprintf( "%o", $mode ),
sprintf( "%o", $filenames{$fname}{stat}{mode} ), 'mode
+' ],
[ scalar localtime $mtime,
scalar localtime $filenames{$fname}{stat}{time}, 'time
+' ],
);
compare( \@these, $fname );
# $DB{$fname} .= ':CHECKED';
} else { # !frecord
print "- new file: $fname\n";
}
}
sub compare() { # \@, $fname
my( $tests, $fname ) = @_;
foreach( @$tests ) {
my( $was, $is, $type ) = @$_;
if( $was ne $is ) {
print "File $fname - $type changed:\n",
"\t old: $was\n\t new: $is\n";
}
}
}
sub deletions { # Prints a list of file found but not in database
print "\nDeleted files:\n";
print "\t", join "\n\t", grep { not exists $filenames{$_} } keys
+%DB;
print "\n";
}
sub totals {
print "Files checked: ";
print scalar grep $filenames{$_}{openp}, keys %filenames ;
print "\n";
}
sub header {
return join '', (
"$NAME V. $VERSION\n",
"Begin time: ", scalar localtime time, "\n",
"\n---- BEGIN REPORT ---\n",
$opt{i}?"Initialize database: $dbfile\n":'',
);
}
sub footer {
return ( "\n---- END REPORT ---\n",
"End time: ", scalar localtime time, "\n" );
}
__END__