#!/usr/bin/perl -w
# Build engine to create reports out of a nmap output-file
# by reading in the input file with hosts-to-scan, scan them,
# and create a report.
# 
# To speed-up scanning lots of targets, we will fork the nmap-scans
# and thus run multiple threads at the same time.
#
# An experimental feature is also included to fetch a header
# from the deamon running behind the open port.
# As the behavior of the target is not predictable, the output
# will not always be neat. Use it with care. 
# 
# Copyright (C) 2003 Bart Somers <bart@doornenburg.homelinux.net>
#
# $Id: bilbo,v 0.11 2004/11/25 20:58:13 bart Exp $
# $Revision: 0.11 $
#
# This software is released under the GPL. The full licence
# can be found at:
# http://www.gnu.org/licenses/licenses.html#GPL
#


use strict;
use IO::Socket::INET;
our ($nmap, @kidpid, %resultdb);

# To prevent others from reading the information, make sure the
# umask is set very restrictive. Be aware that a less restrictive umask-
# setting can give others also access to your database with scanned hosts!
umask 077;

# Set the admin-mail, so we can mail the report.
# Multiple email adresses are seperated by a space.
# Be aware of the "\" before the "@" as perl might get confused:
# mail\@domain.com
our $mail = 0;	# put to 0 for no mail.
our $adminmail = "admin";

# CRLF to communicate with not-unix systems. Please do not change.
our $crlf = "\015\012";

# Enhanced mode tries to gather header-information from the service running
# behind the opened port, for as far as we know a way to get this information.
# This information is stored in the "hash-database" %liveports.
# Warning! Putting enhanced to 1 can clutter up your results.
our $enhanced = 0;

# This is the "hash-database" which hold the sequences to send
# to the client-port. Add new if you know them and please send them to me too.
our %liveports = (
	"80" => "HEAD / HTTP/1.0".$crlf.$crlf,
	"25" => "quit".$crlf,
	"21" => "quit".$crlf,
	"22" => "quit".$crlf,
	"110" => "quit".$crlf,
	"143" => "quit".$crlf,
	"3128" => "HEAD http://localhost:25/ HTTP/1.0".$crlf.$crlf,
	"8080" => "HEAD http://localhost:25/ HTTP/1.0".$crlf.$crlf,
);

# Change the default options passed to nmap as you like.
# See "man nmap" or "nmap -h" for details. 
# Remember, the options can be changed in the input-file with
# the flag <OPT>.
#our $nmap_options = "-sS -sU -P0";
our $nmap_options = "-sS";

# Debug. Will put more info on your screen.  Usually not usefull.
# Warning: Maybe it will clutter-up your screen!
# Put to 1 to have debugging.
our $debug = 0;

# Tune. Limit the amount of processes to run simultanuous.
# Put to a resonable value of your machine AND network. You can
# get problems with these if it's set too high!
# On a normal network, 5 should be reasonable.
our $tune = 5;

# Match mode.  Compare the results against a database from previous scans.
# If the database does not exists, it will be created.
# Match mode has 3 modes it can run in:
# 0	Do nothing. It WILL scan and report, but no database-actions will be taken.
# 1	Match against the database, but do not update the database.
# 2	Match against the database and update it while running.
#
# match_db is the place where the database is stored.
my $match_mode = 2;
my $match_db = "/var/tmp/match.db";

# Report Only
my $report_only = 0;
# No configurable options below this line
# ############################################################################################

our $infile = $ARGV[0];
our $repfile = $ARGV[1];
our $tmpfile = "/tmp/".$infile.".tmp";

our $nmap_append = " --append_output -oG $tmpfile";
$nmap_options = $nmap_options.$nmap_append;

sub scanner
{
	#our $tmpfile = $infile.".tmp";
	if ( -e $tmpfile ) {
		print "Temporary file $tmpfile exists.\n";
		# Now we should do anything, but during test is not handy.
		# For the time beeing, just bail-out.
		exit (1);
	}
	open (INPUT, " $infile") || die("Can't open $infile : $!");
	while ( my $dest = <INPUT> ){
		chomp $dest;
		if ( scalar @kidpid >= $tune ) {
			scheduler();
		}
		if ( $dest =~ /^<OPT>/ ) {
			$dest =~ s/^<OPT>//;
			$nmap_options = $dest.$nmap_append;
			#print "New nmap_options: $nmap_options \n\n";
		} elsif ( $dest !~ /^#/ && $dest !~ /^$/ ) {
			if ( my $pid = fork ) {
				# Whe appear to be the parent process
				push @kidpid, $pid;
			} else {
				defined($pid) or die "Oops, something went wrong during fork: $!";
				if ( $debug == 1 ) {
					print "$nmap $nmap_options $dest \n\n";
				}
				open (STDOUT, "/dev/null");
				#exec ($nmap, $nmap_options, $dest) or die "nmap made a boo-boo: $!";
				system "$nmap $nmap_options $dest";
				exit (0);
			}
			## Shoot the standard output from nmap into our black-hole-device	
			#open (STDOUT, "/dev/null");	
			#system "$nmap $nmap_options $dest";
			##print "$nmap $nmap_options $dest \n\n";
			#close STDOUT;
		}
	}	
	close INPUT;
}

