#!/usr/bin/perl

#
# Copyright 2004 Berbee Information Networks
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2,
# as published by the Free Software Foundation
#

use strict;
no strict "refs";
no strict "subs";

my $CVSid = q$Id$;

use Getopt::Long;
Getopt::Long::Configure ("bundling");
use Pod::Usage;
use Data::Dumper;

=head1 NAME

nqr - Nessus Quick Summary

=head1 SYNOPSIS

B<nqs>
    [ --help (-h) ]
    [ --man (-m) ]
    [ --print-all (-a) ]
    [ --class <nessus-class>[,<nessus-class>...] (-c) ]
    [ --output-file <name> (-f) ]
    [ --nessus-id <Nessus-ID> (-i) ]
    [ --omit <Nessus-ID>[,<Nessus-ID>...] (-o) ]
    [ --severity <[hole | note | warning]> (-s) ]
    [ --host <host> (-t) ]
    [ --verbose (-v) ]
    [ --debug (-d) ]
    [ --regex <perl-regular-expression> ]
    [ --plugin-list <nessus-plugin-names> ]
    [ --classname-list <nessus-class-names> ]
    <nessus-NBE-file> [ ... ]

=head1 DESCRIPTION

B<nqs> scans the Nessus NBE file(s) for "results" and formats a simple report.

=head1 OPTIONS

=over 8

=item B<--host>            causes B<nqs> to report on a specific host from the file(s)

=item B<--nessus-id>       causes B<nqs> to report on a specific Nessus vulnerability ID from the file(s)

=item B<--class>           causes B<nqs> to report on one or more specific Nessus Class of vulnerabilities from the file(s). You may include multimple class names in a commma-separated list or with multiple "-c" settings. See the file "nessus-class-names" for the complete list of Nessus vulnerability classes

=item B<--severity>        causes B<nqs> to report on a specific Nessus Severity (Note, Hole, or Warning) from the file(s)

=item B<--output-file>     specifies the name of an output file

=item B<--omit>            omit the Nessus ID from the report. You may specify multiple IDs in a comma-separated list or with individual "-o" settings

=item B<--print-all>       print all entries, even duplicates

=item B<--verbose>         dump the contents of the nessus output as well as the name of the vulnerability

=item B<--regex>           use the specified Perl regular expression to search for matching entries in the file(s). You do not need to include the delimiters. The text in the regular expression will be matched case insensitive

=item B<--plugin-list>     specifies the name of a list of Nessus plugins in the format "ID|Name" (note: there is no short option letter for this option)

=item B<--class-name-list> specifies the name of a list of Nessus classes in the format "ID|Name". Use the class name "list" for a list of current Nessus classes (note: there is no short option letter for this option)

=item B<--help>            display a brief usage message

=item B<--man>             display the complete manual page for this command

=back

=head1 ENVIRONMENT

B<nqs> searches the colon-separated (":") path names in the shell
environment variable "NESSUS_PLUGIN_NAMES" for directories that point
to a file named "nessus-plugin-names". This file should contain lines
of the form

Nessus-ID|Nessus-Category|Plugin Name

For example

10412|Windows|SMB Registry : Autologon

The default value for NESSUS_PLUGIN_NAMES is
"/pub/tools/nessus-support:/usr/share/nessus-support:/usr/local/share/nessus-support"

=head1 EXAMPLES

=over 8

=item nqs nessus.nbe

=item nqs --output /tmp/nqs.out --host 99.11.22.33 nessus.nbe
Reports on all vulnerabilities for the host 99.11.22.33, and leaves the output in /tmp/nqs.out.

=item nqs -v -i 10412 -s warning nessus.nbe
Reports on all instances of Nessus ID 10412 which Nessus flagged as "Security Warning".

=item nqs -v -o 10287 -o 10107 nessus.nbe
Produces a verbose report ommiting Nessus IDs 10287 (traceroute to the targets), and 10107 (remote web server type).

