Perl – SysGuard

I had a problem with web crackers changing some of my pages, adding some nasty JavaScript at the bottom. This script I wrote checks a database defined list of directories and files for various types of change. It can detect changes like file size, inode change, owner, group owner, permissions, etc. If the scan is running on a directory, it detects for any missing / added files. It is to be run from the bash shell, or from CRON.

Running it with result in this help screen:

SysGuard - Bray Almini 2009-06-28.
Keeps an eye on files and folders, detecting new files and changes.

usage: ./sysguard.pl [funtion] [file path]

        Functions:
        --scan  Check all files and folders in DB for changes
        --update        Update data on old record, or create a new record
        --list  Display all files and folders being watched
        --clear Delete a record from the DB


The MySQL table setup:

CREATE TABLE IF NOT EXISTS `sysguard_table` (
  `dev` int(12) NOT NULL,
  `ino` int(12) NOT NULL,
  `mode` int(12) NOT NULL,
  `nlink` int(12) NOT NULL,
  `uid` int(12) NOT NULL,
  `gid` int(12) NOT NULL,
  `rdev` int(12) NOT NULL,
  `size` int(12) NOT NULL,
  `atime` int(12) NOT NULL,
  `mtime` int(12) NOT NULL,
  `ctime` int(12) NOT NULL,
  `blksize` int(12) NOT NULL,
  `blocks` int(12) NOT NULL,
  `file` varchar(256) NOT NULL,
  `stamp` timestamp NOT NULL default CURRENT_TIMESTAMP,
  `ls` text,
  PRIMARY KEY  (`file`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Perl code:

#!/usr/bin/perl

#########################
# Settings
$cfg->{'sqlserv'}       = 'localhost';
$cfg->{'sqluser'}       = 'user';
$cfg->{'sqlpass'}       = 'password';
$cfg->{'sqlbase'}       = 'database';
$cfg->{'sqltabl'}       = 'sysguard_table';

$cfg->{'verbose'}       = 0;
#$cfg->{'reallydb'}     = 1;
$cfg->{'thisfile'}      = "/path/to/this/file/sysguard.pl

#########################
# Setup
use DBI;

$file = $ARGV[1]; # MAKE THIS CLEAN!

our @DEFS    =  (
        "device number of filesystem",
        "inode",
        "type or permissions",
        "number of (hard) links to the file",
        "user owner",
        "group owner",
        "the device identifier",
        "total size",
        "last access time",
        "last modify time",
        "inode change",
        "preferred block size",
        "actual number of blocks allocated"
);

if ($ARGV[0] eq '--scan'){
        $dbh = dbconnect();
        my $sth_inbox = $dbh->prepare("SELECT * FROM `$cfg->{'sqltabl'}` where 1");
        $sth_inbox->execute();
        while (my $ref = $sth_inbox->fetchrow_hashref()) {push(@listok,$ref);}
        foreach my $current(@listok){
            my $changes=0;
            @is = getinfo($current->{'file'});
            @was = (
                $current->{'dev'},$current->{'ino'},$current->{'mode'},$current->{'nlink'},
                $current->{'uid'},$current->{'gid'},$current->{'rdev'},$current->{'size'},
                $current->{'atime'},$current->{'mtime'},$current->{'ctime'},$current->{'blksize'},
                $current->{'blocks'});
            for($i = 0; $i < 13; $i++) {
              next if ($i == "8");
              next if ($i == "9");

              if ($was[$i] != $is[$i]){
                  $changes=1;
                print "$DEFS[$i] changed on file $current->{'file'}\n";
                print "$was[$i]\t!=\t$is[$i]\n" if ($cfg->{'verbose'});
              }else{
                print "$was[$i]\t==\t$is[$i]\n" if ($cfg->{'verbose'});
              }
            }
            # if it's a directory, also check what files it for new / missing files
            $thefirst = substr($is[2], 0, 1);
            if ($thefirst == '1'){
              $ll = `ls -a1 $current->{'file'}`;
              @now_ll = split(/\n/, $ll);
              @was_ll = split(/\n/, $current->{'ls'});
              my @ll_new;
              my @ll_rem;
              foreach $item (@now_ll){
                push(@ll_new, $item) unless grep(/^$item$/, @was_ll);
              }
              foreach $item (@was_ll){
                push(@ll_rem, $item) unless grep(/^$item$/, @now_ll);
              }
              if (@ll_rem){
                $changes=1;
                print "Missing files in $current->{'file'}:";
                print "\n";
                foreach my $mis_ll1(@ll_rem){
                print "\t $mis_ll1\n";
              }
            }
            if (@ll_new){
                  $changes=1;
              print "New files in $current->{'file'}:";
              print "\n";
              foreach my $add_ll1(@ll_new){
                print "\t- $add_ll1\n";
              }
            }
          }
          if ($changes){
                exec("$cfg->{'thisfile'} --update $current->{'file'}");
                $changes=0;
          }
        }
        $dbh->disconnect() if ($dbh);
        exit;
}

if ($ARGV[0] eq '--update'){
        if (-e '$file') {
          print "ERROR: File $file does not exist.\n";
          die;
          }
        $dbh = dbconnect();
        @is = getinfo($file);
        # if it's a directory, also check what files it for new / missing files
        $thefirst = substr($is[2], 0, 1);
        if ($thefirst == '1'){
          $ll = `ls -a1 $file`;
          }

        $dbh->do("INSERT INTO `$cfg->{'sqltabl'}`
        (`dev`,`ino`,`mode`,`nlink`,`uid`,`gid`,`rdev`,`size`,
        `atime`,`mtime`,`ctime`,`blksize`,`blocks`,`file`,`stamp`,`ls`)
        VALUES 
        ('$is[0]','$is[1]','$is[2]','$is[3]','$is[4]','$is[5]','$is[6]','$is[7]',
        '$is[8]','$is[9]','$is[10]','$is[11]','$is[12]','$file',NOW(),'$ll' )
        ON DUPLICATE KEY UPDATE
        `dev`='$is[0]',`ino`='$is[1]',`mode`='$is[2]',`nlink`='$is[3]',`uid`='$is[4]',
        `gid`='$is[5]',`rdev`='$is[6]',`size`='$is[7]',`atime`='$is[8]',`mtime`='$is[9]',
        `ctime`='$is[10]',`blksize`='$is[11]',`blocks`='$is[12]',`file`='$file',`stamp`=NOW(),`ls`='$ll';
        ") or print $dbh->errstr;# if $config->{'reallydb'};
        $dbh->disconnect() if ($dbh);
        exit;
}

if ($ARGV[0] eq '--clear'){
        if (-e '$file') {
          print "ERROR: File $file does not exist.\n";
          die;
          }
        print "Clearing file: $file\n";
        $dbh = dbconnect();
        $dbh->do("DELETE FROM `".$cfg->{'sqltabl'}."` WHERE `file` = '$file'")
          or die("file didn't exist in DB\n");
        $dbh->disconnect() if ($dbh);
        exit;
}

if ($ARGV[0] eq '--list'){
        $dbh = dbconnect();
        my $sth_inbox = $dbh->prepare("SELECT * FROM `$cfg->{'sqltabl'}` where 1");
        $sth_inbox->execute();
        print "sysguard is watching the following files:\n";
        while (my $ref = $sth_inbox->fetchrow_hashref()) {push(@listok,$ref);}
        foreach my $current(@listok){
          print "  $current->{'file'}\n";
        }
        $dbh->disconnect() if ($dbh);
        exit;
}


sub dbconnect{
        our $cfg;
        my $dbh = DBI->connect("dbi:mysql:".$cfg->{'sqlbase'}.':'.$cfg->{'sqlserv'}.":3306",
        "$cfg->{sqluser}", "$cfg->{sqlpass}") or die $DBI::errstr; # connect to db
        return $dbh;
}

sub getinfo($path){
        my @newinfo=stat($_[0]);
        if (!@newinfo){
          print "Can't stat $path $_[0], $!\n";
          return 0;
          }
        return (@newinfo);
}

sub safekeys {
        $_ = $_[0];
        s/â€/"/g;
        s/”/"/g;
        s/´/'/g;
        s/’/'/g;
        s/‘/'/g;
        s/`/'/g;
        s/[^[:alnum:].:!@#\$%^&*()_+-=\[\]<>'"\/\\{}|?\n]/ /go;
        return $_;
}


print "\nSysGuard - Bray Almini 2009-06-28.\n";
print "Keeps an eye on files and folders, detecting new files and changes.\n";
print "\n";
print "usage: $0 [funtion] [file path]\n";
print "\n\tFunctions:\n";
print "\t--scan\tCheck all files and folders in DB for changes\n";
print "\t--update\tUpdate data on old record, or create a new record\n";
print "\t--list\tDisplay all files and folders being watched\n";
print "\t--clear\tDelete a record from the DB\n";
print "\n";