#!/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.4 2000/08/16 23:54:01 tom Exp tom $
#
# 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 Digest::SHA1;
use Digest::MD2;
use FileHandle;
use strict;
require "newgetopt.pl";

# you may edit this values
my $configfile = "/etc/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,$opt_u,
   $opt_q,
   %suid_mask, $suid_msg, $dir_msg, $algo,
  );

# define the version string from RCS
$version ="$Revision: 1.4 $dummy";
$version =~ s/^: //;
$version =~ s/ $//;

# get commandline options and store them in scalar refs.
GetOptions (
	    "init|i!"    => \$opt_i,    # no arg
	    "reset|r!"   => \$opt_r,    # no arg
	    "config|c=s" => \$opt_c,    # string arg required
	    "help|h!"    => \$opt_h,    # no arg
	    "version|v!" => \$opt_v,    # no arg
	    "dump|d=s"   => \$opt_d,    # string arg required
	    "raw!"       => \$opt_raw,  # no arg, no shortcut
	    "update|u=s" => \$opt_u,    # string arg required
	    "quiet|q!"   => \$opt_q,    # no arg
	    );

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;
}



# load the config file using the internal Conf module.
$conf = new Conf($configfile);
%config = $conf->getall();


# see, which algorithm we'll use
if ($config{use_algo} =~ /^(MD5|MD2|SHA1)$/) {
  $algo = "$config{use_algo}";
}
elsif ($config{use_algo}) {
  print "Unknown algorithm $config{use_algo}.\n";
  exit;
}
else {
  # the default
  $algo = "MD5";
}


# look if there were more args after parsing options
# and pass them to the update function, IF there were some
if ($opt_u) {
  my @u_files;
  if (@ARGV) {
    @u_files = @ARGV;
  }
  push @u_files, $opt_u;
  &update_file(@u_files);
  exit;
}

# 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";
}


# check the base database dir, create it if neccessary
if(!-x $config{db}->{basedir} && -e $config{db}->{basedir}) {
    die "permission denied: $config{db}->{basedir}\n";
}
elsif (!-d $config{db}->{basedir} && -e $config{db}->{basedir}) {
    die "$config{db}->{basedir} is not a directory!\n";
}
elsif (!-e $config{db}->{basedir}) {
    print STDERR "$config{db}->{basedir} does not exist. I create it for you.\n";
    mkdir $config{db}->{basedir}, oct(700) or die "Could not create $config{db}->{basedir}: $!\n";
}
chdir $config{db}->{basedir};



# check for per dir inheritance
# and set up default properties if nothing else specified
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_/);
	  next if($switch =~ /^chk_custom$/);
	  if (exists $config{directory}->{$dir}->{$switch} and
               $config{directory}->{$dir}->{$switch} !~ /^(1|on)$/) {
	      delete $config{directory}->{$dir}->{$switch};
	  }
	  else {
	    $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);
    }
}


# init mode (if -r or -i used)
if($Reset || $FirstTime) {
  if (-e "keydb") {
    # authentivate if the keydb already exists!
    &auth;
  }
  if ($config{db}->{protected}) {
    if (-e "keydb") {
      # we will create a new one later on
      unlink "keydb" or die "Could not remove keydb: $!\n";
    }
    print $separator, "\n";
    print "        Setting a new password for protected updates\n\n";
    &set_passwd;
  }
  else {
    if (-e "keydb") {
      # no more protection, but the keydb still exists, so remove it.
      unlink "keydb" or die "Could not remove keydb: $!\n";
    }
  }

  print $separator, "\n";

  print "        Resetting nabou's Databases\n" if($Reset);
  print "        Initializing nabou's Databases\n" if($FirstTime);

  print $underline, "\n";

  $FirstTime = 1;

  # remove the databases and swap files.
  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");
}



############################################
###              main                    ###
############################################
&verify_programs;

&compile_custom;

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

&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} || $config{check_files});
&update_dir_db       if($config{check_md5} || $config{check_files});

&check_diskusage     if($config{check_diskusage});


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


exit 0;

# the end of the script.














###############################################################
###                        subs                             ###
###############################################################



