diff --git a/pandora_server/ChangeLog b/pandora_server/ChangeLog index 5e57d84013..26b6083949 100644 --- a/pandora_server/ChangeLog +++ b/pandora_server/ChangeLog @@ -1,3 +1,10 @@ +2011-08-24 Ramon Novoa + + * lib/PandoraFMS/NmapParser.pm: Added to repository. Patched + Nmap::Parser. See http://search.cpan.org/dist/Nmap-Parser/. + + * lib/PandoraFMS/ReconServer.pm: Use Nmap::Parser. + 2011-08-24 Ramon Novoa * lib/PandoraFMS/DataServer.pm: Exclude the datalist tag from the diff --git a/pandora_server/lib/PandoraFMS/NmapParser.pm b/pandora_server/lib/PandoraFMS/NmapParser.pm new file mode 100644 index 0000000000..1759975f6c --- /dev/null +++ b/pandora_server/lib/PandoraFMS/NmapParser.pm @@ -0,0 +1,1904 @@ +# Copyright (c) <2003-2011> + +# MIT License + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +package Nmap::Parser; + +use strict; +use XML::Twig; +use Storable qw(dclone); +use vars qw($VERSION %D); + +$VERSION = 1.21; + + +sub new { + + my ( $class, $self ) = shift; + $class = ref($class) || $class; + + %{ $self->{HOSTS} } = %{ $self->{SESSION} } = (); + + $self->{twig} = new XML::Twig( + start_tag_handlers => { nmaprun => \&_nmaprun_start_tag_hdlr }, + + twig_roots => { + scaninfo => \&_scaninfo_tag_hdlr, + prescript => \&_prescript_tag_hdlr, + postscript => \&_postscript_tag_hdlr, + finished => \&_finished_tag_hdlr, + host => \&_host_tag_hdlr + }, + ignore_elts => { + addport => 1, + debugging => 1, + verbose => 1, + hosts => 1, + taskbegin => 1, + taskend => 1, + taskprogress => 1 + } + ); + + bless( $self, $class ); + return $self; +} + +#/*****************************************************************************/ +# NMAP::PARSER OBJECT METHODS +#/*****************************************************************************/ + +#Safe parse and parsefile will return $@ which will contain the error +#that occured if the parsing failed (it might be empty when no error occurred) +sub _init { + my $self = shift; + $D{callback} = $self->{callback}; +} + +sub _clean { + my $self = shift; + $self->{SESSION} = dclone( $D{$$}{SESSION} ) if ( $D{$$}{SESSION} ); + $self->{HOSTS} = dclone( $D{$$}{HOSTS} ) if ( $D{$$}{HOSTS} ); + delete $D{$$}; + delete $D{callback}; +} + +sub callback { + my $self = shift; + my $callback = shift; #first arg is CODE + if ( ref($callback) eq 'CODE' ) { + $self->{callback}{coderef} = $callback; + $self->{callback}{is_registered} = 1; + } + else { + $self->{callback}{is_registered} = 0; + } + + #returns if a callback is registered or not + return $self->{callback}{is_registered}; +} + +sub parse { + my $self = shift; + $self->_init(); + $self->{twig}->safe_parse(@_); + if ($@) { die $@; } + $self->_clean(); + $self->purge; + return $self; +} + +sub parsefile { + my $self = shift; + $self->_init(); + $self->{twig}->safe_parsefile(@_); + if ($@) { die $@; } + $self->_clean(); + $self->purge; + return $self; +} + +sub parsescan { + my $self = shift; + my $nmap = shift; + my $args = shift; + my @ips = @_; + my $FH; + + if ( $args =~ /-o(?:X|N|G)/ ) { + die +"[Nmap-Parser] Cannot pass option '-oX', '-oN' or '-oG' to parsecan()"; + } + + my $cmd; + $self->_init(); + +#if output file is defined, point it to a localfile then call parsefile instead. + if ( defined( $self->{cache_file} ) ) { + $cmd = + "$nmap $args -v -v -v -oX " + . $self->{cache_file} . " " + . ( join ' ', @ips ); + `$cmd`; #remove output from STDOUT + $self->parsefile( $self->{cache_file} ); + } + else { + $cmd = "$nmap $args -v -v -v -oX - " . ( join ' ', @ips ); + open $FH, + "$cmd |" || die "[Nmap-Parser] Could not perform nmap scan - $!"; + $self->parse($FH); + close $FH; + } + + $self->_clean(); + $self->purge; + return $self; + +} + +sub cache_scan { + my $self = shift; + $self->{cache_file} = shift || 'nmap-parser-cache.' . time() . '.xml'; +} + +sub purge { + my $self = shift; + $self->{twig}->purge; + return $self; +} + +sub addr_sort { + my $self = shift if ref $_[0]; + + return ( + map { unpack("x16A*", $_) } + sort { $a cmp $b } + map { + my @vals; + if( /:/ ) { #IPv6 + @vals = split /:/; + @vals = map { $_ eq '' ? (0) x (8-$#vals) : hex } @vals + } else { #IPv4 + my @v4 = split /\./; + # Sort as IPv4-mapped IPv6, per RFC 4291 Section 2.5.5.2 + @vals = ( (0) x 5, 0xffff, map { 256*$v4[$_] + $v4[$_+1] } (0,2) ); + } + pack("n8A*", @vals, $_) + } @_ + ); +} + +#MAIN SCAN INFORMATION +sub get_session { + my $self = shift; + my $obj = Nmap::Parser::Session->new( $self->{SESSION} ); + return $obj; +} + +#HOST STUFF +sub get_host { + my ( $self, $ip ) = (@_); + if ( $ip eq '' ) { + warn "[Nmap-Parser] No IP address given to get_host()\n"; + return undef; + } + $self->{HOSTS}{$ip}; +} + +sub del_host { + my ( $self, $ip ) = (@_); + if ( $ip eq '' ) { + warn "[Nmap-Parser] No IP address given to del_host()\n"; + return undef; + } + delete $self->{HOSTS}{$ip}; +} + +sub all_hosts { + my $self = shift; + my $status = shift || ''; + + return ( values %{ $self->{HOSTS} } ) if ( $status eq '' ); + + my @hosts = grep { $_->{status} eq $status } ( values %{ $self->{HOSTS} } ); + return @hosts; +} + +sub get_ips { + my $self = shift; + my $status = shift || ''; + + return $self->addr_sort( keys %{ $self->{HOSTS} } ) if ( $status eq '' ); + + my @hosts = + grep { $self->{HOSTS}{$_}{status} eq $status } + ( keys %{ $self->{HOSTS} } ); + return $self->addr_sort(@hosts); + +} + +#/*****************************************************************************/ +# PARSING TAG HANDLERS FOR XML::TWIG +#/*****************************************************************************/ + +sub _nmaprun_start_tag_hdlr { + + my ( $twig, $tag ) = @_; + + $D{$$}{SESSION}{start_time} = $tag->{att}->{start}; + $D{$$}{SESSION}{nmap_version} = $tag->{att}->{version}; + $D{$$}{SESSION}{start_str} = $tag->{att}->{startstr}; + $D{$$}{SESSION}{xml_version} = $tag->{att}->{xmloutputversion}; + $D{$$}{SESSION}{scan_args} = $tag->{att}->{args}; + $D{$$}{SESSION} = Nmap::Parser::Session->new( $D{$$}{SESSION} ); + + $twig->purge; + +} + +sub _scaninfo_tag_hdlr { + my ( $twig, $tag ) = @_; + my $type = $tag->{att}->{type}; + my $proto = $tag->{att}->{protocol}; + my $numservices = $tag->{att}->{numservices}; + + if ( defined($type) ) { #there can be more than one type in one scan + $D{$$}{SESSION}{type}{$type} = $proto; + $D{$$}{SESSION}{numservices}{$type} = $numservices; + } + $twig->purge; +} + +sub _prescript_tag_hdlr { + my ( $twig, $tag ) = @_; + my $scripts_hashref; + for my $script ( $tag->children('script') ) { + chomp($scripts_hashref->{ $script->{att}->{id} } = + $script->{att}->{output}); + } + $D{$$}{SESSION}{prescript} = $scripts_hashref; + $twig->purge; +} + +sub _postscript_tag_hdlr { + my ( $twig, $tag ) = @_; + my $scripts_hashref; + for my $script ( $tag->children('script') ) { + chomp($scripts_hashref->{ $script->{att}->{id} } = + $script->{att}->{output}); + } + $D{$$}{SESSION}{postscript} = $scripts_hashref; + $twig->purge; +} + +sub _finished_tag_hdlr { + my ( $twig, $tag ) = @_; + $D{$$}{SESSION}{finish_time} = $tag->{att}->{time}; + $D{$$}{SESSION}{time_str} = $tag->{att}->{timestr}; + $twig->purge; +} + +#parses all the host information in one swoop (calling __host_*_tag_hdlrs) +sub _host_tag_hdlr { + my ( $twig, $tag ) = @_; + my $id = undef; + + return undef unless ( defined $tag ); + + #GET ADDRESS INFO + my $addr_hashref; + $addr_hashref = __host_addr_tag_hdlr($tag); + + #use this as the identifier + $id = + $addr_hashref->{ipv4} + || $addr_hashref->{ipv6} + || $addr_hashref->{mac}; #worstcase use MAC + + $D{$$}{HOSTS}{$id}{addrs} = $addr_hashref; + + return undef unless ( defined($id) || $id ne '' ); + + #GET HOSTNAMES + $D{$$}{HOSTS}{$id}{hostnames} = __host_hostnames_tag_hdlr($tag); + + #GET STATUS + $D{$$}{HOSTS}{$id}{status} = $tag->first_child('status')->{att}->{state}; + + #CONTINUE PROCESSING IF STATUS IS UP - OTHERWISE NO MORE XML + if ( lc( $D{$$}{HOSTS}{$id}{status} ) eq 'up' ) { + + $D{$$}{HOSTS}{$id}{ports} = __host_port_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{os} = __host_os_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{uptime} = __host_uptime_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{tcpsequence} = __host_tcpsequence_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{ipidsequence} = __host_ipidsequence_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{tcptssequence} = __host_tcptssequence_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{hostscript} = __host_hostscript_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{distance} = __host_distance_tag_hdlr($tag); #returns simple value + $D{$$}{HOSTS}{$id}{trace} = __host_trace_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{trace_error} = __host_trace_error_tag_hdlr($tag); + $D{$$}{HOSTS}{$id}{times} = __host_times_tag_hdlr($tag); + } + + #CREATE HOST OBJECT FOR USER + $D{$$}{HOSTS}{$id} = Nmap::Parser::Host->new( $D{$$}{HOSTS}{$id} ); + + if ( $D{callback}{is_registered} ) { + &{ $D{callback}{coderef} }( $D{$$}{HOSTS}{$id} ); + delete $D{$$}{HOSTS}{$id}; + } + + $twig->purge; + +} + +sub __host_addr_tag_hdlr { + my $tag = shift; + my $addr_hashref; + + #children() will return all children with tag name address + for my $addr ( $tag->children('address') ) { + if ( lc( $addr->{att}->{addrtype} ) eq 'mac' ) { + + #we'll assume for now, only 1 MAC address per system + $addr_hashref->{mac}{addr} = $addr->{att}->{addr}; + $addr_hashref->{mac}{vendor} = $addr->{att}->{vendor}; + } + elsif ( lc( $addr->{att}->{addrtype} ) eq 'ipv4' ) { + $addr_hashref->{ipv4} = $addr->{att}->{addr}; + } #support for ipv6? we'll see + elsif ( lc( $addr->{att}->{addrtype} ) eq 'ipv6' ) { + $addr_hashref->{ipv6} = $addr->{att}->{addr}; + } + + } + + return $addr_hashref; +} + +sub __host_hostnames_tag_hdlr { + my $tag = shift; + + my $hostnames_tag = $tag->first_child('hostnames'); + return undef unless ( defined $hostnames_tag ); + + my @hostnames; + + for my $name ( $hostnames_tag->children('hostname') ) { + push @hostnames, $name->{att}->{name}; + } + + return \@hostnames; + +} + +sub __host_port_tag_hdlr { + my $tag = shift; + my ( $port_hashref, $ports_tag ); + + $ports_tag = $tag->first_child('ports'); + + return undef unless ( defined $ports_tag ); + + #Parsing Extraports + my $extraports_tag = $ports_tag->first_child('extraports'); + if ( defined $extraports_tag && $extraports_tag ne '' ) { + $port_hashref->{extraports}{state} = $extraports_tag->{att}->{state}; + $port_hashref->{extraports}{count} = $extraports_tag->{att}->{count}; + } + + #Parsing regular port information + + my ( $tcp_port_count, $udp_port_count ) = ( 0, 0 ); + + for my $port_tag ( $ports_tag->children('port') ) { + my $proto = $port_tag->{att}->{protocol}; + my $portid = $port_tag->{att}->{portid}; + my $state = $port_tag->first_child('state'); + my $owner = $port_tag->first_child('owner') || undef; + + $tcp_port_count++ if ( $proto eq 'tcp' ); + $udp_port_count++ if ( $proto eq 'udp' ); + + $port_hashref->{$proto}{$portid}{state} = $state->{att}->{state} + || 'unknown' + if ( $state ne '' ); + + #GET SERVICE INFORMATION + $port_hashref->{$proto}{$portid}{service} = + __host_service_tag_hdlr( $port_tag, $portid ) + if ( defined($proto) && defined($portid) ); + + #GET SCRIPT INFORMATION + $port_hashref->{$proto}{$portid}{service}{script} = + __host_script_tag_hdlr( $port_tag, $portid) + if ( defined($proto) && defined($portid) ); + + #GET OWNER INFORMATION + $port_hashref->{$proto}{$portid}{service}{owner} = $owner->{att}->{name} + if ( defined($owner) ); + + #These are added at the end, otherwise __host_service_tag_hdlr will overwrite + #GET PORT STATE + + } + + $port_hashref->{tcp_port_count} = $tcp_port_count; + $port_hashref->{udp_port_count} = $udp_port_count; + + return $port_hashref; + +} + +sub __host_service_tag_hdlr { + my $tag = shift; + my $portid = shift; #need a way to remember what port this service runs on + my $service = $tag->first_child('service[@name]'); + my $service_hashref; + $service_hashref->{port} = $portid; + + if ( defined $service ) { + $service_hashref->{name} = $service->{att}->{name} || 'unknown'; + $service_hashref->{version} = $service->{att}->{version}; + $service_hashref->{product} = $service->{att}->{product}; + $service_hashref->{extrainfo} = $service->{att}->{extrainfo}; + $service_hashref->{proto} = + $service->{att}->{proto} + || $service->{att}->{protocol} + || 'unknown'; + $service_hashref->{rpcnum} = $service->{att}->{rpcnum}; + $service_hashref->{tunnel} = $service->{att}->{tunnel}; + $service_hashref->{method} = $service->{att}->{method}; + $service_hashref->{confidence} = $service->{att}->{conf}; + $service_hashref->{fingerprint} = $service->{att}->{servicefp}; + } + + return $service_hashref; +} + +sub __host_script_tag_hdlr { + my $tag = shift; + my $script_hashref; + + for ( $tag->children('script') ) { + chomp($script_hashref->{ $_->{att}->{id} } = + $_->{att}->{output}); + } + + return $script_hashref; +} + +sub __host_os_tag_hdlr { + my $tag = shift; + my $os_tag = $tag->first_child('os'); + my $os_hashref; + my $portused_tag; + my $os_fingerprint; + + if ( defined $os_tag ) { + + #get the open port used to match os + $portused_tag = $os_tag->first_child("portused[\@state='open']"); + $os_hashref->{portused}{open} = $portused_tag->{att}->{portid} + if ( defined $portused_tag ); + + #get the closed port used to match os + $portused_tag = $os_tag->first_child("portused[\@state='closed']"); + $os_hashref->{portused}{closed} = $portused_tag->{att}->{portid} + if ( defined $portused_tag ); + + #os fingerprint + $os_fingerprint = $os_tag->first_child("osfingerprint"); + $os_hashref->{os_fingerprint} = + $os_fingerprint->{'att'}->{'fingerprint'} + if ( defined $os_fingerprint ); + + #This will go in Nmap::Parser::Host::OS + my $osmatch_index = 0; + for my $osmatch ( $os_tag->children('osmatch') ) { + $os_hashref->{osmatch_name}[$osmatch_index] = + $osmatch->{att}->{name}; + $os_hashref->{osmatch_name_accuracy}[$osmatch_index] = + $osmatch->{att}->{accuracy}; + $osmatch_index++; + } + $os_hashref->{'osmatch_count'} = $osmatch_index; + + #parse osclass tags + my $osclass_index = 0; + for my $osclass ( $os_tag->children('osclass') ) { + $os_hashref->{osclass_osfamily}[$osclass_index] = + $osclass->{att}->{osfamily}; + $os_hashref->{osclass_osgen}[$osclass_index] = + $osclass->{att}->{osgen}; + $os_hashref->{osclass_vendor}[$osclass_index] = + $osclass->{att}->{vendor}; + $os_hashref->{osclass_type}[$osclass_index] = + $osclass->{att}->{type}; + $os_hashref->{osclass_class_accuracy}[$osclass_index] = + $osclass->{att}->{accuracy}; + $osclass_index++; + } + $os_hashref->{'osclass_count'} = $osclass_index; + } + + return $os_hashref; + +} + +sub __host_uptime_tag_hdlr { + my $tag = shift; + my $uptime = $tag->first_child('uptime'); + my $uptime_hashref; + + if ( defined $uptime ) { + $uptime_hashref->{seconds} = $uptime->{att}->{seconds}; + $uptime_hashref->{lastboot} = $uptime->{att}->{lastboot}; + + } + + return $uptime_hashref; + +} + +sub __host_tcpsequence_tag_hdlr { + my $tag = shift; + my $sequence = $tag->first_child('tcpsequence'); + my $sequence_hashref; + return undef unless ($sequence); + $sequence_hashref->{class} = $sequence->{att}->{class}; + $sequence_hashref->{difficulty} = $sequence->{att}->{difficulty}; + $sequence_hashref->{values} = $sequence->{att}->{values}; + $sequence_hashref->{index} = $sequence->{att}->{index}; + + return $sequence_hashref; + +} + +sub __host_ipidsequence_tag_hdlr { + my $tag = shift; + my $sequence = $tag->first_child('ipidsequence'); + my $sequence_hashref; + return undef unless ($sequence); + $sequence_hashref->{class} = $sequence->{att}->{class}; + $sequence_hashref->{values} = $sequence->{att}->{values}; + return $sequence_hashref; + +} + +sub __host_tcptssequence_tag_hdlr { + my $tag = shift; + my $sequence = $tag->first_child('tcptssequence'); + my $sequence_hashref; + return undef unless ($sequence); + $sequence_hashref->{class} = $sequence->{att}->{class}; + $sequence_hashref->{values} = $sequence->{att}->{values}; + return $sequence_hashref; +} + +sub __host_times_tag_hdlr { + my $tag = shift; + my $times = $tag->first_child('times'); + my $times_hashref; + + if(defined $times){ + $times_hashref->{srtt} = $times->{att}->{srtt}; + $times_hashref->{rttvar} = $times->{att}->{rttvar}; + $times_hashref->{to} = $times->{att}->{to}; + + } + + return $times_hashref; + +} + +sub __host_hostscript_tag_hdlr { + my $tag = shift; + my $scripts = $tag->first_child('hostscript'); + my $scripts_hashref; + return undef unless ($scripts); + for my $script ( $scripts->children('script') ) { + chomp($scripts_hashref->{ $script->{att}->{id} } = + $script->{att}->{output}); + } + return $scripts_hashref; +} + +sub __host_distance_tag_hdlr { + my $tag = shift; + my $distance = $tag->first_child('distance'); + return undef unless ($distance); + return $distance->{att}->{value}; +} + +sub __host_trace_tag_hdlr { + my $tag = shift; + my $trace_tag = $tag->first_child('trace'); + my $trace_hashref = { hops => [], }; + + if ( defined $trace_tag ) { + + my $proto = $trace_tag->{att}->{proto}; + $trace_hashref->{proto} = $proto if defined $proto; + + my $port = $trace_tag->{att}->{port}; + $trace_hashref->{port} = $port if defined $port; + + for my $hop_tag ( $trace_tag->children('hop') ) { + + # Copy the known hop attributes, they will go in + # Nmap::Parser::Host::TraceHop + my %hop_data; + $hop_data{$_} = $hop_tag->{att}->{$_} for qw( ttl rtt ipaddr host ); + delete $hop_data{rtt} if $hop_data{rtt} !~ /^[\d.]+$/; + + push @{ $trace_hashref->{hops} }, \%hop_data; + } + + } + + return $trace_hashref; +} + +sub __host_trace_error_tag_hdlr { + my $tag = shift; + my $trace_tag = $tag->first_child('trace'); + + if ( defined $trace_tag ) { + + my $error_tag = $trace_tag->first_child('error'); + if ( defined $error_tag ) { + + # If an error happens, always provide a true value even if + # it doesn't contains a useful string + my $errorstr = $error_tag->{att}->{errorstr} || 1; + return $errorstr; + } + } + + return; +} + +#/*****************************************************************************/ +# NMAP::PARSER::SESSION +#/*****************************************************************************/ + +package Nmap::Parser::Session; +use vars qw($AUTOLOAD); + +sub new { + my $class = shift; + $class = ref($class) || $class; + my $self = shift || {}; + bless( $self, $class ); + return $self; +} + +#Support for: +#start_time, start_str, finish_time, time_str, nmap_version, xml_version, scan_args +sub AUTOLOAD { + ( my $param = $AUTOLOAD ) =~ s{.*::}{}xms; + return if ( $param eq 'DESTROY' ); + no strict 'refs'; + *$AUTOLOAD = sub { return $_[0]->{ lc $param } }; + goto &$AUTOLOAD; +} + +sub numservices { + my $self = shift; + my $type = shift + || ''; #(syn|ack|bounce|connect|null|xmas|window|maimon|fin|udp|ipproto) + + return unless ( ref( $self->{numservices} ) eq 'HASH' ); + + if ( $type ne '' ) { return $self->{numservices}{$type}; } + else { + my $total = 0; + for ( values %{ $self->{numservices} } ) { $total += $_; } + return $total; + } #(else) total number of services together +} + +sub scan_types { + return sort { $a cmp $b } ( keys %{ $_[0]->{type} } ) + if ( ref( $_[0]->{type} ) eq 'HASH' ); +} +sub scan_type_proto { return $_[1] ? $_[0]->{type}{ $_[1] } : undef; } + +sub prescripts { + my $self = shift; + my $id = shift; + unless ( defined $id ) { + return sort keys %{ $self->{prescript} }; + } + else { + return $self->{prescript}{$id}; + } +} + +sub postscripts { + my $self = shift; + my $id = shift; + unless ( defined $id ) { + return sort keys %{ $self->{postscript} }; + } + else { + return $self->{postscript}{$id}; + } +} + +#/*****************************************************************************/ +# NMAP::PARSER::HOST +#/*****************************************************************************/ + +package Nmap::Parser::Host; +use vars qw($AUTOLOAD); + +sub new { + my $class = shift; + $class = ref($class) || $class; + my $self = shift || {}; + bless( $self, $class ); + return $self; +} + +sub status { return $_[0]->{status}; } + +sub addr { + my $default = $_[0]->{addrs}{ipv4} || $_[0]->{addrs}{ipv6}; + return $default; +} + +sub addrtype { + if ( $_[0]->{addrs}{ipv4} ) { return 'ipv4'; } + elsif ( $_[0]->{addrs}{ipv6} ) { return 'ipv6'; } +} + +sub ipv4_addr { return $_[0]->{addrs}{ipv4}; } +sub ipv6_addr { return $_[0]->{addrs}{ipv6}; } + +sub mac_addr { return $_[0]->{addrs}{mac}{addr}; } +sub mac_vendor { return $_[0]->{addrs}{mac}{vendor}; } + +#returns the first hostname +sub hostname { + my $self = shift; + my $index = shift || 0; + if ( ref( $self->{hostnames} ) ne 'ARRAY' ) { return ''; } + if ( scalar @{ $self->{hostnames} } <= $index ) { + $index = scalar @{ $self->{hostnames} } - 1; + } + return $self->{hostnames}[$index] if ( scalar @{ $self->{hostnames} } ); +} + +sub all_hostnames { return @{ $_[0]->{hostnames} }; } +sub extraports_state { return $_[0]->{ports}{extraports}{state}; } +sub extraports_count { return $_[0]->{ports}{extraports}{count}; } +sub distance { return $_[0]->{distance}; } + +sub hostscripts { + my $self = shift; + my $id = shift; + unless ( defined $id ) { + return sort keys %{ $self->{hostscript} }; + } + else { + return $self->{hostscript}{$id}; + } +} + +sub all_trace_hops { + + my $self = shift; + + return unless defined $self->{trace}->{hops}; + return map { Nmap::Parser::Host::TraceHop->new( $_ ) } + @{ $self->{trace}->{hops} }; +} + +sub trace_port { return $_[0]->{trace}->{port} } +sub trace_proto { return $_[0]->{trace}->{proto} } +sub trace_error { return $_[0]->{trace_error} } + +sub _del_port { + my $self = shift; + my $proto = pop; #portid might be empty, so this goes first + my @portids = @_; + @portids = grep { $_ + 0 } @portids; + + unless ( scalar @portids ) { + warn "[Nmap-Parser] No port number given to del_port()\n"; + return undef; + } + + delete $self->{ports}{$proto}{$_} for (@portids); +} + +sub _get_ports { + my $self = shift; + my $proto = pop; #param might be empty, so this goes first + my $state = lc(shift); #open, filtered, closed or any combination + my @matched_ports = (); + + #if $state eq '', then tcp_ports or udp_ports was called for all ports + #therefore, only return the keys of all ports found + if ( $state eq '' ) { + return sort { $a <=> $b } ( keys %{ $self->{ports}{$proto} } ); + } + +#the port parameter can be set to either any of these also 'open|filtered' +#can count as 'open' and 'filetered'. Therefore I need to use a regex from now on +#if $param is empty, then all ports match. + + for my $portid ( keys %{ $self->{ports}{$proto} } ) { + + #escape metacharacters ('|', for example in: open|filtered) + #using \Q and \E + push( @matched_ports, $portid ) + if ( $self->{ports}{$proto}{$portid}{state} =~ /\Q$state\E/ ); + + } + + return sort { $a <=> $b } @matched_ports; + +} + +sub _get_port_state { + my $self = shift; + my $proto = pop; #portid might be empty, so this goes first + my $portid = lc(shift); + + return undef unless ( exists $self->{ports}{$proto}{$portid} ); + return $self->{ports}{$proto}{$portid}{state}; + +} + +#changed this to use _get_ports since it was similar code +sub tcp_ports { return _get_ports( @_, 'tcp' ); } +sub udp_ports { return _get_ports( @_, 'udp' ); } + +sub tcp_port_count { return $_[0]->{ports}{tcp_port_count}; } +sub udp_port_count { return $_[0]->{ports}{udp_port_count}; } + +sub tcp_port_state { return _get_port_state( @_, 'tcp' ); } +sub udp_port_state { return _get_port_state( @_, 'udp' ); } + +sub tcp_del_ports { return _del_port( @_, 'tcp' ); } +sub udp_del_ports { return _del_port( @_, 'udp' ); } + +sub tcp_service { + my $self = shift; + my $portid = shift; + if ( $portid eq '' ) { + warn "[Nmap-Parser] No port number passed to tcp_service()\n"; + return undef; + } + return Nmap::Parser::Host::Service->new( + $self->{ports}{tcp}{$portid}{service} ); +} + +sub udp_service { + my $self = shift; + my $portid = shift; + if ( $portid eq '' ) { + warn "[Nmap-Parser] No port number passed to udp_service()\n"; + return undef; + } + return Nmap::Parser::Host::Service->new( + $self->{ports}{udp}{$portid}{service} ); + +} + +#usually the first one is the highest accuracy + +sub os_sig { return Nmap::Parser::Host::OS->new( $_[0]->{os} ); } + +#Support for: +#tcpsequence_class, tcpsequence_values, tcpsequence_index, +#ipidsequence_class, ipidsequence_values, tcptssequence_values, +#tcptssequence_class, uptime_seconds, uptime_lastboot +#tcp_open_ports, udp_open_ports, tcp_filtered_ports, udp_filtered_ports, +#tcp_closed_ports, udp_closed_ports, times_srtt, times_rttvar, times_to +sub AUTOLOAD { + ( my $param = $AUTOLOAD ) =~ s{.*::}{}xms; + return if ( $param eq 'DESTROY' ); + my ( $type, $val ) = split /_/, lc($param); + +#splits the given method name by '_'. This will determine the function and param + no strict 'refs'; + + if ( ( $type eq 'tcp' || $type eq 'udp' ) + && ( $val eq 'open' || $val eq 'filtered' || $val eq 'closed' ) ) + { + +#they must be looking for port info: tcp or udp. The $val is either open|filtered|closed + *$AUTOLOAD = sub { return _get_ports( $_[0], $val, $type ); }; + goto &$AUTOLOAD; + + } + elsif ( defined $type && defined $val ) { + + #must be one of the 'sequence' functions asking for class/values/index + *$AUTOLOAD = sub { return $_[0]->{$type}{$val} }; + goto &$AUTOLOAD; + } + else { die '[Nmap-Parser] method ->' . $param . "() not defined!\n"; } +} + +#/*****************************************************************************/ +# NMAP::PARSER::HOST::SERVICE +#/*****************************************************************************/ + +package Nmap::Parser::Host::Service; +use vars qw($AUTOLOAD); + +sub new { + my $class = shift; + $class = ref($class) || $class; + my $self = shift || {}; + bless( $self, $class ); + return $self; +} + +sub scripts { + my $self = shift; + my $id = shift; + + unless ( defined $id ) { + return sort keys %{ $self->{script} }; + } + else { + return $self->{script}{$id}; + } +} + +#Support for: +#name port proto rpcnum owner version product extrainfo tunnel method confidence +#this will now only load functions that will be used. This saves +#on delay (increase speed) and memory + +sub AUTOLOAD { + ( my $param = $AUTOLOAD ) =~ s{.*::}{}xms; + return if ( $param eq 'DESTROY' ); + no strict 'refs'; + + *$AUTOLOAD = sub { return $_[0]->{ lc $param } }; + goto &$AUTOLOAD; +} + +#/*****************************************************************************/ +# NMAP::PARSER::HOST::OS +#/*****************************************************************************/ + +package Nmap::Parser::Host::OS; +use vars qw($AUTOLOAD); + +sub new { + my $class = shift; + $class = ref($class) || $class; + my $self = shift || {}; + bless( $self, $class ); + return $self; +} + +sub portused_open { return $_[0]->{portused}{open}; } +sub portused_closed { return $_[0]->{portused}{closed}; } +sub os_fingerprint { return $_[0]->{os_fingerprint}; } + +sub name_count { return $_[0]->{osmatch_count}; } + +sub all_names { + my $self = shift; + @_ = (); + if ( $self->{osclass_count} < 1 ) { return @_; } + if ( ref( $self->{osmatch_name} ) eq 'ARRAY' ) { + return sort @{ $self->{osmatch_name} }; + } + +} #given by decreasing accuracy + +sub class_count { return $_[0]->{osclass_count}; } + +#Support for: +#name,names, name_accuracy, osfamily, vendor, type, osgen, class_accuracy +sub AUTOLOAD { + ( my $param = $AUTOLOAD ) =~ s{.*::}{}xms; + return if ( $param eq 'DESTROY' ); + no strict 'refs'; + $param = lc($param); + + $param = 'name' if ( $param eq 'names' ); + if ( $param eq 'name' || $param eq 'name_accuracy' ) { + + *$AUTOLOAD = sub { _get_info( $_[0], $_[1], $param, 'osmatch' ); }; + goto &$AUTOLOAD; + } + else { + + *$AUTOLOAD = sub { _get_info( $_[0], $_[1], $param, 'osclass' ); }; + goto &$AUTOLOAD; + } +} + +sub _get_info { + my ( $self, $index, $param, $type ) = @_; + $index ||= 0; + + #type is either osclass or osmatch + if ( $index >= $self->{ $type . '_count' } ) { + $index = $self->{ $type . '_count' } - 1; + } + return $self->{ $type . '_' . $param }[$index]; +} + +#/*****************************************************************************/ +# NMAP::PARSER::HOST::TRACEHOP +#/*****************************************************************************/ + +package Nmap::Parser::Host::TraceHop; +use vars qw($AUTOLOAD); + +sub new { + my $class = shift; + $class = ref($class) || $class; + my $self = shift || {}; + bless( $self, $class ); + return $self; +} + +sub AUTOLOAD { + ( my $param = $AUTOLOAD ) =~ s{.*::}{}xms; + return if ( $param eq 'DESTROY' ); + no strict 'refs'; + $param = lc($param); + + # Supported accessors: + my %subs; + @subs{ qw( ttl rtt ipaddr host ) } = 1; + + if ( exists $subs{$param} ) { + + *$AUTOLOAD = sub { $_[0]->{$param} }; + goto &$AUTOLOAD; + } + else { die '[Nmap-Parser] method ->' . $param . "() not defined!\n"; } +} + +1; + +__END__ + +=pod + +=head1 NAME + +Nmap::Parser - parse nmap scan data with perl + +=head1 SYNOPSIS + + use Nmap::Parser; + my $np = new Nmap::Parser; + + $np->parsescan($nmap_path, $nmap_args, @ips); + #or + $np->parsefile($file_xml); + + my $session = $np->get_session(); + #a Nmap::Parser::Session object + + my $host = $np->get_host($ip_addr); + #a Nmap::Parser::Host object + + my $service = $host->tcp_service(80); + #a Nmap::Parser::Host::Service object + + my $os = $host->os_sig(); + #a Nmap::Parser::Host::OS object + + #--------------------------------------- + + my $np2 = new Nmap::Parser; + + $np2->callback(\&my_callback); + + $np2->parsefile($file_xml); + #or + $np2->parsescan($nmap_path, $nmap_args, @ips); + + sub my_callback { + + my $host = shift; + #Nmap::Parser::Host object + #.. see documentation for all methods ... + + } + + +I + +=head1 DESCRIPTION + +This module implements a interface to the information contained in an nmap scan. +It is implemented by parsing the xml scan data that is generated by nmap. This +will enable anyone who utilizes nmap to quickly create fast and robust security scripts +that utilize the powerful port scanning abilities of nmap. + +The latest version of this module can be found on here L + +=head1 OVERVIEW + +This module has an internal framework to make it easy to retrieve the desired information of a scan. +Every nmap scan is based on two main sections of informations: the scan session, and the scan information of all hosts. +The session information will be stored as a Nmap::Parser::Session object. This object will contain its own methods +to obtain the desired information. The same is true for any hosts that were scanned using the Nmap::Parser::Host object. +There are two sub objects under Nmap::Parser::Host. One is the Nmap::Parser::Host::Service object which will be used to obtain +information of a given service running on a given port. The second is the Nmap::Parser::Host::OS object which contains the +operating system signature information (OS guessed names, classes, osfamily..etc). + + Nmap::Parser -- Core parser + | + +--Nmap::Parser::Session -- Nmap scan session information + | + +--Nmap::Parser::Host -- General host information + | | + | |-Nmap::Parser::Host::Service -- Port service information + | | + | |-Nmap::Parser::Host::OS -- Operating system signature information + + +=head1 METHODS + +=head2 Nmap::Parser + +The main idea behind the core module is, you will first parse the information +and then extract data. Therefore, all parse*() methods should be executed before +any get_*() methods. + +=over 4 + + +=item B + +=item B + +Parses the nmap scan information in $string. Note that is usually only used if +you have the whole xml scan information in $string or if you are piping the +scan information. + +=item B + +Parses the nmap scan data in $xml_file. This file can be generated from an nmap +scan by using the '-oX filename.xml' option with nmap. If you get an error or your program dies due to parsing, please check that the +xml information is compliant. The file is closed no matter how C returns. + +=item B + +This method runs an nmap scan where $nmap is the path to the nmap executable or binary, +$args are the nmap command line parameters, and @ips are the list of IP addresses +to scan. parsescan() will automagically run the nmap scan and parse the information. + +If you wish to save the xml output from parsescan(), you must call cache_scan() method B +you start the parsescan() process. This is done to conserve memory while parsing. cache_scan() will +let Nmap::Parser know to save the output before parsing the xml since Nmap::Parser purges everything that has +been parsed by the script to conserve memory and increase speed. + +I + +I + +If you get an error or your program dies due to parsing, please check that the +xml information is compliant. If you are using parsescan() or an open filehandle +, make sure that the nmap scan that you are performing is successful in returning +xml information. (Sometimes using loopback addresses causes nmap to fail). + +=item B + +This function allows you to save the output of a parsescan() (or nmap scan) to the disk. $filename +is the name of the file you wish to save the nmap scan information to. It defaults to nmap-parser-cache.xml +It returns the name of the file to be used as the cache. + + #Must be called before parsescan(). + $np->cache_scan($filename); #output set to nmap-parser-cache.xml + + #.. do other stuff to prepare for parsescan(), ex. setup callbacks + + $np->parsescan('/usr/bin/nmap',$args,@IPS); + +=item B + +Cleans the xml scan data from memory. This is useful if you have a program where +you are parsing lots of nmap scan data files with persistent variables. + +=item B + +Sets the parsing mode to be done using the callback function. It takes the parameter +of a code reference or a reference to a function. If no code reference is given, +it resets the mode to normal (no callback). + + $np->callback(\&my_function); #sets callback, my_function() will be called + $np->callback(); #resets it, no callback function called. Back to normal. + + +=item B + +Obtains the Nmap::Parser::Session object which contains the session scan information. + +=item B + +Obtains the Nmap::Parser::Host object for the given $ip_addr. + +=item B + +Deletes the stored Nmap::Parser::Host object whose IP is $ip_addr. + +=item B + +=item B + +Returns an array of all the Nmap::Parser::Host objects for the scan. If the optional +status is given, it will only return those hosts that match that status. The status +can be any of the following: C<(up|down|unknown|skipped)> + +=item B + +=item B + +Returns the list of IP addresses that were scanned in this nmap session. They are +sorted using addr_sort. If the optional status is given, it will only return +those IP addresses that match that status. The status can be any of the +following: C<(up|down|unknown|skipped)> + +=item B + +This function takes a list of IP addresses and returns the correctly sorted +version of the list. + +=back + +=head2 Nmap::Parser::Session + +This object contains the scan session information of the nmap scan. + + +=over 4 + + +=item B + +Returns the numeric time that the nmap scan finished. + +=item B + +Returns the version of nmap used for the scan. + +=item B + +=item B + +If numservices is called without argument, it returns the total number of services +that were scanned for all types. If $type is given, it returns the number of services +for that given scan type. See scan_types() for more info. + +=item B + +Returns a string which contains the nmap executed command line used to run the +scan. + +=item B + +Returns the protocol type of the given scan type (provided by $type). See scan_types() for +more info. + +=item B + +Returns the list of scan types that were performed. It can be any of the following: +C<(syn|ack|bounce|connect|null|xmas|window|maimon|fin|udp|ipproto)>. + +=item B + +Returns the human readable format of the start time. + +=item B + +Returns the numeric form of the time the nmap scan started. + +=item B + +Returns the human readable format of the finish time. + +=item B + +Returns the version of nmap xml file. + +=back + +=head2 Nmap::Parser::Host + +This object represents the information collected from a scanned host. + + +=over 4 + +=item B + +Returns the state of the host. It is usually one of these +C<(up|down|unknown|skipped)>. + +=item B + +Returns the main IP address of the host. This is usually the IPv4 address. If +there is no IPv4 address, the IPv6 is returned (hopefully there is one). + +=item B + +Returns the address type of the address given by addr() . + +=item B + +Returns a list of all hostnames found for the given host. + +=item B + +Returns the number of extraports found. + +=item B + +Returns the state of all the extraports found. + +=item B + +=item B + +As a basic call, hostname() returns the first hostname obtained for the given +host. If there exists more than one hostname, you can provide a number, which +is used as the location in the array. The index starts at 0; + + #in the case that there are only 2 hostnames + hostname() eq hostname(0); + hostname(1); #second hostname found + hostname(400) eq hostname(1) #nothing at 400; return the name at the last index + + +=item B + +Explicitly return the IPv4 address. + +=item B + +Explicitly return the IPv6 address. + +=item B + +Explicitly return the MAC address. + +=item B + +Return the vendor information of the MAC. + +=item B + +Return the distance (in hops) of the target machine from the machine that performed the scan. + +=item B + +Returns a true value (usually a meaningful error message) if the traceroute was +performed but could not reach the destination. In this case C +contains only the part of the path that could be determined. + +=item B + +Returns an array of Nmap::Parser::Host::TraceHop objects representing the path +to the target host. This array may be empty if Nmap did not perform the +traceroute for some reason (same network, for example). + +Some hops may be missing if Nmap could not figure out information about them. +In this case there is a gap between the C values of consecutive returned +hops. See also C. + +=item B + +Returns the name of the protocol used to perform the traceroute. + +=item B + +Returns the port used to perform the traceroute. + +=item B + +Returns an Nmap::Parser::Host::OS object that can be used to obtain all the +Operating System signature (fingerprint) information. See Nmap::Parser::Host::OS +for more details. + + $os = $host->os_sig; + $os->name; + $os->osfamily; + +=item B + +=item B + +=item B + +Returns the class, index and values information respectively of the tcp sequence. + +=item B + +=item B + +Returns the class and values information respectively of the ipid sequence. + +=item B + +=item B + +Returns the class and values information respectively of the tcpts sequence. + +=item B + +Returns the human readable format of the timestamp of when the host had last +rebooted. + +=item B + +Returns the number of seconds that have passed since the host's last boot from +when the scan was performed. + +=item B + +=item B + +A basic call to hostscripts() returns a list of the names of the host scripts +run. If C<$name> is given, it returns the text output of the script with that +name, or undef if that script was not run. + +=item B + +=item B + +Returns the sorted list of TCP|UDP ports respectively that were scanned on this host. Optionally +a string argument can be given to these functions to filter the list. + + $host->tcp_ports('open') #returns all only 'open' ports (even 'open|filtered') + $host->udp_ports('open|filtered'); #matches exactly ports with 'open|filtered' + +I + +=item B + +=item B + +Returns the total of TCP|UDP ports scanned respectively. + +=item B + +=item B + +Deletes the current $portid from the list of ports for given protocol. + +=item B + +=item B + +Returns the state of the given port, provided by the port number in $portid. + +=item B + +=item B + +Returns the list of open TCP|UDP ports respectively. Note that if a port state is +for example, 'open|filtered', it will appear on this list as well. + +=item B + +=item B + +Returns the list of filtered TCP|UDP ports respectively. Note that if a port state is +for example, 'open|filtered', it will appear on this list as well. + +=item B + +=item B + +Returns the list of closed TCP|UDP ports respectively. Note that if a port state is +for example, 'closed|filtered', it will appear on this list as well. + +=item B + +=item B + +Returns the Nmap::Parser::Host::Service object of a given service running on port, +provided by $portid. See Nmap::Parser::Host::Service for more info. + + $svc = $host->tcp_service(80); + $svc->name; + $svc->proto; + + +=back + +=head3 Nmap::Parser::Host::Service + +This object represents the service running on a given port in a given host. This +object is obtained by using the tcp_service($portid) or udp_service($portid) method from the +Nmap::Parser::Host object. If a portid is given that does not exist on the given +host, these functions will still return an object (so your script doesn't die). +Its good to use tcp_ports() or udp_ports() to see what ports were collected. + +=over 4 + + +=item B + +Returns the confidence level in service detection. + +=item B + +Returns any additional information nmap knows about the service. + +=item B + +Returns the detection method. + +=item B + +Returns the service name. + +=item B + +Returns the process owner of the given service. (If available) + +=item B + +Returns the port number where the service is running on. + +=item B + +Returns the product information of the service. + +=item B + +Returns the protocol type of the service. + +=item B + +Returns the RPC number. + +=item B + +Returns the tunnel value. (If available) + +=item B + +Returns the service fingerprint. (If available) + +=item B + +Returns the version of the given product of the running service. + +=item B + +=item B + +A basic call to scripts() returns a list of the names of the scripts +run for this port. If C<$name> is given, it returns the text output of the +script with that name, or undef if that script was not run. + +=back + +=head3 Nmap::Parser::Host::OS + +This object represents the Operating System signature (fingerprint) information +of the given host. This object is obtained from an Nmap::Parser::Host object +using the C method. One important thing to note is that the order of OS +names and classes are sorted by B. This is more important than +alphabetical ordering. Therefore, a basic call +to any of these functions will return the record with the highest accuracy. +(Which is probably the one you want anyways). + +=over 4 + +=item B + +Returns the list of all the guessed OS names for the given host. + +=item B + +=item B + +A basic call to class_accuracy() returns the osclass accuracy of the first record. +If C<$index> is given, it returns the osclass accuracy for the given record. The +index starts at 0. + +=item B + +Returns the total number of OS class records obtained from the nmap scan. + +=item B + +=item B + +=item B + +=item B + +A basic call to name() returns the OS name of the first record which is the name +with the highest accuracy. If C<$index> is given, it returns the name for the given record. The +index starts at 0. + +=item B + +=item B + +A basic call to name_accuracy() returns the OS name accuracy of the first record. If C<$index> is given, it returns the name for the given record. The +index starts at 0. + +=item B + +Returns the total number of OS names (records) for the given host. + +=item B + +=item B + +A basic call to osfamily() returns the OS family information of the first record. +If C<$index> is given, it returns the OS family information for the given record. The +index starts at 0. + +=item B + +=item B + +A basic call to osgen() returns the OS generation information of the first record. +If C<$index> is given, it returns the OS generation information for the given record. The +index starts at 0. + +=item B + +Returns the closed port number used to help identify the OS signatures. This might not +be available for all hosts. + +=item B + +Returns the open port number used to help identify the OS signatures. This might +not be available for all hosts. + +=item B + +Returns the OS fingerprint used to help identify the OS signatures. This might not be available for all hosts. + +=item B + +=item B + +A basic call to type() returns the OS type information of the first record. +If C<$index> is given, it returns the OS type information for the given record. The +index starts at 0. + +=item B + +=item B + +A basic call to vendor() returns the OS vendor information of the first record. +If C<$index> is given, it returns the OS vendor information for the given record. The +index starts at 0. + +=back + +=head3 Nmap::Parser::Host::TraceHop + +This object represents a router on the IP path towards the destination or the +destination itself. This is similar to what the C command outputs. + +Nmap::Parser::Host::TraceHop objects are obtained through the +C and C Nmap::Parser::Host methods. + +=over 4 + +=item B + +The Time To Live is the network distance of this hop. + +=item B + +The Round Trip Time is roughly equivalent to the "ping" time towards this hop. +It is not always available (in which case it will be undef). + +=item B + +The known IP address of this hop. + +=item B + +The host name of this hop, if known. + +=back + +=head1 EXAMPLES + +I think some of us best learn from examples. These are a couple of examples to help +create custom security audit tools using some of the nice features +of the Nmap::Parser module. Hopefully this can double as a tutorial. +More tutorials (articles) can be found at L + +=head2 Real-Time Scanning + +You can run a nmap scan and have the parser parse the information automagically. +The only constraint is that you cannot use '-oX', '-oN', or '-oG' as one of your +arguments for nmap command line parameters passed to parsescan(). + + use Nmap::Parser; + + my $np = new Nmap::Parser; + my @hosts = @ARGV; #get hosts from cmd line + + #runs the nmap command with hosts and parses it automagically + $np->parsescan('/usr/bin/nmap','-sS O -p 1-1023',@hosts); + + for my $host ($np->all_hosts()){ + print $host->hostname."\n"; + #do mor stuff... + } + +If you would like to run the scan using parsescan() but also save the scan xml output, +you can use cache_scan(). You must call cache_scan() BEFORE you initiate the parsescan() method. + + use Nmap::Parser; + my $np = new Nmap::Parser; + + #telling np to save output + $np->cache_scan('nmap.localhost.xml'); + $np->parsescan('/usr/bin/nmap','-F','localhost'); + #do other stuff... + +=head2 Callbacks + +This is probably the easiest way to write a script with using Nmap::Parser, +if you don't need the general scan session information. During the parsing +process, the parser will obtain information of every host. The +callback function (in this case 'booyah()') is called after the parsing of +every host (sequentially). When the callback returns, the parser will delete all +information of the host it had sent to the callback. This callback function is +called for every host that the parser encounters. I + + use Nmap::Parser; + my $np = new Nmap::Parser; + + + $np->callback( \&booyah ); + + $np->parsefile('nmap_results.xml'); + # or use parsescan() + + sub booyah { + my $host = shift; #Nmap::Parser::Host object, just parsed + print 'IP: ',$host->addr,"\n"; + # ... do more stuff with $host ... + + #when it returns, host object will be deleted from memory + #(good for processing VERY LARGE files or scans) + } + + +=head2 Multiple Instances - (C) + +Using multiple instances of Nmap::Parser is extremely useful in helping +audit/monitor the network B

