#!/local/bin/perl # # Program to scan filesystems and search for suspicious looking # directories, and only notify when changes occur. # # This program is not guaranteed to find everthing that may be # considered suspicious. # # Requires the DataTrack module (http://www.cise.ufl.edu/~jfh/jst) # # Usage: # # fsaudit [ -a addr] [ -l ] [dir1, dir2, ..., dirN ] # # -a addr # # mail address to receive report # -l # # list the current database # # Edit the variables $SUSPICIOUS_FILENAMES, $ALLOWED_CHARS, and %EXCEPTIONS # as desired # # Try to prevent suprises from the environment $ENV{PATH} = "/local/bin:/bin"; $ENV{IFS} = " \t\n"; use vars qw ( $MAILER $DBDIR $FINDCMD @FINDOPTS $DEFAULT_DB_TYPE $opt_t $opt_p $opt_a $opt_l $opt_n $ADMIN_ADDR $LIST_DB $NO_UPDATE $FILEAUDIT_DB $TMPHASH $SUSPICOUS_FILENAMES @TARGETS $ALLOWED_CHARS $SUSPICIOUS_FILENAMES %tmphash %EXCEPTIONS ); use strict; use Getopt::Std; use DataTrack::Hash; use DB_File; use File::Find; $DEFAULT_DB_TYPE = "DB_File"; # # Defaults # $MAILER = "/usr/lib/sendmail"; $DBDIR = "/var/fsaudit"; $FILEAUDIT_DB = "$DBDIR/fa.db"; $TMPHASH = "$DBDIR/x_$$"; # # $SUSPICIOUS_FILENAMES : dirnames recoreded in the database surrounded # by "*** $dir ***"; # # $ALLOWED_CHARS : characters allowed in dir names # # %EXCEPTIONS : hash db of files not to put in the db # $SUSPICIOUS_FILENAMES = "(satan|rootkit|eggdrop|mscan|c50|crack|saint|warez)"; $ALLOWED_CHARS = '^[\/\w\.\-\+\_\*\&\~\,#: \?\'\@\%\=\!]+$'; %EXCEPTIONS = ( '/collections-local/sbeck-local/Make.tmp' => 1 ); sub wanted { my $pathname = $File::Find::dir . "/" . $_; my ($dev, $ino, $mode, $nlink, $uid, $gid); # # With some help from find2perl . -type d -xdev # (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && -d _ && !($File::Find::prune |= ($dev != $File::Find::topdev)) && !($EXCEPTIONS{$pathname}) && do { # # Guidelines for dir names: # - no dir should have "\.[\.]+" # - no dir should have a space at the beginning or end of a # path component # - no dir should have two or more spaces next to each other # in a given path component # - no dir should have characters other than those specified # in the allowed chars line # - ~/.tmp has been used to hide stuff # if (/$SUSPICIOUS_FILENAMES/io) { $tmphash{"***" . $pathname . "***"} = "1"; } elsif ( /\.\./ || ( ! /$ALLOWED_CHARS/o ) # allowed chars || /\.tmp\b/ || m,/\s, || m,\s/, || m,\s\s, || m,\s$, ) { $tmphash{$pathname} = "1"; } }; } Main(); sub Main { my (@outlines, @dblist, @new, @removed, $key, $val); my ($fh, $hostname, $mailfh); $fh = safeopen("r", "/bin/hostname"); chomp($hostname = <$fh>); close($fh); # # Get options from command line and set respective # variables # getopts("t:p:a:ln"); ($opt_a) && ($ADMIN_ADDR = $opt_a); ($opt_l) && ($LIST_DB = 1); ($opt_n) && ($NO_UPDATE = 1); print "opt a is $opt_a\n"; # # Get all top level directories to scan # @TARGETS = @ARGV; # # Create the dbdir if it doesn't exist # mkdir ($DBDIR, 0700) unless (-f $DBDIR); # # Create the DataTrack Object # my $dt = new DataTrack::Hash($FILEAUDIT_DB, "$DEFAULT_DB_TYPE"); SWITCH: { ($LIST_DB) && do { # # Get the entries from the database and put them in # @outlines # @dblist = $dt->listDB(); foreach my $entry (@dblist) { push(@outlines, "'$entry->[0]'\n"); } next SWITCH; }; do { tie (%tmphash, "$DEFAULT_DB_TYPE", $TMPHASH, O_RDWR|O_CREAT, 0600, $DB_HASH) || die "file '$TMPHASH' : $!"; foreach my $target (@TARGETS) { my ($topdevice,$topino,$topmode,$topnlink,$topuid,$topgid) = lstat($target); next if ($topdevice eq undef); find(\&wanted, $target); } $dt->newData(\%tmphash); $dt->compare; @new = $dt->listNew(); foreach my $line (@new) { push(@outlines, "New file : '$line->[0]' \n"); } @removed = $dt->listMissing(); foreach my $line (@removed) { push(@outlines, "Removed file : '$line->[0]' \n"); } $dt->update unless ($NO_UPDATE); untie(%tmphash); unlink($TMPHASH) || die "Error removing file '$TMPHASH'";; next SWITCH; }; }; # # If mailing output, open a pipe to sendmail and dup STDOUT, # otherwise just print @outlines to stdout. # if (@outlines) { print "Yo, admin addr is $ADMIN_ADDR\n"; ($ADMIN_ADDR) && do { print "YO\n"; close(STDOUT) ; $mailfh = safeopen("w", $MAILER, $ADMIN_ADDR); open(STDOUT, ">&" . fileno($mailfh) ) || die "cannot dup stdout"; print "To: $ADMIN_ADDR\n"; print "Subject: FILEAUDIT_OUTPUT_$hostname\n\n"; }; print sort @outlines; ($ADMIN_ADDR) && (close(STDOUT)); } } # # Open a program without using the shell # sub safeopen { my ($rorw, $prog, @opts) = @_; my ($fh, $pid); # # From the perlipc man page # if ($rorw eq "r") { $pid = open(FH, "-|"); } elsif ($rorw eq "w") { $pid = open(FH, "|-"); } else { die "safeopen : expected 'r' or 'w', got $rorw"; } $fh = \*FH; return $fh if ($pid); exec($prog, @opts) || die "cannot exec program '$prog' : $!"; } # # Get directory entries # sub getdents { my $dir = shift; die "'$dir' : Not a directory" unless (-d $dir); opendir(DIR, $dir) || die "$dir : $!"; my @dents = readdir(DIR); closedir(DIR); @dents; }