sub verify_programs {
  my(@dbcsumsize, %dbmisc, $mailprog, $crontab, $trans, $msg);
  $trans = new File;
  if((-l ($config{db}->{miscDB} . ".dir")) || (-l ($config{db}->{miscDB} . ".pag"))) {
    $msg .= "$config{db}->{miscDB} files exist as a link, and could be harmful if written to.\n";
  }
  if((-l ($config{db}->{pwdDB} . ".dir")) || (-l ($config{db}->{pwdDB} . ".pag"))) {
    $msg .= "$config{db}->{pwdDB} files exist as a link, and could be harmful if written to.\n";
  }
  if((-l ($config{db}->{sugidDB} . ".dir")) || (-l ($config{db}->{sugidDB} . ".pag"))) {
    $msg .= "$config{db}->{sugidDB} files exist as a link, and could be harmful if written to.\n";
  }
  if((-l ($config{db}->{csumDB} . ".dir")) || (-l ($config{db}->{csumDB} . ".pag"))) {
    $msg .= "$config{db}->{csumDB} files exist as a link, and could be harmful if written to.\n";
  }
  if((-l ($config{db}->{cronDB} . ".dir")) || (-l ($config{db}->{cronDB} . ".pag"))) {
    $msg .= "$config{db}->{cronDB} files exist as a link, and could be harmful if written to.\n";
  }


  # now check the files in miscDB
  dbmopen(%dbmisc, $config{db}->{miscDB}, 0600) || die "Can't open $config{db}->{miscDB}\: $!\n";

  $mailprog = new File($config{bin}->{sendmail});
  $trans->csv($dbmisc{$config{bin}->{sendmail}});
  if($mailprog->md5 ne $trans->md5 || $mailprog->mtime ne $trans->mtime) {
    if($FirstTime == 1) {
      print "Updating: $config{bin}->{sendmail}\n";
      $dbmisc{$config{bin}->{sendmail}} = $mailprog->csv;
    }
    else{
      $msg .= "$config{bin}->{sendmail}\'s file info has changed.  It's Possible this program\n"
	  ."has been tampered with.\n";
    }
  }
  $crontab = new File($config{bin}->{crontab});
  $trans->csv($dbmisc{$config{bin}->{crontab}});
  if($crontab->md5 ne $trans->md5 || $crontab->mtime ne $trans->mtime) {
    if($FirstTime == 1) {
      print "Updating: $config{bin}->{crontab}\n";
      $dbmisc{$config{bin}->{crontab}} = $crontab->csv;
    }
    else{
      $msg .= "$config{bin}->{crontab}\'s file info has changed.  It's Possible this program\n"
	  ."has been tampered with.";
    }
  }
  if ($config{db}->{protected}) {
    my $keyfile = $config{db}->{basedir} . "/keydb";
    my $keyobj   = new File($keyfile);
    $trans->csv($dbmisc{$keyfile});
    if ($keyobj->md5 ne $trans->md5 || $keyobj->mtime ne $trans->mtime) {
      if ($FirstTime == 1) {
	print "Updating: $keyfile\n";
	$dbmisc{$keyfile} = $keyobj->csv;
      }
      else {
	$msg .= "$keyfile\'s file info has changed.  It's Possible this program\n"
	  ."has been tampered with.";
      }
    }
  }
  if (($opt_q && $msg) || !$opt_q) {
    print $separator, "\n";
    print "     Verifying the stability of nabou\n";
    print $underline, "\n";
    print $msg . "\n";
  }
  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=$home\tSHELL=$shell\tPASSWD=$passwd\n";
  }
}




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




sub check_suid {
    &recurse_suid("/");
}


sub recurse_suid {
    my($dir) = @_;
    my($file);
    my $fh = new IO::Handle;
    opendir $fh, $dir or die "$!\n";
    my @allfiles = readdir($fh);
    closedir $fh;
    undef $fh;
    foreach my $file (sort @allfiles) {
        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;
        }
    }
}



