#!/usr/bin/perl -w

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


#$Id: ncat.PL,v 2.14 2002/03/21 16:24:40 gmj Exp $
#
# ncat - Network Config Audit Tool for IOS (and other) configs
#
#
# 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 - Network Config Audit Tool for IOS (and other) configs

=head1 SYNOPSIS

B<ncat> [OPTIONS] I<config [config ...]>

=head1 DESCRIPTION

B<ncat> reads a rules file (default F</etc/ncat.conf>)
and checks one or more config files specified on the command line
against rules found in the rules file.  Rules specify that a particular
chunk of text is either required or forbidden.  Once all rules have been
checked, a report is output listing violations.

There are some special features that apply if the file being checked
is a CISCO IOS configuration. There are special rules (see
below) that allow for parsing of per-interface and per-line
configurations.



=head1 OPTIONS

=over 8

=item B<-r, --rules>

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

=item B<-l, --limitrulesto>

The C<--limitrulesto> allows the commandline specification of a regular
expression to limit the rules that are checked.  The name of the rule
must match the regexp specified or the rule is skipped.  You might
try something like

  --limitrulesto=finger

or

   --limitrulesto='finger\|syslog'

=item B<-c, --limitclassto>

The C<--limitclassto> allows the command line specification of a regular
expression to limit the rules that are checked.  The class of the rule
must match the regexp specified or the rule is skipped.  You might
try something like

  --limitclassto=access
  --limitclassto=localrules
  --limitclassto=access,logging,aaa
  --limitclassto='access\|logging\|local.*'

See the rules file for definition of rule classes.  By default, only rules
matching the class "default" are checked.  "all" is  synonym for ".*".
You can give a "normal" comma separated list of classes that you want
to check because "," is treated as a synonym for the regular 
expression or ("|").

=item B<-p, --onlypass>

The C<--onlypass> flag indicates flag indicates that only passing rules
should be reported.  It may not be combined with C<--onlyfail>

=item B<-f, --onlyfail>

The C<--onlyfail> flag indicates flag indicates that only failing rules
should be reported.  It may not be combined with C<--onlypass>

=item B<-V, --version>

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

=back

=head1 NCAT CONFIG FILE SYNTAX

=over 8

The ncat config file or "rules file" (ncat.conf/ncat.conf.MASTER)
contains four different types of information.  "Rules" define rules to
be checked, "ConfigClass" specifies groups of optional rules,
"ConfigLocal" specified local configuration value, and "ConfigGlobal"
specifies global configuration options.

=head2 Rules Syntax

The rules file contains a series of records defining rules to be checked.
Each Record begins with "RuleName:..." field and continues until the next "RuleName:.."
field or end-of-file.  Each record consists of a number of named fields.  The
fields begin with an alphanumeric keyword followed by a colon and then a value.
Values may be continued across multiple lines by ending the line with a backslash (\).
The following is the list of valid field names and permissible values.

C<										   
  RuleName:unique rule name
  RuleClass:class[,class...]
  RuleVersion:regular-expression
  RuleContext:(Global,IOSInterface,IOSLine)
  RuleType:(Required|Forbidden)
  RuleMatch:regular-expression
  [RuleInstance:regular-expression]
  [RuleImportance:number]
  [RuleDescription:Text...]
  [RuleFix:Text...]
>
 

where

  * RuleName specifies a unique name for the rule.  

  * RuleClass specifies a class or classes to which the rule belongs.  Every rule
    is a member of the class "default" unless otherwise specified.   A rule can
    be a member of multiple classes.  The list of member classes is a comma separated list.   

  * RuleVersion specifies a pattern that indicates which config file
    version a rules applies to.

    For example, if RuleVersion is "1[12].*" then the rule will apply
    to all IOS 11 and 12 configurations.

  * RuleContext lists the context of the rule.  Possible values are

    - Global - The rule must match anywhere in the config.

    - IOSInterface - The rule must match in the context of an IOS interface definition

    - IOSLine - The rule must match in the context of an IOS line definition.

  * RuleType determines if the match should be required or forbidden

  * RuleMatch specifies a regular expression to match within the given context.

  * RuleInstance defines the instance of the rule that must match, for instance
    "Serial0/0","Vlan\d+","vty", or "aux".
 
  * RuleImportance specifies a number indicating the relative importance
    of a rule.  The higher the number, the more important it is.
										    
  * RuleDescription contains a description/justification of the rule.

  * RuleFix contains text to be applied to make the config "correct"

  * regular-expression is a Perl regular expression

  * rules and values enclosed in "[...]" are optional.

