File Coverage

blib/lib/Farly/Rule/Optimizer.pm
Criterion Covered Total %
statement 178 211 84.3
branch 41 70 58.5
condition 14 27 51.8
subroutine 36 40 90.0
pod 9 13 69.2
total 278 361 77.0


line stmt bran cond sub pod time code
1             package Farly::Rule::Optimizer;
2            
3 1     1   596 use 5.008008;
  1         3  
  1         32  
4 1     1   4 use strict;
  1         1  
  1         25  
5 1     1   4 use warnings;
  1         2  
  1         23  
6 1     1   3 use Carp;
  1         2  
  1         57  
7 1     1   5 use Log::Any qw($log);
  1         2  
  1         7  
8            
9 1     1   571 use Farly::Template::Cisco;
  1         3  
  1         2682  
10            
11             our $VERSION = '0.26';
12            
13             sub new {
14 5     5 1 45 my ( $class, $rules ) = @_;
15            
16 5 50       20 confess "Farly::Object::List object required"
17             unless ( defined($rules) );
18            
19 5 50       39 confess "Farly::Object::List object required"
20             unless ( $rules->isa('Farly::Object::List') );
21            
22 5         26 my $self = {
23             RULES => $rules,
24             OPTIMIZED => Farly::Object::List->new(),
25             REMOVED => Farly::Object::List->new(),
26             P_ACTION => 'permit',
27             D_ACTION => 'deny',
28             MODE => 'L4',
29             PROTOCOLS => [ 0, 6, 17 ],
30             PROPERTIES => [ 'PROTOCOL', 'SRC_IP', 'SRC_PORT', 'DST_IP', 'DST_PORT' ],
31             VERBOSE => undef,
32             TEMPLATE => Farly::Template::Cisco->new('ASA'),
33             };
34            
35 5         21 bless $self, $class;
36            
37            
38 5         29 $log->info("$self NEW");
39 5         37 $log->info( "$self RULES " . $self->{RULES} );
40            
41             #validate input rule set
42 5         27 $self->_is_valid_rule_set();
43 3         18 $self->_is_expanded();
44            
45 3         20 return $self;
46             }
47            
48 34     34 0 157 sub rules { return $_[0]->{RULES}; }
49 5     5 1 37 sub optimized { return $_[0]->{OPTIMIZED}; }
50 1     1 1 5 sub removed { return $_[0]->{REMOVED}; }
51 3     3 0 18 sub p_action { return $_[0]->{P_ACTION}; }
52 3     3 0 14 sub d_action { return $_[0]->{D_ACTION}; }
53 18     18   91 sub _mode { return $_[0]->{MODE}; }
54 6     6   9 sub _protocols { return @{ $_[0]->{PROTOCOLS} }; }
  6         25  
55 17     17   21 sub _properties { return @{ $_[0]->{PROPERTIES} }; }
  17         68  
