#!/usr/bin/perl
#
# This is NABOU, a system integrity checker
# written in perl.
#
# It is based on a script called "thor.pl",
# which seems no longer being maintained,
# so I decided to enhance it and to remove
# some bugs.
# The result is nabou. Read more about it
# in the supplied manpage.
#
# $Id: nabou,v 1.3 2000/08/13 23:09:00 thomas Exp thomas $
#
# Copyright 2000 (c) Thomas Linden.
# All rights reserved.
#
# This program is published under the terms
# of the GPL. You may redistribute or modify
# the program as you wish.
# The author of the program gives absolutely
# no warranty for damages caused by this
# program. Use it at your own risk.
#
# Of course, you can email me, if you encounter
# any problems or if you find another bug :-)
#
# Thomas Linden <tom@daemon.de>

use Data::Dumper;
use Digest::MD5;
use FileHandle;
use strict;
use Getopt::Long;

# you may edit this value
my $configfile = "nabourc";
my $separator = "  ";
my $underline  = "  " . "-" x 36 . "";

my(
   %config, $conf,				# config obj and hash
   $FirstTime, $Help, $Reset,			# modi
   $md5,					# the MD5 object.
   %userhash,					# contains actual userinfo
   %suidlist,					# file nfo (set u|gid)
   %ncsumlist,					# file list
   %cronlist,                                   # cronjob list
   %dbcronlist,                                 # -""-
   $version, $dummy, $Revision,
   $opt_c, $opt_i, $opt_r, $opt_h, $opt_v, $opt_d, $opt_raw,
   %suid_mask,
  );

$version ="$Revision: 1.3 $dummy";
$version =~ s/^: //;
$version =~ s/ $//;

Getopt::Long::Configure( qw(no_ignore_case));
GetOptions (
	    "init|i!"    => \$opt_i,
	    "reset|r!"   => \$opt_r,
	    "config|c=s" => \$opt_c,
	    "help|h|?!"  => \$opt_h,
	    "version|v!" => \$opt_v,
	    "dump|d=s"   => \$opt_d,
	    "raw!"       => \$opt_raw,
	    );

if ($opt_c) {
    $configfile = $opt_c;
}

if ($opt_h or ($opt_r and $opt_i)) {
    &usage;
}

if ($opt_v) {
    print "This is nabou version $version Copyright 2000 (c) Thomas Linden\n";
    exit 1;
}


$Reset     = 1 if($opt_r);
$FirstTime = 1 if($opt_i);

if ($opt_d) {
    &dump($opt_d, $opt_raw);
    exit;
}


$conf = new Conf($configfile);
%config = $conf->getall();



# use mail instead of STDOUT
if($config{usemail} && !$opt_r && !$opt_i) {
  open(MAIL, "|$config{bin}->{sendmail} -t") or die $!;
  select MAIL;
  print "From: $config{mail}->{from}\nTo: $config{mail}->{rcpt}\n"
       ."Subject: $config{mail}->{subject}\n\n\n";
}

# autoflush on
#$| = 1;

if(!-x $config{db}->{basedir}) {
    die "permission denied: $config{db}->{basedir}\n";
}
elsif (!-d $config{db}->{basedir}) {
    die "$config{db}->{basedir} does not exist or is not a directory!\n";
}
else {
    chdir $config{db}->{basedir};
}


# check for per dir inheritance
# and set up default properties
foreach my $dir (sort keys %{$config{directory}}) {
      if($config{directory}->{$dir}->{inherit}) {
	  if(!exists $config{directory}->{ $config{directory}->{$dir}->{inherit} }) {
	      print "directory settings for $dir cannot be inherited!\n"
	      	   ."$config{directory}->{$dir}->{inherit} is not defined!\n"
		   ."Using default check: MD5 Checksum\n";
	      $config{directory}->{$dir} = {};
	      $config{directory}->{$dir}->{md5} = 1;
	  }
	  else {
	      my $inhdir = $config{directory}->{$dir}->{inherit};
	      %{$config{directory}->{$dir}} = %{$config{directory}->{$inhdir}};
	  }
      }
      my $str_switches;
      foreach my $switch (sort keys %{$config{directory}->{$dir}}) {
	  next if($switch !~ /^chk_/);
	  if (exists $config{directory}->{$dir}->{$switch} and
               $config{directory}->{$dir}->{$switch} !~ /^(1|on)$/) {
	      delete $config{directory}->{$dir}->{$switch};
	  }
	  $str_switches .=  $switch;
      }
      if ($str_switches eq "chk_all") {
	  # use all senceful checks
	  my $origswitches = $config{directory}->{$dir};
	  $config{directory}->{$dir} = {
					chk_md5   => 1,
					chk_size  => 1,
					chk_mtime => 1,
					chk_uid   => 1,
					chk_nlink => 1,
					chk_gid   => 1,
					chk_ino   => 1,
					chk_mode  => 1,
					};
	  # restore orig options
	  %{$config{directory}->{$dir}} = (%{$config{directory}->{$dir}}, %{$origswitches});
	  delete $config{directory}->{$dir}->{chk_all};
      }
      elsif ($str_switches eq "") {
	  # use the default checks
	  $config{directory}->{$dir} = {
					chk_md5   => 1,
					};
      }
}

