File Coverage

blib/lib/Device/PaloAlto/Firewall/Test.pm
Criterion Covered Total %
statement 29 301 9.6
branch 0 142 0.0
condition 0 18 0.0
subroutine 10 49 20.4
pod 22 22 100.0
total 61 532 11.4


line stmt bran cond sub pod time code
1             package Device::PaloAlto::Firewall::Test;
2              
3 4     4   53 use 5.006;
  4         11  
4 4     4   18 use strict;
  4         7  
  4         59  
5 4     4   16 use warnings;
  4         10  
  4         137  
6              
7             our $VERSION = '0.081'; # VERSION - generated by DZP::OurPkgVersion
8              
9 4     4   1620 use Moose;
  4         1569382  
  4         28  
10 4     4   28767 use Modern::Perl;
  4         29916  
  4         27  
11 4     4   619 use Carp;
  4         9  
  4         240  
12 4     4   28 use List::Util qw( any first );
  4         10  
  4         280  
13             #use List::MoreUtils qw( uniq );
14             #use Array::Utils qw{ array_minus };
15 4     4   1652 use Params::Validate qw( :all );
  4         8610  
  4         684  
16              
17 4     4   1435 use Data::Dumper;
  4         20386  
  4         807  
18              
19             =head1 NAME
20              
21             Device::PaloAlto::Firewall::Test- Run a suite of tests against Palo Alto firewalls.
22              
23             =head1 VERSION
24              
25             version 0.081
26              
27             =cut
28              
29             =head1 SYNOPSIS
30              
31             This module contains a set of methods that run tests against an Palo Alto firewall.
32             The functions take arguments and return 1 or 0 depending on the current runtime state of the firewall.
33              
34             These methods should be used in conjunction with the C<ok()> function provided by C<Test::More>.
35             Multiple '.t' files can be created with tests for each firewall and run using the C<prove> test harness.
36              
37             use Device::PaloAlto::Firewall;
38             use Test::More qw{ no_plan };
39              
40             my $tester = Device::PaloAlto::Firewall->new(uri => 'https://test_firewall.int', username => 'ro_account', password => 'complex_password)->tester();
41              
42             ok( $tester->environmentals(), "No alarms on the firewall" );
43             ok( $tester->interfaces_up(interfaces => ['ethernet1/1']), "WAN interface is up");
44              
45              
46             =cut
47              
48             has 'firewall' => ( is => 'ro', isa => 'Device::PaloAlto::Firewall', default => sub { });
49              
50             =head1 SUBROUTINES
51              
52             =head2 System Tests
53              
54             These methods test system related aspects of the firewalls.
55              
56             =head3 version
57              
58             Takes a C<version> (as a string) and returns 1 if the firewall is running that version of PAN-OS. Returns 0 if it is running a different version.
59              
60             ok( $fw_test->version(version => '7.1.2'), "Firewall running PAN-OS 7.1.2");
61              
62             Hotfixes (version suffixed with '-h1', '-h2', etc) are considered equivalent to their base versions.
63              
64             =cut
65              
66             sub version {
67 0     0 1   my $self = shift;
68 0           my %args = validate(@_,
69             {
70             version => { type => SCALAR },
71             }
72             );
73              
74              
75 0           my $sysinfo = $self->firewall->system_info();
76              
77 0 0         return 0 if !defined $sysinfo;
78              
79             # Hotfixes are considered equivalent to the base version. We strip out hotfix
80             # suffixes from any arguments and from the version returned from the firewall.
81 0           my $hotfix_regex = qr{-h(\d+)$}xms;
82 0           $sysinfo->{'sw-version'} =~ s{$hotfix_regex}{};
83 0           $args{version} =~ s{$hotfix_regex}{};
84              
85 0 0         return 0 if !%{ $sysinfo };
  0            
86              
87 4     4   911 use version qw{ is_lax };
  4         5751  
  4         29  
88              
89 0 0         if (!is_lax($args{version})) {
90 0           carp "Version argument ($args{version}) is not in a valid version format, test returns 0";
91 0           return 0;
92             }
93              
94 0 0         if (!is_lax($sysinfo->{'sw-version'})) {
95 0           carp "Version retrieved from firewall ($sysinfo->{'sw-version'}) is not in a valid version format, test returns 0";
96 0           return 0;
97             }
98              
99 0 0         return 0 if (version->parse($args{version}) > version->parse($sysinfo->{'sw-version'}));
100              
101 0           return 1;
102             }
103              
104             =head3 environmentals
105              
106             Returns 1 if there are no environmental alarms. These are platform dependent, but generally consist of fantray and fans, power supplies and power, and temperature. If there are B<any> alarms, returns 0.
107              
108             VMs don't have any environmental information. In this instance the test will succeed, but a warning is generated.
109              
110             ok( $test->environmentals(), "No environmental alarms" );
111              
112             =cut
113              
114             sub environmentals {
115 0     0 1   my $self = shift;
116              
117 0           my $environ = $self->firewall->environmentals();
118            
119 0 0         return 0 if !defined $environ;
120              
121             # VMs don't have any environmental info and return an empty hash.
122 0 0         if (!%{ $environ }) {
  0            
123 0           carp "No environmentals - is this a VM? Returning success";
124 0           return 1;
125             }
126              
127 0           for my $property (values %{ $environ }) {
  0            
128 0           for my $slot (values %{ $property }) {
  0            
129 0 0   0     return 0 if any { lc $_->{alarm} ne 'false' } @{ $slot };
  0            
  0            
130             }
131             }
132              
133 0           return 1;
134             }
135              
136              
137              
138              
139             =head2 Network Tests
140              
141             These methods test network related functions of the firewalls.
142              
143             =head3 interfaces_up
144              
145             C<interfaces_up> takes an ARRAYREF of interfaces are returns 1 if B<all> of the interfaces are up. Returns 0 if B<any> of the interfaces are down.
146              
147             Interfaces are matched in case insensitive manner.
148              
149             ok(
150             $fw_test->interfaces_up(
151             interfaces => ['ethernet1/1', 'ethernet1/2']), "Interfaces are up"
152             )
153             );
154              
155             =cut
156              
157             sub interfaces_up {
158 0     0 1   my $self = shift;
159 0           my %args = validate(@_,
160             {
161             interfaces => { type => ARRAYREF },
162             }
163             );
164              
165 0 0         if (!@{ $args{interfaces} }) {
  0            
166 0           carp "Warning: no interfaces specified - test returns true";
167 0           return 1;
168             }
169              
170 0           my $fw_interfaces = $self->firewall->interfaces();
171              
172 0 0         return 0 if !defined $fw_interfaces;
173              
174 0           for my $test_interface (@{ $args{interfaces} }) {
  0            
175 0 0   0     return 0 if !any { lc $_->{name} eq lc $test_interface and $_->{state} eq 'up' } @{ $fw_interfaces->{hw}->{entry} };
  0 0          
  0            
176             }
177              
178 0           return 1;
179             }
180              
181             =head3 interfaces_duplex
182              
183              
184             C<interfaces_duplex> takes an ARRAYREF of interfaces and returns 1 if B<all> the interfaces are in a full duplex state. Returns 0 if any of the interfaces are not in a full dupex state.
185             Returns 0 and warns if it detects a virtual machine as it cannot report on the duplex state.
186              
187             The names of the interfaces are matched in a case-insensitive manner.
188              
189             ok(
190             $fw_test->interfaces_duplex(
191             interfaces => ['ethernet1/1', 'ethernet./(2|3)']
192             ), "Interfaces are running full duplex"
193             );
194              
195             =cut
196              
197             sub interfaces_duplex {
198 0     0 1   my $self = shift;
199 0           my %args = validate(@_,
200             {
201             interfaces => { type => ARRAYREF },
202             }
203             );
204              
205 0 0         if (!@{ $args{interfaces} }) {
  0            
206 0           carp "Warning: no interfaces specified - test returns true";
207 0           return 1;
208             }
209              
210 0           my $fw_interfaces = $self->firewall->interfaces();
211              
212 0 0         return 0 if !defined $fw_interfaces;
213              
214 0           for my $test_interface (@{ $args{interfaces} }) {
  0            
215 0 0   0     return 0 if !any{ _half_duplex_search($test_interface, $_) } @{ $fw_interfaces->{hw}->{entry} };
  0            
  0            
216             }
217            
218 0           return 1;
219              
220             }
221              
222              
223              
224             # _half_duplex_search( $interface_structure_ref )
225             #
226             # Takes a "hw" interace array member returned from a firewall
227             # Returns 0 if the interface is:
228             # * Not up
229             # * A probable virtual machine interface (also warns)
230             # * Is in full duplex mode
231             # Returns 1 for everything else. Most likely 'duplex' == 'half', but could be 'duplex' == '[n/a]'
232              
233             sub _half_duplex_search {
234 0     0     my $test_interface = shift;
235 0           my $fw_interface = shift;
236              
237 0 0         return 0 if lc $test_interface ne lc $fw_interface->{name};
238              
239 0 0         return 0 if $fw_interface->{state} ne 'up';
240            
241 0 0         if ($fw_interface->{duplex} eq 'auto') {
242 0           carp "Warning: detected 'auto' duplex, probable VM? Test will fail";
243 0           return 0;
244             }
245              
246 0 0         return 1 if $fw_interface->{duplex} eq 'full';
247              
248 0           return 0;
249             }
250              
251              
252              
253             =head3 interface_errors_logical
254              
255             Takes a C<percent> argument between (0, 100] and returns 0 if, for any interface:
256              
257             =over 4
258              
259             =item * The number of input errors divided by the number of input packets is greater than or equal to C<percent>, B<OR>
260              
261             =item * The number of output errors divided by the number of output packets is greater than or equal to C<percent>.
262              
263             =back
264              
265             Otherwise it returns 1. If no C<percent> argument is supplied, it defaults to 1%.
266              
267             ok(
268             $fw_test->interface_errors_logical(percent => 2), "No interfaces with more than 2% errors"
269             );
270              
271             =cut
272              
273             sub interface_errors_logical {
274 0     0 1   my $self = shift;
275             my %args = validate(@_,
276             {
277             percent => {
278             type => SCALAR,
279             default => 1,
280             callbacks => {
281 0 0   0     'valid_percent' => sub{ $_[0] > 0 and $_[0] <= 100; }
282             },
283             }
284             }
285 0           );
286              
287 0           my $interface_counters = $self->firewall->interface_counters_logical();
288              
289 0 0         return 0 if !defined $interface_counters;
290              
291 0 0         return 0 if !@{ $interface_counters };
  0            
292              
293             INTERFACE:
294 0           for my $interface (@{ $interface_counters }) {
  0            
295             # We don't care if the interface hasn't sent and received.
296             # Also helps us avoid the divide by 0 issues.
297 0 0 0       next INTERFACE if ($interface->{ipackets} == 0 or $interface->{opackets} == 0);
298              
299             my @percent = (
300             ($interface->{ierrors} / $interface->{ipackets}) * 100,
301 0           ($interface->{ifwderrors} / $interface->{opackets}) * 100
302             );
303              
304 0 0 0       return 0 if $percent[0] >= $args{percent} or $percent[1] >= $args{percent};
305             }
306              
307 0           return 1;
308             }
309              
310              
311             =head3 routes_exist
312              
313             Takes an ARRAYREF of routes and searches for these routes in the virtual router specified by C<vrouter>.
314             If B<all> of the exact routes are present in the routing table it returns 1. If B<any> exact routes are not present, it
315             returns 0.
316              
317             C<routes> is mandatory. C<vrouter> is optional, and is set to 'default' if not specified.
318             An empty ARRAYREF will emit a warning but will still return 1.
319              
320             ok(
321             $fw_test->routes_exist(
322             vrouter => 'virt_router_a',
323             routes => ['192.0.2.0/30', '192.0.2.128/25']
324             ), "All expected routes are present in 'virt_router_a'"
325             );
326              
327              
328             =cut
329             sub routes_exist {
330 0     0 1   my $self = shift;
331 0           my %args = validate(@_,
332             {
333             routes => { type => ARRAYREF },
334             vrouter => { default => 'default', type => SCALAR | UNDEF },
335             }
336             );
337              
338 0 0         if (!@{ $args{routes} }) {
  0            
339 0           carp "Empty routes ARRAYREF specified - test will still return true";
340 0           return 1;
341             }
342              
343 0           my $route_search_ref = delete $args{routes};
344              
345 0           my $routing_table = $self->firewall->routing_table(%args);
346              
347 0           for my $route (@{ $route_search_ref }) {
  0            
348 0 0         if (!grep { $route eq $_->{destination} } @{ $routing_table }) {
  0            
  0            
349 0           return 0;
350             }
351             }
352              
353 0           return 1;
354             }
355              
356              
357             =head3 bgp_peers_up
358            
359             Returns 1 if B<all> of the BGP peers specified in the C<peer_ips> are established. Returns 0 if any of the peers are not in the established state.
360              
361             C<vrouter> specifies the virtual router that the BGP peers are configured under. If not supplied, the vrouter 'default' will be used.
362              
363             ok(
364             $fw_test->bgp_peers_ip(
365             vrouter => 'virt_router_a',
366             peer_ips => ['192.0.2.1', '192.0.2.20']
367             ), "BGP peerings for 'virt_router-a' are up"
368             );
369              
370              
371              
372             =cut
373              
374             sub bgp_peers_up {
375 0     0 1   my $self = shift;
376 0           my %args = validate(@_,
377             {
378             peer_ips => { type => ARRAYREF },
379             vrouter => { default => 'default', type => SCALAR | UNDEF },
380             }
381             );
382              
383 0           my $peer_ip_search_ref = delete $args{peer_ips};
384              
385 0           my $bgp_peers = $self->firewall->bgp_peers(%args);
386              
387 0           my @up_peers = grep { $_->{status} eq 'Established' } @{ $bgp_peers };
  0            
  0            
388              
389             # Iterate through the peer IPs passed to us and determine whether they're up.
390             # If the peer is up, 'peer-address' is host:port, so we split and match against
391             # the first array member
392 0           for my $peer_search (@{ $peer_ip_search_ref }) {
  0            
393 0 0         if (!grep { $peer_search eq (split(':', $_->{'peer-address'}))[0] } @up_peers ) {
  0            
394 0           return 0;
395             }
396             }
397              
398 0           return 1;
399             }
400              
401              
402              
403             =head3 bgp_prefixes_in_rib
404              
405             Returns 1 if B<all> of the prefixes specified in the C<prefixes> are present in the local routing information base (RIB) for a specific C<vrouter>. Returns 0 if any of the prefixes are not present.
406              
407             If C<vrouter> is not specified, the vrouter 'default' will be used.
408              
409             Note that this only determines whether a prefix is present within the RIB. It doesn't take into account how many times the prefix is present or what peer it received it from. The prefix could also
410             have been locally originated and this would still return 1.
411              
412             ok(
413             $fw_test->bgp_prefixes_in_rib(
414             vrouter => 'virt_router_a',
415             prefixes => ['192.168.0.0/24', '0.0.0.0/0']
416             ), "Default and local private range prefixes in RIB"
417             );
418              
419             =cut
420              
421             sub bgp_prefixes_in_rib {
422 0     0 1   my $self = shift;
423 0           my %args = validate(@_,
424             {
425             prefixes => { type => ARRAYREF },
426             vrouter => { default => 'default', type => SCALAR | UNDEF },
427             }
428             );
429              
430 0           my $test_prefixes = delete $args{prefixes};
431              
432 0           my $bgp_prefixes = $self->firewall->bgp_rib(%args);
433              
434 0 0         return 0 if !$bgp_prefixes;
435              
436 0 0         return 0 if !@{ $bgp_prefixes };
  0            
437              
438             # Return 0 if the test prefix is not present in the RIB.
439 0           for my $test_prefix (@{ $test_prefixes }) {
  0            
440 0 0   0     return 0 if !any { $test_prefix eq $_->{prefix} } @{ $bgp_prefixes };
  0            
  0            
441             }
442              
443 0           return 1;
444             }
445              
446              
447              
448             =head3 ospf_neighbours_up
449              
450             Returns 1 if B<all> of the OSPF neighbours specified in the C<neighbours> argument are up for a specific C<vrouter>. Neighbours are specified by their IP address, B<NOT> by their router ID. Returns 0 if B<any> of the neighbours are not in a 'full' state (i.e. in init/2-way/extart/exchange state), or the neighbour was not returned at all and is therefore down.
451              
452             If a C<vrouter> is not specified, the vrouter 'default' will be used.
453              
454             ok(
455             $fw_test->ospf_neighbours_up(
456             vrouter => 'virt_router_a',
457             neighbours => ['192.168.1.1', '172.16.2.1']
458             ), "Expected OSPF neighbours are up"
459             );
460              
461             =cut
462              
463             sub ospf_neighbours_up {
464 0     0 1   my $self = shift;
465 0           my %args = validate(@_,
466             {
467             neighbours => { type => ARRAYREF },
468             vrouter => { default => 'default', type => SCALAR | UNDEF },
469             }
470             );
471              
472 0           my $test_ospf_nbrs = delete $args{neighbours};
473              
474 0           my $ospf_neighbours = $self->firewall->ospf_neighbours(%args);
475              
476 0           for my $test_ospf_nbr (@{ $test_ospf_nbrs }) {
  0            
477             return 0 if !any {
478 0 0   0     $test_ospf_nbr eq $_->{'neighbor-address'} and lc $_->{status} eq 'full'
479 0 0         } @{ $ospf_neighbours };
  0            
480             }
481            
482 0           return 1;
483             }
484              
485              
486              
487              
488             =head3 pim_neighbours_up
489              
490              
491             Returns 1 if B<all> of the PIM neighbours specified in the C<neighbours> argument are up for a specific C<vrouter>. Neighbours are specified by their IP address. are up within a specific vrouter. Returns 0 if any of the neighbours are not up.
492              
493             If C<vrouter> is not specified, the vrouter 'default' will be used.
494              
495             ok(
496             $fw_test->pim_neighbours_up(
497             vrouter => 'virt_router_a',
498             neighbours => ['192.168.1.1', '172.16.2.1']
499             ), "Expected PIM adjacencies are up"
500             );
501              
502             =cut
503              
504             sub pim_neighbours_up {
505 0     0 1   my $self = shift;
506 0           my %args = validate(@_,
507             {
508             neighbours => { type => ARRAYREF },
509             vrouter => { default => 'default', type => SCALAR | UNDEF },
510             }
511             );
512              
513 0           my $test_pim_neighbours = delete $args{neighbours};
514              
515 0           my $pim_neighbours = $self->firewall->pim_neighbours(%args);
516              
517 0 0         return 0 if !defined $pim_neighbours;
518              
519 0 0         return 0 if !@{ $pim_neighbours };
  0            
520              
521 0           for my $test_pim_neighbour (@{ $test_pim_neighbours }) {
  0            
522 0 0   0     return 0 if !any { $test_pim_neighbour eq $_->{Address} } @{ $pim_neighbours };
  0            
  0            
523             }
524              
525 0           return 1;
526             }
527              
528             =head3 bfd_peers_up
529              
530             Takes an ARRAYREF of interface names and returns 1 if:
531              
532             =over 4
533              
534             =item * All of the interfaces have BFD sessions associated with them, and
535              
536             =item * All of the BFD sessions are up.
537              
538             =back
539              
540             Otherwise it returns 0. If no interfaces are specified (and empty ARRAYREF), all BFD sessions are checked.
541              
542             ok(
543             $fw_test->bfd_peers_up(
544             interfaces => ['ethernet1/1', 'ethernet1/2']
545             ), "All BFD sessions are up"
546             );
547              
548             =cut
549              
550             sub bfd_peers_up {
551 0     0 1   my $self = shift;
552 0           my %args = validate(@_,
553             {
554             interfaces => { type => ARRAYREF, optional => 1 },
555             }
556             );
557              
558 0           my $bfd_peers = $self->firewall->bfd_peers();
559              
560 0 0         return 0 if !$bfd_peers;
561              
562 0 0         return 0 if !@{ $bfd_peers };
  0            
563              
564 0           my @relevant_bfd_peers; # Filtered by $args{interfaces} (if present) or all of them
565              
566              
567 0 0         if (defined $args{interfaces}) {
568 0           for my $interface (@{ $args{interfaces} }) {
  0            
569 0     0     my $bfd_peer_ref = first { $_->{interface} eq $interface } @{ $bfd_peers };
  0            
  0            
570             # If the interface isn't returned (not configured) return 0
571 0 0         return 0 if !$bfd_peer_ref;
572              
573 0           push @relevant_bfd_peers, $bfd_peer_ref;
574             }
575             } else {
576 0           @relevant_bfd_peers = @{ $bfd_peers };
  0            
577             }
578              
579              
580             # If any peer isn't up we return 0
581 0 0         return 0 if grep { lc $_->{'state-local'} ne 'up' } @relevant_bfd_peers;
  0            
582              
583 0           return 1;
584             }
585              
586              
587             =head3 ntp_synchronised
588              
589             Returns 0 if the firewall is not synchronised with an NTP peer. Returns 1 if the firewall is synchronised with B<at least> one NTP peer.
590              
591             ok( $fw_test->ntp_synchronised(), "Firewall is synchronised with at least one NTP server" );
592              
593             =cut
594              
595             sub ntp_synchronised {
596 0     0 1   my $self = shift;
597              
598 0           my $ntp_response = $self->firewall->ntp();
599              
600 0 0 0       return 0 if !defined $ntp_response->{synched} or $ntp_response->{synched} eq 'LOCAL';
601              
602 0           return 1;
603             }
604              
605              
606              
607             =head3 ntp_reachable
608              
609             Returns 1 if all of the configured NTP servers are reachable. Returns 0 if any of the configured NTP servers are not reachable.
610              
611             ok ( $fw_test->ntp_reachable(), "Firewall can reach all of its NTP servers" );
612              
613             =cut
614              
615             sub ntp_reachable {
616 0     0 1   my $self = shift;
617              
618 0           my $ntp_response = $self->firewall->ntp();
619              
620 0 0         return 0 if !defined $ntp_response->{synched};
621              
622 0           delete $ntp_response->{synched};
623              
624 0 0         return 0 if !keys %{ $ntp_response }; # No peers configured.
  0            
625              
626 0 0   0     return 0 if any { $ntp_response->{$_}->{reachable} ne 'yes' } keys %{ $ntp_response }; # Any of the servers are not reachable.
  0            
  0            
627              
628 0           return 1;
629             }
630              
631             =head3 panorama_connected
632              
633             Returns 1 if the firewall is connectedt to B<all> of the configured Panorama management servers, otherwise it returns 0. Also returns 0 if no Panorama servers are configured.
634              
635             ok( $fw_test->panorama_connected(), "Firewall is connected to Panorama" );
636              
637             =cut
638              
639             sub panorama_connected {
640 0     0 1   my $self = shift;
641              
642 0           my $panorama_status_ref = $self->firewall->panorama_status();
643              
644 0 0         return 0 if !$panorama_status_ref;
645              
646 0 0         return 0 if !@{ $panorama_status_ref };
  0            
647              
648 0 0   0     return 0 if any { lc $_->{connected} ne 'yes' } @{ $panorama_status_ref };
  0            
  0            
649              
650 0           return 1;
651             }
652              
653             =head2 High Availability Tests
654              
655             These methods test aspects of the high availability function of the firewalls.
656              
657             =head3 ha_enabled
658              
659             Returns 1 if HA is enabled on the devices. Returns if HA is not enabled.
660              
661             ok( $test->ha_enabled(), "HA is enabled on the firewall" );
662              
663             =cut
664              
665             sub ha_enabled {
666 0     0 1   my $self = shift;
667              
668 0           my $ha_response = $self->firewall->high_availability();
669              
670 0           return $self->_check_ha_enabled($ha_response->{enabled});
671              
672 0           return 0;
673             }
674              
675             =head3 ha_state
676              
677             Returns 1 if the firewall is in the same state as the C<state> parameter passed to the function. Returns 0 if it is not, or if HA is not enabled on the device.
678              
679             ok( $test->ha_state(state => 'active'), "Firewall is in the active HA state" );
680             ok( $test->ha_state(state => 'passive'), "Firewall is in the passive HA state" );
681              
682              
683             The SCALAR string passed must be either 'active' or 'passive', however it is case insensitive.
684              
685             =cut
686              
687             sub ha_state {
688 0     0 1   my $self = shift;
689 0           my %args = validate(@_,
690             {
691             state => { type => SCALAR, regex => qr{active|passive}i }
692             }
693             );
694              
695 0           my $ha_response = $self->firewall->high_availability();
696              
697             # Check if HA is running
698 0 0         return 0 if !$self->_check_ha_enabled($ha_response->{enabled});
699              
700 0 0         return 1 if (lc $args{state} eq lc $ha_response->{group}->{'local-info'}->{state});
701              
702 0           return 0;
703             }
704              
705             =head3 ha_version
706              
707             Returns 1 if the app, threat, antivirus, PAN-OS and GlobalProtect versions match between the HA peers. Returns 0 if any one of these do not match, or HA is not enabled on the device.
708              
709             ok( $test->ha_version(), "HA peers have matching versions" );
710              
711             =cut
712              
713             sub ha_version {
714 0     0 1   my $self = shift;
715              
716             # These are the keys from the returned hash that all need to eq 'Match'
717 0           my @version_match_keys = qw{ url-compat threat-compat av-compat gpclient-compat build-compat vpnclient-compat app-compat };
718              
719 0           my $ha_response = $self->firewall->high_availability();
720              
721             # Check if HA is running
722 0 0         return 0 if !$self->_check_ha_enabled($ha_response->{enabled});
723              
724 0 0   0     return 0 if any { lc $_ ne 'match' } @{ $ha_response->{group}->{'local-info'} }{ @version_match_keys };
  0            
  0            
725              
726              
727 0           return 1;
728             }
729              
730              
731             =head3 ha_peer_up
732              
733             Returns 1 if the peer firewall is considerd 'up', and that the HA1, heartbeat backup and HA2 connections are 'up'. Returns 0 if any one of these conditions is not 'up'.
734              
735             ok( $test->ha_peer_up(), "HA peer is up" );
736              
737             =cut
738              
739             sub ha_peer_up {
740 0     0 1   my $self = shift;
741              
742 0           my @ha_interface_keys = qw{ conn-mgmt conn-ha1 conn-ha2 };
743              
744 0           my $ha_response = $self->firewall->high_availability();
745              
746             # Check if HA is running
747 0 0         return 0 if !$self->_check_ha_enabled($ha_response->{enabled});
748              
749 0           my $peer_info = $ha_response->{group}->{'peer-info'};
750              
751             # Return 0 if the peer isn't considered 'up', or any of the HA interfaces aren't considered 'up'
752 0 0 0 0     return 0 if (lc $peer_info->{'conn-status'} ne 'up') and (any { lc $_->{'conn-status'} ne 'up' } @{ $peer_info }{ @ha_interface_keys });
  0            
  0            
753              
754 0           return 1;
755             }
756              
757             =head3 ha_config_sync
758              
759             Returns 1 if the configuration has been successfully synchronised between the devices. Returns 0 if the configuration has not been synchronised, if config synchronisation is not enabled, or if HA is not enabled.
760              
761             ok( $test->ha_config_sync(), "Config is sync'ed between HA peers" );
762              
763             =cut
764              
765             sub ha_config_sync {
766 0     0 1   my $self = shift;
767              
768 0           my $ha_response = $self->firewall->high_availability();
769              
770             # Check if HA is running
771 0 0         return 0 if !$self->_check_ha_enabled($ha_response->{enabled});
772              
773 0 0 0       return 0 if (lc $ha_response->{group}->{'running-sync-enabled'} ne 'yes') and (lc $ha_response->{group}->{'running-sync'} ne 'synchronized');
774              
775 0           return 1;
776             }
777              
778              
779              
780              
781             # This functon is used in all of the ha_* subs to check if HA is enabled before doing any further checks.
782             sub _check_ha_enabled {
783 0     0     my $self = shift;
784 0           my $enabled = shift;
785              
786 0 0         return 0 if !defined $enabled;
787 0 0         return 0 if $enabled ne 'yes';
788              
789 0           return 1;
790              
791             }
792              
793              
794             =head2 Firewall Tests
795              
796             =head3 ip_user_mapping
797              
798             Takes a C<domain> and an ARRAYREF of C<users> as arguments. Returns 1 if there is a valid IP mapping for all of the users within the specified domain.
799              
800             If no C<domain> is specified then the users are matched for any domain. If no C<domain> or C<users> are specified then it returns 1 if there is B<any> user to IP mapping, and 0 if there are none.
801              
802             ok(
803             $fw_test->ip_user_mapping(
804             domain => 'internal.local',
805             users => ['user_a', 'user_b']
806             ), "Valid User/IP mappings for user_a & user_b"
807             );
808              
809             =cut
810              
811             sub ip_user_mapping {
812 0     0 1   my $self = shift;
813 0           my %args = validate(@_,
814             {
815             domain => { type => SCALAR, optional => 1 },
816             users => { type => ARRAYREF, optional => 1 },
817             }
818             );
819              
820 0           my $mappings = $self->firewall->ip_user_mapping();
821              
822 0 0         return 0 if !defined $mappings;
823              
824 0           my @user_mappings = @{ $mappings };
  0            
825              
826             # If domain is supplied, we only want to look at users matching that domain.
827 0 0         if (defined $args{domain}) {
828 0           @user_mappings = grep { lc $_->{domain} eq lc $args{domain} } @user_mappings;
  0            
829             }
830              
831             # If no users are specified, we just need to see a single user to IP mapping
832             # But we're still only looking within the domain if specified.
833 0 0         if (!defined $args{users}) {
834 0 0         return 0 if !grep { lc $_->{type} ne 'unknown' } @user_mappings;
  0            
835             }
836              
837             # Return 0 if our user isn't in the user to IP mappings
838 0           for my $user (@{ $args{users} }) {
  0            
839 0 0         return 0 if !grep { $user eq $_->{user} } @user_mappings;
  0            
840             }
841            
842 0           return 1; # All users have an entry.
843             }
844              
845              
846             =head3 userid_server_monitor
847              
848             Takes an ARRAYREF of C<servers> returns 1 if all of the servers are connnected. Returns 0 if B<any> of the servers are not connected. Each server must be specified as their fully qualified domain name, e.g. 'ad01.domain.int'.
849              
850             If no C<servers> argument is given, returns 1 if B<all> of the servers configured are connected, and returns 0 of B<any> of the servers are not connected.
851              
852             ok(
853             $fw_test->userid_server_monitor(
854             servers => ['ad01.int', 'ad02.int']
855             ), "AD servers reachable for UserID"
856             );
857              
858             =cut
859              
860             sub userid_server_monitor {
861 0     0 1   my $self = shift;
862 0           my %args = validate(@_,
863             {
864             servers => { type => ARRAYREF, optional => 1 },
865             }
866             );
867              
868 0           my $server_monitor = $self->firewall->userid_server_monitor();
869              
870 0 0         return 0 if !defined $server_monitor;
871              
872             # No servers is considered a failure
873 0 0         return 0 if !@{ $server_monitor };
  0            
874              
875             # If no server arg is specified, any server that's not 'connected' is a failure
876 0 0         if (!defined $args{servers}) {
877 0 0   0     return if any { lc $_->{connected} ne 'connected' } @{ $server_monitor };
  0            
  0            
878             }
879              
880 0           for my $server (@{ $args{servers} }) {
  0            
881 0     0     my $server_state = first { $server eq $_->{name} } @{ $server_monitor };
  0            
  0            
882 0 0 0       return 0 if (!$server_state or lc $server_state->{connected} ne 'connected')
883             }
884              
885 0           return 1;
886             }
887              
888              
889              
890             =head3 vpn_tunnels_up
891              
892             Takes an ARRAYREF of C<peer_ips> and returns 1 if B<all> of the VPN tunnels are up. A VPN tunnel is considered up if its phase 1 (IKE) security association up, and all of its phase 2 (IPSEC) security associations are up.
893              
894             If any of the VPN tunnels are not up - including not being configured at all, then it it returns 0.
895              
896             ok(
897             $fw_test->vpn_tunnels_up(
898             peer_ips => ['192.168.1.1', '172.16.2.1']
899             ), "3rd party VPN tunnels are up"
900             );
901              
902             =cut
903              
904             sub vpn_tunnels_up {
905 0     0 1   my $self = shift;
906 0           my %args = validate(@_,
907             {
908             peer_ips => { type => ARRAYREF },
909             }
910             );
911              
912 0           my $vpn_tunnels = $self->firewall->vpn_tunnels();
913              
914 0 0         return 0 if !defined $vpn_tunnels;
915              
916             # No VPNs at all indicates a failure
917 0 0         return 0 if !@{ $vpn_tunnels };
  0            
918              
919 0           for my $peer_ip (@{ $args{peer_ips} }) {
  0            
920 0 0   0     return 0 if !any { $_->{peerip} eq $peer_ip and lc $_->{state} eq 'active' } @{ $vpn_tunnels };
  0 0          
  0            
921             }
922              
923 0           return 1;
924             }
925              
926              
927              
928              
929             =head1 AUTHOR
930              
931             Greg Foletta, C<< <greg at foletta.org> >>
932              
933             =head1 BUGS
934              
935             Please report any bugs or feature requests to C<bug-device-firewall-paloaltoat rt.cpan.org>, or through
936             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Device-PaloAlto-Firewall>. I will be notified, and then you'll
937             automatically be notified of progress on your bug as I make changes.
938              
939              
940              
941              
942             =head1 SUPPORT
943              
944             You can find documentation for this module with the perldoc command.
945              
946             perldoc Device::PaloAlto::Firewall::Test
947              
948              
949             You can also look for information at:
950              
951             =over 4
952              
953             =item * RT: CPAN's request tracker (report bugs here)
954              
955             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Device-PaloAlto-Firewall>
956              
957             =item * AnnoCPAN: Annotated CPAN documentation
958              
959             L<http://annocpan.org/dist/Device-PaloAlto-Firewall>
960              
961             =item * CPAN Ratings
962              
963             L<http://cpanratings.perl.org/d/Device-PaloAlto-Firewall>
964              
965             =item * Search CPAN
966              
967             L<http://search.cpan.org/dist/Device-PaloAlto-Firewall/>
968              
969             =back
970              
971              
972             =head1 ACKNOWLEDGEMENTS
973              
974              
975             =head1 LICENSE AND COPYRIGHT
976              
977             Copyright 2016 Greg Foletta.
978              
979             This program is free software; you can redistribute it and/or modify it
980             under the terms of the the Artistic License (2.0). You may obtain a
981             copy of the full license at:
982              
983             L<http://www.perlfoundation.org/artistic_license_2_0>
984              
985             Any use, modification, and distribution of the Standard or Modified
986             Versions is governed by this Artistic License. By using, modifying or
987             distributing the Package, you accept this license. Do not use, modify,
988             or distribute the Package, if you do not accept this license.
989              
990             If your Modified Version has been derived from a Modified Version made
991             by someone other than you, you are nevertheless required to ensure that
992             your Modified Version complies with the requirements of this license.
993              
994             This license does not grant you the right to use any trademark, service
995             mark, tradename, or logo of the Copyright Holder.
996              
997             This license includes the non-exclusive, worldwide, free-of-charge
998             patent license to make, have made, use, offer to sell, sell, import and
999             otherwise transfer the Package with respect to any patent claims
1000             licensable by the Copyright Holder that are necessarily infringed by the
1001             Package. If you institute patent litigation (including a cross-claim or
1002             counterclaim) against any party alleging that the Package constitutes
1003             direct or contributory patent infringement, then this Artistic License
1004             to you shall terminate on the date that such litigation is filed.
1005              
1006             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
1007             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
1008             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
1009             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
1010             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
1011             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
1012             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
1013             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1014              
1015              
1016             =cut
1017              
1018             1; # End of Device::PaloAlto::Firewall::Test