#
# perl/exclusions.pl
# $Id: exclusions.pl,v 1.2 2001/02/23 20:11:54 payerle Exp payerle $
#


use strict;

#Methods:
#
# add_exclusion($record) : add record to the exclusions list
#
# drop_exclusion($record) : drop record from the exclusions list
#
# clear_exclusions : drop all records from the exclusions list
#
# save_exclusions($path): saves the exclusions to the named file.
#
# read_exclusions($path): reads the exclusions from the named file.
#
# merge_exclusions($path): merges in-core exclusions with those in named file.
#
# is_excluded($severity) : returns true if the named severity matches an
#	exclusion in the list, false otherwise
#
#Data:
# %exclusions_by_host - hash keyed on hostname
# $exclusions_by_host{$host} - pointer to hash keyed on service output
# ${$exclusions_by_host{$host}}{$type} - returns the exclusion record
#	Basically same format as "fact" record, but some fields may be
#	wild-carded (all but target and service_output).

sub split_fact ($)
#Does sara_split, satan_split, or saint_split as appropriate
{	my $fact=shift;
	
	if ( ! $::system )
	{	#$::system not defined
		$::system="saint";
		warn("\$e/.exfilerc not defined, setting to saint\n");
	}
	return &sara_split($fact) if ($::system eq "sara");
	return &saint_split($fact) if ($::system eq "saint");
	return &satan_split($fact) if ($::system eq "satan");
	die "Illegal value for \$::system = $::system";
}

sub split_fact_local ($)
#Similar to split_fact, but encapsulates all split fields in a local so
#should not have any affect outside itself.
#Returns a null list if ill-formed fact, else returns
#($target,$service,$status,$severity,$trustee,$trusted,$service_output,$text)
#for named fact
{	my $fact=shift;
	local ($::target,$::service,$::status,$::severity,$::trustee,
		$::trusted,$::service_output,$::text);
	my $res = split_fact($fact);
	my $version="";
	if ( $res ) { return (); }
	return ($::target,$::service,$::status,$::severity,$::trustee,
		$::trusted,$::service_output,$::text,$version);

}

sub split_exclusion ($)
#Similar to split_fact_local, but for exclusions.  Currently, $version field
#is not included yet in severities so is always null
#0 $target
#1 $service
#2 $status
#3 $severity
#4 $trustee
#5 $trusted
#6 $service_output
#7 $text
#8 $version
{	my $fact=shift;
	my @fact= split /\|/, $fact;
	if ( scalar(@fact) == $::SAINT_FIELDS )
	{	push @fact, ""; #Add a null version field
	} elsif ( scalar(@fact) != $::SAINT_FIELDS+1 )
	{ 	warn "Ill-formed exclusion: $fact\n";
		return ();
	}
	return @fact;
}

sub does_exclusion_match_fact ($$)
# Checks if given exclusion and fact match
# Allows for wildcarding on the exclusion
#Returns true if does, false otherwise
#USage: does_exclusion_match_fact $exc, $fact;
# $exc and $fact can either be strings in standard fact/exclusion format
# (ie | delimitted fields), or array refs to the decomposed fact/exclusion
{	my $excl=shift;
	my $fact=shift;
	my ($pexc, $pfact);

	#Decompose exclusion if not already done
	if ( ref($excl) ) 
	{ 	$pexc=$excl; 
	} else
	{	my @tmp=split_exclusion($excl);
		if ( scalar(@tmp)==0 ) 
		{	warn "Ill-formatted exclusion: $excl\n";
			return undef;
		}
		$pexc = \@tmp;
	}

	#Decompose fact if not already done
	if ( ref($fact) ) 
	{ 	$pfact=$fact; 
	} else
	{	my @tmp=split_fact_local($fact);
		if ( scalar(@tmp)==0 ) 
		{	warn "Ill-formatted fact: $fact\n";
			return undef;
		}
		$pfact = \@tmp;
	}


	if ( scalar(@$pexc)  != $::SAINT_FIELDS+1 )
	{	warn "Ill-formatted exclusion: Only ".  scalar(@$pexc) . "
			 fields\n";
		return undef;
	}
	if ( scalar(@$pfact)  != $::SAINT_FIELDS+1 )
	{	warn "Ill-formatted fact: Only " .  scalar(@$pfact) . 
			" fields\n";
		return undef;
	}

	my $i;
	my $VERSION_FIELD=8;
	for ( $i=0; $i< scalar(@$pexc); $i++ )
	{ 	if ( $i != $VERSION_FIELD )
		{	if ( $$pexc[$i] ne $$pfact[$i] && $$pexc[$i] ne "*")
			{	return undef;	
			}
		} else
		{	#Version field just needs exclusion version >= fact
			if ( $$pexc[$i] ne $$pfact[$i] && $$pexc[$i] ne "*"
				&& $$pfact[$i] gt $$pexc[$i] )
			{	return undef;	
			}
		}
	}
	#If got here, they match
	return 1;
}
			