sub update_suid_db {
  my(%dbsugid, $dbfile, $newfile, $msg);
  dbmopen(%dbsugid, $config{db}->{sugidDB}, 0600) or
    die "Can't open $config{db}->{sugidDB}: $!\n";
  $dbfile  = new File; # empty File objects for checking, see below.
  $newfile = new File;
  foreach my $file (sort keys %suidlist) {
    if(! $dbsugid{$file}) {
      $msg .= "$file:\tFile was not in the DataBase. [Adding...]\n";
      $dbsugid{$file} = $suidlist{$file};
      $msg .= &ShellChecksum($file);
    }
    elsif($dbsugid{$file} ne $suidlist{$file}) {
      $dbfile->csv($dbsugid{$file});
      $newfile->csv($suidlist{$file});
      foreach my $bit (sort keys %suid_mask) {
	  if($bit eq "md5" && $newfile->md5 ne $dbfile->md5) {
	      $msg .= "$file:\t ($algo checksum has changed)\n"
		   ."[Old] " . $dbfile->md5 . "\n[New] " . $newfile->md5. "\n\n";
	  }
	  elsif($bit eq "ino" && $newfile->ino ne $dbfile->ino) {
	      $msg .= "$file:\t (Inode has changed)\n"
		   ."[Old] " . $dbfile->ino . "\n[New] " . $newfile->ino . "\n\n";
	  }
	  elsif ($bit eq "dev" && $newfile->dev ne $dbfile->dev) {
	      $msg .= "$file:\t (Filesystem device number has changed)\n"
		   ."[Old] " . $dbfile->dev . "\n[New] " . $newfile->dev . "\n\n";
	  }
	  elsif ($bit eq "mode" &&  $newfile->mode ne $dbfile->mode) {
	      my $oldmode = sprintf("%04o", $dbfile->mode & 07777);
	      my $newmode = sprintf("%04o", $newfile->mode & 07777);
	      $msg .= "$file:\t (File mode has changed)\n"
		   ."[Old] $oldmode\n[New] $newmode\n\n";
	  }
	  elsif ($bit eq "nlink" && $newfile->nlink ne $dbfile->nlink) {
	      $msg .= "$file:\t (Number of links to this file has changed)\n"
		   ."[Old] " . $dbfile->nlink . "\n[New] " . $newfile->nlink . "\n\n";
	  }
	  elsif ($bit eq "uid" && $newfile->uid ne $dbfile->uid) {
	      my $olduser = getpwnam($dbfile->uid);
	      my $newuser = getpwnam($newfile->uid);
	      $msg .= "$file:\t (Owner has changed)\n"
		   ."[Old] $olduser\n[New] $newuser\n\n";
	  }
	  elsif ($bit eq "gid" && $newfile->gid ne $dbfile->gid ) {
	      my $olduser = getgrgid($dbfile->gid);
	      my $newuser = getgrgid($newfile->gid);
	      $msg .= "$file:\t (Group has changed)\n"
		   ."[Old] $olduser\n[New] $newuser\n\n";
	  }
	  elsif ($bit eq "size" && $newfile->size ne $dbfile->size) {
	      $msg .= "$file:\t (Size has changed)\n"
		   ."[Old] " . $dbfile->size . " bytes\n[New] " . $newfile->size . " bytes\n\n";
	  }
	  elsif ($bit eq "mtime" && $newfile->mtime ne $dbfile->mtime) {
	      $msg .= "$file:\t (Modification time has changed)\n"
		   ."[Old] \"" . scalar localtime($dbfile->mtime)
		   ."\"\n[New] \"" . scalar localtime($newfile->mtime) . "\"\n\n";
	  }
	  elsif ($bit eq "ctime" && $newfile->ctime ne $dbfile->ctime) {
	      $msg .= "$file:\t (Inode change time has changed)\n"
		   ."[Old] \"" . scalar localtime($dbfile->ctime)
		   ."\"\n[New] \"" . scalar localtime($newfile->ctime) . "\"\n\n";
	  }
	  elsif ($bit eq "blocks" && $newfile->blocks ne $dbfile->blocks) {
	      $msg .= "$file:\t (Number of allocated blocks has changed)\n"
	           ."[Old] " . $dbfile->blocks . " blocks\n[New] " . $newfile->blocks . " blocks\n\n";
	  }
      }
      $dbsugid{$file} = $suidlist{$file};
    }
  }

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

  if (($opt_q && $msg) || !$opt_q) {
    print $separator, "\n";
    print "    Changes in suid/sgid files\n";
    print $underline, "\n";
    print $msg . "\n";
  }
}



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}) {
      return "Warning:\t$file has the same checksum as $shell\!\n";
    }
  }
}




