#!/usr/bin/perl
#
# $Id: NmapFile.pm,v 1.16 2001/11/24 03:16:27 levine Exp $
#
# Copyright (C) 2000  James D. Levine (jdl@vinecorp.com)
#
#
#   This program is free software; you can redistribute it and/or
#   modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation; either version 2
#   of the License, or (at your option) any later version.
# 
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
# 
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 
#   02111-1307, USA.
#
####################################################################

#
# A DataStore subclass which parses and produces NMap machine-readable
# format data.
#

use PortScan::DataStore;
use PortScan::ScannedHost;
use PortScan::PortSpec;


package PortScan::NmapFile;


use strict;
no strict 'refs';


@PortScan::NmapFile::ISA = qw(PortScan::DataStore);

sub new
{
    my ($type, $parms) = @_;

    my $self = PortScan::DataStore::new($type, $parms);

    $self->{attach_file} = "";	# nmap-output file to attach do
    $self->{attach_metafile} = ""; # metadata file


    return $self;
}

#
# Retrieves nmap data from the configured directory (or current dir)
# and returns a populated ScanSet.  Also reads in any associated
# metadata file  into the ScanSet's properties.  The supplied filename
# is treated as a filename root.

sub retrieve_scanset
{
    my ($self, $tag) = @_;
    my ($filepath, $metapath) = $self->calc_filenames($tag);

    my $set =  new PortScan::ScanSet($tag, undef, undef, undef);
    hosts_from_nmap_scan_file($filepath, $set);

    my $props = read_metafile($metapath);
    $props = {} if !defined($props);
    $set->properties($props);

    return $set;
}
 
# Write the supplied ScanSet to an nmap file and metadata file
# in the configured directory.  The supplied tag is treated as 
# a filename root.
sub put_scanset
{
    my ($self, $set) = @_;

    my ($filepath, $metapath) = $self->calc_filenames($set->tag());
    hosts_to_nmap_file($filepath, $set);
    write_metafile($metapath, $set->properties());
}

# Set the associated nmap file for reading/writing.
sub attach_file
{
    my ($self, $f) = @_;
    $self->{attach_file} = $f;
}

# Sets the appropriate metadata file for reading/writing.
sub attach_metafile
{
    my ($self, $f) = @_;
    $self->{attach_metafile} = $f;
}




# figures out the path to the nmap and metadata files
sub calc_filenames
{
    my($self, $tag) = @_;

    my $filepath = $self->{attach_file};
    my $metapath = $self->{attach_metafile};

    $filepath = $self->full_path_to_nmap_file($tag)
	if (!length($filepath));

    $metapath = $self->full_path_to_meta_file($tag)
	if (!length($metapath));

    ($filepath, $metapath);
}

# writes a  metadata file to disk
sub write_metafile
{
    my ($filepath, $properties) = @_;

    open (OUT, ">$filepath") || return 0;

    while( my($k, $v) = each %$properties)
    {
	printf OUT "$k = $v\n";
    }
    
    close OUT;
    return 1;
}

# reads a metadata file from disk
sub read_metafile
{
    my $filepath = shift;

    open (IN, "<$filepath") || return undef;

    my @lines = <IN>;
    close IN;
    chomp @lines;

    my $h = {};

    foreach my $line (@lines)
    {
	my ($k, $v) = split /=/, $line;
	$h->{$k} = $v;
    }

    $h;
}

# outputs a ScanSet to an nmap file
sub hosts_to_nmap_file
{
    my ($filepath, $scan_set) = @_;

    open (OUT, ">$filepath") || return 0;

    printf OUT scanned_ports_to_nmap_format($scan_set->all_scanned_ports()). "\n";

    foreach my $host (PortScan::ScannedHost::sorted_list values %{$scan_set->hosts()})
    {
#	print $host;
	printf OUT host_to_nmap_format($host). "\n";
    }
    
    close OUT;
    return 1;
}


#
# will be used to condense a port list N,N+1,N+2,...,M to "N-M" for smaller
# output to nmap files.

sub port_list_to_range
{
    my( $l ) = @_;		

    return "" if $#$l == -1;

    # sort numerically
    
    my $range_start = shift @$l;

    my $range_end = $range_start;

    my $result = "";

    foreach my $p ( @$l )
    {
	if( $p > ( 1+ $range_end ) )
	{
	    if( $range_start != $range_end )
	    {
		$result .= $range_start . "-" . $range_end . ",";
	    }
	    else
	    {
		$result .= $range_start . ",";
	    }

	    $range_start = $p;
	    $range_end = $range_start;
	}
	else
	{
	    $range_end = $p;
	}

    }

    chop $result;

    $result;
}

# private.
# takes care of outputting a list of scanned ports in nmap file format
#

