monkerz57 has asked for the wisdom of the Perl Monks concerning the following question:

So I took on a new job as a network engineer, but had no base line as to what all was in production within this network. I was then tasked with "truing up" our SMARTnet contract. The task seemed tedious and after a few days sitting on the project I decided to write a perl script to inventory the network for me. Below is the product of that brainstorming session. The script crawls the network looking for devices with telnet and/or ssh ports open and compiles a list of those devices. The script then uses that list and attempts to connect via telnet if open otherwise uses ssh. Could you please review and let me know what I may need to change to improve the script or gotchas I may have overlooked? Any and all feedback is greatly appreciated as I am still very new to perl.

use strict; use warnings; use Net::SSH2; use Net::Telnet; my $directory = 'C:/myDir'; my $aaaUser = 'myAaaUser'; my $aaaPass = 'myAaaPass'; my $localUser = 'myLocalUser'; my $localPass1 = 'myLocalPass1'; my $localPass2 = 'myLocalPass2'; my $telnetLog = "$directory/telnet_log.txt"; my $inventory = "$directory/inventory_log.txt"; #-- Run nmap against supernets for open ports ssh/telnet and print to +file for archiving qx{nmap 10.146.0.0/15 10.120.0.0/14 10.95.0.0/16 -p 22-23 -n -oG inven +tory_script_port_scan.txt}; my $portScanFileIn = "$directory/inventory_script_port_scan.txt"; #-- Read nmap output file into array for formatting. my @rawPortScan = do { open( my $fi, "<", $portScanFileIn ) or die "Couldn't open $portScanFileIn: $!"; <$fi>; }; #-- Format array to <ipAddress> <sshStatus> <telnetStatus> for only li +nes containing 'open' my @filteredPortScan = grep { /open/ } @rawPortScan; chomp @filteredPortScan; s/Host: // for @filteredPortScan; s/\s.*: / / for @filteredPortScan; s/\/tcp\/\/ssh\/\/\/\,// for @filteredPortScan; s/\/tcp\/\/telnet\/\/\/// for @filteredPortScan; s/ /;/ for @filteredPortScan; s/ /;/ for @filteredPortScan; my $portScanFileOut = "$directory/inventory_script_port_scan_FILTERED. +txt"; #-- Print formatted array to file for archiving. open( my $fo, '>', "$portScanFileOut" ) or die "Cannot open $portScanFileOut: $!"; for my $line (@filteredPortScan) { print $fo "$line\n"; } close $fo; #-- Read formatted port scan into array for use. open my $fh, '<', "$portScanFileOut"; chomp( my @list = <$fh> ); close $fh; #-- Open log files to append open( my $log, ">>", $telnetLog ) or die "Couldn't open telnet log file: $!"; open( my $inv, ">>", $inventory ) or die "Couldn't open inventory log file: $!"; #-- Classify each ip to attempt telnet or ssh session #-- If telnet port open, will push to telnet sub; else will push to ss +h sub. for my $node (@list) { if ( $node =~ /^(\d+\.\d+\.\d+\.\d+);(22\/.+);(23\/.+)/ ) { my ( $ip, $sshStatus, $telnetStatus ) = ( $1, $2, $3 ); if ( $telnetStatus =~ /open/ ) { logging("Trying $ip:23"); audit_hardware_using_telnet($ip); } else { logging("Trying $ip:22"); audit_hardware_using_ssh($ip); } } } sub audit_hardware_using_ssh { my ($ip) = @_; my $ssh = Net::SSH2->new(); #-- Attempt ssh to ip; if cannot connect log and move on. if ( !$ssh->connect($ip) ) { logging(" --> ssh connection failed\n"); return; } else { logging(" --> ssh connected"); } #-- Attempt authentication; if cannot authenticate log and move on +. if ( !$ssh->auth_password( $aaaUser, $aaaPass ) ) { logging(" --> SSH Authentication Failed\n"); return; } else { logging(" --> ssh auth succeeded.\n"); } #-- Open channel and read all to array. my $channel = $ssh->channel(); $channel->blocking(0); $channel->shell(); sleep(2); print $channel "show inventory\n"; my @output = <$channel>; $channel->close; $ssh->disconnect; #-- Format output for use in inventory file. my @hostname = grep /[#>] $|[#>]$/, @output; s/[#>]// for @hostname; my @showInventory = grep /NAME:|PID:/i, @output; @showInventory = grep /\S/, @showInventory; #-- Move first two lines of array to scalars, then print hostname, + ip #-- and each hardware item to inventory file on single line delimi +ted. while (@showInventory) { my $invLine1 = shift @showInventory; my $invLine2 = shift @showInventory; $invLine1 =~ s/\r|\n//g; $invLine2 =~ s/\r|\n//g; print $inv "@hostname, $ip, $invLine1, $invLine2\n"; } } sub audit_hardware_using_telnet { my ($ip) = @_; my $t = new Net::Telnet( Host => $ip, Timeout => 15, Prompt => '/\S+[#] ?$/', Errmode => 'return' ); #-- Attempt telnet to ip; if cannot connect log and move on. if ( !defined $t ) { logging(" --> telnet connection failed.\n"); return; } else { logging(" --> telnet connected.\n"); } #-- Match initial login prompt and continue to respective login se +quence. my ( $prematch, $match ) = $t->waitfor( Match => '/Login as: ?|Username: ?|Login: ?/i', Match => '/Password: ?/' ); #-- If cannot match initial login prompt, log and move on. if ( !defined $match ) { logging("$ip: ERROR_1\n"); return; } #-- If initial prompt received was (Login as:, Username:, Login:) +push to the acs_local login sequence. if ( $match =~ /Login as: ?|Username: ?|Login: ?/i ) { acs_local_login( $ip, $t ); } #-- If initial prompt received was (Password:) push to the telnet +login sequence. elsif ( $match =~ /Password: ?/ ) { telnet_login( $ip, $t ); } } sub acs_local_login { my ( $ip, $t ) = (@_); #-- Print aaa username for initial prompt, then wait for #-- password prompt. If no prompt, log and move on. $t->print("$aaaUser"); if ( !$t->waitfor('/Password: ?/i') ) { logging("$ip: ERROR_2\n"); return; } #-- Print aaa password and wait for prompt. If "Username:" receive +d #-- move to second login attempt. If user-exec prompt achieved, p +roceed #-- to enable sequence. If priv-exec prompt received, continue. $t->print("$aaaPass"); my ( $prematch, $match ) = $t->waitfor( Match => '/Username: ?/', Match => '/\S+[>] ?$/', Match => $t->prompt ); #-- Attempt second login using local database username, then #-- wait for password prompt. If no prompt, log and move on. if ( $match =~ /Username: ?/ ) { $t->print("$localUser"); if ( !$t->waitfor('/Password: ?/i') ) { logging("$ip: ERROR_3\n"); return; } #-- Attempt second login using local database password #1. If +"Username:" #-- received move to third login attempt. If user-exec prompt + achieved, #-- proceed to enable sequence. If priv-exec prompt received, +continue. $t->print("$localPass1"); my ( $prematch, $match ) = $t->waitfor( Match => '/Username: ?/', Match => '/\S+[>] ?$/', Match => $t->prompt ); #-- Attempt third login using local database username, then #-- wait for password prompt. If no prompt, log and move on. if ( $match =~ /Username: ?/ ) { $t->print("$localUser"); if ( !$t->waitfor('/Password: ?/i') ) { logging("$ip: ERROR_4\n"); return; } #-- Attempt third login using local database password #2. +If user-exec prompt #-- achieved, proceed to enable sequence. If priv-exec pro +mpt received, continue. $t->print("$localPass2"); my ( $prematch, $match ) = $t->waitfor( Match => '/\S+[>] ?$/', Match => $t->prompt ); #-- Third login attempt returned user-exec prompt. Attempt + to gain priv-exec by #-- requesting enable. If password prompt is not received + log and move on. if ( $match =~ /\S+[>] ?$/ ) { $t->print('enable'); if ( !$t->waitfor('/Password: ?/i') ) { logging("$ip: ERROR_5\n"); return; } #-- Print local database password #2 to gain priv-exec +, then wait for prompt. $t->print("$localPass2"); my ( $prematch, $match ) = $t->waitfor( Match => $t->p +rompt ); } #-- If could not match a prompt after third login attempt, + log and move on. if ( !defined $match ) { logging("$ip: ERROR_6\n"); return; } } #-- Second login attempt returned user-exec prompt. Attempt to + gain priv-exec by #-- requesting enable. If password prompt is not received log + and move on. if ( $match =~ /\S+[>] ?$/ ) { $t->print('enable'); if ( !$t->waitfor('/Password: ?/i') ) { logging("$ip: ERROR_7\n"); return; } #-- Print local database password #1 to gain priv-exec, th +en wait for prompt. $t->print("$localPass1"); my ( $prematch, $match ) = $t->waitfor( Match => $t->promp +t ); } #-- If could not match a prompt after second login attempt, lo +g and move on. if ( !defined $match ) { logging("$ip: ERROR_8\n"); return; } } #-- Initial login attempt returned user-exec prompt. Attempt to ga +in priv-exec by #-- requesting enable. If password prompt is not received log and + move on. if ( $match =~ /\S+[>] ?$/ ) { $t->print('enable'); if ( !$t->waitfor('/Password: ?/i') ) { logging("$ip: ERROR_9\n"); return; } #-- Print aaa password to gain priv-exec, then wait for prompt +. $t->print("$aaaPass"); my ( $prematch, $match ) = $t->waitfor( Match => $t->prompt ); } #-- If could not match a prompt after initial login attempt, log a +nd move on. if ( !defined $match ) { logging("$ip: ERROR_10\n"); return; } #-- After acs_local login, proceed to information gathering sub. get_inventory( $ip, $t ); } sub telnet_login { my ( $ip, $t ) = (@_); #-- Print telnet line password #1 for initial password prompt. If +user-exec #-- prompt achieved, proceed to enable sequence. If "Password:" re +ceived move #-- to second telnet line login attempt. If priv-exec prompt recei +ved, continue. $t->print("$localPass1"); my ( $prematch, $match ) = $t->waitfor( Match => '/\S+[>] ?$/', Match => '/[Pp]assword: ?$/', Match => $t->prompt ); #-- Initial telnet line login attempt returned user-exec prompt. A +ttempt to gain priv-exec #-- by requesting enable. If password prompt is not received log +and move on. if ( $match =~ /\S+[>] ?$/ ) { $t->print('enable'); if ( !$t->waitfor('/[Pp]assword: ?/') ) { logging("$ip: ERROR_11\n"); return; } #-- Print telnet line password #1 to gain priv-exec, then wait + for prompt. $t->print("$localPass1"); my ( $prematch, $match ) = $t->waitfor( Match => $t->prompt ); } #-- Attempt second login using telnet line password #2. If user-ex +ec prompt #-- achieved, proceed to enable sequence. If priv-exec prompt rece +ived, continue. if ( $match =~ /[Pp]assword: ?$/ ) { $t->print("$localPass2"); my ( $prematch, $match ) = $t->waitfor( Match => '/\S+[>] ?$/', Match => $t->prompt ); #-- Second telnet line login attempt returned user-exec prompt +. Attempt to gain priv-exec #-- by requesting enable. If password prompt is not received +log and move on. if ( $match =~ /\S+[>] ?$/ ) { $t->print('enable'); if ( !$t->waitfor('/[Pp]assword: ?/') ) { logging("$ip: ERROR_12\n"); return; } #-- Print telnet line password #2 to gain priv-exec, then +wait for prompt. $t->print("$localPass2"); my ( $prematch, $match ) = $t->waitfor( Match => $t->promp +t ); } #-- If could not match a prompt after second telnet line login + attempt, log and move on. if ( !defined $match ) { logging("$ip: ERROR_13\n"); return; } } #-- If could not match a prompt after initial telnet line login at +tempt, log and move on. if ( !defined $match ) { logging("$ip: ERROR_14\n"); return; } #-- After telnet login, proceed to information gathering sub. get_inventory( $ip, $t ); } sub get_inventory { my ( $ip, $t ) = (@_); #-- Once logged in change terminal length, then store output of ho +stname and inventory. if ( !$t->cmd('terminal length 0') ) { logging("$ip: Error changing terminal length\n"); } #-- Format output for use in inventory file. my @showRun = $t->cmd('show run | inc hostname'); chomp( my @hostname = grep /hostname/, @showRun ); s/hostname // for @hostname; chomp( my @showInventory = $t->cmd('show inventory') ); @showInventory = grep /\S/, @showInventory; #-- Move first two lines of array to scalars, then print hostname, + ip #-- and each hardware item to inventory file on single line delimi +ted. while (@showInventory) { my $invLine1 = shift @showInventory; my $invLine2 = shift @showInventory; print $inv "$hostname[0], $ip, $invLine1, $invLine2\n"; } } sub logging { #-- Consolidates log printing into one liner. my @args = @_; print @args; print $log @args; }

  • Comment on Could you suggest improvements to my Cisco network device audit script?
  • Download Code

