#!/usr/bin/perl ########################################################################## # Pandora FMS Recon Server # http://www.pandorafms.com ########################################################################## # Copyright (c) 2007-2008 Sancho Lerena, slerena@gmail.com # (c) 2007-2008 Artica Soluciones Tecnologicas S.L # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; 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. ########################################################################## # Includes list use strict; use warnings; use Date::Manip; # Needed to manipulate DateTime formats # of input, output and compare use Net::Ping; use Time::Local; # DateTime basic manipulation use NetAddr::IP; # To manage IP Addresses # Detect if Net:Traceroute::Pureperl is available. # If not, parent detection will be disabled my $traceroute_loaded = 1; # Traceroute needs traceroute command unless ( eval "use Net::Traceroute::PurePerl; 1" ) { $traceroute_loaded = 0; } use POSIX; # to use ceil() function use Socket; # to resolve address use threads; use threads::shared; # Pandora Modules use PandoraFMS::Config; use PandoraFMS::Tools; use PandoraFMS::DB; # Queue management my @pending_task : shared; my %pending_task_hash : shared; my %current_task_hash : shared; my $queue_lock : shared; my $icmp_lock : shared; # FLUSH in each IO (only for debug, very slooow) # ENABLED in DEBUGMODE # DISABLE FOR PRODUCTION $| = 0; my %pa_config; $SIG{'TERM'} = 'pandora_shutdown'; $SIG{'INT'} = 'pandora_shutdown'; # Inicio del bucle principal de programa pandora_init(\%pa_config, "Pandora FMS Recon server"); # Read config file for Global variables pandora_loadconfig (\%pa_config, 3); # Audit server starting pandora_audit (\%pa_config, "Pandora FMS Recon Daemon starting", "SYSTEM", "System"); # Check for traceroute if ($traceroute_loaded == 0){ print " [!] Traceroute has noot been compiled. Parent detection disabled.\n\n"; } # Check for xprobe2 my $xprobe2 = $pa_config{"xprobe2"}; if (! -e $xprobe2) { print " [E] $xprobe2 not found. Pandora FMS Recon cannot detect OS types without it.\n\n"; exit; } else { print " [*] $xprobe2 Detected.\n\n"; } sleep(1); # Daemonize and put in background if ( $pa_config{"daemon"} eq "1" ){ if ($pa_config{"quiet"} eq "0"){ print " [*] Backgrounding Pandora FMS Recon Server process.\n\n"; } &pandora_daemonize ( \%pa_config); } # Launch now all plugin threads # $ax is local thread id for this server for (my $ax=0; $ax < $pa_config{'recon_threads'}; $ax++){ threads->new( \&pandora_recon_consumer, \%pa_config, $ax); } # Launch now the producer thread threads->new( \&pandora_recon_producer, \%pa_config); sleep(1); # Connect ONCE to Database, we cannot pass DBI handler to all subprocess because # cannot share DBI between threads without use method CLONE. my $pa_config = \%pa_config; my $dbhost = $pa_config->{'dbhost'}; my $dbuser = $pa_config->{'dbuser'}; my $dbpass = $pa_config->{'dbpass'}; my $dbname = $pa_config->{'dbname'}; my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost:3306", $dbuser, $dbpass, { RaiseError => 1, AutoCommit => 1 }); while ( 1 ){ pandora_serverkeepaliver ($pa_config, 3, $dbh); threads->yield; sleep($pa_config->{"server_threshold"}); } #------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------ #--------------------- Main Perl Code below this line----------------------- #------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------ sub pandora_recon_producer ($) { my $pa_config = $_[0]; print " [*] Starting up Recon Producer Thread ...\n"; my $dbh = DBI->connect("DBI:mysql:$pa_config->{'dbname'}:$pa_config->{'dbhost'}:3306", $pa_config->{'dbuser'}, $pa_config->{'dbpass'}, { RaiseError => 1, AutoCommit => 1 }); my $server_id = $pa_config->{'server_id'}; # Initialize variables for posterior usage my $query_sql; my @sql_data1; my $data_id_task; my $exec_sql1; while (1) { $query_sql = "SELECT * FROM trecon_task WHERE id_recon_server = $server_id AND ( status = 1 OR (utimestamp + interval_sweep) < UNIX_TIMESTAMP() ) "; $exec_sql1 = $dbh->prepare($query_sql); $exec_sql1 ->execute; while (@sql_data1 = $exec_sql1->fetchrow_array()) { $data_id_task = $sql_data1[0]; # Skip modules already queued if ((!defined($pending_task_hash{$data_id_task})) && (!defined($current_task_hash{$data_id_task}))) { pandora_update_reconstatus ($pa_config, $dbh, $data_id_task, 0); # Locking scope, do not remove redundant { } { lock $queue_lock; push (@pending_task, $data_id_task); $pending_task_hash {$data_id_task}=1; } } } $exec_sql1->finish(); threads->yield; sleep($pa_config->{"server_threshold"}); } # Main loop } ########################################################################## # SUB pandora_recon_consumer # This module runs each X seconds (server threshold) checking for recon task ########################################################################## sub pandora_recon_consumer ($$) { my $pa_config = $_[0]; my $thread_id = $_[1]; if ($pa_config->{"quiet"} == 0){ print " [*] Starting up Recon Consumer Thread # $thread_id \n"; } my $data_id_task; # Create Database handler my $dbh = DBI->connect("DBI:mysql:$pa_config->{'dbname'}:$pa_config->{'dbhost'}:3306", $pa_config->{'dbuser'}, $pa_config->{'dbpass'}, { RaiseError => 1, AutoCommit => 1 }); my $counter =0; LOOP: while (1) { if ($counter > 10) { threads->yield; sleep($pa_config->{"server_threshold"}); $counter = 0; } # Take the first element on the shared queue # Insert this element on the current task hash { lock $queue_lock; if (scalar(@pending_task) == 0){ $counter++; next LOOP; } $data_id_task = shift(@pending_task); delete($pending_task_hash{$data_id_task}); $current_task_hash{$data_id_task}=1; } # Executing recon task with unmanaged error trapping eval { pandora_recon_exec_task ($pa_config, $data_id_task, $dbh); }; if ($@){ logger ($pa_config, "[ERROR] Recon Task for Task ID $data_id_task causes a system exception", 0); logger ($pa_config, "ERROR Code: $@", 1); } # Remove from queue. If catch an error, probably data is # not been processed, but has been freed from task queue { lock $queue_lock; delete($current_task_hash{$data_id_task}); } $counter = 0; threads->yield; } } ########################################################################## # SUB pandora_detect_os (paconfig, host) # Detect OS using xprobe2 tool. Return tconfig_os id code. ########################################################################## sub pandora_detect_os { my $pa_config = $_[0]; my $host = $_[1]; my $xprobe2 = $pa_config->{"xprobe2"}; if (! -e $xprobe2){ return 10; # other } my $command= ""; eval { $command = `$xprobe2 $host 2> /dev/null | grep "Running OS" 2> /dev/null | head -1 2> /dev/null`; }; if ($@){ return 10; } return pandora_get_os ($command); } ########################################################################## # SUB pandora_exec_task (pa_config, id_task) # Execute task ########################################################################## sub pandora_recon_exec_task { my $pa_config = $_[0]; my $id_task = $_[1]; my $dbh = $_[2]; my $target_ip; # Real ip to check my @ip2; # temp array for NetAddr::IP my $space; # temp var to store space of ip's for netaddr::ip my $query_sql; # for use in SQL my $exec_sql; # for use in SQL my $sql_data; # for use in SQL $query_sql = "SELECT * FROM trecon_task WHERE id_rt = $id_task"; $exec_sql = $dbh->prepare($query_sql); $exec_sql ->execute; if ($exec_sql->rows == 0) { # something wrong.. return -1; } $sql_data = $exec_sql->fetchrow_hashref(); my $status = $sql_data->{"status"}; my $interval = $sql_data->{"interval"}; my $target_network = $sql_data->{"subnet"}; my $task_name = $sql_data->{"name"}; my $task_ncprofile = $sql_data->{"id_network_profile"}; my $task_group = $sql_data->{"id_group"}; my $task_create_incident = $sql_data->{"create_incident"}; my $task_id_os = $sql_data->{"id_os"}; my $position = 0; my $list_ip = ""; my $list_host = ""; my $host_found = 0; my $add_host = 0; my $id_parent = 0; my $id_os = 0; # Asign target dir to netaddr object "space" $space = new NetAddr::IP $target_network; if (!defined($space)){ logger ($pa_config, "Bad network $target_network for task $task_name", 2); pandora_update_reconstatus ($pa_config, $dbh, $id_task, -1); pandora_task_set_utimestamp ($pa_config, $dbh, $id_task); return -1; } my $total_hosts= $space->num +1 ; # Begin scanning main loop do { @ip2 = split(/\//,$space); $target_ip = $ip2[0]; $space++; $position++; $add_host = 0; # Is this IP listed for any agent ? if (pandora_check_ip ($pa_config, $dbh, $target_ip) == 0){ # Check ICMP for this IP if ( scan_icmp ($target_ip, $pa_config->{'networktimeout'}) == 1) { $id_os = pandora_detect_os ($pa_config, $target_ip); if ($task_id_os == -1){ $add_host = 1; } elsif ($id_os == $task_id_os){ $add_host = 1; } } if ($add_host == 1){ $host_found ++; my $target_ip_resolved = resolv_ip2name($target_ip); $list_ip = $list_ip . " " . $target_ip; $list_host = $list_host . " " . $target_ip_resolved; $id_parent = pandora_getparent ($pa_config, $target_ip, $dbh); # If has a network profile, create agent and modules my $agent_id; if ($task_ncprofile > 0){ # Create address, agent and more... my $target_ip_id = pandora_task_create_address ($pa_config, $dbh, $id_task, $target_ip); $agent_id = pandora_task_create_agent ($pa_config, $dbh, $target_ip, $target_ip_id, $task_group, $target_ip_resolved, $id_parent, $id_os); pandora_task_create_agentmodules ($pa_config, $dbh, $agent_id, $task_ncprofile, $target_ip); } else { my $target_ip_id = pandora_task_create_address ($pa_config, $dbh, $id_task, $target_ip); $agent_id = pandora_task_create_agent($pa_config, $dbh, $target_ip, $target_ip_id, $task_group, $target_ip_resolved, $id_parent, $id_os); } my $title = "[RECON] New host [$target_ip_resolved] detected on network [$target_network]"; # Always create event about this detected IP pandora_event ($pa_config, $title,$task_group, $agent_id, 2, 0, 0, 'recon_host_detected', $dbh); } } my $progress = ceil($position / ($total_hosts / 100)); pandora_update_reconstatus ($pa_config, $dbh, $id_task, $progress); pandora_task_set_utimestamp ($pa_config, $dbh, $id_task); } while ($space < $space->broadcast); # fin del buclie principal de iteracion de Ips # Create incident if (($host_found > 0) && ($task_create_incident == 1)){ my $my_timestamp = &UnixDate("today","%Y-%m-%d %H:%M:%S"); my $text = "At $my_timestamp a new hosts ($host_found) has been 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_name].\n\n"; if ($task_ncprofile > 0){ $text = $text."Aditionally, and following instruction for this task, agent(s) has been created, with modules assigned to network component profile [".give_network_component_profile_name ($pa_config, $dbh, $task_ncprofile)."]. Please check this agent as soon as possible to verify it."; } $text = $text . "\n\nThis is the list of IP addresses found: \n\n$list_host "; pandora_create_incident ( $pa_config, $dbh, "[RECON] New hosts detected", $text, 0, 0, "Pandora FMS Recon Server", $task_group); } # Mark RECON TASK as done (-1) pandora_update_reconstatus ($pa_config, $dbh, $id_task, -1); pandora_task_set_utimestamp ($pa_config, $dbh, $id_task); } ############################################################################## # pandora_ping_icmp (destination, timeout) - Do a ICMP scan, 1 if alive, 0 if not ############################################################################## sub scan_icmp { my $dest = $_[0]; my $l_timeout = $_[1]; # Temp vars. my $result = 0; my $p; # Check for valid destination if (!defined($dest)){ return 0; } { lock $icmp_lock; $p = Net::Ping->new(); } if ($p->ping($dest)){ $p->close(); undef ($p); return 1; } else { return 0; } } ############################################################################## # pandora_ping_icmp (destination, timeout) - Do a ICMP scan, 1 if alive, 0 if not ############################################################################## sub scan_tcp { my $dest = $_[0]; my $l_timeout = $_[1]; my $dest_port = $_[2]; my $opened = 0; eval { alarm $l_timeout; my $handle=IO::Socket::INET->new( Proto=>"tcp", PeerAddr=>$dest, PeerPort=>$dest_port); if ($handle){ $opened =1; } alarm 0; }; if ($@){ return 0; } return $opened; } ########################################################################## # SUB resolv_ip2name (ip_address) # return name (if could resolve) or ip of ipaddress ########################################################################## sub resolv_ip2name { my $ip = $_[0]; my $addr=inet_aton($ip); if ($addr) { my $name=gethostbyaddr($addr, AF_INET); if ($name) { return $name; } else { return $ip; } } else { return $ip; } } ########################################################################## # SUB pandora_check_ip (pa_config, dbh, ip_address) # Return 1 if this IP exists, 0 if not ########################################################################## sub pandora_check_ip { my $pa_config = $_[0]; my $dbh = $_[1]; my $ip_address = $_[2]; my $query_sql = "SELECT * FROM taddress WHERE ip = '$ip_address' "; my $exec_sql = $dbh->prepare($query_sql); $exec_sql ->execute; if ($exec_sql->rows != 0) { $exec_sql->finish(); return 1; } else { $exec_sql->finish(); return 0; } } ########################################################################## # SUB pandora_get_agent_from_ip (pa_config, dbh, ip_address) # Return Id_agent for agent with IP_Address passed as parameter ########################################################################## sub pandora_get_agent_from_ip { my $pa_config = $_[0]; my $dbh = $_[1]; my $ip_address = $_[2]; return get_db_free_field ("SELECT id_agent FROM taddress, taddress_agent WHERE taddress_agent.id_a = taddress.id_a AND ip = '$ip_address'", $dbh); } ########################################################################## # SUB pandora_update_reconstatus (pa_config, dbh, id_task, status) # Update recontask status flag ########################################################################## sub pandora_update_reconstatus { my $pa_config = $_[0]; my $dbh = $_[1]; my $id_task = $_[2]; my $status = $_[3]; my $query_sql2 = "UPDATE trecon_task SET status = $status WHERE id_rt = $id_task"; $dbh->do($query_sql2); } ########################################################################## # SUB pandora_task_set_utimestamp (pa_config, dbh, id_task) # Update utimestamp to current timestamp ########################################################################## sub pandora_task_set_utimestamp { my $pa_config = $_[0]; my $dbh = $_[1]; my $id_task = $_[2]; my $my_timestamp = &UnixDate("today","%Y-%m-%d %H:%M:%S"); my $my_utimestamp = &UnixDate($my_timestamp, "%s"); # convert from human to integer my $query_sql2 = "UPDATE trecon_task SET utimestamp = '$my_utimestamp' WHERE id_rt = $id_task"; $dbh->do($query_sql2); } ########################################################################## # SUB pandora_task_create_address (pa_config, dbh, id_task, address) # Add address to table taddress, return ID of created record ########################################################################## sub pandora_task_create_address { my $pa_config = $_[0]; my $dbh = $_[1]; my $id_task = $_[2]; my $ip_address = $_[3]; my $query_sql2 = "INSERT INTO taddress (ip) VALUES ('$ip_address')"; $dbh->do ($query_sql2); my $lastid = $dbh->{'mysql_insertid'}; return $lastid; } ########################################################################## # SUB pandora_task_create_agent (pa_config, dbh, target_ip, target_ip_id, # id_group, name, id_parent) # Create agent, and associate address to agent in taddress_agent table. # it returns created id_agent. ########################################################################## sub pandora_task_create_agent { my $pa_config = $_[0]; my $dbh = $_[1]; my $target_ip = $_[2]; my $target_ip_id = $_[3]; my $id_group = $_[4]; my $name = $_[5]; my $id_parent = $_[6]; my $id_os = $_[7]; return pandora_create_agent ($pa_config, $dbh, $target_ip, $target_ip_id, $id_group, 0, $name, $id_parent, $id_os); } ########################################################################## # SUB pandora_task_create_agentmodules (pa_config, dbh, agent_id, ncprofile, ipaddress) # Create modules from a network component profile and associated to given agent ########################################################################## sub pandora_task_create_agentmodules { my $pa_config = $_[0]; my $dbh = $_[1]; my $agent_id = $_[2]; my $ncprofile_id = $_[3]; my $ip_adress = $_[4]; my @sql_data; # Search each network component that belongs to ncprofile_id my $query_sql = "SELECT * FROM tnetwork_profile_component where id_np = $ncprofile_id "; my $exec_sql = $dbh->prepare($query_sql); $exec_sql ->execute; while (@sql_data = $exec_sql->fetchrow_array()) { my $id_nc = $sql_data[1]; my $query_sql2 = "SELECT * FROM tnetwork_component where id_nc = $id_nc "; my $exec_sql2 = $dbh->prepare($query_sql2); $exec_sql2 ->execute; if ($exec_sql2->rows != 0) { my $sql_data2 = $exec_sql2->fetchrow_hashref(); my $name = ""; $name = $sql_data2->{"name"}; my $description = ""; $description = $sql_data2->{"description"}; my $type = "1"; $type = $sql_data2->{"type"}; my $max = 0; $max = $sql_data2->{"max"}; my $min = 0; $min = $sql_data2->{"min"}; my $interval = 300; $interval = $sql_data2->{"module_interval"}; my $tcp_port = ""; $tcp_port = $sql_data2->{"tcp_port"}; my $tcp_send = ""; $tcp_send = $sql_data2->{"tcp_send"}; my $tcp_rcv = ""; $tcp_rcv = $sql_data2->{"tcp_rcv"}; my $snmp_community = "public"; $snmp_community = $sql_data2->{"snmp_community"}; my $snmp_oid = ""; $snmp_oid = $sql_data2->{"snmp_oid"}; my $id_module_group = 0; $id_module_group = $sql_data2->{"id_module_group"}; my $id_module = 0; $id_module = $sql_data2->{"id_modulo"}; my $plugin_user = ""; $plugin_user = $dbh->quote($sql_data2->{"plugin_user"}); my $plugin_pass = ""; $plugin_pass = $dbh->quote($sql_data2->{"plugin_pass"}); my $plugin_parameter = ""; $plugin_parameter = $dbh->quote($sql_data2->{"plugin_parameter"}); my $max_timeout = "30"; $max_timeout = $sql_data2->{"max_timeout"}; my $query_sql3 = "INSERT INTO tagente_modulo (id_agente, id_tipo_modulo, descripcion, nombre, max, min, module_interval, tcp_port, tcp_send, tcp_rcv, snmp_community, snmp_oid, ip_target, id_module_group, flag, disabled, plugin_user, plugin_pass, plugin_parameter, max_timeout, id_modulo ) VALUES ( $agent_id, $type, '$description', '$name', $max, $min, $interval, $tcp_port, '$tcp_send', '$tcp_rcv', '$snmp_community', '$snmp_oid', '$ip_adress', $id_module_group, 1, 0, $plugin_user, $plugin_pass, $plugin_parameter, $max_timeout, $id_module)"; $dbh->do($query_sql3); my $last_id_agente_modulo = $dbh->{'mysql_insertid'}; logger($pa_config,"Recon Server: Creating module $name for agent $ip_adress",3); my $query_sql4; if (($type == 2) || ($type == 6) || ($type == 21) || ($type == 9) || ($type == 18)) { # for monitors $query_sql4 = "INSERT INTO tagente_estado (id_agente_modulo, datos, timestamp, cambio, estado, id_agente, last_try, utimestamp, current_interval, running_by) VALUES ($last_id_agente_modulo, '', '0000-00-00 00:00:00', 0, 0, $agent_id, '0000-00-00 00:00:00', 0, $interval, 0)"; } else { # not monitors $query_sql4 = "INSERT INTO tagente_estado (id_agente_modulo, datos, timestamp, cambio, estado, id_agente, last_try, utimestamp, current_interval, running_by) VALUES ($last_id_agente_modulo, '', '0000-00-00 00:00:00', 0, 100, $agent_id, '0000-00-00 00:00:00', 0, $interval, 0)"; } $dbh->do($query_sql4); } $exec_sql2->finish(); } $exec_sql->finish(); } sub pandora_getparent ($$){ my $pa_config = $_[0]; my $destination = $_[1]; my $dbh = $_[2]; # If don't have traceroute available, just skip if ($traceroute_loaded == 0){ return 0; } my $t = new Net::Traceroute::PurePerl( backend => 'PurePerl', host => $destination, debug => 0, max_ttl => 15, query_timeout => $pa_config->{"networktimeout"}, packetlen => 40, protocol => 'udp', # udp or icmp ); my $success = 0; $success = $t->traceroute(); if ($t->hops > 1){ if ($success){ my $parent_ip = $t->hop_query_host($t->hops-1,0); return pandora_get_agent_from_ip ($pa_config, $dbh, $parent_ip); } } return 0; } ######################################################################################## # pandora_shutdown () # Close system ######################################################################################## sub pandora_shutdown { logger (\%pa_config,"Pandora FMS Server '".$pa_config{'servername'}.$pa_config{"servermode"}."' Shutdown by signal ",0); print " [*] Shutting down ".$pa_config{'servername'}.$pa_config{"servermode"} ."(received signal)...\n"; pandora_event (\%pa_config, $pa_config{'servername'}.$pa_config{"servermode"}." going Down", 0, 0, 4, 0, 0, "system", $dbh); pandora_updateserver (\%pa_config, $pa_config{'servername'}, 0, 3, $dbh); exit; }