# install suid_mask, used by suid_update()
if ($config{check_suid}) {
    if (!exists $config{suid}) {
	# this is the default for suid checks
	$config{suid}->{chk_md5}  = 1;
	$config{suid}->{chk_mode} = 1;
    }
    foreach my $bit (sort keys %{$config{suid}}) {
	next if($bit !~ /^chk_/);
	my $msk     = $config{suid}->{$bit};
	$bit        =~ s/^chk_//;
	$suid_mask{$bit} = $msk if($msk);
    }
}

#print Dumper($config{directory});
#exit;

if($Reset || $FirstTime) {
  $FirstTime = 1;
  print $separator, "\n";
  print "        Resetting nabou's Databases\n";
  print $underline, "\n"; 
  unlink($config{db}->{pwdDB});
  unlink($config{db}->{csumDB});
  unlink($config{db}->{cronDB});
  unlink($config{db}->{sugidDB});
  unlink($config{db}->{miscDB});

  unlink($config{db}->{pwdDB}   . ".dir");
  unlink($config{db}->{pwdDB}   . ".pag");

  unlink($config{db}->{csumDB}  . ".dir");
  unlink($config{db}->{csumDB}  . ".pag");

  unlink($config{db}->{cronDB}  . ".dir");
  unlink($config{db}->{cronDB}  . ".pag");

  unlink($config{db}->{sugidDB} . ".dir");
  unlink($config{db}->{sugidDB} . ".pag");

  unlink($config{db}->{miscDB}  . ".dir");
  unlink($config{db}->{miscDB}  . ".pag");
}


# run
&verify_programs;

&get_root_info       if($config{check_root});
&show_roots          if($config{check_root});

&update_pwd_db       if($config{check_users});

&check_crontab       if($config{check_cron});
&update_cron_db      if($config{check_cron});

&check_suid          if($config{check_suid});
&update_suid_db      if($config{check_suid});

&check_directories   if($config{check_md5});
&update_dir_db       if($config{check_md5});

print "$separator\n";

if($FirstTime == 1) {
  print "\nYou are ready to install nabou as a daily cronjob.\n";
}

sub verify_programs {
  my(@dbcsumsize, %dbmisc, $mailprog, $crontab);
  print $separator, "\n";
  print "     Verifying the stability of nabou\n";
  print $underline, "\n";
  if((-l ($config{db}->{miscDB} . ".dir")) || (-l ($config{db}->{miscDB} . ".pag"))) {
    die("$config{db}->{miscDB} files exist as a link, and could be harmful if written to.");
  }
  if((-l ($config{db}->{pwdDB} . ".dir")) || (-l ($config{db}->{pwdDB} . ".pag"))) {
    die("$config{db}->{pwdDB} files exist as a link, and could be harmful if written to.");
  }
  if((-l ($config{db}->{sugidDB} . ".dir")) || (-l ($config{db}->{sugidDB} . ".pag"))) {
    die("$config{db}->{sugidDB} files exist as a link, and could be harmful if written to.");
  }
  if((-l ($config{db}->{csumDB} . ".dir")) || (-l ($config{db}->{csumDB} . ".pag"))) {
    die("$config{db}->{csumDB} files exist as a link, and could be harmful if written to.");
  }
  if((-l ($config{db}->{cronDB} . ".dir")) || (-l ($config{db}->{cronDB} . ".pag"))) {
    die("$config{db}->{cronDB} files exist as a link, and could be harmful if written to.");
  }
  dbmopen(%dbmisc, $config{db}->{miscDB}, 0600) || die "Can't open $config{db}->{miscDB}\: $!\n";
  $mailprog = new File($config{bin}->{sendmail});
  if($mailprog->csv != $dbmisc{$config{bin}->{sendmail}}) {
    if($FirstTime == 1) {
      print "Updating: $config{bin}->{sendmail}\n";
      $dbmisc{$config{bin}->{sendmail}} = $mailprog->csv;
    }
    else{
      die("$config{bin}->{sendmail}\'s file info has changed.  It's Possible this program\n"
	  ."has been tampered with.");
    }
  }
  $crontab = new File($config{bin}->{crontab});
  if($crontab->csv != $dbmisc{$config{bin}->{crontab}}) {
    if($FirstTime == 1) {
      print "Updating: $config{bin}->{crontab}\n";
      $dbmisc{$config{bin}->{crontab}} = $crontab->csv;
    }
    else{
      die("$config{bin}->{crontab}\'s file info has changed.  It's Possible this program\n"
	  ."has been tampered with.");
    }
  }
  dbmclose(%dbmisc);
}