Replies are listed 'Best First'.
Re: Could you suggest improvements to my Cisco network device audit script?
by GrandFather (Saint) on Dec 23, 2014 at 01:22 UTC

    There is a mixture of coding styles there. Some good, some bad:

    • Always use three parameter open and lexical file handles
      good: open my $fi, "<", $portScanFileIn or die "...";
      bad: open LOG, ">>$backup_log" or die "...";
    • Always use explicit loop variables
      good: for my $ip (@ssh_queue) {
      bad: foreach (@filtered_portscan)

    In many places you test if (!defined $match) { after using (the possibly undefined) $match in earlier tests. Check that the order of your tests makes sense.

    An if that ends in a return doesn't need an else. Instead:

    if (...) { ... return; } ... # "else" stuff

    In many places you print to stdout and to a log file. Why not have a log sub that prints to both?

    Don't put the loop body on the same line as the loop statement. The loop construct as a statement modifier is ok though:

    while (<$channel>) {push @output, $_} #becomes push @output, $_ while <$channel>; #although my @output = <$channel>; #is even better
    for my $i (@show_inventory) { my $inv_line1 = shift @show_inventory; my $inv_line2 = shift @show_inventory;

    is plain wrong. Maybe you meant:

    while (@show_inventory) { my $inv_line1 = shift @show_inventory; my $inv_line2 = shift @show_inventory;
    Perl is the programming world's equivalent of English
Re: Could you suggest improvements to my Cisco network device audit script?
by roboticus (Chancellor) on Dec 23, 2014 at 03:32 UTC

    monkerz57:

    I notice a good few variations of:

    if (condition) { print "Some string\n"; print LOG "Some string\n"; return; }

    It's frequent enough that I'd do something about it to simplify things a bit. I think I'd first create a subroutine like:

    sub LOGIT { my @args = @_; print @args, "\n"; print LOG @args, "\n"; }

    Then I'd probably use it like so:

    condition or return LOGIT("some string");

    Some variation of that might help you make the code a little easier on the eyes.

    ...roboticus

    When your only tool is a hammer, all problems look like your thumb.

Re: Could you suggest improvements to my Cisco network device audit script?
by Anonymous Monk on Dec 23, 2014 at 00:27 UTC
    That's a lot of code.

    Well, as for suggestions, use perltidy. Hopefully you see why.

    if (!defined $match) { print "$ip: ERROR_1\n"; print LOG "$ip: ERROR_1\n"; return; } ... if (!defined $match) { print "$ip: ERROR_13\n"; print LOG "$ip: ERROR_13\n"; return; } ... sub get_inventory { my ($ip, $t) = (@_); if (!$t->cmd('terminal length 0')) { print "$ip: Error changing terminal length\n"; print LOG "$ip: Error changing terminal length\n"; } my @show_run = $t->cmd('show run | inc hostname'); ...
    Did you assemble this stuff from different sources?
      Thanks for the reply. I updated the initial question code with perlTidy's output. I did google quite a bit for the ssh portion of code. I couldn't get the channel to function as I wanted using cpan's example code. The telnet portion was created from a simple telnet script a co-worker created a few years ago, however I added quite a few conditionals and login subs due to a lack of standardization within the environment. I couldn't source the tidbits if I tried, if that is what you are getting at. Thanks, Monk
Re: Could you suggest improvements to my Cisco network device audit script?
by trippledubs (Deacon) on Dec 23, 2014 at 05:00 UTC

    I think you would be better served by using the xml output of nmap and parsing that with a module like XML::Twig instead of parsing your own. Looks like we are past that design decision though so moving on...

    In the section for my $node(@list) {... You assign at most one element into the arrays @telnet_queue and @ssh_queue. There is no use in looping over an array of one or zero elements. That could be rewritten.

    if ($telnet_status =~ /open/) { logall("Trying $ip_addresss"); audit_hardware_using_telnet($ip_address); }
    You can also chomp at the same time that you read a filehandle, for example:
    chomp(my @list = <$fh>);
Re: Could you suggest improvements to my Cisco network device audit script?
by jmlynesjr (Deacon) on Dec 23, 2014 at 01:27 UTC

    Use readmore tags around long code segments. And a personal pet peeve - add comments for the guy that takes over after you move on.

    James

    There's never enough time to do it right, but always enough time to do it over...

      Use readmore tags around long code segments.

      Do readmore tags affect code blocks?

      Cheers, Sören

      Créateur des bugs mobiles - let loose once, run everywhere.
      (hooked on the Perl Programming language)

        Not that I know of, but makes scrolling through SoPW posts quicker.

        James

        There's never enough time to do it right, but always enough time to do it over...

Re: Could you suggest improvements to my Cisco network device audit script?
by monkerz57 (Initiate) on Dec 23, 2014 at 21:09 UTC
    Thank you all for your suggestions. I have updated the initial question is the revised code. Please feel free to critique. I always like finding or pointed in the direction of a more efficient way to do something.