#!/usr/bin/perl -w

eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
    if 0; # not running under some shell

#$Id: ncat_report.PL,v 2.17 2002/03/21 16:24:41 gmj Exp $
#
# ncat_report - produce reports from ncat output
#
#
# Copyright (C) 2002  George M. Jones <gmj@users.sourceforge.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

=head1 NAME

ncat_report - produce reports from ncat output.

=head1 SYNOPSIS

B<ncat_report> [OPTIONS] I<xxx.ncat_out.txt [yyy.ncat_out.txt ...]>

=head1 DESCRIPTION

B<ncat_report> reads a rules file (default F</etc/ncat.conf>)
and one or more ncat output files.  It produces text and HTML reports
($config.html, $config.ncat_report.txt) listing rules violations found per
the config file.  It also produces "fix" files ($config.ncat_fix.txt) that are
suitable for cut-and-pasting to fix problems identified where
possible.

=head1 OPTIONS

=over 8

=item B<-r, --rules>

The C<--rules> flag allows the specification of an alternate rules file.

=item B<-s, --sortorder=value>

The C<--sortorder> flag allows the specification of the field that reports are sorted by.
Possible values are "importance" (default), "passfail","rule","device","line","instance".

=item B<-V, --version>

The C<--version> option displays the current program version.


=back

=head1 RETURN VALUE

0  - success
>0 - some error occurred

=head1 FILES

=over 8

 $config    			- the config file that was pulled
 $config.ncat_out.txt    	- a passwd style file with raw results
 $config.ncat_fix.txt    	- commands to correct problems found
 $config.ncat_report.txt    	- a simple text report with statistics
 $config.html		    	- an HTML version of the report w/fixes
 rules.html			- an HTML version of the rules applied
 index.html			- an index of the rules and html files

=back

=head1 NCAT OUTPUT FILE SYNTAX

=over 8

The ncat output file is formatted to be friendly for import to
spreadsheets and databases.  It is series of colon delimited records,
one per line.  The first line contains field names.  Each succeeding
line contains individual records.  Lines beginning with "#" are
comments and should be ignored.

Individual fields are

C<
  Config:type:rule:Instance:Line
>

where

    * Config is the name of the config that was checked.

    * Type is "Forbidden" to indicate that a forbidden rule was found
      or "Required" to indicate that a required rule was missing.

    * Rule is name of the rule per the ncat rules file.

    * Instance defines, for non-global rules, which instance
      of a class (lines, interfaces) violated the rule, for
      example "Serial0/0" or "vty".

    * Line indicates the line of the original config where
      the violation was detected.

=back

=head1 CAVEATS

=head1 BUGS

Yes.

=head1 SEE ALSO

  bin/ncat_report		- this program
  bin/rat			- audit tool main program
  bin/router-snarf		- the config puller
  bin/ncat			- the audit tool
  etc/ncat.conf	    		- file containing audit rules


=head1 AUTHOR

George M. Jones <gmj@users.sourceforge.net>

=head1 CREDIT WHERE CREDIT IS DUE

=cut