olicy (ohh noo! its that 'P' word!). +In this example, we have a set of hosts that had been scanned previously for tcp +services where the image was saved in I. We now will scan the +same hosts, and compare if any new tcp have been open since then +(good way to look for suspicious new services). Easy security Bompliance detection. +(ooh noo! The 'C' word too!). + + + use Nmap::Parser; + use vars qw($nmap_exe $nmap_args @ips); + my $base = new Nmap::Parser; + my $curr = new Nmap::Parser; + + + $base->parsefile('base_image.xml'); #load previous state + $curr->parsescan($nmap_exe, $nmap_args, @ips); #scan current hosts + + for my $ip ($curr->get_ips ) + { + #assume that IPs in base == IPs in curr scan + my $ip_base = $base->get_host($ip); + my $ip_curr = $curr->get_host($ip); + my %port = (); + + #find ports that are open that were not open before + #by finding the difference in port lists + my @diff = grep { $port{$_} < 2} + (map {$port{$_}++; $_} + ( $ip_curr->tcp_open_ports , $ip_base->tcp_open_ports )); + + print "$ip has these new ports open: ".join(',',@diff) if(scalar @diff); + + for (@diff){print "$_ seems to be ",$ip_curr->tcp_service($_)->name,"\n";} + + } + + +=head1 SUPPORT + +=head2 Discussion Forum + +If you have questions about how to use the module, or any of its features, you +can post messages to the Nmap::Parser module forum on CPAN::Forum. +L + +=head2 Bug Reports and Enhancements + +Please submit any bugs or feature requests to: +L + +B This can be done by running your scan with the I<-oX filename.xml> nmap switch. +Please remove any important IP addresses for security reasons. It saves time in reproducing issues. + +=head1 SEE ALSO + + nmap, XML::Twig + +The Nmap::Parser page can be found at: L. +It contains the latest developments on the module. The nmap security scanner +homepage can be found at: L. + +=head1 AUTHORS + +Anthony G Persaud L . Please see Changes file and CONTRIBUTORS file for a list of other great contributors. + +Additional Contributors: + * Robin Bowes L + * Daniel Miller L + * See Changes file for other contributors. + +=head1 COPYRIGHT + +Copyright (c) <2003-2010> + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=cut diff --git a/pandora_server/lib/PandoraFMS/ReconServer.pm b/pandora_server/lib/PandoraFMS/ReconServer.pm index 7de6e4eaee..2e6bf46067 100644 --- a/pandora_server/lib/PandoraFMS/ReconServer.pm +++ b/pandora_server/lib/PandoraFMS/ReconServer.pm @@ -36,6 +36,9 @@ use PandoraFMS::Core; use PandoraFMS::ProducerConsumerServer; use PandoraFMS::GIS qw(get_reverse_geoip_sql get_reverse_geoip_file get_random_close_point); +# Patched Nmap::Parser. See http://search.cpan.org/dist/Nmap-Parser/. +use PandoraFMS::NmapParser; + # Inherits from PandoraFMS::ProducerConsumerServer our @ISA = qw(PandoraFMS::ProducerConsumerServer); @@ -128,38 +131,30 @@ sub data_consumer ($$) { } # Call nmap - my $nmap = $pa_config->{'nmap'}; - my $subnet = $task->{'subnet'}; - my @output = `$nmap -nsP $subnet`; - if ($? != 0) { + my $np = new Nmap::Parser; + eval { + $np->parsescan($pa_config->{'nmap'},'-nsP', ($task->{'subnet'})); + }; + if ($@) { update_recon_task ($dbh, $task_id, -1); return; } - # Parse nmap output - my $addr = ''; - my $found_hosts = {}; - foreach my $line (@output) { - chomp ($line); - - if ($line =~ m/Nmap scan report for (\S+).*/) { - $addr = $1; - } elsif ($line =~ m/Host is up \((\S+)s.*/) { - next unless ($addr ne ''); - $found_hosts->{$addr} = 1; - $addr = ''; - } - } - - # Process found hosts + # Parse scanned hosts + my $module_hash; + my @up_hosts = $np->all_hosts ('up'); + my $total_up = scalar (@up_hosts); my $progress = 0; - my $added = ''; - my $total_hosts = scalar (keys (%{$found_hosts})); - foreach my $addr (keys (%{$found_hosts})) { + my $added_hosts = ''; + foreach my $host (@up_hosts) { $progress++; + # Get agent address + my $addr = $host->addr(); + next unless ($addr ne '0'); + # Update the recon task or break if it does not exist anymore - last if (update_recon_task ($dbh, $task_id, ceil ($progress / ($total_hosts / 100))) eq '0E0'); + last if (update_recon_task ($dbh, $task_id, ceil ($progress / ($total_up / 100))) eq '0E0'); # Does the host already exist? my $agent = get_agent_from_addr ($dbh, $addr); @@ -282,19 +277,19 @@ sub data_consumer ($$) { create_network_profile_modules ($pa_config, $dbh, $agent_id, $task->{'id_network_profile'}, $addr, $task->{'snmp_community'}); # Generate an event - pandora_event ($pa_config, "[RECON] New host [$host_name] detected on network [" . $task->{'subnet'} . ']', - $task->{'id_group'}, $agent_id, 2, 0, 0, 'recon_host_detected', 0, $dbh); + pandora_event ($pa_config, "[RECON] New host [$host_name] detected on network [" . $task->{'subnet'} . ']', + $task->{'id_group'}, $agent_id, 2, 0, 0, 'recon_host_detected', 0, $dbh); - $added .= $addr . ' '; + $added_hosts .= "$addr "; } # Create an incident with totals - if ($total_hosts > 0 && $task->{'create_incident'} == 1) { - my $text = "At " . strftime ("%Y-%m-%d %H:%M:%S", localtime()) . " ($total_hosts) new hosts were detected by Pandora FMS Recon Server running on [" . $pa_config->{'servername'} . "_Recon]. This incident has been automatically created following instructions for this recon task [" . $task->{'id_group'} . "].\n\n"; + if ($added_hosts ne '' && $task->{'create_incident'} == 1) { + my $text = "At " . strftime ("%Y-%m-%d %H:%M:%S", localtime()) . " ($added_hosts) new hosts were detected by Pandora FMS Recon Server running on [" . $pa_config->{'servername'} . "_Recon]. This incident has been automatically created following instructions for this recon task [" . $task->{'id_group'} . "].\n\n"; if ($task->{'id_network_profile'} > 0) { $text .= "Aditionally, and following instruction for this task, agent(s) has been created, with modules assigned to network component profile [" . get_nc_profile_name ($dbh, $task->{'id_network_profile'}) . "]. Please check this agent as soon as possible to verify it."; } - $text .= "\n\nThis is the list of IP addresses found: \n\n$added"; + $text .= "\n\nThis is the list of IP addresses found: \n\n$added_hosts"; pandora_create_incident ($pa_config, $dbh, "[RECON] New hosts detected", $text, 0, 0, 'Pandora FMS Recon Server', $task->{'id_group'}); } @@ -378,7 +373,7 @@ sub tcp_scan ($$$) { my ($pa_config, $host, $portlist) = @_; my $nmap = $pa_config->{'nmap'}; - my $output = `$nmap -p$portlist $host | grep open | wc -l`; + my $output = `$nmap -p$portlist $host | grep open | wc -l`; return 0 if ($? != 0); return $output; }