533 lines
19 KiB
Perl
533 lines
19 KiB
Perl
package PandoraFMS::ReconServer;
|
|
##########################################################################
|
|
# Pandora FMS Recon Server.
|
|
# Pandora FMS. the Flexible Monitoring System. http://www.pandorafms.org
|
|
##########################################################################
|
|
# Copyright (c) 2005-2009 Artica Soluciones Tecnologicas S.L
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public License
|
|
# as published by the Free Software Foundation; version 2
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
##########################################################################
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use threads;
|
|
use threads::shared;
|
|
use Thread::Semaphore;
|
|
|
|
use IO::Socket::INET;
|
|
use POSIX qw(strftime ceil);
|
|
use JSON qw(decode_json encode_json);
|
|
use Encode qw(encode_utf8);
|
|
|
|
# Default lib dir for RPM and DEB packages
|
|
use lib '/usr/lib/perl5';
|
|
|
|
use PandoraFMS::Tools;
|
|
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);
|
|
|
|
# Patched Nmap::Parser. See http://search.cpan.org/dist/Nmap-Parser/.
|
|
use PandoraFMS::NmapParser;
|
|
|
|
# Inherits from PandoraFMS::ProducerConsumerServer
|
|
our @ISA = qw(PandoraFMS::ProducerConsumerServer);
|
|
|
|
# Global variables
|
|
my @TaskQueue :shared;
|
|
my %PendingTasks :shared;
|
|
my $Sem :shared;
|
|
my $TaskSem :shared;
|
|
|
|
########################################################################################
|
|
# Recon Server class constructor.
|
|
########################################################################################
|
|
sub new ($$$$$$) {
|
|
my ($class, $config, $dbh) = @_;
|
|
|
|
return undef unless $config->{'reconserver'} == 1;
|
|
|
|
if (! -e $config->{'nmap'}) {
|
|
logger ($config, ' [E] ' . $config->{'nmap'} . " needed by Pandora FMS Recon Server not found.", 1);
|
|
print_message ($config, ' [E] ' . $config->{'nmap'} . " needed by Pandora FMS Recon Server not found.", 1);
|
|
return undef;
|
|
}
|
|
|
|
# Initialize semaphores and queues
|
|
@TaskQueue = ();
|
|
%PendingTasks = ();
|
|
$Sem = Thread::Semaphore->new;
|
|
$TaskSem = Thread::Semaphore->new (0);
|
|
|
|
# Call the constructor of the parent class
|
|
my $self = $class->SUPER::new($config, 3, \&PandoraFMS::ReconServer::data_producer, \&PandoraFMS::ReconServer::data_consumer, $dbh);
|
|
|
|
bless $self, $class;
|
|
return $self;
|
|
}
|
|
|
|
###############################################################################
|
|
# Run.
|
|
###############################################################################
|
|
sub run ($) {
|
|
my $self = shift;
|
|
my $pa_config = $self->getConfig ();
|
|
|
|
print_message ($pa_config, " [*] Starting Pandora FMS Recon Server.", 1);
|
|
$self->setNumThreads ($pa_config->{'recon_threads'});
|
|
$self->SUPER::run (\@TaskQueue, \%PendingTasks, $Sem, $TaskSem);
|
|
}
|
|
|
|
###############################################################################
|
|
# Data producer.
|
|
###############################################################################
|
|
sub data_producer ($) {
|
|
my $self = shift;
|
|
my ($pa_config, $dbh) = ($self->getConfig (), $self->getDBH ());
|
|
|
|
my @tasks;
|
|
|
|
my $server_id = get_server_id ($dbh, $pa_config->{'servername'}, $self->getServerType ());
|
|
return @tasks unless defined ($server_id);
|
|
|
|
# Manual tasks have interval_sweep = 0
|
|
# Manual tasks are "forced" like the other, setting the utimestamp to 1
|
|
# By default, after create a tasks it takes the utimestamp to 0
|
|
# Status -1 means "done".
|
|
|
|
my @rows = get_db_rows ($dbh, 'SELECT * FROM trecon_task
|
|
WHERE id_recon_server = ?
|
|
AND disabled = 0
|
|
AND utimestamp = 0 OR (status = -1 AND interval_sweep > 0 AND (utimestamp + interval_sweep) < UNIX_TIMESTAMP())', $server_id);
|
|
foreach my $row (@rows) {
|
|
|
|
# Update task status
|
|
update_recon_task ($dbh, $row->{'id_rt'}, 1);
|
|
|
|
push (@tasks, $row->{'id_rt'});
|
|
}
|
|
|
|
return @tasks;
|
|
}
|
|
|
|
###############################################################################
|
|
# Data consumer.
|
|
###############################################################################
|
|
sub data_consumer ($$) {
|
|
my ($self, $task_id) = @_;
|
|
my ($pa_config, $dbh) = ($self->getConfig (), $self->getDBH ());
|
|
|
|
# Get recon task data
|
|
my $task = get_db_single_row ($dbh, 'SELECT * FROM trecon_task WHERE id_rt = ?', $task_id);
|
|
return -1 unless defined ($task);
|
|
|
|
# Is it a recon script?
|
|
if (defined ($task->{'id_recon_script'}) && ($task->{'id_recon_script'} != 0)) {
|
|
exec_recon_script ($pa_config, $dbh, $task);
|
|
return;
|
|
} else {
|
|
logger($pa_config, 'Starting recon task for net ' . $task->{'subnet'} . '.', 10);
|
|
}
|
|
|
|
# Call nmap
|
|
my $timeout = $pa_config->{'networktimeout'}*1000;
|
|
my $nmap_args = '-nsP -PE --max-retries '.$pa_config->{'icmp_checks'}.' --host-timeout '.$timeout.' -T'.$pa_config->{'recon_timing_template'};
|
|
my $np = new PandoraFMS::NmapParser;
|
|
eval {
|
|
$np->parsescan($pa_config->{'nmap'}, $nmap_args, ($task->{'subnet'}));
|
|
};
|
|
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 if ($agent->{'modo'} != 1);
|
|
}
|
|
|
|
# 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
|
|
$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 (' . $RDBMS_QUOTE . 'id_a' . $RDBMS_QUOTE . ', ' .
|
|
$RDBMS_QUOTE . 'id_agent' . $RDBMS_QUOTE . ')
|
|
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 $timeout = $pa_config->{'networktimeout'}*1000;
|
|
my $nmap_args = '-nsP -PE --traceroute --max-retries '.$pa_config->{'icmp_checks'}.' --host-timeout '.$timeout.' -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
|
|
$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 = pandora_create_agent ($pa_config, $pa_config->{'servername'}, $host_name, $host_addr, $group, $parent_id, $id_os, '', 300, $dbh);
|
|
$agent_id = 0 unless defined ($parent_id);
|
|
db_do ($dbh, 'INSERT INTO taddress_agent (' . $RDBMS_QUOTE . 'id_a' . $RDBMS_QUOTE . ',' .
|
|
$RDBMS_QUOTE . 'id_agent' . $RDBMS_QUOTE . ')
|
|
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);
|
|
}
|
|
|
|
##########################################################################
|
|
# Update recon task status.
|
|
##########################################################################
|
|
sub update_recon_task ($$$) {
|
|
my ($dbh, $id_task, $status) = @_;
|
|
|
|
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
|
|
##########################################################################
|
|
sub exec_recon_script ($$$) {
|
|
my ($pa_config, $dbh, $task) = @_;
|
|
|
|
# Get recon plugin data
|
|
my $script = get_db_single_row ($dbh, 'SELECT * FROM trecon_script WHERE id_recon_script = ?', $task->{'id_recon_script'});
|
|
return -1 unless defined ($script);
|
|
|
|
logger($pa_config, 'Executing recon script ' . safe_output($script->{'name'}), 10);
|
|
|
|
my $command = safe_output($script->{'script'});
|
|
|
|
my $macros = safe_output($task->{'macros'});
|
|
|
|
# \r and \n should be escaped for decode_json().
|
|
$macros =~ s/\n/\\n/g;
|
|
$macros =~ s/\r/\\r/g;
|
|
my $decoded_macros = decode_json (encode_utf8($macros));
|
|
|
|
my $macros_parameters = '';
|
|
|
|
# Add module macros as parameter
|
|
if(ref($decoded_macros) eq "HASH") {
|
|
# Convert the hash to a sorted array
|
|
my @sorted_macros;
|
|
while (my ($i, $m) = each (%{$decoded_macros})) {
|
|
$sorted_macros[$i] = $m;
|
|
}
|
|
|
|
# Remove the 0 position
|
|
shift @sorted_macros;
|
|
|
|
foreach my $m (@sorted_macros) {
|
|
$macros_parameters = $macros_parameters . ' "' . $m->{"value"} . '"';
|
|
}
|
|
}
|
|
|
|
if (-x $command) {
|
|
`$command $task->{'id_rt'} $task->{'id_group'} $task->{'create_incident'} $macros_parameters`;
|
|
} else {
|
|
logger ($pa_config, "Cannot execute recon task command $command.");
|
|
}
|
|
|
|
# Notify this recon task is ended
|
|
update_recon_task ($dbh, $task->{'id_rt'}, -1);
|
|
|
|
logger($pa_config, 'Done executing recon script ' . safe_output($script->{'name'}), 10);
|
|
return 0;
|
|
}
|
|
|
|
1;
|
|
__END__
|