#!SPPSYM_PERL_CMD
#spp comment vim: set filetype=perl: #
#spp include ../bldcfg/paths.spp
use strict; 
#  leave a standard path unless there is some reason not to
$ENV{'PATH'} = '/bin:/usr/bin:/sbin:/usr/sbin';
($main::progname = $0) =~ s/^.*\/([^\/]+)/$1/;
$main::svn_id = '$HeadURL$ $LastChangedRevision$';
$main::version_scheme = 'release';
$main::release_version = 'SPPSYM_RELEASE_ID';
my($tmp_dir) = ($ENV{'TMPDIR'}) ? $ENV{'TMPDIR'} : "SPPSYM_TMPDIR";

#  Defaults for options
my($dflt_cmpext) = "SPPSYM_FADSCANCMPEXT";
undef my($dflt_mode);
my($dflt_sttdir) = "SPPSYM_STTDIR/fadscan";
my($dflt_cfgdir) = "SPPSYM_CFGDIR/fadscan";
my($dflt_logdir) = "SPPSYM_LOGDIR/fadscan";
undef my(@dflt_mailto);
my($dflt_nocrcs) = 0;
my($dflt_simulate) = 0;

#  Option variables (needed at global level 'cos used in path reporting function)
undef my($opt_cmpext);
undef my($opt_mode);
undef my($opt_sttdir);
undef my($opt_cfgdir);
undef my($opt_logdir);
undef my(@opt_mailto);
undef my($opt_nocrcs);
undef my($opt_simulate);

#  Other globals
#  Extra perl library directories
#  Are there any directories where other libraries are to be found? Just use
#  a colon separated list of directories.
my($extra_perllib_dirs) = "SPPSYM_EXTRAPERLLIBDIRS";
#  Important bits
push(@INC,split(/[:\s]+/,$extra_perllib_dirs));
require File::Find;
require Sys::Hostname;
my(@find_roots, $fileset);
my($total_lines, $del_cnt, $own_cnt, $grp_cnt, $mde_cnt, $typ_cnt, $lnk_cnt);
my($crc_cnt, $mmn_cnt, $add_cnt, $sym_cnt, $schedflag_file); 
#  The names of the days and monthes, used in date conversion
my(@day_names)  = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
#  Log and state files should not be readable by common users
$main::umask = 077;

