#!/usr/bin/perl
#
# Docs are at the bottom below the =head1.
# You could just type perldoc nmap2netsaint.pl;
#
# Changes:
#  
#  2000/03/01: v0.01 Original Version
#  2000/03/14: v0.02 Minor bug fixes, added support for more plugins and added POD (Plain Old Documentation)
#  2000/05/22: v0.03 Updated to handle 2.5x output (Thanks Yuriy!)

use strict;

use Data::Dumper;
$Data::Dumper::Indent = 1;

use File::Path;
use File::Basename;
use Getopt::Std;

my $data_ref = {};

($data_ref->{Program},
 $data_ref->{Path},
 $data_ref->{Ext}) = fileparse($0, '.pl');

$data_ref->{Version} = '0.0.3';

my $options_ref = {};
getopts('hvVl:d:o:', $options_ref);
$data_ref->{nmap_log}       = $options_ref->{l};
$data_ref->{output_file}    = $options_ref->{o};
$data_ref->{default_domain} = $options_ref->{d};
$data_ref->{Verbose}        = $options_ref->{v};
$data_ref->{VERBOSE}        = $options_ref->{V};

if (defined $options_ref->{h} ||
    !$options_ref->{l},  
    !$options_ref->{o}) { 
  print "$data_ref->{Program}: -h -v -l {nmap_log_file} -o {output_file} -d {default_domain}\n" .
        "  -v Verbose\n" .
        "  -V Serious Verbose\n" .
        "  -h This screen\n";
  exit;
}

if ($data_ref->{output_file} eq 'hosts.cfg') {
  print "I'm sorry.  It's not that I don't trust you, but I'm not going to possibly overwrite your hosts.cfg\n";
  exit;
}

$data_ref->{default_host_cfg} = {
  parent_host => '',
  host_check_command => 'check-host-alive',
  max_attempts => '3',
  notification_interval => '5',
  notification_period => '24x7',
  notify_recovery => '1',
  notify_down => '1',
  notify_unreachable => '1',
  event_handler => ''
};

$data_ref->{default_service_cfg} = {
  check_period => '24x7',
  max_attempts => '3',
  check_interval => '5',
  retry_interval => '1',
  contactgroups => 'unix-admins',
  notification_interval => '5',
  notification_period => '24x7',
  notify_recovery => '1',
  notify_critical => '1',
  notify_warning => '0',
  event_handler => '',
  check_command => 'check-ping'
};

my $service_check_command = {
  'http'   => {
    'check_command' => 'check_http',
    'description'   => 'HTTP',
  },
  'ftp'    => {
    'check_command' => 'check_ftp',
    'description'   => 'FTP',
  },
  'telnet' => {
    'check_command' => 'check_telnet',
    'description'   => 'Telnet',
  },
  'domain' => {
    'check_command' => 'check_dns',
    'description'   => 'DNS',
  },
  'pop-3'  => {
    'check_command' => 'check_pop',
    'description'   => 'Pop-3',
  },
  'smtp'   => {
    'check_command' => 'check_smtp',
    'description'   => 'SMTP',
  },
  'ssh'    => {
    'check_command' => 'check_ssh',
    'description'   => 'ssh',
  },
  'mysql'  => {
    'check_command' => 'check_mysql',
    'description'   => 'mySQL',
  },
  'nntp'   => {
    'check_command' => 'check_nntp',
    'description'   => 'News (nntp)',
  },
  'ping'   => {
    'check_command' => 'check_ping',
    'description'   => 'Connectivity',
  }
};

print "\n", $data_ref->{Program}, '-', $data_ref->{Version}, "\n\n" if ($data_ref->{Verbose});

