/[nagios-plugins-perl]/trunk/plugins/check_cert_chain.pl
ViewVC logotype

Contents of /trunk/plugins/check_cert_chain.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 204 - (show annotations) (download)
Fri Oct 5 14:59:46 2018 UTC (2 years, 1 month ago) by racvision
File MIME type: text/plain
File size: 14772 byte(s)
Fusion des modifs sur supervision01/02 et racvision3. Pas fini.
1 #!/usr/bin/perl -w
2 #
3 # This script is a Nagios plugin to check the certificate chain (SSL)
4 #
5 # Copyright (c) 2015 - Jean-Christophe TOUSSAINT <jean-christophe.toussaint@ac-nancy-metz.fr>
6 # Copyright (c) 2017 - Stéphane URBANOVSKI <s.urbanovski@ac-nancy-metz.fr>
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 #
21 # $Id: $
22
23
24
25 ### TODO:
26 ### - test de présence d'un cert root ex: IGC/A (paramètre supplémentaire)
27 ### - test startdate
28 ### - ajouter la possibilité d'ignorer un certificat
29 ### - prendre le premier certificat qui périme, et l'ajouter dans le OK final
30
31
32
33 use strict;
34 use warnings;
35 use utf8;
36 use Nagios::Plugin qw/OK WARNING CRITICAL UNKNOWN %STATUS_TEXT/;
37 use Time::HiRes qw/time alarm/;
38 use File::Basename;
39 use Locale::gettext;
40 use POSIX qw/setlocale/;
41 use Data::Dumper;
42 use IPC::Open2;
43 use DateTime;
44 use DateTime::Format::Strptime;
45
46
47
48 ### global vars
49 my $PROGNAME = basename($0);
50 '$Revision: 1.0 $' =~ /^.*(\d+.\d+) \$$/; # Use The Revision from RCS/CVS/SVN
51 my $VERSION = $1;
52 our $param_debug = 0; ### debug or not debug, that is the question
53 my $tz = 'Europe/Paris';
54 my $global_start_time = time();
55
56
57
58 ### default configuration
59
60
61
62 ### i18n
63 setlocale(LC_MESSAGES, '');
64 textdomain('nagios-plugins-cert-chain');
65
66
67
68 our $np = Nagios::Plugin->new(
69 version => $VERSION,
70 blurb => _gt("Plugin to check Quantum DXi"),
71 usage => "Usage: %s --hostname|-H <hostname> [ --port|-P <port> ] [ --warning|-w <days> ] [ --critical|-c <days> ] [ --testdn|-n <domain-name> ] [ -d|--debug ] [ --help ] [ --timeout|-t <timeout> ] [ --openssl|-O <path-to-openssl-bin> ]",
72 timeout => 10,
73 extra => <<__EXTRADOC
74 Test expiration of all certificates from a SSL server
75
76 * --hostname|-H <hostname>: IP address of the SSL server
77 * --port|-P <port>: SSL port
78 * --warning|-w <days>: threshold "warning" (in days)
79 * --critical|-c <days>: threshold "critical" (in days)
80 * --testdn|-n <domain-name>: test the domain name
81 * --timeout|-t <timeout>: set the timeout of this plugin
82 * --openssl|-O <path-to-openssl-bin>: path to openssl binary
83 * -d|--debug: debug mode
84 * --help: this help
85 __EXTRADOC
86 );
87
88
89
90 ### command line arguments
91 $np->add_arg(
92 spec => 'debug|d',
93 help => '-d, --debug',
94 default => 0,
95 required => 0,
96 );
97
98 $np->add_arg(
99 spec => 'hostname|H=s',
100 help => _gt('Hostname'),
101 required => 1,
102 );
103
104 $np->add_arg(
105 spec => 'port|P=i',
106 help => 'Port',
107 required => 0,
108 default => 443
109 );
110
111 $np->add_arg(
112 spec => 'warning|w=i',
113 label => 'integer (days)',
114 help => _gt("Warning threshold, number of days before end date"),
115 default => 45,
116 required => 0,
117 );
118
119 $np->add_arg(
120 spec => 'critical|c=i',
121 label => 'integer (days)',
122 help => _gt("Critical threshold, number of days before end date"),
123 default => 0,
124 required => 0,
125 );
126
127 $np->add_arg(
128 spec => 'openssl|O=s',
129 help => _gt('Openssl, path to openssl binary'),
130 default => '/usr/bin/openssl',
131 required => 0,
132 );
133
134 $np->add_arg(
135 spec => 'testdn|n=s',
136 help => _gt('Test the domain name'),
137 default => '',
138 required => 0,
139 );
140
141
142
143 ### parse param, first part
144 $np->getopts;
145 $param_debug = $np->opts->get('debug');
146 my $param_hostname = $np->opts->get('hostname');
147 my $param_port = $np->opts->get('port');
148 my $param_timeout = $np->opts->get('timeout');
149 my $param_openssl = $np->opts->get('openssl');
150 my $param_testdn = $np->opts->get('testdn');
151
152
153
154 ### thresholds
155 our $param_warn = $np->opts->get('warning');
156 our $param_crit = $np->opts->get('critical');
157
158
159
160 ### check param
161 if ($param_debug == 1) {
162 print STDERR "debug activated\n";
163 }
164
165 if (length($param_hostname) == 0) {
166 $np->nagios_exit(CRITICAL, _gt('param hostname is empty'));
167 }
168 if ($param_hostname !~ /^[\w\d\-\.]+$/) {
169 $np->nagios_exit(CRITICAL, _gt('param hostname contains bad character'));
170 }
171
172 if ($param_port !~ /^[\d]+$/ || $param_port < 0 || $param_port > 65535) {
173 $np->nagios_exit(CRITICAL, _gt('param port contains bad character'));
174 }
175
176 if ($param_warn < 0) {
177 $np->nagios_exit(CRITICAL, _gt('param warning is below 0'));
178 }
179
180 if ($param_crit < 0) {
181 $np->nagios_exit(CRITICAL, _gt('param critical is below 0'));
182 }
183
184 if ($param_timeout <= 0) {
185 $np->nagios_exit(CRITICAL, _gt('param timeout is below 1'));
186 }
187
188 if (! -x $param_openssl) {
189 $np->nagios_exit(CRITICAL, _gt('param openssl is not found or not executable'));
190 }
191
192 if (length($param_testdn) > 0) {
193 debug('test domain name');
194 my ($dn, $aliases, $addrtype, $length, @addrs) = gethostbyname($param_testdn);
195 if (!defined($dn)) {
196 $np->add_message(WARNING, sprintf(_gt('cannot resolv domain %s'), $param_testdn));
197 }
198 }
199
200
201
202 ### timeout management
203 debug("timeout=$param_timeout");
204 $SIG{'ALRM'} = sub {
205 $np->nagios_exit(UNKNOWN, _gt("execution timeout!"));
206 };
207 alarm($param_timeout);
208
209
210
211 ### get certs chain list
212 my $openssl_s_client_cmd = "$param_openssl s_client -connect $param_hostname:$param_port -showcerts";
213 my $openssl_s_client_cmd_start_time = time(); ### timer
214 my %ret = exec_cmd($openssl_s_client_cmd, 'Q');
215 my $openssl_s_client_cmd_end_time = time(); ### timer
216
217 if ($ret{'status'} != OK) {
218 $np->nagios_exit($ret{'status'}, _gt($ret{'error'}));
219 }
220 debug("the command (openssl s_client) returns " . $ret{'returncode'});
221 if ($ret{'returncode'} != 0) {
222 debug('openssl s_client output: "' . join(' ', @{$ret{'output'}}) . '"');
223 $np->nagios_exit(UNKNOWN, _gt("failed to exec openssl s_client"));
224 }
225
226
227
228 ### exec time of openssl s_client (perf data)
229 $np->add_perfdata(
230 'label' => 't_s_client',
231 'value' => sprintf('%.6f', ($openssl_s_client_cmd_end_time - $openssl_s_client_cmd_start_time)),
232 'uom' => 's',
233 'min' => 0
234 );
235
236
237
238 ### catch all certificates
239 my @cert_list;
240 my $raw_cert_list = join('', @{$ret{'output'}});
241 while ($raw_cert_list =~ /(-+BEGIN CERTIFICATE-+.+?-+END CERTIFICATE-+)/sg) {
242 push @cert_list, $1;
243 }
244 my $s = (scalar(@cert_list) >= 2) ? 's' : '';
245 debug('found ' . scalar(@cert_list) . " cert$s");
246
247 if (scalar(@cert_list) == 0) {
248 $np->nagios_exit(CRITICAL, _gt("can't found any certificate"));
249 }
250
251
252
253 ### datetime parser
254 my $dt_parser = DateTime::Format::Strptime->new(
255 pattern => '%b %d %T %Y %Z',
256 locale => 'en_US'
257 );
258 my $dt_now = DateTime->now(time_zone => 'UTC');
259 my $openssl_x509_cmd_dur = 0.0;
260
261 my $next_cert_delta = 0;
262 my $next_cert_domain = '';
263 my $next_cert_expire = '';
264
265
266
267
268 ### loop on all certs
269 my $openssl_x509_cmd = "$param_openssl x509 -noout -startdate -enddate -subject -nameopt oneline";
270 my $cert_counter = 0;
271 foreach my $cert (@cert_list) {
272 $cert_counter++;
273
274 debug("### start test cert #$cert_counter");
275
276 ### execute openssl x509
277 my $openssl_x509_cmd_start_time = time(); ### timer
278 my %ret = exec_cmd($openssl_x509_cmd, $cert);
279 my $openssl_x509_cmd_end_time = time(); ### timer
280 $openssl_x509_cmd_dur += $openssl_x509_cmd_end_time - $openssl_x509_cmd_start_time;
281
282 if ($ret{'status'} != OK) {
283 $np->nagios_exit($ret{'status'}, _gt($ret{'error'}));
284 }
285
286 debug("the command (openssl x509) returns " . $ret{'returncode'});
287
288 if ($ret{'returncode'} != 0) {
289 debug('openssl x509 output: "' . join(' ', $ret{'output'}) . '"');
290 $np->nagios_exit(UNKNOWN, _gt("failed to exec openssl x509"));
291 }
292
293 ### test output
294 my @output = @{$ret{'output'}};
295 if (scalar(@output) == 0) {
296 $np->nagios_exit(CRITICAL, _gt("there is no output with openssl x509"));
297 }
298
299 ### clean output
300 for (my $i = 0; $i < scalar(@output); $i++) {
301 chomp $output[$i];
302 }
303
304 my $certname = '';
305 my $certdomain = '?';
306 my $certexpire = '?';
307
308 my $cn = '';
309
310 ### test subject
311 if ($output[2] =~ /^subject=\s*(.+)$/) {
312 my @split = split(/, /, $1);
313 my @certname;
314 foreach my $s (@split) {
315 if ($s =~ /^(O|CN) = (.+)$/) {
316 my $t = $1;
317 my $v = $2;
318 push(@certname, $v);
319 if ($t eq 'CN') {
320 $cn = $v;
321 $certdomain = $v;
322 }
323 }
324 }
325 $certname = join(', ', @certname);
326 $certname =~ s/^\s+//;
327
328 if (length($certname) == 0) {
329 $np->add_message(CRITICAL, sprintf(_gt('cannot find any Organization (O=) or Common Name (CN=) in subject (cert #%d)'), $cert_counter));
330 next;
331 }
332 }
333 else {
334 $np->add_message(CRITICAL, sprintf(_gt('there is no subject line (cert #%d)'), $cert_counter));
335 next;
336 }
337 debug("certname: $certname");
338
339 ### test enddate
340 if ($output[1] =~ /^notAfter=(.+)$/) {
341 my $str_enddate = $1;
342 debug("end date: $str_enddate");
343 if ($str_enddate =~ /^[a-z]+ \d \d{2}:/i) {
344 $str_enddate =~ s/ / /g;
345 debug("found whitespace before day number, new date: $str_enddate");
346 }
347
348 my $dt_enddate = $dt_parser->parse_datetime($str_enddate);
349 if (!defined($dt_enddate)) {
350 $np->add_message(CRITICAL, sprintf(_gt('cannot parse the end date (cert #%d)'), $cert_counter));
351 next;
352 }
353
354 my $pretty_enddate = $dt_enddate->ymd . ' ' . $dt_enddate->hms;
355 debug('parsed enddate: ' . $pretty_enddate);
356 $certexpire = $pretty_enddate;
357
358 my $delta = $dt_now->delta_days($dt_enddate);
359 my $deltadays = $delta->in_units('days');
360 debug('delta days: ' . $deltadays);
361
362 if ($dt_now >= $dt_enddate) {
363 $np->add_message(CRITICAL, sprintf(_gt('the certificate "%s" is expired (at %s, cert #%d)'), $certname, $pretty_enddate, $cert_counter));
364 }
365 elsif ($deltadays <= $param_crit) {
366 $np->add_message(CRITICAL, sprintf(_gt('the certificate "%s" expires in %d days (at %s, cert #%d)'), $certname, $deltadays, $pretty_enddate, $cert_counter));
367 }
368 elsif ($deltadays <= $param_warn) {
369 $np->add_message(WARNING, sprintf(_gt('the certificate "%s" expires in %d days (at %s, cert #%d)'), $certname, $deltadays, $pretty_enddate, $cert_counter));
370 }
371
372 if ( $next_cert_delta == 0 || $deltadays < $next_cert_delta) {
373 $next_cert_delta = $deltadays;
374 $next_cert_domain = $certdomain;
375 $next_cert_expire = $pretty_enddate;
376 }
377 }
378 else {
379 $np->nagios_exit(CRITICAL, sprintf(_gt("there is no end date (certname=%s)"),$certname));
380 }
381
382 ### test CN / domain name
383 if ($cert_counter == 1 && length($param_testdn) > 0) {
384 debug("test domain name: $param_testdn");
385
386 ### execute openssl x509 -text
387 my $openssl_x509text_cmd = "$param_openssl x509 -noout -text -nameopt oneline";
388 my $openssl_x509_cmd_start_time = time(); ### timer
389 my %ret = exec_cmd($openssl_x509text_cmd, $cert);
390 my $openssl_x509_cmd_end_time = time(); ### timer
391 $openssl_x509_cmd_dur += $openssl_x509_cmd_end_time - $openssl_x509_cmd_start_time;
392
393 if ($ret{'status'} != OK) {
394 $np->nagios_exit($ret{'status'}, _gt($ret{'error'}));
395 }
396
397 debug("the command (openssl x509 text) returns " . $ret{'returncode'});
398 if ($ret{'returncode'} != 0) {
399 debug('openssl x509 output: "' . join(' ', $ret{'output'}) . '"');
400 $np->nagios_exit(UNKNOWN, _gt("failed to exec openssl x509 text"));
401 }
402
403 ### test output
404 my @output = @{$ret{'output'}};
405 if (scalar(@output) == 0) {
406 $np->nagios_exit(CRITICAL, _gt("there is no output with openssl x509"));
407 }
408
409 ### clean output
410 for (my $i = 0; $i < scalar(@output); $i++) {
411 chomp $output[$i];
412 }
413
414 ### parse X509v3 Subject Alternative Name
415 my @altnames;
416 push @altnames, $cn;
417 foreach my $dns (@output) {
418 if ($dns =~ /^\s*DNS:(.+)$/) {
419 foreach my $alt (split(/, /, $dns)) {
420 $alt =~ s/^\s*DNS://;
421 push @altnames, $alt;
422 }
423 }
424 }
425 @altnames = uniq(@altnames); ### clean
426 debug('altnames=' . join(' ', @altnames));
427
428 ### test if param cn == @altnames
429 my $cn_ok = 0;
430 foreach my $an (@altnames) {
431 if ($an =~ /^\*/) {
432 debug("altname $an contains a wildcard, testing with wildcard mode");
433 my $antest = "$an";
434 $antest =~ s/^\*//;
435 if ($param_testdn =~ /^.+$antest$/) {
436 debug("the domain name $param_testdn matches altname $an (wildcard mode)");
437 $cn_ok++;
438 }
439 }
440 else {
441 if ($param_testdn eq $an) {
442 debug("the domain name $param_testdn matches altname $an");
443 $cn_ok++;
444 }
445 }
446 }
447 if ($cn_ok == 0) {
448 $np->add_message(CRITICAL, sprintf(_gt('cannot find %s in cert alt. names'), $param_testdn));
449 }
450 else {
451 debug('test domain name OK');
452 }
453 }
454
455 debug("### end test cert #$cert_counter");
456 }
457
458
459
460 ### timer openssl x509
461 $np->add_perfdata(
462 'label' => 't_x509',
463 'value' => sprintf('%.6f', $openssl_x509_cmd_dur),
464 'uom' => 's',
465 'min' => 0
466 );
467
468
469
470 ### timer nb cert
471 $np->add_perfdata(
472 'label' => 'nbcerts',
473 'value' => $cert_counter,
474 'min' => 0
475 );
476
477
478
479 ### global timer
480 my $global_end_time = time();
481 $np->add_perfdata(
482 'label' => 't',
483 'value' => sprintf('%.6f', ($global_end_time - $global_start_time)),
484 'uom' => 's',
485 'min' => 0
486 );
487
488
489
490 ### output
491 my ($status, $message) = $np->check_messages(
492 join => ', ',
493 join_all => ', '
494 );
495
496
497
498 ### add a pretty message if alright
499 if ($status == OK) {
500 if ($cert_counter >= 2) {
501 $message .= ', ' . sprintf(_gt('all certs are OK (%d), the certificate \'%s\' will expire on %s.'), $cert_counter, $next_cert_domain, $next_cert_expire);
502 }
503 else {
504 $message .= ', ' . sprintf(_gt('the certificate \'%s\' will expire on %s.'), $next_cert_domain, $next_cert_expire);
505 }
506 $message =~ s/^, //;
507 }
508
509
510
511 ### the end.
512 $np->nagios_exit($status, $message);
513
514
515
516 ### must never go here
517 exit;
518
519
520
521 ### functions
522
523
524
525 ### exec cmd
526 sub exec_cmd {
527 my $cmd = shift;
528 my $in = shift;
529
530 my %return;
531 $return{'returncode'} = -1;
532 $return{'status'} = UNKNOWN;
533 $return{'error'} = '';
534 $return{'output'} = \();
535
536
537 my $PROCESS_OUT;
538 my $PROCESS_IN;
539 my $pid;
540 my @output = ();
541
542 debug("exec command: $cmd");
543 eval {
544 $pid = open2($PROCESS_OUT, $PROCESS_IN, "LANG=C $cmd 2>&1");
545 };
546 if (my $error = $@) {
547 $return{'status'} = UNKNOWN;
548 $return{'error'} = 'failed to exec program, error caught';
549 return %return;
550 }
551 if ($pid <= 0) {
552 $return{'status'} = UNKNOWN;
553 $return{'error'} = 'pid error, failed to exec program';
554 return %return;
555 }
556 debug("pid=$pid");
557
558
559
560 ### write into stdin
561 print $PROCESS_IN "$in";
562 close($PROCESS_IN);
563
564
565
566 ### loop on result
567 while (my $t = <$PROCESS_OUT>) {
568 push(@output, $t);
569 }
570 close($PROCESS_OUT);
571
572 waitpid($pid, 0);
573 $return{'returncode'} = $?;
574 $return{'status'} = OK;
575 $return{'error'} = '';
576 $return{'output'} = \@output;
577
578 return %return;
579 }
580
581
582
583 ### print debug information if $param_debug > 0
584 sub debug {
585 print STDERR '[DEBUG] ' . $_[0] . "\n" if (defined($param_debug) && $param_debug == 1);
586 }
587
588
589
590 ### gettext wrapper
591 sub _gt {
592 return gettext($_[0]);
593 }
594
595
596
597 ### uniq array
598 sub uniq {
599 my %seen;
600 return grep { !$seen{$_}++ } @_;
601 }

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC 1.1.8