sub get_root_info {
  # store all root user accounts
  my($login,$passwd,$uid,$gid,$comment,$home,$shell,@rest,$user);
  open(PASSWD, "<$config{passwd}") || die "Can't open $config{passwd}: $!\n";
  while(<PASSWD>) {
    chomp;
    ($login,$passwd,$uid,$gid,$comment,$home,$shell) = split(":", $_);
    if(($uid == 0 || $gid == 0) || ($uid == 131072 || $gid == 131072)) {
      if($config{shadow} == 1) {
	open(SHADOW, $config{shadow}) || die "Can't open $config{shadow}: $!\n";
	while(<SHADOW>) {
	  if(/^$login/) {
	    chomp;
	    ($user, $passwd, @rest) = split /:/;
	  }
	}
	close(SHADOW);
      }
      $userhash{$login} = join ":", ($login,$passwd,$uid,$gid,$comment,$home,$shell);
    }
  }
  close(PASSWD);
}




sub show_roots {
  # print out all about 0 userz
  my($login,$passwd,$uid,$gid,$comment,$home,$shell);
  print $separator, "\n";
  print "     Users with root UID's and GID's\n";
  print $underline, "\n";
  foreach(keys %userhash) {
    ($login,$passwd,$uid,$gid,$comment,$home,$shell) = split(":", $userhash{$_});
    print "User: $login (UID=$uid\tGID=$gid\tHome Dir=$home\tSHELL=$shell)\n";
  }
}




sub update_pwd_db {
  my(%dbpwd);
  print $separator, "\n";
  print "    Changed user accounts\n";
  print $underline, "\n";
  dbmopen(%dbpwd, $config{db}->{pwdDB}, 0600) || die "Can't open $config{db}->{pwdDB}\: $!\n";
  foreach my $login (keys %userhash) {
    if(! $dbpwd{$login}) {
      print "$login:\tAccount was not in the DataBase. [Adding...]\n";
      $dbpwd{$login} = $userhash{$login};
    }
    elsif($userhash{$login} ne $dbpwd{$login}) {
      print "$login:\tAccount information was changed.\n";
      my @olddata = split(":", $userhash{$login});
      my @dbdata  = split(":", $dbpwd{$login});
      print "[Old]\tUID=$dbdata[2]\tGID=$dbdata[3]\tHome Dir=$dbdata[5]\tShell=$dbdata[6]\n";
      print "[New]\tUID=$olddata[2]\tGID=$olddata[3]\tHome Dir=$olddata[5]\tShell=$olddata[6]\n";
      $dbpwd{$login} = $userhash{$login};
    }
  }
  foreach my $login(keys %dbpwd) {
    if(! $userhash{$login}) {
      print "$login\:\tAccount was not found. [Removing...]\n";
      delete($dbpwd{$login});
    }
  }
  dbmclose(%dbpwd);
}




sub check_suid {
    print $separator, "\n";
    print "    Changes in suid/sgid files\n";
    print $underline, "\n";
    &recurse_suid("/");
}


sub recurse_suid {
    my($dir) = @_;
    my($file);
    my $fh = new IO::Handle;
    opendir $fh, $dir or die "$!\n";
    while($file = readdir($fh)) {
        next if($file =~ /^\.$/ || $file =~ /^\.\.$/);
	if($dir ne "/") {
            $file = $dir . "/" . $file;
        }
	else {
	    $file = $dir . $file;
	}
	next if($file =~ /^\/proc/);
        if(-d $file && !-l $file) {
            &recurse_suid($file);
        }
        if(!-l $file && !-d $file && (-u $file || -g $file)) {
            my $obj = new File($file);
            $suidlist{$file} = $obj->csv;
        }
    }
    closedir $fh;
    undef $fh;
}