#
#  $Log: ncat_report.PL,v $
#  Revision 2.17  2002/03/21 16:24:41  gmj
#  * updated POD documentation to describe all rule syntax
#
#  Revision 2.16  2002/03/20 10:21:17  gmj
#  * Hashes imported from NCAT.pm changed names.  Updated to reflect name change.
#
#  Revision 2.15  2002/03/17 20:07:12  gmj
#  * Config parsing moved to common code in NCAT.pm
#
#  Revision 2.14  2002/03/04 17:03:53  gmj
#  * Added ConfigLineSkip config directive.  Defaults to '^ shutdown' to skip shutdown interfaces
#
#  Revision 2.13  2002/03/04 12:40:40  gmj
#  sort rule names in rules.html (benchmark) within category
#
#  Revision 2.12  2002/03/02 13:40:48  gmj
#  * fixed typo
#
#  Revision 2.11  2002/02/27 14:09:53  gmj
#  Intial Windows/ActiveState version
#
#  Revision 2.10.2.2  2002/02/27 13:43:09  gmj
#  * set PATH for unix
#  * Do ~ substituion for rule file path.
#  * Fix OverallScore calculation.
#
#  Revision 2.10.2.1  2002/02/25 16:09:09  joshwr1ght
#
#  First update of code to support ActiveState Perl on Windows.
#
#  Revision 2.10  2002/02/19 16:07:57  gmj
#  * Made -V synonymous with --version
#  * Count total checks, total checks passed.  Report those instead of
#    total rules.  This is more intuitive.  If we check "no ip directed
#     broadcast" on 10 interfaces, it's 10 checks, not 1 "total rule".
#
#  Revision 2.9  2002/02/05 20:05:20  gmj
#  * Numerous documentation changes suggested by Anthony Williams.
#  * Don't output HTML links to config reports that were not generated
#    due to no matching rules.
#
#  Revision 2.8  2002/02/04 23:11:56  gmj
#  Add RCS Id to --version output
#
#  Revision 2.7  2002/02/04 17:06:17  jnssf
#  INSTALL.txt: missing directions on ensuring the PATH variable had the PREFIX/bin in it so that rat can find its supporting files
#
#  Timestamp/skew issues with some files, yet no differences
#
#  Revision 2.6  2002/02/04 16:49:19  jnssf
#  Makefile.PL
#  	Created another environment variable for PREFIX so that we can
#  	auto-reference the etc directory where the ncat.conf file is.
#  All Others
#  	Auto-referenced the PREFIX/etc for ncat.conf as the default
#  	location, plus updated documentation indicating that lib had
#  	a default directory for ncat.conf when it was no longer the
#  	case
#
#  Revision 2.5  2002/02/01 13:15:40  gmj
#  Changed authors email address
#
#  Revision 2.4  2002/02/01 11:22:19  gmj
#  * Upated meta-tags to insure that report pages are not cached by browsers
#  * Don't print fix scripts at the end of all.html
#
#  Revision 2.3  2002/01/29 16:46:23  jnssf
#  Modifications for automatically changing @INC and allowing the
#  NCAT.pm file to be placed in $(PREFIX)/lib for ease of us (non root
#  install)
#
#  Revision 2.2  2002/01/29 13:17:34  gmj
#  Fixed problem with output grouping.  Was splitting on wrong char.
#
#  Revision 2.1  2002/01/24 21:52:08  gmj
#  merge back to mainline
#
#  Revision 2.0.2.4  2002/01/24 21:31:56  gmj
#  * Updated copyright
#  * Changed default "split" character from ":" to ";" to allow cisco ios subinteraces to work
#
#  Revision 2.0.2.3  2002/01/23 22:13:48  gmj
#  * Added (copied) documentation for rules file syntax.
#  * Added config file global option ConfigOutputGroups and docs
#  * Only list configs actualy processed in index.html (was listing
#    all .html files in output dir)
#  * Check to see if any rules applied.  Report configs that we skip
#    because there are no matching rules.
#
#  Revision 2.0.2.2  2002/01/07 19:40:33  gmj
#  * Added parsing for global config lines.
#
#  * Moved all hard-coded organization specific info out of ncat_report
#  binary and into the config file.  This was done to facilitate
#  customization to the needs of local organizations.
#
#  * Made use of/search for config guide optional, subject to config options.
#
#  Revision 2.0.2.1  2001/12/21 17:37:01  gmj
#  Updated to use CIS Terms of Service
#
#  Revision 2.0  2001/12/21 15:23:36  gmj
#  Level set version to 2.0 prior to branch for Center for Internet Security.
#
#  Revision 1.9  2001/12/21 15:17:10  gmj
#  Use program version number as version of benchmark document instead of RCS version of ncat.conf
#
#  Revision 1.8  2001/12/21 14:17:02  gmj
#  Search for Guide, lnink to it.  Insert PDF links
#
#  Revision 1.7.2.1  2001/12/21 13:09:54  gmj
#  * Go look for the Guide and symlink to it.  Warn if not found.
#  * Output hyperlinks into PDF documents.
#
#  Revision 1.7  2001/12/20 08:00:47  gmj
#  Rules.html is not the benchmark document.
#  Added intro/benchmark and rat/feedback/credits sections.
#  Cleanup of rule descriptions, spelling, grammer, etc.
#
#  Revision 1.6  2001/12/18 15:38:32  gmj
#  * use Sys::hostname to get local hostname.
#  * Extract RulesVersion from ncat.conf file to use as benchmark
#    version number.
#  * List Level 1 (default) and Level 2 (non-default) rules separately.
#  * Added CIS header, "Intro" Section, "Benchmark and RAT" section,
#    "Feedback" section, info on downloading, "Credits" section.
#
#  Revision 1.5  2001/12/14 11:27:26  gmj
#  Added --verbose flag.
#
#  Revision 1.4  2001/12/11 21:50:37  jnssf
#  time skew on repository forced an update on NCAT/Makefile
#  and ncat_report now has --version
#
#  Revision 1.3  2001/12/11 14:07:02  gmj
#  Documented short (single character) versions of command line switches.
#  Added --help.
#
#  Revision 1.2  2001/11/26 14:58:04  gmj
#  Fix minor problems with option parsing.
#
#  Revision 1.1.1.1  2001/11/14 21:40:18  gmj
#  Initial CVS checkin.
#
#
#  Revision 2.2  2001/11/07 07:32:40  jns
#  removed the initialization
#
#  Revision 2.1  2001/11/07 07:22:41  jns
#  modifications for $progname
#  autoflush on STDERR
#  added the ENV variables
#
#  Revision 2.0  2001/11/07 01:43:30  jns
#  y
#  branch for jns work
#
#  Revision 1.9  2001/11/03 00:49:18  gjones
#  - Use GetOptions
#  - Change default sort order for non-numeric fields in reports.
#  - Round up overall score.
#  - Put html references in rules.html
#  - Add html anchor/references when we see http:// in description.
#
#  Revision 1.8  2001/11/02 16:25:33  gjones
#  - Added -sortorder switch
#  - Added 0-10 scoring
#  - Fixed problems with merged ("all") report.
#
#  Revision 1.7  2001/11/01 20:03:13  gjones
#  - Added device name to output.
#  - Output a single summary report ("all").
#
#  Revision 1.6  2001/10/10 17:03:32  gjones
#  Doc changes.
#
#  Revision 1.5  2001/10/09 17:50:39  gjones
#  Added HTML reports, rules.
#
#  Revision 1.4  2001/10/06 06:23:17  gjones
#  Final updates for rat 0.1 release.  Documentation.
#  Also, slight change to scoring algorithm.
#
#  Revision 1.3  2001/10/06 01:44:41  gjones
#  Cleanup and bug fixing for release.
#
#  Revision 1.2  2001/10/05 17:20:03  gjones
#  Generate fix scripts.
#
#  Revision 1.1  2001/10/05 14:57:53  gjones
#  Initial revision
#

