#!/usr/bin/perl -w # # syschk.pl - given a list of directories, compare the hack of the files # 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 $DEBUG); 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(< use another checksum file instead of $DEFDB -c 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 create 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*$/ )) } ; 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{$fname}); 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__