sub get_all_exclusions_for_host_severity($$)
#Returns a lsit of all exclusions matching hostname and severity
# Allows for wildcards in hostname or severity.
#USage: $list=get_all_exclusions_for_host_severity($host,$severity)
{	my $hostname=shift;
	my $severity=shift;

	my @hosts=($hostname);
	my @sevs=($severity);
	if ( $hostname ne "*" ) { push @hosts, "*"; }
	if ( $severity ne "*" ) { push @sevs, "*"; }

	my ($host,$sev);
	my @exclist=();
	my @tmplist;

	my $hosthash;
	hostloop: foreach $host (@hosts)
	{	if ( ! exists $Exclusions::exclusions_by_host{$host} )
		{	next hostloop;
		}
		$hosthash =  $Exclusions::exclusions_by_host{$host};
		sevloop: foreach $sev (@sevs)
		{ if ( ! exists $$hosthash{$sev} ) { next sevloop; }
			@tmplist=split /\n+/, $$hosthash{$sev};
			push @exclist, @tmplist;
		}
	}

	#Shouldn;t be any duplicates, but lets remove them anyway
	my %tmphash = map {$_=>undef} @exclist;
	@exclist = keys %tmphash;

	return @exclist;
}

sub get_all_exclusions_matching_fact($)
#Returns a list of all exclusions matching the given fact
# Allows for wildcards in exclusions
{	my $fact = shift;

	#Decompose fact
	my @fact=split_fact_local($fact);
	if ( ! @fact ) 
	{	warn "Ill-formatted fact: $fact\n";
		return undef;
	}
	my $host=$fact[$::TARGET_FIELD]; 
	my $sev=$fact[$::SERVICE_OUTPUT_FIELD];

	my @exclist = get_all_exclusions_for_host_severity($host,$sev);
	my $exc;
	my @match=();

	foreach $exc (@exclist)
	{	if ( does_exclusion_match_fact($exc,\@fact) )
		{	push @match,$exc;
			print "Exclusion found: target=$host, sev=$sev, " .
				"excl=$exc\n" if $::debug;
		}
	}

	return @match;
}


#
# Check if the given fact/severity is "excluded"
# Return true if excluded, false otherwise
#
sub is_excluded ($)
{	my $fact=shift;
	my @matched = get_all_exclusions_matching_fact($fact);

	return scalar(@matched);
}