sub reporter {
	# First wait until all kiddo's are finished
	my $piddie;
	foreach $piddie ( @kidpid ) {
		if ( $debug == 1 ) {
			my $test = kill 0,$piddie;
			print "Kid-pid: $piddie, Value: $test\n";
		}
		waitpid $piddie,0;
	}
	# OK, Start to generate a report
	if ( ! defined $infile || ! defined $repfile || $< != 0 ) {
		print "Usage (as root): $0 <input-file> <report-file> \n";
		exit (1);
	}	
	open (REPFILE, "> $repfile") || die("Can't open $repfile : $!");
	open (TEMPFILE, " $tmpfile") || die("Can't open $tmpfile : $!"); 
	while ( my $line = <TEMPFILE> ) {
		if ( $line =~ /^#/ && $line =~ / scan initiated/ ) {
			# A target was selected
			chomp $line;
			my @target = split " ", $line;
			print (REPFILE "Scanning host $target[-1] for open ports.\n");
			print "Target $target[-1] seen.\n" if ( $debug == 1 );
		} elsif ( $line !~ /^#/ ) {
			# We do have a result....
			chomp $line;
			# first, remove the comma's
			$line =~ tr/,//d;
			# second, change the [tab] into [space]
			$line =~ tr/\t/ /;
			# third, put it in an array
			our @line = split " ", $line;
			our $line_count = scalar @line;
			# Check for more then 4 fields, only then you have open ports.
			if ( $line_count > 4 && $line !~ /\sUp$/ ) 
			{
				print "More than 4 fields found: $line \n\n" if ( $debug == 1 );
				print (REPFILE $line[0], " ", $line[1], " ", $line[2], " has open ports: \n");
				# Forget the last 4 fields if the line ends with "Ignored State ....."
				my $line_count_int;
				if ( $line =~ /Ignored/ ) {
					$line_count_int = $line_count - 4;
					print "Line contains \"Ignored\" . Count = $line_count_int\n" if ( $debug == 1 );
				} else {
					$line_count_int = $line_count;
					print "Line count = $line_count_int\n" if ( $debug == 1 );
				}
				# Start counting at field 4, as we allready had the first fields.
				my $portnumber = 4;
				while ( $portnumber < $line_count_int )
				{
					# Throw everything again in an array, to process it nicely
					print "Try to split field $portnumber, which is this: $line[$portnumber]\n\n" if ( $debug == 1 );
					my @portdef = split "/", $line[$portnumber];
					if ( $portdef[1] eq "open" ) {
						#print (REPFILE "  Port: $portdef[0] Protocol: $portdef[2]");
						print (REPFILE "  Port: $portdef[0] - $portdef[2]");
					}
					if ( defined $portdef[4] && $portdef[1] eq "open" ) 
					{ 
						#print (REPFILE " Known service: $portdef[4] \n"); 
						print (REPFILE " - $portdef[4] \n"); 
						database(host => $line[1], port => $portdef[0]) if ( $match_mode > 0 );
						# Now check if we are in enhanced mode, to
						# gather some header info.
						if ( $enhanced == 1 && defined $liveports{$portdef[0]} ) {
							print (REPFILE "  Header information: \n");
							#print " Header information: \n";
							my $remote = IO::Socket::INET->new(
								Proto => $portdef[2],
								PeerAddr => $line[1],
								PeerPort => $portdef[0],
								Timeout => 3,
							)
								or die "Cannot connect";
							print $remote $liveports{$portdef[0]};
							read $remote, $a, 256;
							print (REPFILE $a);
							shutdown ($remote, 2);
							print (REPFILE "\n");
						}
						print (REPFILE "\n");
					} elsif ( ! defined $portdef[4] && $portdef[1] eq "open" ) {
						print (REPFILE "\n");
						database(host => $line[1], port => $portdef[0]) if ( $match_mode > 0 );
						# Draw a line to maintain readability.
						print (REPFILE "\n");
					}	
					$portnumber ++;
				}	
			} elsif ( $line_count == 5 && $line =~ /\sUp$/ )  {
				# Host is up, but no ports open.
				print (REPFILE "Host $line[1] up, but doesn't show open ports.\n\n");
			}
		}
		if ( $debug == 0 ) {
			unlink $tmpfile;
		}
	}	
	close TEMPFILE;
	close REPFILE;
}