=item nqs --verbose --class infos,attack --omit 10107 --output internal.nqs internal.nbe
Produces a verbose report that includes only the results in the Nessus classes "infos" and "attack".

=item nqs -v --regex 'snmp' nessus.nbe
Produces a verbose report that includes only entries matching the Perl regular expression "snmp".

=back

=head1 ENVIRONMENT

=over 8

=item NESSUS_PLUGIN_NAMES
Set this to a colon (":") separated list of path names to a file containing a list of Nessus plugin names. The file should consist of lines of the form: "<Nessus ID>|<Plugin Class>|<Descriptive Plugin Name>". The default path list is in the Perl constant "DEFAULT_PLUGIN_PATH".

=item NESSUS_CLASS_NAMES
Set this to a colon (":") separated list of path names to a file containing a list of Nessus "class names" The file should consist of lines of the form: "<Nessus Class>|<Class Description>". The default path list is in the Perl constant "DEFAULT_CLASS_PATH".

=back

=head1 SEE ALSO

nessus(1), perl(1), sh(1), grep(1), pcre(3)

=cut

use constant DEFAULT_PLUGIN_PATH => "/pub/tools/nessus-support:/usr/share/nessus-support:/usr/local/share/nessus-support";
use constant DEFAULT_PLUGIN_NAMES => "nessus-plugin-names";
use constant DEFAULT_CLASS_PATH => "/pub/tools/nessus-support:/usr/share/nessus-support:/usr/local/share/nessus-support";
use constant DEFAULT_CLASS_NAMES => "nessus-class-names";

sub main;
sub getPluginList ($$$);
sub getClassList ($$);
sub nessusPluginNames;
sub nessusClassNames;
sub listClasses ($);
sub byIP;
sub debug ($$);

main;