sub update_suid_db {
  my(%dbsugid);
  dbmopen(%dbsugid, $config{db}->{sugidDB}, 0600) or
    die "Can't open $config{db}->{sugidDB}: $!\n";
  foreach my $file (sort keys %suidlist) {
    if(! $dbsugid{$file}) {
      print "$file:\tFile was not in the DataBase. [Adding...]\n";
      $dbsugid{$file} = $suidlist{$file};
      &ShellChecksum($file);
    }
    elsif($dbsugid{$file} ne $suidlist{$file}) {
      my @dbdata  = split(":", $dbsugid{$file});
      my @newdata = split(":", $suidlist{$file});
      foreach my $bit (sort keys %suid_mask) {
	  if($bit eq "md5" && $newdata[0] ne $dbdata[0]) {
	      print "$file:\t (MD5 checksum has changed)\n"
		   ."[Old] $dbdata[0]\n[New] $newdata[0]\n\n";
	  }
	  elsif($bit eq "ino" && $newdata[2] ne $dbdata[2]) {
	      print "$file:\t (Inode has changed)\n"
		   ."[Old] $dbdata[2]\n[New] $newdata[2]\n\n";
	  }
	  elsif ($bit eq "dev" && $newdata[1] ne $dbdata[1]) {
	      print "$file:\t (Filesystem device number has changed)\n"
		   ."[Old] $dbdata[1]\n[New] $newdata[1]\n\n";
	  }
	  elsif ($bit eq "mode" &&  $newdata[3] ne $dbdata[3]) {
	      my $oldmode = sprintf("%04o", $dbdata[3] & 07777);
	      my $newmode = sprintf("%04o", $newdata[3] & 07777);
	      print "$file:\t (File mode has changed)\n"
		   ."[Old] $oldmode\n[New] $newmode\n\n";
	  }
	  elsif ($bit eq "nlink" && $newdata[4] ne $dbdata[4]) {
	      print "$file:\t (Number of links to this file has changed)\n"
		   ."[Old] $dbdata[4]\n[New] $newdata[4]\n\n";
	  }
	  elsif ($bit eq "uid" && $newdata[5] ne $dbdata[5]) {
	      my $olduser = getpwnam($dbdata[5]);
	      my $newuser = getpwnam($newdata[5]);
	      print "$file:\t (Owner has changed)\n"
		   ."[Old] $olduser\n[New] $newuser\n\n";
	  }
	  elsif ($bit eq "gid" && $newdata[6] ne $dbdata[6] ) {
	      my $olduser = getgrgid($dbdata[6]);
	      my $newuser = getgrgid($newdata[6]);
	      print "$file:\t (Group has changed)\n"
		   ."[Old] $olduser\n[New] $newuser\n\n";
	  }
	  elsif ($bit eq "size" && $newdata[8] ne $dbdata[8]) {
	      print "$file:\t (Size has changed)\n"
		   ."[Old] $dbdata[8] bytes\n[New] $newdata[8] bytes\n\n";
	  }
	  #elsif ($bit eq "atime" && $newdata[9] ne $dbdata[9]) {
	  #    print "$file:\t (Access time has changed)\n"
	  #   ."[Old] " . scalar localtime($dbdata[9])
	  #   ."   [New] " . scalar localtime($newdata[9]) . "\n\n";
	  #}
	  elsif ($bit eq "mtime" && $newdata[10] ne $dbdata[10]) {
	      print "$file:\t (Modification time has changed)\n"
		   ."[Old] \"" . scalar localtime($dbdata[10])
		   ."\"\n[New] \"" . scalar localtime($newdata[10]) . "\"\n\n";
	  }
	  elsif ($bit eq "ctime" && $newdata[11] ne $dbdata[11]) {
	      print "$file:\t (Inode change time has changed)\n"
		   ."[Old] \"" . scalar localtime($dbdata[11])
		   ."\"\n[New] \"" . scalar localtime($newdata[11]) . "\"\n\n";
	  }
	  elsif ($bit eq "blocks" && $newdata[13] ne $dbdata[13]) {
	      print "$file:\t (Number of allocated blocks has changed)\n"
	           ."[Old] $dbdata[13] blocks\n[New] $newdata[13] blocks\n\n";
	  }
      }
      $dbsugid{$file} = $suidlist{$file};
    }
  }

  foreach my $file (sort keys %dbsugid) {
    if(! $suidlist{$file}) {
      print "$file:\tFile was not found. [Removing...]\n";
      delete($dbsugid{$file});
    }
  }
  dbmclose(%dbsugid);
  undef %suidlist;
}



sub ShellChecksum {
  my($file) = @_;
  my(%scsum);
  open(CSUM, $config{shells}) or die "Can't open $config{shells}: $!";
  while(<CSUM>) {
    chomp;
    if(! -l $_) {
      my $obj = new File($_);
      $scsum{$_}  = $obj->md5;
    }
  }
  close(CSUM);
  my $setobj = new File($file);
  foreach my $shell (sort keys %scsum) {
    if($setobj->md5 eq $scsum{$shell}) {
      print "Warning:\t$file has the same checksum as $shell\!\n";
    }
  }
}




sub check_directories {
  my(@exclude, @include, %mask);
  print $separator . "\n";
  print "    Changed files in monitored dirs\n";
  print $underline . "\n";
  foreach my $csdir (sort keys %{$config{directory}}) {
    my $exclude = $config{directory}->{$csdir}->{exclude};
    if(ref($exclude) eq "ARRAY") {
	foreach (@{$exclude}) {
	    push @exclude, $csdir . "/" . $_;
	}
    }
    else {
	@exclude = ($csdir . "/" . $exclude) if ($exclude);
    }

    @exclude = &regex(@exclude);

    my $include = $config{directory}->{$csdir}->{include};
    if(ref($include) eq "ARRAY") {
	foreach (@{$include}) {
	    push @include, $csdir . "/" . $_;
	}
    }
    else {
	@include = ($csdir . "/" . $include) if ($include);
    }

    %mask = ();
    foreach my $bit (sort keys %{$config{directory}->{$csdir}}) {
	next if($bit !~ /^chk_/);
	my $msk = $config{directory}->{$csdir}->{$bit};
	$bit =~ s/^chk_//;
	$mask{$bit} = $msk if($msk);
    }

    print "  => $csdir\n\n";
    if (@include) {
	# process only the specified filez
	&process_includes(\%mask, \@include);
    }
    else {
	# go through all filez
	&recurse_dirs($csdir, \%mask, \@exclude, $config{directory}->{$csdir}->{recursive});
    }
  }
}