sub mailreport
{
	if ( $mail == 1 ) {
		my $hostname = `hostname`;
		my $osystem = `uname -s`;
		chop $osystem;
		# OK, checking for SunOS is a little bit silly right now, but hey, what the heck.
		if ( -s $repfile && $osystem ne "SunOS" && defined $adminmail )
		{
			system ("cat $repfile | mail -s \"Scanreport from Bilbo with inputfile $infile \" $adminmail");
		} elsif ( -s $repfile && defined $adminmail )
		{
			system ("cat $repfile | mailx -s \"Scanreport from Bilbo with inputfile $infile \" $adminmail");
		}	
	}
}

sub findnmap
{
	if ( -x "/usr/local/bin/nmap" ) {
		$nmap = "/usr/local/bin/nmap";
	} elsif ( -x "/usr/bin/nmap" ) {
		$nmap = "/usr/bin/nmap";
	} else {
		print "Can't find nmap in /usr/local/bin/nmap or /usr/bin/nmap.";
		exit(1);
	}
}

sub scheduler
{
	# First wait until one kiddo finishes
	my ( $piddie, @alivepid, $tmppid );
	wait;
	foreach $piddie ( @kidpid ) {
		my $test = kill 0,$piddie;
		if ( $test == 1 ) {
			# Kid appears to be alive.
			push @alivepid, $piddie;
		}
		if ( $debug == 1 ) {
			print "Kid-pid: $piddie, Value: $test\n";
			print "Pid's alive: ";
			foreach $tmppid (@alivepid) {
				print "$tmppid ";
			}
			print "\n";
		}
		#waitpid $piddie,0;
	}
	@kidpid = @alivepid;
}

sub database
{
	# resultdb is written to disk.
	# matchdb and match_db are references to the file on disk.
	my ( $host, $rest, @port );
	my %options = @_;
	if ( defined $options{"mode"} && $options{"mode"} eq "read" ) {
		open (MATCHDB, " $match_db") || die "Can\'t open $match_db : $!";
		while ( my $dbline = <MATCHDB> ) {
			next if ( $dbline =~ /^#/ || $dbline =~ /^(\s*)$/ );
			chomp $dbline;
			( $host, $rest ) = split /:\s*/, $dbline, 2;
			@port = split / /, $rest;
			$resultdb{$host} = [ @port ];
		}
		close MATCHDB;
	} elsif ( defined $options{"mode"} && $options{"mode"} eq "write" ) {
		open (MATCHDB, " >$match_db") || die "Can\'t open $match_db for writing: $!";
		for $host ( keys %resultdb ) {
			print "host: $host ports: @{ $resultdb{$host} } \n" if ( $debug == 1 );
			#print (MATCHDB $host, ": ", @{ $resultdb{$host} }, "\n");
			print (MATCHDB $host, ": ");
			for my $i ( 0 .. $#{ $resultdb{$host} } ) {
				print (MATCHDB $resultdb{$host}[$i], " ");
			}
			print (MATCHDB "\n");
		}
		close MATCHDB;
	} elsif ( defined $options{"host"} && defined $options{"port"} ) {
		# We have a host with an open port....
		$host = $options{"host"};
		if ( defined $resultdb{$host} ) {
			# Host is known
			my $match = 0;
			for my $i ( 0 .. $#{ $resultdb{$host} } ) {
				if ( $resultdb{$host}[$i] eq $options{"port"} ) {
					# We have a match
					$match = 1;
				}
			}
			if ( $match == 0 ) {
				print (REPFILE "  ** New port found on host ", $host, " : port ", $options{"port"}, "\n");
				push @{ $resultdb{$host} }, $options{"port"} if ( $match_mode == 2 );
				print (REPFILE "  ** Port added to database.\n") if ( $match_mode == 2 );
			}
		} elsif ( ! defined $resultdb{$host} ) {
			print (REPFILE "  ** New host ", $host, " found, no ports to compare.\n");
			$resultdb{$host} = [ $options{"port"} ] if ( $match_mode == 2 );
			print "Adding: $resultdb{$host} with port $options{\"port\"} to database\n" if ( $debug == 1 );
			print (REPFILE "  ** Host added.\n") if ( $match_mode == 2 );
		}
	}

}

# Finally, execute somthing....

findnmap();
if ( -r $match_db && $match_mode > 0 ) {
	database( mode => "read");
}
if ( $report_only == 0 ) {
	scanner();
}
#scheduler();
reporter();
mailreport();
if ( $match_mode == 2 ) {
	database( mode => "write");
}

exit (0);
