File Coverage

blib/lib/PIX/Walker.pm
Criterion Covered Total %
statement 24 193 12.4
branch 0 80 0.0
condition 0 28 0.0
subroutine 7 30 23.3
pod 12 18 66.6
total 43 349 12.3


line stmt bran cond sub pod time code
1             package PIX::Walker;
2              
3 1     1   22893 use strict;
  1         3  
  1         34  
4 1     1   6 use warnings;
  1         1  
  1         27  
5              
6 1     1   5 use Carp;
  1         6  
  1         78  
7 1     1   533 use PIX::Object;
  1         3  
  1         28  
8 1     1   569 use PIX::Accesslist;
  1         2  
  1         37  
9              
10             BEGIN {
11 1     1   7 use Exporter;
  1         2  
  1         116  
12              
13 1     1   2 our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
14              
15 1         3 $VERSION = '1.10';
16              
17 1         17 @ISA = qw(Exporter);
18 1         2 @EXPORT = qw();
19 1         3 @EXPORT_OK = qw();
20 1         2661 %EXPORT_TAGS = ();
21             }
22              
23             =pod
24              
25             =head1 NAME
26              
27             PIX::Walker - Process Cisco PIX configs and 'walk' access-lists
28              
29             =head1 SYNOPSIS
30              
31             PIX::Walker is an object that allows you to process PIX (and ASA) firewall
32             configs and 'walk' an access-list for matches. PIX OS versions 6 and 7 are
33             supported. Note, ACL's that use the 'interface' keyword will not match properly
34             since there is no way for the Walker to match an IP to an interface, at least
35             not yet.
36              
37             'Loose' ACL matching performed. This means that you can specify as little as a
38             single IP to match what line(s) that IP would match in the ACL on the firewall.
39             Or you can provide every detail including source/dest IPs, ports, and protocol
40             to match a specific line of an ACL. Loose matching allows you to see potential
41             lines in a large ruleset that a single source or destination IP might match.
42              
43             More than just the first line match can be returned. If your search criteria can
44             technically match multiple lines they will all be returned. This is useful for
45             seeing extra matches in your ACL that might also match and can help you optimize
46             your ACL.
47              
48             =head1 EXAMPLE
49              
50             use PIX::Walker;
51              
52             my $config = "... firewall config buffer or filename ...";
53             my $fw = new PIX::Walker($config);
54             my $acl = $fw->acl("outside_access") || die("ACL does not exist");
55              
56             my $matched = 0;
57             # search each line of the ACL for possible matches
58             foreach my $line ($acl->lines) {
59             if ($line->match(
60             source => "10.0.1.100",
61             dest => "192.168.1.3",
62             dport => "80", # dest port
63             proto => "tcp")) {
64             if (!$matched++) {
65             print "Matched ACL " . $acl->name .
66             " (" . $acl->elements . " ACE)\n";
67             }
68             print $line->print, "\n";
69             }
70             }
71              
72             =head1 METHODS
73              
74             =over
75              
76             =cut
77              
78             =item B
79              
80             =over
81              
82             Returns a new PIX::Walker object using the $config string passed in. The
83             configuration is processed and broken out into various objects automatically.
84              
85             The $config string is either a full string buffer containing the configuration
86             of a firewall or is used as a filename to read the configuration from, using
87             various filename formats (tried with and without any extension on the filename)
88              
89             * {$config}
90             * {$config}.conf
91            
92             If $not_a_file is true then the $config string is never checked against the
93             file system.
94              
95             =back
96              
97             =cut
98             sub new {
99 0     0 1   my $proto = shift;
100 0   0       my $class = ref($proto) || $proto;
101 0           my $self = { debug => 0 };
102 0           my ($fw_config, $not_a_file) = @_;
103 0           my $conf;
104 0 0         croak("Must provide firewall configuration") unless $fw_config;
105              
106 0           bless($self, $class);
107              
108 0           $self->{firewall} = '';
109              
110 0           my ($file, $host);
111              
112 0           $file = (split(/\n/, $fw_config, 2))[0];
113 0           $host = (split(/\./, $file, 2))[0];
114              
115 0 0 0       if (!$not_a_file and -f $file) {
    0 0        
    0 0        
    0 0        
116 0 0         open(F, "<$file") or croak("Error opening file for reading: $!");
117 0           $conf = join('', );
118 0           close(F);
119             } elsif (!$not_a_file and -f "$file.conf") {
120 0 0         open(F, "<$file.conf") or croak("Error opening file for reading: $!");
121 0           $conf = join('', );
122 0           close(F);
123             } elsif (!$not_a_file and -f $host) {
124 0 0         open(F, "<$host") or croak("Error opening file for reading: $!");
125 0           $conf = join('', );
126 0           close(F);
127             } elsif (!$not_a_file and -f "$host.conf") {
128 0 0         open(F, "<$host.conf") or croak("Error opening file for reading: $!");
129 0           $conf = join('', );
130 0           close(F);
131             } else {
132 0           $conf = $fw_config;
133             }
134 0 0         croak("No firewall configuration found") unless $conf;
135 0           $self->{config} = [ split(/\n/, $conf) ];
136 0           $self->{config_block} = [ split(/\n/, $conf) ];
137              
138 0           $self->_init;
139 0           $self->_process;
140              
141 0           return $self;
142             }
143              
144             sub _init {
145 0     0     my $self = shift;
146 0           $self->{objects} = {};
147 0           $self->{acls} = {};
148 0           $self->{alias} = {};
149 0           $self->{ports} = {
150             # insert static entries here...
151             'imap4' => '143',
152             'h323' => '1720',
153             'sqlnet' => '1521',
154             'pcanywhere-data' => '5631',
155             'pcanywhere-status' => '5632',
156             'citrix-ica' => '1494',
157              
158             # cisco PIX defined
159             # (there may be more now; I have not updated this in awhile)
160             'aol' => '5190',
161             'bgp' => '179',
162             'biff' => '512',
163             'bootpc' => '68',
164             'bootps' => '67',
165             'chargen' => '19',
166             'cmd' => '514',
167             'rsh' => '514',
168             'daytime' => '13',
169             'discard' => '9',
170             'domain' => '53',
171             'dnsix' => '195',
172             'echo' => '7',
173             'exec' => '512',
174             'finger' => '79',
175             'ftp' => '21',
176             'ftp-data' => '20',
177             'gopher' => '70',
178             'hostname' => '101',
179             'https' => '443',
180             'nameserver' => '42',
181             'ident' => '113',
182             'irc' => '194',
183             'isakmp' => '500',
184             'klogin' => '543',
185             'kshell' => '544',
186             'ldap' => '389',
187             'ldaps' => '636',
188             'lotusnotes' => '1352',
189             'lpd' => '515',
190             'login' => '513',
191             'mobile-ip' => '434',
192             'netbios-ns' => '137',
193             'netbios-dgm' => '138',
194             'netbios-ssn' => '139',
195             'nntp' => '119',
196             'ntp' => '123',
197             'pim-auto-rp' => '496',
198             'pop2' => '109',
199             'pop3' => '110',
200             'pptp' => '1723',
201             'radius-acct' => '1813',
202             'rip' => '520',
203             'rtsp' => '554',
204             'sip' => '5060',
205             'smtp' => '25',
206             'snmp' => '161',
207             'snmptrap' => '162',
208             'ssh' => '22',
209             'sunrpc' => '111',
210             'syslog' => '514',
211             'tacacs' => '49',
212             'talk' => '517',
213             'telnet' => '23',
214             'tftp' => '69',
215             'time' => '37',
216             'uucp' => '540',
217             'who' => '513',
218             'whois' => '43',
219             'www' => '80',
220             'xdmcp' => '177',
221             };
222              
223             # Look for services files (nmap is better) and build a translation
224             # table. This reads ALL the files listed and merges the results into a
225             # single hash lookup table. the first name-to-port lookup found is used
226             # and is not overwritten. This obviously only works on Linux.
227 0           my @files = qw( /usr/local/share/nmap/nmap-services /usr/share/nmap/nmap-services /etc/services );
228 0           while (defined(my $file = shift @files)) {
229 0 0         next unless -f $file;
230 0 0         open(F, "<$file") or next;
231 0           while (defined(my $line = )) {
232 0           $line =~ s/^\s+//;
233 0           $line =~ s/\s+$//;
234 0 0 0       next if $line eq '' or $line =~ /^#/;
235 0           my ($name, $str) = split(/\s+/, $line);
236 0           my $port = (split(/\//, $str))[0];
237 0 0         $self->{ports}{$name} = $port unless exists $self->{ports}{$name};
238             }
239 0           close(F);
240             }
241             }
242              
243             # INTERNAL: processes the configuration and breaks things apart into different bits
244             sub _process {
245 0     0     my $self = shift;
246              
247             # continue until all config lines are gone
248 0           while (defined(my $line = $self->_nextline)) {
249 0 0         if ($line =~ /^object-group (\S+) (\S+)/i) {
    0          
    0          
250 0           my ($type, $name) = ($1,$2);
251 0           my $conf = [ $line ];
252 0           $line = $self->_nextline;
253 0   0       while (defined $line && $line =~ /^\s*(\w+-object|desc)/) {
254 0           push(@$conf, $line);
255 0           $line = $self->_nextline;
256             }
257 0           $self->_rewind($line); # rewind 1 line so we don't skip past it on the next iteration
258 0           $self->add_obj($type, $name, $conf);
259             } elsif ($line =~ /^access-list (\S+)/) {
260 0           my $name = $1;
261 0 0         next if $name eq 'compiled';
262 0           my $conf = [ $line ];
263 0           $line = $self->_nextline;
264 0   0       while (defined $line && $line =~ /^access-list $name/) {
265 0           push(@$conf, $line);
266 0           $line = $self->_nextline;
267             }
268 0           $self->_rewind($line);
269 0           $self->add_acl($name, $conf);
270              
271             } elsif ($line =~ /^name (\S+) (\S+)/) { # ignore descriptions
272 0           $self->{alias}{$2} = $1;
273             }
274             }
275              
276             }
277              
278             =item B
279              
280             =over
281              
282             Returns an PIX::Accesslist object for the ACL named by $name.
283              
284             =back
285              
286             =cut
287             sub acl {
288 0     0 1   my $self = shift;
289 0           my $name = shift;
290 0 0         return exists $self->{acls}{$name} ? $self->{acls}{$name} : undef;
291             }
292              
293             =item B
294              
295             =over
296              
297             Returns an array of access-list strings for each access-list
298             found in the firewall configuration. Returns undef if there is no
299             matching ACL. Use walker->acl('acl_name') to retrieve the actual
300             PIX::Accesslist object.
301              
302             =back
303              
304             =cut
305 0     0 1   sub acls { keys %{$_[0]->{acls}} }
  0            
306              
307             =item B
308              
309             =over
310              
311             Add's an access-list object to the PIX::Walker object. $conf is an arrayref
312             of the configuration lines that make up the access-list and can be empty.
313              
314             =back
315              
316             =cut
317             sub add_acl {
318 0     0 1   my ($self, $name, $conf) = @_;
319 0   0       return $self->{acls}{$name} = new PIX::Accesslist($name, $conf || [], $self);
320             }
321              
322             =item B
323              
324             =over
325              
326             Add's an object-group object to the PIX::Walker object. $conf is an arrayref
327             of the configuration lines that make up the object-group and can be empty.
328              
329             =back
330              
331             =cut
332             sub add_obj {
333 0     0 1   my ($self, $type, $name, $conf) = @_;
334 0   0       return $self->{objects}{$name} = new PIX::Object($type, $name, $conf || [], $self);
335             }
336              
337             =item B
338              
339             =over
340              
341             Returns the IP of the alias given in $alias. If no alias is found than the
342             string is returned unchanged.
343              
344             =back
345              
346             =cut
347             sub alias {
348 0     0 1   my $self = shift;
349 0           my $alias = shift;
350 0 0         return exists $self->{alias}{$alias} ? $self->{alias}{$alias} : $alias;
351             }
352              
353             =item B
354              
355             =over
356              
357             Matches the IP to an existing network-group. Does not validate it within any ACL.
358             If a single group is matched a scalar is returned with the name, otherwise an
359             array reference is returned containing all matches.
360              
361             * I<$ip> is an IP address to look for.
362              
363             * I<$trace> is an optional reference to a trace buffer.
364              
365             If an IP is found in a nested group the trace will allow you to find out where
366             it was nested. See L for more information.
367              
368             =back
369              
370             =cut
371             sub findip {
372 0     0 1   my ($self, $ip, $trace) = @_;
373 0           my $found = [];
374              
375 0           foreach my $obj (keys %{$self->{objects}}) {
  0            
376 0           my $grp = $self->{objects}{$obj};
377 0 0         next unless $grp->type eq 'network'; # we only care about network groups
378 0           my $localtrace = [];
379 0           my $match = $grp->matchip($ip, $localtrace);
380 0 0         if ($match) {
381 0 0         push(@$trace, $localtrace) if defined $trace;
382 0           push(@$found, $match);
383             }
384             }
385 0 0         if (scalar @$found) {
386 0           my %u;
387 0           my @uniq = grep { !$u{$_}++ } sort @$found;
  0            
388 0 0         return (scalar @uniq == 1) ? $uniq[0] : \@uniq;
389             }
390 0           return undef;
391             }
392              
393             =item B
394              
395             =over
396              
397             Matches the PORT to an existing service-group. Does not validate it within any ACL.
398             If a single group is matched a scalar is returned with the name, otherwise an
399             array reference is returned containing all matches.
400              
401             * I<$port> is the PORT to look for.
402              
403             * I<$trace> is an optional reference to a trace buffer.
404              
405             If a PORT is found in a nested group the trace will allow you to find out where
406             it was nested. See L for more information.
407              
408             =back
409              
410             =cut
411             sub findport {
412 0     0 1   my ($self, $port, $trace) = @_;
413 0           my $found = [];
414              
415 0           foreach my $obj (keys %{$self->{objects}}) {
  0            
416 0           my $grp = $self->{objects}{$obj};
417 0 0         next unless $grp->type eq 'service'; # we only care about service groups
418 0           my $localtrace = [];
419 0           my $match = $grp->matchport($port, $localtrace);
420 0 0         if ($match) {
421 0 0         push(@$trace, $localtrace) if defined $trace;
422 0           push(@$found, $match);
423             }
424             }
425 0 0         if (scalar @$found) {
426 0           my %u;
427 0           my @uniq = grep { !$u{$_}++ } sort @$found;
  0            
428 0 0         return (scalar @uniq == 1) ? $uniq[0] : \@uniq;
429             }
430 0           return undef;
431             }
432              
433             =item B
434              
435             =over
436              
437             Returns an B object for the object-group that matches the $name given.
438              
439             =back
440              
441             =cut
442             sub obj {
443 0     0 1   my $self = shift;
444 0           my $name = shift;
445 0 0         return exists $self->{objects}{$name} ? $self->{objects}{$name} : undef;
446             }
447              
448             =item B
449              
450             =over
451              
452             Returns an array of object-group strings for each object-group found in the
453             firewall configuration. If $type is specified only groups matching that type
454             are returned.
455              
456             Returns undef if there are no object-groups. Use walker->obj('obj_name') to
457             retreive the actual PIX::Object object.
458              
459             =back
460              
461             =cut
462             sub objs {
463 0     0 1   my ($self, $type) = @_;
464 0           $type = lc $type;
465 0 0         if ($type) {
466 0           return grep { $self->{objects}{$_}->type eq $type } keys %{$self->{objects}};
  0            
  0            
467             } else {
468 0           return keys %{$self->{objects}};
  0            
469             }
470             }
471              
472             =item B
473              
474             =over
475              
476             Returns the port NUMBER of the port name given. This function will DIE() if the
477             port name is not known. This is harsh because the routines that use this
478             function will not work if a single port lookup fails (not being able to lookup
479             a port number can cause some of your acl searching to fail). This function is
480             meant to be used internally only.
481              
482             =back
483              
484             =cut
485             sub portnum {
486 0     0 1   my ($self, $port) = @_;
487 0 0         return $port if $port =~ /^\d+$/;
488             # using die() below is a bit harsh but I don't have a better way to deal with it for now.
489 0 0         return exists $self->{ports}{$port} ? $self->{ports}{$port} : die("Unknown port name '$port'");
490             }
491              
492             =item B
493              
494             =over
495              
496             Prints out the trace dump given. This will allow you to see where IP's and PORT's are being
497             matched within their object-groups even if they are nested.
498              
499             =over
500              
501             $matched = $fw->findip($ip, $trace);
502             $fw->tracedump($trace);
503              
504             =back
505              
506             =cut
507             sub tracedump {
508 0     0 1   my ($self, $trace) = @_;
509 0 0         return '' unless defined $trace;
510 0 0         print "\nMatch Trace: \n" if @$trace;
511             # use Data::Dumper; print Dumper($trace); return;
512 0           foreach my $tr (@$trace) {
513 0           my $idx = 0;
514 0           for (my $i=0; $i<@$tr; $i=$i+3) {
515 0           my ($name, $line, $extra) = @$tr[$i..$i+2];
516             # print "\t"x($idx++) . $name;
517 0 0         print " -> " if $idx++;
518 0           print $name;
519 0 0         print " (match: $extra)" if $extra;
520 0 0         print " (idx: $line)" if $line;
521             # print "\n";
522             }
523 0           print "\n";
524             }
525 0           print "\n";
526             }
527              
528 0     0     sub _nextline { shift @{$_[0]->{config_block}} }
  0            
529 0     0     sub _rewind { unshift @{$_[0]->{config_block}}, $_[1] }
  0            
530 0     0     sub _reset { $_[0]->{config_block} = $_[0]->{config} }
531              
532             sub total_config_lines {
533 0     0 0   my $self = shift;
534 0 0         return 0 unless defined $self->{config};
535 0           return scalar @{$self->{config}};
  0            
536             }
537 0     0 0   sub total_network_objects { my $self=shift; return scalar grep { $self->{objects}{$_}->{class} =~ /network$/ } keys %{$self->{objects}} }
  0            
  0            
  0            
538 0     0 0   sub total_service_objects { my $self=shift; return scalar grep { $self->{objects}{$_}->{class} =~ /service$/ } keys %{$self->{objects}} }
  0            
  0            
  0            
539 0     0 0   sub total_protocol_objects { my $self=shift; return scalar grep { $self->{objects}{$_}->{class} =~ /protocol$/ } keys %{$self->{objects}} }
  0            
  0            
  0            
540 0     0 0   sub total_icmp_type_objects { my $self=shift; return scalar grep { $self->{objects}{$_}->{class} =~ /icmp_type$/ } keys %{$self->{objects}} }
  0            
  0            
  0            
541 0     0 0   sub total_object_groups { return scalar keys %{$_[0]->{objects}} }
  0            
542             ##sub total_acl_lines { return scalar @{$_[0]->{acl}} }
543              
544             1;
545              
546             =head1 AUTHOR
547              
548             Jason Morriss
549              
550             =head1 BUGS
551              
552             Please report any bugs or feature requests to
553             C, or through the web interface at
554             L.
555             I will be notified, and then you'll automatically be notified of progress on
556             your bug as I make changes.
557              
558             =head1 SUPPORT
559              
560             perldoc PIX::Walker
561              
562             perldoc PIX::Accesslist
563             perldoc PIX::Accesslist::Line
564              
565             perldoc PIX::Object
566             perldoc PIX::Object::network
567             perldoc PIX::Object::service
568             perldoc PIX::Object::protocol
569             perldoc PIX::Object::icmp_type
570              
571             =head1 ACKNOWLEDGEMENTS
572              
573             B - For pushing me to make this module and for supplying me with
574             endless ideas.
575              
576             =head1 COPYRIGHT & LICENSE
577              
578             Copyright 2006-2008 Jason Morriss, all rights reserved.
579              
580             This program is free software; you can redistribute it and/or modify it
581             under the same terms as Perl itself.
582              
583             =cut