diff --git a/pandora_console/extras/pandoradb_migrate_6.0_to_7.0.mysql.sql b/pandora_console/extras/pandoradb_migrate_6.0_to_7.0.mysql.sql index 235fdfd12b..360eb14c0e 100644 --- a/pandora_console/extras/pandoradb_migrate_6.0_to_7.0.mysql.sql +++ b/pandora_console/extras/pandoradb_migrate_6.0_to_7.0.mysql.sql @@ -262,4 +262,10 @@ UPDATE treport_custom_sql SET `sql` = 'select direccion, alias, c UPDATE treport_custom_sql SET `sql` = 'select (select tagente.alias from tagente where tagente.id_agente = tagente_modulo.id_agente) as agent_nombre, nombre , (select tmodule_group.name from tmodule_group where tmodule_group.id_mg = tagente_modulo.id_module_group) as module_group, module_interval from tagente_modulo where delete_pending = 0 order by nombre;' WHERE id = 2; UPDATE treport_custom_sql SET `sql` = 'select t1.alias as agent_name, t2.nombre as module_name, (select talert_templates.name from talert_templates where talert_templates.id = t3.id_alert_template) as template, (select group_concat(t02.name) from talert_template_module_actions as t01 inner join talert_actions as t02 on t01.id_alert_action = t02.id where t01.id_alert_template_module = t3.id group by t01.id_alert_template_module) as actions from tagente as t1 inner join tagente_modulo as t2 on t1.id_agente = t2.id_agente inner join talert_template_modules as t3 on t2.id_agente_modulo = t3.id_agent_module order by agent_name, module_name;' - WHERE id = 3; \ No newline at end of file + WHERE id = 3; + +-- --------------------------------------------------------------------- +-- Table `tmodule_relationship` +-- --------------------------------------------------------------------- +ALTER TABLE tmodule_relationship ADD COLUMN `id_server` varchar(100) NOT NULL DEFAULT ''; + diff --git a/pandora_console/pandoradb.sql b/pandora_console/pandoradb.sql index f28adb86ae..46978c9d3e 100644 --- a/pandora_console/pandoradb.sql +++ b/pandora_console/pandoradb.sql @@ -736,6 +736,7 @@ CREATE TABLE IF NOT EXISTS `trecon_task` ( CREATE TABLE IF NOT EXISTS `tmodule_relationship` ( `id` int(10) unsigned NOT NULL auto_increment, `id_rt` int(10) unsigned DEFAULT NULL, + `id_server` varchar(100) NOT NULL DEFAULT '', `module_a` int(10) unsigned NOT NULL, `module_b` int(10) unsigned NOT NULL, `disable_update` tinyint(1) unsigned NOT NULL default '0', diff --git a/pandora_server/lib/PandoraFMS/DataServer.pm b/pandora_server/lib/PandoraFMS/DataServer.pm index 7d483b61af..8098f5cd69 100644 --- a/pandora_server/lib/PandoraFMS/DataServer.pm +++ b/pandora_server/lib/PandoraFMS/DataServer.pm @@ -232,6 +232,8 @@ sub data_consumer ($$) { unlink ($file_name); if (defined($xml_data->{'server_name'})) { process_xml_server ($self->getConfig (), $file_name, $xml_data, $self->getDBH ()); + } elsif (defined($xml_data->{'connection_source'})) { + enterprise_hook('process_xml_connections', [$self->getConfig (), $file_name, $xml_data, $self->getDBH ()]); } else { process_xml_data ($self->getConfig (), $file_name, $xml_data, $self->getServerID (), $self->getDBH ()); } diff --git a/pandora_server/lib/PandoraFMS/ReconServer.pm b/pandora_server/lib/PandoraFMS/ReconServer.pm index d72ec5dda9..b91f1a39c2 100644 --- a/pandora_server/lib/PandoraFMS/ReconServer.pm +++ b/pandora_server/lib/PandoraFMS/ReconServer.pm @@ -37,6 +37,7 @@ use PandoraFMS::DB; use PandoraFMS::Core; use PandoraFMS::ProducerConsumerServer; use PandoraFMS::GIS qw(get_reverse_geoip_sql get_reverse_geoip_file get_random_close_point); +use Recon::Base; # Patched Nmap::Parser. See http://search.cpan.org/dist/Nmap-Parser/. use PandoraFMS::NmapParser; @@ -50,6 +51,11 @@ my %PendingTasks :shared; my $Sem :shared; my $TaskSem :shared; +# IDs from tconfig_os. +use constant OS_OTHER => 10; +use constant OS_ROUTER => 17; +use constant OS_SWITCH => 18; + ######################################################################################## # Recon Server class constructor. ######################################################################################## @@ -147,309 +153,32 @@ sub data_consumer ($$) { my $nmap_args = '-nsP -PE --max-retries '.$pa_config->{'icmp_checks'}.' --host-timeout '.$pa_config->{'networktimeout'}.'s -T'.$pa_config->{'recon_timing_template'}; my $np = new PandoraFMS::NmapParser; eval { - $np->parsescan($pa_config->{'nmap'}, $nmap_args, ($task->{'subnet'})); + my @subnets = split(/,/, $task->{'subnet'}); + my @communities = split(/,/, $task->{'snmp_community'}); + + my $recon = new Recon::Base( + communities => \@communities, + dbh => $dbh, + group_id => $task->{'id_group'}, + id_os => $task->{'id_os'}, + id_network_profile => $task->{'id_network_profile'}, + os_detection => $task->{'os_detect'}, + parent_detection => $task->{'parent_detection'}, + parent_recursion => $task->{'parent_recursion'}, + pa_config => $pa_config, + recon_ports => $task->{'recon_ports'}, + resolve_names => $task->{'resolve_names'}, + subnets => \@subnets, + task_id => $task->{'id_tr'}, + %{$pa_config} + ); + + $recon->scan(); }; if ($@) { update_recon_task ($dbh, $task_id, -1); return; } - - # Parse scanned hosts - my $module_hash; - my @up_hosts = $np->all_hosts ('up'); - my $total_up = scalar (@up_hosts); - my $progress = 0; - 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_up / 100))) eq '0E0'); - - # Resolve hostnames - my $host_name = undef; - if ($task->{'resolve_names'} == 1){ - $host_name = gethostbyaddr (inet_aton($addr), AF_INET); - } - $host_name = $addr unless defined ($host_name); - - # Does the host already exist? - my $agent = get_agent_from_addr ($dbh, $addr); - if (! defined ($agent)) { - $agent = get_agent_from_name ($dbh, $host_name); - } - - my $agent_id = defined ($agent) ? $agent->{'id_agente'} : 0; - if ($agent_id > 0) { - - # Skip if not in learning mode - next unless (($agent->{'modo'} == 1) || ($agent->{'modo'} == 2)); - } - - # Get the parent host - my $parent_id = 0; - if ($task->{'parent_detection'} == 1) { - $parent_id = get_host_parent ($pa_config, $addr, $dbh, $task->{'id_group'}, $task->{'parent_recursion'}, $task->{'resolve_names'}, $task->{'os_detect'}); - } - - # If the agent already exists update parent and continue - if ($agent_id > 0) { - if ($parent_id > 0) { - db_do ($dbh, 'UPDATE tagente SET id_parent = ? WHERE id_agente = ?', $parent_id, $agent_id ); - } - next; - } - - # Filter by TCP port - if ((defined ($task->{'recon_ports'})) && ($task->{'recon_ports'} ne "")) { - next unless (tcp_scan ($pa_config, $addr, $task->{'recon_ports'}) > 0); - } - - # Filter by OS - my $id_os = 11; # Network by default - if ($task->{'os_detect'} == 1){ - $id_os = guess_os ($pa_config, $dbh, $addr); - next if ($task->{'id_os'} > 0 && $task->{'id_os'} != $id_os); - } - - # GIS Code ----------------------------- - - # If GIS is activated try to geolocate the ip address of the agent - # and store also it's position. - - if($pa_config->{'activate_gis'} == 1 && $pa_config->{'recon_reverse_geolocation_mode'} !~ m/^disabled$/i) { - - # Try to get aproximated positional information for the Agent. - my $region_info = undef; - if ($pa_config->{'recon_reverse_geolocation_mode'} =~ m/^sql$/i) { - logger($pa_config, "Trying to get gis data of $addr from the SQL database", 8); - $region_info = get_reverse_geoip_sql($pa_config, $addr, $dbh); - } - elsif ($pa_config->{'recon_reverse_geolocation_mode'} =~ m/^file$/i) { - logger($pa_config, "Trying to get gis data of $addr from the file database", 8); - $region_info = get_reverse_geoip_file($pa_config, $addr); - } - else { - logger($pa_config, "ERROR:Trying to get gis data of $addr. Unknown source", 5); - } - if (defined($region_info)) { - my $location_description = ''; - if (defined($region_info->{'region'})) { - $location_description .= "$region_info->{'region'}, "; - } - if (defined($region_info->{'city'})) { - $location_description .= "$region_info->{'city'}, "; - } - if (defined($region_info->{'country_name'})) { - $location_description .= "($region_info->{'country_name'})"; - } - # We store a random offset in the coordinates to avoid all the agents apear on the same place. - my ($longitude, $latitude) = get_random_close_point ($pa_config, $region_info->{'longitude'}, $region_info->{'latitude'}); - - logger($pa_config, "Placing agent on random position (Lon,Lat) = ($longitude, $latitude)", 8); - # Crate a new agent adding the positional info (as is unknown we set 0 time_offset, and 0 altitude) - $agent_id = pandora_create_agent ($pa_config, $pa_config->{'servername'}, - $host_name, $addr, $task->{'id_group'}, - $parent_id, $id_os, '', 300, $dbh, 0, - $longitude, $latitude, 0, $location_description); - } - else { - logger($pa_config,"Id location of '$addr' for host '$host_name' NOT found", 3); - # Create a new agent - $agent_id = pandora_create_agent ($pa_config, $pa_config->{'servername'}, - $host_name, $addr, $task->{'id_group'}, - $parent_id, $id_os, '', 300, $dbh); - } - } - # End of GIS code ----------------------------- - else { - # Create a new agent - logger ($pa_config, "Creating an agent through recon task: " . $host_name, 10); - $agent_id = pandora_create_agent ($pa_config, $pa_config->{'servername'}, - $host_name, $addr, $task->{'id_group'}, - $parent_id, $id_os, '', 300, $dbh); - } - - # Check agent creation - if ($agent_id <= 0) { - logger($pa_config, "Error creating agent '$host_name'.", 3); - next; - } - - # Add the new address if it does not exist - my $addr_id = get_addr_id ($dbh, $addr); - $addr_id = add_address ($dbh, $addr) unless ($addr_id > 0); - if ($addr_id <= 0) { - logger($pa_config, "Could not add address '$addr' for host '$host_name'.", 3); - next; - } - - # Assign the new address to the agent - my $agent_addr_id = get_agent_addr_id ($dbh, $addr_id, $agent_id); - if ($agent_addr_id <= 0) { - db_do ($dbh, 'INSERT INTO taddress_agent (id_a, id_agent) - VALUES (?, ?)', $addr_id, $agent_id); - } - - # Create network profile modules for the agent - 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); - - $added_hosts .= "$addr "; - } - - # Create an incident with totals - 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_hosts"; - pandora_create_incident ($pa_config, $dbh, "[RECON] New hosts detected", $text, 0, 0, 'Pandora FMS Recon Server', $task->{'id_group'}); - } - - logger($pa_config, "Finished recon task for net " . $task->{'subnet'} . ".", 10); - - # Mark recon task as done - update_recon_task ($dbh, $task_id, -1); -} - - -########################################################################## -# Returns the ID of the parent of the given host if available. -########################################################################## -sub get_host_parent { - my ($pa_config, $host, $dbh, $group, $max_depth, $resolve, $os_detect) = @_; - - # Call nmap - my $nmap_args = '-nsP -PE --traceroute --max-retries '.$pa_config->{'icmp_checks'}.' --host-timeout '.$pa_config->{'networktimeout'}.'s -T'.$pa_config->{'nmap_timing_template'}; - my $np = new PandoraFMS::NmapParser; - eval { - $np->parsescan($pa_config->{'nmap'}, $nmap_args, ($host)); - }; - if ($@) { - return 0; - } - - # Get hops - my ($h) = $np->all_hosts (); - return 0 unless defined ($h); - my @all_hops = $h->all_trace_hops (); - my @hops; - - # Skip target host - pop (@all_hops); - - # Save the last max_depth hosts in reverse order - for (my $i = 0; $i < $max_depth; $i++) { - my $hop = pop (@all_hops); - last unless defined ($hop); - push (@hops, $hop); - } - - # Parse hops from first to last - my $parent_id = 0; - for (my $i = 0; $i < $max_depth; $i++) { - my $hop = pop (@hops); - last unless defined ($hop); - - # Get host information - my $host_addr = $hop->ipaddr (); - - # Check if the host exists - my $agent = get_agent_from_addr ($dbh, $host_addr); - if (defined ($agent)) { - # Move to the next host if module is not in learning mode - if ($agent->{'modo'} != 1) { - $parent_id = $agent->{'id_agente'}; - next; - } - } - - - # Add the new address if it does not exist - my $addr_id = get_addr_id ($dbh, $host_addr); - $addr_id = add_address ($dbh, $host_addr) unless ($addr_id > 0); - - # Should not happen - if ($addr_id <= 0) { - logger($pa_config, "Could not add address '$host_addr'", 1); - return 0; - } - - # Get the host's name - my $host_name = undef; - if ($resolve == 1){ - $host_name = gethostbyaddr(inet_aton($host_addr), AF_INET); - } - $host_name = $host_addr unless defined ($host_name); - - # Detect host's OS - my $id_os = 11; - if ($os_detect == 1) { - $id_os = guess_os ($pa_config, $dbh, $host_addr); - } - - # Create the host - my $agent_id = 0; - my $agent_parent = get_agent_from_addr ($dbh, $host_addr); - if (!defined($agent_parent)) { - $agent_parent = get_agent_from_name($dbh, $host_addr); - } - if (defined ($agent_parent)) { - $agent_id = $agent_parent->{'id_agente'}; - logger ($pa_config, "Updating agent " . $agent_id . " with parent $parent_id in host $host_addr"); - db_do ($dbh, 'UPDATE tagente SET id_parent=? WHERE id_agente=?', $parent_id, $agent_id); - } else { - $agent_id = pandora_create_agent ($pa_config, $pa_config->{'servername'}, $host_name, $host_addr, $group, $parent_id, $id_os, '', 300, $dbh); - db_do ($dbh, 'INSERT INTO taddress_agent (id_a, id_agent) - VALUES (?, ?)', $addr_id, $agent_id); - } - - # Move to the next host - $parent_id = $agent_id; - } - return $parent_id; -} - -############################################################################## -# TCP scan the given host/port. Returns 1 if successful, 0 otherwise. -############################################################################## -sub tcp_scan ($$$) { - my ($pa_config, $host, $portlist) = @_; - - my $nmap = $pa_config->{'nmap'}; - my $output = `"$nmap" -p$portlist $host | grep open | wc -l`; - return 0 if ($? != 0); - return $output; -} - -########################################################################## -# Guess OS using xprobe2. -########################################################################## -sub guess_os { - my ($pa_config, $dbh, $host) = @_; - - # Use xprobe2 if available - my $xprobe = $pa_config->{'xprobe2'}; - if (-e $xprobe){ - my $output = `$xprobe $host 2>$DEVNULL | grep 'Running OS' | head -1`; - return 10 if ($? != 0); - return pandora_get_os ($dbh, $output); - } - - # Use nmap by default - my $nmap = $pa_config->{'nmap'}; - my $output = `"$nmap" -F -O $host 2>$DEVNULL | grep 'Aggressive OS guesses'`; - return 10 if ($? != 0); - return pandora_get_os ($dbh, $output); } ########################################################################## @@ -461,33 +190,6 @@ sub update_recon_task ($$$) { db_do ($dbh, 'UPDATE trecon_task SET utimestamp = ?, status = ? WHERE id_rt = ?', time (), $status, $id_task); } -########################################################################## -# Create network profile modules for the given agent. -########################################################################## -sub create_network_profile_modules { - my ($pa_config, $dbh, $agent_id, $np_id, $addr, $snmp_community) = @_; - - return unless ($np_id > 0); - - # Get network components associated to the network profile - my @np_components = get_db_rows ($dbh, 'SELECT * FROM tnetwork_profile_component WHERE id_np = ?', $np_id); - - foreach my $np_component (@np_components) { - # Get network component data - my $component = get_db_single_row ($dbh, 'SELECT * FROM tnetwork_component WHERE id_nc = ?', $np_component->{'id_nc'}); - - if (! defined ($component)) { - logger($pa_config, "Network component ID " . $np_component->{'id_nc'} . " for agent $addr not found.", 3); - next; - } - - # Use snmp_community from network task instead the component snmp_community - $component->{'snmp_community'} = safe_output ($snmp_community); - - pandora_create_module_from_network_component($pa_config, $component, $agent_id, $dbh); - } -} - ########################################################################## # Executes recon scripts ########################################################################## @@ -540,5 +242,384 @@ sub exec_recon_script ($$$) { return 0; } +########################################################################## +# Guess the OS using xprobe2 or nmap. +########################################################################## +sub Recon::Base::guess_os($$) { + my ($self, $device) = @_; + + # OS detection disabled. Use the device type. + if ($self->{'os_detection'} == 0) { + my $device_type = $self->get_device_type($device); + return OS_ROUTER if ($device_type eq 'router'); + return OS_SWITCH if ($device_type eq 'switch'); + return OS_OTHER; + } + + # Use xprobe2 if available + if (-e $self->{pa_config}->{xprobe2}) { + my $output = `"$self->{pa_config}->{xprobe2}" $device 2>$DEVNULL | grep 'Running OS' | head -1`; + return OS_OTHER if ($? != 0); + return pandora_get_os($self->{'dbh'}, $output); + } + + # Use nmap by default + if (-e $self->{pa_config}->{nmap}) { + my $output = `"$self->{pa_config}->{nmap}" -F -O $device 2>$DEVNULL | grep 'Aggressive OS guesses'`; + return OS_OTHER if ($? != 0); + return pandora_get_os($self->{'dbh'}, $output); + } + + return OS_OTHER; +} + +############################################################################## +# Returns the number of open ports from the given list. +############################################################################## +sub Recon::Base::tcp_scan ($$) { + my ($self, $host) = @_; + + my $open_ports = `"$self->{pa_config}->{nmap}" -p$self->{recon_ports} $host | grep open | wc -l`; + return $open_ports; +} + +########################################################################## +# Create network profile modules for the given agent. +########################################################################## +sub Recon::Base::create_network_profile_modules($$$) { + my ($self, $agent_id, $device) = @_; + + return unless ($self->{'id_network_profile'} > 0); + + # Get network components associated to the network profile. + my @np_components = get_db_rows($self->{'dbh'}, 'SELECT * FROM tnetwork_profile_component WHERE id_np = ?', $self->{'id_network_profile'}); + foreach my $np_component (@np_components) { + + # Get network component data + my $component = get_db_single_row($self->{'dbh'}, 'SELECT * FROM tnetwork_component WHERE id_nc = ?', $np_component->{'id_nc'}); + if (!defined ($component)) { + $self->call('message', "Network component ID " . $np_component->{'id_nc'} . " not found.", 5); + next; + } + + # Use snmp_community from network task instead the component snmp_community + $component->{'snmp_community'} = safe_output($self->get_community($device)); + + pandora_create_module_from_network_component($self->{'pa_config'}, $component, $agent_id, $self->{'dbh'}); + } +} + +########################################################################## +# Connect the given devices in the Pandora FMS database. +########################################################################## +sub Recon::Base::connect_agents($$$$$) { + my ($self, $dev_1, $if_1, $dev_2, $if_2) = @_; + + # Check switch connectivy. + return if ($self->is_switch_connected($dev_1, $if_1, $dev_2, $if_2) == 1); + + # Get the agent for the first device. + my $agent_1 = get_agent_from_addr($self->{'dbh'}, $dev_1); + if (!defined($agent_1)) { + $agent_1 = get_agent_from_name($self->{'dbh'}, $dev_1); + } + return unless defined($agent_1); + + # Get the agent for the second device. + my $agent_2 = get_agent_from_addr($self->{'dbh'}, $dev_2); + if (!defined($agent_2)) { + $agent_2 = get_agent_from_name($self->{'dbh'}, $dev_2); + } + return unless defined($agent_2); + + # Check whether the modules exists. + my $module_name_1 = safe_input($if_1 eq '' ? 'ping' : "${if_1}_ifOperStatus"); + my $module_name_2 = safe_input($if_2 eq '' ? 'ping' : "${if_2}_ifOperStatus"); + my $module_id_1 = get_agent_module_id($self->{'dbh'}, $module_name_1, $agent_1->{'id_agente'}); + if ($module_id_1 <= 0) { + $self->call('message', "ERROR: Module " . safe_output($module_name_1) . " does not exist for agent $dev_1.", 5); + return; + } + my $module_id_2 = get_agent_module_id($self->{'dbh'}, $module_name_2, $agent_2->{'id_agente'}); + if ($module_id_2 <= 0) { + $self->call('message', "ERROR: Module " . safe_output($module_name_2) . " does not exist for agent $dev_2.", 5); + return; + } + + # Make sure the modules are not already connected. + if ($self->are_connected($dev_1, $if_1, $dev_2, $if_2)) { + $self->call('message', "Devices $dev_1 and $dev_2 are already connected.", 10); + return; + } + + # Connect the modules if they are not already connected. + my $connection_id = get_db_value($self->{'dbh'}, 'SELECT id FROM tmodule_relationship WHERE (module_a = ? AND module_b = ?) OR (module_b = ? AND module_a = ?)', $module_id_1, $module_id_2, $module_id_1, $module_id_2); + if (! defined($connection_id)) { + db_do($self->{'dbh'}, 'INSERT INTO tmodule_relationship (`module_a`, `module_b`, `id_rt`) VALUES(?, ?, ?)', $module_id_1, $module_id_2, $self->{'task_id'}); + } +} + +########################################################################## +# Create an agent for the given device. Returns the ID of the new (or +# existing) agent, undef on error. +########################################################################## +sub Recon::Base::create_agent($$) { + my ($self, $device) = @_; + + my @agents = get_db_rows($self->{'dbh'}, + 'SELECT * FROM taddress, taddress_agent, tagente + WHERE tagente.id_agente = taddress_agent.id_agent + AND taddress_agent.id_a = taddress.id_a + AND ip = ?', $device + ); + + # Does the host already exist? + my $agent; + foreach my $candidate (@agents) { + $agent = {map {$_} %$candidate}; # copy contents, do not use shallow copy + # exclude $device itself, because it handle corner case when target includes NAT + my @registered = map {$_->{ip}} get_db_rows($self->{'dbh'}, + 'SELECT ip FROM taddress, taddress_agent, tagente + WHERE tagente.id_agente = taddress_agent.id_agent + AND taddress_agent.id_a = taddress.id_a + AND tagente.id_agente = ? + AND taddress.ip != ?', $agent->{id_agente}, $device + ); + foreach my $ip_addr (@registered) { + my @matched = grep { $_ =~ /^$ip_addr$/ } $self->get_addresses($device); + if (scalar(@matched) == 0) { + $agent = undef; + last; + } + } + last if(defined($agent)); # exit loop if match all ip_addr + } + + if (!defined($agent)) { + $agent = get_agent_from_name($self->{'dbh'}, $device); + } + + my ($agent_id, $agent_learning); + if (!defined($agent)) { + + # Resolve hostnames. + my $host_name = $self->{'resolve_names'} == 1 ? gethostbyaddr (inet_aton($device), AF_INET) : $device; + $host_name = $device unless defined ($host_name); + + # Guess the OS. + my $id_os = $self->guess_os($device); + + # Are we filtering hosts by OS? + return if ($self->{'id_os'} > 0 && $id_os != $self->{'id_os'}); + + # Are we filtering hosts by TCP port? + return if ($self->{'recon_ports'} ne '' && $self->tcp_scan($device) == 0); + + $agent_id = pandora_create_agent($self->{'pa_config'}, $self->{'pa_config'}->{'servername'}, $host_name, $device, $self->{'group_id'}, 0, $id_os, '', 300, $self->{'dbh'}); + return undef unless defined ($agent_id) and ($agent_id > 0); + pandora_event($self->{'pa_config'}, "[RECON] New " . $self->get_device_type($device) . " found (" . join(',', $self->get_addresses($device)) . ").", $self->{'group_id'}, $agent_id, 2, 0, 0, 'recon_host_detected', 0, $self->{'dbh'}); + $agent_learning = 1; + + # Create network profile modules for the agent + $self->create_network_profile_modules($agent_id, $device); + } + else { + $agent_id = $agent->{'id_agente'}; + $agent_learning = $agent->{'modo'}; + } + + # Do not create any modules if the agent is not in learning mode. + return unless ($agent_learning == 1); + + # Add found IP addresses to the agent. + foreach my $ip_addr ($self->get_addresses($device)) { + my $addr_id = get_addr_id($self->{'dbh'}, $ip_addr); + $addr_id = add_address($self->{'dbh'}, $ip_addr) unless ($addr_id > 0); + next unless ($addr_id > 0); + + # Assign the new address to the agent + my $agent_addr_id = get_agent_addr_id($self->{'dbh'}, $addr_id, $agent_id); + if ($agent_addr_id <= 0) { + db_do($self->{'dbh'}, 'INSERT INTO taddress_agent (`id_a`, `id_agent`) + VALUES (?, ?)', $addr_id, $agent_id); + } + } + + # Create a ping module. + my $module_id = get_agent_module_id($self->{'dbh'}, "ping", $agent_id); + if ($module_id <= 0) { + my %module = ('id_tipo_modulo' => 6, + 'id_modulo' => 2, + 'nombre' => "ping", + 'descripcion' => '', + 'id_agente' => $agent_id, + 'ip_target' => $device); + pandora_create_module_from_hash ($self->{'pa_config'}, \%module, $self->{'dbh'}); + } + + # Add interfaces to the agent if it responds to SNMP. + my $community = $self->get_community($device); + return $agent_id unless defined($community); + + my @output = $self->snmp_get_value_array($device, $Recon::Base::IFINDEX); + foreach my $if_index (@output) { + next unless ($if_index =~ /^[0-9]+$/); + + # Check the status of the interface. + if ($self->{'all_ifaces'} == 0) { + my $if_status = $self->snmp_get_value($device, "$Recon::Base::IFOPERSTATUS.$if_index"); + next unless $if_status == 1; + } + + # Fill the module description with the IP and MAC addresses. + my $mac = $self->get_if_mac($device, $if_index); + my $ip = $self->get_if_ip($device, $if_index); + my $if_desc = ($mac ne '' ? "MAC $mac " : '') . ($ip ne '' ? "IP $ip" : ''); + + # Get the name of the network interface. + my $if_name = $self->snmp_get_value($device, "$Recon::Base::IFNAME.$if_index"); + $if_name = "if$if_index" unless defined ($if_name); + $if_name =~ s/"//g; + + # Check whether the module already exists. + my $module_id = get_agent_module_id($self->{'dbh'}, "${if_name}_ifOperStatus", $agent_id); + next if ($module_id > 0 && !$agent_learning); + + # Encode problematic characters. + $if_name = safe_input($if_name); + $if_desc = safe_input($if_desc); + + # Interface status module. + $module_id = get_agent_module_id($self->{'dbh'}, "${if_name}_ifOperStatus", $agent_id); + if ($module_id <= 0) { + my %module = ('id_tipo_modulo' => 18, + 'id_modulo' => 2, + 'nombre' => "${if_name}_ifOperStatus", + 'descripcion' => $if_desc, + 'id_agente' => $agent_id, + 'ip_target' => $device, + 'tcp_send' => 1, + 'snmp_community' => $community, + 'snmp_oid' => "$Recon::Base::IFOPERSTATUS.$if_index" + ); + pandora_create_module_from_hash ($self->{'pa_config'}, \%module, $self->{'dbh'}); + } else { + my %module = ( + 'descripcion' => $if_desc, + 'ip_target' => $device, + 'snmp_community' => $community, + ); + pandora_update_module_from_hash ($self->{'pa_config'}, \%module, 'id_agente_modulo', $module_id, $self->{'dbh'}); + } + + # Incoming traffic module. + $module_id = get_agent_module_id($self->{'dbh'}, "${if_name}_ifInOctets", $agent_id); + if ($module_id <= 0) { + my %module = ('id_tipo_modulo' => 16, + 'id_modulo' => 2, + 'nombre' => "${if_name}_ifInOctets", + 'descripcion' => 'The total number of octets received on the interface, including framing characters.', + 'id_agente' => $agent_id, + 'ip_target' => $device, + 'tcp_send' => 1, + 'snmp_community' => $community, + 'snmp_oid' => "$Recon::Base::IFINOCTECTS.$if_index"); + pandora_create_module_from_hash ($self->{'pa_config'}, \%module, $self->{'dbh'}); + } else { + my %module = ( + 'ip_target' => $device, + 'snmp_community' => $community, + ); + pandora_update_module_from_hash ($self->{'pa_config'}, \%module, 'id_agente_modulo', $module_id, $self->{'dbh'}); + } + + # Outgoing traffic module. + $module_id = get_agent_module_id($self->{'dbh'}, "${if_name}_ifOutOctets", $agent_id); + if ($module_id <= 0) { + my %module = ('id_tipo_modulo' => 16, + 'id_modulo' => 2, + 'nombre' => "${if_name}_ifOutOctets", + 'descripcion' => 'The total number of octets received on the interface, including framing characters.', + 'id_agente' => $agent_id, + 'ip_target' => $device, + 'tcp_send' => 1, + 'snmp_community' => $community, + 'snmp_oid' => "$Recon::Base::IFOUTOCTECTS.$if_index"); + pandora_create_module_from_hash ($self->{'pa_config'}, \%module, $self->{'dbh'}); + } else { + my %module = ( + 'ip_target' => $device, + 'snmp_community' => $community, + ); + pandora_update_module_from_hash ($self->{'pa_config'}, \%module, 'id_agente_modulo', $module_id, $self->{'dbh'}); + } + } + + return $agent_id; +} + +########################################################################## +# Delete already existing connections. +########################################################################## +sub Recon::Base::delete_connections($) { + my ($self) = @_; + + $self->call('message', "Deleting connections...", 10); + db_do($self->{'dbh'}, 'DELETE FROM tmodule_relationship WHERE id_rt=?', $self->{'task_id'}); +} + +####################################################################### +# Print log messages. +####################################################################### +sub Recon::Base::message($$$) { + my ($self, $message, $verbosity) = @_; + + logger($self->{'pa_config'}, "[Recon task " . $self->{'task_id'} . "] $message", $verbosity); +} + +########################################################################## +# Connect the given hosts to its parent. +########################################################################## +sub Recon::Base::set_parent($$$) { + my ($self, $host, $parent) = @_; + + return unless ($self->{'parent_detection'} == 1); + + # Get the agent for the host. + my $agent = get_agent_from_addr($self->{'dbh'}, $host); + if (!defined($agent)) { + $agent = get_agent_from_name($self->{'dbh'}, $host); + } + return unless defined($agent); + + # Check if the parent agent exists. + my $agent_parent = get_agent_from_addr($self->{'dbh'}, $parent); + if (!defined($agent_parent)) { + $agent_parent = get_agent_from_name($self->{'dbh'}, $parent); + } + + my $parent_id; + if (defined ($agent_parent)) { + $parent_id = $agent_parent->{'id_agente'}; + return unless ($agent_parent->{'modo'} == 1); + } else { + $parent_id = $self->call('create_agent', $parent); + } + + # Connect the host to its parent. + if ($parent_id > 0) { + db_do($self->{'dbh'}, 'UPDATE tagente SET id_parent=? WHERE id_agente=?', $parent_id, $agent->{'id_agente'}); + } +} + +########################################################################## +# Update recon task status. +########################################################################## +sub Recon::Base::update_progress ($$) { + my ($self, $progress) = @_; + + db_do ($self->{'dbh'}, 'UPDATE trecon_task SET utimestamp = ?, status = ? WHERE id_rt = ?', time (), $progress, $self->{'task_id'}); +} + 1; __END__ diff --git a/pandora_server/lib/Recon/Base.pm b/pandora_server/lib/Recon/Base.pm new file mode 100644 index 0000000000..f50f957c4b --- /dev/null +++ b/pandora_server/lib/Recon/Base.pm @@ -0,0 +1,1344 @@ +#!/usr/bin/perl +# (c) Ártica ST 2014 +# Module for network topology discovery. + +package Recon::Base; +use strict; +use warnings; + +# Default lib dir for RPM and DEB packages +use lib '/usr/lib/perl5'; + +use NetAddr::IP; +use POSIX qw/ceil/; +use Recon::NmapParser; +use Recon::Util; +use Socket qw/inet_aton/; + +# Some useful OIDs. +our $DOT1DBASEBRIDGEADDRESS = ".1.3.6.1.2.1.17.1.1.0"; +our $DOT1DBASEPORTIFINDEX = ".1.3.6.1.2.1.17.1.4.1.2"; +our $DOT1DTPFDBADDRESS = ".1.3.6.1.2.1.17.4.3.1.1"; +our $DOT1DTPFDBPORT = ".1.3.6.1.2.1.17.4.3.1.2"; +our $IFDESC = ".1.3.6.1.2.1.2.2.1.2"; +our $IFINDEX = ".1.3.6.1.2.1.2.2.1.1"; +our $IFINOCTECTS = ".1.3.6.1.2.1.2.2.1.10"; +our $IFOPERSTATUS = ".1.3.6.1.2.1.2.2.1.8"; +our $IFOUTOCTECTS = ".1.3.6.1.2.1.2.2.1.16"; +our $IPENTADDR = ".1.3.6.1.2.1.4.20.1.1"; +our $IFNAME = ".1.3.6.1.2.1.31.1.1.1.1"; +our $IFPHYSADDRESS = ".1.3.6.1.2.1.2.2.1.6"; +our $IPNETTOMEDIAPHYSADDRESS = ".1.3.6.1.2.1.4.22.1.2"; +our $IPADENTIFINDEX = ".1.3.6.1.2.1.4.20.1.2"; +our $IPROUTEIFINDEX = ".1.3.6.1.2.1.4.21.1.2"; +our $IPROUTENEXTHOP = ".1.3.6.1.2.1.4.21.1.7"; +our $IPROUTETYPE = ".1.3.6.1.2.1.4.21.1.8"; +our $PRTMARKERINDEX = ".1.3.6.1.2.1.43.10.2.1.1"; +our $SYSDESCR = ".1.3.6.1.2.1.1.1"; +our $SYSSERVICES = ".1.3.6.1.2.1.1.7"; +our $SYSUPTIME = ".1.3.6.1.2.1.1.3"; +our $VTPVLANIFINDEX = ".1.3.6.1.4.1.9.9.46.1.3.1.1.18.1"; + +our @ISA = ("Exporter"); +our %EXPORT_TAGS = ( 'all' => [ qw( ) ] ); +our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); +our @EXPORT = qw( + $DOT1DBASEBRIDGEADDRESS + $DOT1DBASEPORTIFINDEX + $DOT1DTPFDBADDRESS + $DOT1DTPFDBPORT + $IFDESC + $IFINDEX + $IFINOCTECTS + $IFOPERSTATUS + $IFOUTOCTECTS + $IPADENTIFINDEX + $IPENTADDR + $IFNAME + $IPNETTOMEDIAPHYSADDRESS + $IFPHYSADDRESS + $IPADENTIFINDEX + $IPROUTEIFINDEX + $IPROUTENEXTHOP + $IPROUTETYPE + $PRTMARKERINDEX + $SYSDESCR + $SYSSERVICES + $SYSUPTIME +); + +####################################################################### +# Create a new ReconTask object. +####################################################################### +sub new { + my $class = shift; + + my $self = { + + # Known aliases (multiple IP addresses for the same host. + aliases => {}, + + # Keep our own ARP cache to connect hosts to switches/routers. + arp_cache => {}, + + # Working SNMP community for each device. + community_cache => {}, + + # Connections between devices. + connections => {}, + + # Devices by type. + hosts => [], + routers => [], + switches => [], + + # Found MAC addresses. + mac => {}, + + # Route cache. + routes => [], + default_gw => undef, + + # SNMP query cache. + snmp_cache => {}, + + # Switch to switch connections. Used to properly connect hosts + # that are connected to a switch wich is in turn connected to another switch, + # since the hosts will show up in the latter's switch AFT too. + switch_to_switch => {}, + + # Visited devices (initially empty). + visited_devices => {}, + + # Per device VLAN cache. + vlan_cache => {}, + + # Configuration parameters. + all_ifaces => 0, + communities => [], + icmp_checks => 2, + icmp_timeout => 2, + id_os => 0, + id_network_profile => 0, + nmap => '/usr/bin/nmap', + parent_detection => 1, + parent_recursion => 5, + os_detection => 0, + recon_timing_template => 3, + recon_ports => '', + resolve_names => 0, + snmp_checks => 2, + snmp_timeout => 2, + subnets => [], + @_, + + }; + + # Perform some sanity checks. + die("No subnet was specified.") unless defined($self->{'subnets'}); + + return bless($self, $class); +} + +######################################################################################## +# Add an address to a device. +######################################################################################## +sub add_addresses($$$) { + my ($self, $device, $ip_address) = @_; + + $self->{'visited_devices'}->{$device}->{'addr'}->{$ip_address} = ''; +} + +######################################################################################## +# Add a MAC/IP address to the ARP cache. +######################################################################################## +sub add_mac($$$) { + my ($self, $mac, $ip_addr) = @_; + + $self->{'arp_cache'}->{$mac} = $ip_addr; +} + +######################################################################################## +# Return 1 if the given devices are connected to each other, 0 otherwise. +######################################################################################## +sub are_connected($$$$$) { + my ($self, $dev_1, $if_1, $dev_2, $if_2) = @_; + + # Check for aliases! + $dev_1 = $self->{'aliases'}->{$dev_1} if defined($self->{'aliases'}->{$dev_1}); + $dev_2 = $self->{'aliases'}->{$dev_2} if defined($self->{'aliases'}->{$dev_2}); + + # Use ping modules when interfaces are unknown. + $if_1 = "ping" if $if_1 eq ''; + $if_2 = "ping" if $if_2 eq ''; + + if (defined($self->{'connections'}->{"${dev_1}\t${if_1}\t${dev_2}\t{$if_2}"}) || + defined($self->{'connections'}->{"${dev_2}\t${if_2}\t${dev_1}\t{$if_1}"})) { + return 1; + } + + return 0; +} + +######################################################################################## +# Find devices using ARP caches. +######################################################################################## +sub arp_cache_discovery($$) { + my ($self, $device) = @_; + + # The device has already been visited. + return if ($self->is_visited($device)); + + # The device does not belong to one of the scanned sub-nets. + return if ($self->in_subnet($device) == 0); + + # Mark the device as visited. + $self->mark_visited($device); + + # Check if the device responds to SNMP. + if ($self->snmp_responds($device)) { + + # Fill the VLAN cache. + $self->find_vlans($device); + + # Guess device type. + $self->guess_device_type($device); + + # Find aliases for the device. + $self->find_aliases($device); + + # Get ARP cache. + my @output = $self->snmp_get($device, $IPNETTOMEDIAPHYSADDRESS); + foreach my $line (@output) { + next unless ($line =~ /^$IPNETTOMEDIAPHYSADDRESS.\d+.(\S+)\s+=\s+\S+:\s+(.*)$/); + my ($ip_addr, $mac_addr) = ($1, $2); + + # Skip broadcast, net and local addresses. + next if ($ip_addr =~ m/\.255$|\.0$|127\.0\.0\.1$/); + + # Save the MAC to connect hosts to switches/routers. + $mac_addr = parse_mac($mac_addr); + $self->add_mac($mac_addr, $ip_addr); + + # Recursively visit found devices. + $self->arp_cache_discovery($ip_addr); + } + } + + # Separate devices by type to find device connectivity later. + my $device_type = $self->get_device_type($device); + if ($device_type eq 'host' || $device_type eq 'printer') { + + # Hosts are indexed to help find router/switch to host connectivity. + push(@{$self->{'hosts'}}, $device); + } + elsif ($device_type eq 'switch') { + push(@{$self->{'switches'}}, $device); + } + elsif ($device_type eq 'router') { + push(@{$self->{'routers'}}, $device); + } + else { + $self->call('message', "Unknown device type: $device_type", 5); + } + + # Create an agent for the device. + $self->call('create_agent', $device); +} + +####################################################################### +# Try to call the given function on the given object. +####################################################################### +sub call { + my $self = shift; + my $func = shift; + my @params = @_; + + if ($self->can($func)) { + $self->$func(@params); + } +} + +########################################################################## +# Connect the given hosts to its gateway. +########################################################################## +sub gateway_connectivity($$) { + my ($self, $host) = @_; + + my $gw = $self->get_gateway($host); + return unless defined($gw); + + # Check for aliases! + $host = $self->{'aliases'}->{$host} if defined($self->{'aliases'}->{$host}); + $gw = $self->{'aliases'}->{$gw} if defined($self->{'aliases'}->{$gw}); + + # Same host, different IP addresses. + return if ($host eq $gw); + + $self->call('set_parent', $host, $gw); + $self->call('message', "Host $host is reached via gateway $gw.", 5); + $self->mark_connected($host); +} + +######################################################################################## +# Find IP address aliases for the given device. +######################################################################################## +sub find_aliases($$) { + my ($self, $device) = @_; + + # Get ARP cache. + my @ip_addresses = $self->snmp_get_value_array($device, $IPENTADDR); + foreach my $ip_address (@ip_addresses) { + + # Skip broadcast and localhost addresses. + next if ($ip_address =~ m/\.255$|\.0$|127\.0\.0\.1$/); + + # Sometimes we find the same IP address we had. + next if ($ip_address eq $device); + + $self->add_addresses($device, $ip_address); + push(@{$self->{'hosts'}}, $ip_address); + + $self->call('message', "Found address $ip_address for host $device.", 5); + + # Link the two addresses. + $self->{'aliases'}->{$ip_address} = $device; + } +} + +######################################################################################## +# Find the device's VLANs and fill the VLAN cache. +######################################################################################## +sub find_vlans ($$) { + my ($self, $device) = @_; + my %vlan_hash; + + foreach my $vlan ($self->snmp_get_value_array($device, $VTPVLANIFINDEX)) { + next if $vlan eq '0'; + $vlan_hash{$vlan} = 1; + } + my @vlans = keys(%vlan_hash); + + $self->{'vlan_cache'}->{$device} = []; + push(@{$self->{'vlan_cache'}->{$device}}, @vlans) if (scalar(@vlans) > 0); +} + +######################################################################################## +# Return the addresses of the given device as an array. +######################################################################################## +sub get_addresses($$) { + my ($self, $device) = @_; + + if (defined($self->{'visited_devices'}->{$device})) { + return keys(%{$self->{'visited_devices'}->{$device}->{'addr'}}); + } + + # By default return the given address. + return ($device); +} + +######################################################################################## +# Return a device structure from an IP address. +######################################################################################## +sub get_device($$) { + my ($self, $addr) = @_; + + if (defined($self->{'visited_devices'}->{$addr})) { + return $self->{'visited_devices'}->{$addr}; + } + + return undef; +} + +######################################################################################## +# Return all known hosts, switches and routers. +######################################################################################## +sub get_all_devices($) { + my ($self) = @_; + + return (@{$self->{'hosts'}}, @{$self->{'switches'}}, @{$self->{'routers'}}); +} + +######################################################################################## +# Get the SNMP community of the given device. Returns undef if no community was found. +######################################################################################## +sub get_community($$) { + my ($self, $device) = @_; + + if (defined($self->{'community_cache'}->{$device})) { + return $self->{'community_cache'}->{$device}; + } + + return undef; +} + +######################################################################################## +# Return the connection hash. +######################################################################################## +sub get_connections($) { + my ($self) = @_; + + return $self->{'connections'}; +} + +######################################################################################## +# Get the type of the given device. +######################################################################################## +sub get_device_type($$) { + my ($self, $device) = @_; + + if (defined($self->{'visited_devices'}->{$device})) { + return $self->{'visited_devices'}->{$device}->{'type'}; + } + + # Assume 'host' by default. + return 'host'; +} + +######################################################################################## +# Return all known hosts that are not switches or routers. +######################################################################################## +sub get_hosts($) { + my ($self) = @_; + + return $self->{'hosts'}; +} + +######################################################################################## +# Get an interface name from an AFT entry. Returns undef on error. +######################################################################################## +sub get_if_from_aft($$$) { + my ($self, $switch, $mac) = @_; + + # Get the port associated to the MAC. + my $port = $self->snmp_get_value($switch, "$DOT1DTPFDBPORT." . mac_to_dec($mac)); + return '' unless defined($port); + + # Get the interface index associated to the port. + my $if_index = $self->snmp_get_value($switch, "$DOT1DBASEPORTIFINDEX.$port"); + return '' unless defined($if_index); + + # Get the interface name. + my $if_name = $self->snmp_get_value($switch, "$IFNAME.$if_index"); + return "if$if_index" unless defined($if_name); + + $if_name =~ s/"//g; + return $if_name; + +} + +######################################################################################## +# Get an interface name from an IP address. +######################################################################################## +sub get_if_from_ip($$$) { + my ($self, $device, $ip_addr) = @_; + + # Get the port associated to the IP address. + my $if_index = $self->snmp_get_value($device, "$IPROUTEIFINDEX.$ip_addr"); + return '' unless defined ($if_index); + + # Get the name of the interface associated to the port. + my $if_name = $self->snmp_get_value($device, "$IFNAME.$if_index"); + return '' unless defined ($if_name); + + $if_name =~ s/"//g; + return $if_name; +} + +######################################################################################## +# Get an interface name from a MAC address. +######################################################################################## +sub get_if_from_mac($$$) { + my ($self, $device, $mac) = @_; + + # Get the port associated to the IP address. + my @output = $self->snmp_get($device, $IFPHYSADDRESS); + foreach my $line (@output) { + chomp($line); + next unless $line =~ /^IFPHYSADDRESS.(\S+)\s+=\s+\S+:\s+(.*)$/; + my ($if_index, $if_mac) = ($1, $2); + + # Make sure the MAC addresses match. + next unless (mac_matches($mac, $if_mac) == 1); + + # Pupulate the ARP cache. + $self->add_mac($mac, $device); + + # Get the name of the interface associated to the port. + my $if_name = $self->snmp_get_value($device, "$IFNAME.$if_index"); + return '' unless defined ($if_name); + + $if_name =~ s/"//g; + return $if_name; + } + + return ''; +} + +######################################################################################## +# Returns the IP address of the given interface (by index). +######################################################################################## +sub get_if_ip($$$) { + my ($self, $device, $if_index) = @_; + + my @output = $self->snmp_get($device, $IPADENTIFINDEX); + foreach my $line (@output) { + chomp ($line); + return $1 if ($line =~ m/^IPADENTIFINDEX.(\S+)\s+=\s+\S+:\s+$if_index$/); + } + + return ''; +} + +######################################################################################## +# Returns the MAC address of the given interface (by index). +######################################################################################## +sub get_if_mac($$$) { + my ($self, $device, $if_index) = @_; + + my $mac = $self->snmp_get_value($device, "$IFPHYSADDRESS.$if_index"); + return '' unless defined($mac); + + # Clean-up the MAC address. + $mac = parse_mac($mac); + + return $mac; +} + +######################################################################################## +# Get an IP address from the ARP cache given the MAC address. +######################################################################################## +sub get_ip_from_mac($$) { + my ($self, $mac_addr) = @_; + + if (defined($self->{'arp_cache'}->{$mac_addr})) { + return $self->{'arp_cache'}->{$mac_addr}; + } + + return undef; +} + +######################################################################################## +# Return all known routers. +######################################################################################## +sub get_routers($) { + my ($self) = @_; + + return $self->{'routers'}; +} + +######################################################################################## +# Fill the route cache. +######################################################################################## +sub get_routes($) { + my ($self) = @_; + + # Empty the current route cache. + $self->{'routes'} = []; + + # Parse route's output. + my @output = `route -n 2>/dev/null`; + foreach my $line (@output) { + chomp($line); + if ($line =~ /^0\.0\.0\.0\s+(\d+\.\d+\.\d+\.\d+).*/) { + $self->{'default_gw'} = $1; + } elsif ($line =~ /^(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+).*/) { + push(@{$self->{'routes'}}, { dest => $1, gw => $2, mask => $3 }); + } + } + + # Replace 0.0.0.0 with the default gateway's IP. + return unless defined ($self->{'default_gw'}); + foreach my $route (@{$self->{'routes'}}) { + $route->{gw} = $self->{'default_gw'} if ($route->{'gw'} eq '0.0.0.0'); + } +} + +######################################################################################## +# Get the gateway to reach the given host. +######################################################################################## +sub get_gateway($) { + my ($self, $host) = @_; + + # Look for a specific route to the given host. + foreach my $route (@{$self->{'routes'}}) { + if (subnet_matches($host, $route->{'dest'}, $route->{'mask'})) { + return $route->{'gw'}; + } + } + + # Return the default gateway. + return $self->{'default_gw'} if defined($self->{'default_gw'}); + + # Ops! + return undef; +} + +######################################################################################## +# Return a pointer to an array containing configured subnets. +######################################################################################## +sub get_subnets($) { + my ($self) = @_; + + return $self->{'subnets'}; +} + +######################################################################################## +# Return all known switches. +######################################################################################## +sub get_switches($) { + my ($self) = @_; + + # No subnets specified. + return $self->{'switches'}; +} + +######################################################################################## +# Get an array of all the visited devices. +# NOTE: This functions returns the whole device structures, not just address +# like get_hosts, get_switches, get_routers and get_all_devices. +######################################################################################## +sub get_visited_devices($) { + my ($self) = @_; + + return $self->{'visited_devices'}; +} + +######################################################################################## +# Returns an array of found VLAN IDs. +######################################################################################## +sub get_vlans($$) { + my ($self, $device) = @_; + + return () unless defined($self->{'vlan_cache'}->{$device}); + + return @{$self->{'vlan_cache'}->{$device}}; +} + +######################################################################################## +# Guess the type of the given device. +######################################################################################## +sub guess_device_type($$) { + my ($self, $device) = @_; + + # Get the value of sysServices. + my $services = $self->snmp_get_value($device, "$SYSSERVICES.0"); + return unless defined($services); + + # Check the individual bits. + my @service_bits = split('', unpack('b8', pack('C', $services))); + + # Check for layer 2 connectivity support. + my $bridge_mib = $self->snmp_get_value($device, $DOT1DBASEBRIDGEADDRESS); + + # L2? + my $device_type; + if ($service_bits[1] == 1) { + # L3? + if ($service_bits[2] == 1) { + # Bridge MIB? + if (defined($bridge_mib)) { + $device_type = 'switch'; + } else { + # L7? + if ($service_bits[6] == 1) { + $device_type = 'host'; + } else { + $device_type = 'router'; + } + } + } + else { + # Bridge MIB? + if (defined($bridge_mib)) { + $device_type = 'switch'; + } else { + $device_type = 'host'; + } + } + } + else { + # L3? + if ($service_bits[2] == 1) { + # L4? + if ($service_bits[3] == 1) { + $device_type = 'switch'; + } else { + # L7? + if ($service_bits[6] == 1) { + $device_type = 'host'; + } else { + $device_type = 'router'; + } + } + } + else { + # Printer MIB? + my $printer_mib = $self->snmp_get_value($device, $PRTMARKERINDEX); + if (defined($printer_mib)) { + $device_type = 'printer'; + } else { + $device_type = 'host'; + } + } + } + + # Set the type of the device. + $self->set_device_type($device, $device_type); +} + +######################################################################################## +# Discover host connectivity. +######################################################################################## +sub host_connectivity($$) { + my ($self, $device) = @_; + + return unless defined($self->get_community($device)); + + # Get the device type. + my $device_type = $self->get_device_type($device); + + # Get the address forwarding table (AFT) of the device. + my @aft = $self->snmp_get_value_array($device, $DOT1DTPFDBADDRESS); + foreach my $mac (@aft) { + $mac = parse_mac($mac); + + # We need an IP address. + my $host = $self->get_ip_from_mac($mac); + next unless defined($host); + + # Did we scan that IP address? + next unless $self->is_visited($host); + + my $device_if_name = $self->get_if_from_aft($device, $mac); + next unless ($device_if_name ne ''); + + # We may or may not be able to get the host's interface via SNMP. + my $host_if_name = $self->get_if_from_mac($host, $mac); + + # Are the devices already connected? + next if ($self->are_connected($device, $device_if_name, $host, $host_if_name)); + + if ($device_type eq 'router') { + next if ($self->is_switch_connected($device, $device_if_name)); # The router is probably connected to a switch. + $self->call('message', "Host $host" . ($host_if_name ne '' ? " (if $host_if_name)" : '') . " is connected to router $device (if $device_if_name).", 5); + } + elsif ($device_type eq 'switch') { + next if ($self->is_switch_connected($device, $device_if_name)); # The switch is probably connected to another switch. + $self->call('message', "Host $host" . ($host_if_name ne '' ? " (if $host_if_name)" : '') . " is connected to switch $device (if $device_if_name).", 5); + } + else { + $self->call('message', "Host $host" . ($host_if_name ne '' ? " (if $host_if_name)" : '') . " is connected to host $device (if $device_if_name).", 5); + } + + # Connect the two devices. + $self->call('connect_agents', $device, $device_if_name, $host, $host_if_name); + $self->mark_connected($device, $device_if_name, $host, $host_if_name); + } +} + +######################################################################################## +# Returns 1 if the device belongs to one of the scanned subnets. +######################################################################################## +sub in_subnet($$) { + my ($self, $device) = @_; + $device = ip_to_long($device); + + # No subnets specified. + return 1 if (scalar(@{$self->{'subnets'}}) <= 0); + + foreach my $subnet (@{$self->{'subnets'}}) { + if (subnet_matches($device, $subnet)) { + return 1; + } + } + + return 0; +} + +######################################################################################## +# Return 1 if the given device is connected, 0 otherwise. +######################################################################################## +sub is_connected($$) { + my ($self, $device) = @_; + + # Check for aliases! + $device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device}); + + if ($self->{'visited_devices'}->{$device}->{'connected'} == 1) { + return 1; + } + + return 0; +} + +########################################################################## +# Check for switches that are connected to other switches/routers and show +# up in a switch/router's port. +########################################################################## +sub is_switch_connected($$$) { + my ($self, $device, $iface) = @_; + + # Check for aliases! + $device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device}); + + return 1 if defined ($self->{'switch_to_switch'}->{"${device}\t${iface}"}); + + return 0; +} + +######################################################################################## +# Returns 1 if the given device has already been visited, 0 otherwise. +######################################################################################## +sub is_visited($$) { + my ($self, $device) = @_; + + # Check for aliases! + $device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device}); + + if (defined($self->{'visited_devices'}->{$device})) { + return 1; + } + + return 0; +} + +######################################################################################## +# Mark the given devices as connected to each other on the given interfaces. +######################################################################################## +sub mark_connected($$;$$$) { + my ($self, $dev_1, $if_1, $dev_2, $if_2) = @_; + + # Check for aliases! + $dev_1 = $self->{'aliases'}->{$dev_1} if defined($self->{'aliases'}->{$dev_1}); + $self->{'visited_devices'}->{$dev_1}->{'connected'} = 1; + + if (defined($if_2)) { + # Use ping modules when interfaces are unknown. + $if_1 = "ping" if $if_1 eq ''; + $if_2 = "ping" if $if_2 eq ''; + + $dev_2 = $self->{'aliases'}->{$dev_2} if defined($self->{'aliases'}->{$dev_2}); + $self->{'connections'}->{"${dev_1}\t${if_1}\t${dev_2}\t${if_2}"} = 1; + $self->{'visited_devices'}->{$dev_2}->{'connected'} = 1; + + $self->call('set_parent', $dev_2, $dev_1); + } +} + +######################################################################################## +# Mark the given switch as having a connection on the given interface. +######################################################################################## +sub mark_switch_connected($$$) { + my ($self, $device, $iface) = @_; + + # Check for aliases! + $device = $self->{'aliases'}->{$device} if defined($self->{'aliases'}->{$device}); + $self->{'switch_to_switch'}->{"${device}\t${iface}"} = 1; +} + +######################################################################################## +# Mark the given device as visited. +######################################################################################## +sub mark_visited($$) { + my ($self, $device) = @_; + + $self->{'visited_devices'}->{$device} = { 'addr' => { $device => '' }, + 'connected' => 0, + 'type' => 'host' }; +} + +######################################################################################## +# Looks for a working SNMP community for the given device. Returns 1 if one is +# found, 0 otherwise. Updates the SNMP community cache. +######################################################################################## +sub snmp_responds($$) { + my ($self, $device) = @_; + + # We already have a working SNMP community for this device. + return 1 if (defined($self->get_community($device))); + + foreach my $community (@{$self->{'communities'}}) { + `snmpwalk -M/dev/null -r$self->{'snmp_checks'} -t$self->{'snmp_timeout'} -v1 -On -Oe -c $community $device .0 2>/dev/null`; + if ($? == 0) { + $self->set_community($device, $community); + return 1; + } + } + + return 0; +} + +############################################################################## +# Ping the given host. Returns 1 if the host is alive, 0 otherwise. +############################################################################## +sub ping ($$$) { + my ($self, $host) = @_; + my ($timeout, $retries, $packets) = ( + $self->{'icmp_timeout'}, + $self->{'icmp_checks'}, + 1, + ); + + # Windows + if (($^O eq "MSWin32") || ($^O eq "MSWin32-x64") || ($^O eq "cygwin")){ + $timeout *= 1000; # Convert the timeout to milliseconds. + for (my $i = 0; $i < $retries; $i++) { + my $output = `ping -n $packets -w $timeout $host`; + return 1 if ($output =~ /TTL/); + } + + return 0; + } + + # Solaris + if ($^O eq "solaris"){ + my $ping_command = $host =~ /\d+:|:\d+/ ? "ping -A inet6" : "ping"; + for (my $i = 0; $i < $retries; $i++) { + # Note: There is no timeout option. + `$ping_command -s -n $host 56 $packets >/dev/null 2>&1`; + return 1 if ($? == 0); + } + + return 0; + } + + # FreeBSD + if ($^O eq "freebsd"){ + my $ping_command = $host =~ /\d+:|:\d+/ ? "ping6" : "ping -t $timeout"; + for (my $i = 0; $i < $retries; $i++) { + # Note: There is no timeout option for ping6. + `$ping_command -q -n -c $packets $host >/dev/null 2>&1`; + return 1 if ($? == 0); + } + + return 0; + } + + # NetBSD + if ($^O eq "netbsd"){ + my $ping_command = $host =~ /\d+:|:\d+/ ? "ping6" : "ping -w $timeout"; + for (my $i = 0; $i < $retries; $i++) { + # Note: There is no timeout option for ping6. + `$ping_command -q -n -c $packets $host >/dev/null 2>&1`; + if ($? == 0) { + return 1; + } + } + + return 0; + } + + # Assume Linux by default. + my $ping_command = $host =~ /\d+:|:\d+/ ? "ping6" : "ping"; + for (my $i = 0; $i < $retries; $i++) { + `$ping_command -q -W $timeout -n -c $packets $host >/dev/null 2>&1`; + return 1 if ($? == 0); + } + + return 0; +} + +######################################################################################## +# Discover router to router connectivity. +######################################################################################## +sub router_to_router_connectivity($$) { + my ($self, $router_1, $router_2) = @_; + + return unless defined($self->get_community($router_1)) and + defined($self->get_community($router_2)); + + # Get the list of next hops of the routers. + my %next_hops_1 = $self->snmp_get_value_hash($router_1, $IPROUTENEXTHOP); + my %next_hops_2 = $self->snmp_get_value_hash($router_2, $IPROUTENEXTHOP); + + # Search for matching entries. + foreach my $ip_addr_1 ($self->get_addresses($router_1)) { + if (defined($next_hops_2{$ip_addr_1})) { + foreach my $ip_addr_2 ($self->get_addresses($router_2)) { + if (defined($next_hops_1{$ip_addr_2})) { + my $if_1 = $self->get_if_from_ip($router_1, $ip_addr_2); + next unless $if_1 ne ''; + my $if_2 = $self->get_if_from_ip($router_2, $ip_addr_1); + next unless $if_2 ne ''; + + next if ($self->are_connected($router_1, $if_1, $router_2, $if_2)); + $self->call('message', "Router $ip_addr_1 (if $if_2) is connected to router $ip_addr_2 (if $if_2).", 5); + $self->call('connect_agents', $router_1, $if_1, $router_2, $if_2); + $self->mark_connected($router_1, $if_1, $router_2, $if_2); + + # Mark connections in case the routers are switches too. + $self->mark_switch_connected($router_1, $if_1); + $self->mark_switch_connected($router_1, $if_2); + } + } + } + } +} + +######################################################################################## +# Discover router to switch connectivity. +######################################################################################## +sub router_to_switch_connectivity($$$) { + my ($self, $router, $switch) = @_; + my (%mac_temp, @aft_temp); + + return unless defined($self->get_community($router)) and + defined($self->get_community($switch)); + + # Get the list of MAC addresses of the router. + my %mac_router; + %mac_temp = $self->snmp_get_value_hash($router, $IFPHYSADDRESS); + foreach my $mac (keys(%mac_temp)) { + $mac_router{parse_mac($mac)} = ''; + } + + # Get the address forwarding table (AFT) of the switch. + my @aft; + @aft_temp = $self->snmp_get_value_array($switch, $DOT1DTPFDBADDRESS); + foreach my $mac (@aft_temp) { + push(@aft, parse_mac($mac)); + } + + # Search for matching entries in the AFT. + foreach my $aft_mac (@aft) { + if (defined($mac_router{$aft_mac})) { + + # Get the router interface. + my $router_if_name = $self->get_if_from_mac($router, $aft_mac); + next unless ($router_if_name ne ''); + + # Get the switch interface. + my $switch_if_name = $self->get_if_from_aft($switch, $aft_mac); + next unless ($switch_if_name ne ''); + + next if ($self->are_connected($router, $router_if_name, $switch, $switch_if_name)); + $self->call('message', "Router $router (if $router_if_name) is connected to switch $switch (if $switch_if_name).", 5); + $self->call('connect_agents', $router, $router_if_name, $switch, $switch_if_name); + $self->mark_connected($router, $router_if_name, $switch, $switch_if_name); + + # Mark connections in case the routers are switches too. + $self->mark_switch_connected($switch, $switch_if_name); + $self->mark_switch_connected($router, $router_if_name); + } + } +} + +########################################################################## +# Perform a network scan. +########################################################################## +sub scan($) { + my ($self) = @_; + + # 1% + $self->call('update_progress', 1); + + # Find devices. + $self->call('message', "[1/7] Scanning the network...", 3); + my @subnets = @{$self->get_subnets()}; + foreach my $subnet (@subnets) { + my $net_addr = new NetAddr::IP ($subnet); + if (!defined($net_addr)) { + $self->call('message', "Invalid network: $subnet", 3); + return; + } + + my @hosts = map { (split('/', $_))[0] } $net_addr->hostenum; + my ($step, $progress) = ((50.0 / scalar(@subnets)) / scalar(@hosts), 1); # The first 50% of the recon task. + foreach my $host (@hosts) { + + $self->call('message', "Scanning host: $host", 5); + $self->call('update_progress', ceil($progress)); + $progress += $step; + + # Check if the device has already been visited. + next if ($self->is_visited($host)); + + # Check if the host is up. + next if ($self->ping($host) == 0); + + $self->arp_cache_discovery($host); + } + } + + # Get a list of found hosts. + my @hosts = (@{$self->get_routers()}, @{$self->get_switches()}, @{$self->get_hosts()}); + + # Delete previous connections. + $self->call('delete_connections'); + + # Try all known connectivity methods by brute force. + $self->call('message', "[2/7] Finding switch to switch connectivity...", 3); + for (my $i = 0; defined($hosts[$i]); $i++) { + my $switch_1 = $hosts[$i]; + for (my $j = $i + 1; defined($hosts[$j]); $j++) { + my $switch_2 = $hosts[$j]; + $self->switch_to_switch_connectivity($switch_1, $switch_2) if ($switch_1 ne $switch_2); + } + } + $self->call('update_progress', 60); + + $self->call('message', "[3/7] Finding router to switch connectivity...", 3); + foreach my $router (@hosts) { + foreach my $switch (@hosts) { + $self->router_to_switch_connectivity($router, $switch) if ($router ne $switch); + } + } + $self->call('update_progress', 70); + + $self->call('message', "[4/7] Finding router to router connectivity...", 3); + for (my $i = 0; defined($hosts[$i]); $i++) { + my $router_1 = $hosts[$i]; + for (my $j = $i + 1; defined($hosts[$j]); $j++) { + my $router_2 = $hosts[$j]; + $self->router_to_router_connectivity($router_1, $router_2) if ($router_1 ne $router_2); + } + } + $self->call('update_progress', 80); + + # Find switch/router to host connections. + $self->call('message', "[5/7] Finding switch/router to end host connectivity...", 3); + foreach my $device (@hosts) { + $self->host_connectivity($device); + } + $self->call('update_progress', 90); + + # Connect hosts that are still unconnected using traceroute. + $self->call('message', "[6/7] Finding host to hop connectivity.", 3); + foreach my $host (@hosts) { + next if ($self->is_connected($host)); + $self->traceroute_connectivity($host); + } + $self->call('update_progress', 95); + + # Connect hosts that are still unconnected using known gateways. + $self->call('message', "[7/7] Finding host to gateway connectivity.", 3); + $self->get_routes(); # Update the route cache. + foreach my $host (@hosts) { + next if ($self->is_connected($host)); + $self->gateway_connectivity($host); + } + $self->call('update_progress', -1); + + # Print debug information on found devices. + $self->call('message', "[Summary]", 3); + foreach my $host (@hosts) { + my $device = $self->get_device($host); + next unless defined($device); + + # Print device information. + my $dev_info = "Device: " . $device->{'type'} . " ("; + foreach my $ip_address ($self->get_addresses($host)) { + $dev_info .= "$ip_address,"; + } + chop($dev_info); + $dev_info .= ')'; + $self->call('message', $dev_info, 3); + } +} + +######################################################################################## +# Set an SNMP community for the given device. +######################################################################################## +sub set_community($$$) { + my ($self, $device, $community) = @_; + + $self->{'community_cache'}->{$device} = $community; +} + +######################################################################################## +# Set the type of the given device. +######################################################################################## +sub set_device_type($$$) { + my ($self, $device, $type) = @_; + + $self->{'visited_devices'}->{$device}->{'type'} = $type; +} + +######################################################################################## +# Performs an SNMP WALK and returns the response as an array. +######################################################################################## +sub snmp_get($$$) { + my ($self, $device, $oid) = @_; + my @output; + + my $community = $self->get_community($device); + return () unless defined ($community); + + # Check the SNMP query cache first. + if (defined($self->{'snmp_cache'}->{"${device}_${oid}"})) { + return @{$self->{'snmp_cache'}->{"${device}_${oid}"}}; + } + + # Check VLANS. + my @vlans = $self->get_vlans($device); + if (scalar(@vlans) == 0) { + @output = `snmpwalk -M/dev/null -r$self->{'snmp_checks'} -t$self->{'snmp_timeout'} -v1 -On -Oe -c $community $device $oid 2>/dev/null`; + } + else { + # Handle duplicate lines. + my %output_hash; + foreach my $vlan (@vlans) { + foreach my $line (`snmpwalk -M/dev/null -r$self->{'snmp_checks'} -t$self->{'snmp_timeout'} -v1 -On -Oe -c $community\@$vlan $device $oid 2>/dev/null`) { + $output_hash{$line} = 1; + } + } + push(@output, keys(%output_hash)); + } + + # Update the SNMP query cache. + $self->{'snmp_cache'}->{"${device}_${oid}"} = [@output]; + + return @output; +} + +######################################################################################## +# Performs an SNMP WALK and returns the value of the given OID. Returns undef +# on error. +######################################################################################## +sub snmp_get_value($$$) { + my ($self, $device, $oid) = @_; + + my @output = $self->snmp_get($device, $oid); + foreach my $line (@output) { + chomp ($line); + return $1 if ($line =~ /^$oid\s+=\s+\S+:\s+(.*)$/); + } + + return undef; +} + +######################################################################################## +# Performs an SNMP WALK and returns an array of values. +######################################################################################## +sub snmp_get_value_array($$$) { + my ($self, $device, $oid) = @_; + my @values; + + my @output = $self->snmp_get($device, $oid); + foreach my $line (@output) { + chomp ($line); + push(@values, $1) if ($line =~ /^$oid\S*\s+=\s+\S+:\s+(.*)$/); + } + + return @values; +} + +######################################################################################## +# Performs an SNMP WALK and returns a hash of values. +######################################################################################## +sub snmp_get_value_hash($$$) { + my ($self, $device, $oid) = @_; + my %values; + + my @output = $self->snmp_get_value_array($device, $oid); + foreach my $line (@output) { + $values{$line} = ''; + } + + return %values; +} + +######################################################################################## +# Discover switch to switch connectivity. +######################################################################################## +sub switch_to_switch_connectivity($$$) { + my ($self, $switch_1, $switch_2) = @_; + my (%mac_temp, @aft_temp); + + return unless defined($self->get_community($switch_1)) and + defined($self->get_community($switch_2)); + + # Get the list of MAC addresses of each switch. + my %mac_1; + %mac_temp = $self->snmp_get_value_hash($switch_1, $IFPHYSADDRESS); + foreach my $mac (keys(%mac_temp)) { + $mac_1{parse_mac($mac)} = ''; + } + my %mac_2; + %mac_temp = $self->snmp_get_value_hash($switch_2, $IFPHYSADDRESS); + foreach my $mac (keys(%mac_temp)) { + $mac_2{parse_mac($mac)} = ''; + } + + # Get the address forwarding table (AFT) of each switch. + my @aft_1; + @aft_temp = $self->snmp_get_value_array($switch_1, $DOT1DTPFDBADDRESS); + foreach my $mac (@aft_temp) { + push(@aft_1, parse_mac($mac)); + } + my @aft_2; + @aft_temp = $self->snmp_get_value_array($switch_2, $DOT1DTPFDBADDRESS); + foreach my $mac (@aft_temp) { + push(@aft_2, parse_mac($mac)); + } + + # Search for matching entries. + foreach my $aft_mac_1 (@aft_1) { + if (defined($mac_2{$aft_mac_1})) { + foreach my $aft_mac_2 (@aft_2) { + if (defined($mac_1{$aft_mac_2})) { + my $if_name_1 = $self->get_if_from_aft($switch_1, $aft_mac_1); + next unless ($if_name_1) ne ''; + my $if_name_2 = $self->get_if_from_aft($switch_2, $aft_mac_2); + next unless ($if_name_2) ne ''; + + next if ($self->are_connected($switch_1, $if_name_1, $switch_2, $if_name_2)); + $self->call('message', "Switch $switch_1 (if $if_name_1) is connected to switch $switch_2 (if $if_name_2).", 5); + $self->call(' / scalar(@subnets))connect_agents', $switch_1, $if_name_1, $switch_2, $if_name_2); + $self->mark_connected($switch_1, $if_name_1, $switch_2, $if_name_2); + + # Mark switch to switch connections. + $self->mark_switch_connected($switch_1, $if_name_1); + $self->mark_switch_connected($switch_2, $if_name_2); + } + } + } + } +} + +########################################################################## +# Connect the given hosts to its parent using traceroute. +########################################################################## +sub traceroute_connectivity($$) { + my ($self, $host) = @_; + + # Perform a traceroute. + my $nmap_args = '-nsP -PE --traceroute --max-retries '.$self->{'icmp_checks'}.' --host-timeout '.$self->{'icmp_timeout'}.'s -T'.$self->{'recon_timing_template'}; + my $np = Recon::NmapParser->new(); + eval { + $np->parsescan($self->{'nmap'}, $nmap_args, ($host)); + }; + return if ($@); + + # Get hops to the host. + my ($h) = $np->all_hosts (); + return unless defined ($h); + my @hops = $h->all_trace_hops (); + + # Skip the target host. + pop(@hops); + + # Reverse the host order (closest hosts first). + @hops = reverse(@hops); + + # Look for parents. + my $device = $host; + for (my $i = 0; $i < $self->{'parent_recursion'}; $i++) { + last unless defined($hops[$i]); + my $parent = $hops[$i]->ipaddr(); + + $self->call('set_parent', $device, $parent); + if ($device eq $host) { + $self->call('message', "Host $host is one hop away from host $parent.", 10); + $self->mark_connected($host); + } + + # Move on to the next hop. + $device = $parent; + } +} + +1; +__END__ + diff --git a/pandora_server/lib/Recon/NmapParser.pm b/pandora_server/lib/Recon/NmapParser.pm new file mode 100644 index 0000000000..17dee4965f --- /dev/null +++ b/pandora_server/lib/Recon/NmapParser.pm @@ -0,0 +1,2013 @@ +# Modified for Pandora FMS. + +# 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 Recon::NmapParser; + +use strict; +use XML::Twig; +use Storable qw(dclone); +use vars qw($VERSION %D); + +$VERSION = 1.30; + + +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(); + + eval { + $self->{twig}->safe_parse(@_); + }; + + if ($@) { + return; + } + + $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 ); + + if ($^O eq 'MSWin32') { + `$cmd 2> /Nul`; #remove output from STDOUT + } else { + `$cmd 2> /dev/null`; #remove output from STDOUT + } + $self->parsefile( $self->{cache_file} ); + } + else { + $cmd = "\"$nmap\" $args -v -v -v -oX - " . ( join ' ', @ips ); + if ($^O eq 'MSWin32') { + open $FH, "$cmd 2>/Nul |" || die "[Nmap-Parser] Could not perform nmap scan - $!"; + } else { + open $FH, "$cmd 2>/dev/null |" || 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 = NmapParser::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} = NmapParser::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') ) { + $scripts_hashref->{ $script->{att}->{id} } = + __script_tag_hdlr( $script ); + } + $D{$$}{SESSION}{prescript} = $scripts_hashref; + $twig->purge; +} + +sub _postscript_tag_hdlr { + my ( $twig, $tag ) = @_; + my $scripts_hashref; + for my $script ( $tag->children('script') ) { + $scripts_hashref->{ $script->{att}->{id} } = + __script_tag_hdlr( $script ); + } + $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} = NmapParser::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') ) { + $script_hashref->{ $_->{att}->{id} } = + __script_tag_hdlr($_); + } + + 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( $D{$$}{SESSION}{xml_version} eq "1.04") + 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 NmapParser::Host::OS + my $osmatch_index = 0; + my $osclass_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++; + for my $osclass ( $osmatch->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->{'osmatch_count'} = $osmatch_index; + + #parse osclass tags + 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') ) { + $scripts_hashref->{ $script->{att}->{id} } = + __script_tag_hdlr( $script ); + } + 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 + # NmapParser::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; +} + +sub __script_tag_hdlr { + my $tag = shift; + my $script_hashref = { + output => $tag->{att}->{output} + }; + chomp %$script_hashref; + if ( not $tag->is_empty()) { + $script_hashref->{contents} = __script_table($tag); + } + return $script_hashref; +} + +sub __script_table { + my $tag = shift; + my ($ref, $subref); + my $fc = $tag->first_child(); + if ($fc) { + if ($fc->is_text) { + $ref = $fc->text; + } + else { + if ($fc->{att}->{key}) { + $ref = {}; + $subref = sub { + $ref->{$_->{att}->{key}} = shift; + }; + } + else { + $ref = []; + $subref = sub { + push @$ref, shift; + }; + } + for ($tag->children()) { + if ($_->tag() eq "table") { + $subref->(__script_table( $_ )); + } + else { + $subref->($_->text); + } + } + } + } + return $ref +} + +#/*****************************************************************************/ +# NMAP::PARSER::SESSION +#/*****************************************************************************/ + +package NmapParser::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 NmapParser::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 { NmapParser::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 = shift; #open, filtered, closed or any combination + my @matched_ports = (); + + #if $state is undef, then tcp_ports or udp_ports was called for all ports + #therefore, only return the keys of all ports found + if ( not defined $state ) { + return sort { $a <=> $b } ( keys %{ $self->{ports}{$proto} } ); + } + else { + $state = lc($state) + } + +#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 NmapParser::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 NmapParser::Host::Service->new( + $self->{ports}{udp}{$portid}{service} ); + +} + +#usually the first one is the highest accuracy + +sub os_sig { return NmapParser::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 NmapParser::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 NmapParser::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 NmapParser::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 + +NmapParser - parse nmap scan data with perl + +=head1 SYNOPSIS + + use NmapParser; + my $np = new NmapParser; + + $np->parsescan($nmap_path, $nmap_args, @ips); + #or + $np->parsefile($file_xml); + + my $session = $np->get_session(); + #a NmapParser::Session object + + my $host = $np->get_host($ip_addr); + #a NmapParser::Host object + + my $service = $host->tcp_service(80); + #a NmapParser::Host::Service object + + my $os = $host->os_sig(); + #a NmapParser::Host::OS object + + #--------------------------------------- + + my $np2 = new NmapParser; + + $np2->callback(\&my_callback); + + $np2->parsefile($file_xml); + #or + $np2->parsescan($nmap_path, $nmap_args, @ips); + + sub my_callback { + + my $host = shift; + #NmapParser::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 NmapParser::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 NmapParser::Host object. +There are two sub objects under NmapParser::Host. One is the NmapParser::Host::Service object which will be used to obtain +information of a given service running on a given port. The second is the NmapParser::Host::OS object which contains the +operating system signature information (OS guessed names, classes, osfamily..etc). + + NmapParser -- Core parser + | + +--NmapParser::Session -- Nmap scan session information + | + +--NmapParser::Host -- General host information + | | + | |-NmapParser::Host::Service -- Port service information + | | + | |-NmapParser::Host::OS -- Operating system signature information + + +=head1 METHODS + +=head2 NmapParser + +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 NmapParser know to save the output before parsing the xml since NmapParser 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 NmapParser::Session object which contains the session scan information. + +=item B + +Obtains the NmapParser::Host object for the given $ip_addr. + +=item B + +Deletes the stored NmapParser::Host object whose IP is $ip_addr. + +=item B + +=item B + +Returns an array of all the NmapParser::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 NmapParser::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. + +=item B + +=item B + +A basic call to prescripts() returns a list of the names of the NSE scripts +run in the pre-scanning phase. If C<$name> is given, it returns the text output of the +a reference to a hash with "output" and "content" keys for the +script with that name, or undef if that script was not run. +The value of the "output" key is the text output of the script. The value of the +"content" key is a data structure based on the XML output of the NSE script. + +=item B + +=item B + +A basic call to postscripts() returns a list of the names of the NSE scripts +run in the post-scaning phase. If C<$name> is given, it returns the text output of the +a reference to a hash with "output" and "content" keys for the +script with that name, or undef if that script was not run. +The value of the "output" key is the text output of the script. The value of the +"content" key is a data structure based on the XML output of the NSE script. + +=back + +=head2 NmapParser::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 NmapParser::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 NmapParser::Host::OS object that can be used to obtain all the +Operating System signature (fingerprint) information. See NmapParser::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 +a reference to a hash with "output" and "content" keys for the +script with that name, or undef if that script was not run. +The value of the "output" key is the text output of the script. The value of the +"content" key is a data structure based on the XML output of the NSE script. + +=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 NmapParser::Host::Service object of a given service running on port, +provided by $portid. See NmapParser::Host::Service for more info. + + $svc = $host->tcp_service(80); + $svc->name; + $svc->proto; + + +=back + +=head3 NmapParser::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 +NmapParser::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 NSE scripts +run for this port. If C<$name> is given, it returns +a reference to a hash with "output" and "content" keys for the +script with that name, or undef if that script was not run. +The value of the "output" key is the text output of the script. The value of the +"content" key is a data structure based on the XML output of the NSE script. + +=back + +=head3 NmapParser::Host::OS + +This object represents the Operating System signature (fingerprint) information +of the given host. This object is obtained from an NmapParser::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 NmapParser::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. + +NmapParser::Host::TraceHop objects are obtained through the +C and C NmapParser::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 NmapParser 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 NmapParser; + + my $np = new NmapParser; + 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 NmapParser; + my $np = new NmapParser; + + #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 NmapParser, +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 NmapParser; + my $np = new NmapParser; + + + $np->callback( \&booyah ); + + $np->parsefile('nmap_results.xml'); + # or use parsescan() + + sub booyah { + my $host = shift; #NmapParser::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 NmapParser 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 NmapParser; + use vars qw($nmap_exe $nmap_args @ips); + my $base = new NmapParser; + my $curr = new NmapParser; + + + $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 NmapParser 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 NmapParser 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/Recon/Util.pm b/pandora_server/lib/Recon/Util.pm new file mode 100644 index 0000000000..f88596ab35 --- /dev/null +++ b/pandora_server/lib/Recon/Util.pm @@ -0,0 +1,116 @@ +#!/usr/bin/perl +# (c) Ártica ST 2016 +# Utility functions for the network topology discovery modules. + +package Recon::Util; +use strict; +use warnings; + +# Default lib dir for RPM and DEB packages. +use lib '/usr/lib/perl5'; + +use Socket qw/inet_aton/; + +our @ISA = ("Exporter"); +our %EXPORT_TAGS = ( 'all' => [ qw( ) ] ); +our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); +our @EXPORT = qw( + ip_to_long + mac_matches + mac_to_dec + parse_mac + subnet_matches +); + +######################################################################################## +# Return the numeric representation of the given IP address. +######################################################################################## +sub ip_to_long($) { + my $ip_address = shift; + + return unpack('N', inet_aton($ip_address)); +} + +######################################################################################## +# Returns 1 if the two given MAC addresses are the same. +######################################################################################## +sub mac_matches($$) { + my ($mac_1, $mac_2) = @_; + + if (parse_mac($mac_1) eq parse_mac($mac_2)) { + return 1; + } + + return 0; +} + +######################################################################################## +# Convert a MAC address to decimal dotted notation. +######################################################################################## +sub mac_to_dec($) { + my $mac = shift; + + my $dec_mac = ''; + my @elements = split(/:/, $mac); + foreach my $element (@elements) { + $dec_mac .= unpack('s', pack 's', hex($element)) . '.' + } + chop($dec_mac); + + return $dec_mac; +} + +######################################################################################## +# Make sure all MAC addresses are in the same format (00 11 22 33 44 55 66). +######################################################################################## +sub parse_mac($) { + my ($mac) = @_; + + # Remove leading and trailing whitespaces. + $mac =~ s/(^\s+)|(\s+$)//g; + + # Replace whitespaces and dots with colons. + $mac =~ s/\s+|\./:/g; + + # Convert hex digits to uppercase. + $mac =~ s/([a-f])/\U$1/g; + + # Add a leading 0 to single digits. + $mac =~ s/^([0-9A-F]):/0$1:/g; + $mac =~ s/:([0-9A-F]):/:0$1:/g; + $mac =~ s/:([0-9A-F])$/:0$1/g; + + return $mac; +} + +######################################################################################## +# Returns 1 if the given IP address belongs to the given subnet. +######################################################################################## +sub subnet_matches($$;$) { + my ($ipaddr, $subnet, $mask) = @_; + my ($netaddr, $netmask); + + # Decimal dot notation mask. + if (defined($mask)) { + $netaddr = $subnet; + $netmask = ip_to_long($mask); + } + # CIDR notation. + else { + ($netaddr, $netmask) = split('/', $subnet); + return 0 unless defined($netmask); + + # Convert the netmask to a numeric format. + $netmask = -1 << (32 - $netmask); + } + + if ((ip_to_long($ipaddr) & $netmask) == (ip_to_long($netaddr) & $netmask)) { + return 1; + } + + return 0; +} + +1; +__END__ +