diff options
Diffstat (limited to 'application/clicommands/CheckCommand.php')
-rw-r--r-- | application/clicommands/CheckCommand.php | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/application/clicommands/CheckCommand.php b/application/clicommands/CheckCommand.php new file mode 100644 index 0000000..ae7f641 --- /dev/null +++ b/application/clicommands/CheckCommand.php @@ -0,0 +1,236 @@ +<?php +// Icinga Web 2 X.509 Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\X509\Clicommands; + +use Icinga\Application\Logger; +use Icinga\Module\X509\Command; +use Icinga\Module\X509\Job; +use ipl\Sql\Select; + +class CheckCommand extends Command +{ + const UNIT_PERCENT = 'percent'; + const UNIT_INTERVAL = 'interval'; + + /** + * Check a host's certificate + * + * This command utilizes this module's database to check if the given host serves valid certificates. + * + * USAGE + * + * icingacli x509 check host [options] + * + * OPTIONS + * + * You can either pass --ip or --host or both at the same time but at least one is mandatory. + * + * --ip A hosts IP address + * --host A hosts name + * --port The port to check in particular + * --warning Less remaining time results in state WARNING + * Default: 25% + * --critical Less remaining time results in state CRITICAL + * Default: 10% + * --allow-self-signed Ignore if a certificate or its issuer has been + * self-signed + * + * EXAMPLES + * + * icingacli x509 check host --ip 10.0.10.78 + * icingacli x509 check host --host mail.example.org + * icingacli x509 check host --ip 10.0.10.78 --host mail.example.org --port 993 + * + * THRESHOLD DEFINITION + * + * Thresholds can either be defined relative (in percent) or absolute + * (time interval). Time intervals consist of a digit and an accompanying + * unit (e.g. "3M" are three months). Supported units are: + * + * Year: y, Y + * Month: M + * Day: d, D + * Hour: h, H + * Minute: m + * Second: s, S + */ + public function hostAction() + { + $ip = $this->params->get('ip'); + $hostname = $this->params->get('host'); + if ($ip === null && $hostname === null) { + $this->showUsage('host'); + exit(3); + } + + $targets = (new Select()) + ->from('x509_target t') + ->columns([ + 't.port', + 'cc.valid', + 'cc.invalid_reason', + 'c.subject', + 'self_signed' => 'COALESCE(ci.self_signed, c.self_signed)', + 'c.valid_from', + 'c.valid_to' + ]) + ->join('x509_certificate_chain cc', 'cc.id = t.latest_certificate_chain_id') + ->join('x509_certificate_chain_link ccl', 'ccl.certificate_chain_id = cc.id') + ->join('x509_certificate c', 'c.id = ccl.certificate_id') + ->joinLeft('x509_certificate ci', 'ci.subject_hash = c.issuer_hash') + ->where(['ccl.order = ?' => 0]); + + if ($ip !== null) { + $targets->where(['t.ip = ?' => Job::binary($ip)]); + } + if ($hostname !== null) { + $targets->where(['t.hostname = ?' => $hostname]); + } + if ($this->params->has('port')) { + $targets->where(['t.port = ?' => $this->params->get('port')]); + } + + $allowSelfSigned = (bool) $this->params->get('allow-self-signed', false); + list($warningThreshold, $warningUnit) = $this->splitThreshold($this->params->get('warning', '25%')); + list($criticalThreshold, $criticalUnit) = $this->splitThreshold($this->params->get('critical', '10%')); + + $output = []; + $perfData = []; + + $state = 3; + foreach ($this->getDb()->select($targets) as $target) { + if ($target['valid'] === 'no' && ($target['self_signed'] === 'no' || ! $allowSelfSigned)) { + $invalidMessage = $target['subject'] . ': ' . $target['invalid_reason']; + $output[$invalidMessage] = $invalidMessage; + $state = 2; + } + + $now = new \DateTime(); + $validFrom = (new \DateTime())->setTimestamp($target['valid_from']); + $validTo = (new \DateTime())->setTimestamp($target['valid_to']); + $criticalAfter = $this->thresholdToDateTime($validFrom, $validTo, $criticalThreshold, $criticalUnit); + $warningAfter = $this->thresholdToDateTime($validFrom, $validTo, $warningThreshold, $warningUnit); + + if ($now > $criticalAfter) { + $state = 2; + } elseif ($state !== 2 && $now > $warningAfter) { + $state = 1; + } elseif ($state === 3) { + $state = 0; + } + + $remainingTime = $now->diff($validTo); + if (! $remainingTime->invert) { + // The certificate has not expired yet + $output[$target['subject']] = sprintf( + '%s expires in %d days', + $target['subject'], + $remainingTime->days + ); + } else { + $output[$target['subject']] = sprintf( + '%s has expired since %d days', + $target['subject'], + $remainingTime->days + ); + } + + $perfData[$target['subject']] = sprintf( + "'%s'=%ds;%d;%d;0;%d", + $target['subject'], + $remainingTime->invert + ? 0 + : $target['valid_to'] - time(), + $target['valid_to'] - $warningAfter->getTimestamp(), + $target['valid_to'] - $criticalAfter->getTimestamp(), + $target['valid_to'] - $target['valid_from'] + ); + } + + echo ['OK', 'WARNING', 'CRITICAL', 'UNKNOWN'][$state]; + echo ' - '; + + if (! empty($output)) { + echo join('; ', $output); + } elseif ($state === 3) { + echo 'Host not found'; + } + + if (! empty($perfData)) { + echo '|' . join(' ', $perfData); + } + + echo PHP_EOL; + exit($state); + } + + /** + * Parse the given threshold definition + * + * @param string $threshold + * + * @return array + */ + protected function splitThreshold($threshold) + { + $match = preg_match('/(\d+)([%\w]{1})/', $threshold, $matches); + if (! $match) { + Logger::error('Invalid threshold definition: %s', $threshold); + exit(3); + } + + switch ($matches[2]) { + case '%': + return [(int) $matches[1], self::UNIT_PERCENT]; + case 'y': + case 'Y': + $intervalSpec = 'P' . $matches[1] . 'Y'; + break; + case 'M': + $intervalSpec = 'P' . $matches[1] . 'M'; + break; + case 'd': + case 'D': + $intervalSpec = 'P' . $matches[1] . 'D'; + break; + case 'h': + case 'H': + $intervalSpec = 'PT' . $matches[1] . 'H'; + break; + case 'm': + $intervalSpec = 'PT' . $matches[1] . 'M'; + break; + case 's': + case 'S': + $intervalSpec = 'PT' . $matches[1] . 'S'; + break; + default: + Logger::error('Unknown threshold unit given: %s', $threshold); + exit(3); + } + + return [new \DateInterval($intervalSpec), self::UNIT_INTERVAL]; + } + + /** + * Convert the given threshold information to a DateTime object + * + * @param \DateTime $from + * @param \DateTime $to + * @param int|\DateInterval $thresholdValue + * @param string $thresholdUnit + * + * @return \DateTime + */ + protected function thresholdToDateTime(\DateTime $from, \DateTime $to, $thresholdValue, $thresholdUnit) + { + $to = clone $to; + if ($thresholdUnit === self::UNIT_INTERVAL) { + return $to->sub($thresholdValue); + } + + $coveredDays = (int) round($from->diff($to)->days * ($thresholdValue / 100)); + return $to->sub(new \DateInterval('P' . $coveredDays . 'D')); + } +} |