File Coverage

blib/lib/Device/PaloAlto/Firewall.pm
Criterion Covered Total %
statement 33 35 94.2
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 45 47 95.7


line stmt bran cond sub pod time code
1             package Device::PaloAlto::Firewall;
2              
3 6     6   367745 use 5.006;
  6         21  
4 6     6   29 use strict;
  6         11  
  6         105  
5 6     6   23 use warnings;
  6         14  
  6         229  
6              
7             our $VERSION = '0.091'; # VERSION - generated by DZP::OurPkgVersion
8              
9 6     6   1906 use Device::PaloAlto::Firewall::Test;
  6         15  
  6         186  
10              
11 6     6   43 use Moose;
  6         11  
  6         33  
12 6     6   23954 use Modern::Perl;
  6         13  
  6         37  
13 6     6   3691 use LWP::UserAgent;
  6         106635  
  6         176  
14 6     6   797 use HTTP::Request;
  6         13  
  6         134  
15 6     6   26 use Carp;
  6         10  
  6         324  
16 6     6   34 use Params::Validate qw{:all};
  6         12  
  6         881  
17 6     6   33 use URI;
  6         12  
  6         136  
18 6     6   9580 use XML::Twig;
  0            
  0            
19             use Memoize qw{memoize unmemoize};
20             use POSIX qw{strftime};
21             use Devel::StackTrace;
22              
23             use Data::Dumper;
24              
25             =head1 NAME
26              
27             Device::PaloAlto::Firewall - Interact with the Palo Alto firewall API
28              
29             =head1 VERSION
30              
31             version 0.091
32              
33             =cut
34              
35             =head1 SYNOPSIS
36              
37             Device::PaloAlto::Firewall provides interfaces to B<retrieve> information from a Palo Alto firewall.
38              
39             my $firewall = Device::PaloAlto::Firewall->new(uri => 'http://localhost.localdomain', username => 'admin', password => 'complex_password')
40              
41             my $environ = $firewall->environmentals();
42             my $interfaces = $firewall->interfaces();
43              
44             A key point is that that methods only retrieve information. There are no methods within this module to modify or commit configuration.
45              
46              
47             =head1 RETURN VALUES
48              
49             If the methods succeed they generally return either an ARRAYREF or a HASHREF. This includes an empty ARRAYREF or HASHREF if something is not configured or there are no entries (e.g. no OSPF neighbours).
50              
51             If the method fails - either because the device is unreachable, there's an authentication issue, or the device has thrown an error - it will croak a message and return undef.
52              
53             What type (ARRAYREF, HASHREF, etc) a method returns will be in each method's section, however the full data structures is not documented. They don't adhere to a strict schema, but examples for each method are provided on the L<Device::PaloAlto:Firewall::Return> page.
54              
55             =head1 CONSTRUCTOR
56              
57             The C<new()> constructor takes the following arguments:
58              
59             =over 4
60              
61             =item * C<uri> - A HTTP or HTTPS URI to the firewall.
62              
63             =item * C<username> - a username to authenticate to the device.
64              
65             =item * C<password> - a password for the username.
66              
67             =back
68              
69             =cut
70              
71             has 'user_agent' => ( is => 'ro', isa => 'LWP::UserAgent', init_arg => undef, default => sub { LWP::UserAgent->new } );
72             has 'uri' => ( is => 'ro', writer => '_uri', required => 1 );
73              
74             has 'username' => ( is => 'ro', isa => 'Str', required => 1 );
75             has 'password' => ( is => 'ro', isa => 'Str', required => 1 );
76             has '_api_key' => ( is => 'rw', init_arg => undef, default => undef );
77              
78             has 'optimise' => ( is => 'rw', init_arg => undef, trigger => \&_optimise_trigger, default => 0 );
79              
80             has 'trace' => ( is => 'rw', init_arg => undef, trigger => \&_trace_trigger, default => 0 );
81             has '_trace_fh' => ( is => 'rw' );
82             has '_trace_id' => ( is => 'rw', default => 0 );
83              
84             # These allow the calling routines to pull out the raw HTTP::Request and the raw Palo response
85             # if there's an error.
86             has '_raw_http_error' => ( is => 'rw' );
87             has '_raw_pa_error' => ( is => 'rw' );
88              
89              
90             sub BUILD {
91             my $self = shift;
92            
93             #URI string gets changed into a URI object
94             my $uri_obj = URI->new($self->uri);
95             if (!$uri_obj->has_recognized_scheme) {
96             croak "Unrecognised URI passed to constructor";
97             }
98              
99             #Set the path to API located
100             $uri_obj->path("/api/");
101             $self->_uri( $uri_obj );
102              
103             # Lower the timeout for the user agent to 15 seconds
104             $self->user_agent->timeout( 15 );
105              
106             }
107              
108              
109              
110              
111              
112              
113              
114              
115             =head1 METHODS
116              
117             =head2 META
118              
119             These methods affect the way requests are made to the firewalls.
120              
121             =head3 authenticate
122              
123             Manually authenticates to the firewall and retrieves an API key which is stored internally in the object.
124             If the authentication succeeds, returns 1. If the authentication fails or the device is not accessible, returns undef.
125              
126             If this isn't called explicitly, the first method to make a request to the firewall will see there is no API key and call C<authenticate()>.
127             This is presented to the user as it useful to test for connectivity and authentication before making other requests.
128              
129             =cut
130              
131             sub authenticate {
132             my $self = shift;
133              
134             return 1 if $self->_api_key;
135              
136             my $http_request = HTTP::Request->new();
137             $http_request->method('GET');
138              
139             $self->uri->query( "type=keygen&user=".$self->username."&password=".$self->password );
140             $http_request->uri( $self->uri->as_string );
141            
142             # Get the HTTP response and check it for errors
143             my $http_response = $self->_send_http_request($http_request);
144             return if !$self->_check_http_response($http_response);
145              
146             # Get the PA response (XML to a Perl Structure) from the body and check for errors
147             my $api_key_response = $self->_get_pa_response($http_response);
148             return if !$self->_check_pa_response($api_key_response);
149              
150             if (!$api_key_response or !$api_key_response->{result} or !$api_key_response->{result}->{key}) {
151             carp "API key error: no valid key in response";
152             return;
153             }
154              
155             $self->_api_key( $api_key_response->{result}->{key} );
156              
157             return 1;
158             }
159              
160              
161             =head3 verify_hostname
162              
163             Enables/disables the verification of the peer certificate and hostname if 'https' is used for API calls. By default TLS peer verification is B<enabled>.
164              
165             $fw->verify_hostname(1); Enable TLS peer verification
166             $fw->verify_hostname(0); Disable TLS verification
167              
168             =cut
169              
170             sub verify_hostname {
171             my $self = shift;
172             my $verify_bool = shift;
173             my $verify_mode = $verify_bool ?
174             0x01 # 'SSL_VERIFY_PEER'
175             :
176             0x00; # 'SSL_VERIFY_NONE'
177              
178             $self->user_agent->ssl_opts( verify_hostname => $verify_bool, SSL_verify_mode => $verify_mode );
179              
180             return;
181             }
182              
183             =head3 optimise
184              
185             Enables/disables the local caching of requests and responses to the firewall. This is disabled by default.
186              
187             $fw->optimise(1); # Enable optimisation
188             my $system_info = $fw->system_info(); # API call to retrieve interface information
189             $system_info = $fw->system_info(); # Information retrieved from local cache
190              
191             The first call to C<system_info()> will make an API call to the firewall and cache the result. The second request will retrieve the response from the local cache without making an API call.
192             Under the covers it uses C<Memoize> to cache the API request call. This means that each function & arguments receive their own cache. For example:
193            
194             $fw->optimise(1);
195             my $default_vr bgp_peers = $fw->bgp_peers(vrouter => 'default');
196             my $other_vr_bgp_peers = $fw->bgp_peers(vrouter => 'other');
197              
198             Both of these methods would make an API call to the firewall as the arguments differ.
199              
200             =cut
201              
202             sub _optimise_trigger {
203             my ($self, $new, $old) = @_;
204              
205             return if !($old xor $new); # Return if previous and new values are the same.
206              
207             if ($new) {
208             memoize('Device::PaloAlto::Firewall::_send_request');
209             } else {
210             unmemoize('Device::PaloAlto::Firewall::_send_request');
211             }
212              
213             return;
214             }
215              
216             =head3 trace
217              
218             Enables/disables tracing of the requests and responses to/from the firewall. If trace is set to 1, a file named <uri>_<datetime>.trace is created in the current working directory (e.g. 192.168.1.1_10-09-11_03:22:31.trace).
219             This file is has the string representations of the HTTP requests and responses written to it.i If trace is set to 2, a stacktrace is written as well.
220              
221             As the trace represents requests/responses to/from the firewall, if the C<optimise()> method is set to 1 only B<non-cached> requests and responses are written to the file.
222              
223             If the trace file cannot be opened, a warning is printed and trace reset back to 0;
224              
225              
226             =cut
227              
228             sub _trace_trigger {
229             my ($self, $trigger, $prev_trigger) = @_;
230              
231             return if !($trigger xor $prev_trigger);
232              
233             if ($trigger) {
234             # If trigger is true, lets open the filename
235             my $fh;
236             my $filename = $self->uri->host() . "_" . strftime('%Y-%m-%d_%H:%M:%S', gmtime()) . ".trace";
237              
238             if (!open $fh, '>', $filename) {
239             croak "Cannot open '$filename' for tracing: $!";
240             $self->trace(0);
241             }
242              
243             $self->_trace_fh( $fh );
244             } else {
245             # If trigger is false, we close the file
246             close( $self->_trace_fh );
247             }
248              
249             }
250              
251              
252              
253             =head3 tester
254              
255             Retrieves a C<Device::PaloAlto::Firewall::Test> object for this firewall.
256              
257             use Test::More;
258             my $test = Device::PaloAlto::Firewall->new(uri => 'http://remote_pa.domain', username => 'test', password => 'test')->tester();
259              
260             ok( $test->interfaces_up(interfaces => ['ethernet1/1']) );
261              
262             For more information, see the L<Device::PaloAlto::Firewall::Test> documentation.
263            
264             =cut
265              
266             sub tester {
267             my $self = shift;
268              
269             return Device::PaloAlto::Firewall::Test->new(firewall => $self);
270             }
271              
272             =head2 PLATFORM
273              
274             These methods retrieve information on the firewall platform.
275              
276             =head3 system_info
277              
278             Returns system information from the firewall.
279              
280             my $system_info = $fw->system_info();
281             say "Current Time on Firewall: $system_info->{time}";
282              
283             =cut
284              
285             sub system_info {
286             my $self = shift;
287             my $system_info = $self->_send_request(command => "<show><system><info></info></system></show>");
288              
289             return if !defined $system_info;
290              
291             return $system_info->{system};
292             }
293              
294              
295              
296             =head3 environmentals
297              
298             Returns information on the system environmentals. This includes the fantray and fans, power supplies and power, temperature. B<Note:> virtual machines don't have any environmental information and won't return any information.
299              
300             =cut
301              
302             sub environmentals {
303             my $self = shift;
304              
305             my $environs = $self->_send_request(command => "<show><system><environmentals></environmentals></system></show>");
306              
307             return if !defined $environs;
308              
309             # Our structure comes back looking like
310             # { $property => { $slot => { 'entry' => [ { %info } ] } } }
311             #
312             # We modify the structure to remove the redundant 'entry' and make sure
313             # Single and multiple '%info' hashes are in an arrayref
314             # { $property => { $slot => [ { %info } ] } }
315            
316             for my $property (values %{ $environs }) {
317             for my $slot (values %{ $property }) {
318             $slot = $slot->{entry};
319             }
320             }
321              
322              
323             return $environs;
324             }
325              
326              
327              
328             =head3 high_availability
329              
330             Retrieves information on the high availability status of the firewall.
331              
332             =cut
333              
334             sub high_availability {
335             my $self = shift;
336             my $ha = $self->_send_request(command => "<show><high-availability><all></all></high-availability></show>");
337              
338             return if !defined $ha;
339              
340             return {} if !%{ $ha };
341              
342             return $ha;
343             }
344              
345              
346              
347             =head3 software_check
348              
349             Asks the firewall to make a request to the Palo Alto update server to get a list of the available PAN-OS software. Returns an ARRAYREF
350             of all of the software available. If it cannot reach the server, an empty ARRAYREF is returned.
351              
352             =cut
353              
354             sub software_check {
355             my $self = shift;
356              
357             # Test for the 255 comms error condition we don't want to carp on
358             my $comms_error_test = sub {
359             # $_[0] is $self
360             return (
361             defined $_[0]->_raw_pa_error and
362             $_[0]->_raw_pa_error->{code} eq '255' and
363             $_[0]->_raw_pa_error->{msg}->{line} =~ m{Failed to check upgrade info due to generic communication error.}ms
364             );
365             };
366              
367             my $software = $self->_request_with_supressed_error(
368             command => '<request><system><software><check></check></software></system></request>',
369             test => $comms_error_test,
370             return_sup_err => sub { return {} }
371             );
372              
373             return if !defined $software;
374            
375             return [] if !%{ $software }; # Retrun an empty ARRAYREF if we receive an empty HASHREF back
376              
377              
378             return $software->{'sw-updates'}->{versions}->{entry};
379             }
380              
381              
382              
383             =head3 content_check
384              
385             Asks the firewall to make a request to the Palo Alto update server to get a list of the available content. Returns an ARRAYREF
386             of all of the content available. If it cannot reach the server, an empty ARRAYREF is returned.
387              
388             =cut
389              
390             sub content_check {
391             my $self = shift;
392              
393             # Test for the 255 comms error condition we don't want to carp on
394             my $comms_error_test = sub {
395             # $_[0] is $self
396             return (
397             defined $_[0]->_raw_pa_error and
398             $_[0]->_raw_pa_error->{code} eq '255' and
399             $_[0]->_raw_pa_error->{msg}->{line} =~ m{Failed to check content upgrade info due to generic communication error.}ms
400             );
401             };
402              
403             my $content= $self->_request_with_supressed_error(
404             command => '<request><content><upgrade><check></check></upgrade></content></request>',
405             test => $comms_error_test,
406             return_sup_err => sub { return {} }
407             );
408              
409             return if !defined $content;
410              
411             return [] if !%{ $content };
412              
413             return $content->{'content-updates'}->{entry};
414             }
415              
416              
417              
418             =head3 antivirus_check
419              
420             Asks the firewall to make a request to the Palo Alto update server to get a list of the available antivirus signatures. Returns an ARRAYREF
421             of all of the signatures available. If it cannot reach the server, an empty ARRAYREF is returned.
422              
423             =cut
424              
425             sub antivirus_check {
426             my $self = shift;
427              
428             # Test for the 255 comms error condition we don't want to carp on
429             my $comms_error_test = sub {
430             # $_[0] is $self
431             return (
432             defined $_[0]->_raw_pa_error and
433             $_[0]->_raw_pa_error->{code} eq '255' and
434             $_[0]->_raw_pa_error->{msg}->{line} =~ m{Failed to check content upgrade info due to generic communication error.}ms
435             );
436             };
437              
438             my $av = $self->_request_with_supressed_error(
439             command => '<request><anti-virus><upgrade><check></check></upgrade></anti-virus></request>',
440             test => $comms_error_test,
441             return_sup_err => sub { return {} }
442             );
443              
444             return if !defined $av;
445              
446             return [] if !%{ $av };
447              
448             return $av->{'content-updates'}->{entry};
449             }
450              
451              
452              
453             =head3 gp_client_check
454              
455             Asks the firewall to make a request to the Palo Alto update server to get a list of the GlobalProtect clients available. Returns an ARRAYREF
456             of all of the clients available. If it cannot reach the server, an empty ARRAYREF is returned.
457              
458             =cut
459              
460             sub gp_client_check {
461             my $self = shift;
462              
463             # Test for the 255 comms error condition we don't want to carp on
464             my $comms_error_test = sub {
465             # $_[0] is $self
466             return (
467             defined $_[0]->_raw_pa_error and
468             $_[0]->_raw_pa_error->{code} eq '255' and
469             $_[0]->_raw_pa_error->{msg}->{line} =~ m{Failed to check upgrade info due to generic communication error.}ms
470             );
471             };
472              
473             my $gp_client = $self->_request_with_supressed_error(
474             command => '<request><global-protect-client><software><check></check></software></global-protect-client></request>',
475             test => $comms_error_test,
476             return_sup_err => sub { return {} }
477             );
478              
479             return if !defined $gp_client;
480              
481             return [] if !%{ $gp_client };
482              
483             return $gp_client->{'sw-updates'}->{versions}->{entry};
484             }
485              
486              
487              
488             # This method creates a request that has the ability to supress error carping.
489             # It takes:
490             # command - the command to send to the firewall
491             # test - test for the error condition to supress
492             # return_sup_err - what to return from the supressed error condfition.
493             # It always returns undef for non supressed errors.
494             #
495             # How it works:
496             # We save the original carp to a lexial, localise 'carp' and capture the message that woud have been carped if there was an error.
497             # We then make the request, and check if there was an error
498             # * If the error test returns true, we return 'return_sup_error->()
499             # * If it was any other error, we carp what should have been and return undef.
500             # * Otherwise we return 'return_no_err->()'
501             #
502             sub _request_with_supressed_error {
503             my $self = shift;
504              
505             # The CODEREFs are each passed the following values from the function:
506             # $_[0] = $self
507             # $_[1] = $fw_return
508             # $_[2] = $carped_message
509             #
510             # By default, the captured error will return an empty ARRAYREF, and no
511             # error will return the structure from the firewall. A non-captured
512             # error always returns undef.
513             my %args = validate(@_,
514             {
515             command => { type => SCALAR },
516             test => { type => CODEREF },
517             return_sup_err => { type => CODEREF | UNDEF, default => sub { return [] } },
518             }
519             );
520              
521             {
522             my $carped_message;
523             no warnings 'redefine';
524              
525              
526             # Save the previous version of carp, and create a new version that only captures the messagse to carp.
527             my $saved_carp = \&Device::PaloAlto::Firewall::carp;
528             local *Device::PaloAlto::Firewall::carp = sub { $carped_message = $_[0] };
529              
530             # Send the command to the firewall
531             my $fw_return= $self->_send_request(command => "$args{command}");
532              
533             # If the command returned undef, there was an error. The error message is now in $carped_message.
534             if (!defined $fw_return) {
535             if ($args{test}->($self, $carped_message, $fw_return)) { # If our custom test returns true
536             return $args{return_sup_err}->($self, $fw_return, $carped_message);
537             } else {
538             $saved_carp->($carped_message);
539             return;
540             }
541             }
542              
543             return $fw_return;
544             }
545             }
546              
547              
548              
549              
550              
551             =head3 licenses
552              
553             Returns an ARRAYREF with information on the licenses installed on the firewall. Includes active and expired licenses.
554             If there are no licenses installed on the firewall, an empty ARRAYREF is returned.
555              
556             =cut
557              
558             sub licenses {
559             my $self = shift;
560              
561             my $licensing = $self->_send_request(command => '<request><license><info></info></license></request>');
562              
563             return if !defined $licensing;
564              
565             return [] if !%{ $licensing->{licenses} };
566              
567             return $licensing->{licenses}->{entry};
568             }
569              
570             =head2 NETWORK
571              
572             These methods retrieve network information from the firewall.
573              
574             =head3 interfaces
575              
576              
577             Retrieves interface information.
578              
579             =cut
580              
581             sub interfaces {
582             my $self = shift;
583             my $interfaces = $self->_send_request(command => "<show><interface>all</interface></show>");
584             return $interfaces;
585             }
586              
587              
588              
589             =head3 interface_counters_logical
590              
591             Retrieves information on the logical interface counters.
592              
593             =cut
594              
595             sub interface_counters_logical {
596             my $self = shift;
597             my $counters = $self->_send_request(command => '<show><counter><interface>all</interface></counter></show>');
598              
599             return if !defined $counters;
600              
601             my $ret = $counters->{ifnet}->{ifnet}->{entry};
602              
603             return [] if !defined $ret;
604              
605             return $ret;
606             }
607              
608              
609              
610             =head3 routing_table
611              
612             Retrives information on the routing table for a particular virtual router. If no C<vrouter> argument is specified it retrieves the 'default' vrouter's routing table.
613              
614             my $default_vr_table = $fw->routing_table();
615             my $corp_vr_table = $fw->routing_table(vrouter => 'corp');
616              
617             =cut
618              
619             sub routing_table {
620             my $self = shift;
621             my %args = validate(@_,
622             {
623             vrouter => { default => 'default', type => SCALAR | UNDEF },
624             }
625             );
626              
627             # TODO: Have a look at sanitising the argument passed to the firewall.
628             my $routing_table = $self->_send_request(command => "<show><routing><route><virtual-router>$args{vrouter}</virtual-router></route></routing></show>");
629             return $routing_table->{entry};
630             }
631              
632              
633              
634             =head3 bgp_peers
635              
636             Retrieves information on the configured BGP peers for a particular virtual router. If no C<vrouter> argument is specified it retrieves the 'default' vrouter's BGP peers.
637              
638             my $default_vr_bgp_peers = $fw->bgp_peers();
639             my $corp_vr_bgp_peers = $fw->bgp_peers(vrouter => 'corp');
640              
641             =cut
642              
643             sub bgp_peers {
644             my $self = shift;
645             my %args = validate(@_,
646             {
647             vrouter => { default => 'default', type => SCALAR | UNDEF },
648             }
649             );
650              
651             # TODO: Have a look at sanitising the argument passed to the firewall.
652             my $bgp_peer_response = $self->_send_request(command =>
653             "<show><routing><protocol><bgp><peer><virtual-router>$args{vrouter}</virtual-router></peer></bgp></protocol></routing></show>"
654             );
655              
656             return if !defined $bgp_peer_response;
657              
658             return [] if !%{ $bgp_peer_response }; # No BGP peers configured.
659              
660             return $bgp_peer_response->{entry};
661             }
662              
663              
664              
665             =head3 bgp_rib
666              
667             Retrieves information the local routing information base (RIB) for a specific virtual router. If no C<vrouter> argument is specified, the 'default' vrouter's loc RIB is returned.
668              
669             my $default_vr_rib = $fw->bgp_rib();
670             my $corp_vr_rib = $fw->bgp_rib(vrouter => 'corp');
671              
672             If BGP is not configured, or there are no prefixes in the local RIB, an empty ARRAYREF is returned. Otherwise an ARRAYREF is returned containing the prefixes in the local RIB.
673              
674             =cut
675              
676             sub bgp_rib {
677             my $self = shift;
678             my %args = validate(@_,
679             {
680             vrouter => { default => 'default', type => SCALAR | UNDEF },
681             }
682             );
683              
684             # TODO: Have a look at sanitising the argument passed to the firewall.
685             my $bgp_rib = $self->_send_request(command =>
686             "<show><routing><protocol><bgp><loc-rib><virtual-router>$args{vrouter}</virtual-router></loc-rib></bgp></protocol></routing></show>"
687             );
688              
689             return if !defined $bgp_rib;
690              
691             # As we're only getting a single VR, there's only one array member, hence the [0].
692             my $rib_prefixes_ref = $bgp_rib->{entry}->[0]->{'loc-rib'};
693              
694             # Return and empty arrayref if there's nothing in the loc RIB.
695             return [] if !%{ $rib_prefixes_ref };
696              
697             return $rib_prefixes_ref->{member};
698             }
699              
700              
701              
702             =head3 ospf_neighbours
703              
704             Returns and ARRAYREF containing information on the current OSPF neighbours for a specific virtual router. If no C<vrouter> argument is specified, the 'default' vrouter's neighbours are returned.
705              
706             If OSPF is not configured, or there are no OSPF neighbours up, an empty ARRAYREF
707              
708             Neighbours are returned who have not completed a full OSPF handshake - for example they may be in EXSTART if there is an MTU mismatch on the interface.
709              
710             =cut
711              
712             sub ospf_neighbours {
713             my $self = shift;
714             my %args = validate(@_,
715             {
716             vrouter => { default => 'default', type => SCALAR | UNDEF },
717             }
718             );
719              
720             my $ospf_neighbours = $self->_send_request(command =>
721             "<show><routing><protocol><ospf><neighbor><virtual-router>$args{vrouter}</virtual-router></neighbor></ospf></protocol></routing></show>"
722             );
723              
724             return if !defined $ospf_neighbours;
725              
726             return [] if _is_null_response($ospf_neighbours->{entry});
727              
728             return $ospf_neighbours->{entry};
729             }
730              
731              
732             =head3 pim_neighbours
733              
734             Retrieves information on the PIM neighbours for a specific virtual router. If no C<vrouter> argument is specified, the neighbours for the 'default' vrouter are returned.
735              
736             my $pim_neighbours = $fw->pim_neighbours(vrouter => 'corp');
737              
738             If PIM is not configured, or there are currently no neighbours, an empty ARRAYREF is returned.
739              
740             =cut
741              
742             sub pim_neighbours {
743             my $self = shift;
744             my %args = validate(@_,
745             {
746             vrouter => { default => 'default', type => SCALAR | UNDEF },
747             }
748             );
749              
750             my $pim_neighbours = $self->_send_request(command =>
751             "<show><routing><multicast><pim><neighbor><virtual-router>$args{vrouter}</virtual-router></neighbor></pim></multicast></routing></show>"
752             );
753              
754             return if !defined $pim_neighbours;
755              
756             return [] if !%{ $pim_neighbours };
757              
758             return $pim_neighbours->{entry};
759             }
760              
761             =head3 bfd_peers
762              
763             Returns information on BFD peers.
764              
765             =cut
766              
767             sub bfd_peers {
768             my $self = shift;
769              
770             my $bfd_peers = $self->_send_request(command => '<show><routing><bfd><summary></summary></bfd></routing></show>');
771              
772             return if !defined $bfd_peers;
773              
774             return [] if !defined $bfd_peers->{entry};
775              
776              
777             # The interfaces seem to have trailing whitespace, e.g.:
778             # $VAR1 = [ {
779             # 'status' => 'up',
780             # 'interface' => 'ethernet1/23 '
781             # }, ]
782             # We go through and remove it.
783             map { $_->{interface} =~ s{\s+$}{} } @{ $bfd_peers->{entry} };
784              
785             return $bfd_peers->{entry};
786             }
787              
788             =head2 MANAGEMENT
789              
790             These methods retrieve information on the management / operational status of the firewall.
791              
792             =head3 ntp
793              
794             Retrieves information on the current synchronisation and reachability of configured NTP peers.
795              
796             =cut
797              
798             sub ntp {
799             my $self = shift;
800             my $ntp = $self->_send_request(command => "<show><ntp></ntp></show>");
801              
802             return if !defined $ntp;
803              
804             return $ntp;
805             }
806              
807              
808              
809             =head3 panorama_status
810              
811             Returns information on the current Panorama runtime status.
812              
813             =cut
814              
815             sub panorama_status {
816             my $self = shift;
817             my @ret;
818            
819             my $panorama_status_regex = qr{
820             Panorama\s+Server\s+(?<id>\d)
821             \s+ : \s+
822             (?<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})
823             \n
824             \s+ Connected \s+ : \s+ (?<connected>\w+)
825             \n
826             \s+ HA \s state \s+ : \s+ (?<ha_state>\w+)
827             }xms;
828              
829             my $panorama_status = $self->_send_request(command => '<show><panorama-status></panorama-status></show>');
830              
831             return if !defined $panorama_status;
832              
833             return [] if ref $panorama_status eq 'HASH' and !%{ $panorama_status };
834              
835             while ($panorama_status =~ m{$panorama_status_regex}g) {
836             my %pano_peer = %+;
837             push @ret, \%pano_peer;
838             }
839              
840             return \@ret;
841             }
842              
843             =head2 SECURITY
844              
845             These methods retrieve information on the security functions of the firewall.
846              
847             =head3 ip_user_mapping
848              
849             Returns the ip to user mapping table.
850              
851             =cut
852              
853             sub ip_user_mapping {
854             my $self = shift;
855              
856             my $ip_user_mappings = $self->_send_request(command => '<show><user><ip-user-mapping><all></all></ip-user-mapping></user></show>');
857              
858             return if !defined $ip_user_mappings;
859              
860              
861             return [] if !%{ $ip_user_mappings };
862              
863             # Split the user and domain into their own keys
864             IP_USER_MAP:
865             for my $user_map (@{ $ip_user_mappings->{entry} }) {
866             if (lc $user_map->{user} eq 'unknown') {
867             $user_map->{domain} = 'unknown';
868             next IP_USER_MAP;
869             }
870              
871              
872             # Split on the backslash
873             my @domain_and_user = split(m{\\}, $user_map->{user});
874             carp "User to IP mapping contains no deliniaton ('\\') between domain and user: $user_map->{user}" if @domain_and_user != 2;
875            
876             $user_map->{domain} = $domain_and_user[0];
877             $user_map->{user} = $domain_and_user[1];
878             }
879              
880             return $ip_user_mappings->{entry};
881             }
882              
883              
884              
885             =head3 userid_server_monitor
886              
887             Returns the state of the servers used to monitor User-ID IP-to-user mappings.
888              
889             =cut
890              
891             sub userid_server_monitor {
892             my $self = shift;
893             my @ret;
894              
895             my $server_monitor = $self->_send_request(command => '<show><user><server-monitor><statistics></statistics></server-monitor></user></show>');
896              
897             return if !defined $server_monitor;
898              
899             return {} if !$server_monitor;
900              
901             # Clean up the output, turning it into an ARRARREF rather than a HASHREF keyed on the server name
902             for my $server (keys %{ $server_monitor->{entry} }) {
903             $server_monitor->{entry}->{ $server }->{name} = $server;
904             push @ret, $server_monitor->{entry}->{ $server };
905             }
906              
907             return \@ret;
908             }
909              
910              
911              
912             =head3 ike_peers
913              
914             Returns information on active IKE (Phase 1) VPN peers.
915              
916             =cut
917              
918             sub ike_peers {
919             my $self = shift;
920              
921             my $ike_peers = $self->_send_request(command => '<show><vpn><ike-sa></ike-sa></vpn></show>');
922              
923             return if !defined $ike_peers;
924              
925             return [] if !%{ $ike_peers };
926              
927             return $ike_peers->{entry};
928             }
929              
930              
931              
932             =head3 ipsec_peers
933              
934             Returns information on the active IPSEC (Phase 2) VPN peers.
935              
936             =cut
937              
938             sub ipsec_peers {
939             my $self = shift;
940              
941             my $ipsec_peers = $self->_send_request(command => '<show><vpn><ipsec-sa></ipsec-sa></vpn></show>');
942              
943             return if !defined $ipsec_peers;
944              
945             return [] if !%{ $ipsec_peers->{entries} };
946              
947             return $ipsec_peers->{entries}->{entry};
948             }
949              
950              
951              
952             =head3 vpn_tunnels
953              
954             Returns dataplane IPSEC VPN tunnel information.
955              
956             =cut
957              
958             sub vpn_tunnels {
959             my $self = shift;
960              
961             my $vpn_tunnels = $self->_send_request(command => '<show><vpn><flow></flow></vpn></show>');
962              
963             return if !defined $vpn_tunnels;
964              
965             return [] if !%{ $vpn_tunnels->{IPSec} };
966              
967             return $vpn_tunnels->{IPSec}->{entry};
968              
969             }
970              
971              
972              
973              
974              
975              
976              
977              
978             ####################
979             # Utility Functions
980             #
981             ####################
982              
983              
984             sub _send_request {
985             my $self = shift;
986             my %args = validate(@_,
987             {
988             command => 1,
989             }
990             );
991              
992             my $http_request = HTTP::Request->new();
993             $http_request->method('GET');
994              
995             # Is the API key defined? If not, request one.
996             if (!defined $self->_api_key) {
997             return if !$self->authenticate();
998             }
999              
1000             #Set up the query string and the HTTP request
1001             $self->uri->query( "type=op&cmd=$args{command}&key=".$self->_api_key );
1002             $http_request->uri( $self->uri->as_string );
1003              
1004             # Reset the error codes and string. These will be set if there's
1005             # an error in the _check_http_response and the _check_pa_response
1006             $self->_raw_http_error(undef);
1007             $self->_raw_pa_error(undef);
1008              
1009             # Send and get the HTTP response, write it to a tracefile if it's enabled,
1010             # and check it for errors
1011             my $http_response = $self->_send_http_request($http_request);
1012             $self->_write_tracefile($http_request, $http_response);
1013             return if !$self->_check_http_response($http_response);
1014              
1015             # Get the PA response (XML to a Perl Structure) from the body and check for errors
1016             my $pa_response = $self->_get_pa_response($http_response);
1017             return if !$self->_check_pa_response($pa_response);
1018              
1019             # Return the structure
1020             return $pa_response->{result};
1021             }
1022              
1023              
1024              
1025             sub _send_http_request {
1026             my $self = shift;
1027             my $http_request = shift;
1028            
1029             return $self->user_agent->request( $http_request );
1030              
1031             }
1032              
1033             sub _check_http_response {
1034             my $self = shift;
1035             my $http_response = shift;
1036              
1037             # Check the HTTP response codes
1038             if ($http_response->is_error) {
1039             carp "HTTP Error (".$http_response->code.")";
1040              
1041             $self->_raw_http_error( $http_response );
1042              
1043             return;
1044             }
1045              
1046             return 1;
1047             }
1048              
1049              
1050             sub _write_tracefile {
1051             my $self = shift;
1052             my ($http_req, $http_resp) = @_;
1053              
1054             # We increment the trace ID for each call
1055             $self->_trace_id( $self->_trace_id() + 1 );
1056              
1057             if ($self->trace) {
1058             my $fh = $self->_trace_fh();
1059              
1060             print $fh '=' x 10 . " ID" . $self->_trace_id . " Request " . '=' x 10 . "\n";
1061              
1062             print $fh $http_req->as_string();
1063              
1064             print $fh '=' x 10 . " ID" . $self->_trace_id . " Response " . '=' x 10 . "\n";
1065              
1066             print $fh $http_resp->as_string();
1067              
1068             if ($self->trace == 2) {
1069             print $fh '=' x 10 . " ID" . $self->_trace_id . " Stack Trace " . '=' x 10 . "\n";
1070             my $stack = Devel::StackTrace->new();
1071             print $fh $stack->as_string();
1072             }
1073              
1074             print $fh "\n\n";
1075             }
1076             }
1077              
1078             sub _get_pa_response {
1079             my $self = shift;
1080             my $http_response = shift;
1081             my $xml_parser = XML::Twig->new();
1082              
1083             my $pa_response_twig = $xml_parser->safe_parse( $http_response->decoded_content );
1084              
1085             if (!$pa_response_twig) {
1086             carp "Invalid XML returned from firewall";
1087             return;
1088             }
1089            
1090             my $pa_response = $pa_response_twig->simplify( forcearray => ['entry'] );
1091              
1092             return $pa_response;
1093             }
1094              
1095              
1096             sub _check_pa_response {
1097             my $self = shift;
1098             my $pa_response = shift;
1099              
1100             return if !defined $pa_response;
1101              
1102             if ($pa_response->{status} eq 'error') {
1103             # If there's no code, we create our own 'psuedo error code'
1104             $pa_response->{code} //= '255';
1105              
1106             carp "API Error: ".$self->_api_error_to_string($pa_response->{code});
1107              
1108             $self->_raw_pa_error( $pa_response );
1109              
1110             return;
1111             }
1112              
1113             return $pa_response;
1114             }
1115              
1116              
1117              
1118             sub _is_null_response {
1119             my $response = shift;
1120              
1121             if (!$response
1122             || (ref $response eq 'ARRAY' and !@{ $response })
1123             || (ref $response eq 'HASH' and !%{ $response })) {
1124             return 1;
1125             }
1126              
1127             return 0;
1128             }
1129              
1130              
1131             sub _api_error_to_string {
1132             my $self = shift;
1133             my $code = shift;
1134              
1135             return {
1136             400 => 'Bad request (400)',
1137             403 => 'Forbidden (403)',
1138             1 => 'Unknown command (1)',
1139             2 => 'Internal error (2)',
1140             3 => 'Internal error (3)',
1141             4 => 'Internal error (4)',
1142             5 => 'Internal error (5)',
1143             6 => 'Bad Xpath (6)',
1144             7 => 'Object not present (7)',
1145             8 => 'Object not unique (8)',
1146             10 => 'Reference count not zero (10)',
1147             11 => 'Internal error (11)',
1148             12 => 'Invalid object (12)',
1149             14 => 'Operation not possible (14)',
1150             15 => 'Operation denied (15)',
1151             16 => 'Unauthorized (16)',
1152             17 => 'Invalid command (17)',
1153             18 => 'Malformed (18)',
1154             19 => 'Success (19)',
1155             20 => 'Success (20)',
1156             21 => 'Internal error (21)',
1157             22 => 'Session timed out (22)',
1158             255 => 'Unknown Error Code',
1159             }->{$code};
1160             }
1161              
1162              
1163             sub _debug_print {
1164             my $self = shift;
1165             my $debug_msg = shift;
1166             my $debug_structure = shift;
1167              
1168              
1169             print STDERR $debug_msg."\n" if $self->debug == 1;
1170             print STDERR (Dumper $debug_structure) if $debug_structure;
1171              
1172             return;
1173             }
1174              
1175              
1176             =head1 AUTHOR
1177              
1178             Greg Foletta, C<< <greg at foletta.org> >>
1179              
1180             =head1 BUGS
1181              
1182             Please report any bugs or feature requests to C<bug-device-paloalto-firewall at rt.cpan.org>, or through
1183             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Device-PaloAlto-Firewall>. I will be notified, and then you'll
1184             automatically be notified of progress on your bug as I make changes.
1185              
1186              
1187              
1188              
1189             =head1 SUPPORT
1190              
1191             You can find documentation for this module with the perldoc command.
1192              
1193             perldoc Device::PaloAlto::Firewall
1194              
1195              
1196             You can also look for information at:
1197              
1198             =over 4
1199              
1200             =item * RT: CPAN's request tracker (report bugs here)
1201              
1202             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Device-PaloAlto-Firewall>
1203              
1204             =item * AnnoCPAN: Annotated CPAN documentation
1205              
1206             L<http://annocpan.org/dist/Device-PaloAlto-Firewall>
1207              
1208             =item * CPAN Ratings
1209              
1210             L<http://cpanratings.perl.org/d/Device-PaloAlto-Firewall>
1211              
1212             =item * Search CPAN
1213              
1214             L<http://search.cpan.org/dist/Device-PaloAlto-Firewall/>
1215              
1216             =back
1217              
1218              
1219             =head1 ACKNOWLEDGEMENTS
1220              
1221              
1222             =head1 LICENSE AND COPYRIGHT
1223              
1224             Copyright 2017 Greg Foletta.
1225              
1226             This program is free software; you can redistribute it and/or modify it
1227             under the terms of the the Artistic License (2.0). You may obtain a
1228             copy of the full license at:
1229              
1230             L<http://www.perlfoundation.org/artistic_license_2_0>
1231              
1232             Any use, modification, and distribution of the Standard or Modified
1233             Versions is governed by this Artistic License. By using, modifying or
1234             distributing the Package, you accept this license. Do not use, modify,
1235             or distribute the Package, if you do not accept this license.
1236              
1237             If your Modified Version has been derived from a Modified Version made
1238             by someone other than you, you are nevertheless required to ensure that
1239             your Modified Version complies with the requirements of this license.
1240              
1241             This license does not grant you the right to use any trademark, service
1242             mark, tradename, or logo of the Copyright Holder.
1243              
1244             This license includes the non-exclusive, worldwide, free-of-charge
1245             patent license to make, have made, use, offer to sell, sell, import and
1246             otherwise transfer the Package with respect to any patent claims
1247             licensable by the Copyright Holder that are necessarily infringed by the
1248             Package. If you institute patent litigation (including a cross-claim or
1249             counterclaim) against any party alleging that the Package constitutes
1250             direct or contributory patent infringement, then this Artistic License
1251             to you shall terminate on the date that such litigation is filed.
1252              
1253             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
1254             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
1255             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
1256             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
1257             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
1258             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
1259             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
1260             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1261              
1262              
1263             =cut
1264              
1265             1; # End of Device::PaloAlto::Firewall