=head2 Rules Example

Here is an example of a rules file

    RuleName:enable secret
    RuleClass:default,access
    RuleVersion:version 1[12]\.*
    RuleContext:Global
    RuleType:Required
    RuleMatch:enable secret \d \S+
    RuleImportance:3
    RuleDescription:Require enable secret.\
    See rules.html#enablesecret for details.
    RuleFix:enable secret EDIT-BY-HAND

    RuleName:Apply VTY ACL
    RuleClass:default,access
    RuleVersion:version 1[12]\.*
    RuleContext:IOSLine
    RuleInstance:vty
    RuleType:Required
    RuleMatch: access-class 92 in
    RuleImportance:2
    RuleDescription:Require ACL 92 to be applied to VTYs\
    See rules.html#ApplyVTYACL for details.
    RuleFix:\
    line vty 0 4\
    access-class 92 in\
    exit

    RuleName:no ip directed broadcast
    RuleClass:default,routing
    RuleVersion:version 11\.*
    RuleContext:IOSInterface
    RuleInstance:.*
    RuleType:Required
    RuleMatch:no ip directed-broadcast
    RuleImportance:2
    RuleDescription:Disallow directed broadcasts by default.\
    See rules.html#noipdirectedbroadcast for details.
    RuleFix:\
    int INSTANCE\
    no ip directed-broadcast\
    exit

In the above example, the rules have the following meaning

    Rule 1 requires a global (anywhere in the config) rule matching
    "enable secret" followed by a non-blank string.

    Rule 2 requires that all vty lines have "access-class 92 in". It lists the commands
    that need to be entered to add the rule.

    Rule 3 requires that all Vlan interfaces have "no ip directed
    broadcast" set and lists the commands needed to set it.  This rule applies 
    only to IOS version 11.

=head2 Global Config Options Syntax and Example

In addition, there are several global rules file options:

C<										   
  ConfigVersion:1.3
  ConfigOrganization:My Organization
  ConfigDocumentType:Security Audit Rules
  ConfigPlatforms:Cisco IOS Routers
  ConfigFeedbackTo:me@my.org
  ConfigGuide:guide.pdf
  ConfigGuidePath:/usr/doc /usr/local/doc ~/doc ./doc
  ConfigRulesAlias:my-special-audit.html
  ConfigIntroText:Text (html)...
  ConfigTrailingText:Text (html)...
  ConfigOutputGroups:value [value ...]
  ConfigLineSkip:pattern[:pattern...]
>

 
where

  * ConfigVersion defines the version number of the rules

  * ConfigOrganziation describes the auditing organization

  * ConfigDocumentType describes the type of document (e.g. "rules", "benchmark"...)

  * ConfigPlatforms describes the type of systems being audited ("production routers...")

  * ConfigFeedback lists contact info (e.g "me@some.place.org")

  * ConfigGuide defines the name of a "configuration guide" to be symlinked into
    the directory where the audits are run.  This is useful because it allows the
    description portion of individual rules to make hyperlinks to a document
    containing a more complete justification for the rule.

  * ConfigGuidePath defines the path to search for the guide.										   

  * ConfigLineSkip defines a list of one or more patterns that causes
    checking to be skipped.  The default is "^ shutdown".

  * ConfigRulesAlias defines an alias for the rules.html file.  A symlink
    from this name to "rules.html" will be created.    

  * ConfigIntroText defines text (HTML) to be inserted before the list
    of individual rules.

  * ConfigTrailingText defines text (HTML) to be inserted after the list
    of individual rules.

  * ConfigOutputGroups specifies groups (such as datacenter names) by which
    output is grouped in index.html.  The groups are space separated and
    are regular expressions.  The regular expressions are matched against
    the names of the configurations being checked.										  

These global rules file options are mostly used during report generation.

=head2 Local Configuration Options Syntax

ConfigLocal options allow the specification simple text substitution
macros, i.e. a string that will be replaced with a given value.
The syntax is										   

In addition, there are several global rules file options:

C<										   
  ConfigLocalName:KEY
  ConfigLocalValue:VALUE
  [ConfigLocalPrereqs:CLASS_NAME[,CLASS_NAME...]]
  [ConfigLocalDescription:COMMENT]
