@@ -25,12 +25,13 @@ use warnings;
2525use centreon::plugins::http;
2626use centreon::plugins::statefile;
2727use DateTime;
28- use Digest::MD5 qw( md5_hex) ;
28+ use URI::Escape;
29+ use Digest::SHA qw( sha256_hex) ;
2930use centreon::plugins::misc qw( json_decode is_empty value_of is_excluded) ;
3031
3132sub new {
3233 my ($class , %options ) = @_ ;
33- my $self = {};
34+ my $self = {};
3435 bless $self , $class ;
3536
3637 if (!defined ($options {output })) {
@@ -41,23 +42,24 @@ sub new {
4142 $options {output }-> add_option_msg(short_msg => " Class Custom: Need to specify 'options' argument." );
4243 $options {output }-> option_exit();
4344 }
44-
45+
4546 if (!defined ($options {noptions })) {
4647 $options {options }-> add_options(arguments => {
47- ' key-id:s' => { name => ' key_id' , default => ' ' },
48- ' key-secret:s' => { name => ' key_secret' , default => ' ' },
49- ' api-path:s' => { name => ' api_path' , default => ' /v1' },
50- ' hostname:s' => { name => ' hostname' , default => ' api.zdxcloud.net' },
51- ' port:s' => { name => ' port' , default => 443 },
52- ' proto:s' => { name => ' proto' , default => ' https' },
53- ' timeout:s' => { name => ' timeout' , default => 10 }
48+ ' auth-url:s' => { name => ' auth_url' , default => ' ' },
49+ ' client-id:s' => { name => ' client_id' , default => ' ' },
50+ ' client-secret:s' => { name => ' client_secret' , default => ' ' },
51+ ' api-path:s' => { name => ' api_path' , default => ' /zdx/v1' },
52+ ' hostname:s' => { name => ' hostname' , default => ' api.zsapi.net' },
53+ ' port:s' => { name => ' port' , default => 443 },
54+ ' proto:s' => { name => ' proto' , default => ' https' },
55+ ' timeout:s' => { name => ' timeout' , default => 10 }
5456 });
5557 }
5658 $options {options }-> add_help(package => __PACKAGE__ , sections => ' REST API OPTIONS' , once => 1);
5759
5860 $self -> {output } = $options {output };
59- $self -> {http } = centreon::plugins::http-> new(%options , default_backend => ' curl' );
60- $self -> {cache } = centreon::plugins::statefile-> new(%options );
61+ $self -> {http } = centreon::plugins::http-> new(%options , default_backend => ' curl' );
62+ $self -> {cache } = centreon::plugins::statefile-> new(%options );
6163
6264 return $self ;
6365}
@@ -70,14 +72,13 @@ sub get_token {
7072 $self -> settings();
7173
7274 my $has_cache_file = $self -> {cache }-> read (
73- statefile => ' zdx_token_' . md5_hex(
74- $self -> {hostname }
75- . ' :' . $self -> {port }
76- . ' _' . $self -> {key_id })
75+ statefile => ' zdx_token_' . sha256_hex(
76+ $self -> {auth_url }
77+ . ' _' . $self -> {client_id })
7778 );
7879 # return token stored in cache if exists after checking it is still valid
7980 if ($has_cache_file ) {
80- my $expiration = $self -> {cache }-> get(name => ' expiration' );
81+ my $expiration = $self -> {cache }-> get(name => ' expiration' );
8182 if (defined ($expiration ) && $expiration > time () + 60) {
8283 $self -> {token } = $self -> {cache }-> get(name => ' token' );
8384 $self -> {token_type } = $self -> {cache }-> get(name => ' token_type' );
@@ -89,17 +90,22 @@ sub get_token {
8990 # if we do not have a token or if it has to be renewed
9091 my $content = $self -> {http }-> request(
9192 method => ' POST' ,
92- url_path => ' /v1/oauth/token' ,
93- query_form_post => ' {"key_id": "' . $self -> {key_id } . ' ", "key_secret": "' . $self -> {key_secret } . ' "}' ,
94-
93+ full_url => $self -> {auth_url },
94+ header => [ ' Content-Type: application/x-www-form-urlencoded' ],
95+ query_form_post => " grant_type=client_credentials&client_id=" . uri_escape($self -> {client_id })
96+ . " &client_secret=" . uri_escape($self -> {client_secret }),
9597 );
9698 my $decoded_content = json_decode($content , output => $self -> {output });
9799
98- $self -> {output }-> option_exit(short_msg => " No token found in '$content '" ) unless ($decoded_content -> {token });
99- $self -> {token } = $decoded_content -> {token };
100- $self -> {token_type } = $decoded_content -> {token_type };
100+ $self -> {output }-> option_exit(short_msg => " No token found in '$content '" )
101+ unless ($decoded_content -> {access_token } || $decoded_content -> {token });
102+ # store in $self for further requests
103+ $self -> {token } = $decoded_content -> {access_token } // $decoded_content -> {token };
104+ $self -> {token_type } = $decoded_content -> {token_type } // ' Bearer' ;
105+ # store in cache for further executions
101106 $self -> {http }-> add_header(key => ' Authorization' , value => $self -> {token_type } . ' ' . $self -> {token });
102- $self -> {cache }-> write (data => { token => $decoded_content -> {token }, token_type => $decoded_content -> {token_type }, expiration => ($decoded_content -> {expires_in } + time ()) });
107+ $self -> {cache }-> write (data => { token => $self -> {token }, token_type => $self -> {token_type }, expiration => ($decoded_content -> {expires_in } + time ()) });
108+
103109 return $self -> {token };
104110}
105111
@@ -120,7 +126,7 @@ sub build_location_filters {
120126 # if location filters are provided, get the list of location ids
121127 my $locations = $self -> get_locations(%options );
122128 # $locations points to an array of {id, name}
123- return { loc => [map { $_ -> {id }} @$locations ] };
129+ return { loc => [ map {$_ -> {id }} @$locations ] };
124130}
125131
126132sub get_unique_app {
@@ -129,24 +135,26 @@ sub get_unique_app {
129135 my $content = $self -> {http }-> request(
130136 method => ' GET' ,
131137 get_params => $self -> {get_params },
138+ unknown_status => " %{http_code} < 200 or %{http_code} > 404" ,
132139 url_path => $self -> {option_results }-> {api_path } . ' /apps/' . $options {application_id }
133140 );
134141 my $app = json_decode($content , output => $self -> {output });
142+
135143 return {
136- id => $app -> {id },
137- name => $app -> {name },
138- score => $app -> {score },
139- total_users => $app -> {stats }-> {active_users }
144+ id => $app -> {id } // $options { application_id } ,
145+ name => $app -> {name } // " not found " ,
146+ score => $app -> {score } // -1 ,
147+ total_users => $app -> {stats }-> {active_users } // 0
140148 }
141149}
142150
143151sub get_unique_app_metrics {
144152 my ($self , %options ) = @_ ;
145153
146- my $to = time ();
147- my $get_params = { %{ $self -> {get_params }} };
154+ my $to = time ();
155+ my $get_params = { %{$self -> {get_params }} };
148156 $get_params -> {from } = $to - 60 * ($options {max_metrics_age } // 20);
149- $get_params -> {to } = $to ;
157+ $get_params -> {to } = $to ;
150158
151159 my $content = $self -> {http }-> request(
152160 method => ' GET' ,
@@ -183,6 +191,7 @@ sub get_unique_app_metrics {
183191 }
184192 }
185193
194+
186195 return $data ;
187196}
188197
@@ -221,6 +230,12 @@ sub get_apps {
221230 total_users => $app -> {total_users }
222231 };
223232 }
233+ push @stats , {
234+ id => $options {application_id } // -1,
235+ name => $options {include_application_name } //" not found" ,
236+ score => -1,
237+ total_users => 0
238+ } unless @stats ;
224239
225240 return \@stats ;
226241}
@@ -232,8 +247,8 @@ sub get_locations {
232247
233248 # get all locations and check which ones match the filters
234249 my $locations_json = $self -> {http }-> request(
235- method => ' GET' ,
236- url_path => $self -> {option_results }-> {api_path } . ' /administration/locations/'
250+ method => ' GET' ,
251+ url_path => $self -> {option_results }-> {api_path } . ' /administration/locations/'
237252 );
238253 my $locations = json_decode($locations_json , output => $self -> {output });
239254
@@ -246,8 +261,8 @@ sub get_locations {
246261 $options {exclude_location_name });
247262
248263 push @result , {
249- id => $loc -> {id },
250- name => $loc -> {name },
264+ id => $loc -> {id },
265+ name => $loc -> {name },
251266 };
252267 }
253268
@@ -256,13 +271,28 @@ sub get_locations {
256271
257272sub set_defaults {}
258273
274+ sub from_fqdn_to_url {
275+ my $input = shift ;
276+ # remove trailing /
277+ $input =~ s /\/ $// ;
278+ # if url is already complete, return
279+ return $input if $input =~ / ^https?:\/\/ [\w\. :]+\/ .+$ / ;
280+ # add default auth path if nothing after fqdn
281+ $input .= " /oauth2/v1/token" if $input =~ / ^(https?:\/\/ )?[\w\. :]+$ / ;
282+ # add default auth proto if no proto given
283+ $input = " https://" . $input if $input !~ / ^https?:\/\/ / ;
284+ return $input ;
285+ }
286+
259287sub check_options {
260288 my ($self , %options ) = @_ ;
261289
262- $self -> {$_ } = $self -> {option_results }-> {$_ } foreach qw( hostname port proto api_path timeout key_id key_secret ) ;
290+ $self -> {$_ } = $self -> {option_results }-> {$_ } foreach qw( hostname port proto timeout auth_url api_path client_id client_secret) ;
291+ $self -> {output }-> option_exit(short_msg => " Mandatory option 'auth-url' is missing for OneAPI authentication." ) if ($self -> {auth_url } eq ' ' );
263292
264- foreach (qw( key_id key_secret) ) {
265- $self -> {output }-> option_exit(short_msg => " Mandatory option '$_ ' is missing." ) if ($self -> {$_ } eq ' ' );
293+ $self -> {auth_url } = from_fqdn_to_url($self -> {auth_url });
294+ foreach (qw( client_id client_secret) ) {
295+ $self -> {output }-> option_exit(short_msg => " Mandatory option '$_ ' is missing for OneAPI authentication." ) if ($self -> {$_ } eq ' ' );
266296 $self -> {output }-> option_exit(short_msg => " Option '$_ ' contains illegal characters." ) if ($self -> {$_ } =~ / ([\b\f\n\r\t\"\\ ]+)/ );
267297 }
268298
@@ -274,9 +304,7 @@ sub check_options {
274304sub settings {
275305 my ($self , %options ) = @_ ;
276306
277- # $self->build_options_for_httplib();
278307 $self -> {http }-> add_header(key => ' Accept' , value => ' application/json' );
279- $self -> {http }-> add_header(key => ' Content-Type' , value => ' application/json' );
280308 $self -> {http }-> set_options(%{$self -> {option_results }});
281309}
282310
@@ -298,29 +326,37 @@ Zscaler Digital Experience (ZDX) Rest API
298326
299327=over 8
300328
329+ =item B<--auth-url >
330+
331+ Authentication URL (or FQDN) to get a token from (mandatory).
332+
333+ Depends on your Zscaler customer name.
334+
335+ Example: C<https://company-name.zslogin.net/oauth2/v1/token > or C<company-name.zslogin.net > .
336+
301337=item B<--hostname >
302338
303- ZDX API hostname (default: C<api.zdxcloud .net > )
339+ API URL (default: C<api.zsapi .net > ). You should not need to change it.
304340
305341=item B<--port >
306342
307- API port (default: 443)
343+ API port (default: 443).
308344
309345=item B<--proto >
310346
311- Specify http if needed (default: 'https')
347+ Specify http if needed (default: 'https').
312348
313349=item B<--api-path >
314350
315- API URL path (default: '/api ')
351+ API URL path (default: '/zdx/v1 ')
316352
317- =item B<--key -id >
353+ =item B<--client -id >
318354
319- Key ID (see L<here| https://help.zscaler.com/zdx/managing-zdx-api-keys> for more details).
355+ Client ID for OneAPI authentication (see https://help.zscaler.com/zidentity/understanding-oneapi-authentication for more details).
320356
321- =item B<--key -secret >
357+ =item B<--client -secret >
322358
323- Key secret (see L<here| https://help.zscaler.com/zdx/managing-zdx-api-keys> for more details).
359+ Client secret for OneAPI authentication (see https://help.zscaler.com/zidentity/understanding-oneapi-authentication for more details).
324360
325361=item B<--timeout >
326362
0 commit comments