#
# Add one exclusion to the exclusions list
# Second argument if present and true means should save exclusions to file
# (usally set to $Exclusions::autosave)
sub add_exclusion ($;$)
{
	my $fact = shift;
	my $save = shift || undef;
	my @fact;
	my ($host, $sev);
	my @fact=split_exclusion($fact);
	if ( scalar( @fact)==0 )
	{	warn "Ill-formatted fact: $fact\n";
		return undef;
	}	
	$host=$fact[$::TARGET_FIELD]; $sev=$fact[$::SERVICE_OUTPUT_FIELD];

	if ( ! defined $Exclusions::exclusions_by_host{$host})
	{	$Exclusions::exclusions_by_host{$host}={};
	}
	my $exclusions = $Exclusions::exclusions_by_host{$host};

	#Ensure we don't duplicate
	my $exclist=$$exclusions{$sev};
	my @exclist=split /\n+/, $exclist;
	my %exchash = map { $_=>undef} @exclist;

	if ( exists $exchash{$fact} ) 
	{	warn "Attempting to add exclusion already existing: $fact\n";
	} else
	{ 	$exchash{$fact}=undef;
		@exclist = keys %exchash;
		$exclist = join "\n", @exclist;
		$$exclusions{$sev}=$exclist;

		print "Add-exclusion: $fact\n" if $::debug;
		if ( $save )
		{	autosave_exclusions();
		}
		force_recalc();
	}
	return;
}

#
# Drop one exclusion from the exclusions list
# Usage:
# drop_exclusion $excl_rec;
# where $excl_rec is the exclusion record to drop
#Second arg if present and true means save exclusions file after dropping
#
sub drop_exclusion ($;$)
{
	my $erec = shift;
	my $save=shift || undef;
	print "dropping exclusion $erec\n" if $::debug;

	my ($host,$sev);
	my @fact;
	if ( !( ( @fact=split_exclusion($erec)) ) ) 
	{	warn "Ill-formatted erec: $erec\n"; #Probably unnecessary
		return undef;
	}	
	$host=$fact[$::TARGET_FIELD]; 
	$sev=$fact[$::SERVICE_OUTPUT_FIELD];

	my $exclusions = $Exclusions::exclusions_by_host{$host};
	if ( ! exists $$exclusions{$sev} )
	{	warn "Attempt to delete non-existant exclusion $erec.\n";
		return;
	}
	my $exclist = $$exclusions{$sev};
	my @exclist = split /\n/, $exclist;
	my %exchash = map { $_=>undef} @exclist;

	if ( ! exists $exchash{$erec} )
	{	warn "Attempt to delete non-existing exclusion $erec.\n";
		return;
	}

	delete $exchash{$erec};
	@exclist = keys %exchash;
	$exclist = join "\n", @exclist;

	if ( scalar(@exclist) )
	{	#Still some exclusions in the list
		$$exclusions{$sev} = $exclist;
	} else
	{	#Nothing left under $host, $sev, so delete the hash element
		delete $$exclusions{$sev};
		print "deleting exclusion hash element for $host,$sev\n"
			if $::debug;
		if ( ! keys %$exclusions )
		{	#Nothing left under that host, so delete it as well
			delete $Exclusions::exclusions_by_host{$host};
			print "deleting exclusion hash element for $host\n"
				if $::debug;
		}
	}

	if ( $save )
	{	autosave_exclusions();
	}
	force_recalc();
	return;
}

#
#  Clear ALL exclusions in the exclusions list
#  If arg given and true, save the empty exclusions file
sub clear_exclusions  (;$)
{	my $save=shift || undef;
	my @targets=keys %Exclusions::exclusions_by_host;
	undef %Exclusions::exclusions_by_host;
	my $target;
	if ( $save )
	{	autosave_exclusions();
	}
	force_recalc();
}