sub process_includes {
    my($mask, $include) = @_;
    foreach my $file (@{$include}) {
	if (!-l $file && !-d $file && -e $file) {
	    my $obj = new File($file);
	    $ncsumlist{$file} = $obj->csv;
    	    &CheckChange($file, $mask);
	}
    }
}

sub recurse_dirs {
    my($dir, $mask, $exclude, $recursive) = @_;
    my($file,$infile);
    my $fh = new FileHandle;
    opendir $fh, $dir;
    while($infile = readdir($fh)) {
	$file = $infile;
	next if($file =~ /^\.$/ || $file =~ /^\.\.$/);
	$file = $dir . "/" . $file;
	next if(grep { $file =~ /$_/ } @{$exclude});
	if($recursive) {
	    if(-d $file && !-l $file) {
		&recurse_dirs($file, $mask, $exclude, $recursive);
	    }
	}
	if(!-l $file && !-d $file) {
	    my $obj = new File($file);
	    $ncsumlist{$file} = $obj->csv;
    	    &CheckChange($file, $mask);
	}
    }
    close $fh;
}


sub regex {
    foreach (@_) {
	$_ =~ s/\*/\.\*/g;
	$_ =~ s/\?/./g;
    }
    return @_;
}


sub CheckChange {
  my($file, $mask) = @_;
  my(%dbcsumlist);
  dbmopen(%dbcsumlist, $config{db}->{csumDB}, 0600) or
                          die "Can't open $config{db}->{csumDB}\: $!\n";
  if(! $dbcsumlist{$file}) {
      print "$file:\tFile was not in the DataBase. [Adding...]\n";
      $dbcsumlist{$file} = $ncsumlist{$file};
  }
  elsif($dbcsumlist{$file} ne $ncsumlist{$file}) {
      my @dbdata  = split(":", $dbcsumlist{$file});
      my @newdata = split(":", $ncsumlist{$file});
      foreach my $bit (sort keys %{$mask}) {
	  if($bit eq "md5" && $newdata[0] ne $dbdata[0]) {
	      print "$file:\t (MD5 checksum has changed)\n"
		   ."[Old] $dbdata[0]\n[New] $newdata[0]\n\n";
	  }
	  elsif($bit eq "ino" && $newdata[2] ne $dbdata[2]) {
	      print "$file:\t (Inode has changed)\n"
		   ."[Old] $dbdata[2]\n[New] $newdata[2]\n\n";
	  }
	  elsif ($bit eq "dev" && $newdata[1] ne $dbdata[1]) {
	      print "$file:\t (Filesystem device number has changed)\n"
		   ."[Old] $dbdata[1]\n[New] $newdata[1]\n\n";
	  }
	  elsif ($bit eq "mode" &&  $newdata[3] ne $dbdata[3]) {
	      my $oldmode = sprintf("%04o", $dbdata[3] & 07777);
	      my $newmode = sprintf("%04o", $newdata[3] & 07777);
	      print "$file:\t (File mode has changed)\n"
		   ."[Old] $oldmode\n[New] $newmode\n\n";
	  }
	  elsif ($bit eq "nlink" && $newdata[4] ne $dbdata[4]) {
	      print "$file:\t (Number of links to this file has changed)\n"
		   ."[Old] $dbdata[4]\n[New] $newdata[4]\n\n";
	  }
	  elsif ($bit eq "uid" && $newdata[5] ne $dbdata[5]) {
	      my $olduser = getpwuid($dbdata[5]);
	      my $newuser = getpwnam($newdata[5]);
	      print "$file:\t (Owner has changed)\n"
		   ."[Old] $olduser\n[New] $newuser\n\n";
	  }
	  elsif ($bit eq "gid" && $newdata[6] ne $dbdata[6] ) {
	      my $olduser = getgrgid($dbdata[6]);
	      my $newuser = getgrgid($newdata[6]);
	      print "$file:\t (Group has changed)\n"
		   ."[Old] $olduser\n[New] $newuser\n\n";
	  }
	  elsif ($bit eq "size" && $newdata[8] ne $dbdata[8]) {
	      print "$file:\t (Size has changed)\n"
		   ."[Old] $dbdata[8] bytes\n[New] $newdata[8] bytes\n\n";
	  }
	  #elsif ($bit eq "atime" && $newdata[9] ne $dbdata[9]) {
	  #    print "$file:\t (Access time has changed)\n"
	  #   ."[Old] " . scalar localtime($dbdata[9])
	  #   ."   [New] " . scalar localtime($newdata[9]) . "\n\n";
	  #}
	  elsif ($bit eq "mtime" && $newdata[10] ne $dbdata[10]) {
	      print "$file:\t (Modification time has changed)\n"
		   ."[Old] \"" . scalar localtime($dbdata[10])
		   ."\"\n[New] \"" . scalar localtime($newdata[10]) . "\"\n\n";
	  }
	  elsif ($bit eq "ctime" && $newdata[11] ne $dbdata[11]) {
	      print "$file:\t (Inode change time has changed)\n"
		   ."[Old] \"" . scalar localtime($dbdata[11])
		   ."\"\n[New] \"" . scalar localtime($newdata[11]) . "\"\n\n";
	  }
	  elsif ($bit eq "blocks" && $newdata[13] ne $dbdata[13]) {
	      print "$file:\t (Number of allocated blocks has changed)\n"
	           ."[Old] $dbdata[13] blocks\n[New] $newdata[13] blocks\n\n";
	  }
      }
      $dbcsumlist{$file} = $ncsumlist{$file};
  }
  dbmclose(%dbcsumlist);
}