>

 
where

    * KEY is a keyword (e.g. "EternalInterface") that will
      be replaced by VALUE in the resulting config file.

    * VALUE is a value ("Ethernet0") that will be substituted.

    * CLASS_NAME is a list of one or more classes to which use the option

    * COMMENT is a description of the option

    * comments may be continued across several lines by ending
      each line to be continued with "\".					    


=head2 Local Configuration Options Example

Here is an example of a local configuration option:

    ConfigLocalName:Local_Timezone
    ConfigLocalValue:GMT
    ConfigLocalClassPrereq:localtime 
    ConfigLocalDescription:\
     Specify the name of the timezone to be used.  For example, GMT,EST, etc.

In the above example, the fields have the following meaning

    * ConfigLocalName specifies "Local_Timezone" as the name of the local option

    * ConfigLocalValue specifies the value "GMT"

    * ConfigLocalClassPrereq says that this option only applies if the class
      "localtime" is selected (this sets the "local" timezone to "GMT")

    * ConfigLocalDescription specifies text to describe the option

=head2 Optional Rule Class Syntax										   

The master file may also contain ConfigClass: rules, which describe
groups of configuration options.  ncat_config will prompt for
inclusion/exclusion of classes ConfigClass: entries.  They are of the
form:

C<										   
  ConfigClass:CLASS_NAME
  [ConfigClassConflictsWith:CLASS_NAME[,CLASS_NAME...]]
  [ConfigClassPrereq:CLASS_NAME[,CLASS_NAME...]]
  [ConfigClassDescription:COMMENT]
  .
  .
  .
>
 
where

    * CLASS_NAME is the name of the rule class.  It should correspond to
      one rules having the same value for RuleClass.

    * COMMENT is a description of the option

    * Comments may be continued across several lines by ending
      each line to be continued with "\".

    * ConfigClassClassPrereq: is a comma-separated list of classes
      that are prerequisites of the class being defined
 
   * ConfigClassConflictsWith: is a comma-separated list of classes
     that are incompatible with this class.

=head2 Optional Rule Class Example

Here is an example of a rule class:

    ConfigClass:2nd_External_Interface
    ConfigClassPrereq:exterior_router
    ConfigClassDescription:\
       	Define a second external interface.

In this example

    * ConfigClass specified the name of the config class as "2nd_External_Interface"

    * ConfigClassPrereq says that this rule only applies of the ruleclass
      exterior_router is specified.

    * ConfigClassDescription describes the purpose of the second ruleclass.										    

=head1 RETURN VALUE

0  - success
>0 - some error occurred


=head1 FILES

=over 8

F</etc/ncat.conf>    - The rules file.

F<a config file> - At least one config file on the
		   command line (required)

=back

=head1 CAVEATS

Rules themselves may not contain colon (":") characters.

=head1 BUGS

Yes.

=head1 AUTHOR

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

=head1 CREDIT WHERE CREDIT IS DUE

John Stewart has helped with the code in numerous ways.  It's much cleaner, 
and the install process is better thanks to his efforts.

Rob Thomas collected and wrote an excellent baseline IOS 12 secure
configuration which is used as the basis for the example ncat.conf.
The first version of that config provided the "ah ha" insight that
"config checking can be simple" and thus the impetus for the creation
of this script.

Eric Brandwine has written a much more elegant and complete config
checker.  Some of the features of this script are inspired by his
work.  In particular, the size and complexity of that program inspired
the (at least initial) simplicity of this one.

Joshua Wright did the port for ActiveState on Windows.

=cut

