Getting
to Know Your Network -- Part I
Luis Enrique Muñoz
If your job is like mine, you've heard the words "vulnerability
management" a lot during the past couple of years. Generally speaking,
"VM" comprises all the tasks we must do -- such as patching, device
and machine inventories, and security audits -- to keep our networks
working despite the various software flaws exploited by malware.
There is an endless stream of vulnerability information generated
daily from a variety of sources. Some of those vulnerabilities affect
specific combinations of software and operating systems. When you
just have a few dozen machines to worry about, assessing whether
a particular vulnerability is the next big one or just a blip on
the radar is relatively easy. Throw in a few thousand machines in
a nationwide corporate network, however, and it becomes more of
a challenge.
Some types of information are very useful in managing this task
-- a network map describing (in general terms) where each IP subnet
in our network is and a complete device inventory, for example.
In this series of articles, I will gather all this information with
the use of a few open source tools, a bit of Perl, and very little
effort. It turns out that a lot of this information could be found
if we had a tool allowing us to assemble commands on the fly, send
them to network devices, and work on the results. I'll show how
to build exactly that.
We will need just a couple of Perl modules from the CPAN toolbox,
which we will be using throughout these articles. These modules
will help us with connecting and sending commands to the Cisco devices
that we will be focusing on this series -- parsing configuration
files, manipulating IP addresses, and abstracting database tables
and rows into Perl objects, respectively.
Installing these modules is easy. Just do this as root:
# perl -MCPAN -e shell
cpan> install Net::Telnet::Cisco
...
cpan> install Cisco::Reconfig
...
cpan> install NetAddr::IP
...
cpan> install Class::DBI
...
To put those modules to use, we will need a script allowing us to
organize or generate the commands and execute them automatically.
For this, I'll take a much longer script made for a similar task and
trim it down for our purposes. I'll call this script "aconfig" or
"Automatic CONFIGuration". aconfig will take as input a file of IP
addresses or devices named with optional user-names and passwords
to log into the devices; it will perform commands from scripts we
will give it through the command line. A sample device list would
look like:
10.10.10.10 user password
10.10.10.11 user2 password2
...
The aconfig script will then execute automatic configuration scripts
-- let's call them "ascripts" -- against each of the network devices
provided. Ascripts can be quite powerful, featuring a simple language
that allows us to send commands to the device and also act on the
result. The language is defined as follows:
- Blank lines are ignored.
- Comments are preceded with %# and extend to the end
of the line.
- %INCLUDE file% is replaced by the contents of "file".
- %LABEL mylabel% is replaced by the empty string and
defines the named label, usable for flow control.
- %GOTO mylabel% causes the ascript to continue at the
location of the named label.
- %SGOTO scalar% causes the ascript to continue at the
label named "scalar" if the Perl scalar "$main::scalar" evaluates
to true.
- %RGOTO scalar% causes the ascript to continue at the
label whose name is contained in the Perl scalar "$main::scalar".
- %ENABLE% issues commands to put the device in "enable"
mode.
- %EXEC% issues commands to put the device in the "exec"
mode.
- The %[ ... ]% block evaluates its contents as if within
Perl double-quotes. The block must be contained on a single line.
- The %{ ... }% block evaluates everything inside it as
Perl code in the "main" name space.
Also, a few variables are available to Perl code:
- $main::TELNET -- This contains the Net::Telnet::Cisco
object used to interact with the current device.
- $main::ADDR -- The address or name of the device to
which the ascript is now connected.
- $main::LAST -- The results of the last successful command.
Let's take a quick look at the script providing all of this. We
will go through it superficially, because this is only an accessory
to what we'll be doing, and you can download the complete script
from the Sys Admin Web site at: http://www.sysadminmag.com.
The first few lines are quite common. Note that at line 3, we
place the script within its own name space. This is done to maintain
some isolation between the script's name space, and the name space
where the snippets of code within our ascripts will be executing.
In this case, we want to ensure that should one of those snippets
of code want to touch aconfig's variables, it can. At line 14, we
turn off line buffering, causing any output to be flushed immediately:
1: #!/usr/bin/perl
2:
3: package aconfig;
4: use strict;
5: use warnings;
6:
7: use IO::File;
8: use Getopt::Std;
9: use Net::Telnet::Cisco;
...
14: $|++;
On line 12, we define a small piece of Perl code that will be prepended
to each snippet to be executed. This places that code, by default,
in the "main" name space and disables "strict" and "warnings", as
most of those snippets will resemble one-liners and will likely benefit
from the non-strictness:
12: use constant PREAMBLE => qq{ no strict; no warnings; package main; };
The following lines take care of parsing any command-line options
passed to aconfig and setting some default values automatically. On
line 20, we allow our timeout to be "absolute" by prepending a "+"
to its value, in seconds:
10: use vars qw($opt_c $opt_e $opt_l $opt_t $opt_V $opt_v);
...
16: getopts('c:el:t:Vv');
17: $opt_t ||= 60; # Default timeout for commands
18: $opt_l ||= 20; # Default login timeout
20: $opt_t += time if $opt_t =~ /^\+\d+/;
For convenience, we'll define handlers for "warn" and "die" that add
useful debug information, such as the device's address and the time
the event occurred. This is done in lines 24 to 32:
24: sub warn_handler {
25: warn scalar(localtime(time))
26: . " [" . ($main::ADDR || $ip || 'no ip') . "]: ", @_;
27: }
28:
29: sub die_handler {
30: die scalar(localtime(time))
31: . " [" . ($main::ADDR || $ip || 'no ip') . "]: ", @_;
32: }
Lines 38-52 define a function that will read commands from a file
and return them in a list. This is used at lines 54-66 to handle the
%INCLUDE ...% directive and recursively build the complete
ascript we will be using:
38: sub _read_commands ($) {
39: my @c = ();
40:
41: for my $f (split(/,+/, shift))
42: {
43: my $fh = new IO::File $f
44: or die "Cannot open script file $f: $!\n";
45:
46: push @c, $fh->getlines;
47:
48: $fh->close;
49: }
50: # Strip comments away
51: return map { $_ =~ s/%#.*$//g; $_ } @c;
52: }
54: our @commands = _read_commands $opt_c;
56: for (my $line = 0; $line <= $#commands; $line ++)
57: {
58:
59: if ($commands[$line] =~ m!%INCLUDE ([^%]+)%!) {
60: die "Failed to include '$1' as referenced: $!\n"
61: unless -f $1;
62:
63: splice(@commands, $line, 1, _read_commands $1);
64: }
65:
66: }
Next, aconfig goes in a loop through all the devices, in which it
makes a copy of the ascript, creates and initializes the Net::Telnet::Cisco
object, and executes its contents line by line. This happens in lines
68 through 226:
Now let's do a little testing. For now, we'll work from the comfort
of our home directory. Let's create the following directories:
~/aconfig
~/aconfig/ascripts
~/aconfig/ascripts/include
~/aconfig/output
The idea is that useful and common ascript snippets should be placed
at ~/aconfig/ascripts/include, while our ascripts would be stored
at ~/aconfig/ascripts, thus keeping everything neatly organized. The
~/aconfig/output directory is where we will be putting the result
of our work.
Let's create a small ascript "include" that performs some basic
session configuration. We will call this script ~/aconfig/ascripts/include/setup,
and its contents will be:
terminal length 0
Simple enough. If needed, we can add commands to this later. Now,
our first script will be called ~/aconfig/ascripts/save-version and
it should store the result of the "show version" command on a file
under ~/aconfig/output. This will be useful later when we start building
our device database. The contents of the ascript would be:
%INCLUDE ascripts/include/setup%
show version
%{
package save::version;
use IO::File;
my $fh = new IO::File './output/' . $main::ADDR, "w"
or die "Failed to create output file: $!\n";
print $fh $main::LAST;
close $fh;
undef;
}%
Note that at the end of the Perl block, we return undef to mean that
no command must be sent to the device as a result. Now, let's put
the addresses or names, user names, and passwords of the devices we
want to work with into the file aconfig.hosts. Be very careful with
the permissions of this file, as likely you do not want anybody reading
it. Once you're ready, you can simply invoke aconfig from the ~/aconfig
directory as shown:
$ ./aconfig -Vv -c ascripts/save-version aconfig.hosts
Sat Oct 15 15:55:31 2005 [device-1]: begin
terminal length 0
show version
Sat Oct 15 15:55:32 2005 [device-1]: done
Sat Oct 15 15:55:32 2005 [device-2]: begin
terminal length 0
show version
Sat Oct 15 15:55:34 2005 [device-2]: done
...
And then, under ~/aconfig/output, you'll have one file for each device,
with the contents of the show version command indicating software
version and platform for each device. Normally, you would run this
without the -Vv flags out of a crontab. This would keep a nicely
updated directory of device configuration.
Let's take this a step further and add the configuration information
to what we're harvesting from our network. For this, let's modify
our scripts.
Let's write a simple "data-store" ascript that will invoke the
different components we will need. This ascript would look like
this:
$ cat ascripts/data-store
%INCLUDE ascripts/include/setup%
%INCLUDE ascripts/include/save-version%
%INCLUDE ascripts/include/save-config%
We will remove the %INCLUDE from our previous "save-version"
and put it under ascripts/include/save-version. We will use this as
a base to write ascripts/include/save-config, which will look like:
$ cat ascripts/include/save-config
show run
%{
package save::config;
use IO::Zlib;
my $fh = new IO::File './output/' . $main::ADDR . '.config', "w";
die "Failed to create output file: $!\n"
unless $fh;
print $fh $main::LAST;
close $fh;
undef;
}%
As you can see, both save-config and save-version share most of their
code. Only the actual filename of the resulting file changes. You
might want to factor the common code in its own include file, and
then simply use it to store the results of your command. I'll leave
this as an exercise for the reader.
All we have to do now is simply run this new script with:
$ ./aconfig -c ascripts/save-version aconfig.hosts
After a while, you'll end up with a lot of files under your ./output
directory. The ones with the ".version" extension contain the output
of the show version command. The ".config" extension surprisingly
will contain the output of the show run shorthand command we
used in our ascript. This directory will be our main data source,
as here we have a record of each device, each interface, and each
IP subnet in our network.
Note that you could also use many other data sources. You could
look at Cisco Discovery Protocol data on each device, or you could
issue more intricate commands. You could also peek in the routing
configuration, VLAN configuration, and many other variables. In
this regard, this article has simply provided a nice tool. The end
uses are up to you.
In the next article in this series, I'll show how to put this
information to use. Meanwhile, you may want to experiment with this
new tool, as there are many interesting things you can do.
Luis has been working in various areas of computer science
since the late 1980s. Some people blame him for conspiring to bring
the Internet into his home country, where currently he spends most
of his time teaching others about Perl and taking care of network
security at the largest ISP there as its CISO. He also believes
that being a sys admin is supposed to be fun. |