sub main
{
    my(@ARGV) = @_;
    my($rc, $confirm, $cmd, $pars, $lc_dayname);
    my($oldfad_stem, $oldfad_compext, $log_file, $config_file);
    my(@find_filters, %headers, $combined_scan_args);
    my($listpaths, $optval);

    ##########################################################################
    #
    #  PROCESS OPTIONS
    #
    ##########################################################################

    #
    #  Load option values from command line
    #
    while (defined($ARGV[0]) && $ARGV[0] =~ /^-./) {
        $_ = $ARGV[0];

        #  standard options
        if (/^(?:-V|--version)$/) {
            &ade_msg_version(\&app_version);

        } elsif (/^(-d$|--debug=)(.*)/) {
            $main::verboselevel = ($1 !~ /^-[^-]/) ? $2 : ((defined($ARGV[1])) ? scalar($optval=$ARGV[1],shift @ARGV,$optval) : &ade_msg_usage(\&app_usage, 1));

        } elsif (/^(?:-v|--verbose)$/) {
            $main::verboselevel = 3;

        } elsif (/^(?:-h|--help)$/) {
            &ade_msg_usage(\&app_usage, 0);

        } elsif (/^(?:-p|--list-paths)$/) {
            $listpaths = 1;

        #  -i is operate in "init" mode
        } elsif (/^(-i$|--mode-init$)/) {
            $opt_mode = "init";

        #  -c is operate in "check" mode
        } elsif (/^(-c$|--mode-check$)/) {
            $opt_mode = "check";

        #  -r is operate in "refresh" mode
        } elsif (/^(-r$|--mode-refresh$)/) {
            $opt_mode = "refresh";

        #  -s is operate in "schedule" mode
        } elsif (/^(-s$|--mode-schedule$)/) {
            $opt_mode = "schedule";

 	#  --config-dir is specify an alternative base directory for fadscan files
        } elsif (/^--config-dir=(.*)/) {
            $opt_cfgdir = $1;

	#  --log-dir is specify an alternative logs directory
        } elsif (/^--log-dir=(.*)/) {
            $opt_logdir = $1;

 	#  --state-dir is specify an alternative state directory 
        } elsif (/^--state-dir=(.*)/) {
            $opt_sttdir = $1;

	#  --gzip is gzip all snapshot and log files
        } elsif (/^--gzip$/) {
            $opt_cmpext = ".gz";

	#  --compress is compress all snapshot and log files
        } elsif (/^--compress$/) {
            $opt_cmpext = ".Z";

        #  --no-compress is overrule any defaults and don't compress the snapshots or logs
        } elsif (/^--no-compress$/) {
            $opt_cmpext = "";

	# -m is mail the report to alternative recipients
        } elsif (/^(-m$|--mail=)(.*)/) {
            &ade_msg_debug(4, "main: \$1=$1, \$2=$2");
            push(@opt_mailto, ($1 !~ /^-[^-]/) ? $2 : ((defined($ARGV[1])) ? scalar($optval=$ARGV[1],shift @ARGV,$optval) : &ade_msg_usage(\&app_usage, 1)));
            &ade_msg_debug(4, sprintf("main: \$optval=$optval, \@opt_mailto=(%s)", join(" ", @opt_mailto)));

        #  -n is simulate
        } elsif (/^(-n$|--simulate$)/) {
            $opt_simulate = 1;

        #  --suppress-crcs is no crcs in fad db
        } elsif (/^(--suppress-crcs$)/) {
            $opt_nocrcs = 1;

        } elsif (/^--$/) {
            shift @ARGV;
	    last;

        } else {
            &ade_msg_usage(\&app_usage, 1);
        }

        shift @ARGV;
    }

    #
    #  Load option values from environment
    #
    $opt_cfgdir = $ENV{'FADSCAN_CFGDIR'} if (!defined($opt_cfgdir) && $ENV{'FADSCAN_CFGDIR'});
    $opt_logdir = $ENV{'FADSCAN_LOGDIR'} if (!defined($opt_logdir) && $ENV{'FADSCAN_LOGDIR'});
    $opt_sttdir = $ENV{'FADSCAN_STTDIR'} if (!defined($opt_sttdir) && $ENV{'FADSCAN_STTDIR'});
    $opt_cmpext = $ENV{'FADSCAN_CMP_EXT'} if (!defined($opt_cmpext) && $ENV{'FADSCAN_CMP_EXT'});

    #
    #  Load option values from config file
    #

    #  (not used for this program)

    #
    #  hard-coded defaults
    #
    $opt_cmpext   = $dflt_cmpext   if (!defined($opt_cmpext));
    $opt_mode     = $dflt_mode     if (!defined($opt_mode));
    $opt_sttdir   = $dflt_sttdir   if (!defined($opt_sttdir));
    $opt_cfgdir   = $dflt_cfgdir   if (!defined($opt_cfgdir));
    $opt_logdir   = $dflt_logdir   if (!defined($opt_logdir));
    @opt_mailto   = @dflt_mailto   if (!defined(@opt_mailto));
    $opt_nocrcs   = $dflt_nocrcs   if (!defined($opt_nocrcs));
    $opt_simulate = $dflt_simulate if (!defined($opt_simulate));

    #
    #  Sanity check and option rewrites
    #
    (!$listpaths) && (!defined($opt_mode)) && &ade_msg_usage(\&app_usage, 1);;

    ##########################################################################
    #
    #  ARGUMENT PROCESSING
    #
    ##########################################################################

    &ade_msg_listpaths(\&app_listpaths) if ($listpaths);
    (!$ARGV[0] || $ARGV[1]) && &ade_msg_usage(\&app_usage, 1);
    $fileset = $ARGV[0];

    ##########################################################################
    #
    #  FORKING AND LOCKING
    #
    ##########################################################################

    #  (not needed for this program)

    ##########################################################################
    #
    #	GUTS START HERE
    #
    ##########################################################################

    (-d $opt_cfgdir) || &ade_msg_error("can't access $opt_cfgdir");
    (-d $opt_logdir) || &ade_msg_error("can't access $opt_logdir");
    (-d $opt_sttdir) || &ade_msg_error("can't access $opt_sttdir");

    #  Set some global variables that we'll want to access everywhere.
    $main::hostname = &Sys::Hostname::hostname;
    $main::hostname || &ade_msg_error("can't determine hostname");
    ($main::nowsec, $main::nowmin, $main::nowhour, $main::nowmday, $main::nowmon, $main::nowyear, $main::nowwday, $main::nowyday, $main::nowisdst) = localtime;
    $oldfad_stem = "$opt_sttdir/$fileset-basesnap";
    $oldfad_compext = $opt_cmpext;
    ($lc_dayname = $day_names[$main::nowwday]) =~ tr/A-Z/a-z/;
    $log_file = "$opt_logdir/$fileset-$lc_dayname$opt_cmpext";
    $config_file = "$opt_cfgdir/$fileset.conf";
    $schedflag_file = "$opt_sttdir/$fileset-scheduled";
    
    #  Read in config data
    (-f $config_file) || &ade_msg_error("$config_file: can't access");
    if ($opt_mode ne "schedule") {
        &load_config_file($config_file, \@find_filters, \@find_roots, undef);
        &ade_msg_error("no roots supplied") if (!(@find_roots));
    }

    $rc = 0;

    #  ade_spc_rollingstatusmanager() only allows one parameter to be passed through, and we
    #  need to pass two. So here we squeeze them both into one.
    $combined_scan_args = { 
        findfilters_ref => \@find_filters, 
        findroots_ref => \@find_roots 
    };

    #  And here we make do all the work.
    &ade_tmp_register($log_file);
    if (($rc=&ade_spc_rollingstatusmanager($opt_mode, 
                                           $schedflag_file, 
                                           \&fadscan_scan_cb, 
                                           \&fadscan_comp_cb, 
					   undef,
                                           $oldfad_stem, 
                                           $oldfad_compext, 
                                           $log_file, 
                                           $combined_scan_args,
	                                   $tmp_dir,
					   1)) != 0) {
        return($rc);
    }
    #  No need to delete it we get this far.
    &ade_tmp_deregister($log_file);

    #  Mail the log file if necessary.
    if (($opt_mode eq "check" || $opt_mode eq "refresh") && defined(@opt_mailto)) {
        &ade_msg_debug(4, "main: opening compressed log file ...");
        if (($rc=&ade_fcm_openreadcompressed($log_file, \*LOG_HANDLE)) != 0) {
            &ade_msg_errorerror("can't open $log_file");
            return($rc);
        }
        %headers = ();
        &ade_msg_debug(4, "main: emailing log file ...");
        &ade_mua_addmailheader(\%headers, "Subject: Fadscan ($main::hostname $fileset $day_names[$main::nowwday])");
        if (($rc=&ade_mua_sendmail(\%headers, \*LOG_HANDLE, \@opt_mailto, $opt_simulate)) != 0) {
            return($rc);
        }
        close LOG_HANDLE;
    }
    
    return(0);
}