print "Reading '$data_ref->{nmap_log}'\n";
if (open(LOG, $data_ref->{nmap_log})) {
  my $host_ref;
  my $port_info = 0;
  while(<LOG>) {
    chomp;

    if (/^Starting nmap V\. ([^ ]+)/) {
      $data_ref->{Version} = $1;  
      print "Processing log from nmap v$data_ref->{Version}\n" if ($data_ref->{Verbose});
      print "  Upgrading to 2.50 or greater is recommended!\n" if (($data_ref->{Version} < 2.5) && $data_ref->{Verbose});
    }
    elsif (/^Interesting ports on /i) {
      $host_ref = process_host($data_ref);
      $port_info = 0;
    }
    elsif (/^No ports open for host /i) {
      $host_ref = process_host($data_ref);
      $port_info = 0;
    }
    elsif (/^port\s+state\s+protocol\s+service/i && defined $host_ref) {
      $port_info = 1;
    }
    elsif (/^port\s+state\s+service/i && defined $host_ref) {
      $port_info = 2;
    }
    elsif (!$_) {
      $port_info = 0 if ($port_info > 0);
    }
    elsif ($port_info == 1 && defined $host_ref) {
      my $port_ref = {};
      ($port_ref->{port},
       $port_ref->{state},
       $port_ref->{protocol},
       $port_ref->{service}) = split(' ', $_); 

      $host_ref->{port}->{$port_ref->{port}} = $port_ref;

      print "  Found service '$port_ref->{service}' on '$port_ref->{port}'\n" if ($data_ref->{VERBOSE});
    }
    elsif ($port_info == 2 && defined $host_ref) {
      my $port_ref = {};
      ($port_ref->{port_protocol},
       $port_ref->{state},
       $port_ref->{service}) = split(' ', $_); 

      ($port_ref->{port}, $port_ref->{protocol}) = split('/', $port_ref->{port_protocol}) ;

      $host_ref->{port}->{$port_ref->{port}} = $port_ref;

      print "  Found service '$port_ref->{service}' on '$port_ref->{port}'\n" if ($data_ref->{VERBOSE});
    }
  }

  output_netsaint_config($data_ref);
}
else {
  print STDERR "Unable to open '$data_ref->{nmap_log}'\n";
}