#
# $Log: ncat.PL,v $
# Revision 2.14  2002/03/21 16:24:40  gmj
# * updated POD documentation to describe all rule syntax
#
# Revision 2.13  2002/03/20 10:02:40  gmj
# * Rule matches now ignore spaces as well as case.
#
# Revision 2.12  2002/03/17 20:07:11  gmj
# * Config parsing moved to common code in NCAT.pm
#
# Revision 2.11  2002/03/15 22:20:39  gmj
# * Make matches case-insensitive
#
# Revision 2.10  2002/03/13 21:20:58  gmj
# - Match IOSLine instance followed by a single number,  Don't require two
#
# Revision 2.9  2002/03/04 17:03:52  gmj
# * Added ConfigLineSkip config directive.  Defaults to '^ shutdown' to skip shutdown interfaces
#
# Revision 2.8  2002/02/27 14:09:53  gmj
# Intial Windows/ActiveState version
#
# Revision 2.7.2.2  2002/02/27 13:40:45  gmj
# * set PATH for unix.  * do ~ substition for rules file path.
#
# Revision 2.7.2.1  2002/02/25 16:09:08  joshwr1ght
#
# First update of code to support ActiveState Perl on Windows.
#
# Revision 2.7  2002/02/19 16:05:57  gmj
# Made -V synonymous with --version
#
# Revision 2.6  2002/02/05 20:03:03  gmj
# * Numerous documentation changes suggested by Anthony Williams
#
# Revision 2.5  2002/02/04 23:11:56  gmj
# Add RCS Id to --version output
#
# Revision 2.4  2002/02/04 16:49:18  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.3  2002/02/01 13:15:40  gmj
# Changed authors email address
#
# Revision 2.2  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.1  2002/01/24 21:52:08  gmj
# merge back to mainline
#
# Revision 2.0.2.4  2002/01/24 21:33:30  gmj
# * Updated copyright
# * Changed internal slit character from ":" to ";" to allow ios subinterfaces to work
#
# Revision 2.0.2.3  2002/01/23 22:09:16  gmj
# Added global config file option ConfigOutputGroups.
#
# Revision 2.0.2.2  2002/01/07 19:36:27  gmj
# Added parsing and documentation for global config statements.
#
# 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:35  gmj
# Level set version to 2.0 prior to branch for Center for Internet Security.
#
# Revision 1.5  2001/12/14 11:26:31  gmj
# Added -verbose flag.
# Updated credits.
#
# Revision 1.4  2001/12/11 21:44:12  jnssf
# Changes for allowing 'use NCAT;' to incorporate --version flag,
# misc changes to make code similar to other entries.  Change to NCAT.pm
# so it doesn't use bootstrapping, but instead a function call to ensure
# we no longer need the Auto/Dyna loader routines (which in the end weren't
# used anyway), to allow for the 'use' statements with the .ix or .so files.
#
# 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/12/10 15:48:51  gmj
# Changd default class form .* to "default".
#
# Revision 1.1.1.1  2001/11/14 21:40:18  gmj
# Initial CVS checkin.
#
#
# Revision 2.4  2001/11/07 07:21:20  jns
# added ENV variables
# changed to '' where possible
# modifications to code for autoflush on STDERR
#
# Revision 2.3  2001/11/07 02:41:57  jns
# with autoflush comes requiring the base package of IO::Handle
#
# Revision 2.2  2001/11/07 02:24:49  jns
# added the IFS/SHELL and PATH variable changes
#
# Revision 2.1  2001/11/07 01:43:18  jns
# initial release log message, now Makefile.PL'd
#
# Revision 2.0  2001/11/06 03:03:14  jns
# *** empty log message ***
#
# Revision 1.15  2001/11/03 00:42:54  gjones
# - Converted to using GetOptions
#
# Revision 1.14  2001/11/02 16:22:56  gjones
# - Documentation and debugging fixes.
#
# Revision 1.13  2001/11/01 20:04:51  gjones
# - Added ruleclasses.
# - Added -limit{rule,class}to switches.
#
# Revision 1.12  2001/10/22 18:55:13  gjones
# * Added -limitrulesto, -onlypass, -onlyfail flags
# * Removed "special" version rule, now each rule must
#   contain a regexp identifying the config(s) to which
#   it applies.
#
# Revision 1.11  2001/10/06 06:20:25  gjones
# Final updates for rat release 0.1.  Mostly documentation.
#
# Revision 1.10  2001/10/06 01:34:28  gjones
# Much munging of report formats, command line arguments
# and general bug fixing in preparation for release.
#
# Revision 1.9  2001/10/05 12:34:12  gjones
# Output rule summaries for all rules (pass and fail) to report.
# Include pass/fail + importance in report.
#
# Revision 1.8  2001/10/04 16:32:26  gjones
# Moved config parsing into a subroutine.
# Renamed "master" to "rules".
# Various small cleanups.
#
# Revision 1.7  2001/10/03 21:45:42  gjones
# Finished restructuring config file and basic report generation.
#
# Revision 1.6  2001/10/03 19:05:00  gjones
# Checkpoint after most of the surgery was done the patient nearly lost
# due to an editing blunder.  Still have some suturing to do.
#
# Revision 1.5  2001/10/02 19:07:01  gjones
# Checkpoint before radical surgery: new config file format.
#
# Revision 1.4  2001/09/29 22:23:26  gjones
# Changed return values (return 0 now if all was successful to be a good player when called from other programs)
#
# Revision 1.3  2001/03/15 22:24:33  gjones
# Store comments with the rule.  Comments must be on the line(s)
# immediatly preceeding the rule the describe and being with "#..."
#
# Revision 1.2  2001/01/25 23:33:58  gjones
# Added Pre- and Post-Rule text to config file syntax.
# These are chunks of text that are not part of the
# rules that are expected to be found in the config,
# but are output before/after the rule text to
# effect the proper change.
#
# Cleaned up the multiline rule code.
#
# Generalized a bunch of stuff that was IOS dependant.
#
# Made the fix script output much more reliable.
#
# Revision 1.1  2001/01/25 10:05:56  gjones
# Initial revision
#

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

