#!/usr/bin/perl -w
##############################################################################
# Copyright (c) 2008-2010, League of Crafty Programmers Ltd <info@locp.co.uk>
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY LEAGUE OF CRAFTY PROGRAMMERS ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL LEAGUE OF CRAFTY PROGRAMMERS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
##############################################################################
use Getopt::Long;
use IO::Handle;
use Nmap::Parser;
use Pod::Usage;
use strict;

##############################################################################
# Main processing starts here.
##############################################################################

# Declare and initialise local variables.
my $baseline_file;
my $c;
my $command_line;
my $format;
my $gen_stylesheet;
my $help;
my $hostname_key;
my $man;
my $node_key = "IP";
my $observed_file;
my $output_file;
my $output_hosts;
my $output_ports;
my $report;
my $run_date;
my $show_version;
my $stylesheet;
my $xslt;

# OK so all the other variables are in lowercase and local.  The reason why
# version is specified differently is that that it is compatible with
# Module::Build therefore making distribution and installing easier.
my $VERSION = '1.3';

$run_date = localtime;
$report = Report->new;

##############################################################################
# Command line parsing.
##############################################################################

if ($#ARGV == 1 and -f $ARGV[0] and -f $ARGV[1])
{
	@ARGV = ('--baseline', $ARGV[0], '--observed', $ARGV[1]);
}

$command_line = join(' ', @ARGV);

GetOptions(
	'baseline=s' => \$baseline_file,
	'format=s' => \$format,
	'gen-stylesheet=s' => \$gen_stylesheet,
	'help|?' => \$help,
	'hostname-key|hk' => \$hostname_key,
	'man' => \$man,
	'observed|o=s' => \$observed_file,
	'output-file|of=s' => \$output_file,
	'output-hosts|os=s' => \$output_hosts,
	'output-ports|op=s' => \$output_ports,
	'stylesheet=s' => \$stylesheet,
	'version|V' => \$show_version
	) or pod2usage(1);

pod2usage(0) if $help;
pod2usage(-verbose => 2) if $man;

if ($show_version) {
	if ($baseline_file || $format || $gen_stylesheet || $help || $man ||
	    $observed_file || $output_ports || $stylesheet) {
		pod2usage(1);
	} else {
		print "Version: $VERSION\n";
		exit(0);
	}
}

if ($gen_stylesheet) {
	if ($baseline_file || $format || $help || $hostname_key || $man ||
	    $observed_file || $output_ports || $show_version || $stylesheet) {
		pod2usage(1);
	} else {
		$xslt = new Stylesheet;
		$xslt->generate($gen_stylesheet);
		exit(0);
	}
}

if ($stylesheet && ! -e $stylesheet) {
	unless ($stylesheet =~ m/:\/\//i) {
		# To be here, a stylesheet has been specified that is not a
		# URL (i.e. it is a file) and the file doesn't currently
		# exist, so create it.
		$xslt = new Stylesheet;
		$xslt->generate($stylesheet);
	}
}

if ($output_hosts) {
	$report->show_changed_hosts(0);
	$report->show_missing_hosts(0);
	$report->show_new_hosts(0);

	for (my $i = 0; $i < length($output_hosts); $i++) {
		$c = substr($output_hosts, $i, 1);
		if ($c eq "c") {
			$report->show_changed_hosts(1);
		} elsif ($c eq "m") {
			$report->show_missing_hosts(1);
		} elsif ($c eq "n") {
			$report->show_new_hosts(1);
		} else {
			pod2usage(1);
		}
	}
}

if ($output_ports) {
	for (my $i = 0; $i < length($output_ports); $i++) {
		$c = substr($output_ports, $i, 1);

		if ($c eq 'o') {
			next;
		} elsif ($c eq 'f') {
			next;
		} elsif ($c eq 'c') {
			next;
		} elsif ($c eq 'x') {
			next;
		} else {
			pod2usage(1);
		}
	}
}

if ($format) {
	if ($format eq 'xml') {
		$report->format($format);
	} else {
		pod2usage(1);
	}
}

##############################################################################
# Open and parse the files
##############################################################################
pod2usage(1) if !$baseline_file;
my $baseline = new Scan;
$baseline->parsefile($baseline_file);
pod2usage(1) if !$observed_file;
my $observed = new Scan;
$observed->parsefile($observed_file);

if ($hostname_key) {
	# The user has specified that special DHCP jiggery-pokery is to be
	# done.
	$baseline->dhcp(1);
	$baseline->create_maps;

	if ($baseline->dhcp) {
		# If the DHCP flag is still true, then the maps were created
		# successfully.  Otherwise DHCP processing is not possible.
		$observed->dhcp(1);
		$observed->create_maps;

		if (!$observed->dhcp) {
			# If we get here, then the baseline maps were created
			# OK, but the observed maps had something wrong.  We
			# can't have one without the other, so reset the
			# baseline DHCP flag back to false.
			$baseline->dhcp(0);
		} else {
			# If we're here, then both scans have created their
			# maps OK.
			$node_key = "Hostname";
		}
	}
}

# Redirect the output if requested.  Obviously this must be called after
# all command line checks and usage messages.
$report->output_file($output_file) if ($output_file);
$report->go();

##############################################################################
# Package: Host
#
# This package is for use with yandiff and is for using an object representing
# a host.
#
# Example:
#
#	$host = Host->new();
#	$host->hostname("example");
#	$host->status("up");
#	$host->ip_addr("192.168.0.1");
#
#	...
#
#	$host->add_service($service);
##############################################################################
package Host;
use strict;

# Create a new instance of the Host object.
sub new {
	my $self = {};
	$self->{BASELINE_OS} = 'N/A';
	$self->{HOSTNAME} = undef;
	$self->{IP_ADDR} = undef;
	$self->{MAC_ADDR} = undef;
	$self->{OBSERVED_OS} = 'N/A';
	$self->{STATUS} = undef;
	$self->{SERVICES} = [];
	bless($self);
	return $self;
}

# Associate a Service object with this host.
sub add_service {
	my $self = shift;
	push @{ $self->{SERVICES} }, shift;
}

# Get or set the baseline OS
sub baseline_os {
	my $self = shift;
	$self->{BASELINE_OS} = shift if (@_);
	return $self->{BASELINE_OS};
}

# A conveniance function for numeric sorting.
sub by_number_ascending
{
	$a <=> $b;
}

# Return a service that matches the associated portid and protocol.
#
# Example:
#
#	$service = $host->get_service(80, "tcp");
sub get_service {
	my $self = shift;
	my $portid = shift;
	my $proto = shift;
	my $null_service = ();

	for my $service (@{ $self->{SERVICES} }) {
		if ($portid eq $service->portid && $proto eq $service->proto) {
			return $service;
		}
	}

	return $null_service;
}

# Returns a sorted list of the portid's for the TCP services associated with
# the host.
sub get_tcp_service_list {
	my $self = shift;
	my @list = ();

	for my $service (@{ $self->{SERVICES} }) {
		push @list, $service->portid if ($service->proto eq "tcp");
	}

	return sort(by_number_ascending @list);
}

# Returns a sorted list of the portid's for the UDP services associated with
# the host.
sub get_udp_service_list {
	my $self = shift;
	my @list = ();

	for my $service (@{ $self->{SERVICES} }) {
		push @list, $service->portid if ($service->proto eq "udp");
	}

	return sort(by_number_ascending @list);
}

# Get or set the hostname
sub hostname {
	my $self = shift;
	$self->{HOSTNAME} = shift if (@_);
	return $self->{HOSTNAME};
}

# Get or set the IP address.
sub ip_addr {
	my $self = shift;
	$self->{IP_ADDR} = shift if (@_);
	return $self->{IP_ADDR};
}

# Get or set the MAC address.
sub mac_addr {
	my $self = shift;
	$self->{MAC_ADDR} = shift if (@_);
	return $self->{MAC_ADDR};
}

# Get or set the observed OS
sub observed_os {
	my $self = shift;
	$self->{OBSERVED_OS} = shift if (@_);
	return $self->{OBSERVED_OS};
}

# Get or set the status of the host.
sub status {
	my $self = shift;
	$self->{STATUS} = shift if (@_);
	return $self->{STATUS};
}

# Return the host details (not any associated ports) as xml.
#
# Parameters:
#  $no_sub_elements - a boolean value indicating if the element is to be
#      closed when first declared, or if it should remain open to have
#      sub-elements declared within it (and for later closing).  An example
#      of an element with no sub-elements is:
#
#          <element attrib="..."/>
#
#      Whereas the following is an example of an element with sub-elements:
#
#          <element attrib="...">
#           <sub_elem_a>...</sub_elem_a>
#           <sub_elem_b>...</sub_elem_b>
#          </element>
sub to_xml {
	my $self = shift;
	my $close_fully = shift;
	my $s = '<host ip_addr="' . $self->{IP_ADDR} . '"';
	$s .= ' mac_addr="' . $self->{MAC_ADDR} . '"'
	    if defined $self->{MAC_ADDR};
	$s .= ' hostname="' . $self->{HOSTNAME} . '"' if ($self->{HOSTNAME});
	$s .= ' status="' . $self->{STATUS} . '"' if defined $self->{STATUS};
	my $close = ($close_fully) ? '/>' : '>';
	$s .= $close;
	return $s;
}
1;

##############################################################################
# Package: Report
#
# This package is for use with yandiff and is for using an object representing
# a report.
#
##############################################################################
package Report;
use XML::Parser;
use strict;

sub new {
	my $self = {};
	$self->{FORMAT} = 'text';
	$self->{OUTPUT_FILE} = undef;
	$self->{SHOW_CHANGED_HOSTS} = 1;
	$self->{SHOW_MISSING_HOSTS} = 1;
	$self->{SHOW_NEW_HOSTS} = 1;
	bless($self);
	return $self;
}

# Is the output format to be text or xml?
sub format {
	my $self = shift;
	$self->{FORMAT} = shift if (@_);
	return $self->{FORMAT};
}

# Actually generate the report
#
# Globals:
#	$VERSION		(character string)
#	$baseline		(Scan object)
#	$baseline_file		(character string)
#	$command_line		(character string)
#	$node_key		(character string)
#	$observed		(Scan object)
#	$observed_file		(character string)
#	$output_ports		(character string)
#	$run_date		(character string)
#	$stylesheet		(character string)
sub go {
	my $self = shift;
	my $xml;

	$xml = '<?xml version="1.0" encoding="utf-8"?>'
    	    . "\n";
	$xml .= '<?xml-stylesheet type="text/xsl" href="'
    	    . $stylesheet
    	    . '"?>'
    	    . "\n" if ($stylesheet);
	$xml .= "<yandiff rundate=\"$run_date\""
    	    . ' version="'
	    . $VERSION
	    . '"'
    	    . ' command_line="'
	    . $command_line
	    . '">'
	    . "\n";
	my $session = $baseline->get_session();
	$xml .= " <parameters node_key=\"$node_key\"";
	$xml .= ' nmap_version_warning="true"' if ($session->nmap_version()
	    ne $observed->get_session()->nmap_version());
	$xml .= ">\n";
	$xml .= "  <baseline>\n"
    	    . "   <file>$baseline_file</file>\n"
    	    . '   <scan_args>'
	    . $session->scan_args
	    . "</scan_args>\n"
    	    . '   <nmap_version>'
	    . $session->nmap_version
	    . "</nmap_version>\n"
    	    . '   <scan_start>'
	    . $session->start_str
	    . "</scan_start>\n"
    	    . "  </baseline>\n";
	$session = $observed->get_session();
	$xml .= "  <observed>\n"
    	    . "   <file>$observed_file</file>\n"
    	    . '   <scan_args>'
	    . $session->scan_args
	    . "</scan_args>\n"
    	    . '   <nmap_version>'
	    . $session->nmap_version
	    . "</nmap_version>\n"
    	    . '   <scan_start>'
	    . $session->start_str
	    . "</scan_start>\n"
    	    . "  </observed>\n"
	    . " </parameters>\n";

	$xml .= report_new_hosts($baseline, $observed)
	    if $self->show_new_hosts;
	$xml .= report_missing_hosts($baseline, $observed)
	    if $self->show_missing_hosts;
	$xml .= report_changed_hosts($baseline, $observed,
	    $output_ports) if $self->show_changed_hosts;
	$xml .= "</yandiff>\n";

	if ($self->{FORMAT} eq 'xml') {
		print $xml;
	} else {
		my $parser = new XML::Parser(Handlers => {
		    Start => \&handle_start
		});
		$parser->parse($xml);
	}
}

# An XML::Parser handling sub-routine.  See the doucmentation for XML::Parser
# for more information.
sub handle_start {
	my ($p, $element, %attrs) = @_;
	my @context = $p->context;

	if ($element eq 'yandiff') {
		print "$element run $attrs{rundate}\n",
		    "command line: $attrs{command_line}\n",
		    "baseline: $baseline_file\n",
		    "observed: $observed_file\n";
	} elsif ($element eq 'parameters') {
		if (exists($attrs{nmap_version_warning})
		    && $attrs{nmap_version_warning}) {
			print "Warning: Nmap version mismatch between the "
			    . "baseline and observed scans.\n";
		}

		print "node key: $attrs{node_key}";
	} elsif ($element eq 'new') {
		print "\nNew hosts:\n";
	} elsif ($element eq 'host') {
		my $ip_addr = $attrs{ip_addr};
		my $mac_addr = $attrs{mac_addr};
		my $hostname = $attrs{hostname};
		my $status = $attrs{status};
		print "\t$ip_addr";
		print ", $mac_addr" if ($mac_addr);
		print " ($hostname)" if ($hostname);
		print " - $status" if ($status);
		print "\n";
	} elsif ($element eq 'service' && $context[1] eq 'new') {
		print "\t\t$attrs{portid}/$attrs{proto}/$attrs{status}\n";
	} elsif ($element eq 'service' && $context[1] eq 'changed') {
		print "\t\t\t$attrs{portid}/$attrs{proto}/$attrs{status}";
		my $name = $attrs{name};
		print " ($name)" if ($name);
		my $previous = $attrs{previous};
		print " (previously $previous)" if ($previous);
		print "\n";
	} elsif ($element eq 'missing') {
		print "\nMissing hosts:\n";
	} elsif ($element eq 'changed') {
		print "\nChanged hosts:\n";
	} elsif ($element eq 'new_services') {
		print "\t\tNew Services:\n";
	} elsif ($element eq 'missing_services') {
		print "\t\tMissing Services:\n";
	} elsif ($element eq 'changed_services') {
		print "\t\tChanged Services:\n";
	} elsif ($element eq 'os') {
		print "\t\tOS (accuracy%):\n";
		print "\t\t\tBaseline: $attrs{baseline}\n";
		print "\t\t\tObserved: $attrs{observed}\n";
	}
}

# Get or set the output file.
sub output_file {
	my $self = shift;

	if (@_) {
		$self->{OUTPUT_FILE} = shift;
		open OUTPUT, '>', $self->{OUTPUT_FILE}
		    or die "[FATAL]Unable to open $self->{OUTPUT_FILE}: $!";
		STDOUT->fdopen(\*OUTPUT, 'w') or die $!;
	}

	return $self->{OUTPUT_FILE};
}

# Find hosts that are in both the observed and baseline scans.  Compare the
# status of the host and the status of each of the individual ports.
sub report_changed_hosts {
	my ($baseline, $observed, $ports) = @_;
	my $portid;
	my @ip_list;
	my $output;

	for my $baseline_ip ($baseline->common($observed)) {
		my $baseline_host = $baseline->get_host($baseline_ip);
		my $observed_host;

		if ($observed->dhcp()) {
			$observed_host = $baseline->get_host_dhcp($baseline_ip,
			    $observed);
		} else {
			$observed_host = $observed->get_host($baseline_ip);
		}

		my $host = Host->new();
		$host->hostname($observed_host->hostname());
		$host->ip_addr($observed_host->addr());
		$host->mac_addr($observed_host->mac_addr());
		$host->status($observed_host->status());

		# Try and extract the OS information for the baseline host.
		my $OS = $baseline_host->os_sig();
		my $name;
		my $accuracy;
		my $os;

		if ($OS->class_count()) {
			$name = $OS->name();

			# If the name is set then find out how accurate the
			# guess was, if not, fall back to the OS family name
			# (i.e. Linux).
			if ($name) {
				$accuracy = $OS->name_accuracy();
			} else {
				$name = $OS->osfamily;
			}

			$name = $OS->vendor() if (!$name);
			$accuracy = $OS->class_accuracy() if (!$accuracy);

			# If we still don't have an OS name, fall back to the
			# final resort of the vendor name, if we can't even get
			# that, set to N/A.
			$name = $OS->vendor() if (!$name);
			$accuracy = $OS->class_accuracy() if (!$accuracy);
			$name = 'N/A' if (!$name);
			$os = $name;
			$os .= " ($accuracy%)" if ($accuracy);
			$host->baseline_os($os);
		}

		# Now do the same OS extraction for the observed host.
		$OS = $observed_host->os_sig();

		if ($OS->class_count()) {
			$name = $OS->name();

			if ($name) {
				$accuracy = $OS->name_accuracy();
			} else {
				$name = $OS->osfamily;
			}

			$name = $OS->vendor() if (!$name);
			$accuracy = $OS->class_accuracy() if (!$accuracy);
			$name = $OS->vendor() if (!$name);
			$accuracy = $OS->class_accuracy() if (!$accuracy);
			$name = 'N/A' if (!$name);
			$os = $name;
			$os .= " ($accuracy%)" if ($accuracy);
			$host->observed_os($os);
		}

		my $host_has_changed = 0;
		my @changed_services = ();
		my @missing_services = ();
		my @new_services = ();
		$host_has_changed = 1
		    if ($observed_host->status() ne $baseline_host->status());
		$host_has_changed = 1
		    if ($host->baseline_os() ne $host->observed_os());

		# Check for new or changed TCP services
		my @observed_services = ();
		my @baseline_services = ();
		@baseline_services = $baseline_host->tcp_ports();

		if ($ports) {
			if ( $ports =~ m/o/i ) {
				push(@observed_services,
				    $observed_host->tcp_ports('open'));
			}

			if ( $ports =~ m/f/i ) {
				for $portid
				    ($observed_host->tcp_ports('filtered')) {
					push(@observed_services, $portid) if
					    ($observed_host->tcp_port_state(
					    $portid) !~ m/unfiltered/i );
				}
			}

			if ( $ports =~ m/c/i ) {
				push(@observed_services,
				    $observed_host->tcp_ports('closed'));
			}

			if ( $ports =~ m/x/i ) {
				push(@observed_services,
				    $observed_host->tcp_ports('unfiltered'));
			}
		} else {
			@observed_services = $observed_host->tcp_ports();
		}

		for $portid (@observed_services) {
			# new services
			my $s = $observed_host->tcp_service($portid);
			my $service = Service->new();
			$service->name($s->name());
			$service->portid($portid);
			$service->proto("tcp");
			$service->status(
			    $observed_host->tcp_port_state($portid));

			if (!grep {$_ eq $portid} @baseline_services) {
				$host_has_changed = 1;
				push @new_services, $service;
				next;
			}

			# must be a changed service
			my $service2 = Service->new();
			$service2->name($s->name());
			$service2->portid($portid);
			$service2->proto("tcp");
			$service2->status(
			    $baseline_host->tcp_port_state($portid));

			if ($service->compare($service2) != 0) {
				$host_has_changed = 1;
				$service->previous($service2->status);
				push @changed_services, $service;
			}
		}

		# Check for missing TCP ports
		if (! $ports ) {
			for $portid (@baseline_services) {
				my $s = $baseline_host->tcp_service($portid);
				my $service = Service->new();
				$service->name($s->name());
				$service->portid($portid);
				$service->proto("tcp");
				$service->status(
				    $baseline_host->tcp_port_state($portid));

				if (!grep {$_ eq $portid}
				    @observed_services) {
					$host_has_changed = 1;
					push @missing_services, $service;
					next;
				}
			}
		}

		# Check for new or changed UDP services
		@observed_services = ();
		@baseline_services = ();
		@baseline_services = $baseline_host->udp_ports();

		if ($ports) {
			if ( $ports =~ m/o/i ) {
				push(@observed_services,
				    $observed_host->udp_ports('open'));
			}

			if ( $ports =~ m/f/i ) {
				for $portid (
				    $observed_host->udp_ports('filtered')) {
					push(@observed_services, $portid) if
					    ($observed_host->udp_port_state(
					    $portid) !~ m/unfiltered/i );
				}
			}

			if ( $ports =~ m/c/i ) {
				push(@observed_services,
				    $observed_host->udp_ports('closed'));
			}

			if ( $ports =~ m/x/i ) {
				push(@observed_services,
				    $observed_host->udp_ports('unfiltered'));
			}
		} else {
			@observed_services = $observed_host->udp_ports();
		}

		for $portid (@observed_services) {
			my $s = $observed_host->udp_service($portid);
			my $service = Service->new();
			$service->name($s->name());
			$service->portid($portid);
			$service->proto("udp");
			$service->status(
			    $observed_host->udp_port_state($portid));

			if (!grep {$_ eq $portid} @baseline_services) {
				$host_has_changed = 1;
				push @new_services, $service;
				next;
			}

			my $service2 = Service->new();
			$service2->name($s->name());
			$service2->portid($portid);
			$service2->proto("udp");
			$service2->status(
			    $baseline_host->udp_port_state($portid));

			if ($service->compare($service2) != 0) {
				$host_has_changed = 1;
				$service->previous($service2->status);
				push @changed_services, $service;
			}
		}

		# Check for missing UDP ports
		if (! $ports ) {
			for $portid (@baseline_services) {
				my $s = $baseline_host->udp_service($portid);
				my $service = Service->new();
				$service->name($s->name());
				$service->portid($portid);
				$service->proto("udp");
				$service->status(
				    $baseline_host->udp_port_state($portid));

				if (!grep {$_ eq $portid}
				    @observed_services) {
					$host_has_changed = 1;
					push @missing_services, $service;
					next;
				}
			}
		}

		next if (!$host_has_changed);

		# To have reached here then a change has been detected in the
		# host between the scans.

		$output .= "  " . $host->to_xml(0) . "\n";

		if (scalar(@new_services) == 0) {
			$output .= "   <new_services/>\n";
		} else {
			$output .= "   <new_services>\n";

			for my $service (@new_services) {
				$output .= "    "
				    . $service->to_xml()
				    . "\n";
			}

			$output .= "   </new_services>\n";
		}

		if (scalar(@missing_services) == 0) {
			$output .= "   <missing_services/>\n";
		} else {
			$output .= "   <missing_services>\n";

			for my $service (@missing_services) {
				$output .= "    "
				    . $service->to_xml()
				    . "\n";
			}

			$output .= "   </missing_services>\n";
		}

		if (scalar(@changed_services) == 0) {
			$output .= "   <changed_services/>\n";
		} else {
			$output .= "   <changed_services>\n";

			for my $service (@changed_services) {
				$output .= "    "
				    . $service->to_xml()
				    . "\n";
			}

			$output .= "   </changed_services>\n";
		}

		if ($host->baseline_os() ne $host->observed_os()) {
			$output .= '   <os baseline="'
			    . $host->baseline_os()
			    . '" observed="'
			    . $host->observed_os()
			    . '"/>'
			    . "\n";
		}

		$output .= "  </host>\n";
	}

	if ($output) {
		$output = " <changed>\n" . $output . " </changed>\n";
	} else {
		$output = " <changed/>\n";
	}

	return $output;
}

# Find hosts that are defined in the baseline but are missing from the
# observed scan.
sub report_missing_hosts {
	my ($baseline, $observed) = @_;
	my @hosts = ();

	for my $baseline_ip ($baseline->unique($observed)) {
			my $baseline_host = $baseline->get_host($baseline_ip);
			my $host = Host->new();
			$host->hostname($baseline_host->hostname());
			$host->ip_addr($baseline_ip);
			$host->mac_addr($baseline_host->mac_addr());
			push @hosts, $host;
	}

	my $output = '';
	$output = " <missing>\n" if (scalar(@hosts) != 0);
	$output = " <missing/>\n" if (scalar(@hosts) == 0);

	for my $host (@hosts) {
		$output .= "  " . $host->to_xml(1) . "\n";
	}

	$output .= " </missing>\n" if (scalar(@hosts) != 0);
	return $output;
}

# Find hosts that are defined in the baseline scan, but are missing from the
# observed scan.
sub report_new_hosts {
	my ($baseline, $observed) = @_;
	my @hosts = ();

	for my $ip ($observed->unique($baseline)) {
		my $observed_host = $observed->get_host($ip);
		my $host = Host->new();
		$host->hostname($observed_host->hostname());
		$host->ip_addr($ip);
		$host->mac_addr($observed_host->mac_addr());
		$host->status($observed_host->status());

		for my $portid ($observed_host->tcp_ports()) {
			my $s = $observed_host->tcp_service($portid);
			my $service = Service->new();
			$service->name($s->name());
			$service->portid($portid);
			$service->proto("tcp");
			$service->status(
			    $observed_host->tcp_port_state($portid));
			$host->add_service($service);
		}

		for my $portid ($observed_host->udp_ports()) {
			my $s = $observed_host->udp_service($portid);
			my $service = Service->new();
			$service->name($s->name());
			$service->portid($portid);
			$service->proto("udp");
			$service->status(
			    $observed_host->udp_port_state($portid));
			$host->add_service($service);
		}

		push @hosts, $host;
	}

	my $output;
	$output = " <new>\n" if (scalar(@hosts) != 0);
	$output = " <new/>\n" if (scalar(@hosts) == 0);

	for my $host (@hosts) {
		$output .= "  " . $host->to_xml(0) . "\n";

		for my $portid ($host->get_tcp_service_list()) {
			my $svc = $host->get_service($portid, "tcp");
			$output .= "   " . $svc->to_xml() . "\n";
		}

		for my $portid ($host->get_udp_service_list()) {
			my $svc = $host->get_service($portid, "udp");
			$output .= "   " . $svc->to_xml() . "\n";
		}

		$output .= "  </host>\n";
	}

	$output .= " </new>\n" if (scalar(@hosts) != 0);
	return $output;
}

sub show_changed_hosts {
	my $self = shift;
	$self->{SHOW_CHANGED_HOSTS} = shift if (@_);
	return $self->{SHOW_CHANGED_HOSTS};
}

sub show_missing_hosts {
	my $self = shift;
	$self->{SHOW_MISSING_HOSTS} = shift if (@_);
	return $self->{SHOW_MISSING_HOSTS};
}

sub show_new_hosts {
	my $self = shift;
	$self->{SHOW_NEW_HOSTS} = shift if (@_);
	return $self->{SHOW_NEW_HOSTS};
}

1;

##############################################################################
# Package: Scan
#
# This package is for use with yandiff and is for using an object representing
# a scan.  It is largely inherited from the Nmap::Parser object.
#
##############################################################################
package Scan;
use strict;
use base ("Nmap::Parser");

sub new {
	my $proto = shift;
	my $class = ref($proto) || $proto;
	my $self  = $class->SUPER::new();
	$self->{DHCP} = 0;
	$self->{H2IP} = undef;
	$self->{IP2H} = undef;
	bless ($self, $class);
	return $self;
}

# Provide a list of IP addresses that are present in both the calling scan,
# and in the provided scan.
#
# Arguments:
#
#  $scan - the provided scan for comparison
sub common {
	my $self = shift;
	my $scan = shift;
	my @ip_list = ();

	for my $ip ($self->get_ips()) {
		my $host;

		if ($self->dhcp()) {
			$host = $self->get_host_dhcp($ip, $scan);
		} else {
			$host = $scan->get_host($ip);
		}

		push(@ip_list, $ip) if (defined($host));
	}

	return @ip_list;
}

# Create the IP and hostname maps
sub create_maps {
	my $self = shift;
	my $hostname;

	for my $ip ($self->get_ips()) {
		$hostname = $self->get_host($ip)->hostname;
		$hostname = $ip if (!$hostname);

		# If there is a duplicate hostname, the lookups are not
		# going to work, set the DHCP flag for this scan back
		# to false and give a message to STDERR.
		if (defined($self->{H2IP}->{$hostname})) {
			print STDERR
			    "WARNING: Duplicate hostname $hostname\n";
			$self->{DHCP} = 0;
			last;
		} else {
			$self->{H2IP}->{$hostname} = $ip;
			$self->{IP2H}->{$ip} = $hostname;
		}
	}

	return $self->{DHCP};
}

# Is the object to use DHCP processing
sub dhcp {
	my $self = shift;
	$self->{DHCP} = shift if (@_);
	return $self->{DHCP};
}

# Get a mapped host via DHCP
#
# Parameters:
#
#  $ip - The IP address as contained within the local scan.
#  $scan - The scan that is being used for a comparison.
sub get_host_dhcp {
	my $self = shift;
	my $ip = shift;
	my $scan = shift;
	my $host = undef
	my $hostname;

	# If the IP is present in this scan, find out it's corresponding
	# hostname.
	if (defined($self->{IP2H}->{$ip})) {
		$hostname = $self->{IP2H}->{$ip};

		# If this hostname exists in the comparison scan, extract
		# the relevant IP address, followed by the host.
		if (defined($scan->{H2IP}->{$hostname})) {
			$ip = $scan->{H2IP}->{$hostname};
			$host = $scan->get_host($ip);
		}
	}

	# Return the host.  This value will still be undifined if the IP
	# parameter can't be found in the local scan, or the IP/host can't
	# be found in the comparison scan.
	return $host;
}

# Provide a list of IP addresses that are present in the calling scan,
# but are not present in the provided scan.
#
# Parameters:
#
#  $scan - the provided scan for comparison
sub unique {
	my $self = shift;
	my $scan = shift;
	my @ip_list = ();

	for my $ip ($self->get_ips()) {
		my $host;

		if ($self->dhcp()) {
			$host = $self->get_host_dhcp($ip, $scan);
		} else {
			$host = $scan->get_host($ip);
		}

		push(@ip_list, $ip) if (!defined($host));
	}

	return @ip_list;
}
1;

##############################################################################
# Package: Service
#
# This package is for use with yandiff and is for using an object representing
# a TCP or UDP service.
#
# Example:
#
#	$service = Service->new();
#	$service->name("ssh");
#	$service->portid(22);
#	$service->proto("tcp");
#	$service->status("open");
#	$service->previous("closed");
#	$service->compare($another_service);
##############################################################################
package Service;
use strict;

# Create a new instance of the Service object.
sub new {
	my $self = {};
	$self->{NAME} = '';
	$self->{PORTID} = undef;
	$self->{PROTO} = undef;
	$self->{STATUS} = 'unknown';
	bless($self);
	return $self;
}

# Compare the current service with another Service instance.  Returns zero if
# the services are identical, non-zero otherwise.
sub compare {
	my $self = shift;
	my $service = shift;


	if (defined ($service->name)) {
		return 1 if (!defined ($self->{NAME}));
		return 1 if ($service->name ne $self->{NAME});
	} elsif (defined ($self->{NAME})) {
		return 1;
	}

	return 1 if ($service->status ne $self->{STATUS});
	return 0;
}

# Get or set the name of the service.
sub name {
	my $self = shift;
	$self->{NAME} = shift if (@_);
	return $self->{NAME};
}

# Get or set the portid of the service.
sub portid {
	my $self = shift;
	$self->{PORTID} = shift if (@_);
	return $self->{PORTID};
}

# Get or set the previous status of the service.
sub previous {
	my $self = shift;
	$self->{PREVIOUS} = shift if (@_);
	return $self->{PREVIOUS};
}

# Get or set the protocol of the service ("tcp" or "udp").
sub proto {
	my $self = shift;
	$self->{PROTO} = shift if (@_);
	return $self->{PROTO};
}

# Get or set the status of the service.
sub status {
	my $self = shift;
	$self->{STATUS} = shift if (@_);
	return $self->{STATUS};
}

# Return an XML representation of the service.
sub to_xml {
	my $self = shift;
	my $s = '<service portid="' . $self->{PORTID} . '"'
	    . ' proto="' . $self->{PROTO} . '"'
	    . ' status="' . $self->{STATUS} . '"';
	$s .= ' name="' . $self->{NAME} . '"' if ($self->{NAME});
	$s .= ' previous="' . $self->{PREVIOUS} . '"' if ($self->{PREVIOUS});
	$s .= '/>';
	return $s;
}

1;

##############################################################################
# Package: Stylesheet
#
# This package is for use with yandiff and is for generating an XML
# stylesheet.
#
# Example:
#
#    $xslt = Service->new();
#    $xslt->generate(filename);
##############################################################################
package Stylesheet;
use strict;

# Create a new instance of the Stylesheet object.
sub new {
	my $self = {};
	$self->{FILENAME} = '';
	$self->{XSL_VERSION} = '1.3';
	bless($self);
	return $self;
}

# Write the stylesheet to the specified filename.
sub generate {
	my $self = shift;
	my $filename = shift;
	open OUTPUT, '>', $filename or die $!;
	print OUTPUT <<_EOF;
<?xml version="1.0" encoding="utf-8"?>
<!--
 Generated by the yandiff program.

 See http://code.google.com/p/yandiff for details.
-->

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<!-- ............... Set the VERSION here ....................... -->
<xsl:variable name="yandiff_xsl_version">$self->{XSL_VERSION}</xsl:variable>
<!-- ............................................................ -->

<xsl:template match="yandiff">
<html>
<head>
<title>yandiff report</title>
<style type="text/css">
/* stylesheet print */
\@media print
{
 #menu { display:none; }

 h1 { font-size: 13pt; font-weight:bold; margin:4pt 0pt 0pt 0pt;
  padding:0; }

 h2 { font-size: 12pt; font-weight:bold; margin:3pt 0pt 0pt 0pt; padding:0; }

 h3 { font-size: 9pt; font-weight:bold; margin:1pt 0pt 0pt 20pt; padding:0; }

 p,ul { font-size: 9pt; margin:1pt 0pt 8pt 40pt; padding:0; text-align:left; }

 li { font-size: 9pt; margin:0; padding:0; text-align:left; }

 table { margin:1pt 0pt 8pt 40pt; border:0px; width:90% }

 td { border:0px; border-top:1px solid black; font-size: 9pt; }

 .head td { border:0px; font-weight:bold; font-size: 9pt; }
}

/* stylesheet screen */
\@media screen {
 body { margin: 0px; background-color: #FFFFFF; color: #000000;
  text-align: center; }

 #container { text-align:left; margin: 10px auto; width: 90%; }

 h1 { font-family: Verdana, Helvetica, sans-serif; font-weight:bold;
  font-size: 14pt; color: #000000; background-color:#87CEFA;
  margin:10px 0px 0px 0px; padding:5px 4px 5px 4px; width: 100%;
  border:1px solid black; text-align: left; }

 h2 { font-family: Verdana, Helvetica, sans-serif; font-weight:bold;
  font-size: 11pt; color: #000000; margin:30px 0px 0px 0px; padding:4px;
  width: 100%; border:1px solid black; background-color:#F0F8FF;
  text-align: left; }

 h2.green { color: #000000; background-color:#CCFFCC; border-color:#006400; }

 h2.red { color: #000000; background-color:#FFCCCC; border-color:#8B0000; }

 h2.orange { color: #000000; background-color:#FFDDBB; border-color:#8B0000; }
   
 h3 { font-family: Verdana, Helvetica, sans-serif; font-weight:bold;
  font-size: 10pt; color:#000000; background-color: #FFFFFF; width: 75%;
  text-align: left; }

 p { font-family: Verdana, Helvetica, sans-serif; font-size: 8pt;
  color:#000000; background-color: #FFFFFF; width: 75%; text-align: left; }

 p i { font-family: "Courier New", Courier, mono; font-size: 8pt;
  color:#000000; background-color: #CCCCCC; }

 ul { font-family: Verdana, Helvetica, sans-serif; font-size: 8pt;
  color:#000000; background-color: #FFFFFF; width: 75%; text-align: left; }

 a { font-family: Verdana, Helvetica, sans-serif; text-decoration: none;
  font-size: 8pt; color:#000000; font-weight:bold; background-color: #FFFFFF;
  color: #000000; }

 li a { font-family: Verdana, Helvetica, sans-serif; text-decoration: none;
  font-size: 10pt; color:#000000; font-weight:bold; background-color: #FFFFFF;
  color: #000000; }

 a:hover { text-decoration: underline; }

 a.red { color:#8B0000; }

 a.orange { color: orange; }

 a.green { color:#006400; }

 table { width: 80%; border:0px; color: #000000; background-color: #000000;
  margin:10px; }

 tr { vertical-align:top; font-family: Verdana, Helvetica, sans-serif;
  font-size: 8pt; color:#000000; background-color: #D1D1D1; }

 tr.head { background-color: #E1E1E1; color: #000000; font-weight:bold; }

 tr.open { background-color: #CCFFCC; color: #000000; }

 tr.filtered { background-color: #FFDDBB; color: #000000; }

 tr.closed { background-color: #FFAFAF; color: #000000; }

 td { padding:2px; }

 .status { display:none; }

 #menu li { display: inline; margin: 0; padding: 0; list-style-type: none; }
}
</style>
</head>
<body>
 <h1>Yandiff</h1>

 <h2>Run Parameters</h2>

 <table border="1">
  <caption><h3>Yandiff Information</h3></caption>
  <tr>
   <td>Run Date</td>
   <td><xsl:value-of select="\@rundate"/></td>
  </tr>
  <tr>
   <td>Version</td>
   <td><xsl:value-of select="\@version"/></td>
  </tr>
  <tr>
   <td>Command Arguments</td>
   <td><i><xsl:value-of select="\@command_line"/></i></td>
  </tr>
  <xsl:if test="parameters/\@node_key">
   <tr>
    <td>Node Key</td>
    <td><xsl:value-of select="parameters/\@node_key"/></td>
   </tr>
  </xsl:if>
 </table>

 <table border="1">
  <caption><h3>Baseline Information</h3></caption>
  <tr>
   <td>Run Date</td>
   <td><xsl:value-of select="parameters/baseline/scan_start"/></td>
  </tr>
  <tr>
   <td>Nmap Version</td>
   <td><xsl:value-of select="parameters/baseline/nmap_version"/></td>
  </tr>
  <tr>
   <td>Scan Arguments</td>
   <td><i><xsl:value-of select="parameters/baseline/scan_args"/></i></td>
  </tr>
 </table>

 <table border="1">
  <caption><h3>Observed Information</h3></caption>
  <tr>
   <td>Run Date</td>
   <td><xsl:value-of select="parameters/observed/scan_start"/></td>
  </tr>
  <tr>
   <td>Nmap Version</td>
   <td><xsl:value-of select="parameters/observed/nmap_version"/></td>
  </tr>
  <tr>
   <td>Scan Arguments</td>
   <td><i><xsl:value-of select="parameters/observed/scan_args"/></i></td>
  </tr>
 </table>

 <xsl:if test="parameters/\@nmap_version_warning">
  <p style="color: orange">Warning: Nmap version mismatch between the baseline
  and the observed scan.</p>
 </xsl:if>

 <h2>Host Summary</h2>

 <xsl:if test="new">
  <p><b>New</b>:
  <xsl:for-each select="new">
   <xsl:apply-templates select="host"/>
  </xsl:for-each>
  </p>
 </xsl:if>

 <xsl:if test="missing">
  <p><b>Missing</b>:
  <xsl:for-each select="missing">
   <xsl:apply-templates select="host"/>
  </xsl:for-each>
  </p>
 </xsl:if>

 <xsl:if test="changed">
  <p><b>Changed</b>:
  <xsl:for-each select="changed">
   <xsl:apply-templates select="host"/>
  </xsl:for-each>
  </p>
 </xsl:if>

 <xsl:if test="new">
  <h2>New</h2>

  <xsl:for-each select="new/host">
   <xsl:element name="a">
    <xsl:attribute name="name">
     <xsl:value-of select="translate(\@ip_addr, '.', '_') " />
    </xsl:attribute>
   </xsl:element>

   <xsl:choose>
    <xsl:when test="\@status = 'up'">
     <h2 class="green">
      <xsl:value-of select="\@ip_addr"/>
      <xsl:if test="\@mac_addr">
       , <xsl:value-of select="\@mac_addr"/>
      </xsl:if>
      <xsl:if test="\@hostname">
       (<xsl:value-of select="\@hostname"/>)
      </xsl:if>
      - <xsl:value-of select="\@status"/>
     </h2>
    </xsl:when>
    <xsl:otherwise>
     <h2 class="red">
      <xsl:value-of select="\@ip_addr"/>
      <xsl:if test="\@hostname">
       / <xsl:value-of select="\@hostname"/>
      </xsl:if>
      (<xsl:value-of select="\@status"/>)
     </h2>
    </xsl:otherwise>
   </xsl:choose>

   <table border="1">
    <tr>
     <th>Port</th>
     <th>Protocol</th>
     <th>Status</th>
     <th>Name</th>
    </tr>

   <xsl:apply-templates select="service"/>

   </table>
  </xsl:for-each>

 </xsl:if>

 <xsl:if test="missing">
  <h2>Missing</h2>
  <ul>

  <xsl:for-each select="missing/host">
   <xsl:element name="a">
    <xsl:attribute name="name">
     <xsl:value-of select="translate(\@ip_addr, '.', '_') " />
    </xsl:attribute>
   </xsl:element>
   <li><b><xsl:value-of select="\@ip_addr"/>
      <xsl:if test="\@mac_addr">
       , <xsl:value-of select="\@mac_addr"/>
      </xsl:if>
      <xsl:if test="\@hostname">
       (<xsl:value-of select="\@hostname"/>)
      </xsl:if>
   </b></li>
  </xsl:for-each>
  </ul>
 </xsl:if>

 <xsl:if test="changed">
  <h2>Changed</h2>

  <xsl:for-each select="changed/host">
   <xsl:element name="a">
    <xsl:attribute name="name">
     <xsl:value-of select="translate(\@ip_addr, '.', '_') " />
    </xsl:attribute>
   </xsl:element>
   <xsl:choose>
    <xsl:when test="\@status = 'up'">
     <h2 class="green">
      <xsl:value-of select="\@ip_addr"/>
      <xsl:if test="\@mac_addr">
       , <xsl:value-of select="\@mac_addr"/>
      </xsl:if>
      <xsl:if test="\@hostname">
       (<xsl:value-of select="\@hostname"/>)
      </xsl:if>
      - <xsl:value-of select="\@status"/>
     </h2>
    </xsl:when>
    <xsl:otherwise>
     <h2 class="red">
      <xsl:value-of select="\@ip_addr"/>
      <xsl:if test="\@mac_addr">
       , <xsl:value-of select="\@mac_addr"/>
      </xsl:if>
      <xsl:if test="\@hostname">
       (<xsl:value-of select="\@hostname"/>)
      </xsl:if>
      - <xsl:value-of select="\@status"/>
     </h2>
    </xsl:otherwise>
   </xsl:choose>

   <table border="1">
    <tr>
     <th>Change Type</th>
     <th>Port</th>
     <th>Protocol</th>
     <th>Status</th>
     <th>Name</th>
    </tr>

    <xsl:for-each select="new_services">
     <xsl:apply-templates select="service"/>
    </xsl:for-each>

    <xsl:for-each select="missing_services">
     <xsl:apply-templates select="service"/>
    </xsl:for-each>

    <xsl:for-each select="changed_services">
     <xsl:apply-templates select="service"/>
    </xsl:for-each>
   </table>
   <xsl:if test="os">
    <xsl:apply-templates select="os"/>
   </xsl:if>
  </xsl:for-each>
 </xsl:if>

 <sub>
  Yandiff stylesheet version: <xsl:value-of select="\$yandiff_xsl_version" />
  &lt;<a href = 
  "http://code.google.com/p/yandiff">http://code.google.com/p/yandiff</a>&gt;
 </sub>
</body>
</html>
</xsl:template>

<xsl:template match="host">
   &lt;<xsl:element name="a">
   <xsl:if test="name(parent::node()) = 'new'">
    <xsl:attribute name="class">green</xsl:attribute>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'missing'">
    <xsl:attribute name="class">red</xsl:attribute>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'changed'">
    <xsl:attribute name="class">orange</xsl:attribute>
   </xsl:if>
   <xsl:attribute name="href">
    #<xsl:value-of select="translate(\@ip_addr, '.', '_') "/>
   </xsl:attribute>
    <xsl:value-of select="\@ip_addr"/><xsl:if test="\@hostname"> /
    <xsl:value-of select="\@hostname"/></xsl:if>
   </xsl:element>&gt;
</xsl:template>

<xsl:template match="os">
 <p><b>OS:</b>
  <ul>
   <li>Baseline: <xsl:value-of select="\@baseline"/></li>
   <li>Observed: <xsl:value-of select="\@observed"/></li>
  </ul>
 </p>
</xsl:template>

<xsl:template match="service">
<xsl:choose>
 <xsl:when test="\@status = 'open'">
  <tr class="open">
   <xsl:if test="name(parent::node()) = 'new_services'">
    <td>New</td>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'missing_services'">
    <td>Missing</td>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'changed_services'">
    <td>Changed</td>
   </xsl:if>
   <td><xsl:value-of select="\@portid"/></td>
   <td><xsl:value-of select="\@proto"/></td>
   <xsl:choose>
    <xsl:when test="\@previous">
     <td><xsl:value-of select="\@status"/> (previously <xsl:value-of
      select="\@previous"/>)</td>
    </xsl:when>
    <xsl:otherwise>
     <td><xsl:value-of select="\@status"/></td>
    </xsl:otherwise>
   </xsl:choose>
   <td><xsl:value-of select="\@name"/></td>
  </tr>
 </xsl:when>
 <xsl:when test="\@status = 'closed'">
  <tr class="closed">
   <xsl:if test="name(parent::node()) = 'new_services'">
    <td>New</td>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'missing_services'">
    <td>Missing</td>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'changed_services'">
    <td>Changed</td>
   </xsl:if>
   <td><xsl:value-of select="\@portid"/></td>
   <td><xsl:value-of select="\@proto"/></td>
   <xsl:choose>
    <xsl:when test="\@previous">
     <td><xsl:value-of select="\@status"/> (previously <xsl:value-of
      select="\@previous"/>)</td>
    </xsl:when>
    <xsl:otherwise>
     <td><xsl:value-of select="\@status"/></td>
    </xsl:otherwise>
   </xsl:choose>
   <td><xsl:value-of select="\@name"/></td>
  </tr>
 </xsl:when>
 <xsl:when test="\@status = 'filtered'">
  <tr class="filtered">
   <xsl:if test="name(parent::node()) = 'new_services'">
    <td>New</td>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'missing_services'">
    <td>Missing</td>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'changed_services'">
    <td>Changed</td>
   </xsl:if>
   <td><xsl:value-of select="\@portid"/></td>
   <td><xsl:value-of select="\@proto"/></td>
   <xsl:choose>
    <xsl:when test="\@previous">
     <td><xsl:value-of select="\@status"/> (previously <xsl:value-of
      select="\@previous"/>)</td>
    </xsl:when>
    <xsl:otherwise>
     <td><xsl:value-of select="\@status"/></td>
    </xsl:otherwise>
   </xsl:choose>
   <td><xsl:value-of select="\@name"/></td>
  </tr>
 </xsl:when>
 <xsl:otherwise>
  <tr>
   <xsl:if test="name(parent::node()) = 'new_services'">
    <td>New</td>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'missing_services'">
    <td>Missing</td>
   </xsl:if>
   <xsl:if test="name(parent::node()) = 'changed_services'">
    <td>Changed</td>
   </xsl:if>
   <td><xsl:value-of select="\@portid"/></td>
   <td><xsl:value-of select="\@proto"/></td>
   <xsl:choose>
    <xsl:when test="\@previous">
     <td><xsl:value-of select="\@status"/> (previously <xsl:value-of
      select="\@previous"/>)</td>
    </xsl:when>
    <xsl:otherwise>
     <td><xsl:value-of select="\@status"/></td>
    </xsl:otherwise>
   </xsl:choose>
   <td><xsl:value-of select="\@name"/></td>
  </tr>
 </xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
_EOF
	close(OUTPUT);
}
1;
__END__

=head1 NAME

yandiff - Yet Another Nmap Differential Script

=head1 SYNOPSIS

yandiff
[-f, --format <text | xml>]
[-hk, --hostname-key]
[-of, --output-file file]
[-oh, --output-hosts <nmc>]
[-op, --output-ports <ofcx>]
[-s, --stylesheet <file | URL>]
--baseline file
--observed file

yandiff file file

yandiff --gen-stylesheet file

yandiff --help | -?

yandiff --man

yandiff --version

=head1 OPTIONS

=over 8

=item B<-b> file

=item B<--baseline> file

Specifies the nmap results to use as the baseline for the comparison.
May be ommitted if the baseline and observed files are the only two arguments
given.

=item B<-f> <text | xml>

=item B<--format> <text | xml>

Specifies the output format (text is the default).

=item B<-g> file

=item B<--gen-stylesheet> file

Generate an XML stylesheet (XSLT) suitable for use with the --stylesheet
option.

=item B<-h>

=item B<--help>

Print a brief help message and exit.

=item B<-hk>

=item B<--hostname-key>

This flag can be used in a DHCP environment where host names remain static but
IP addresses are allocated dynamically.  As an example, a baseline is carried
out on a windows PC (win1) which has an IP of 192.168.1.8 and a Linux node
(linux1) with an IP of 192.168.1.16.  By the time an observed scan is carried
out, both nodes have been rebooted and reallocated each other's address while
retaining their configured names.  Using this flag, the IP addresses are mapped
back to the original hosts so that win1 in the baseline is compared to win1 in
the observed file.  Otherwise the IP addresses would be used for the comparison
meaning win1 would be incorrectly compared to linux1 and vice versa.  Using
this flag in an environment with static IP allocation would have no functional
effect.  There would simply be the performance overhead of the unnecessary
mapping logic. 

=item B<-m>

=item B<--man>

Print the manual page and exit.

=item B<-o> file

=item B<--observed> file

Specifies the nmap results to use as the "observed results" for the
comparison.
May be ommitted if the baseline and observed files are the only two arguments
given.

=item B<-of> file

=item B<--output-file> file

Send the output to the specified file.

=item B<-oh> <nmc>

=item B<--output-hosts> <nmc>

Specifies which types of hosts to display.  Any combination of n, m, or c may
be specified, as follows:

n = new hosts in the "observed" scan.

m = missing hosts in the "observed" scan.

c = changed hosts in the "observed" scan.

The default is to show new, missing and changed hosts.

=item B<-op> <ofcx>

=item B<--output-ports> <ofcx>

Specifies which ports to check when outputting changed hosts. Open, filtered
or closed ports.  Any combination of [ofcx] may be specified, as follows:

o = open ports in the "observed" scan.

f = filtered ports in the "observed" scan.

c = closed ports in the "observed" scan.

x = unfiltered ports in the "observed" scan

The default is to show all ports.

=item B<-s> <file | URL>

=item B<--stylesheet> <file | URL>

Specifies the location of an XML stylesheet to be referred to in any XML
output.  If the stylesheet specified is not a URL and is found to be a
non-existent file then it will be generated.  A suitable stylesheet can also
be generated using the --gen-stylesheet option.

=item B<-V>

=item B<--version>

Print the version and exit.

=back

=head1 DESCRIPTION

B<yandiff> is a command line utility (written in Perl) that allows users to
monitor networks for changes in port states and running services. It does this
by comparing the XML output of two nmap scans, one designated the "baseline",
the other the "observation". Alternatively a third XML file can be created
containing the differences.

=head1 EXAMPLES

To generate a report to screen:

=over 8

yandiff baseline.xml observed.xml

=back

which is equivalent to

=over 8

yandiff --baseline baseline.xml --observed observed.xml

=back

To generate a stylesheet to file:

=over 8

yandiff --gen-stylesheet yandiff.xsl

=back

To generate a report to an XML file and using the stylesheet generated in the
previous example:

=over 8

yandiff --baseline baseline.xml --observed observed.xml \
	--format xml --output-file report.xml

=back

=head1 HISTORY

Yandiff is loosely based on ndiff written by James Levine, except
where ndiff used "grepable" output from nmap, yandiff reads XML output using
Nmap::Parser.

=head1 AUTHORS

The Yandiff project at http://code.google.com/p/yandiff

=head1 BUGS

The project members graciously accept that there may be bugs.  If there are
any found, please report them at the link below or browse the issues reported
to see if a fix is either available or in progress.

http://code.google.com/p/yandiff/issues

Also it could be said that there are too many command line switches.  Even
the authors require the occassional reference to the manual.  We're sorry, we
have tried to keep it simple.

=cut