sub main {

    my ( $result, $help, $man, $classnameList, $debug, $host, $nessusid, $outputfile, $pluginList, $printAll, $regex, $severity, $verbose );
    my ( @omit );
    my ( @class );
    my $outputfh;
    my @nbeLines;
    my ( $classRef, $pluginRef );
    my $NBE;
    my $resultsRegExIP = q(results\|(\\d+\\.\\d+\\.\\d+)?\|\\d+\\.\\d+\\.\\d+\\.\\d+\|.*\|\\d+\|.*\|.*(?:\\\\n)+);
    my $resultsRegExHost = q(results\|((\\w+\\.)*\\w+\\.\\w+)?\|(\\w+\\.)*\\w+\\.\\w+\|.*\|\\d+\|.*\|.*(?:\\\\n)+);
    my ( $j1, $network, $ipaddress, $protocolType, $nbeid, $nbeseverity, $output );
    my $vulnCount = 1;

    $result = GetOptions (
			  "print-all|a" => \$printAll,
			  "host|t=s" => \$host,
			  "class|c=s" => \@class,
			  "nessus-id|i=s" => \$nessusid,
			  "severity|s=s" => \$severity,
			  "output-file|f=s" => \$outputfile,
			  "omit|o=s" => \@omit,
			  "regex=s" => \$regex,
			  "plugin-list=s" => \$pluginList,
			  "classname-list=s" => \$classnameList,
			  "verbose|v" => \$verbose,
			  "debug|d" => \$debug,
			  "help|h" => \$help,
			  "man|m" => \$man,
			  );
    pod2usage ( 2 )             if ( ! $result);
    pod2usage ( 1 )             if ( defined ( $help ));
    pod2usage ( -verbose => 2 ) if ( defined ( $man ));

    @omit = split ( /,/, join ( ',', @omit )); # allow comma-separated list of IDs to omit

    @class = split ( /,/, join ( ',', @class )); # allow comma-separated list of classes to include

    if ( $severity ) {
	die "Severity must be one of [ note | hole | warning ]. Stopped"
	    unless ( $severity =~ /hole|note|warning/i );
    }

    if ( ! $classnameList ) {
	$classnameList = nessusClassNames;
    }

    $classRef = getClassList ( $classnameList, $debug );

    # list the classes and exit
    if ( grep ( /list/, @class )) {
	listClasses ( $classRef );
	exit;
    }

    if ( ! $pluginList ) {
	$pluginList = nessusPluginNames;
    }

    $pluginRef = getPluginList ( $pluginList, \@class, $debug );

    foreach $NBE ( @ARGV ) {	      # snarf all lines from all files

	open ( NBE, $NBE ) || die "cannot open $NBE for reading ($!). Stopped";
	push ( @nbeLines, <NBE> );
	close ( NBE );

    }

    chomp @nbeLines;

    foreach my $nbeLine ( @nbeLines ) {

	my @results = ();

# line format:
#results|209.204.148|209.204.148.24|general/tcp|10336|Security Note|Nmap found that this host is running Draytek Vigor 2200e DSL router v2.1b, FreeSCO 0.27 (Linux kernel 2.0.38), Linux kernel 2.2.16, Linux kernel 2.4.18 (x86), Linux Kernel 2.4.0 - 2.5.20, Linux kernel 2.4.20, Siemens Speedstream 2602 DSL/Cable router, Microsoft Xbox running Debian Linux 2.4.20\n\n

	# ignore timestamps for now
	next if ( $nbeLine =~ m@^timestamps@ );

	( $j1, $network, $ipaddress, $protocolType, $nbeid, $nbeseverity, $output ) =
	    ( undef, undef, undef, undef, undef, undef, undef, undef, undef );

	if ( $nbeLine =~ m{^$resultsRegExIP}i || $nbeLine =~ m{^$resultsRegExHost}i ) {

	    if ( $regex ) {
		next unless ( $nbeLine =~ m{$regex}i );
	    }

	    ($j1, $network, $ipaddress, $protocolType, $nbeid, $nbeseverity, $output) = split ( /\|/, $nbeLine, 7 );

	    next if ( $j1 eq "results" && ! $nbeid ); # skip short lines
	    next if ( grep ( /$nbeid/, @omit )); # skip omission requests
	    next unless ( $pluginRef->{$nbeid} );

	    $output =~ s{\\[nr]}{\n\t}g;	     # clean up the raw output

	    if (( $host && $ipaddress eq $host ) && ( ! $nessusid ) && ( ! $severity )) {

		debug ( "nbeLine (host+!nessusid): ($nbeid) $ipaddress $protocolType\n", $debug );

		push ( @{$pluginRef->{$nbeid}->{'findings'}},
		       {
			   'output' => $output,
			   'protocol' => $protocolType,
			   'severity' => $nbeseverity,
		       } );

	    } elsif (( $host && $ipaddress eq $host ) && ( $severity && $nbeseverity =~ /$severity/i ) && ( ! $nessusid )) {

		debug ( "nbeLine (host+severity+!nessusid): ($nbeid) $ipaddress $protocolType\n", $debug );

		push ( @{$pluginRef->{$nbeid}->{'findings'}},
		       {
			   'output' => $output,
			   'protocol' => $protocolType,
			   'severity' => $nbeseverity,
		       } );

	    } elsif (( $host && $ipaddress eq $host ) && ( $nessusid && $nbeid == $nessusid )) {

		debug ( "nbeLine (host+nessusid): ($nbeid) $ipaddress $protocolType\n", $debug );

		push ( @{$pluginRef->{$nbeid}->{'findings'}},
		       {
			   'output' => $output,
			   'protocol' => $protocolType,
			   'severity' => $nbeseverity,
		       } );

	    } elsif (( $nessusid && $nbeid == $nessusid ) && ( ! $severity ) && ( ! $host )) {

		debug ( "nbeLine (nessusid+!severity): ($nbeid) $ipaddress $protocolType\n", $debug );

		push ( @{$pluginRef->{$nbeid}->{'findings'}},
		       {
			   'ipaddress' => $ipaddress,
			   'output' => $output,
			   'protocol' => $protocolType,
			   'severity' => $nbeseverity,
		       } );

	    } elsif (( $nessusid && $nbeid == $nessusid ) && ( $severity && $nbeseverity =~ /$severity/i )) {

		debug ( "nbeLine (nessusid+severity): ($nbeid) $ipaddress $protocolType\n", $debug );

		push ( @{$pluginRef->{$nbeid}->{'findings'}},
		       {
			   'ipaddress' => $ipaddress,
			   'output' => $output,
			   'protocol' => $protocolType,
			   'severity' => $nbeseverity,
		       } );
	    } elsif ( ! $host && ! $nessusid && $severity && $nbeseverity =~ /$severity/i ) {

		debug ( "nbeLine (!host+!nessusid+severity): ($nbeid) $ipaddress $protocolType\n", $debug );

		push ( @{$pluginRef->{$nbeid}->{'findings'}},
		       {
			   'ipaddress' => $ipaddress,
			   'output' => $output,
			   'protocol' => $protocolType,
			   'severity' => $nbeseverity,
		       } );
	    } elsif ( ! $host && ! $nessusid && ! $severity ) {

		debug ( "nbeLine (all): ($nbeid) $ipaddress $protocolType\n", $debug );

		push ( @{$pluginRef->{$nbeid}->{'findings'}},
		       {
			   'ipaddress' => $ipaddress,
			   'output' => $output,
			   'protocol' => $protocolType,
			   'severity' => $nbeseverity,
		       } );
	    }

	}
    }

    print STDERR Data::Dumper->Dump ( [ $pluginRef ] ) if ( $debug );

    if ( $outputfile ) {
	open (OUTFILE, ">$outputfile" ) ||
	    die "Cannot open $outputfile for writing ($!). Stopped";
	$outputfh = OUTFILE;
    } else {
	$outputfh = STDOUT;
    }

    # print the report
    print $outputfh
	"Nessus report ";
    print $outputfh "for ", $host if ( $host );
    print $outputfh " Nessus ID: $nessusid" if ( $nessusid );
    print $outputfh " Severity \"Security ", ucfirst( $severity ), "\"" if ( $severity );
    print $outputfh "\n\n";

    foreach my $id ( sort { $pluginRef->{$a}->{'group'} cmp $pluginRef->{$b}->{'group'} } %$pluginRef ) {
	my ( $last, $lastOutput );

	next unless ( @{$pluginRef->{$id}->{'findings'}} );

	next if ( ! $printAll && ( $pluginRef->{$id}->{'class'} =~ /init|settings|scanner/ ));

	print $outputfh
	    sprintf ( "%3d", $vulnCount++ ),
	    ": ",
	    $pluginRef->{$id}->{'name'},
	    " (nessus ID: ",
	    $id,
	    ")\n",
	    "\t(",
	    $pluginRef->{$id}->{'group'},
	    ") [",
	    $classRef->{$pluginRef->{$id}->{'class'}},
	    "]",
	    "\n";

	foreach my $findingRef ( sort byIP @{$pluginRef->{$id}->{'findings'}} ) {
	    next if ( ! $printAll && ( $findingRef->{'ipaddress'} . $findingRef->{'protocol'} . $findingRef->{'output'} eq $last ));

	    print $outputfh
		"\t",
		$findingRef->{'ipaddress'},
		" (", $findingRef->{'protocol'}, ")",
		" [", $findingRef->{'severity'}, "]",;

	    if ( $verbose ) {

		if ( $findingRef->{'output'} eq $lastOutput ) {
		    print " (duplicate output)\n";
		} else {
		    print $outputfh
			"\n\t",
			$findingRef->{'output'},
			"\n";
		}

		$lastOutput = $findingRef->{'output'};
	    } else {
		print "\n";
	    }

	    $last = $findingRef->{'ipaddress'} . $findingRef->{'protocol'} . $findingRef->{'output'};
	}
    }
}

