File Coverage

blib/lib/Device/PaloAlto/Firewall/Test.pm
Criterion Covered Total %
statement 29 310 9.3
branch 0 148 0.0
condition 0 18 0.0
subroutine 10 50 20.0
pod 23 23 100.0
total 62 549 11.2


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