sub fadscan_comp_cb
{
    my ($oldfad_file, $newfad_file, $rollno) = @_;
    my ($rc, $fmtdatestart, $fmtdateend);
    my (%old_fad_headers, %headers);
    my (%attribs, %names, %madeupdevinodes, $diff_rc);

    #  Get the time of the report.
    $fmtdatestart = localtime;

    #  Open the two input files 
    if (($rc=&ade_fcm_openreadcompressed($oldfad_file, \*OLDFAD_HANDLE)) != 0) {
        &ade_msg_errorerror("can't open $oldfad_file");
        return($rc);
    }
    if (($rc=&ade_fcm_openreadcompressed($newfad_file, \*NEWFAD_HANDLE)) != 0) {
        &ade_msg_errorerror("can't open $newfad_file");
        return($rc);
    }
    
    #  Read the headers - saving valuable info from the old headers.
    if (($rc=&read_fad_header(\*OLDFAD_HANDLE, \%old_fad_headers)) != 0) {
        &ade_msg_errorerror("bad FAD header in old fad file");
        return($rc);
    }
    if (($rc=&read_fad_header(\*NEWFAD_HANDLE, undef)) != 0) {
        &ade_msg_errorerror("bad FAD header in new fad file");
        return($rc);
    }

    #  Write a pretty header to the report
    print "SECURITY VALIDATION PROCESS\n";
    print "===========================\n";
    print "\n";
    print "HOST:                                      $main::hostname\n";
    print "FILESET:                                   $fileset\n";
    print "START DATE:                                $fmtdatestart\n";
    print "LAST REFRESH DATE:                         " . 
            localtime($old_fad_headers{'Unix-Time'}) . "\n";
    print "FADSCAN:                                   $main::version\n";
    print "\n";

    #  Write the title of the differences report
    print "FILE BY FILE\n";
    print "------------\n";
    print "\n"; 
    
    #  Initialise some counters that are used to generate a report summary
    $total_lines = 0;
    $del_cnt = 0; 
    $own_cnt = 0; 
    $grp_cnt = 0; 
    $mde_cnt = 0;  
    $typ_cnt = 0;  
    $lnk_cnt = 0;
    $crc_cnt = 0; 
    $mmn_cnt = 0; 
    $add_cnt = 0;   
    $sym_cnt = 0;

    #  Actually work out the differences
    if (($rc=&fadstreams_to_diffstream(\*OLDFAD_HANDLE, \*NEWFAD_HANDLE, \&fsscancallback, \*STDOUT, \$diff_rc)) != 0) {
        close NEWFAD_HANDLE;
        close OLDFAD_HANDLE;
        &ade_msg_error("failed to work out differences");
    }

    #  We've finished reading the fad files now.
    close NEWFAD_HANDLE;
    close OLDFAD_HANDLE;
    print "\n"; 


    #  Do some summary information here in the report
    print "SUMMARY\n";
    print "-------\n";
    print "\n";
    print "Total files changed:                       $total_lines\n";
    print "Added:                                     $add_cnt\n";
    print "Deleted:                                   $del_cnt\n";
    print "Owner changed:                             $own_cnt\n";
    print "Group changed:                             $grp_cnt\n";
    print "Mode changed:                              $mde_cnt\n";
    print "Links changed:                             $lnk_cnt\n";
    print "Checksum changed:                          $crc_cnt\n"; 
    print "Type changed:                              $typ_cnt\n"; 
    print "Symlink dest changed:                      $sym_cnt\n"; 
    print "Major minor no. changed:                   $mmn_cnt\n"; 
    print "\n";
    $fmtdateend = localtime;
    print "END DATE:                                  $fmtdateend\n";
    #  We've finished writing to the report

    #  Superfluous really.
    undef %attribs;
    undef %names;
    undef %madeupdevinodes;

    return(0);
}