sub getPluginList ($$$) {
    my $pluginFileName = shift;
    my $classRef = shift;
    my $debug = shift;

    my @p = ();
    my $pRef;

    open ( P, $pluginFileName ) || die "cannot open $pluginFileName for reading ($!). Stopped";
    @p = <P>;
    close ( P );

    chomp ( @p );

    foreach my $plugin ( @p ) {

	my ( $id, $group, $name, $class ) = split ( /\|/, $plugin, 4 );

	if ( @$classRef ) {
	    next unless ( grep ( /$class/, @$classRef ));
	}

	$pRef->{ $id }->{'group'} = $group;
	$pRef->{ $id }->{'name'} = $name;
	$pRef->{ $id }->{'class'} = $class;
	debug ( "getPluginList:  $id ($pRef->{ $id }->{class}, in group $pRef->{ $id }->{group}) is $pRef->{ $id }->{name}\n", $debug );
    }

    debug ( "getPluginList: End of HASH BUILD\n", $debug );
    if ( $debug ) {
	foreach my $id ( sort { $pRef->{$a}->{'group'} cmp $pRef->{$b}->{'group'} } keys (%$pRef )) {
	    debug ( "getPluginList:  $id ($pRef->{ $id }->{class}, in group $pRef->{ $id }->{group}) is  $pRef->{ $id }->{name}\n", $debug );
	}
    }

    $pRef;
}