use lib '/usr/lib';


use Getopt::Long;
use IO::Handle;
use strict;
use Config;

use vars qw($opt_debug);

# Imports

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

# Exports
$opt_debug = '';

# Locals

my (
    $RulesVersion,
    $name,		$opt_dir,
    $opt_rules,         $opt_limitrulesto, 
    $opt_limitclassto,  $opt_onlypass,
    $opt_onlyfail,
    $opt_help,         $opt_version,
    $opt_verbose,
    $rules_mtime,

    $Context,           $If, 
    $IfName,            $InstanceName,
    $Line,              $LineName,
    $Offset,		$loginname,
    $windows,		$home,
    $skip,		$skip_pattern,
    ) = ('','','','','','','','','','','','','','','','','','');


my ($rules_size, 
    $BaseLineNum, $LineNum,
    $OffSet) = 0;

my (@ConfigNames) = ();

my (
    %Configs, %IfNames, %IfLineNumber, %IfLines,
    %LineNames, %LineLineNumber, %LineLines,
    %RulesCheckedByConfigAndRule,
    %VersionMatches,
    %ViolationsByRule, %ViolationsByConfig, %ViolationsByConfigAndRule
    ) = ();

#test for Windows
if ($Config{'osname'} =~ /MSWin/) {
    $windows=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_limitrulesto = '.*';
$opt_limitclassto = 'default';
$opt_onlypass     = 0;
$opt_onlyfail     = 0;
$opt_debug        = "";

#parse command line

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

	   # ncat specific
	   "limitrulesto|l=s", \$opt_limitrulesto,
	   "limitclassto|c=s", \$opt_limitclassto,
	   "onlyfail|f", 	\$opt_onlyfail,
	   "onlypass|p", 	\$opt_onlypass,
	  )
  or &Usage("");


&Version if $opt_version;


$opt_limitclassto =~ s/^all$/\.\*/i;
$opt_limitclassto =~ s/,/|/g; # allow "," as a synonym for "|"

sub Version {

    print STDERR "This is $progname version ", NCAT::Version, "\n";
    print STDERR '$Id: ncat.PL,v 2.14 2002/03/21 16:24:40 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) = @_;
  print STDERR "$progname: $msg\n" unless ($msg eq "");
  print STDERR "Usage:\n";
  print STDERR "  $progname [options] configfile.txt [configfile2.txt...]\n";
  print STDERR "    -r, --rules RULES_FILE\n";  
  print STDERR "    -l, --limitrulesto RULES_REGEXP\n";  
  print STDERR "    -c, --limitclassto CLASS_REGEXP\n";  
  print STDERR "    -p, --onlypass\n";  
  print STDERR "    -f, --onlyfail\n";  
  print STDERR "    -d, --debug DEBUG_OPTIONS\n";  
  print STDERR "    -V, --version\n";  
  exit 1;
}


# process command line stuff

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

my($ConfigName) = shift || &Usage("");

die "Conflicting options.  Only one of -onlypass and -onlyfail may be specified."
  if ($opt_onlypass and $opt_onlyfail);

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

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

unless (-s $opt_rules) {
    print STDERR "ncat: $opt_rules does not exist.  Please specifiy the correct path with -rules\n";
    exit 1;
}
										   