sub scanned_ports_to_nmap_format
{
    my($h) = shift;
    my(@tcp, @udp);

    foreach my $spec (PortScan::PortSpec::sorted_list values %$h)
    {
	($spec->proto() eq "tcp") && (push @tcp, $spec->number());
	($spec->proto() eq "udp") && (push @udp, $spec->number());
    }

    "# Ports scanned: TCP(" . (1+ $#tcp) . ";" . (join ",", @tcp) 
	. ") UDP(" . (1 + $#udp) . ";" . (join "," ,@udp) . ")";
}

# private
# takes care of outputting a host in nmap file format

sub host_to_nmap_format
{
    my $host = shift;

    my $result = "Host: " . $host->addr() . " ()     Ports: ";

    my $pcount = 0;

    foreach my $spec ($host->port_specs_sorted_list())
    {
	$result .= portspec_to_nmap_format($spec) . ", ";
	++$pcount;
    }
    (chop $result, chop $result) if $pcount;

    $result .= "    Ignored State: " . $host->default_state();

    $result;
}

# private
# takes care of outputting a portspec in nmap file format.
sub portspec_to_nmap_format
{
    my $sp = shift;
    return sprintf ("%s/%s/%s/%s/%s/%s/%s", 
		    $sp->number, $sp->state, $sp->proto, $sp->u1, $sp->service, $sp->u2, $sp->u3);
}


# reads a host from nmap format text, producing a ScannedHost instance.
sub host_from_nmap_format
{
    my $text = shift;

    my $o = new PortScan::ScannedHost;
    
    $text =~ s/Host://g;
    $text =~ s/Ports:/|/g;
    $text =~ s/Ignored State:/|/g;

    my ($h, $p, $d) = split /\|/, $text;

    $h =~ /\s+([0-9\.]+)/;
    my $addr = $1;
    $addr =~ s/\s*//g;

    $o->addr($addr);
    
    my $raw_ports_text = $p;

    $d =~ /\s*(\w*)/;
    my $default_state = $1;
    $default_state =~ s/\s*//g;

    $o->default_state($default_state);

    $raw_ports_text =~ s/ //g;

    my @raw_ports_list = split /,/, $raw_ports_text;

    my $ports_hash = {};
    my $port_specs_hash = {};

    if ( scalar(@raw_ports_list) > 0 ) 
    {
	foreach my $port_spec (@raw_ports_list)
	{
	    my ($port, $state, $proto, $u1, $service, $u2, $u3) = split /\//, $port_spec;

	    $port =~ s/\s*//g;
	    $port = 0 + $port;	# be extra sure it's numeric


	    $state =~ s/\s*//g;
	    $proto =~ s/\s*//g;

	    $ports_hash->{$port} = $state;

	    my $ps = new PortScan::PortSpec(
					    $port, $state, $proto, $u1,
					    $service, $u2, $u3
					    );
	    $port_specs_hash->{$ps->key_for()} = $ps;
	}

	$o->port_specs($port_specs_hash);

    }

    $o;
}


# private.
# parses the hosts portion of an nmap file

sub hosts_from_nmap_scan_file
{
    my  ($filename, $scan_set) = @_;

    my $hosts = {};		# hash of ScannedHost objects keys on IP addr
    my $ports = {};		# hash of PortSpec objects keyed on PortSpec::key_for() values

    open (IN, "<$filename") || return undef;

    my @lines = <IN>;
    chomp @lines;
    close IN;

  LINE:
    foreach my $line (@lines)
    {
	($line =~ /^# Ports scanned:/) 
	 && do 
	 {
	     $scan_set->all_scanned_ports( parse_ports($line) ); 
	     next LINE;
	 };

	 next LINE if $line =~ /^#/;
	 next LINE if $line =~ /^[\s]*$/;

	 my $scan_host =  host_from_nmap_format($line);

	 $scan_set->add_host($scan_host);
     }

 }

# private.
# parses the ports list of an nmap file
sub parse_ports
{
    my ($line) = @_;

    my $h = {};

    $line =~ /TCP\(\d*;([0-9,-]*)\)/;

    my @l = split /,/, $1;
    my (@tcp_specs, @udp_specs);

    foreach my $port (@l)
    {
	if ($port =~ /(\d*)-(\d*)/) {
	    push @tcp_specs, ($1 .. $2);
	} else {
	    push @tcp_specs, $port;
	}
    }

    foreach my $port (@tcp_specs)
    {
	my $ps =  new PortScan::PortSpec($port, "", "tcp", "", "", "", "");
	$h->{$ps->key_for()} = $ps;
    }

    $line =~ /UDP\(\d*;([0-9,-]*)\)/;
    @l = split /,/, $1;
    foreach my $port (@l)
    {
	if ($port =~ /(\d*)-(\d*)/) {
	    push @udp_specs, ($1 .. $2);
	} else {
	    push @udp_specs, $port;
	}
    }

    foreach my $port (@udp_specs)
    {
	my $ps = new PortScan::PortSpec($port, "", "udp", "", "", "", "");
	$h->{$ps->key_for()} = $ps;
    }

    $h;
}


# private
# calculates the path to an nmap file using a tag and the configured nmap
# file data store directory
sub full_path_to_nmap_file
{
    my ($self, $tag) = @_;

    $self->path_to_dir() . "/${tag}.nm";
}

# private
# calculates the path to a metadata file using a tag and the configured nmap
# file data store directory
sub full_path_to_meta_file
{
    my ($self, $tag) = @_;

    $self->path_to_dir() . "/${tag}.info";
}

# private
# calculates the path to the nmap file data store directory
# using the DataStore user property "root"
sub path_to_dir
{
    my $self = shift;

    my $path = $self->get_user_property("root");
    $path = "." if !length($path);

    $path;
}


sub tests
{


}


1;











