#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. Exiting...\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 it.\n"; while () { 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, $password, $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, $password, $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 option. 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{

(.*)

}) {$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{

(.*)

}) {$hostname = $1;}
    else {$hostname = 'unknown_device';}
    
    #Isolate CDP information
    if ($dirty_output =~ m{Port ID(.*)}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:
      # / or  
      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::errstr\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 writing.\n";

  print OUTPUT "Hostname,Switch,Port,VLAN,MAC\n";
  while (my ($mac, $arrayref) = each %$output_hash) {
    my (@row) = (@$arrayref[0], @$arrayref[1], @$arrayref[2], @$arrayref[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 () {
        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 () {
        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 () {
        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_array_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) = <
    
    
    Options:

    -s ...         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 ...       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                  Use cam-database from file.
                              (A substitute for -s)
                              
    -i                  Use mac-database from file.
                              (A substitute for -m)
                              
    -d                  Use host-map database from file.
                              (A substitute for -s and -m)

    
    -q ...       Query host-map for specified host(s).
                              (Used with -s & -m, or -d)
    
    -h                        Display help.
END
  print "$help\n";
} #End show_help