sub check_directories {
  my(@exclude, @include, @custom, %mask, $msg);
  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_/);
	next if($bit =~ /^chk_custom$/);
	my $msk = $config{directory}->{$csdir}->{$bit};
	$bit =~ s/^chk_//;
	$mask{$bit} = $msk if($msk);
    }
    my $custom = $config{directory}->{$csdir}->{chk_custom};
    if ($custom) {
      if (ref($custom) eq "ARRAY") {
	foreach (@{$custom}) {
	  push @custom, $_;
	}
      }
      else {
	@custom = ($custom) if($custom);
      }
      # add the custom script names as bits to %mask
      foreach my $name (@custom) {
	$mask{"custom_$name"} = 1;
      }
    }

    if (@include) {
	# process only the specified filez
	$config{directory}->{$csdir}->{du} =
	  &process_includes(\%mask, \@include, $csdir);
    }
    else {
	# go through all filez
	$config{directory}->{$csdir}->{du} =
	  &recurse_dirs($csdir, \%mask, \@exclude, $config{directory}->{$csdir}->{recursive});
    }
    $msg .= "\n  => $csdir\n" if(!$opt_q || ($opt_q && $dir_msg));
    $msg .= $dir_msg;
    $dir_msg = "";
  }
  if (!$opt_q || ($opt_q && $msg)) {
    print $separator . "\n";
    print "    Changed files in monitored dirs\n";
    print $underline . "\n";
    print $msg . "\n";
  }
}

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

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


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