#
# Save exclusions to named file
# Returns 0 on error, otherwise number of exclusions saved
sub save_exclusions($)
{	my $path = shift;
	my $res;
	my ($sev,$exclist);
	my $count=0;
        $res = open(FACTS, ">$path");
	if ( ! $res )
	{	warn "cannot save exclusions to $path: $!";
		return 0;
	}
	my $host;
	
        foreach $host (keys %Exclusions::exclusions_by_host) 
	{	my $tmp = $Exclusions::exclusions_by_host{$host};
		$res=1;
		foreach $sev ( keys %$tmp ) 
		{ 	$exclist=$$tmp{$sev};
			$res&&=print FACTS "$exclist\n";
			$count++;
		}
		if ( ! $res )
		{	warn "error printing exclusions to $path: $!";
			return 0;
		}
        }
        $res = close(FACTS);
	if ( ! $res ) 
	{	warn "Problems saving exclusions to $path: $!";
		return 0;
	}
	return $count;
}

sub exclusion_file_path (;$)
{
	my $file = shift;
	if ( ! $::system )
	{	$::system="saint";
		warn("system not defined, setting to saint\n");
	}
	return &sara_exclusion_file_path($file) if ($::system eq "sara");
	return &saint_exclusion_file_path($file) if ($::system eq "saint");
	return &satan_exclusion_file_path($file) if ($::system eq "satan");
	die "Illegal value for \$::system = $::system";
}