sub update_dir_db {
    my(%dbcsumlist);
    dbmopen(%dbcsumlist, $config{db}->{csumDB}, 0600) or
                      die "Can't open $config{db}->{csumDB}\: $!\n";
    foreach my $file (sort keys %dbcsumlist) {
	if(! $ncsumlist{$file}) {
	    print "$file: File was not found or no more being monitored. [Removing...]\n";
	    delete($dbcsumlist{$file});
	}
    }
    dbmclose(%dbcsumlist);
}



sub check_crontab{
  print $separator, "\n";
  print "    Changes in user crontabs\n";
  print $underline, "\n";
  foreach my $login(keys %userhash) {
    open(CRON, "$config{crontab} -u $login -l |");
    while(<CRON>) {
      next if(/^#/);
	$cronlist{$login} = $cronlist{$login} . $_;
    }
    close(CRON);
  }
  undef %userhash;
}

sub update_cron_db{
  dbmopen(%dbcronlist, $config{db}->{cronDB}, 0600) || die "Can't open $config{db}->{cronDB}\: $!\n";
  foreach my $login (sort keys %cronlist) {
    if(! $dbcronlist{$login}) {
      print "$login\:\tAccount was not in the DataBase. [Adding...]\n";
      $dbcronlist{$login} = $cronlist{$login};
    }elsif($dbcronlist{$login} ne $cronlist{$login}) {
      print "$login\:\tCrontab has changed.\n";
      print "[Old Crontab]\n$dbcronlist{$login}";
      print "[New Crontab]\n$cronlist{$login}";
      $dbcronlist{$login} = $cronlist{$login};
    }
  }
  foreach my $login (sort keys %dbcronlist) {
    if(! $cronlist{$login}) {
      print "$login\:\tAccount was not found. [Removing...]\n";
      delete($dbcronlist{$login});
    }
  }
  dbmclose(%dbcronlist);
  undef %dbcronlist;
  undef %cronlist;
}




sub dump {
    my($db, $raw) = @_;
    my %database;
    my $c = ",";
    dbmopen(%database, $db, 0600) or
      die "Can't open $db: $!\n";
    foreach my $file (sort keys %database) {
	print $file . $c;
	if ($raw) {
	    my $line = $database{$file};
	    $line =~ s/:/,/g;
	    print $line . "\n";
	}
	else {
	    my @data  = split(":", $database{$file});
	    print $data[0] . $c . $data[1] . $c . $data[2] . $c;
	    print sprintf("%04o", $data[3] & 07777);
	    print $c . $data[4] . $c;
	    print getpwuid($data[5]) . $c;
	    print getgrgid($data[6]) . $c;
	    print $data[7] . $c . $data[8] . $c;
	    print scalar localtime($data[9]);
	    print $c;
	    print scalar localtime($data[10]);
	    print $c;
	    print scalar localtime($data[11]);
	    print $c . $data[12] . $c . $data[12];
	    print "\n";
	}
    }
}

sub usage {
  print "usage: $0 [options]\n"
       ."-c --config <file> use another config file\n"
       ."-i --init          initialize $0\n"
       ."-r --reset         reset $0 database\n"
       ."-d --dump <file>   dump the contents of a nabou db\n"
       ."   --raw           causes an unformatted dump\n"
       ."-h --help          show this message\n"
       ."-v --version       show version number\n"
       ."$0 with no options is normal operation mode\n";
  exit;
}


#########################################################################################
# packages
#########################################################################################


package Conf;

# Constants
sub TRUE {return 1};
sub FALSE{return 0};




sub new {
  #
  # create new Config object
  #
  my($this, $configfile ) = @_;
  my $class = ref($this) || $this;
  my $self = {};
  bless($self,$class);

  my(%config);
  %config = ();
  $self->{level} = 1;

  $self->{configfile} = $configfile;

  # open the file and read the contents in
  $self->_open($self->{configfile});

  return $self;
}



sub getall {
  #
  # just return the whole config hash
  # parse the contents of the file
  #
  my($this) = @_;

  # avoid twice parsing
  if (!$this->{parsed}) {
    $this->{parsed} = 1;
    $this->{config} = $this->_parse({}, $this->{content});
  }
  my %allhash = %{$this->{config}};
  return %allhash;
}



sub _open {
  #
  # open the config file
  # and store it's contents in @content
  #
  my($this, $configfile) = @_;
  my(@content, $c_comment, $longline, $hier, $hierend, @hierdoc);

  my $fh = new FileHandle;

  if (-e $configfile) {
    open $fh, "<$configfile" or die "Could not open $configfile!($!)\n";
    while (<$fh>) {
      chomp;
      next if (/^\s*$/ || /^\s*#/);               # ignore whitespace(s) and lines beginning with #
      if (/^([^#]+?)#/) {
	$_ = $1;                                  # remove trailing comment
      }
      if (/^\s*(.+?)(\s*=\s*|\s+)<<(.+?)$/) {     # we are @ the beginning of a here-doc
	$hier = $1;                               # $hier is the actual here-doc
	$hierend = $3;                            # the here-doc end string, i.e. "EOF"
      }
      elsif (/^(\s*)\Q$hierend\E$/) {             # the current here-doc ends here
	my $indent = $1;                          # preserve indentation
	$hier .= " " . chr(182) . "\n";           # append a "" to the here-doc-name, so _parse will also preserver indentation
	if ($indent) {
	  foreach (@hierdoc) {
	    $_ =~ s/^$indent//;                   # i.e. the end was: "    EOF" then we remove "    " from every here-doc line
	    $hier .= $_ . "\n";                   # and store it in $hier
	  }
	}
	else {
	  $hier .= join "\n", @hierdoc;           # there was no indentation of the end-string, so join it 1:1
	}
	push @{$this->{content}}, $hier;          # push it onto the content stack
	@hierdoc = ();
	undef $hier;
	undef $hierend;
      }
      elsif (/^\s*\/\*/) {                        # the beginning of a C-comment ("/*"), from now on ignore everything
	$c_comment = 1;                           # until a "*/" occurs.
      }
      elsif (/\*\//) {
	if (!$c_comment) {
	  warn "invalid syntax: found end of C-comment without previous start!\n";
	}
	$c_comment = 0;                           # the current C-comment ends here, go on 
      }
      elsif (/\\$/) {                             # a multiline option, indicated by a trailing backslash
	chop;
	$_ =~ s/^\s*//;
	$longline .= $_ if(!$c_comment);          # store in $longline
      }
      else {                                      # any "normal" config lines
	if ($longline) {                          # previous stuff was a longline and this is the last line of the longline
	  $_ =~ s/^\s*//;
	  $longline .= $_ if(!$c_comment);
	  push @{$this->{content}}, $longline;    # push it onto the content stack
	  undef $longline;
	}
	elsif ($hier) {                           # we are inside a here-doc
	  push @hierdoc, $_;                      # push onto here-dco stack
	}
	else {
	  if (/^<<include (.+?)>>$/) {            # include external config file
	    $this->_open($1) if(!$c_comment);     # call _open with the argument to include assuming it is a filename
	  }
	  else {                                  # standard config line, push it onto the content stack
	    push @{$this->{content}}, $_ if(!$c_comment);
	  }
	}
      }
    }
    close $fh;
  }
  else {
    die "The file \"$configfile\" does not exist!\n";
  }
  return TRUE;
}




sub _parse {
  #
  # parse the contents of the file
  #
  my($this, $config, $content) = @_;
  my(@newcontent, $block, $blockname, $grab, $chunk,$block_level);

  foreach (@{$content}) {                                  # loop over content stack
    chomp;
    $chunk++;
    $_ =~ s/^\s*//;                                        # strip spaces @ end and begin
    $_ =~ s/\s*$//;

    my ($option,$value) = split /\s*=\s*|\s+/, $_, 2;      # option/value assignment, = is optional
    my $indichar = chr(182);                               # , inserted by _open, our here-doc indicator
    $value =~ s/^$indichar//;                              # a here-doc begin, remove indicator
    $value =~ s/^"//;                                      # remove leading and trailing "
    $value =~ s/"$//;
    if (!$block) {                                         # not inside a block @ the moment
      if (/^<([^\/]+?.*?)>$/) {                            # look if it is a block
	$this->{level} += 1;
	$block = $1;                                       # store block name
	($grab, $blockname) = split /\s\s*/, $block, 2;    # is it a named block? if yes, store the name separately
	if ($blockname) {
	  $block = $grab;
	}
	undef @newcontent;
	next;
      }
      elsif (/^<\/(.+?)>$/) {                              # it is an end block, but we don't have a matching block!
	die "EndBlock \"<\/$1>\" has no StartBlock statement (level: $this->{level}, chunk $chunk)!\n";
      }
      else {                                               # insert key/value pair into actual node
	if ($this->{NoMultiOptions}) {                     # configurable via special method ::NoMultiOptions()
	  if (exists $config->{$option}) {
	    die "Option $config->{$option} occurs more than once (level: $this->{level}, chunk $chunk)!\n";
	  }
	  $config->{$option} = $value;
	}
	else {
	  if (exists $config->{$option}) {	           # value exists more than once, make it an array
	    if (ref($config->{$option}) ne "ARRAY") {      # convert scalar to array
	      my $savevalue = $config->{$option};
	      delete $config->{$option};
	      push @{$config->{$option}}, $savevalue;
	    }
	    push @{$config->{$option}}, $value;            # it's still an array, just push
	  }
	  else {
	    $config->{$option} = $value;                   # standard config option, insert key/value pair into node
	  }
	}
      }
    }
    elsif (/^<([^\/]+?.*?)>$/) {                           # found a start block inside a block, don't forget it
      $block_level++;                                      # $block_level indicates wether we are still inside a node
      push @newcontent, $_;                                # push onto new content stack for later recursive call of _parse()
    }
    elsif (/^<\/(.+?)>$/) {
      if ($block_level) {                                  # this endblock is not the one we are searching for, decrement and push
	$block_level--;                                    # if it is 0 the the endblock was the one we searched for, see below 
	push @newcontent, $_;                              # push onto new content stack
      }
      else {                                               # calling myself recursively, end of $block reached, $block_level is 0
	if ($blockname) {
	  $config->{$block}->{$blockname} =                # a named block, make it a hashref inside a hash within the current node
	    $this->_parse($config->{$block}->{$blockname}, \@newcontent);
	}
	else {                                             # standard block
	  $config->{$block} = $this->_parse($config->{$block}, \@newcontent);
	}
	undef $blockname;
	undef $block;
	$this->{level} -= 1;
	next;
      }
    }
    else {                                                 # inside $block, just push onto new content stack
      push @newcontent, $_;
    }
  }
  if ($block) {
    # $block is still defined, which means, that it had
    # no matching endblock!
    die "Block \"<$block>\" has no EndBlock statement (level: $this->{level}, chunk $chunk)!\n";
  }
  return $config;
}


sub NoMultiOptions {
  #
  # turn NoMultiOptions off
  #
  my($this) = @_;
  $this->{NoMultiOptions} = 1;
}

# keep this one
1;








package File;

sub new {
  #
  # create new File object
  #
  my($this, $file ) = @_;
  my $class = ref($this) || $this;
  my $self = {};
  bless($self,$class);

  my(%stats);
  %stats = ();

  $self->{file} = $file;

  # open the file and get stats 
  $self->_stats;

  $self->_md5;

  return $self;
}


sub _stats {
=head1 
  0 dev      device number of filesystem
  1 ino      inode number
  2 mode     file mode  (type and permissions)
  3 nlink    number of (hard) links to the file
  4 uid      numeric user ID of file's owner
  5 gid      numeric group ID of file's owner
  6 rdev     the device identifier (special files only)
  7 size     total size of file, in bytes
  8 atime    last access time since the epoch
  9 mtime    last modify time since the epoch
 10 ctime    inode change time (NOT creation time!) since the epoch
 11 blksize  preferred block size for file system I/O
 12 blocks   actual number of blocks allocated
=cut
  my($this) = @_;
  my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
         $atime,$mtime,$ctime,$blksize,$blocks); 
  eval {
       ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
       $atime,$mtime,$ctime,$blksize,$blocks) = stat($this->{file});
  };
  if($@) {
	print $@;
  }
  else {
      my %stats = (
      	dev	=> $dev,
	ino	=> $ino,
	mode	=> $mode,
	nlink	=> $nlink,
	uid	=> $uid,
	gid	=> $gid,
	rdev	=> $rdev,
	size	=> $size,
	atime	=> $atime,
	mtime	=> $mtime,
	ctime	=> $ctime,
	blksize	=> $blksize,
	blocks	=> $blocks,
	);
      $this->{stats} = \%stats;
  }
}

sub csv {
	# return colon separated list of all properties.
	# used for database storage
	my($this) = @_;
	my $list =   $this->md5     . ":"
		   . $this->dev     . ":"
		   . $this->ino     . ":"
		   . $this->mode    . ":"
		   . $this->nlink   . ":"
		   . $this->uid     . ":"
		   . $this->gid     . ":"
		   . $this->rdev    . ":"
		   . $this->size    . ":"
		   . $this->atime   . ":"
		   . $this->mtime   . ":"
		   . $this->ctime   . ":"
		   . $this->blksize . ":"
		   . $this->blocks;
	return $list;
}


sub _md5 {
        my($this) = @_;
	$md5 = new Digest::MD5;
        eval {
            open FILE, $this->{file} or die "Can't open file $this->{file} for check: $!\n";
            binmode(FILE);
            $md5->addfile(*FILE);
        };
        if($@) {
            print $@;
        }
        $this->{stats}->{md5} = $md5->hexdigest;
	close FILE;
	undef $md5;
}


sub AUTOLOAD {
   # return a %stats value
   my($this) = shift;
   my $SUB = $File::AUTOLOAD;  # get to know how we were called
   $SUB =~ s/.*:://; # remove package name!
   return (exists $this->{stats}->{$SUB}) ? $this->{stats}->{$SUB} : "";
}
