#!/usr/local/bin/perl

##
## Rscan 1.4.0 (c) 1994, 1995 Nate Sammons
##
## This script initiates remote scans on machines specified in the 
## "config" file and collates the info they send back about the security
## of the remote machine[s].
##

###########################################################################
##           THERE ARE NO CONFIGURATION VARIABLES IN THIS FILE           ##
###########################################################################

##
## whoami and what's my version
##
$name    = 'Rscan';
$version = '1.4.0';
$vdate   = 'April 3, 1995';

##
## Signal handler
##
$SIG{'INT'}  = 'sighandler';
$SIG{'QUIT'} = 'sighandler';

##
## The big, fancy title, courtesy figlet.
##
$bigtitle = join('',
      &center(' ____                      '), "\n",
      &center('|  _ \ ___  ___ __ _ _ __  '), "\n",
      &center("| |_) / __|/ __/ _` | '_ \\ "), "\n",
      &center('|  _ <\__ \ (_| (_| | | | |'), "\n",
      &center('|_| \_\___/\___\__,_|_| |_|'), "\n",
      &center('Heterogeneous Network Interrogation'), "\n\n",
      &center("Version $version -- $vdate"), "\n\n");

##
## See if we're root or not (some of the programs checked are only readable
## by root) besides... user's don't need to know what this dredges up ;-)
##
if ($> != 0) {
  die "\nRscan must be run as root.\n\n";
  }

##
## some miscellaneous setup
##
$|                = 1;                       ## Set non-buffered I/O on STDOUT
$configdir        = './config';              ## where to look for config files
$config           = "$configdir/default.cf"; ## default config file
$now              = $^T;                     ## current time at startup
$report{'dir'}    = './reports';             ## reports go here
$scannerdir       = './scanner';             ## where is the scanner?
$defaultmodlist   = 'any';                   ## default modlist for undefined.
$tempdir          = '/tmp';                  ## default temp directory
$redirect         = '2>&1';                  ## STDERR >> STDOUT
$scannedhost      = 'localhost';             ## because of the editor

##
## default report naming convention
## it is the same as specifying a report name of
##     scan.%H-%M.%D.%Y-%h:%m:%s
## in the configuration file or on the commandline
##
$report{'onameformat'} = 'rscan.%H-%M.%D.%Y-%h:%m:%s';
$report{'nameformat'} = '"rscan.$scannedhost-$time{month}.$time{day}.' .
                    '$time{year}-$time{hour}:$time{minute}:$time{second}"';

##
## report owner, group and permissions
##
$report{'owner'} = '0';
$report{'group'} = '0';
$report{'perm'}  = '0200';

##
## setup time data for report naming
##
($time{'second'}, $time{'minute'}, $time{'hour'},
 $time{'day'}, $time{'month'}, $time{'year'}) = localtime($now);
$time{'hour'}   = sprintf("%02d", $time{'hour'});
$time{'minute'} = sprintf("%02d", $time{'minute'});
$time{'second'} = sprintf("%02d", $time{'second'});
$time{'day'}    = sprintf("%02d", $time{'day'});
$time{'month'}  = sprintf("%02d", $time{'month'});
$time{'year'}   = sprintf("%02d", $time{'year'});

##
## These strings define how we interpret the feedback from the
## other processes we run (mostly on other machines)
##
$interpret{'delim'} = ':';
$interpret{'c'}    = '&printcheck(substr($_, 2))';
$interpret{'o'}    = '&result(substr($_, 2))';
$interpret{'r'}    = '&printreport(substr($_, 2))';
$interpret{'s'}    = 'chop; s/<R>/\n/g; print substr($_, 2)';
$interpret{'h'}    = 'chop; &printheader(substr($_, 2))';
$interpret{'rh'}   = 'push(@reportheader, substr($_, 3))';
$interpret{'m'}    = 'chop; &modulename(substr($_, 2))';
$interpret{'ci'}   = 'chop; ' .
                     "\$remote{(split(\$interpret{'delim'}))[1]} = " .
                     "(split(\$interpret{'delim'}))[2]";

##
## for writing html reports later.  These are the types
## of URL that &linkify() will recognize and convert.
##
$url{'http'}    = 1;
$url{'ftp'}     = 1;
$url{'wais'}    = 1;
$url{'gopher'}  = 1;
$url{'news'}    = 1;
$url{'telnet'}  = 1;

##
## this if for looking up a test result
##
$result{'[ FAIL ] *'} = 'FAILED';
$result{'[ WARN ] +'} = 'WARNING';
$result{'[ PASS ]'}   = 'PASSED';
$result{'[ ERR  ]'}   = 'ERROR';
$result{'[ INFO ]'}   = 'Information';

##
## this is so we can print nice pretty HTML reports.
##
$htmlresult{'[ FAIL ] *'} = '<blink><b>FAILED</b></blink>';
$htmlresult{'[ WARN ] +'} = '<i>WARNING</i>';
$htmlresult{'[ PASS ]'}   = 'PASSED';
$htmlresult{'[ ERR  ]'}   = '<b>ERROR</b>';
$htmlresult{'[ INFO ]'}   = '<i>Information</i>';

##
## Information for time functions
##
@days   = ('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
           'Friday', 'Saturday');
@months = ('January', 'February', 'March', 'April', 'May', 'June',
           'July', 'August', 'September', 'October', 'November', 'December');

##
## command line options, and functions associated with them,
## also, how many options to give them, and if we should
## exit after running the command.
##
## %cl   = the perl command to run
## %clo  = how many options does it take
## %clh  = a little help
## %cle  = exit after running? (0, 1)
## %cld  = what are the options that go with it?
##
$cl{'-h'}          = '&cmdlineopts';
$clo{'-h'}         = 0;
$clh{'-h'}         = 'Prints command line options help';
$cle{'-h'}         = 1;
$cld{'-h'}         = 0;

$cl{'-html'}       = '&sethtml';
$clo{'-html'}      = 0;
$clh{'-html'}      = 'Write HTML reports instead of ascii ones.';
$cle{'-html'}      = 0;
$cld{'-html'}      = 0;

$cl{'-ascii'}      = '&setascii';
$clo{'-ascii'}     = 0;
$clh{'-ascii'}     = 'Write ascii reports instead of HTML ones.';
$cle{'-ascii'}     = 0;
$cld{'-ascii'}     = 0;

$cl{'-local'}      = '&setlocal';
$clo{'-local'}     = 0;
$clh{'-local'}     = 'Only scan the local machine';
$cle{'-local'}     = 0;
$cld{'-local'}     = 0;

$cl{'-debug'}      = '&setdebug';
$clo{'-debug'}     = 0;
$clh{'-debug'}     = 'Turns on debugging information';
$cle{'-debug'}     = 0;
$cld{'-debug'}     = 0;

$cl{'-modlist'}    = '&setmodlist';
$clo{'-modlist'}   = 1;
$clh{'-modlist'}   = 'Set a global modlist for all scans.';
$cle{'-modlist'}   = 0;
$cld{'-modlist'}   = 'modlist';

$cl{'-cf'}         = '&setconfig';
$clo{'-cf'}        = 1;
$clh{'-cf'}        = 'Use an alternate configuration file';
$cle{'-cf'}        = 0;
$cld{'-cf'}        = 'file (absolute or relative to the config dir)';

$cl{'-quiet'}      = '&setquiet';
$clo{'-quiet'}     = 0;
$clh{'-quiet'}     = 'Turns off all STDOUT information (good for cron jobs)';
$cle{'-quiet'}     = 0;
$cld{'-quiet'}     = 0;

$cl{'-reportname'}  = '&setreportname';
$clo{'-reportname'} = 1;
$clh{'-reportname'} = 'Sets the report naming format';
$cle{'-reportname'} = 0;
$cld{'-reportname'} = 'format';

$cl{'-reportdir'}  = "\$report{'dir_already_set'} = 1; &setreportdir";
$clo{'-reportdir'} = 1;
$clh{'-reportdir'} = 'Sets the report output directory';
$cle{'-reportdir'} = 0;
$cld{'-reportdir'} = 'directory';

$cl{'-separate'}   = '&setseparatereports';
$clo{'-separate'}  = 0;
$clh{'-separate'}  = 'Make separate reports for each machine';
$cle{'-separate'}  = 0;
$cld{'-separate'}  = 0;

$cl{'-single'}     = '&setsinglereport';
$clo{'-single'}    = 0;
$clh{'-single'}    = 'Make a single report for the network';
$cle{'-single'}    = 0;
$cld{'-single'}    = 0;

$cl{'-list'}       = '&listmodules';
$clo{'-list'}      = 0;
$clh{'-list'}      = 'List installed modules';
$cle{'-list'}      = 1;
$cld{'-list'}      = 0;