sub saint_exclusion_file_path (;$)
{	my $file= shift || $Exclusions::savefile;
	if ( ! $file ) { return undef; }
	my $path;
	if ( $file =~ /^\// )
	{	$path = $file;
	} elsif ( $::saint_data =~ /\// )
	{	$path = "$::saint_data/$file";
	} else
	{	$path = "results/$::saint_data/$file";
	}
	return $path;
}


sub autosave_exclusions()
#Autosaves exclusisons to current save file
#Returns 0 on error, otherwise number of exclusions saved 
{	my $file=exclusion_file_path();
	if ( ! $file )
	{	warn "Unable to autosave exclusions as no savefile set.\n";
		return 0;
	}

	my $res=save_exclusions($file);
	return $res;
}

#
# Merge the in-memory exclusions with those from the named file
# 
#Returns false on error, otherwise count of number exclusions read
sub merge_exclusions($)
{	my $path = shift;

	if ( ! defined $path ) { return; }
	if ( ! -f $path ) 
	{ 	#warn "Cannot read exclusions from $path\n";
		return; 
	}
	my $res=open(FACTS,"<$path");
	if ( ! $res )
	{	 warn "cannot read exclusions from $path: $!";
		return undef;
	}
	print "Reading exclusions from $path\n" if $::debug;
	my $exc;
	my $count=0;
	while ( $exc=<FACTS> )
	{	chop $exc;
		#Delay any autosaving till after done with whole thing
		add_exclusion($exc,0);
		$count++;
	}
	close(FACTS);
	if ( $Exclusions::autosave ) { autosave_exclusions; }
	return $count;
}

#
#
# Read exclusions from named file
# 
sub read_exclusions($)
{	my $path = shift;

	#Delay autosave
	clear_exclusions(0);
	my $res=merge_exclusions($path);
	return $res;
}

sub init_exclusions
# initialize exclusions stuff
{	$Exclusions::savefile="exclusions";
	$Exclusions::autosave=1;
	$Exclusions::display_ignored=undef;
	my $file = exclusion_file_path;
	my $cnt=read_exclusions($file);
	print STDERR "Read $cnt exclusisons from $file\n" if $::debug;
	force_recalc();
}

sub force_recalc
# force recalculation of totals by resetting flags
{
	$Exclusions::host_severity_count_flag=0;
	$Exclusions::severity_flag = 0;
	$Exclusions::subnet_flag = 0;
	$Exclusions::hosttype_flag = 0;
	$Exclusions::service_flag = 0;
}

#
# Some scaffolding code for stand-alone testing.
#
if ($::running_under_saint) 
{
	no strict;
        require 'perl/misc.pl';
	use strict;
} else 
{
        $::running_under_saint = -1;
        $::debug = 1;

	no strict;
        require 'perl/misc.pl';
	use strict;

	warn "exclusions.pl running in stand-alone mode\n";

	print "Enter exclusions to add followed by blank line\n";
	while ( <> )
	{	chop;
		if ( ! $_ ) { last; }
		add_exclusion($_);
	}
	print "Listing all entered exclusions:...\n";
	my ($key, $exc);
        foreach $key (keys %Exclusions::exclusions_by_host) 
	{	my $tmp = $Exclusions::exclusions_by_host{$key};
		foreach $exc ( keys %$tmp ) { print "$key:$exc:$$tmp{$exc}\n";}
        }
	print "Enter exclusions to delete followed by blank line\n";
	while ( <> )
	{	chop;
		if ( ! $_ ) { last; }
		drop_exclusion($_);
	}
	print "Listing all current exclusions:...\n";
        foreach $key (keys %Exclusions::exclusions_by_host) 
	{	my $tmp = $Exclusions::exclusions_by_host{$key};
		foreach $exc ( keys %$tmp ) { print "$key:$exc:$$tmp{$exc}\n";}
        }
	print "Enter list of facts/severities to check:...\n";
	while ( <> )
	{	chop;
		if ( ! $_ ) { last; }
		if ( is_excluded($_) )
		{	print "Excluded: fact=$_\n";
		} else
		{	print "Included: fact=$_\n";
		}
	}
}

sub recalc_host_severity_count ()
#Recalcs the severities counts for all hosts, taking exclusions into account
#Only done is host_severity_count_flag is set
{	
    my ($severity,$bysev, $sevs, @sevs, $sev, $host);

    #Check if need to do a recalc
    if ( $Exclusions::host_severity_count_flag>0 ) { return; }

    print "Recalcing severity counts\n" if $::debug;
    foreach $host ( keys %::severity_host_count )

    {
	#Zero everything
	$::severity_host_count{$host}=0;
	$Exclusions::severity_ignored_host_count{$host}=0;
	#severity_host_count_all Counts both ignored and not ignored
	$Exclusions::severity_host_count_all{$host}=0; 
	$::severity_green_host_count{$host}=0;
	$Exclusions::severity_ignored_green_host_count{$host}=0;
	$::severity_brown_host_count{$host}=0;
	$Exclusions::severity_ignored_brown_host_count{$host}=0;
	$::severity_yellow_host_count{$host}=0;
	$Exclusions::severity_ignored_yellow_host_count{$host}=0;
	$::severity_red_host_count{$host}=0;
	$Exclusions::severity_ignored_red_host_count{$host}=0;

	foreach $severity (keys %::severity_levels )
	{	$bysev = $::severity_levels{$severity};
		if ( ! exists $$bysev{$host} ) { next; }

		$sevs = $$bysev{$host};
		@sevs = split /\n/, $sevs;
		foreach $sev (@sevs)
		{   $Exclusions::severity_host_count_all{$host}++;
		    if ( is_excluded($sev) )
		    {	$Exclusions::severity_ignored_host_count{$host}++;
			if ( $severity=~/g/ )
			{  $Exclusions::severity_ignored_green_host_count{$host}++;
			} elsif ( $severity=~/z/ )
			{  $Exclusions::severity_ignored_brown_host_count{$host}++;
			} elsif ( $severity=~/y/ )
			{  $Exclusions::severity_ignored_yellow_host_count{$host}++;
			} else
			{  $Exclusions::severity_ignored_red_host_count{$host}++;
			}
		    } else
		    {	$::severity_host_count{$host}++;
			if ( $severity=~/g/ )
			{  $::severity_green_host_count{$host}++;
			} elsif ( $severity=~/z/ )
			{  $::severity_brown_host_count{$host}++;
			} elsif ( $severity=~/y/ )
			{  $::severity_yellow_host_count{$host}++;
			} else
			{  $::severity_red_host_count{$host}++;
			}
		    } 
		}
	}
    }

	$Exclusions::host_severity_count_flag = time;
}

sub get_worst_fact_for_host_vuln($$)
#This returns the "worst" fact for a given host and vulnerability (service
#output field).  I'm not real sure how needed it is, as I think should only
#be a single severity record per host and service output combo, but the
#update_severities function allows for multiple.  Basically, we just return
#the first one we find that isn't excluded, or the any one found if all
#excluded.
{	my $host=shift;
	my $vuln=shift;

	my $facts = $::severity_host_type_info{$host}{$vuln};
	my @facts = split /\n/, $facts;
	my $fact;
	my $worstfact="";

	#If only one fact, don't need to check for exclusions
	#If more than one, don't check first one, and stop at next one
	#find that isn't excluded ( if all excluded, first one will get
	#returned, and doesn't matter if was excluded or not).
	foreach $fact (@facts)
	{	if ( ! $fact ) { next; }
		if ( ! $worstfact ) { $worstfact=$fact; next; }
		if ( is_excluded($fact) ) { next; }
		$worstfact=$fact; 
		last;
	}
	return $worstfact;
}

sub any_nonexcluded_facts_by_severity($;$)
#Returns true if there is a non-excluded fact for the specified severity
#IF optional second argument is given, restricts itself to non-excluded
#severity of given level for the named host(s), otherwise looks at all hosts.
{	my $sev=shift;
	my $hosts=shift || undef;

	my ($host, $facts, $fact);

	#Form list of all facts for specified hosts or everyone, as requested
	if ( $hosts )
	{	$facts="";
		foreach $host (split '\s+', $hosts )
		{	$facts.=${$::severity_levels{$sev}}{$host};
		}
	} else
	{	$facts = join "", values %{$::severity_levels{$sev}};
	}

	my @facts=split /\n/,$facts;
	foreach $fact (@facts)
	{	if (is_excluded($fact) ) { next; }
		#Found a non-excluded fact
		return 1;
	}
	#Found none
	return 0;
}

sub any_nonexcluded_facts_by_host($;$)
#Returns true if there is a non-excluded fact for the host
#IF optional second argument is given, restricts itself to non-excluded
#severity of given severity type, otherwise looks at all types.
{	my $host=shift;
	my $sevs=shift || undef;

	my ($sev, $facts, $fact, @sevs);

	#Form list of all facts for specified severity or all, as requested
	if ( $sevs ) 
	{	@sevs = split '\s+', $sevs;
	} else
	{	@sevs = keys %::severity_levels;
	}
	$facts="";
	foreach $sev ( @sevs )
	{	$facts.=${$::severity_levels{$sev}}{$host};
	}

	my @facts=split /\n/,$facts;
	foreach $fact (@facts)
	{	if (is_excluded($fact) ) { next; }
		#Found a non-excluded fact
		return 1;
	}
	#Found none
	return 0;
}


#
# Generate severities-dependent statistics with exclusion info.
# Based on the standard SATAN version
sub make_severity_exclusion_info ()
{
    my ($severity, $host, $phash);
    #sizeof(*junk) won't work if lexical
    local (%::junk);

    if ($Exclusions::severity_flag > 0) {
	return;
    }
    $Exclusions::severity_flag = time();

    # Make the counts based on all severities
    for $severity (keys %::severity_type_host_info) 
    {   %::junk = %{$::severity_type_host_info{$severity}};
	$::severity_type_count{$severity} = sizeof(*::junk);
	$Exclusions::severity_type_ignored_count{$severity}=0;
	$Exclusions::severity_type_total_count{$severity}= 
		$::severity_type_count{$severity};
    }

    # Now remove excluded cases
	my ($host, $severity, @sevs );
	foreach $host ( keys %Exclusions::exclusions_by_host )
	{	$phash=$Exclusions::exclusions_by_host{$host};
		@sevs = keys %$phash;
		foreach $severity (@sevs)
		{	$::severity_type_count{$severity}--;
			$Exclusions::severity_type_ignored_count{$severity}++;
		}
	}
}


#
# Generate services-dependent statistics.
# A replacement for make_service_info which includes exclusion stats
#
sub make_service_exclusion_info 
{   my ($service, $host);
    #For sizeof(*junk) to work, must not be lexical
    local (%::junk);

    if ($Exclusions::service_flag > 0) { return; }
    $Exclusions::service_flag = time();
    &make_severity_exclusion_info();


    %::server_info = ();
    for $service (keys %::servers) 
    {   %::junk = %{$::servers{$service}};
	$::server_counts{$service} = sizeof(*::junk);
	$::server_severities{$service} = 0;
	$Exclusions::server_severities_total{$service} = 0;
	$Exclusions::server_severities_ignored{$service}=0;
	for $host (keys %::junk) 
	{   $::server_info{$host} .= $service . "\n";
	    #Increment count of hosts with any severities (ignored or not)
	    if (exists($::severity_host_type_info{$host})) 
	    { 	$Exclusions::server_severities_total{$service}++;
		if ( any_nonexcluded_facts_by_host($host) )
	        { 	$::server_severities{$service}++;
	        } else
	    	{	$Exclusions::server_severities_ignored{$service}++;
		}
	    }
	}
    }
    %::other_server_info = ();
    for $service (keys %::other_servers) 
    {   %::junk = %{$::other_servers{$service}};
	$::other_server_counts{$service} = sizeof(*::junk);
	$::other_server_severities{$service} = 0;
	$Exclusions::other_server_severities_total{$service} = 0;
	$Exclusions::other_server_severities_ignored{$service}=0;
	for $host (keys %::junk) 
	{   $::other_server_info{$host} .= $service . "\n";
	    #Increment count of hosts with any severities (ignored or not)
	    if (exists($::severity_host_type_info{$host})) 
	    { 	$Exclusions::other_server_severities_total{$service}++;
		if ( any_nonexcluded_facts_by_host($host) )
	        { 	$::other_server_severities{$service}++;
	        } else
	    	{	$Exclusions::other_server_severities_ignored{$service}++;
		}
	    }
	}
    }
    %::client_info = ();
    for $service (keys %::clients) 
    {   %::junk = %{$::clients{$service}};
	$::client_counts{$service} = sizeof(*junk);
	$::client_severities{$service} = 0;
	$Exclusions::client_severities_total{$service} = 0;
	$Exclusions::client_severities_ignored{$service} = 0;
	for $host (keys %::junk) 
        {   $::client_info{$host} .= $service . "\n";
	    if (exists($::severity_host_type_info{$host})) 
	    {   $Exclusions::client_severities_total{$service}++;
	    	if ( any_nonexcluded_facts_by_host($host) )
	    	{ 	$::client_severities{$service}++;
	    	} else
	    	{	$Exclusions::client_severities_ignored{$service}++;
	    	}
	    }
	}
    }
}

#
# Generate subnet statistics.
# A replacement for make_subnet_info which includes exclusion stats
#
sub make_subnet_exclusion_info 
{ 	my($subnet, $host);

	if ($Exclusions::subnet_flag > 0) { return; }
	$Exclusions::subnet_flag = time();

	&make_severity_exclusion_info();


	%::all_subnets = ();
	%::host_subnet = ();

	for $host (keys %::all_hosts) 
	{ 	if ($host) 
		{ 	($subnet = $::all_hosts{$host}{'IP'}) =~ s/\.[^.]*$//;
			$subnet = "unknown" unless $subnet;
			$::all_subnets{$subnet} .= "$host ";
			$::host_subnet{$host} = $subnet;
		}
	}

	# Cheat in case the facts file has names not in all-hosts.

	for $host (keys %::hosttype, keys %::severity_host_count) 
	{ 	next unless $host;
		next if defined($::host_subnet{$host});
		if ($host =~ /^([0-9]+\.[0-9]+\.[0-9]+)\.[0-9]+$/) 
		{ 	$subnet = $1;
		} else 
		{ 	$subnet = "unknown";
		}
		$::all_subnets{$subnet} .= "$host ";
		$::host_subnet{$host} = $subnet;
	}

	for $subnet (keys %::all_subnets) 
	{ 	$::subnet_count{$subnet}=split(/\s+/, $::all_subnets{$subnet});
		$::subnet_severities{$subnet} = 0;
		$Exclusions::subnet_severities_total{$subnet} = 0;
		$Exclusions::subnet_severities_ignored{$subnet} = 0;
		for $host (split(/\s+/, $::all_subnets{$subnet})) 
		{   if ( exists($::severity_host_type_info{$host}) )
		    { 	$Exclusions::subnet_severities_total{$subnet}++;
			if (  any_nonexcluded_facts_by_host($host) )
			{    $::subnet_severities{$subnet}++ ;
			} else
			{    $Exclusions::subnet_severities_ignored{$subnet}++;
			}
		    }
		}
	}
}

#
# Generate hosttype-dependent statistics.
# A replacement for make_hosttype_info which includes exclusion stats
#
sub make_hosttype_exclusion_info 
{   my($host, $class, $type, $count);

    if ($Exclusions::hosttype_flag > 0) { return; }
    $Exclusions::hosttype_flag = time();

    &make_severity_exclusion_info();

    %::hosts_by_systype = ();
    %::systypes_by_class = ();
    %::systype_severities = ();
    %::systype_counts = ();
    %::sysclass_severities = ();
    %::sysclass_counts = ();

    for $host (keys %::all_hosts) 
    { 	if ($host && $::hosttype{$host} eq "" && &get_host_time($host) > 1)
	{ 	$::hosttype{$host} = $::hosttype_unknown ;
	}
	if ($host && $::hosttype{$host} eq "") 
	{	$::hosttype{$host} = $::hosttype_notprobed ;
	}
    }
    $::sysclass{$::hosttype_unknown} = "other";
    $::sysclass{$::hosttype_notprobed} = "other";

    for $type (sort keys %::sysclass) {
	$::systype_severities{$type} = 0;
	$Exclusions::systype_severities_total{$type} = 0;
	$Exclusions::systype_severities_ignored{$type} = 0;
	$::sysclass_severities{$::sysclass{$type}} = 0;
	$Exclusions::sysclass_severities_total{$::sysclass{$type}} = 0;
	$Exclusions::sysclass_severities_ignored{$::sysclass{$type}} = 0;
    }

    for $host (sort keys %::hosttype) 
    {   $type = $::hosttype{$host};
	if ($type eq "") { $type = $::hosttype{$host} = $::hosttype_unknown; }
	if (exists($::severity_host_type_info{$host})) 
	{ 	$Exclusions::systype_severities_total{$type}++;
		if (  any_nonexcluded_facts_by_host($host) )
		{ 	$::systype_severities{$type}++;
		} else
		{ 	$Exclusions::systype_severities_ignored{$type}++;
		}
	}
	$::hosts_by_systype{$type} .= $host . "\n";
	$::systype_counts{$type}++;
    }

    for $type (sort keys %::sysclass) 
    { 	if (($count = $::systype_counts{$type}) > 0) 
	{   $class = $::sysclass{$type};
	    $::systypes_by_class{$class} .= $type . "\n";
	    $::sysclass_counts{$class} += $count;
	    $::sysclass_severities{$class} += $::systype_severities{$type};
	    $Exclusions::sysclass_severities_ignored{$class} += 
		$Exclusions::systype_severities_ignored{$type};
	    $Exclusions::sysclass_severities_total{$class} += 
		$Exclusions::systype_severities_total{$type};
	} else { delete($::sysclass{$type}); }
    }
}


#Turn off strict as rest of SAINT/SARA/SATAN won't agree with it
no strict;

1;