sub CheckChange {
  my($file, $mask, $dir) = @_;
  my(%dbcsumlist, $dbfile, $newfile);
  dbmopen(%dbcsumlist, $config{db}->{csumDB}, 0600) or
                          die "Can't open $config{db}->{csumDB}\: $!\n";
  $dbfile  = new File; # empty File objects for checking, see below.
  $newfile = new File;
  if(! $dbcsumlist{$file}) {
      $dir_msg .= "$file:\tFile was not in the DataBase. [Adding...]\n";
      $dbcsumlist{$file} = $ncsumlist{$file};
  }
  elsif($dbcsumlist{$file} ne $ncsumlist{$file}) {
      $dbfile->csv($dbcsumlist{$file});
      $newfile->csv($ncsumlist{$file});
      foreach my $bit (sort keys %{$mask}) {
	  if($bit eq "md5" && $newfile->md5 ne $dbfile->md5) {
	       $dir_msg .= "$file:\t ($algo checksum has changed)\n"
		          ."[Old] " . $dbfile->md5 . "\n[New] " . $newfile->md5. "\n\n";
	  }
	  elsif($bit eq "ino" && $newfile->ino ne $dbfile->ino) {
	      $dir_msg .= "$file:\t (Inode has changed)\n"
		         ."[Old] " . $dbfile->ino . "\n[New] " . $newfile->ino . "\n\n";
	  }
	  elsif ($bit eq "dev" && $newfile->dev ne $dbfile->dev) {
	      $dir_msg .= "$file:\t (Filesystem device number has changed)\n"
		         ."[Old] " . $dbfile->dev . "\n[New] " . $newfile->dev . "\n\n";
	  }
	  elsif ($bit eq "mode" &&  $newfile->mode ne $dbfile->mode) {
	      my $oldmode = sprintf("%04o", $dbfile->mode & 07777);
	      my $newmode = sprintf("%04o", $newfile->mode & 07777);
	      $dir_msg .= "$file:\t (File mode has changed)\n"
		         ."[Old] $oldmode\n[New] $newmode\n\n";
	  }
	  elsif ($bit eq "nlink" && $newfile->nlink ne $dbfile->nlink) {
	      $dir_msg .= "$file:\t (Number of links to this file has changed)\n"
		         ."[Old] " . $dbfile->nlink . "\n[New] " . $newfile->nlink . "\n\n";
	  }
	  elsif ($bit eq "uid" && $newfile->uid ne $dbfile->uid) {
	      my $olduser = getpwnam($dbfile->uid);
	      my $newuser = getpwnam($newfile->uid);
	      $dir_msg .= "$file:\t (Owner has changed)\n"
		         ."[Old] $olduser\n[New] $newuser\n\n";
	  }
	  elsif ($bit eq "gid" && $newfile->gid ne $dbfile->gid ) {
	      my $olduser = getgrgid($dbfile->gid);
	      my $newuser = getgrgid($newfile->gid);
	      $dir_msg .= "$file:\t (Group has changed)\n"
		         ."[Old] $olduser\n[New] $newuser\n\n";
	  }
	  elsif ($bit eq "size" && $newfile->size ne $dbfile->size) {
	      $dir_msg .= "$file:\t (Size has changed)\n"
		         ."[Old] " . $dbfile->size . " bytes\n[New] " . $newfile->size . " bytes\n\n";
	  }
	  elsif ($bit eq "mtime" && $newfile->mtime ne $dbfile->mtime) {
	      $dir_msg .= "$file:\t (Modification time has changed)\n"
		         ."[Old] \"" . scalar localtime($dbfile->mtime)
		         ."\"\n[New] \"" . scalar localtime($newfile->mtime) . "\"\n\n";
	  }
	  elsif ($bit eq "ctime" && $newfile->ctime ne $dbfile->ctime) {
	      $dir_msg .= "$file:\t (Inode change time has changed)\n"
		         ."[Old] \"" . scalar localtime($dbfile->ctime)
		         ."\"\n[New] \"" . scalar localtime($newfile->ctime) . "\"\n\n";
	  }
	  elsif ($bit eq "blocks" && $newfile->blocks ne $dbfile->blocks) {
	      $dir_msg .= "$file:\t (Number of allocated blocks has changed)\n"
	                 ."[Old] " . $dbfile->blocks . " blocks\n[New] " . $newfile->blocks . " blocks\n\n";
	  }
	  else {
	    # yes there could be a custom bit
	    if ($bit =~ /^custom_(.*)$/) {
	      my $name = $1;
	      # call the closure.
	      $dir_msg .= &{$config{custom}->{$name}}($newfile, $dir) if($name);
	    }
	  }
      }
      $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{
  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{
  my($msg);
  dbmopen(%dbcronlist, $config{db}->{cronDB}, 0600) || die "Can't open $config{db}->{cronDB}\: $!\n";
  foreach my $login (sort keys %cronlist) {
    if(! $dbcronlist{$login}) {
      $msg .= "$login\:\tAccount was not in the DataBase. [Adding...]\n";
      $dbcronlist{$login} = $cronlist{$login};
    }elsif($dbcronlist{$login} ne $cronlist{$login}) {
      $msg .= "$login\:\tCrontab has changed.\n"
            . "[Old Crontab]\n$dbcronlist{$login}"
            . "[New Crontab]\n$cronlist{$login}";
      $dbcronlist{$login} = $cronlist{$login};
    }
  }
  foreach my $login (sort keys %dbcronlist) {
    if(! $cronlist{$login}) {
      $msg .= "$login\:\tAccount was not found. [Removing...]\n";
      delete($dbcronlist{$login});
    }
  }
  dbmclose(%dbcronlist);
  undef %dbcronlist;
  undef %cronlist;
  if (!$opt_q || ($opt_q && $msg)) {
    print $separator, "\n";
    print "    Changes in user crontabs\n";
    print $underline, "\n";
    print $msg . "\n";
  }
}


sub check_diskusage {
  my(%dudb, $msg);
  dbmopen(%dudb, $config{db}->{diskusageDB}, 0600) or
    die "Can't open $config{db}->{diskusageDB}: $!\n";

  foreach my $dir (sort keys %{$config{directory}}) {
    my $cursize   = $config{directory}->{$dir}->{du};
    my $dbsize    = $dudb{$dir};
    my $overflow  = $config{directory}->{$dir}->{du_increase} || 10;
    my $underflow = $config{directory}->{$dir}->{du_decrease} || 10;
    if ($cursize > $dbsize) {
      my $diff = $cursize - $dbsize;
      my $eins = $dbsize / 100;
      my $prozent = int($diff / $eins) if ($eins != 0);
      if ($prozent >= $overflow) {
	$msg .= "+ $dir: disk usage has increased over $overflow%\n"
	     ."Old Usage: $dbsize bytes, New Usage: $cursize bytes\n\n";
      }
    }
    elsif ($cursize < $dbsize) {
      my $diff = $dbsize - $cursize;
      my $eins = $dbsize / 100;
      my $prozent = int($diff / $eins) if ($eins != 0);
      if ($prozent >= $underflow) {
	$msg .= "- $dir: disk usage has decreased under $underflow%\n"
	     ."Old Usage: $dbsize bytes, New Usage: $cursize bytes\n\n";
      }
    }
    $dudb{$dir} = $cursize;
  }
  if (!$opt_q || ($opt_q && $msg)) {
    print $separator, "\n";
    print "    Changes in directory disk usage\n";
    print $underline, "\n";
    print $msg . "\n";
  }
}



sub dump {
  my($db, $raw) = @_;
  my %database;
  my $c = ",";
  my $trans = new File;
  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 {
      $trans->csv($database{$file});
      print $trans->md5 . $c . $trans->dev . $c . $trans->ino . $c;
      print sprintf("%04o", $trans->mode & 07777);
      print $c . $trans->nlink . $c;
      print getpwuid($trans->uid) . $c;
      print getgrgid($trans->gid) . $c;
      print $trans->rdev . $c . $trans->size . $c;
      print scalar localtime($trans->atime);
      print $c;
      print scalar localtime($trans->mtime);
      print $c;
      print scalar localtime($trans->ctime);
      print $c . $trans->blksize . $c . $trans->blocks;
      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"
       ."-u --update <file> update database entry of <file>\n"
       ."-q --quiet         show only changes, otherwise be quiet\n"
       ."-h --help          show this message\n"
       ."-v --version       show version number\n"
       ."$0 with no options is normal operation mode\n";
  exit;
}



sub set_passwd {
  my ($key, $key1, $key2, $crypted_key, %keydb);
  print STDERR "password: ";
  $key1 = &get_passwd;
  print STDERR "repeat:   ";
  $key2 = &get_passwd;

  if ($key1 ne $key2) {
    print STDERR "Passwords are not identical. Please try again!\n";
    exit 1;
  }
  else {
    $key = $key1;
    # encrypt the key
    my @range=('0'..'9','a'..'z','A'..'Z');
    my $salt=$range[rand(int($#range)+1)] . $range[rand(int($#range)+1)];
    $crypted_key = crypt($key, "$salt");
  }

  # store it into the key db:
  dbmopen(%keydb, $config{db}->{basedir} . "/keydb", 0600) or
    die "Can't open " . $config{db}->{basedir} . "/keydb: $!\n";
  $keydb{root} = $crypted_key;

  dbmclose(%keydb);
}




sub get_passwd {
  #
  # get a password without echo
  #
  my $key;
  eval {
    local($|) = 1;
    local(*TTY);
    open(TTY,"/dev/tty");
    system ("stty -echo </dev/tty");
    chomp($key = <TTY>);
    print STDERR "\r\n";
    system ("stty echo </dev/tty");
    close(TTY);
  };
  if ($@) {
    $key = <>;
  }
  chomp $key;
  return $key;
}



sub auth {
  my(%keydb, $salt, $key);
    if (!exists $ENV{'NABOU_PASSWD'}) {
      print STDERR "password: ";
      $key = &get_passwd;
    }
    else {
      $key = $ENV{'NABOU_PASSWD'};
    }
    chomp $key;

    dbmopen(%keydb, $config{db}->{basedir} . "/keydb", 0600) or
      die "Can't open " . $config{db}->{basedir} . "/keydb: $!\n";

    # compare them
    if ($keydb{root} =~ /^(..).*/ && exists $keydb{root}) {
      $salt = $1;
    }
    else {
      print STDERR $config{db}->{basedir} . "/keydb does not contain an encrypted key for root!\n";
      exit 1;
    }

    # encrypt the key and compare the result
    my $crypted_key = crypt($key, "$salt");

    if ($crypted_key ne $keydb{root}) {
      print STDERR "permission denied.\n";
      exit 1;
    }
    dbmclose(%keydb);
}



sub update_file {
  my (@files) = @_;
  my(%db);
  my $sp = " " if($algo =~ /^MD/);
  if ($config{db}->{protected} || -e $config{db}->{basedir} . "/keydb") {
    &auth;
  }

  my $curdir = `pwd`;
  my $db = $config{db}->{basedir} . "/" . $config{db}->{csumDB};
  dbmopen(%db, $db, 0600) or
    die "Can't open $db: $!\n";

  foreach my $file (@files) {
    # prepend curdir if not absolute filename
    chomp $file;
    chomp $curdir;
    if ($file !~ /^\//) {
      $file = $curdir . "/" . $file;
    }
    print "         Filename: " . $file . "\n";
    if (-e $file) {
      print "           Status: ";
      my $obj = new File($file);
      if (!exists $db{$file}) {
	print "not in the DataBase. [Adding...]\n";
      }
      else {
	print "exists in the DataBase. [Updating...]\n";
      }
      $db{$file} = $obj->csv;
      print "$sp    $algo checksum: " . $obj->md5 . "\n";
      print "             Mode: " . sprintf("%04o", $obj->mode & 07777) . "\n";
      print "            Owner: " . getpwuid($obj->uid) . "\n";
      print "            Group: " . getgrgid($obj->gid) . "\n";
      print "             Size: " . $obj->size . " bytes\n";
      print "      Access Time: " . scalar localtime($obj->atime) . "\n";
      print "Modification Time: " . scalar localtime($obj->mtime) . "\n";
      print "Inode Change Time: " . scalar localtime($obj->ctime). "\n" ;
      print "\n";
    }
    else {
      print "           Status: was not found or no more being monitored. [Removing...]\n";
      delete $db{$file};
    }
  }
  dbmclose(%db)
}




sub compile_custom {
  #
  # yo - guys, now we create an anonymous sub
  # save a closure to this in $config{code}->{scriptname}
  # using perls magic eval.
  # hell, I love perl!
  #
  foreach my $name (keys %{$config{script}}) {
    if ($config{script}->{$name}->{perl}) {
      my $rawcode = $config{script}->{$name}->{perl};
      my $code    = "\$config{custom}->{$name} = sub { $rawcode }";
      eval $code;
    }
  }
}

#########################################################################################
# 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
  if ($file) {
    $self->_stats;
    $self->_md5;
  }
  # else empty file object.
  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, $csv) = @_;
  if (!$csv) {
    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;
  }
  else {
    # initialize $this from given $csv
    my @ar = split /:/, $csv;
    my %stats = (
	md5     => $ar[0],
      	dev	=> $ar[1],
	ino	=> $ar[2],
	mode	=> $ar[3],
	nlink	=> $ar[4],
	uid	=> $ar[5],
	gid	=> $ar[6],
	rdev	=> $ar[7],
	size	=> $ar[8],
	atime	=> $ar[9],
	mtime	=> $ar[10],
	ctime	=> $ar[11],
	blksize	=> $ar[12],
	blocks	=> $ar[13],
	);
    $this->{stats} = \%stats;
    return $csv;
  }
}


sub _md5 {
        my($this) = @_;
	if ($algo eq "MD2") {
	  $md5 = new Digest::MD2;
	}
	elsif ($algo eq "SHA1") {
	  $md5 = new Digest::SHA1;
	}
	else {
	  $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 filename {
  my($this) = @_;
  return $this->{file};
}



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} : "";
}