sub fadscan_scan_cb
{
    my ($combined_scan_args) = @_;
    my ($rc, $findroots_ref, $findfilters_ref);
    my (%attribs, %names, %madeupdevinodes);

    #  ade_spc_rollingstatusmanager() only allowed us to pass one argument through, but we needed
    #  to pass two, so in main() we squeezed the two into one. Now we get the 
    #  two out again.
    $findroots_ref   = $combined_scan_args->{findroots_ref};
    $findfilters_ref = $combined_scan_args->{findfilters_ref};

    #  Do the scan to memory.
    &findhier_to_fadstore($findfilters_ref, $findroots_ref, \%attribs, \%names, 
            \%madeupdevinodes, $opt_nocrcs);

    #  Dump memory to stdout, which has been redirected by ade_spc_rollingstatusmanager()
    if (($rc=&fadstore_to_fadstream($errstack_ref, \*STDOUT, \%attribs, \%names, \%madeupdevinodes)) != $ade_err_ok) {
        return($rc);
    }

    return($ade_err_ok);
}

sub app_version
{
    my($version_ref) = @_;

    ${$version_ref} = $main::version;

    return(0);
}

sub app_usage
{
    print "Usage:   $main::progname { -i | -c | -r | -s } [ <options> ] <scanid>\n";
    print "\n";
    #  standard options
    print "Options: -V | --version               display program version\n";
    print "         -v | --verbose               verbose\n";
    print "         -d | --debug=<level>         set debug level\n";
    print "         -h | --help                  display this text\n";
    print "         -p | --list-paths            list paths used by this program\n";
    #  application-specific option
    print "         -i | --mode-init             operate in 'initialise' mode\n";
    print "         -c | --mode-check            operate in 'check' mode\n";
    print "         -r | --mode-refresh          operate in 'refresh' mode\n";
    print "         -s | --mode-schedule         operate in 'schedule' mode\n";
    print "              --config-dir=<cfgdir>   use alternate config directory\n";
    print "              --log-dir=<logdir>      use alternate log directory\n";
    print "              --state-dir=<sttdir>    use alternate state info directory\n";
    print "              --gzip                  compress files with gzip (default)\n";
    print "              --compress              compress files with compress\n";
    print "              --no-compress           do not compress files\n";
    print "         -m | --mail=<addr>           mail logs to <addr>\n";
    print "         -n | --simulate              do not do anything for real\n";
    print "              --suppress-crcs         do not calculate CRCs\n";
    print "\n";

    return(0);
}