$ENV{'SHELL'}  = '/bin/sh';
$ENV{'IFS'}    = '';

use lib '/usr/lib';

use File::Copy;
use Getopt::Long;
use IO::Handle;
use NCAT;
use Config;
use strict;

use vars qw($opt_debug);

# Imports

use NCAT qw(&ParseRules
	    %RulesDefined
	    %RuleClassesDefined
	    %RuleFieldValues
	    %ConfigGlobalsDefined
	    %ConfigGlobalFieldValues
	    );

# Exports
$opt_debug = '';

# Locals

my (
    $RulesVersion,
    $desc,		$class,
    $text,		$opt_dir,
    $home,              $name,
    $opt_rules,         $opt_limitrulesto, 
    $opt_limitclassto,  $opt_onlypass,
    $opt_sortorder,
    $opt_version,
    $opt_help,		$opt_verbose,

    $Desc,
    $html,              $html_bg,
    $instance,
    $line,              $lineno,
    $required,
    
    $rules_mtime,       $target,

    $AuditDate,         $Config,
    $ConfigName,
    $Text,              $This,
    $Type,
    $IfName,            $InstanceName,
    $Line,              $LineName,
    $Offset,            $OverallScore,
    $PercentPassed,     $PercentWeighted,
    $ReportName,        $RulesName,
    $TotalPossibleWeightedScore,
    $TotalRules,
    $TotalRulesPassed,
    $TotalRulesFailed,  $TotalWeighted,
    $TotalWeightedScore,$found,$path,
    $TotalChecks, $ChecksPassed, $ChecksFailed, $PercentChecksPassed,
    $group,
    $matched,
    $ProgVersion,	$windows,
    $loginname,		$unix,
    $classes,		$level,
    ) = ('?', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '');



my (
    @a,                 @b,
    @Lines,@DocPath,@OutputGroups,@ConfigNames,
    ) = ((), (), (),());



my (
    %AlreadySawRule,    %AuditDate,         
    %Configs,           %FixReport,
    %HTMLReport,
    %TextReport,
    %TotalRules,
    %ConfigsByGroup,
    %skipped,
    );

#test for Windows
if ($Config{'osname'} =~ /MSWin/) {
    $windows=1;
} else {
    # This may be assumptive, but it is difficult to check for the OS
    # as Unix since osname would be "solaris" or "linux" or "oddballnix"
    $unix=1;
}


# do OS specific setup
if ($windows) {

    # get login

    if ("Z$ENV{'USERNAME'}" ne "Z") {
        $loginname = $ENV{'USERNAME'};
    } else {
        $loginname = "Unknown";
    }
} else {
    $ENV{'PATH'}   = '/bin:/usr/bin:/usr/sbin'; #no funny paths
    $loginname = getlogin || getpwuid($<); # get login
}

#default values
my $progname      = $0;
$progname         =~ s,.*/,,;    # only basename left in progname
$progname         =~ s/\.\w*$//; # strip extension if any

$opt_rules        = '/etc/ncat.conf';
$opt_dir          = '/usr';
$opt_sortorder = 'importance';
$opt_debug     = '';
$RulesVersion = '?';
$ProgVersion = NCAT::Version;
#parse command line

Getopt::Long::Configure ("bundling_override");
GetOptions(
	   # Common
	   'rules|r=s',     \$opt_rules,
	   'debug|d=s',     \$opt_debug,
	   'verbose|v',     \$opt_verbose,
	   'version|V',       \$opt_version,
	   "help|h", 	    \$opt_help,

	   # ncat_report specific

	   'sortorder|s=s', \$opt_sortorder,

	  )
  or &Usage('');