56 5     5   40 sub _is_verbose { return $_[0]->{VERBOSE}; }
57 0     0   0 sub _template { return $_[0]->{TEMPLATE}; }
58            
59             sub _is_valid_rule_set {
60 5     5   9 my ($self) = @_;
61            
62 5         16 my $id = $self->rules->[0]->get('ID');
63            
64 5         24 my $search = Farly::Object->new();
65 5         28 $search->set( 'ENTRY', Farly::Value::String->new('RULE') );
66 5         17 $search->set( 'ID', $id );
67            
68 5         13 foreach my $rule ( $self->rules->iter() ) {
69 83 50       200 if ( $rule->has_defined('REMOVE') ) {
70 0         0 die "found REMOVE in firewall ruleset ", $rule->dump();
71             }
72 83 100       228 if ( !$rule->matches($search) ) {
73 2         11 die "found invalid object in firewall ruleset ", $rule->dump();
74             }
75             }
76             }
77            
78             sub _is_expanded {
79 3     3   6 my ($self) = @_;
80 3         10 foreach my $rule ( $self->rules->iter() ) {
81 60         161 foreach my $key ( $rule->get_keys() ) {
82 557 50       1346 if ( $rule->get($key)->isa('Farly::Object::Ref') ) {
83 0         0 die "an expanded firewall ruleset is required";
84             }
85             }
86             }
87             }
88            
89             sub verbose {
90 0     0 1 0 my ( $self, $flag ) = @_;
91 0         0 $self->{VERBOSE} = $flag;
92             }
93            
94             sub set_p_action {
95 0     0 1 0 my ( $self, $action ) = @_;
96 0 0 0     0 confess "invalid action" unless ( defined($action) && length($action) );
97 0         0 $self->{P_ACTION} = $action;
98            
99 0         0 $log->debug("set permit action to $action");
100             }
101            
102             sub set_d_action {
103 0     0 1 0 my ( $self, $action ) = @_;
104 0 0 0     0 confess "invalid action" unless ( defined($action) && length($action) );
105 0         0 $self->{D_ACTION} = $action;
106            
107 0         0 $log->debug("set deny action to $action");
108             }
109            
110             # sort rules in ascending order by line number
111             sub _ascending_LINE {
112 49     49   131 $a->get('LINE')->compare( $b->get('LINE') );
113             }
114            
115             sub set_l4 {
116 1     1 0 6 my ($self) = @_;
117 1         3 $self->{MODE} = 'L4';
118 1         4 $self->{PROTOCOLS} = [ 0, 6, 17 ];
119 1         6 $self->{PROPERTIES} = [ 'PROTOCOL', 'SRC_IP', 'SRC_PORT', 'DST_IP', 'DST_PORT' ];
120             }
121            
122             # sort rules in ascending order so that current can contain next
123             # but next can't contain current
124             sub _ascending_l4 {
125 60 100 100 60   183 $a->get('DST_IP')->compare( $b->get('DST_IP') )
      100        
      66        
126             || $a->get('SRC_IP')->compare( $b->get('SRC_IP') )
127             || $a->get('DST_PORT')->compare( $b->get('DST_PORT') )
128             || $a->get('SRC_PORT')->compare( $b->get('SRC_PORT') )
129             || $a->get('PROTOCOL')->compare( $b->get('PROTOCOL') );
130             }
131            
132             sub set_icmp {
133 1     1 1 123 my ($self) = @_;
134            
135            
136 1         7 $log->info("set_icmp mode");
137            
138 1         5 $self->{MODE} = 'ICMP';
139 1         4 $self->{PROTOCOLS} = [ 0, 1 ];
140 1         6 $self->{PROPERTIES} = [ 'PROTOCOL', 'SRC_IP', 'DST_IP', 'ICMP_TYPE' ];
141             }
142            
143             sub _ascending_icmp {
144 13 100 66 13   69 $a->get('DST_IP')->compare( $b->get('DST_IP') )
      100        
145             || $a->get('SRC_IP')->compare( $b->get('SRC_IP') )
146             || $a->get('ICMP_TYPE')->compare( $b->get('ICMP_TYPE') )
147             || $a->get('PROTOCOL')->compare( $b->get('PROTOCOL') );
148             }
149            
150             sub set_l3 {
151 1     1 1 93 my ($self) = @_;
152            
153            
154            
155 1         9 $log->info("set_l3 mode");
156            
157 1         8 my $ICMP = Farly::Object->new();
158 1         9 $ICMP->set( 'PROTOCOL', Farly::Transport::Protocol->new(1) );
159            
160 1         6 my $TCP = Farly::Object->new();
161 1         7 $TCP->set( 'PROTOCOL', Farly::Transport::Protocol->new(6) );
162            
163 1         5 my $UDP = Farly::Object->new();
164 1         5 $UDP->set( 'PROTOCOL', Farly::Transport::Protocol->new(17) );
165            
166 1         3 my %protocols;
167            
168 1         8 foreach my $rule ( $self->rules->iter() ) {
169            
170 20 100       60 next if $rule->matches($ICMP);
171 15 100       46 next if $rule->matches($TCP);
172 7 100       26 next if $rule->matches($UDP);
173            
174 4 100       16 if ( $rule->has_defined('PROTOCOL') ) {
175 2         8 $protocols{ $rule->get('PROTOCOL')->as_string() }++;
176             }
177             else {
178 2         9 $log->info( "set_l3 skipped:\n" . $rule->dump() );
179             }
180             }
181            
182 1         7 my @p = keys %protocols;
183            
184 1         5 $self->{MODE} = 'L3';
185 1         3 $self->{PROTOCOLS} = \@p;
186 1         11 $self->{PROPERTIES} = [ 'PROTOCOL', 'SRC_IP', 'DST_IP' ];
187             }
188            
189             sub _ascending_l3 {
190 1 50 33 1   6 $a->get('DST_IP')->compare( $b->get('DST_IP') )
191             || $a->get('SRC_IP')->compare( $b->get('SRC_IP') )
192             || $a->get('PROTOCOL')->compare( $b->get('PROTOCOL') );
193             }
194            
195             sub run {
196 3     3 1 26 my ($self) = @_;
197            
198 3         14 $self->_optimize();
199            
200 3         11 $self->{OPTIMIZED} = $self->_keep( $self->rules );
201 3         13 $self->{REMOVED} = $self->_remove( $self->rules );
202             }
203            
204             sub _do_search {
205 6     6   12 my ( $self, $action ) = @_;
206            
207            
208            
209 6         23 my $search = Farly::Object->new();
210 6         29 my $result = Farly::Object::List->new();
211            
212 6         21 foreach my $protocol ( $self->_protocols ) {
213            
214 14         91 $log->info("searching for $action $protocol");
215            
216 14         74 $search->set( 'PROTOCOL', Farly::Transport::Protocol->new($protocol) );
217 14         60 $search->set( 'ACTION', Farly::Value::String->new($action) );
218            
219 14         38 $self->rules->matches( $search, $result );
220             }
221            
222 6         32 return $result;
223             }
224            
225             sub _tuple {
226 17     17   25 my ( $self, $rule ) = @_;
227            
228            
229            
230 17         55 my $r = Farly::Object->new();
231            
232 17         46 my @rule_properties = $self->_properties();
233            
234 17         34 foreach my $property (@rule_properties) {
235 79 50       205 if ( $rule->has_defined($property) ) {
236 79         182 $r->set( $property, $rule->get($property) );
237             }
238             else {
239 0         0 $log->warn( "property $property not defined in " . $rule->dump() );
240             }
241             }
242            
243 17         47 return $r;
244             }
245            
246             # Given rule X, Y, where X precedes Y in the ACL
247             # X and Y are inconsistent if:
248             # Xp contains Yd
249             # Xd contains Yp
250            
251             sub _inconsistent {
252 6     6   12 my ( $self, $s_a, $s_an ) = @_;
253            
254             # $s_a = ARRAY ref of rules of action a
255             # $s_an = ARRAY ref of rules of action !a
256             # $s_a and $s_an are sorted by line number and must be readonly
257            
258 6         7 my $rule_x;
259             my $rule_y;
260            
261             # iterate over rules of action a
262 6         15 for ( my $x = 0 ; $x != scalar( @{$s_a} ) ; $x++ ) {
  27         68  
263            
264 21         27 $rule_x = $s_a->[$x];
265            
266 21 50       51 confess "error : rule_x defined remove"
267             if ( $rule_x->has_defined('REMOVE') );
268            
269             # iterate over rules of action !a
270 21         37 for ( my $y = 0 ; $y != scalar( @{$s_an} ) ; $y++ ) {
  21         58  
271            
272 0         0 $rule_y = $s_an->[$y];
273            
274             #skip check if rule_y is already removed
275 0 0       0 next if $rule_y->has_defined('REMOVE');
276            
277             # if $rule_x comes before $rule_y in the rule set
278             # then check if $rule_x contains $rule_y
279            
280 0 0       0 if ( $rule_x->get('LINE')->number() <= $rule_y->get('LINE')->number() )
281             {
282            
283             # $rule_x1 is rule_x with layer 3 and 4 properties only
284 0         0 my $rule_x1 = $self->_tuple($rule_x);
285            
286 0 0       0 if ( $rule_y->contained_by($rule_x1) ) {
287            
288             # note removal of rule_y and the
289             # rule_x which caused the inconsistency
290 0         0 $rule_y->set( 'REMOVE', Farly::Value::String->new('RULE') );
291 0         0 $self->_log_remove( $rule_x, $rule_y );
292             }
293             }
294             }
295             }
296             }
297            
298             # Given rule X, Y, where X precedes Y in the ACL
299             # if Yp contains Xp and there does not exist rule Zd between
300             # Xp and Yp such that Zd intersect Xp and Xp !contains Zd
301            
302             sub _can_remove {
303 1     1   3 my ( $self, $rule_x, $rule_y, $s_an ) = @_;
304            
305             # $rule_x = the rule contained by $rule_y
306             # $s_an = rules of action !a sorted by ascending DST_IP
307            
308             # $rule_x1 is rule_x with layer 3 and 4 properties only
309 1         3 my $rule_x1 = $self->_tuple($rule_x);
310            
311 1         4 foreach my $rule_z ( @{$s_an} ) {
  1         4  
312            
313 0 0       0 if ( !$rule_z->get('DST_IP')->gt( $rule_x1->get('DST_IP') ) ) {
314            
315             #is Z between X and Y?
316 0 0 0     0 if ( ( $rule_z->get('LINE')->number() >= $rule_x->get('LINE')->number() )
317             && ( $rule_z->get('LINE')->number() <= $rule_y->get('LINE')->number() ) )
318             {
319            
320             # Zd intersect Xp?
321 0 0       0 if ( $rule_z->intersects($rule_x1) ) {
322            
323             # Xp ! contain Zd
324 0 0       0 if ( !$rule_z->contained_by($rule_x1) ) {
325 0         0 return undef;
326             }
327             }
328             }
329             }
330             else {
331            
332             # $rule_z is greater than $rule_x1 therefore rule_x and rule_z are disjoint
333 0         0 last;
334             }
335             }
336            
337 1         6 return 1;
338             }
339            
340             # Given rule X, Y, where X precedes Y in the ACL
341             # a is the action type of the rule
342             # if X contains Y then Y can be removed
343             # if Y contains X then X can be removed if there are no rules Z
344             # in $s_an that intersect X and exist between X and Y in the ACL
345            
346             sub _redundant {
347 6     6   15 my ( $self, $s_a, $s_an ) = @_;
348            
349             # $s_a = ARRAY ref of rules of action a to be validated
350             # $s_an = ARRAY ref of rules of action !a
351             # $s_a and $s_an are sorted by ascending and must be readonly
352            
353             # iterate over rules of action a
354 6         11 for ( my $x = 0 ; $x != scalar( @{$s_a} ) ; $x++ ) {
  27         81  
355            
356             # $rule_x1 is rule_x with layer 3 and 4 properties only
357 21         31 my $rule_x = $s_a->[$x];
358            
359             #skip check if rule_x is already being removed
360 21 100       53 next if $rule_x->has_defined('REMOVE');
361            
362             # remove non layer 3/4 rule properties
363 16         48 my $rule_x1 = $self->_tuple( $s_a->[$x] );
364            
365 16         32 for ( my $y = $x + 1 ; $y != scalar( @{$s_a} ) ; $y++ ) {
  62         145  
366            
367 54         72 my $rule_y = $s_a->[$y];
368            
369             #skip check if a rule_x made more than one rule_y redundant
370 54 50       147 next if $rule_y->has_defined('REMOVE');
371            
372 54 100       137 if ( !$rule_y->get('DST_IP')->gt( $rule_x->get('DST_IP') ) ) {
373            
374             # $rule_x comes before rule_y in the rule array
375             # therefore x might contain y
376            
377 46 100       121 if ( $rule_y->contained_by($rule_x1) ) {
378            
379             # rule_x is before rule_y in the rule set so remove rule_y
380 5 100       21 if ( $rule_x->get('LINE')->number() <= $rule_y->get('LINE')->number() )
381             {
382 4         22 $rule_y->set( 'REMOVE', Farly::Value::String->new('RULE') );
383 4         21 $self->_log_remove( $rule_x, $rule_y );
384             }
385             else {
386            
387             # rule_y is actually after rule_x in the rule set
388 1 50       8 if ( $self->_can_remove( $rule_y, $rule_x, $s_an ) ) {
389 1         6 $rule_y->set( 'REMOVE', Farly::Value::String->new('RULE') );
390 1         4 $self->_log_remove( $rule_x, $rule_y );
391             }
392             }
393             }
394             }
395             else {
396            
397             # rule_y DST_IP is greater than rule_x DST_IP therefore rule_x can't
398             # contain rule_y or any rules after rule_y (they are disjoint)
399 8         28 last;
400             }
401             }
402             }
403             }
404            
405             sub _remove {
406 3     3   8 my ( $self, $a_ref ) = @_;
407            
408 3         13 my $remove = Farly::Object::List->new();
409            
410 3         9 foreach my $rule (@$a_ref) {
411 60 100       132 if ( $rule->has_defined('REMOVE') ) {
412 5         17 $remove->add($rule);
413             }
414             }
415            
416 3         15 return $remove;
417             }
418            
419             sub _keep {
420 12     12   19 my ( $self, $a_ref ) = @_;
421            
422 12         53 my $keep = Farly::Object::List->new();
423            
424 12         31 foreach my $rule (@$a_ref) {
425 102 100       226 if ( !$rule->has_defined('REMOVE') ) {
426 92         210 $keep->add($rule);
427             }
428             }
429            
430 12         29 return $keep;
431             }
432            
433             sub _log_remove {
434 5     5   10 my ( $self, $keep, $remove ) = @_;
435            
436 5 50       16 if ( $self->_is_verbose() ) {
437 0         0 print " ! ";
438 0         0 $self->_template->as_string($keep);
439 0         0 print "\n";
440 0         0 $self->_template->as_string($remove);
441 0         0 print "\n";
442             }
443             }
444            
445             sub _do_sort {
446 9     9   17 my ( $self, $list ) = @_;
447            
448 9         13 my @sorted;
449            
450 9 100       30 if ( $self->_mode eq 'L4' ) {
    100          
    50          
451 3         10 @sorted = sort _ascending_l4 $list->iter();
452             }
453             elsif ( $self->_mode eq 'L3' ) {
454 3         10 @sorted = sort _ascending_l3 $list->iter();
455             }
456             elsif ( $self->_mode eq 'ICMP' ) {
457 3         14 @sorted = sort _ascending_icmp $list->iter();
458             }
459             else {
460 0         0 confess "mode error";
461             }
462            
463 9         37 return \@sorted;
464             }
465            
466             sub _optimize {
467 3     3   8 my ($self) = @_;
468            
469            
470            
471 3         13 my $permits = $self->_do_search( $self->p_action );
472 3         14 my $denies = $self->_do_search( $self->d_action );
473            
474 3         14 my @arr_permits = sort _ascending_LINE $permits->iter();
475 3         19 my @arr_denys = sort _ascending_LINE $denies->iter();
476            
477             # find permit rules that contain deny rules
478             # which are defined further down in the rule set
479 3         14 $log->info("Checking for deny rule inconsistencies...");
480 3         19 $self->_inconsistent( \@arr_permits, \@arr_denys );
481            
482             # create a new list of deny rules which are being kept
483 3         15 $denies = $self->_keep( \@arr_denys );
484            
485             # the consistent deny list sorted by LINE again
486 3         15 @arr_denys = sort _ascending_LINE $denies->iter();
487            
488             # find deny rules which contain permit
489             # rules further down in the rule set
490 3         916 $log->info("Checking for permit rule inconsistencies...");
491 3         18 $self->_inconsistent( \@arr_denys, \@arr_permits );
492            
493             # create the list of permit rules which are being kept
494 3         9 $permits = $self->_keep( \@arr_permits );
495            
496             # sort the rule in ascending order
497 3         17 my $aref_permits = $self->_do_sort($permits);
498 3         13 my $aref_denys = $self->_do_sort($denies);
499            
500 3         17 $log->info("Checking for permit rule redundancies...");
501 3         14 $self->_redundant( $aref_permits, $aref_denys );
502            
503 3         12 $permits = $self->_keep($aref_permits);
504            
505             # sort the permits again
506 3         14 $aref_permits = $self->_do_sort($permits);
507            
508 3         20 $log->info("Checking for deny rule redundancies...");
509 3         14 $self->_redundant( $aref_denys, $aref_permits );
510            
511             }
512            
513             1;
514             __END__