#host-mapper
#version 0.022
#Cedric Nelson, November 2007
#cedric.nelson@gmail.com
use WWW::Mechanize;
use Getopt::Std;
use DBI;
use Crypt::SSLeay;
my ($flags,
$dirty,
@switches,
@hosts,
@nodes,
$default_username,
$default_password,
$username,
$password,
$protocol,
$port,
$url,
$realm,
$database_file,
$DSN,
$dbh,
$input_camdb,
$input_macdb,
$input_db_file,
$output_file,
@unaccounted_for);
#Set variables
$url = '/level/15/exec/-/show/mac-address-table/CR';
$realm = 'level_15_access';
#Example path:
#$database_file = '\\\\server\\share\\SysInfo.mdb';
$database_file = '';
$DSN = "driver=Microsoft Access Driver (*.mdb);dbq=$database_file";
$default_username = '';
$default_password = '';
#Parse invokation options.
if (@ARGV) {
$flags = analyze_options(@ARGV);
foreach my $flag (keys %{$flags}) {
if ($flag !~ /^-([seupmcidqh])$/) {
print "Unrecognized option: $flag\n\n";
$dirty = 1;
show_help();
}
}
unless ($dirty == 1) {
if (${$flags}{'-s'}) {
foreach my $item (@{${$flags}{'-s'}}) {
if (-e $item) {push @switches, parse_files_macro($item);}
else {push @switches, $item;}
}
}
if (${$flags}{'-e'}) {$port = '443';$protocol = 'https';}
else {$port = '80';$protocol = 'http';}
if (${$flags}{'-u'}) {$username = @{${$flags}{'-u'}}[0];}
if (${$flags}{'-p'}) {$password = @{${$flags}{'-p'}}[0];}
unless (${$flags}{'-u'}) {$username = $default_username;}
unless (${$flags}{'-p'}) {$password = $default_password;}
if (${$flags}{'-m'}) {
foreach my $item (@{${$flags}{'-m'}}) {
if (-e $item) {push @nodes, parse_files_macro($item);}
else {push @nodes, $item;}
}
}
if (${$flags}{'-c'}) {$input_camdb = @{${$flags}{'-c'}}[0];}
if (${$flags}{'-i'}) {$input_macdb = @{${$flags}{'-i'}}[0];}
if (${$flags}{'-d'}) {$input_db_file = @{${$flags}{'-d'}}[0];}
if (${$flags}{'-q'}) {
foreach my $item (@{${$flags}{'-q'}}) {
if (-e $item) {push @hosts, parse_files_macro($item);}
else {push @hosts, $item;}
}
}
unless (${$flags}{'-m'}) {@nodes = @hosts;}
###Actions based on flags###
#Search for a host entry
if (${$flags}{'-q'}) {
#Search an existing database
if (${$flags}{'-d'}) {
my $mapped_macs_ref = load_mapdb($input_db_file);
search_macdb($mapped_macs_ref, \&print_mac_entry, @hosts);
print "Hosts unaccounted for:\n";
foreach my $host (@unaccounted_for) {print "$host\n";}
}
#Search using combinations of other resources
else {
my $mapped_macs_ref = map_macro();
search_macdb($mapped_macs_ref, \&print_mac_entry, @hosts);
save_mapdb($mapped_macs_ref);
print "Hosts unaccounted for:\n";
foreach my $host (@unaccounted_for) {print "$host\n";}
}
}
#Create a host-map database
elsif ((${$flags}{'-s'} || ${$flags}{'-c'}) && (${$flags}{'-m'} ||
+ ${$flags}{'-i'})) {
my $mapped_macs_ref = map_macro();
save_mapdb($mapped_macs_ref);
}
#Create a cam-database
elsif (${$flags}{'-s'} || ${$flags}{'-c'}) {
my $cam_ref = cam_macro();
save_camdb($cam_ref);
}
#Create a mac-database
elsif (${$flags}{'-m'} || ${$flags}{'-i'}) {
my $mac_ref = mac_macro();
save_macdb($mac_ref);
}
elsif (${$flags}{'-h'}) {show_help();}
else {print "I don't understand this combination of options. Exitin
+g...\n";}
}
}
#Or display help if there are none.
else {show_help();}
### Macros Section ###
#Parse Files Macro
#Make an array of items by reading them from file(s)
sub parse_files_macro {
my (@files) = @_;
my (@items);
foreach my $file (@files) {
open FH, ($file) or warn "Can't open $file for parsing. Skipping i
+t.\n";
while (<FH>) {
chomp($_);
push @items, $_;
}
close FH;
}
return @items;
}
#CAM Table Macro
#1. Build trunkport-hash from switches
#2. Get cam-hash from file or from switches
sub cam_macro {
my ($cam_ref);
#Get cam-hash
if (${$flags}{'-c'}) {$cam_ref = load_camdb($input_camdb);}
elsif (${$flags}{'-s'}) {
trunk_macro();
foreach my $ip (@switches) {
my $dirty_output = get_data($ip, $port, $realm, $username, $pass
+word, $url);
$cam_ref = parse_cam($dirty_output);
}
}
else {die "In order to proceed I need a CAM table specified.\n";}
return ($cam_ref);
}
#MAC Table Macro
#1. Get mac-hash from a file, host(s), or from default database
sub mac_macro {
my ($mac_ref);
#Get mac-hash
if (${$flags}{'-i'}) {$mac_ref = load_macdb($input_macdb);}
elsif (${$flags}{'-m'}) {
$mac_ref = get_macs(@nodes);
}
elsif (-e $database_file) {$mac_ref = get_mac_db();}
elsif (${$flags}{'-q'}) {
$mac_ref = get_macs(@nodes);
}
else {die "In order to proceed I need a host-MAC table specified.\n"
+;}
return ($mac_ref);
}
#Trunk Macro
#1. Get trunkport-hash from switches
sub trunk_macro {
my ($trunk_ref);
my $url = '/level/15/exec/-/show/cdp/neighbors/CR';
foreach my $ip (@switches) {
my $dirty_output = get_data($ip, $port, $realm, $username, $passwo
+rd, $url);
$trunk_ref = parse_trunks($dirty_output);
}
return ($trunk_ref);
} #End trunk_macro
#Map Macro
#1. Get cam-hash
#2. Get mac-hash
#4. Map MACs
sub map_macro {
my ($cam_ref,
$mac_ref,
$mapped_macs_ref);
$cam_ref = cam_macro();
$mac_ref = mac_macro();
#Map MACs
$mapped_macs_ref = map_macs($cam_ref, $mac_ref);
return ($cam_ref, $mac_ref, $mapped_macs_ref);
} #End map_macro
### Subroutine Section ###
#Anonymous code block for scoping %options, @list properly.
{
my %options;
my @list;
#Defines relationship of invokation arguments as a hash.
sub analyze_options {
my ($element);
($element, @list) = @_;
#Add new options to the hash.
unless ($options{$element}) {$options{$element} = [];}
#Parse the list of arguments.
while (@list) {
my $item = shift(@list);
#If the element is an option, parse it with remaining arguments.
if ($item =~ /^-/) {analyze_options($item, @list);}
#If the element is an argument, it belongs to the preceding opti
+on.
else {push @{$options{$element}}, $item;}
}
#Return a reference to your hash of options.
return \%options;
} #End analyze_options
} #End anonymous code block.
#Grabs html data from a switch, and passes it to a function.
sub get_data {
my ($ip, $port, $realm, $username, $password, $url, $function) = @_;
my $spider;
$spider = WWW::Mechanize->new(autocheck => 1);
$spider->credentials("$ip:$port", $realm, $username, $password);
$spider->get("$protocol://$ip$url");
if (defined($function)) {$function->($spider->content());}
else {return $spider->content();}
} #End get_data
#Anonymous code block for get_macs
{
my %macs;
#Retrieve MAC information from host(s)
sub get_macs {
my (@hosts) = @_;
unless (@hosts) {@hosts = 'localhost';}
foreach my $host (@hosts) {
my ($command,
$output,
$mac);
$command = "wmic /node:'$host' nic where NetConnectionID='Local
+Area Connection' get MACAddress";
@output = `$command`;
#Collect all MAC addresses from output
foreach my $entry (@output) {
if ($entry =~ /([a-f0-9]{2}:){5}[a-f0-9]{2}/i) {
$mac = $&;
$mac =~ s/://g;
$mac =~ s/([a-z0-9]{4})/$1\./gi;
$mac =~ s/\.$//;
unless ($macs{$mac}) {$macs{$mac} = $host;}
}
}
}
return \%macs;
} #End get_macs
}#End anonymous code block
#Anonymous code block
{
my %cam;
#Parse html data for MAC information
#Note that parse_cam is dependant on data created from parse_trunks
sub parse_cam {
my ($dirty_output, $function) = @_;
my ($output,
@lines,
$hostname);
#Parse data for hostname
if ($dirty_output =~ m{<BODY><H1>(.*)</H1><PRE>}) {$hostname = $1;
+}
else {$hostname = 'unknown_device';}
#Cut out un-neeeded output
if ($dirty_output =~ m{---- ----------- -------- -----
+(.*)Total Mac Addresses for this criterion}s) {
$output = $1;
}
else {print "No match found for MACs regex.\n";}
#Parse data for MACs
@lines = split /\n/, $output;
shift @lines;
#Format MACs and re-assign to hash
#Table format: Switch,VLAN,MAC,Type,Port
#Hash format: MAC => [Switch, Port, VLAN]
foreach my $line (@lines) {
$line =~ s/^ +//;
$line =~ s/( )+/,/g;
$line = "$hostname," . $line;
my ($switch, $vlan, $mac, $type, $port) = split /,/, $line;
$port =~ tr/\r\n//d;
if (is_host_port($hostname, $port)) {
unless ($cam{$mac}) {$cam{$mac} = [$switch, $port, $vlan];}
}
}
if (defined($function)) {$function->(\%cam);}
else {return \%cam;}
} #End parse_cam
} #End anonymous code block
#Anonymous code block
{
my %switch_trunks;
#Parse html data for trunk-port information
#This is dependant on cdp being enabled
sub parse_trunks {
my ($dirty_output, $function) = @_;
my ($hostname,
$output,
@lines);
if ($dirty_output =~ m{<BODY><H1>(.*)</H1><PRE>}) {$hostname = $1;
+}
else {$hostname = 'unknown_device';}
#Isolate CDP information
if ($dirty_output =~ m{Port ID(.*)</DL>}s) {$output = $1;}
else {print "No match found for CDP regex.\n";}
#Parse Trunk-port information
@lines = split /\n/, $output;
foreach (@lines) {
#Isolate trunk-port entries with the format:
#<interface> <number>/<number> or <interface> <number>
if ($_ =~ /(\w+ \d+\/\d+|\w+ \d+)/) {
my $match = $1;
#Format trunk-port to match mac-address-table output
#You may need to add to this, depending on what modules
#your switches are using.
$match =~ s/Gig /Gi/;
#Add any trunk-ports to an array for this switch
unless ($switch_trunks{$hostname}) {$switch_trunks{$hostname}
+= [];}
push @{$switch_trunks{$hostname}}, $match;
}
}
if (defined($function)) {$function->(\%switch_trunks);}
else {return \%switch_trunks;}
} #End parse_trunks
#Determine if a specified host is directly connected to switch
sub is_host_port {
my ($switch, $switch_port) = @_;
my $is_host_port = 'maybe';
foreach my $trunk_port (@{$switch_trunks{$switch}}) {
if ($switch_port eq $trunk_port) {$is_host_port = 'no';}
}
if ($is_host_port eq 'no') {return 0;}
else {return 1;}
} #End is_access_port
} #End anonymous code block
#Anonymous block for get_mac_db
{
my %macs;
#Read the default host-mac db
sub get_mac_db {
$dbh = DBI->connect("dbi:ODBC:$DSN", '','') or die "$DBI::errs
+tr\n";
my $sql = "SELECT ComputerName, MACAddress FROM tblSysInfo";
my $sth = $dbh->prepare($sql);
$sth->execute();
my @row;
while (@row = $sth->fetchrow_array) {
my ($hostname, $macaddress) = @row;
#Format MACs to conform to cisco's style
if ($macaddress =~ /([a-z0-9]{2}:){5}[a-z0-9]{2}/i) {
$macaddress =~ s/://g;
$macaddress =~ s/([a-z0-9]{4})/$1\./gi;
$macaddress =~ s/\.$//;
}
$macs{$macaddress} = $hostname;
}
return \%macs;
} #End get_mac_db
} #End anonymous code block.
#Save mac database to csv file
sub save_mapdb {
my ($output_hash) = @_;
unless ($output_file) {$output_file = 'hostmap-database.csv';}
open OUTPUT, ">$output_file" or die "Can't open $output_file for wri
+ting.\n";
print OUTPUT "Hostname,Switch,Port,VLAN,MAC\n";
while (my ($mac, $arrayref) = each %$output_hash) {
my (@row) = (@$arrayref[0], @$arrayref[1], @$arrayref[2], @$arrayr
+ef[3], $mac);
$line = join(",", @row);
print OUTPUT ($line, "\n");
}
close OUTPUT;
} #End save_mapdb
#Anonymous block for load_mapdb
{
my %mapped_mac_info;
#Load a host-map from a file
sub load_mapdb {
my ($input_db) = @_;
if (-e $input_db) {
open INPUT, "<$input_db" or die "Can't open $input_db.\n";
while (<INPUT>) {
chomp($_);
my ($line) = $_;
my (@entry) = split /,/, $_;
my ($hostname, $switch, $port, $vlan, $mac) = @entry;
unless ($mac eq 'MAC') {
$mapped_mac_info{$mac} = [$hostname, $switch, $port, $vlan];
}
}
close INPUT;
return \%mapped_mac_info;
}
else {print "I can't open $input_db.\n";}
} #End load_mapdb
} #End anonymous code block
#Anonymous block for load_camdb
{
my %cam_db;
#Load a cam database from a file
sub load_camdb {
my ($input_db) = @_;
if (-e $input_db) {
open INPUT, "<$input_db" or die "Can't open $input_db.\n";
while (<INPUT>) {
chomp($_);
my ($switch, $port, $vlan, $mac) = split /,/, $_;
unless ($mac eq 'MAC') {
$cam_db{$mac} = [$switch, $port, $vlan, $mac];
}
}
close INPUT;
return \%cam_db;
}
else {print "I can't open $input_db.\n";}
} #End load_camdb
} #End anonymous code block
sub save_camdb {
my ($cam_ref) = @_;
open OUTPUT, ">cam-database.csv" or die "Can't open cam-database.csv
+ for writing.\n";
print OUTPUT "Switch,Port,VLAN,MAC\n";
while (my ($mac, $arrayref) = each %$cam_ref) {
my (@row) = (@$arrayref[0],@$arrayref[1],@$arrayref[2],$mac);
print OUTPUT (join(',', @row), "\n");
}
close OUTPUT;
} #End save_camdb
sub save_macdb {
my ($mac_ref) = @_;
open OUTPUT, ">mac-database.csv" or die "Can't open mac-database.csv
+ for writing.\n";
print OUTPUT "Hostname, MAC\n";
while (my ($mac, $hostname) = each %$mac_ref) {
my (@row) = ($hostname, $mac);
print OUTPUT (join(',', @row), "\n");
}
close OUTPUT;
} #End save_macdb
#Anonymous block for load_macdb
{
my %mac_db;
#Load a mac database from a file
sub load_macdb {
my ($input_db) = @_;
if (-e $input_db) {
open INPUT, "<$input_db" or die "Can't open $input_db.\n";
while (<INPUT>) {
chomp($_);
my ($hostname, $mac) = split /,/, $_;
unless ($mac eq 'MAC') {
$mac_db{$mac} = $hostname;
}
}
close INPUT;
return \%mac_db;
}
else {print "I can't open $input_db.\n";}
} #End load_macdb
} #End anonymous code block
#Search a macdb for a host, and take action for matches.
sub search_macdb {
my ($macdb_ref, $action_ref, @hosts) = @_;
foreach $host (@hosts) {
my ($match_found) = 0;
while (my ($host_mac, $cam_array_ref) = each %$macdb_ref) {
my ($macdb_host) = @$cam_array_ref[0];
if ($host =~ /$macdb_host/i) {
my ($switch, $port, $vlan) = (@$cam_array_ref[1], @$cam_array_
+ref[2], @$cam_array_ref[3]);
$action_ref->($host, $switch, $port, $vlan);
$match_found = 1;
}
}
if ($match_found == 0) {
print "No entry for $host found.\n\n";
push @unaccounted_for, $host;
}
}
} #End search_macdb
#Prints a mac entry.
sub print_mac_entry {
my ($host, $switch, $port, $vlan) = @_;
print "Hostname: $host\nSwitch: $switch\nPort: $port\nVLAN: $vlan\n\
+n";
} #End print_mac_entry
#Anonymous block for map_macs
{
my %mapped_mac_info;
#Maps hostnames to ports using the MAC address
sub map_macs {
my ($switch_mac_hash, $host_mac_hash) = @_;
#There should be a more efficient way to find MAC matches
while (my ($switch_mac, $cam_array_ref) = each %$switch_mac_hash)
+{
while (my ($host_mac, $hostname) = each %$host_mac_hash) {
if ($switch_mac =~ /$host_mac/i) {
my ($switch, $port, $vlan) = (@$cam_array_ref[0], @$cam_arra
+y_ref[1], @$cam_array_ref[2]);
unless ($mapped_mac_info{$host_mac}) {
$mapped_mac_info{$host_mac} = [$hostname, $switch, $port,
+$vlan];
}
}
}
}
return \%mapped_mac_info;
} #End map_macs
} #End anonymous code block.
sub show_help {
my ($error) = @_;
if ($error) {print "$error\n\n";}
my ($help) = <<END;
host-mapper
Version 0.022
Cedric Nelson, 2007
A utility for describing where hosts are physically connected.
Usage: host-mapper <options>
Options:
-s <sw | file>... Query switch(es) for CAM table(s).
(Used alone, -s will save a cam-database
+)
-e Establish encrypted connection to switch
+(es).
-u <...> Username for connection to switch(es).
-p <...> Password for connection to switch(es).
-m <host | file>... Query host(s) for MAC address(es).
(Used alone, -m will save a mac-database
+)
(Using -s & -m alone will save a host-map database.)
-c <file> Use cam-database from file.
(A substitute for -s)
-i <file> Use mac-database from file.
(A substitute for -m)
-d <file> Use host-map database from file.
(A substitute for -s and -m)
-q <host | file>... Query host-map for specified host(s).
(Used with -s & -m, or -d)
-h Display help.
END
print "$help\n";
} #End show_help
|