unless (my($rules_mtime,$rules_size) = (stat $opt_rules)[9,7]) {
  die "Can't stat $opt_rules: $!";
}


# Set config global values that are only know at runtime

$ConfigGlobalFieldValues{"configfeedbackto"} = $loginname;


# Parse the rules file.

ParseRules($opt_rules);

#
# If -limitrulesto was specified, remove all noqn-matching rules
#

foreach $name (keys %RulesDefined) {
      unless ($name =~ /$opt_limitrulesto/i) {
	  delete $RulesDefined{$name};
      }
  }

#
# If -limitclassto was specified, remove all non-matching rules
#

foreach $name (keys %RulesDefined) {
      unless ($RuleFieldValues{"$name:ruleclass"} =~ /$opt_limitclassto/i) {
	  delete $RulesDefined{$name};
      }
  }

#
# 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

  undef($IfName);
  undef($LineName);

  $LineNum = 0;

  #
  # Now go through the config line by line and save
  # the various chunks of text in hashes so that
  # they can be parsed later.
  #

  while(<CONFIG>) {

    $LineNum++;

    # Save entire config

    $Configs{$ConfigName} .= $_;

    # Save interface configs

    if (/^interface\s+(\S+)/) {
      $IfName = $1;
      $IfNames{$ConfigName} .= "$IfName,";
      $IfLineNumber{"$ConfigName:$IfName"} = $LineNum - 1;
    }

    undef($IfName) if (/^!$/);

    $IfLines{"$ConfigName:$IfName"} .= $_ if (defined($IfName));

    # Save line configs

    if (/^line\s+(\w[\w \d]*)/) {
      $LineName = $1;
      $LineNames{$ConfigName} .= "$LineName,";
      $LineLineNumber{"$ConfigName:$LineName"} = $LineNum - 1;
    }

    undef($LineName) if (/^!$/);

    $LineLines{"$ConfigName:$LineName"} .= $_ if (defined($LineName));

  }

  close(CONFIG);

  push @ConfigNames,$ConfigName;
  $ConfigName = shift;
}


#
# Now check the rules
#

for $ConfigName (@ConfigNames) {

  print STDERR "  Ifs: $IfNames{$ConfigName}\n"
      if ($opt_debug =~ /ifnames/);
  print STDERR "Config: /$Configs{$ConfigName}/\n"
      if ($opt_debug =~ /configs/);
  print STDERR "ConfigName: $ConfigName\n"
      if ($opt_debug =~ /configname/ || $opt_debug =~ /ifnames/);

  $BaseLineNum = 0;
  check($Configs{$ConfigName},$ConfigName,"",$BaseLineNum,"Global");

  # Check per-interface rules

  if (defined($IfNames{$ConfigName})) {

      for $If (split(/,/,$IfNames{$ConfigName})) {
      
	  print STDERR "  checking $ConfigName:$If\n"
	      if ($opt_debug =~ /ifcheck/);
	  
	  $BaseLineNum = $IfLineNumber{"$ConfigName:$If"};

	  if (defined $ConfigGlobalsDefined{"configlineskip"}) {

	      $skip = 0;    
	      for $skip_pattern (split(/:/,$ConfigGlobalFieldValues{"configlineskip"})) {
		  
		  if ($IfLines{"$ConfigName:$If"} =~ /$skip_pattern/msi) {
		      printf STDERR "  skipping $ConfigName:$If because it matches /$skip_pattern/\n"
			  if ($opt_verbose);
		      $skip = 1;
		      last;
		  }
	      }
	      
	      next if ($skip);
	  }
	  
	  check($IfLines{"$ConfigName:$If"},$ConfigName,
		$If,$BaseLineNum,"IOSInterface");
	  
      } # for each if
  } # if interfaces define


  # Check line rules
  if (defined($LineNames{$ConfigName})) {

    for $Line (split(/,/,$LineNames{$ConfigName})) {
      
      print STDERR "  checking $ConfigName:$Line\n"
	  if ($opt_debug =~ /linecheck/);

      $BaseLineNum = $LineLineNumber{"$ConfigName:$Line"};
      check($LineLines{"$ConfigName:$Line"}, $ConfigName,
	    $Line,$BaseLineNum, 'IOSLine');

    } # for each interface
  } # if there are any interfaces


} # for each config file

PrintResults();
exit(0);

####################################################################
####################################################################
####################################################################
#
# check: Check a chunk of text for required, forbidden rules
#