# parse command line stuff

&Version if $opt_version;
&Usage("") if ($opt_help);

#

$ConfigName = shift || Usage("At least one ncat_out.txt file must be specified");

if ($windows) {
    $home = "."
} else {
    $home = $ENV{"HOME"} || $ENV{"LOGDIR"} || (getpwuid($<))[7];
}

# expand out ~ (why dosn't Perl do this ?)
$opt_rules =~ s/^~(.*)/$home$1/;

# Set config global values that are only known at runtime.

if ($windows) {
  $ConfigGlobalFieldValues{"configguidepath"} = "$opt_dir /rat/doc ../doc .. $opt_dir/doc /rat";
} else {
  $ConfigGlobalFieldValues{"configguidepath"} = "/usr/doc /usr/local/doc ~/doc ./doc";
}

$ConfigGlobalFieldValues{"configfeedbackto"} = $loginname;

# Parse the rules file.

ParseRules($opt_rules);


# Look for configuration guide if one was specified in the config file and
# symlink it to the current directory if found.

if (defined($ConfigGlobalFieldValues{"configguide"})) {

  unless (-e $ConfigGlobalFieldValues{"configguide"}) {
    printf STDERR "$progname: Guide file $ConfigGlobalFieldValues{'configguide'} ";
    printf STDERR "not found in current directory.  Searching...\n";

    $ConfigGlobalFieldValues{"configguidepath"} =~ s/~(.*)/$home$1/g; 

    for $path (split(/[\s]+/,$ConfigGlobalFieldValues{"configguidepath"})) {
      if (-e "$path/$ConfigGlobalFieldValues{'configguide'}") {
        printf STDERR "Linking to guide found at ";
        printf STDERR "$path/$ConfigGlobalFieldValues{'configguide'}\n";
        if ($windows) {
          # There is no symlink function in Windows.  Make a copy instead.
          copy("$path/$ConfigGlobalFieldValues{'configguide'}","$ConfigGlobalFieldValues{'configguide'}");
        } else {
        symlink("$path/$ConfigGlobalFieldValues{'configguide'}",$ConfigGlobalFieldValues{'configguide'});
        }
        $found = 1;
        last;
      }
    }
	
    unless ($found) {
      warn("Guide file $ConfigGlobalFieldValues{'configguide'} not found.  Hyperlinks form the rules file will fail.")
    }
  }

}

if (defined($ConfigGlobalFieldValues{"configoutputgroups"})) {
    @OutputGroups = split(/\s+/,$ConfigGlobalFieldValues{"configoutputgroups"});
} else {
    undef @OutputGroups;
}

#
# now slurp in all the configs to be checked
#

while (defined($ConfigName)) {

    # Suck in all the configs

    if (! (open(CONFIG,"<$ConfigName"))) {
	warn "Unable to open $ConfigName: $!\n";
	$ConfigName = shift;
	next;
    }
    
    $ConfigName =~ s/.*\///;  # strip leading path
    $name = $ConfigName;
    $name =~ s/.ncat_out.txt//;
    push @ConfigNames,$name;
    
    #
    # Slurp in entire config, save in $Configs.
    #

    $AuditDate{"all"} = "Unknown";
    $AuditDate{$ConfigName} = "Unknown";

    while(<CONFIG>) {
	next if (/^Config;/); # omit header lines

	if (/#AuditDate=(.*)/) {
	    $AuditDate{$ConfigName} = $1;
	    $AuditDate{"all"} = $1;  # cheat. Use the last one.
	    next;
	} elsif (/^#/) {
	    next;
	}

	$Configs{$ConfigName} .= $_;
	$Configs{"all"} .= $_;
    }
    
    close(CONFIG);
    
    # Generate reports, fixex, etc.
    
    MakeReports($ConfigName);
    SaveReports($ConfigName);
    HTMLRules();

    # See if config size and time match the one that was used to
    # check.  Warn if not.
    
    $ConfigName = shift;
}

MakeReports("all");
SaveReports("all");

# Generate HTML index file

open (INDEX, ">index.html") || die "Can't open index.html: $!";
print INDEX "
<HTML>
<HEAD>
<META HTTP-EQUIV='Pragma' CONTENT='no-cache'>
<META HTTP-EQUIV='Expires' CONTENT='-1'>
<BODY bgcolor='FFFFFF'>
<CENTER><B>$ConfigGlobalFieldValues{'configorganization'}<br>
Security Audit Report</B></CENTER><p>
<CENTER><B>For $ConfigGlobalFieldValues{'configplatforms'}</B></CENTER><p>
<p>
<a href='rules.html'>Description of Rules</a>
<p>
<a href='all.html'>Combined Report (all rules on all devices)</a>
<p>
";