$cl{'-tmp'}        = '$tempdir_already_set = 1; &settempdir';
$clo{'-tmp'}       = 1;
$clh{'-tmp'}       = 'Use a directory other than /tmp for temp files';
$cle{'-tmp'}       = 0;
$cld{'-tmp'}       = 'directory';

$cl{'-nocf'}       = '&noconfig';
$clo{'-nocf'}      = 0;
$clh{'-nocf'}      = "Don't even try to look at a configuration file";
$cle{'-nocf'}      = 0;
$cld{'-nocf'}      = 0;

$cl{'-machine'}    = '&setmachine';
$clo{'-machine'}   = 3;
$clh{'-machine'}   = "Add a machine to the list of hosts to scan";
$cle{'-machine'}   = 0;
$cld{'-machine'}   = 'host1,host2,...,hostN pathtoperl modlist';

$cl{'-config'}     = '&configure';
$clo{'-config'}    = 0;
$clh{'-config'}    = "Enter interactive configuration file editor";
$cle{'-config'}    = 1;
$cld{'-config'}    = 0;


##
## interpret command line options and do sanity checking on them
##
for ($i = 0 ; $i <= $#ARGV ; ) {
  
  ## if the option is defined,
  if (defined $clo{$ARGV[$i]}) {
    $o = $ARGV[$i];
    $j = $i + $clo{$ARGV[$i]};
    undef @opts;
    
    ## assemble the options to the command,
    for ($i++ ; $i < $j + 1 ; $i++) {
      if ($i > $#ARGV || (defined $cl{$ARGV[$i]})) {
        print "Option '$o' usage: \"$o $cld{$o}\"\n",
              "Use the '-h' option for help.\n\n";
        exit 0;
        }
      push(@opts, $ARGV[$i]);
      }
    
    ## and pass them to the evaluator.
    eval "$cl{$o}('" . join("', '", @opts) . "')";
    if ($cle{$o}) { exit 0; }
    }
  
  ## if the option is not defined, tell them and exit.
  else {
    print "\n$name $version\n\n",
          "The option '$ARGV[$i]' is not supported.\n\n",
          "Call to rscan was:\n",
          '  ', join(' ', @ARGV), "\n\n";
    
    ## print a list of actual options
    &cmdlineopts;
    exit 0;
    }
  }

##
## undefine things we no longer need from command line operations
## for some reason, these cause problems later for opening files,
## and other stuff (a mystery to me!)
##
undef @opts;
undef $o;
undef $i;
undef $j;
undef %cl;
undef %clo;
undef %clh;
undef %cle;
undef @ARGV;

##
## Tell everyone who we are
##
print $bigtitle;

##
## be sure they know what we're taking so much time doing
##
print "\nInitializing";

##
## this is the OS name ("IRIX", "SunOS", etc)
##
chop($os{'name'} = `/bin/uname`);

##
## OS revision number
##
chop($os{'version'} = `/bin/uname -r`);

##
## CPU type ("IP22", "i486", etc)
##
chop($cpu = `/bin/uname -m`);

##
## machine shortname
##
chop($machine = `/bin/uname -n`);

##
## sets up OS-dependant paths to apps, etc.
## we really need this to work.
##
if (-e "$scannerdir/pathconfig/$os{'name'}") {
  do "$scannerdir/pathconfig/$os{'name'}";
  }
else {
  die "\n\nFATAL: No OS-specific path configuration data.\n",
      "  either brew your own and put it in the file\n",
      "  '$scannerdir/pathconfig/" , $os{'name'} ,
      "' or ask around for one.\n",
      "  If your OS is supported, then your package is either\n",
      "  incomplete or corrupted.\n";
  }

##
## get our hostname and current directory
## ($hostname and $pwd are set in the pathconfig data file)
##
if ($hostname) {
  chop($localhost = `$hostname`);
  }
else {
  die "\n\nFATAL: OS-specific path configuration data\n",
      "  did not set the \$hostname variable.\n",
      "  Either brew your own and put it in the file\n",
      "  '$scannerdir/pathconfig/" , $os{'name'} ,
      "' or ask around for one.\n",
      "  If your OS is supported, then your package is either\n",
      "  incomplete or corrupted.\n";
  }

if ($pwd) {
  chop($pwd = `$pwd`);
  }
else {
  die "\n\nFATAL: OS-specific path configuration data\n",
      "  did not set the \$pwd variable.\n",
      "  Either brew your own and put it in the file\n",
      "  '$scannerdir/pathconfig/" , $os{'name'} ,
      "' or ask around for one.\n",
      "  If your OS is supported, then your package is either\n",
      "  incomplete or corrupted.\n";
  }

##
## read the configuration file if we need to
##
if (!$noconfig) { if (&readconfig) { exit 0; } }

##
## If we set the modlist on the command line, override
## the options given in the config file
##
if ($globalmodlist) {
  foreach $i (keys %perlloc) { $modlist{$i} = $globalmodlist; }
  }

##
## local machine only
##
if ($localonly) {
  undef %perlloc; ## don't scan anyone else
  undef %modlist; ## reset modlist

  ## the name of perl that's running this process
  $perlloc{$localhost} = $^X;
  
  ## set the modlist (ignore the one in the config file)
  if ($globalmodlist) {
    $modlist{$localhost} = $globalmodlist;
    }
  else {
    $modlist{$localhost} = $defaultmodlist;
    }
  }

##
## end of initialization
##
print "\n";

##
## some misc reporting crap.  If we're not writing separate reports,
## open the one we are writing, etc.
##
if (!$report{'separate'}) {

  ## if we're doing a local-only scan, then put the hostname in the
  ## report name, otherwise, use the word 'network'
  if ($localonly) { $scannedhost = $localhost; }
  else { $scannedhost = 'network'; }
  
  ## use the report name configuration data
  $report{'name'} = eval $report{'nameformat'};
  
  ## open the report and add it to the list of reports to read later
  open(REPORT, ">$report{'dir'}/$report{'name'}") ||
    die "ERROR: Cannot open $report{'dir'}/$report{'name'} : $!\n";
  push(@reports, "$report{'dir'}/$report{'name'}");

  ## a little space is nice
  print REPORT "\n";
  
  ## if we're only scanning the local host
  if ($localonly) {
    
    ## HTML formatted reports look nice.
    if ($report{'html'}) {
      print REPORT "\n<title>Rscan $version Local Report</title>\n",
                   "<center><h2>",
                   "Rscan $version Local Report",
                   "</h2></center>\n<P>\n";
      }
    
    ## but ASCII is easier to write ;-)
    else {
      print REPORT &center("Rscan $version Local Report");
      }
    }
  
  ## if we're scanning a network
  else {
    if ($report{'html'}) {
      print REPORT "\n<title>Rscan $version Network Report</title>\n",
                   "<center><h2>",
                   "Rscan $version Network Report",
                   "</h2></center>\n<P>\n";
      }
    else {
      print REPORT &center("Rscan $version Network Report");
      }
    }
  
  ## a little more space
  print REPORT "\n\n";
  }