sub process_host {
  my $data_ref = shift;

  s/^Interesting ports on\s+//g;
  s/^No ports open for host\s+//g;

  my $host_ref = {};

  if (/^\(/) {
    # There's no rDNS for this IP
    
    ($host_ref->{address}) = /\(([^\)]+)/;

    $host_ref->{hostname} = $host_ref->{address};

    $host_ref->{hostname} =~ s/\./-/g;
    $host_ref->{hostname} .= '.' . $data_ref->{default_domain};
  }
  else {
    ($host_ref->{hostname}, $host_ref->{address}) = /([^\s]+)\s+\(([^\)]+)/;
  }

  $host_ref->{name} = $host_ref->{hostname};

  my $extra = 1;
  while (exists $data_ref->{hosts}->{$host_ref->{name}}) {
    $extra++;
    $host_ref->{name} = $host_ref->{hostname} . "_$extra";
  }

  if (!($host_ref->{address} =~ /\.0$/)) {
    $data_ref->{host}->{$host_ref->{name}} = $host_ref;

    print "Found host '$host_ref->{name}'\n" if ($data_ref->{VERBOSE});

    return $host_ref;
  }
  else {
    return;
  }
}

sub output_netsaint_config {
  my $data_ref = shift;

  print "Generating '$data_ref->{output_file}':\n" if ($data_ref->{Verbose}) ;

  if (open(OUTPUT, ">$data_ref->{output_file}")) { 
    foreach my $host_name (sort keys % { $data_ref->{host} }) {
      my $host_ref = $data_ref->{host}->{$host_name};

      print OUTPUT 'host[' . $host_name . ']=' .                             # Name
                   join (';', $host_name,                                    # Alias
                              $host_ref->{address},    
                              $data_ref->{default_host_cfg}->{parent_host},
                              $data_ref->{default_host_cfg}->{host_check_command},
                              $data_ref->{default_host_cfg}->{max_attempts},  
                              $data_ref->{default_host_cfg}->{notification_interval},  
                              $data_ref->{default_host_cfg}->{notification_period},  
                              $data_ref->{default_host_cfg}->{notify_recovery},  
                              $data_ref->{default_host_cfg}->{notify_down},  
                              $data_ref->{default_host_cfg}->{notify_unreachable},  
                              $data_ref->{default_host_cfg}->{event_handler}) . "\n";

      # At least do connectivity checking on it.
      $host_ref->{port}->{ping}->{service} = 'ping';
    
      foreach my $port (sort { $a <=> $b } keys % { $host_ref->{port} }) {
        my $port_ref = $host_ref->{port}->{$port};

        if (exists $service_check_command->{$port_ref->{service}}) {
          my $service_ref = $service_check_command->{$port_ref->{service}};

          print OUTPUT 'service[' . $host_name . ']=' .                          
                       join (';', $service_ref->{description},                                    
                                  $data_ref->{default_service_cfg}->{check_period},
                                  $data_ref->{default_service_cfg}->{max_attempts},
                                  $data_ref->{default_service_cfg}->{check_interval},
                                  $data_ref->{default_service_cfg}->{retry_interval},
                                  $data_ref->{default_service_cfg}->{contactgroups},
                                  $data_ref->{default_service_cfg}->{notification_interval},
                                  $data_ref->{default_service_cfg}->{notification_period},
                                  $data_ref->{default_service_cfg}->{notify_recovery},
                                  $data_ref->{default_service_cfg}->{notify_critical},
                                  $data_ref->{default_service_cfg}->{notify_warning},
                                  $data_ref->{default_service_cfg}->{event_handler},
                                  $service_ref->{check_command}), "\n";
        }
        else {
          print OUTPUT "# No known service check for '$port_ref->{service}' on port '$port_ref->{port}'\n";
        }
      }

      print OUTPUT "\n";
    }
  }
  else {
    print "Unable to create '$data_ref->{output_file}'\n";
  }
}

=head1 NAME

nmap2netsaint.pl - Perl program to process nmap output into Netsaint hosts.cfg entries

=head1 SYNOPSIS

  Note: I'm not going to go into the theory of using nmap.  Please read the nmap docs for that.

  ./nmap -sS -O myserver.mydomain.com > nmap.log

  ./nmap2netsaint.pl -v -l nmap.log -o new.cfg -d mydomain.com

  That's it.

  What this program attempts to do is make you life easier by building your host and service entries for you.

  It does this by parsing the nmap output.

  Here's a sample nmap log

    Starting nmap V. 2.53 by fyodor@insecure.org ( www.insecure.org/nmap/ )
    Interesting ports on zaphod.ixlmemphis.net (207.15.160.27):
    (The 1518 ports scanned but not shown below are in state: closed)
    Port       State       Service
    22/tcp     open        ssh                     
    23/tcp     open        telnet                  
    25/tcp     open        smtp                    
    80/tcp     open        http                    
    3306/tcp   open        mysql                   

    TCP Sequence Prediction: Class=truly random
                             Difficulty=9999999 (Good luck!)
    Remote operating system guess: Linux 2.0.35-38

    Nmap run completed -- 1 IP address (1 host up) scanned in 4 seconds

  1. Host entries
     These are generated from the 'Interesting ports on' line.
     If the host ip has a rDNS (Reverse DNS) mapping then it is used for the hostname.
     
     Note: If you use nmap on a network address (192.168.100.0) it will generate a summary
     for it.  nmap2netsaint filters this out.

  2. Service entries
     nmap2netsaint contains a list of 'known services'.  This is simply a cross-reference 
     between the nmap 'Service' and a Netsaint plugin.
    
     Note: Any service which nmap2netsaint doesn't recogize is added to the output as a comment.
     That way you can determine what you want to do with it. 
     If you know of any typical service -> plugin maps let me know so I can add them.

  Here's what the generated file looks like:

    host[myserver.mydomain.com]=myserver.mydomain.com;192.168.100.1;;check-host-alive;3;5;24x7;1;1;1;
    service[myserver.mydomain.com]=FTP;24x7;3;5;1;unix-admins;5;24x7;1;1;0;;check_ftp
    service[myserver.mydomain.com]=ssh;24x7;3;5;1;unix-admins;5;24x7;1;1;0;;check_ssh
    service[myserver.mydomain.com]=Telnet;24x7;3;5;1;unix-admins;5;24x7;1;1;0;;check_telnet
    service[myserver.mydomain.com]=SMTP;24x7;3;5;1;unix-admins;5;24x7;1;1;0;;check_smtp
    # No known service check for 'sunrpc' on port '111'
    # No known service check for 'printer' on port '515'
    # No known service check for 'X11' on port '6000'

=head1 BUGS

What exactly do you mean by bugs?  By some definitions all my code can be 
considered buggy. ;)

Actually I wouldn't be suprised.  I wrote this in about an hour.

=head1 AUTHOR

Todd A. Green, Home: slaribartfast@awardsforfjords.com,
               Work: tagreen@ionictech.net

=head1 COPYRIGHT

Copyright (c) 2000 Todd A. Green.  All rights reserved.  This program is
free software; you can redistribute it and/or modify it under the same terms as
Perl itself.  If you do modify it though please let the author know.

=head1 DISCLAIMER

This software is Alpha.  In that I'm the only person to see, use,
debug, care about it. Use it at your own risk.  Please send any bug reports or
suggestions to the author.

=head1 SEE ALSO

Netsaint @ http://www.netsaint.org
nmap @ http://www.insecure.org/nmap/index.html 

=cut