sub getClassList ($$) {
    my $classFileName = shift;
    my $debug = shift;

    my @c = ();
    my $cRef;

    open ( C, $classFileName ) || die "cannot open $classFileName for reading ($!). Stopped";
    @c = <C>;
    close ( C );

    chomp ( @c );

    foreach my $class ( @c ) {

	my ( $name, $description ) = split ( /\|/, $class, 2 );

	$cRef->{ $name } = $description;
	debug ( "getClassList:  $name is  $cRef->{ $name }\n", $debug );
    }

    $cRef;
}

sub nessusPluginNames {
    my $envPaths = $ENV{'NESSUS_PLUGIN_NAMES'};
    my @paths;
    my $path;
    my $fileName;
    my $found = 0;

    if ( $envPaths ) {
	@paths = split ( /:/, $envPaths );
    } else {
	@paths = split ( /:/, DEFAULT_PLUGIN_PATH );
    }

    foreach $path ( @paths ) {
	$fileName = $path . "/" . DEFAULT_PLUGIN_NAMES;
	$found = 1, last if ( -r $fileName );
    }

    $fileName = undef unless ( $found );

    $fileName;
}

sub nessusClassNames {
    my ( $envClasses ) = $ENV{'NESSUS_CLASS_NAMES'};
    my @paths;
    my $path;
    my $fileName;
    my $found = 0;

    if ( $envClasses ) {
	@paths = split ( /:/, $envClasses );
    } else {
	@paths = split ( /:/, DEFAULT_CLASS_PATH );
    }

    foreach $path ( @paths ) {
	$fileName = $path . "/" . DEFAULT_CLASS_NAMES;
	$found = 1, last if ( -r $fileName );
    }

    $fileName = undef unless ( $found );

    $fileName;
}

sub listClasses ($) {
    my $classRef = shift;

    my ( $className, $classDescription );

    format STDOUT_TOP =
  Nessus Class          Description
.
    format STDOUT =
@<<<<<<<<<<<<<<<<<<    @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$className,      $classDescription
.

    foreach my $class ( sort ( keys ( %$classRef ))) {
	$className = $class;
	$classDescription = $classRef->{ $class };
	write;
    }

    1;
}

# sort by IP address
#
sub byIP {
    my @oct_a = split /\./, $a->{'ipaddress'};
    my @oct_b = split /\./, $b->{'ipaddress'};

    $oct_a[0] <=> $oct_b[0] or
        $oct_a[1] <=> $oct_b[1] or
        $oct_a[2] <=> $oct_b[2] or
        $oct_a[3] <=> $oct_b[3] or
        $a cmp $b
}

sub debug ($$) {
    my $message = shift;
    my $flag = shift;

    print STDERR "nqs: ", $message if $flag;
}

1;

__END__