sub app_listpaths
{

    return(0);
}

###############################################################################
#
#   function:           &fadscan_check
#
#   description:        builds a temporary snapshot file, compares it with
#                       and older snapshot file.
#
#   parameters:         none
#
#   returns:            return code not used
#
###############################################################################

sub fadscan_check
{
    #  then we need to tidy up.
    return 0;
}

###############################################################################
#
#   function:           &fsscancallback
#
#   description:        called by &fadstreams_to_diffstream to report changes 
#                       between the previous snapshot file and the current one.
#
#   parameters:         $outhandle - file handle for report
#                       $file      - name of a file which has changed from
#                                    one snapshot to the next
#                       @changes   - array of joined change-types, old value
#                                    and new values
#   
#   returns:            return code not used
#
###############################################################################

sub fsscancallback
{
    my($outhandle, $file, @changes) = @_;
    my($change, $oldstate, $newstate, $changetype);
    my($txt_typ, $txt_own, $txt_grp, $txt_mde, $txt_add, $txt_del, $txt_mmn, $txt_crc, $txt_lnk, $txt_sym);

    #  Each output line will consist of all of these fields but with possibly
    #  a couple of them not blank.
    $txt_typ = "   ";
    $txt_own = "   ";
    $txt_grp = "   ";
    $txt_mde = "   ";
    $txt_add = "   ";
    $txt_del = "   ";
    $txt_mmn = "   ";
    $txt_crc = "   ";
    $txt_lnk = "   ";
    $txt_sym = "   ";

    #  Ok we are processing another file
    $total_lines++;

    #  Now analyse the changes that are in @changes
    foreach $change (@changes) {
        
        $changetype = $change->{change_type};
        $oldstate = $change->{old_value};
        $newstate = $change->{new_value};

	#  Now turn on the appropriate bits of text
        &ade_msg_debug(4, "fadscan_cb: got $changetype $oldstate $newstate");
        if ($changetype eq $main::CT_OWNER) {
            $own_cnt++;
            $txt_own = "own";
        } elsif ($changetype eq $main::CT_GROUP) {
            $grp_cnt++;
            $txt_grp = "grp";
        } elsif ($changetype eq $main::CT_MODE) {
            $mde_cnt++;
            $txt_mde = "mde";
        } elsif ($changetype eq $main::CT_LINKS) {
            $lnk_cnt++;
            $txt_lnk = "lnk";
        } elsif ($changetype eq $main::CT_DELETED) {
            $del_cnt++;
            $txt_del = "del";
        } elsif ($changetype eq $main::CT_TYPE) {
            $typ_cnt++;
            $txt_typ = "typ";
        } elsif ($changetype eq $main::CT_ADDED) {
            $add_cnt++;
            $txt_add = "add";
        } elsif ($changetype eq $main::CT_CONT_CRC) {
            $crc_cnt++;
            $txt_crc = "crc";
        } elsif ($changetype eq $main::CT_CONT_SYMLINK) {
            $sym_cnt++;
            $txt_sym = "sym";
        } elsif ($changetype eq $main::CT_CONT_MAJMIN) {
            $mmn_cnt++;
            $txt_mmn = "mmn";
        } else {
            &ade_err_internal("fsscancallback: unexpected CT: $changetype");
        }
    }

    #  and write the record with the file name tagged on the end.
    print $outhandle "$txt_add $txt_del $txt_typ $txt_own $txt_grp $txt_mde $txt_lnk $txt_crc $txt_sym $txt_mmn    $file\n";

    &ade_msg_debug(4, "fadscan_cb: returning success ...");
    return 0;
}
    
#spp include fad.pl.spp
#spp include SPPSYM_ADEROOT_LIB/ade_mail.pl.spp
#spp include SPPSYM_ADEROOT_LIB/ade_utils.pl.spp
#spp include SPPSYM_ADEROOT_LIB/ade_gep.pl.spp