##
## loop through machines to scan, and scan each one of them
## do it in alphabetic order
##
foreach $machine (sort keys %perlloc) {
  $scannedhost = $machine;
  
  ## in case there's no defn for perl fo this machine
  if (!$perlloc{$machine}) {
    print "  INFO:  The rscan run on host $machine was cancelled.\n",
          "   There is no definition for the path to perl for it.\n",
          "   Check to make sure that it's definition in the configuration\n",
          "   file or on the command line is correct.\n\n";
    next;
    }
  
  ## make sure we get fresh data from each machine.
  undef %remote;
  
  ## print a separator
  if ($report{'html'}) {
    print REPORT "<hr size=5>\n";
    }
  else {
    print REPORT '+', '=' x 72, "+\n";
    }
  
  ## print a separator to the screen also
  print '+', '=' x 72, "+\n\n";

  ## separate reports get their names reevaluated at each run
  ## since the %H variable will change.
  if ($report{'separate'}) {
    
    ## evaluate a new reportname from the format
    $report{'name'} = eval $report{'nameformat'};
    
    ## open it or die trying
    open(REPORT, ">$report{'dir'}/$report{'name'}") ||
      die "ERROR: Cannot open $report{'dir'}/$report{'name'} : $!\n";
    
    ## add it to the list of reports to read later
    push(@reports, "$report{'dir'}/$report{'name'}");
    
    ## HTML formatted reports
    if ($report{'html'}) {
      print REPORT "\n<title>Rscan $version Single Machine Report</title>\n",
                   "<center><h2>",
                   "Rscan $version Single Machine Report",
                   "</h2></center>\n<P>\n";
      }
    
    ## ASCII ones.
    else {
      print REPORT &center("Securscan $version Single Machine Report");
      print REPORT "\n\n";
      }
    }

  if ($machine ne $localhost) {
    ## only do this if the remote machine is not the local one.

    ## find out the remote OS, so we know what options to give the
    ## remote processes (It'll be the last line of text, after all
    ## the garbage the shell may give [motd, etc])
    open(UNAM, "$rsh $machine /bin/uname -sr $redirect |");
    while (<UNAM>) {
      chop;
      ($remote{'os'}, $remote{'osver'}) = split(' ');
      }
    close(UNAM);

    ## find out the $tar_extract options and the $tar variable
    ## for the remote machine.  If the remote machine is not
    ## supported, the go on to the next one.
    if (!-e "$scannerdir/pathconfig/$remote{'os'}") {
	 print "  INFO:  The rscan run on host $machine was cancelled.\n",
            "   It's OS type ($remote{'os'}) has no configuration data.\n",
            "   To run Rscan on this host you will need to create the file\n",
            "     '$scannerdir/pathconfig/$remote{'os'}'\n",
            "   and put path configuration data into it.  There are\n",
            "   example files in that directory.\n\n";

	 if ($report{'html'}) {
        print REPORT "<P><pre>\n",
            "  <b>INFO:</b>  The rscan run on host $machine was cancelled.\n",
            "   It's OS type ($remote{'os'}) has no configuration data.\n",
            "   To run Rscan on this host you will need to create the file\n",
            "     '$scannerdir/pathconfig/$remote{'os'}'\n",
            "   and put path configuration data into it.  There are\n",
            "   example files in that directory.\n\n",
            "   Local time is ", &ltime(time), "\n</pre>\n";
        }
	 else {
        print REPORT
            "  INFO:  The rscan run on host $machine was cancelled.\n",
            "   It's OS type ($remote{'os'}) has no configuration data.\n",
            "   To run Rscan on this host you will need to create the file\n",
            "     '$scannerdir/pathconfig/$remote{'os'}'\n",
            "   and put path configuration data into it.  There are\n",
            "   example files in that directory.\n\n",
            "   Local time is ", &ltime(time), "\n</pre>\n";
        }

	 ## skip to the next machine
	 next;
	 }
    else {

	 ## deduce the path to tar and the options to make it
	 ## extract a tar file.
	 open(PATHC, "$scannerdir/pathconfig/$remote{'os'}");
	 while (<PATHC>) {

        ## find out how to extract a tar archive.
        ## it's a little complicated, but this way it's almost
        ## sure to work.
        ##
        ## basically, find all the $tar and $remove definitions
        if (/^\s*\$tar\{/) {
          eval $_;
     	}
        
        if (/^\s*\$tar_extract\{/) {
          eval $_;
     	}

        }
	 close(PATHC);

      ##
      ## resolve how we're going to make tar work on the remote machine.
	 $remote{'tar_extract'} = $tar_extract{$remote{'os'} . $remote{'osver'}} ||
	                          $tar_extract{$remote{'os'} . 'default'};
	 $remote{'tar'} = $tar{$remote{'os'} . $remote{'osver'}} ||
	                  $tar{$remote{'os'} . 'default'};

      ## if we don't get what we want, whine about it.
	 if (!$remote{'tar'}) {
        $remote{'problem'} = 1;
        print "FATAL:  There is no descernable definition for the path\n",
          "        to the tar command for the OS $remote{'os'}.  Make sure\n",
          "        that there is a line like:\n",
          "          \$tar{$remote{'os'}$remote{'osver'}} = '/path/to/tar';\n",
          "         or\n",
          "          \$tar{$remote{'os'}default} = '/path/to/tar';\n",
          "        In the file $scannerdir/pathconfig/$remote{'os'}.\n";
        }
	 if (!$remote{'tar_extract'}) {
        $remote{'problem'} = 1;
        print "FATAL:  There is no descernable definition for the path\n",
          "        to the tar command for the OS $remote{'os'}.  Make sure\n",
          "        that there is a line equivalent to:\n",
          "          \$tar_extract{$remote{'os'}$remote{'osver'}} = 'xBf';\n",
          "         or\n",
          "          \$tar_extract{$remote{'os'}default} = 'xBf';\n",
          "        In the file $scannerdir/pathconfig/$remote{'os'}.\n";
        }
	 
	 ## go onto the next machine if there's a problem.
	 if ($remote{'problem'}) {
        next;
        }

	 }
    }
  
  ## setup how to find tar locally and how to extract an archive
  $tar = $tar{$os{'name'} . $os{'version'}} ||
         $tar{$os{'name'} . 'default'};
  $tar_extract = $tar_extract{$os{'name'} . $os{'version'}} ||
                 $tar_extract{$os{'name'} . 'default'};
  
  ## this is so the remote process will know what kind of report we're
  ## writing, in case they care.
  if ($report{'html'}) { $format = 'html'; }
  else { $format = 'ascii'; }

  ## If we're running local, then just run the scans using the 'scan' script
  if ($localhost eq $machine) {
    print "Initiating a local scan on $machine\n";
    $cmd = join(' ', $perlloc{$machine}, ## location of perl
                     "$scannerdir/scan", ## location of the scanner
                     'local',            ## local, not remote
                     "$pwd/scanner",     ## location of modules, etc
                     $modlist{$machine}, ## the modlist
                     $format,            ## ascii or html reports
                     $redirect);         ## redirect STDERR >> STDOUT
    
    ## if we can't open it, fail.
    open(SCAN, "$cmd |") || &fail("Cannot open $perlloc{$machine}: $!\n");
    }

  ## otherwise, copy the necessary files over to the remote host
  else {
    print "Copying scanner to $machine";
    $cmd = join(' ',
                $tar,                   ## the tar command (locally)
                $tar_create,            ## how to make an archive (locally)
                '-',                    ## write to STDOUT
                $scannerdir,            ## the scanner and modules
                '|',                    ## duh.
                $rsh,                   ## location of rsh (locally)
                $machine,               ## the remote machine
                "\"(cd $tempdir ;",     ## put things in the tempdir
                $remote{'tar'},         ## tar on the remote machine
                $remote{'tar_extract'}, ## how to extract over there.
                '-)"',                  ## read from STDIN.
                $redirect);             ## redirect STDERR >> STDOUT
                
    open(TAR, "$cmd |") || &fail("Cannot tar to $machine: $!\n");
    @tmp = <TAR>;
    close(TAR);
    print "\n";
    print "Initiating a remote scan on $machine\n";

    ##
    ## open the rsh to the remote machine and start a perl process there
    ## we do an 'exec' so the shell doesn't stick around
    ##
    $cmd = join(' ',
                $rsh,                        ## rsh
                $machine,                    ## remote machine
                'exec',                      ## don't leave the shell around
                "\"$perlloc{$machine}",      ## perl on the mcahine
                "$tempdir/$scannerdir/scan", ## what to run
                'remote',                    ## remote not local
                "$tempdir/$scannerdir",      ## where are the modules
                "'$modlist{$machine}'",      ## modlist for the machine
                "$format\"",                 ## html or ascii
                $redirect);                  ## redirect STDERR >> STDOUT

    open(SCAN, "$cmd |");
    
    $child_exit = ($? >> 8);
    if ($child_exit) {
      &fail("Cannot rsh to $machine: $!\n");
      }
    }

  ##
  ## look at what comes back and act on it according to what we
  ## set up in the %interpret hash table.  It's much cleaner than 
  ## just having a bunch of if/elsif/else statements.
  ##
  while (<SCAN>) {
    
    ## if there's an interpretation defined for the prefix
    ## (all chars before the $interpret{'delim'} character)
    if ($interpret{(split($interpret{'delim'}, $_))[0]}) {
      
      ## then do that code, which should act on $_
      eval $interpret{(split($interpret{'delim'}, $_))[0]};
      }
    
    ## if debuggin is on, then print some debugging info,
    ## but we ignore everything else.
    elsif ($debug) {
      chop($tmp = $_);
      print "  DEBUG: $tmp\n";
      }
    }
  print "\n\n";
  
  ## close the process.
  close(SCAN);
  
  ## print the report and start over on report data
  if ($report{'html'}) { print REPORT "<pre>\n"; }
  
  ##
  ## print the data to the report.
  ##
  print REPORT "\n", @reportheader, "\n\n";
  if ($report{'html'}) {
    print REPORT '<b> Test' , ' ' x 50 , "Condition</b>\n";
    }
  else {
    print REPORT ' Test' , ' ' x 50 , "Condition\n";
    }
  
  if ($report{'html'}) {
    print REPORT "<hr>\n";
    }
  else {
    print REPORT '-' x 64 , "\n\n";
    }
  
  print REPORT @checklist;

  if ($report{'html'}) {
    print REPORT @reportdata, "\n";
    }
  else {
    print REPORT "\n", '-' x 64, "\n\n", @reportdata, "\n";
    }
  
  ## get rid of the evidence ;-)
  undef @reportdata;
  undef @reportheader;
  undef @checklist;
  
  ## if we're writing separate reports, then close this one
  if ($report{'separate'}) {
    if ($report{'html'}) {
      print REPORT "<hr size=5>\n</pre>\n";
      }
    else {
      print REPORT "\n\n", '+', '=' x 72, "+\n";
      }
    close(REPORT);
    }
  }

## print a final separator
print '+', '=' x 72, "+\n\n";
if ($report{'html'}) {
  print REPORT "<hr size=5>\n";
  }
else {
  print REPORT '+', '=' x 72, "+\n";
  }

##
## If we're writing separate reports, we have more than one
## report to read, and if it's in HTML, they should read it
## with more than just more or something.  If we're only writing
## one report, then there's only one to read ;-)
##
if ($report{'separate'}) {
  print "\n\nPlease read the following files for full scan reports:\n";
  foreach $i (@reports) {
    print "  $i\n";
    }
  print "\n";
  if ($report{'html'}) {
    print " They are in HTML format and should be read with a tool like\n",
          " Netscape, NCSA Mosaic or Lynx.\n\n";$infopointer
    }
  }
else {
  print "\n\nPlease read the file\n",
        "  $report{'dir'}/$report{'name'}",
        "\nfor a full scan report.\n\n";
  if ($report{'html'}) {
    print " It is in HTML format and should be read with a tool like\n",
          " Netscape, NCSA Mosaic or Lynx.\n\n";
    }
  }

##
## Close the current report if there's only one to write.
##
!$report{'separate'} && close(REPORT);
foreach $file (@reports) {
  chown 0, 0, $file;
  chmod 0400, $file;
  }

##
## Just in case.
##
exit 0;


### SUBROUTINES ################################################

##
## shutup already!  Closes STDOUT, so no output is generated
##
sub setquiet { close(STDOUT); }

##
## turns on debugging output
##
sub setdebug { $debug = 1; }

##
## only scan the local machine
##
sub setlocal { $localonly = 1; }

##
## Adds a line to the reportdata array.  We first
## translate off the <R> for "\n", etc.
##
sub printreport {
  local(@d);
  $_ = join('', @_);
  chop;
  s/<R>/\n   /g;        ## change "<R>" to "\n   "
  s/[\n ]+$/\n/g;       ## whack traliling "\n"'s and " "'s
  $_ = '   ' . $_;      ## add spaces to the beginning
  
  ## chop ending whitespace (or don't bother)
  ##while (substr($_, (length($_) -1)) eq ' ') { chop; }

  ## linkify if we're writing HTML reports
  if ($report{'html'}) {
    $_ = &linkify($_);
    }
  
  ## stick the data onto the array
  push(@reportdata, $_);
  }

##
## Print a check to the screen and add the correctly formatted
## line to the checklist and reportdata arrays.
##
sub printcheck {
  
  ## for HTML anchors
  ++$anchornum;
  local($text) = join('', @_);
  chop $text;
  
  ## print to the screen
  print STDOUT " $text " . '.' x (62-length($text)) . ' ';
  
  ## if we're writing HTML, then put in the anchors, etc.
  if ($report{'html'}) {
    push(@checklist,
      join('', '  <a href="#anchor' , $anchornum , '">' ,
      "$text</a> " , '.' x (51-length($text)) , ' '));
    push(@reportdata,
         "\n <a name=\"anchor$anchornum\"><b>Test: </b><i>$text</i>\n");
    }
  
  ## otherwise, just slap the text in there.
  else {
    push(@checklist, join('', "  $text " , '.' x (51-length($text)) , ' '));
    push(@reportdata, "\n Test: $text\n");
    }
  }

##
## Prints a header line properly.
##
sub printheader {
  local($name, $data) = split('<<SEPARATOR>>', join('', @_));
  
  ## HTML is bolded, etc.
  if ($report{'html'}) {
    if ($name eq "<<NONE>>") { $name = ' ' x 25; }
    else { $name = '<b>' . &nprint("$name:", 25, ' ') . '</b>'; }
    $data = "<i>$data</i>";
    }
  
  ## ASCII is not
  else {
    if ($name eq "<<NONE>>") { $name = ' ' x 25; }
    else { $name = &nprint("$name:", 25, ' '); }
    }
  
  ## put the data into the report
  push(@reportheader, "$name$data\n");
  }

##
## Informs us of the module that is being run remotely.
##
sub modulename {
  local($name) = shift;
  
  ## we dump some info to the report.  Different data for HTML
  ## and ascii.
  if ($report{'html'}) {
    push(@reportdata, "<hr>\n<h3>Test Data for $name</h3>\n");
    push(@checklist, "\n<i>$name</i>\n");
    }
  else {
    push(@reportdata, "Test Data for $name\n");
    push(@checklist, "\n$name\n");
    }

  $currentmodule = $name;
  }

##
## A result (eg, info, warning, fail, error, etc)
##
sub result {
  local($text) = shift;
  print STDOUT $text;
  chop($text);
  
  ## HTML has special stuff like flashing failures, etc ;-)
  if ($report{'html'}) {
    $checklist[$#checklist] .= $htmlresult{$text} . "\n";
    push(@reportdata, "  <b>Test Results:</b> " .
                      $htmlresult{$text} . "\n");
    }
  
  ## ascii is a little more boring.
  else {
    $checklist[$#checklist] .= "$text\n";
    push(@reportdata, "  Test Results: " . $result{$text} . "\n");
    }
  }

##
## Set the configuration file to use
##
sub setconfig {
  $config = $_[0];
  
  ## the file is relative to the $config dir, unless it's
  ## specified absolutely ( /whatever/whatever )
  if (substr($config, 0, 1) ne '/') { $config = "$configdir/$config"; }
  
  ## if it doesn't exist, we're in trouble.
  if (!-e $config) {
    print "FATAL: Configuration file $config does not exist.\n";
    exit 0;
    }
  }

##
## Reads data from the configuration file
##
sub readconfig {
    
  ## open it or die.
  open(CONFIG, $config) || ( print "ERROR: $config: $!\n" && return 1 );
  while ($_ = <CONFIG>) {
    
    ## skip comments and blank lines
    if (/^\s*#/ || /^\s*$/) { next; }
    
    ## chop off the \n at the end.
    chop;
    
    ## chop leading blanks
    while (substr($_, 0, 1) eq ' ') { $_ = substr($_, 1); }
    
    ## split on whitespace
    @tmp = split(/\s+/, $_);
    
    ##
    ## options, some get complicated ;-)
    ##
    
    ##
    ## machine specification (remote and local)
    ##
    if ($tmp[0] eq 'machine') {
      
      ## can give a list of machines like this,that,theother
      foreach $mach (split(',', $tmp[1])) {
        ## format is:
        ##   machine machinename pathtoperl modlist
        ##     (0)      (1)        (2)       (3)
        ##
        ## pathtoperl can be "same" if it's the same as this process.
        if ($tmp[2] eq 'same') {
     	$perlloc{$mach} = $^X;
     	}
        else {
     	$perlloc{$mach} = $tmp[2];
     	}

        ## comma separated fields of modules, make sure the modlist is
        ## OK, though.
        if ($tmp[3]) {
          %tmphash = &parsemodlist($tmp[3], $scannerdir);
          if ($tmphash{'__error__'}) {
            print "\n\nERROR: Line $. of the configuration file $config\n",
                  "  contains an invalid modlist.  Please make sure\n",
                  "  that it conforms to the modlist specification.\n\n";
            return 1;
            }
          undef %tmphash;
          $modlist{$mach} = $tmp[3];
          }
        else {
          $modlist{$mach} = $defaultmodlist;
          }
        }
      }
    
    ##
    ## report directory specification
    ##
    elsif ($tmp[0] eq 'reportdir') {
      ## command line options override the ones in the config file
      if (!$report{'dir_already_set'}) {
        &setreport('dir', $tmp[1]);
        }
      }
    
    ##
    ## report's name, overridden on the command line
    ##
    elsif ($tmp[0] eq 'reportname') {
      if (!$report{'name'}) {
        &setreport('name', $tmp[1]);
        $report{'onameformat'} = $tmp[1];
        }
      }
    
    ##
    ## Separate reports, overridden on commandline
    ##
    elsif (($tmp[0] eq 'separate') && ($tmp[1] eq 'reports')) {
      ## separate reports for each machine
      ## command line options override the ones in the config file
      if (!$report{'already_set_separate'}) {
        &setreport('separate', 1);
        }
      }
    
    ##
    ## HTML reports, overridden on command line.
    ##
    elsif (($tmp[0] eq 'html') && ($tmp[1] eq 'reports')) {
      ## html formatted reports
      $report{'html'} = 1;
      if (substr($report{'nameformat'},
          (length($report{'nameformat'}) -5)) ne '.html"') {
        chop $report{'nameformat'};
        $report{'nameformat'} .= '.html"';
        }
      }
    
    ##
    ## Temp directory other than /tmp
    ##
    elsif ($tmp[0] eq 'tempdir') {
      if (!$tmp[1]) {
        print "FATAL: Error on line $. of $config configuration file.\n",
              "  tempdir declaration gives no directory name.\n\n",
        exit 0;
        }
      else {
        &settempdir($tmp[1]);
        }
      }
    
    ##
    ## Else, whine and die.
    ##
    else {
      print "\nFATAL: Bad configuration option in $config line $.\n";
      return 1;
      }
    }
  
  ## close up shop and we're done.
  close(CONFIG);
  undef @tmp;
  }

##
## set the modlist for all machines
##
sub setmodlist {
  local($ml) = shift;
  local(%tmphash) = &parsemodlist($ml, "$scannerdir/modules");
  if ($tmphash{'__error__'}) {
    print "\n",
          "ERROR: The modlist\n",
          "    \"$ml\"\n",
          "  is invalid.  Please make sure it conforms to the\n",
          "  specification for writing modlists.\n\n";
    exit 0;
    }
  $globalmodlist = $ml;
  }

##
## set reportname (simple, eh?)
##
sub setreportname {
  &setreport('name', $_[0]);
  }

##
## multiple reports (from commandline)
##
sub setseparatereports {
  $report{'already_set_separate'} = 1;
  &setreport('separate', 1);
  }

##
## one report
##
sub setsinglereport {
  $report{'already_set_separate'} = 1;
  &setreport('separate', 0);
  }

##
## set report directory
##
sub setreportdir {
  &setreport('dir', $_[0]);
  }

##
## sets HTML report writing
##
sub sethtml {
  $report{'html'} = 1;
  if (defined $report{'nameformat'}) {
    ## don't set an additional .html in the report name
    if (substr($report{'nameformat'}, (length($report{'nameformat'}) -6)) ne
         '.html"') {
      chop $report{'nameformat'};
      $report{'nameformat'} .= '.html"';
      }
    }
  }

##
## sets ascii report writing (in case HTML is set in the config file)
##
sub setascii {
  $report{'html'} = 0;
  if (defined $report{'nameformat'}) {
    ## remove the extra .html on the end of the name
    if (substr($report{'nameformat'}, (length($report{'nameformat'}) -6)) eq
         '.html"') {
      $report{'nameformat'} =
        substr($report{'nameformat'}, 0, (length($report{'nameformat'}) -6)) . '"';
      }
    }
  }


##
## set the options for the report
##
sub setreport {
  local($temp) = $_;
  local($what) = shift;
  $_ = shift;
  
  ##
  ## set the report name
  ##
  if ($what eq 'name') {
    ## parse out the reportname
    s/\%M/\$time{month}/g;   ## month number
    s/\%D/\$time{day}/g;     ## day number
    s/\%Y/\$time{year}/g;    ## year, eg "95", etc.
    s/\%h/\$time{hour}/g;    ## hour number
    s/\%m/\$time{minute}/g;  ## minute number
    s/\%s/\$time{second}/g;  ## seconds
    s/\%H/\$scannedhost/g;   ## hostname or "network"
    s/\%I/\$^T/g;            ## seconds since Jan 1, 1970.

    ## quote it for evaluation
    $report{'nameformat'} = "\"$_\"";
    if ($report{'html'}) {
      ## don't set an additional .html in the report name
      if (substr($report{'nameformat'}, (length($report{'nameformat'}) -6)) ne
         '.html"') {
        chop $report{'nameformat'};
        $report{'nameformat'} .= '.html"';
        }
      }
    }
  
  ##
  ## Set the report directory
  ##
  elsif ($what eq 'dir') {
    $report{'dir'} = $_;
    }
  
  ##
  ## separate reports
  ##
  elsif ($what eq 'separate') {
    $report{'separate'} = $_;
    }
  
  ## reset $_
  $_ = $temp;
  }

##
## this finds text that looks like a URL and turns it into an actual
## hypertext link.  Ex. "http://www.vis.colostate.edu" becomes
## "<a href="http://www.vis.colostate.edu">http://www.vis.colostate.edu</a>"
##
sub linkify {
  local($tmp) = $_;
  local(@l) = split(/\n/, join('', @_), -1);
  local(@t, @m, $i, $j);
  
  ## if it fits the "something://something" general format
  foreach $i (0..$#l) {
    @m = split(/ /, $l[$i], -1);
    foreach $j (0..$#m) {
      $_ = $m[$j];
      @t = split(':', $_);
      if (( $url{$t[0]}           ) &&
          ( $t[1] =~ /\/\/[^\/]+/ )) {
        $m[$j] = '<a href="' . $_ . '">' . $_ . '</a>';
        }
      }
    $l[$i] = join(' ', @m);
    }
  
  $_ = $tmp;
  
  ## reassemble the final string for return
  $tmp = join("\n", @l);
  }

##
## this one returns a string that describes the time (so we don't need to
## use the date command)
##
sub ltime {
  local($mytime, $ampm);
  $mytime = shift;
  @tmp = localtime($mytime);
  if ($tmp[2] > 12) { $tmp[2] -= 12; $ampm = 'pm'; }
  else { $ampm = 'am'; }
  $tmp[5] += 1900;
  $mytime = join('', $days[$tmp[6]] , ', ' , $months[$tmp[4]] , ' ' ,
   $tmp[3] , ' ' , $tmp[5] , ' at ' , 
   sprintf("%d", $tmp[2]) , ':' , sprintf("%02d", $tmp[1]) , ':' ,
   sprintf("%02d", $tmp[0]) , " $ampm");
  }

##
## center text
##
sub center {
  local($text) = shift;
  $text = ' ' x ((70- length($text))/2) . $text;
  }

##
## fail and print something
##
sub fail {
  local($text) = shift;
  print REPORT "Failed on $machine:\n  $text\n";
  print STDOUT "Failed on $machine:\n  $text\n";
  $failed = 1;
  }

##
## buffered printing to a string of known length
##
sub nprint {
  local($text, $space, $char) = @_;
  $text = $text . $char x ($space - length($text));
  }

##
## prints command line options
##
sub cmdlineopts {
  local($space) = 12;
  print $bigtitle,
        'Command line options override configuration options', "\n",
        'that are set in the configuration file.', "\n\n",
        'Command line options:', "\n";
  foreach $i (sort keys %cl) {
    print ' ', &nprint($i, $space, ' '), '  ', $clh{$i}, "\n";
    if ($cld{$i}) {
      print ' ' x ($space +4), 'Usage: ', $i, ' ', $cld{$i}, "\n";
      }
    }
  print "\n";
  }

##
## list all the installed modules
##
sub listmodules {

  print $bigtitle;
  
  local($name, $version, $it, $line);
  local($indent) = '     ';
  
  ## first, do the generic ones
  print "  The following modules are installed, and can be\n",
        "  selected in a modlist.  The individual scans can\n",
        "  also be selected or deselected in a modlist.\n";
  
  opendir(DIR, "$scannerdir/modules");
  while ($_ = readdir(DIR)) {
    if (/^\.+$/) { next; } ## skip . and ..
    
    print "\n  ", &nprint($_, 15, ' ');
    open(NAME, "$scannerdir/modules/$_/fullname");
    chop($name = <NAME>);
    close(NAME);
    open(VERS, "$scannerdir/modules/$_/version");
    chop($version = <VERS>);
    close(VERS);
    print "  $name\n", ' ' x 19, "version $version\n";
    
    ## now, list each scan associated with each module
    
    ## first look for generic scans
    if (-e "$scannerdir/modules/$_/desc") {
      print "\n    Generic scans in $_:\n";
      open(DESC, "$scannerdir/modules/$_/desc");
      undef %desc;
      while (chop($line = <DESC>)) {
        if ($line =~ /^\s*#/ || $line =~ /^\s*$/) { next; }
        $it = <DESC>;
        $desc{substr($line, 0, (length($line) -3))} = $it;
        }
      close(DESC);
      
      ## print them in alphabetic order
      foreach $it (sort keys %desc) {
        print $indent,
     	    &nprint($it, 15, ' '), ': ',
     	    $desc{$it};
        }
      }
    
    ## now look for scans in each directory under that has a 'desc' file.
    opendir(OS, "$scannerdir/modules/$_");
    while ($os = readdir(OS)) {
      if (!-d "$scannerdir/modules/$_/$os" ||        ## skip non-directories
          $os =~ /^\.+$/ ||                          ## . and ..
          -l "$scannerdir/modules/$_/$os") { next; } ## and links
      opendir(OSVER, "$scannerdir/modules/$_/$os");
      while ($ver = readdir(OSVER)) {
        if (-e "$scannerdir/modules/$_/$os/$ver/desc") {
          print "\n    Scans for $os $ver:\n";
     	undef %desc;
     	open(DESC, "$scannerdir/modules/$_/$os/$ver/desc");
     	while (chop($line = <DESC>)) {
     	  if ($line =~ /^\s*#/ || $line =~ /^\s*$/) { next; }
     	  $it = <DESC>;
            $desc{substr($line, 0, (length($line) -3))} = $it;
     	  }
     	close(DESC);
     	
     	## now print them in alphabetic order
     	foreach $it (sort keys %desc) {
     	print $indent,
     		 &nprint($it, 15, ' '), ': ',
     		 $desc{$it};
     	}
     	}
        }
      closedir(OSVER);
      }
    closedir(OS);
    
    }
  closedir(DIR);
  print "\n\n";
  }

##
## Use a directory other than /tmp for putting the scanner, etc.
##
sub settempdir {
  if (!$tempdir_already_set) { $tempdir = $_[0]; }
  }

##
## Causes Rscan to never look for a configuration file.  All options
## must be set from the command line.
##
sub noconfig { $noconfig = 1; }

##
## Set a machine from the command line,  "pathtoperl" can be
## "same" to use the same perl as currently running.
##
sub setmachine {
  local($hosts, $perl, $modlist) = @_;
  local($i, %tmphash);
  
  %tmphash = &parsemodlist($modlist, $scannerdir);
  if ($tmphash{'__error__'}) {
    print "\nThe modlist\n",
          "     \"$modlist\"\n",
          "  had errors when it was parsed.  Make sure that you conformed\n",
          "  to the guidelines for writing modlists.\n\n";
    exit 0;
    }
  
  ## a shortcut for running the same path to perl as this script
  if ($perl eq 'same') { $perl = $^X; }
  
  ## a machine can be a comma separated list.
  foreach $i (split(',', $hosts)) {
    $perlloc{$i} = $perl;
    $modlist{$i} = $modlist;
    }
  }

##
## Signal handler
##
sub sighandler {
  local($sig) = @_;
  print "\n\nCaught a SIG$sig -- exiting.\n";
  
  if ($remote{'pid'}) {
    ## try to straighten things out if they're still fuzzy
    if (!$remote{'hostname'}) { $remote{'hostname'} = $machine; }
    if (!$remote{'remove'})   { $remote{'remove'} = $remove; }
    if (!$remote{'os'})       { $remote{'os'} = 'Unknown'; }
    if (!$remote{'osver'})    { $remote{'osver'} = '??'; }
    
    
    if ($remote{hostname} ne $localhost) {
      ##try and kill it.
      print 'Attempting infanticide.';
      system "$rsh $remote{hostname} kill -9 $remote{'pid'}";
      $killstatus = ($? >> 8);
      if ($killstatus) {
        print "\n  Attempt to kill pid $remote{'pid'} ",
              "on host $remote{'hostname'} [$remote{'os'} ",
              "$remote{'osver'}] failed.\n";
        }
      else {
        print "\n  Killed PID $remote{'pid'} on host $remote{hostname}.\n";
        }
      
      ##
      ## Try to remove the remote scanner directory
      ##
      print 'Attempting to remove the remote scanner directory.';
      system "$rsh $remote{hostname} $remote{'remove'} $tempdir/$scannerdir";
      $killstatus = ($? >> 8);
      
      if ($killstatus) {
        print "\n  Failed [exitval = $killstatus]\n",
              "  You may need to remove the directory\n",
              "    \"$tempdir/$scannerdir\"\n",
              "  on host $remote{'hostname'}.\n";
        }
      else {
        print " Succeeded.\n";
        }
      }
    }
  
  print "Dumping the rest of the report to the report file.\n",
        'This may not look pretty.';
  
  if ($report{'html'}) {
    print REPORT "\n\n<hr size=5><br>\n",
                 "<b>Scan process stopped by SIG$sig.\n",
                 "Dumping what I can of the report.</b><P>";
    }
  else {
    print REPORT "\n\n\nScan process stopped by SIG$sig.\n",
    "Dumping what I can of the report.\n\n";
    }
    
  ##
  ## print the data to the report.
  ##
  
  if ($report{'html'}) {
    print REPORT "\n\n<P>\n\n<pre>\n";
    }
  
  print REPORT "\n", @reportheader, "\n\n";
  
  if ($report{'html'}) {
    print REPORT '<b> Test' , ' ' x 50 , "Condition</b>\n";
    }
  else {
    print REPORT ' Test' , ' ' x 50 , "Condition\n";
    }
  
  if ($report{'html'}) {
    print REPORT "<hr>\n";
    }
  else {
    print REPORT '-' x 64 , "\n\n";
    }
  
  print REPORT @checklist;

  if ($report{'html'}) {
    print REPORT @reportdata, "\n",
                 "\n<P>\n\n</pre><hr size=5>\n\n";
    }
  else {
    print REPORT "\n", '-' x 64, "\n\n", @reportdata, "\n";
    }
  
  close(REPORT);

  print "\n\n";
  exit 0;
  
  }

##
## Interactive Configuration file editor
##
sub configure {
  ##
  ## undefine things we no longer need from command line operations
  ## for some reason, these cause problems later for opening files,
  ## and other stuff (a mystery to me!)
  ##
  undef @opts;
  undef $o;
  undef $i;
  undef $j;
  undef %cl;
  undef %clo;
  undef %clh;
  undef %cle;
  undef @ARGV;

  ## configuration stuff.
  $prompt         = 'Configure> ';
  $spacer         = '    ';
  $currentfile    = 'default';
  $editor         = $ENV{'VISUAL'} || $ENV{'EDITOR'} || 'vi';
  $pager          = $ENV{'PAGER'} || 'more';

  ## set for commands in the "shell"
  ## available commands and their perl counterparts
  $cmd{'list'}        = '&cfg_list';
  $cmd{'select'}      = '&cfg_select';
  $cmd{'exit'}        = '&cfg_quit';
  $cmd{'quit'}        = '&cfg_quit';
  $cmd{'?'}           = '&cfg_help';
  $cmd{'help'}        = '&cfg_help';
  $cmd{'config'}      = '&cfg_config';
  $cmd{'edit'}        = '&cfg_edit';
  $cmd{'page'}        = '&cfg_page';
  $cmd{'read'}        = '&cfg_read';
  $cmd{'new'}         = '&cfg_new';
  $cmd{'remove'}      = '&cfg_remove';
  $cmd{'rename'}      = '&cfg_rename';
  $cmd{'single'}      = '&cfg_single';
  $cmd{'separate'}    = '&cfg_separate';
  $cmd{'html'}        = '&cfg_html';
  $cmd{'ascii'}       = '&cfg_ascii';
  $cmd{'reportname'}  = '&cfg_reportname';
  $cmd{'reportdir'}   = '&cfg_reportdir';
  $cmd{'machine'}     = '&cfg_machine';

  $help{'list'}        =
    $spacer . "List available configuration files.  The .cf extension is\n" .
    $spacer . "omitted, and should be omitted for all other commands also.\n";

  $help{'select'}      = 
    $spacer . "Select a different configuration file.  The .cf extension\n" .
    $spacer . "should be omitted.\n";

  $help{'exit'}        = 
    $spacer . "Exit Rscan\n";

  $help{'quit'}        = $help{'exit'};

  $help{'?'}           = 
    $spacer . "Get help.  If given alone, this prints a list of available\n" .
    $spacer . "commands.  If given a command name as an argument, it prints\n" .
    $spacer . "help specific to that command.\n";

  $help{'help'}        = $help{'?'};

  $help{'config'}      = 
    $spacer . "Lists the variables that have been setup by the current\n" .
    $spacer . "configuration file along with the current file, and some\n" .
    $spacer . "other miscellaneous information.\n";

  $help{'edit'}        = 
    $spacer . "Hand edit a configuration file.  The editor is set as either\n" .
    $spacer . "the VISUAL environment variable or the EDITOR environment\n" .
    $spacer . "variable or 'vi'  Once the file has been edited, it is\n" .
    $spacer . "re-read.  This edits the currently selected configuration file.\n";

  $help{'page'}        = 
    $spacer . "Page thought the currently selected configuration file. It\n" .
    $spacer . "uses the PAGER environment variable or 'more'.\n";

  $help{'read'}        = 
    $spacer . "Reads the information in the currently selected configuration file.\n";

  $help{'new'}         = 
    $spacer . "Creates a new configration file.\n";

  $help{'remove'}      = 
    $spacer . "Removes a configration file.\n";

  $help{'rename'}      = 
    $spacer . "Rename a configuration file";

  $help{'single'}      = 
    $spacer . "Set the single report attribute in the currently selected\n" .
    $spacer . "configuration file.\n";

  $help{'separate'}    = 
    $spacer . "Set the separate reports attribute in the currently selected\n" .
    $spacer . "configuration file.\n";

  $help{'html'}        = 
    $spacer . "Set the html reports attribute in the currently selected\n" .
    $spacer . "configuration file.\n";

  $help{'ascii'}       = 
    $spacer . "Set the ascii reports attribute in the currently selected\n" .
    $spacer . "configuration file.\n";

  $help{'reportname'}  = 
    $spacer . "Set the reportname attribute in the currently selected\n" .
    $spacer . "configuration file.  If the keyword 'none' is given,\n" .
    $spacer . "then the reportname line is deleted.\n";

  $help{'reportdir'}   = 
    $spacer . "Set the reportdir attribute in the currently selected\n" .
    $spacer . "configuration file.  If the keyword 'none' is given,\n" .
    $spacer . "then the reportdir line is deleted.\n";

  $help{'machine'}     = 
    $spacer . "Sets a machine line in the currently selected configuration\n" .
    $spacer . "file. The first argument is a hostname, second is either\n" .
    $spacer . "the word 'same' or a path to perl on the hostname.  If 'same'\n" .
    $spacer . "is used, then the path to perl that runs Rscan will be used.\n" .
    $spacer . "The third argument is either a modlist or nothing.  If nothing,\n" .
    $spacer . "then the 'any' keyword will be applied by Rscan.\n" .
    $spacer . "If the perl attribute is 'delete' then the machine line for\n" .
    $spacer . "the host will be deleted.\n";

  print $bigtitle,
        &center('Interactive configuration file editor'), "\n\n\n";
  
  &cfg_read;
  
  ## main command loop.
  print $prompt;
  while (<>) {
    chop;
    @tmp = split(' ', $_);  ## split on whitespace

    if (/^\s*$/) { ; } ## no command, just a <CR>

    ## basically, we assemble a perl function to execute from what $commands
    ## says and the options given by the user, then run it.
    elsif ($cmd{$tmp[0]}) {
	 $comd = $cmd{$tmp[0]} . "('" . $tmp[1] . "'";
	 foreach $i (2..$#tmp) { $comd .= ", '$tmp[$i]'"; }
	 $comd .= ')';
	 eval $comd;
	 }
    else { print "Unrecognized command: \"$_\" (type \"help\" for a list)\n"; }
    print $prompt;
    }
  }

#################################################################
##             Configuration editor subroutines                ##
#################################################################

##
## Exit.
##
sub cfg_quit { exit 0; }

##
## List configuration files
##
sub cfg_list {
  
  local($ext) = '.cf';
  local(@files);
  
  opendir(DIR, $configdir) || print "fuck.\n";
  while ($_ = readdir(DIR)) {
    if (substr($_, (length($_) - length($ext))) eq $ext) {
      push(@files, substr($_, 0, (length($_) - length($ext))));
      }
    }
  closedir(DIR);
  
  ## sort the file list.
  @files = sort @files;
  
  print "\nConfiguration files in \"$configdir\"\n\n";
  
  $_ = int($#files / 3);
  foreach $i (0..$_) {
    # this make a nice, 3 column listing, it just looks messy ;-)
    print '  ' , ' ' ,
      	&nprint($files[$i], 20, ' ') , 
      	' ' , &nprint(($files[$i + $_ +1])[0], 20, ' ') , 
      	' ' , $files[$i + (2* $_) +2], "\n";
    	}
   print "\n";

  
  }

##
## Command help
##
sub cfg_help {
  local($it) = shift;
  if (defined $help{$it}) {
    print "\n  help for command '$it' :\n", $help{$it}, "\n";
    }
  else {
  	print "\nKnown commands:\n";
  	local(@tmp) = sort keys %cmd;
  	local($tmp2) = int($#tmp / 3);
  	foreach $i (0..$tmp2) {
    	  # this make a nice, 3 column listing, it just looks messy ;-)
    	  print '  ' , ' ' ,
      	   &nprint($tmp[$i], 20, ' ') , 
      	   ' ' , &nprint(($tmp[$i + $tmp2 +1])[0], 20, ' ') , 
      	   ' ' , $tmp[$i + (2* $tmp2) +2], "\n";
    	}
  	print "\n",
  	      '  Type "help mycommand" to get help on using the ',
  	      "command \"mycommand\"\n",
  	      "\n";
  	}
  }

##
## Select a configuration file to edit.
##
sub cfg_select {
  local($file) = $_[0];
  if (!$file) {
    print "  Usage: \"select filename\"\n\n";
    return 1;
    }
  if (!-e "$configdir/$file.cf") {
    print "  There is no configuration file called \"$file\"\n\n";
    return 1;
    }
  else {
    $currentfile = $file;
    print "  \"$file\" selected for editing\n\n";
    &cfg_read;
    }
  }

##
## Print configuration variables
##
sub cfg_config {
  
  print "\nConfiguration variables:\n\n",
        '  Current file:            ', $currentfile, "\n",
        '  Editor:                  ', $editor, "\n",
        '  Pager:                   ', $pager, "\n";
  
  print "\n Report information hash table:\n";
  foreach $i (sort keys %report) {
    if ($i eq 'nameformat') { next; }
    if ($i eq 'onameformat') {
      print '  ', &nprint('nameformat', 25, ' '), $report{'onameformat'}, "\n";
      }
    else {
      print '  ', &nprint($i, 25, ' '), $report{$i}, "\n";
      }
    }
  
  print "\nThe reportname will be expanded to something like:\n",
        '  Reportname:              ', eval $report{'nameformat'}, "\n";
  
  print "\n Perl executable location hash table:\n";
  foreach $i (sort keys %perlloc) {
    print '  ', &nprint($i, 25, ' '), $perlloc{$i}, "\n";
    }
  
  print "\n Module list hash table:\n";
  foreach $i (sort keys %modlist) {
    print '  ', &nprint($i, 25, ' '), $modlist{$i}, "\n";
    }
  
  print "\n\n";
        
  }

##
## Read a configuration file
##
sub cfg_read {

  local($prompt) = 'reading configuration file';
  print $prompt;
  
  ## first, undef everything,
  undef %report;
  undef %perlloc;
  undef %modlist;
  undef $tempdir;
  
  ## redefine defaults
  $report{'onameformat'} = 'scan.%H-%M.%D.%Y-%h:%m:%s';
  $report{'nameformat'} = '"scan.$scannedhost-$time{month}.$time{day}.' .
                    '$time{year}-$time{hour}:$time{minute}:$time{second}"';
  $report{'owner'} = '0';
  $report{'group'} = '0';
  $report{'perm'}  = '0200';
  $report{'dir'}   = './reports';

  $config = "$configdir/$currentfile.cf";
  if (&readconfig) {
    print "\nThere were errors in the configuration file.  It should be\n",
          " edited before use.\n";
    }
  else { print "\n"; }
  }

##
## Hand edit a file
##
sub cfg_edit {
  system "$editor $configdir/$currentfile.cf";
  &cfg_read;
  }

##
## Page through a file
##
sub cfg_page {
  system "$pager $configdir/$currentfile.cf";
  }

##
## Create a new file
##
sub cfg_new {
  if (!$_[0]) {
    print "  Usage: \"new filename\"\n";
    return 1;
    }
  else {
    if (substr($_[0], (length($_[0]) - length('.cf'))) ne '.cf') {
      $_[0] .= '.cf';
      }
    }
  if (-e "$configdir/$_[0]") {
    print "  File already exists.  Nothing changed.\n";
    return 1;
    }
  open(FILE, "> $configdir/$_[0]");
  close(FILE);
  
  print "File created\n";
  
  ## select it
  &cfg_select(substr($_[0], 0, (length($_[0]) - length('.cf'))));
  }

##
## Remove a file
##
sub cfg_remove {
  local($file) = $_[0];
  if (!$file) {
    print "  Usage: \"remove filename\"\n\n";
    return 1;
    }
  
  if (!-e "$configdir/$file.cf") {
    print "  The configuration file $file does not exist.\n\n";
    return 1;
    }
  
  print "  Delete \"$file\"? [y]es, [n]o : ";
  chop($_ = <STDIN>);
  if ($_ eq 'y') {
    if (! unlink("$configdir/$file.cf")) {
      print "File \"$file\" not deleted: $!\n\n";
      }
    }
  else {
    print "File \"$file\" not deleted\n";
    }
  }

##
## Rename a file
##
sub cfg_rename {
  local($old, $new) = @_;
  if (!$old || !$new) {
    print "  Usage: \"rename oldname newname\"\n\n";
    return 1;
    }
  
  if (!-e "$configdir/$old.cf") {
    print "  The configuration file $file does not exist.\n\n";
    return 1;
    }
  
  if (-e "$configdir/$new.cf") {
    print "  The file \"$new\" already exists.\n",
          "   no files were modified\n\n";
    return 1;
    }
  
  if (! rename("$configdir/$old.cf", "$configdir/$new.cf")) {
    print "  File \"$old\" not renamed: $!\n\n";
    }
  else {
    print "  File \"$old\" renamed to \"$new\"\n";
    }
  }

##
## Sets single report generation flag (unsets separate, actually)
##
sub cfg_single {
  open(FILE, "$configdir/$currentfile.cf") ||
    (print "Cannot open $currentfile: $!\n" && return 1 );

  undef @file;
  while(<FILE>) {
    chop;
    if (! /^\s*separate\s+reports\s*$/) { push(@file, $_); }
    }
  close(FILE);
  unlink("$configdir/$currentfile.cf");
  
  open(FILE, ">$configdir/$currentfile.cf");
  print FILE join("\n", @file), "\n";
  close(FILE);
  undef @file;

  &cfg_read;

  }

##
## Sets separate report
##
sub cfg_separate {
  open(FILE, "$configdir/$currentfile.cf") ||
    (print "Cannot open $currentfile: $!\n" && return 1 );

  undef @file;
  while(<FILE>) {
    chop;
    if (! /^\s*separate\s+reports\s*$/) { push(@file, $_); }
    }
  push(@file, 'separate reports');
  close(FILE);
  unlink("$configdir/$currentfile.cf");
  
  open(FILE, ">$configdir/$currentfile.cf");
  print FILE join("\n", @file), "\n";
  close(FILE);
  undef @file;

  &cfg_read;

  }

##
## Sets ascii reports (unsets html, actually)
##
sub cfg_ascii {
  open(FILE, "$configdir/$currentfile.cf") ||
    (print "Cannot open $currentfile: $!\n" && return 1 );

  undef @file;
  while(<FILE>) {
    chop;
    if (! /^\s*html\s+reports\s*$/) { push(@file, $_); }
    }
  close(FILE);
  unlink("$configdir/$currentfile.cf");
  
  open(FILE, ">$configdir/$currentfile.cf");
  print FILE join("\n", @file), "\n";
  close(FILE);
  undef @file;

  &cfg_read;

  }

##
## Sets ascii reports
##
sub cfg_html {
  open(FILE, "$configdir/$currentfile.cf") ||
    (print "Cannot open $currentfile: $!\n" && return 1 );

  undef @file;
  while(<FILE>) {
    chop;
    if (! /^\s*html\s+reports\s*$/) { push(@file, $_); }
    }
  close(FILE);
  push(@file, 'html reports');
  unlink("$configdir/$currentfile.cf");
  
  open(FILE, ">$configdir/$currentfile.cf");
  print FILE join("\n", @file), "\n";
  close(FILE);
  undef @file;

  &cfg_read;

  }

##
## Sets the report name format
##
sub cfg_reportname {
  local($name) = $_[0];
  
  if (!$name) {
    print "  Usage: \"reportname name\"\n\n";
    return 1;
    }

  open(FILE, "$configdir/$currentfile.cf") ||
    (print "Cannot open $currentfile: $!\n" && return 1 );

  undef @file;
  while(<FILE>) {
    chop;
    if (! /^\s*reportname\s+/) { push(@file, $_); }
    }
  close(FILE);
  if ($name ne 'none') { push(@file, "reportname $name"); }
  unlink("$configdir/$currentfile.cf");
  
  open(FILE, ">$configdir/$currentfile.cf");
  print FILE join("\n", @file), "\n";
  close(FILE);
  undef @file;

  &cfg_read;

  }

##
## Sets the report directory
##
sub cfg_reportdir {
  local($dir) = $_[0];
  
  if (!$dir) {
    print "  Usage: \"reportdir directory\"\n\n";
    return 1;
    }

  open(FILE, "$configdir/$currentfile.cf") ||
    (print "Cannot open $currentfile: $!\n" && return 1 );

  undef @file;
  while(<FILE>) {
    chop;
    if (! /^\s*reportdir\s+/) { push(@file, $_); }
    }
  close(FILE);
  if ($dir ne 'none') { push(@file, "reportdir $dir"); }
  unlink("$configdir/$currentfile.cf");
  
  open(FILE, ">$configdir/$currentfile.cf");
  print FILE join("\n", @file), "\n";
  close(FILE);
  undef @file;

  &cfg_read;

  }

##
## Sets a machine entry
##
sub cfg_machine {
  local($host, $perl, $modlist) = @_;
  
  if (!$host || !$perl) {
    print "  Usage: \"machine hostname1,hostname2,hostnameN pathtoperl modlist\"\n\n";
    return 1;
    }

  open(FILE, "$configdir/$currentfile.cf") ||
    (print "Cannot open $currentfile: $!\n" && return 1 );

  undef @file;
  while(<FILE>) {
    chop;
    if (! /^\s*machine\s+$host/) { push(@file, $_); }
    }
  close(FILE);
  if ($perl ne 'delete') {
    push(@file, "machine $host $perl $modlist");
    }
  unlink("$configdir/$currentfile.cf");
  
  open(FILE, ">$configdir/$currentfile.cf");
  print FILE join("\n", @file), "\n";
  close(FILE);
  undef @file;

  &cfg_read;

  }

##
## Parses a modlist and returns either -1 on error or
## an array of files to run.  It's here to check for
## errors in the modlist.
##
sub parsemodlist {
  local($list) = shift;
  local($moduledir) = shift;
  local($delim1, $delim2, $negatestr) = ('\[', ':', '-');
  local($flag, @runs, %n, @a, $modulename,
        $neg, $pod, %files, $gotlist, $dir, @tmp);
  
  ## if they didn't give a modlist, or specified any,
  ## we make one up for them.
  if ((!$list) || ($list eq 'any')) {
    opendir(MODDIR, $moduledir);
    while ($_ = readdir(MODDIR)) {
      if (/^\.+$/) { next; } ## skip . and ..
      if (-d "$moduledir/$_") {
        push(@tmp, $_);
        }
      }
    $list = join(',', sort @tmp);
    closedir(MODDIR);
    }
  
  
  foreach $_ (split(',', $list)) {

    undef $neg;
    undef $gotlist;
    undef %n;
    undef @a;
    undef @runs;
    ($modulename, $_) = split($delim1);

    ## axe the last ']'
    chop;

    ## get a list of which things to run
    foreach $_ (split($delim2)) {
	 $gotlist = 1;
	 if (substr($_, 0, 1) eq $negatestr) {
        if ($pos) { $files{'__error__'} = 1; return %files; }
        $neg = 1;
        $n{substr($_, 1)} = 1;
        }
	 else {
	   if ($neg) { $files{'__error__'} = 1; return %files; }
	   $pos = 1;
        push(@a, $_);
        }
	 }

    if ($gotlist) {
	 if ($neg) {
	   foreach $dir ('', "$os{'name'}/common", "$os{'name'}/$os{'version'}") {
     	opendir(DIR, "$moduledir/$modulename/$dir");
     	while ($_ = readdir(DIR)) {
            if (( substr($_, (length($_) -3)) eq '.pl' ) &&
          	 ( !$n{substr($_, 0, (length($_) -3))} )) {
              push(@runs, "$modulename/$dir/$_");
              $runs[$#runs] =~ s/\/+/\//g;
              }
            }
     	closedir(DIR);
     	}
	   }
	 else {
	   foreach $dir ('', "$os{'name'}/common", "$os{'name'}/$os{'version'}") {
     	foreach $scan (@a) {
     	  if (-e "$moduledir/$modulename/$dir/$scan.pl") {
              push(@runs, "$modulename/$dir/$scan.pl");
              $runs[$#runs] =~ s/\/+/\//g;
              }
     	  }
     	}
	   }
	 }
    else {
	 foreach $dir ('', "$os{'name'}/common", "$os{'name'}/$os{'version'}") {
        opendir(DIR, "$moduledir/$modulename/$dir");
        while ($_ = readdir(DIR)) {
     	if (substr($_, (length($_) -3)) eq '.pl') {
     	  push(@runs, "$modulename/$dir/$_");
     	  $runs[$#runs] =~ s/\/+/\//g;
     	  }
     	}
        closedir(DIR);
        }
	 }
    $files{$modulename} = join(' ', @runs);
    }
  return %files;
  }
