From 8a34944d76940ef68d36056490bae7c2ec0d3113 Mon Sep 17 00:00:00 2001
From: Ramon Novoa <rnovoa@artica.es>
Date: Wed, 3 Jun 2015 17:15:59 +0200
Subject: [PATCH] Several improvements to the SNMP layer 2 recon script.

(cherry picked from commit cfd7590efccc0ffe2f9f4f73cfc4255e5b0ac7a6)
---
 .../util/recon_scripts/snmp-recon.pl          | 160 +++++++++++++++---
 1 file changed, 132 insertions(+), 28 deletions(-)

diff --git a/pandora_server/util/recon_scripts/snmp-recon.pl b/pandora_server/util/recon_scripts/snmp-recon.pl
index c38fc19454..c97b82aa33 100755
--- a/pandora_server/util/recon_scripts/snmp-recon.pl
+++ b/pandora_server/util/recon_scripts/snmp-recon.pl
@@ -27,6 +27,9 @@ my $ALLIFACES = '';
 # Keep our own ARP cache to connect hosts to switches/routers.
 my %ARP_CACHE;
 
+# IP address of a host given the MAC of one of its interfaces.
+my %IF_CACHE;
+
 # Default configuration values.
 my $OSNAME = $^O;
 my %CONF;
@@ -36,6 +39,7 @@ if ($OSNAME eq "freebsd") {
 		'nmap' => '/usr/local/bin/nmap',
 		'pandora_path' => '/usr/local/etc/pandora/pandora_server.conf',
 		'icmp_checks' => 1,
+		'icmp_packets' => 1,
 		'networktimeout' => 2,
 		'snmp_checks' => 2,
 		'snmp_timeout' => 2,
@@ -48,6 +52,7 @@ if ($OSNAME eq "freebsd") {
 		'nmap' => '/usr/bin/nmap',
 		'pandora_path' => '/etc/pandora/pandora_server.conf',
 		'icmp_checks' => 1,
+		'icmp_packets' => 1,
 		'networktimeout' => 2,
 		'snmp_timeout' => 2,
 		'recon_timing_template' => 3,
@@ -72,10 +77,15 @@ my $DBH;
 my $GROUP_ID;
 
 # Devices by type.
-my %HOSTS;
+my @HOSTS;
 my @ROUTERS;
 my @SWITCHES;
 
+# 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.
+my %SWITCH_TO_SWITCH;
+
 # MAC addresses.
 my %MAC;
 
@@ -145,7 +155,7 @@ sub mac_to_dec($) {
 	my $mac = shift;
 
 	my $dec_mac = '';
-	my @elements = split(/ /, $mac);
+	my @elements = split(/:/, $mac);
 	foreach my $element (@elements) {
         $dec_mac .= unpack('s', pack 's', hex($element)) .  '.'
 	}
@@ -154,6 +164,42 @@ sub mac_to_dec($) {
 	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 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;
+}
+
 ########################################################################################
 # Returns 1 if the device belongs to one of the scanned subnets.
 ########################################################################################
@@ -280,8 +326,9 @@ sub get_if_from_mac($$$) {
 	my @output = snmp_get($device, $community, $IFPHYSADDRESS);
 	foreach my $line (@output) {
 		chomp($line);
-		next unless $line =~ /^$IFPHYSADDRESS.(\S+)\s+=\s+\S+:\s+$mac$/;
-		my $if_index = $1;
+		next unless $line =~ /^$IFPHYSADDRESS.(\S+)\s+=\s+\S+:\s+(.*)$/;
+		my ($if_index, $if_mac) = ($1, $2);
+		next unless (mac_matches($mac, $if_mac) == 1);
 
 		# Get the name of the interface associated to the port.
 		my $if_name = snmp_get_value($device, $community, "$IFNAME.$if_index");
@@ -310,7 +357,7 @@ sub get_if_from_aft($$$) {
 
 	# Get the interface name.
 	my $if_name = snmp_get_value($switch, $COMMUNITIES{$switch}, "$IFNAME.$if_index");
-	return '' unless defined($if_name);
+	return "if$if_index" unless defined($if_name);
 
 	$if_name =~ s/"//g;
 	return $if_name;
@@ -342,8 +389,7 @@ sub get_if_mac($$$) {
 	return '' unless defined($mac);
 
 	# Clean-up the MAC address.
-	$mac =~ s/ /:/g;
-	chop($mac);
+	$mac = parse_mac($mac);
 
 	return $mac;
 }
@@ -415,6 +461,7 @@ sub arp_cache_discovery {
 		foreach my $line (@output) {
 			next unless ($line =~ /^$IPNETTOMEDIAPHYSADDRESS.\d+.(\S+)\s+=\s+\S+:\s+(.*)$/);
 			my ($ip_addr, $mac_addr) = ($1, $2);
+			$mac_addr = parse_mac($mac_addr);
 
 			# Save the mac to connect hosts to switches/routers.
 			$ARP_CACHE{$mac_addr} = $ip_addr;
@@ -428,7 +475,7 @@ sub arp_cache_discovery {
 	if ($device_type eq 'host' || $device_type eq 'printer') {
 
 		# Hosts are indexed to help find router/switch to host connectivity.
-		$HOSTS{$device} = '';
+		push(@HOSTS, $device);
 	}
 	elsif ($device_type eq 'switch') {
 		push(@SWITCHES, $device);
@@ -457,7 +504,7 @@ sub find_synonyms($$$) {
 
 		# There is no need to access switches or routers from different IP addresses.
 		if ($device_type eq 'host' || $device_type eq 'printer') {
-			$HOSTS{$ip_address} = '';
+			push(@HOSTS, $device);
 		}
 	}
 }
@@ -534,17 +581,34 @@ sub guess_device_type($$) {
 ########################################################################################
 sub switch_to_switch_connectivity($$) {
 	my ($switch_1, $switch_2) = @_;
+	my (%mac_temp, @aft_temp);
 
 	# Make sure both switches respond to SNMP.
 	return unless defined($COMMUNITIES{$switch_1} && $COMMUNITIES{$switch_2});
 
 	# Get the list of MAC addresses of each switch.
-	my %mac_1 = snmp_get_value_hash($switch_1, $COMMUNITIES{$switch_1}, $IFPHYSADDRESS);
-	my %mac_2 = snmp_get_value_hash($switch_2, $COMMUNITIES{$switch_2}, $IFPHYSADDRESS);
+	my %mac_1;
+	%mac_temp = snmp_get_value_hash($switch_1, $COMMUNITIES{$switch_1}, $IFPHYSADDRESS);
+	foreach my $mac (keys(%mac_temp)) {
+		$mac_1{parse_mac($mac)} = '';
+	}
+	my %mac_2;
+	%mac_temp = snmp_get_value_hash($switch_2, $COMMUNITIES{$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 = snmp_get_value_array($switch_1, $COMMUNITIES{$switch_1}, $DOT1DTPFDBADDRESS);
-	my @aft_2 = snmp_get_value_array($switch_2, $COMMUNITIES{$switch_2}, $DOT1DTPFDBADDRESS);
+	my @aft_1;
+	@aft_temp = snmp_get_value_array($switch_1, $COMMUNITIES{$switch_1}, $DOT1DTPFDBADDRESS);
+	foreach my $mac (@aft_temp) {
+		push(@aft_1, parse_mac($mac));
+	}
+	my @aft_2;
+	@aft_temp = snmp_get_value_array($switch_2, $COMMUNITIES{$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) {
@@ -552,9 +616,15 @@ sub switch_to_switch_connectivity($$) {
 			foreach my $aft_mac_2 (@aft_2) {
 				if (defined($mac_1{$aft_mac_2})) {
 					my $if_name_1 = get_if_from_aft($switch_1, $COMMUNITIES{$switch_1}, $aft_mac_1);
+					next unless ($if_name_1) ne '';
 					my $if_name_2 = get_if_from_aft($switch_2, $COMMUNITIES{$switch_2}, $aft_mac_2);
+					next unless ($if_name_2) ne '';
 					message("Switch $switch_1 (if $if_name_1) is connected to switch $switch_2 (if $if_name_2).");
 					connect_pandora_agents($switch_1, $if_name_1, $switch_2, $if_name_2);
+
+					# Mark switch to switch connections.
+					$SWITCH_TO_SWITCH{"$switch_1$if_name_1"} = 1;
+					$SWITCH_TO_SWITCH{"$switch_2$if_name_2"} = 1;
 					return;
 				}
 			}
@@ -567,15 +637,24 @@ sub switch_to_switch_connectivity($$) {
 ########################################################################################
 sub router_to_switch_connectivity($$) {
 	my ($router, $switch) = @_;
+	my (%mac_temp, @aft_temp);
 
 	# Make sure both routers respond to SNMP.
 	return unless defined($COMMUNITIES{$router} && $COMMUNITIES{$switch});
 
 	# Get the list of MAC addresses of the router.
-	my %mac_router = snmp_get_value_hash($router, $COMMUNITIES{$router}, $IFPHYSADDRESS);
+	my %mac_router;
+	%mac_temp = snmp_get_value_hash($router, $COMMUNITIES{$router}, $IFPHYSADDRESS);
+	foreach my $mac (keys(%mac_temp)) {
+		$mac_router{parse_mac($mac)} = '';
+	}
 
 	# Get the address forwarding table (AFT) of the switch.
-	my @aft = snmp_get_value_array($switch, $COMMUNITIES{$switch}, $DOT1DTPFDBADDRESS);
+	my @aft;
+	@aft_temp = snmp_get_value_array($switch, $COMMUNITIES{$switch}, $DOT1DTPFDBADDRESS);
+	foreach my $mac (@aft_temp) {
+		push(@aft, parse_mac($mac));
+	}
 
 	# Search for matching entries in the AFT.
 	foreach my $aft_mac (@aft) {
@@ -586,9 +665,14 @@ sub router_to_switch_connectivity($$) {
 
 			# Get the switch interface.
 			my $switch_if_name = get_if_from_aft($switch, $COMMUNITIES{$switch}, $aft_mac);
+			next unless ($switch_if_name ne '');
 
 			message("Router $router (if $router_if_name) is connected to switch $switch (if $switch_if_name).");
 			connect_pandora_agents($router, $router_if_name, $switch, $switch_if_name);
+
+			# Mark connections in case the routers are switches too.
+			$SWITCH_TO_SWITCH{"$switch$switch_if_name"} = 1;
+			$SWITCH_TO_SWITCH{"$router$router_if_name"} = 1;
 			return;
 		}
 	}
@@ -616,6 +700,10 @@ sub router_to_router_connectivity($$) {
 					my $if_2 = get_if_from_ip($router_2, $COMMUNITIES{$router_2}, $ip_addr_1);
 					message("Router $ip_addr_1 (if $if_2) is connected to router $ip_addr_2 (if $if_2).");
 					connect_pandora_agents($router_1, $if_1, $router_2, $if_2);
+					
+					# Mark connections in case the routers are switches too.
+					$SWITCH_TO_SWITCH{"$router_1$if_1"} = 1;
+					$SWITCH_TO_SWITCH{"$router_2$if_2"} = 1;
 					return;
 				}
 			}
@@ -635,15 +723,25 @@ sub host_connectivity($) {
 	# Get the address forwarding table (AFT) of the device.
 	my @aft = snmp_get_value_array($device, $COMMUNITIES{$device}, $DOT1DTPFDBADDRESS);
 	foreach my $mac (@aft) {
-		next unless defined ($ARP_CACHE{$mac});
-		my $host = $ARP_CACHE{$mac};
-		next unless defined ($HOSTS{$host});
+		$mac = parse_mac($mac);
+		my $host;
+		if (defined ($ARP_CACHE{$mac})) {
+			$host = $ARP_CACHE{$mac};
+		} elsif (defined ($IF_CACHE{$mac})) {
+			$host = $IF_CACHE{$mac};
+		} else {
+			next;
+		}
+		next unless defined ($VISITED_DEVICES{$host});
 		my $device_if_name = get_if_from_aft($device, $COMMUNITIES{$device}, $mac);
+		next unless ($device_if_name ne '');
 		my $host_if_name = defined($COMMUNITIES{$host}) ? get_if_from_mac($host, $COMMUNITIES{$host}, $mac) : '';
 		if ($VISITED_DEVICES{$device}->{'type'} eq 'router') {
+			next if defined ($SWITCH_TO_SWITCH{"$device$device_if_name"}); # The switch is probably connected to another router/switch.
 			message("Host $host " . ($host_if_name ne '' ? "(if $host_if_name)" : '') . " is connected to router $device (if $device_if_name).");
 		}
 		elsif ($VISITED_DEVICES{$device}->{'type'} eq 'switch') {
+			next if defined ($SWITCH_TO_SWITCH{"$device$device_if_name"}); # The switch is probably connected to another switch.
 			message("Host $host " . ($host_if_name ne '' ? "(if $host_if_name)" : '') . " is connected to switch $device (if $device_if_name).");
 		}
 		else {
@@ -722,16 +820,19 @@ sub create_pandora_agent($) {
 			next unless $if_status == 1;
 		}
 
-		# Get the name of the network interface.
-		my $if_name = snmp_get_value($device, $COMMUNITIES{$device}, "$IFNAME.$if_index");
-		next unless defined($if_name);
-		$if_name =~ s/"//g;
-
 		# Fill the module description with the IP and MAC addresses.
 		my $mac = get_if_mac($device, $COMMUNITIES{$device}, $if_index);
 		my $ip = get_if_ip($device, $COMMUNITIES{$device}, $if_index);
 		my $if_desc = ($mac ne '' ? "MAC $mac " : '') . ($ip ne '' ? "IP $ip" : '');
 
+		# Fill the interface cache.
+		$IF_CACHE{$mac} = $ip;
+
+		# Get the name of the network interface.
+		my $if_name = snmp_get_value($device, $COMMUNITIES{$device}, "$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($DBH, "ifOperStatus_${if_name}", $agent_id);
 		next if ($module_id > 0);
@@ -833,9 +934,8 @@ sub connect_pandora_agents($$$$) {
 		db_do($DBH, 'INSERT INTO tmodule_relationship (`module_a`, `module_b`, `id_rt`) VALUES(?, ?, ?)', $module_id_1, $module_id_2, $TASK_ID);
 	}
 
-	# Unset parents (otherwise the map will look broken).
-	db_do($DBH, 'UPDATE tagente SET id_parent=0 WHERE id_agente=?', $agent_1->{'id_agente'});
-	db_do($DBH, 'UPDATE tagente SET id_parent=0 WHERE id_agente=?', $agent_2->{'id_agente'});
+	# Update parents.
+	db_do($DBH, 'UPDATE tagente SET id_parent=? WHERE id_agente=?', $agent_1->{'id_agente'}, $agent_2->{'id_agente'});
 }
 
 
@@ -990,6 +1090,10 @@ else {
 	my @scanned_hosts = $np->all_hosts();
 	foreach my $host (@scanned_hosts) {
 		next unless defined($host->addr()) and defined($host->status()) and ($host->status() eq 'up');
+
+		# Make sure the host is up (nmap gives false positives!).
+		next if (pandora_ping(\%CONF, $host->addr(), 1, 1) == 0);
+
 		arp_cache_discovery($host->addr());
 	}
 }
@@ -1028,10 +1132,10 @@ update_recon_task($DBH, $TASK_ID, 75);
 
 # Find switch/router to host connections.
 message("[6/6] Finding switch/router to end host connectivity...");
-foreach my $device ((@ROUTERS, @SWITCHES)) {
+foreach my $device (@ROUTERS, @SWITCHES, @HOSTS) {
 	host_connectivity($device);
 }
-foreach my $host (keys(%HOSTS)) {
+foreach my $host (@HOSTS) {
 	next unless (ref($VISITED_DEVICES{$host}) eq 'HASH'); # Skip aliases.
 	next if ($VISITED_DEVICES{$host}->{'connected'} == 1); # Skip already connected hosts.
 	traceroute_connectivity($host);