#
# Create listings of reports by output groups
#


if (@OutputGroups) {

    $ConfigsByGroup{"Others"} = "";

    for $ConfigName (@ConfigNames) {
	
	$matched = 0;
	
	for $group (@OutputGroups) {
	    $ConfigsByGroup{$group} = "" unless defined ($ConfigsByGroup{$group});
	    
	    if ($ConfigName =~ /$group/i) {
		$ConfigsByGroup{$group} .= "$ConfigName:";
		$matched = 1;
		next;
	    }
	}    
	
	$ConfigsByGroup{"Others"} .= "$ConfigName:" unless ($matched);
    }
    
#
# Print groupings
#
    
    for $group (@OutputGroups,"Others") {
	
	next if ($ConfigsByGroup{$group} eq "");
	print INDEX "Individual Reports for $group:";
	
	for $ConfigName (split(/:/,$ConfigsByGroup{$group})) {
    	    next if (defined($skipped{$ConfigName}));
	    $html = "$ConfigName.html";
	    print INDEX "<a href='" . $html . "'>$ConfigName</a>,\n";
	}
	
	print INDEX "<p><p>";
    }
}

print INDEX "All Individual Reports:";
for $ConfigName (@ConfigNames) {
    next if (defined($skipped{$ConfigName}));
    $html = "$ConfigName.html";
    print INDEX "<a href='" . $html . "'>$ConfigName</a>,\n";
}

close(INDEX);


exit 0;


#
# Generate Reports
#