sub check {
  my($Text,$ConfigName,$InstanceName,$Offset,$Context) = @_;
  my($Instance,$RuleClasses,$Rule,$Found, $TextBeforeMatch,
     $LinesPreceedingMatch,$Pattern);
  
  # Check each rule
  
  foreach $Rule (keys %RulesDefined) {

      # skip unless the context matches the rule

      next unless ($RuleFieldValues{"$Rule:rulecontext"} eq $Context);

      # skip unless the config matches the rules version string

      next unless (VersionMatches($ConfigName,
				  $RuleFieldValues{"$Rule:ruleversion"}));

      $RulesCheckedByConfigAndRule{"$ConfigName:$Rule"} = 1;

      print STDERR "checking $Rule on $ConfigName\n"
	  if ($opt_debug =~ /check/);

      if (defined($RuleFieldValues{"$Rule:ruleinstance"}) and 
	  $RuleFieldValues{"$Rule:ruleinstance"} ne "" and
	  defined($InstanceName)) {
	  print STDERR "/$Rule/ /$InstanceName/ =~ /" . $RuleFieldValues{"$Rule:ruleinstance"} . "/ ?\n" if ($opt_debug =~ /match/);

	  next unless $InstanceName =~ /$RuleFieldValues{"$Rule:ruleinstance"}/i;
	  print STDERR "no match\n" if ($opt_debug =~ /match/);
      }

      print STDERR "/$Rule/ /" . $RuleFieldValues{"$Rule:rulematch"} . "/ found ?\n" if ($opt_debug =~ /match/);

      print STDERR "Text: /$Text/ =~ ? /". 
	$RuleFieldValues{"$Rule:rulematch"} ."/\n" if ($opt_debug =~ /match/);

      # squash all spaces in pattern and text before comparison

      $Pattern = $RuleFieldValues{"$Rule:rulematch"};
      $Text =~ s/[ \t]+//gm;
      $Pattern =~ s/[ \t]+//gm;

      # do multiline, case insensitive comparisons

      $Found = ($Text =~ /$Pattern/mi) ? 1 : 0;


      if ($Found) {
	  print STDERR "found.\n" if ($opt_debug =~ /match/);
	  if ($RuleFieldValues{"$Rule:ruletype"} eq "Forbidden") {
	      $TextBeforeMatch = $`;
	      $LinesPreceedingMatch = $TextBeforeMatch =~ tr/\n/\n/; # don't ask.
	      $LineNum = $Offset+1+$LinesPreceedingMatch;

	      #
	      # Note: what's going on here is we are saving violations
	      # groups (using hashes) by rule/config/rule and config.
	      # They're saved as a single spittable string.  The split
	      # happens on ";".  It was ":", but that causes problems
	      # with IOS sub-interface instances.  This should be done
	      # more elegantly, maybe storing an array of matches in the hash.
	      #

	      $ViolationsByRule{"$Rule"} .= "$ConfigName;$InstanceName;$LineNum;";
	      $ViolationsByConfig{"$ConfigName"} .= "$Rule;$InstanceName;$LineNum;";
	      $ViolationsByConfigAndRule{"$ConfigName:$Rule"} .= "$InstanceName;$LineNum;";

	  }
      } else {
	  print STDERR "not found.\n" if ($opt_debug =~ /match/);
	  if ($RuleFieldValues{"$Rule:ruletype"} eq "Required") {
	      $LineNum = $Offset+1;
	      $ViolationsByRule{"$Rule"} .= 
		  "$ConfigName;$InstanceName;$LineNum;";
	      $ViolationsByConfig{"$ConfigName"} .= 
		  "$Rule;$InstanceName;$LineNum;";
	      $ViolationsByConfigAndRule{"$ConfigName:$Rule"} .= 
		  "$InstanceName;$LineNum;";
	  }
      } # not found
  }
} # check

#
# Report on findings by Rule
#

sub PrintResults {
   my($Errors, $onreport_open) = 0;
   my($Config, $Instance, $Line) = "";
   my ($importance, $rule) = "";
   my (@RuleInfo) = ();

   for $Config (@ConfigNames) {
       
       open(REPORT,">$Config.ncat_out.txt") || 
	   die "Can't open $Config.ncat_out.txt: $!";

       select REPORT;
       REPORT->autoflush(1);

       print REPORT "Config;rule;PassFail;Importance;Instance;Line\n";

       for $rule (sort(keys %RulesDefined)) {

	   # normalize rules

	   $rule =~ s/\\\+/\+/; # unquote quoted + signs	   
	   
	   if (defined($ViolationsByConfigAndRule{"$Config:$rule"})) {

	       print STDERR "ViolationsByConfigAndRule{$Config:$rule}  = /" .
		            $ViolationsByConfigAndRule{"$Config:$rule"} .  "/\n"
		     if ($opt_debug =~ /rule/);

	       # Rule Failed 

	       next if ($opt_onlypass);

	       @RuleInfo = split(/;/,$ViolationsByConfigAndRule{"$Config:$rule"});
	       
	       while(@RuleInfo >= 2) {
		   print STDERR "RuleInfo ".scalar(@RuleInfo).": /$RuleInfo[0],$RuleInfo[1]/\n" 
		     if ($opt_debug =~ /rule/);
		   
		   ($Instance,$Line) = splice(@RuleInfo,0,2);
		   $importance = $RuleFieldValues{"$rule:ruleimportance"};
		   print REPORT "$Config;$rule;FAIL;$importance;$Instance;$Line\n";
		   $Errors++;
	       } # while more instances of failure of this rule

	   } elsif (defined($RulesCheckedByConfigAndRule{"$Config:$rule"})) {

	       # Rule Passed

	       next if ($opt_onlyfail);

	       $importance = $RuleFieldValues{"$rule:ruleimportance"};
	       print REPORT "$Config;$rule;PASS;$importance\;\;\n";

	   }


       } # for each rule

       print REPORT "#AuditDate=" . gmtime(time) . " GMT\n";

       print REPORT "#rules=$opt_rules;size=$rules_size;mtime=$rules_mtime\n";

   } # for each config

  return $Errors;
}

#
# Intersection.
#
# Take two comma seperated lists of items as input and return
# the intersection
#

sub Intersection {
  my($Set1,$Set2) = @_;

  my($Intersection, $Item1, $Item2) = "";
  my(@Set1, @Set2) = ("");


  # Deal with undefined values coming in

  $Set1 = "" unless defined($Set1);
  $Set2 = "" unless defined($Set2);

   # If either set is "*", return a match (*)

  return "*" if ($Set1 eq "*" or $Set2 eq "*");

   # Two null sets intersect by definition

  return "NULLMATCH" if ($Set1 eq "" and $Set2 eq "");

   # Null in either set implies no match

  return "" if ( $Set1 eq "" or  $Set2 eq "");
  
  @Set1 = split(/,/,$Set1);
  @Set2 = split(/,/,$Set2);

  # Yes, this is order N**2

  for $Item1 (@Set1) {
    for $Item2 (@Set2) {
      $Intersection .= "$Item1,"
	if ($Item1 eq $Item2);
    }
  }

  $Intersection =~ s/,$//;

  return $Intersection;
}

#
# Given a config name and a rule name, determine if the config matches
# the rule's version restriction.  Remember previous matches so that
# we don't have to repeat regular expression matches on large configs.
#

sub VersionMatches {
    my($ConfigName,$VersionString) = @_;

     # return cached result if we did this check before

    unless (defined $VersionMatches{"$ConfigName:$VersionString"}) {
	print STDERR "Checking /$ConfigName/ for /$VersionString/..." 
	    if ($opt_debug =~ /version/i);

	if ($Configs{$ConfigName} =~ /$VersionString/) {

	    $VersionMatches{"$ConfigName:$VersionString"} = 1;
	    print STDERR "match\n" if ($opt_debug =~ /version/i);

	    $IfNames{$ConfigName} =~ s/,$// # take off the trailing ","
	      if defined($IfNames{$ConfigName});;
	
	} else {
	    $VersionMatches{"$ConfigName:$VersionString"} = 0;	    
	    print STDERR "no match\n" if ($opt_debug =~ /version/i);
	}

    }    

    if ($VersionMatches{"$ConfigName:$VersionString"}) {
	printf STDERR "match /$ConfigName/ /$VersionString/\n" if ($opt_debug =~ /version/);
    } else {
	printf STDERR "no match /$ConfigName/ /$VersionString/\n" if ($opt_debug =~ /version/);
    }
    return $VersionMatches{"$ConfigName:$VersionString"};
}