sub MakeReports {
   my($ReportName) = @_;
   my($Errors) = 0;
   my($rule);
   my(%RulesThatFailed) = ();
   my($importance) = ""; # keep -w happy
   my($pass_fail) = ""; # keep -w happy
   my($skipped);

   unless (defined $Configs{$ReportName} and $Configs{$ReportName} =~ /\n/) {
       $TotalRules{$ReportName} = 0;
       printf STDERR "$progname: skipping $ReportName.  No matching rules found.\n";
       $skipped = $ReportName;
       $skipped =~ s/.ncat_out.txt//;
       $skipped{$skipped} = $skipped;
       return;
   }

   # pull audit date out of comments if there.

   $AuditDate = $AuditDate{$ReportName};

   @Lines = split(/\n/,$Configs{$ReportName});


   $ReportName =~ s/.ncat_out.txt//;


   # init reports.  We will save the various reports in these.

   $TextReport{$ReportName} = "";
   $FixReport{$ReportName} = "";
   $HTMLReport{$ReportName} = "";


   # Process all lines in priority order

   $TotalRules = 0;
   $TotalRulesFailed = 0;
   $TotalChecks = 0;
   $ChecksPassed = 0;

   $TotalPossibleWeightedScore = 0;
   $TotalWeightedScore = 0;
 

   #
   # Set headers
   #
   

   # text report headers
   $TextReport{$ReportName} = sprintf "Audit report for /$ReportName/ on $AuditDate\n\n";       
   $TextReport{$ReportName} = sprintf "%-10s %-10s %-35s %-15s %5s %15s\n","Importance","Pass/Fail","Rule","Device", "Line#","Instance";

   # HTML Headers
   $HTMLReport{$ReportName} = "<HTML>
<HEAD>
<META HTTP-EQUIV='Pragma' CONTENT='no-cache'>
<META HTTP-EQUIV='Expires' CONTENT='-1'>
</HEAD>
<BODY bgcolor='FFFFFF'>
<CENTER><B><U>$ReportName</U></B>
<P>Audit Date: $AuditDate<P></CENTER>
<table width=100% CELLPADDING=0 CELLSPACING=0 BORDER=0>

<TR>
  <TR>
  <TD><B>Importance</B></TD>
  <TD><B>Pass/Fail</B></TD>
  <TD><B>Rule Name</B></TD>
  <TD><B>Device</B></TD>
  <TD><B>Instance</B></TD>
  <TD><B>Line Number.</B></TD>
  </TR>
  <hr>
";

   # fix file headers
   $FixReport{$ReportName} =  "! The following commands may be entered into the router to fix problems found.
! They must be entered in config mode (IOS).
! Fixes which require specific information (such as uplink interface device name or specific
! access list numbers) are listed but commented out.  Examine them, edit and uncommment.
!
! THESE CHANGES ARE ONLY RECOMMENDATIONS.
! CHECK THESE COMMANDS BY HAND BEFORE EXECUTING.  THEY MAY BE WRONG.  THEY MAY BREAK YOUR ROUTER.
! YOU ASSUME FULL RESPONSIBILITY FOR THE APPLICATION OF THESE CHANGES.
!

";
 
   %AlreadySawRule = ();

   print "Lines: >>>" . join("\n",@Lines) . "<<<\n" if ($opt_debug =~ /line/);

   for $Line (sort sortorder @Lines) {
       next if ($Line =~ /^#/);
       ($Config,$rule,$pass_fail,$importance,$instance,$line) = split(/;/,$Line);
print  "Config=/$Config/,rule=/$rule/,pass_fail=/$pass_fail/,importance=/$importance/,instance=/$instance/,line=/$line/\n" if ($opt_debug =~ /line/);
       $instance = defined($instance) ? $instance : "";
       $line = defined($line) ? $line : "";

       	unless (defined $AlreadySawRule{$rule}) {
	    $TotalRules++;
	    $TotalRulesFailed++ if ($pass_fail =~ /fail/i);
	}


       $AlreadySawRule{$rule} = 1;
       $TotalPossibleWeightedScore += $importance;
       $TotalChecks += 1;

       if ($pass_fail =~ /pass/i) {
	   $TotalWeightedScore += $importance;
	   $ChecksPassed += 1;
	   $pass_fail = lc($pass_fail);
	   $html_bg = "FFFFFF";
       } else {
	   $pass_fail = uc($pass_fail);
	   $FixReport{$ReportName} .= MakeFix($rule,$instance);
	   $html_bg = "FF0000";
	   $instance = "n/a" if ($instance eq "");
       } # else failed
		
       $TextReport{$ReportName} .= sprintf "%-10d %-10s %-35s %-15s %-5s %15s\n",
		$importance,
		$pass_fail,
		$rule,
		$Config,
		$line,
		$instance;



       $target = $rule;
       $target =~ s/ //g;

       $HTMLReport{$ReportName} .= "       
  <TR BGCOLOR=$html_bg>
  <TD VALIGN='TOP'>$importance</TD>
  <TD VALIGN='TOP'>$pass_fail</TD>
  <TD><A HREF=rules.html#$target>" . $RuleFieldValues{"$rule:rulename"} . "</A></TD>
  <TD VALIGN='TOP'>$Config</TD>
  <TD VALIGN='TOP'>$instance</TD>
  <TD VALIGN='TOP'>$line</TD>
  </TR>
";

   }

   $HTMLReport{$ReportName} .= "
</TR>
</table>
<hr>
";

   $TotalRules{$ReportName} = $TotalRules;

   return if ($TotalRules == 0);

       
   #
   # Trailers
   #

   $ChecksFailed = $TotalChecks - $ChecksPassed;
   $PercentChecksPassed = int((100*($TotalChecks-$ChecksFailed))/$TotalChecks); 
   $TotalRulesPassed = $TotalRules-$TotalRulesFailed;
   $PercentPassed = int((100*($TotalRules-$TotalRulesFailed))/$TotalRules);
   $PercentWeighted = int((100*($TotalWeightedScore))/$TotalPossibleWeightedScore);
   $OverallScore = ($PercentWeighted)/10;

   # Text Report
   $TextReport{$ReportName} .= sprintf "\n";
   $TextReport{$ReportName} .= sprintf "Summary for $ReportName\n\n";
   $TextReport{$ReportName} .= sprintf "%-7s %-7s %-7s %-7s\n","#Checks","#Passed","#Failed","%Passed";
   $TextReport{$ReportName} .= sprintf "%-7d %-7d %-7d %-7d\n",
      $TotalChecks,
      $ChecksPassed,
      $ChecksFailed,
      $PercentChecksPassed;


   $TextReport{$ReportName} .= sprintf "\n";
   $TextReport{$ReportName} .= sprintf "%-20s %-18s %-14s\n","PerfectWeightedScore","ActualWeighedScore","%WeightedScore";
   $TextReport{$ReportName} .= sprintf "%-20d %-18d %-14d\n\n",
      $TotalPossibleWeightedScore,
      $TotalWeightedScore,
      $PercentWeighted;


   $TextReport{$ReportName} .= sprintf "%-12s\n","Overall Score (0-10)";
   $TextReport{$ReportName} .= sprintf "%-12d\n",
      $OverallScore;


   $TextReport{$ReportName} .= sprintf "\n";
   $TextReport{$ReportName} .= sprintf "Note: PerfectWeightedScore is the sum of the importance value of all rules.\n";
   $TextReport{$ReportName} .= sprintf "ActualWeightedScore is the sum of the importance value of all rules passed,\n";
   $TextReport{$ReportName} .= sprintf "minus the sum of the importance each instance of a rule failed\n";
   $TextReport{$ReportName} .= sprintf "\n\n\n\n";
   # HTML Report

   $HTMLReport{$ReportName} .="
<CENTER><b>Summary for $ReportName</b></CENTER><p>

<table width=100% CELLPADDING=0 CELLSPACING=0 BORDER=0>
<TR>
  <TR>
  <TD><B>#Checks</B></TD>
  <TD><B>#Passed</B></TD>
  <TD><B>#Failed</B></TD>
  <TD><B>%Passed</B></TD>
  </TR>

  <TR>
  <TD>$TotalChecks</TD>
  <TD>$ChecksPassed</TD>
  <TD>$ChecksFailed</TD>
  <TD>$PercentChecksPassed</TD>
  </TR>
</TR>
</table>
<p>

<table width=100% CELLPADDING=0 CELLSPACING=0 BORDER=0>
<TR>
  <TR>
  <TD><B>Perfect Weighted Score</B></TD>
  <TD><B>Actual Weighted Score</B></TD>
  <TD><B>%Weighted Score</B></TD>
  </TR>

  <TR>
  <TD>$TotalPossibleWeightedScore</TD>
  <TD>$TotalWeightedScore</TD>
  <TD>$PercentWeighted</TD>
  </TR>
</TR>
</table>
<p>

<table width=100% CELLPADDING=0 CELLSPACING=0 BORDER=0>
<TR>
  <TR>
  <TD><B>Ovarall Score (0-10)</B></TD>
  </TR>

  <TR>
  <TD>$OverallScore</TD>
  </TR>
</TR>
</table>
<p>
Note: PerfectWeightedScore is the sum of the importance value of all rules.
ActualWeightedScore is the sum of the importance value of all rules passed,
minus the sum of the importance each instance of a rule failed.
<hr>
";

  unless ($ReportName eq "all") {
     $HTMLReport{$ReportName} .="
<CENTER><b>Fix Script for $ReportName</b></CENTER><p>
<pre>
$FixReport{$ReportName}
</pre>
";
  }


   # HTML Footers
   $HTMLReport{$ReportName} .= "
</body>
</html>";

}


#
# Generate a fix for a particular rule violation
#

sub MakeFix {
    my($Rule,$Instance) = @_;
    my($Fix);

    if (defined $RuleFieldValues{"$Rule:rulefix"}) {

	$Fix = $RuleFieldValues{"$Rule:rulefix"} . "\n";

	# comment out fixes that need to be hand edited

	if ($Fix =~ /EDIT-BY-HAND/) {
	    $Fix =~ s/^/\!/mg;
	}

	# substitute actual instances if we have them

	if ($Fix =~ /INSTANCE/ and 
	    defined $Instance and 
	    $Instance ne ""
	   ) {
	    $Fix =~ s/INSTANCE/$Instance/gs;
	}
		  
    } else {

	$Fix .= "!\n! No fix defined for rule /$Rule/\n!\n";

    } # no fix defined

    return $Fix;
}

#
# Save all reports
#

sub SaveReports {
    my($Config) = @_;
    my(%Suffix,$Report);


    $Config =~ s/.ncat_out.txt//;

    return if ((not defined $TotalRules{$Config}) or $TotalRules{$Config} == 0);
    
    $Suffix{"Fix"} = ".ncat_fix.txt";
    $Suffix{"Text"} = ".ncat_report.txt";
    $Suffix{"HTML"} = ".html";
    
    for $Type ("Fix","Text","HTML") {
	$This = "\$$Type" . 'Report{$Config}';
	$Text = eval $This;

	next if ($Text eq "");

	$ReportName = "$Config$Suffix{$Type}";
	open(REPORT,">$ReportName") 
	  || die "Can't open $ReportName for writing: $!";

	printf STDERR "ncat_report: writing $ReportName.\n";
	print REPORT $Text 
	  || die "Can't write to $ReportName: $!";

	close(REPORT);
	
    }
}

#
# Create HTML version of rulebase
#

sub HTMLRules {
    my($now);
    $now = gmtime(time);

    open(RULES,">rules.html") || die "Unable to open rules.html for writing: $!";
    printf STDERR "ncat_report: writing rules.html ($ConfigGlobalFieldValues{'configrulesalias'}).\n";
    print RULES "
<HTML>
<HEAD>
<META HTTP-EQUIV='Pragma' CONTENT='no-cache'>
<META HTTP-EQUIV='Expires' CONTENT='-1'>
<TITLE>$ConfigGlobalFieldValues{'configdocumenttype'} For $ConfigGlobalFieldValues{'configplatforms'}.  
$ConfigGlobalFieldValues{'configdocumenttype'} version $ConfigGlobalFieldValues{'configversion'}</TITLE>
</HEAD>
<BODY bgcolor='FFFFFF'>
<CENTER>
<h1>$ConfigGlobalFieldValues{'configorganization'}<br>
$ConfigGlobalFieldValues{'configdocumenttype'} Version $ConfigGlobalFieldValues{'configversion'}<br>
For $ConfigGlobalFieldValues{'configplatforms'}</h1>

<h2>$ConfigGlobalFieldValues{'configfeedbackto'}</h2>

</CENTER><p>
$ConfigGlobalFieldValues{'configintrotext'}
</BLOCKQUOTE>
<h2>The $ConfigGlobalFieldValues{'configdocumenttype'}</h2>
<BLOCKQUOTE>

";  


    foreach $level (1..2) {

	print RULES "
<CENTER><B>Level $level $ConfigGlobalFieldValues{'configdocumenttype'} Rules</B></CENTER><p>
<dl>
";
	
	foreach $name (sort keys %RulesDefined) {
	    
	    # skip rules that are not in the "default" class
	    
	    if ($level == 1) {
		next unless defined($RuleFieldValues{"$name:ruleclass:default"});
	    } elsif ($level == 2) {
		next if defined($RuleFieldValues{"$name:ruleclass:default"});
	    } else {
		die "unknown level $level";
	    }
	    
	    # spit out this rule description
	    
	    $desc = $RuleFieldValues{"$name:ruledescription"};
	    
	    $target = $name;
	    $target =~ s/ //g;
	    $desc =~ s/(http:\/\/\S+)/<a href=\"$1\">$1<\/a>/;
	    
	    $desc =~ s/(\S+.pdf\#\S+)/<a href=\"$1\">$1<\/a>/;
	    $desc =~ s/\>rscg\.pdf\#/\>Router Security Configuration Guide, /;
	    
	    $classes = "";
	    
	    foreach $class (keys %RuleClassesDefined) {
		
		next unless defined($RuleFieldValues{"$name:ruleclass:$class"});
		$classes .= "$class, ";
		
	    } # foreach class
	    
	    $classes =~ s/default, /all, /;
	    
	    $classes =~ s/, $//;
	    
	    
	    print RULES "
<dt><A name=$target><b>" . $RuleFieldValues{"$name:rulename"} . "</b></a>
<dd>$desc
<p>
Rule applies to: $classes
<p>
";
	    
	} # foreach name
	
	print RULES "
</dl>
";
    } # foreach level


    print RULES << "EORULES";

$ConfigGlobalFieldValues{'configtrailingtext'}

This document was automatically generated from version $ConfigGlobalFieldValues{'configversion'} of ncat.conf
on $now.
<b>ncat.conf</b> contains the precise definitions (regular
expressions) for each rule.  By default, it is installed in
/etc/ncat.conf during the installation of rat.<p>


EORULES

    close(RULES);
    if ($windows) {
      # There is no symlink function in Windows.  Make a copy instead.
      copy("rules.html","$ConfigGlobalFieldValues{'configrulesalias'}");
    } else {
      symlink("rules.html","$ConfigGlobalFieldValues{'configrulesalias'}");
    }
}

#
# Sort order
#

sub sortorder {
    return -1
      unless ($a =~ /;(PASS|FAIL);/i and
	      $b =~ /;(PASS|FAIL);/i);

    @a = split(/;/,$a);
    @b = split(/;/,$b);

    if ($opt_sortorder =~ /^importance$/i) {
	- ($a[3] <=> $b[3]);
    } elsif ($opt_sortorder =~ /^passfail$/i) {
	$a[2] cmp $b[2];
    } elsif ($opt_sortorder =~ /^rule$/i) {
	$a[1] cmp $b[1];
    } elsif ($opt_sortorder =~ /^device$/i) {
	$a[0] cmp $b[0];
    } elsif ($opt_sortorder =~ /^line$/i) {
	$a[5] = 0 unless defined ($a[5]);
	$b[5] = 0 unless defined ($b[5]);
	- ($a[5] <=> $b[5]);
    } elsif ($opt_sortorder =~ /^instance$/i) {
	$a[4] = "" unless defined ($a[4]);
	$b[4] = "" unless defined ($b[4]);
	$a[4] cmp $b[4];
    } else {
	die "unknown sort order: $opt_sortorder";
    }
 
}

sub Version {


    print STDERR "This is $progname version ", NCAT::Version, "\n";
    print STDERR '$Id: ncat_report.PL,v 2.17 2002/03/21 16:24:41 gmj Exp $';
    print STDERR "\n\n";
    print STDERR "Copyright 2002, George M. Jones\n";
    print STDERR "\n";
    print STDERR "This program is free software; you can redistribute it and/or\n";
    print STDERR "modify it under the same terms as Perl itself\n";
    exit(0);
}


sub Usage {
  my($msg) = @_;

  select(STDERR);
  autoflush STDERR 1;

  print STDERR "$progname: $msg\n" unless ($msg eq "");
  print "Usage:\n";
  print "  $progname [options] \$foo.ncat_out.txt [\$bar.ncat_out.txt...]\n";
  print "    -r, --rules RULES_FILE\n";  
  print "    -s, --sortorder SORT_ORDER\n";  
  print "    -d, --debug DEBUG_OPTIONS\n";  
  print "    -V, --version\n";  

  exit(1);
}

