diff options
Diffstat (limited to 'application/clicommands/ScanCommand.php')
-rw-r--r-- | application/clicommands/ScanCommand.php | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/application/clicommands/ScanCommand.php b/application/clicommands/ScanCommand.php new file mode 100644 index 0000000..3743adc --- /dev/null +++ b/application/clicommands/ScanCommand.php @@ -0,0 +1,163 @@ +<?php + +// Icinga Web 2 X.509 Module | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\X509\Clicommands; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Module\X509\CertificateUtils; +use Icinga\Module\X509\Command; +use Icinga\Module\X509\Common\Database; +use Icinga\Module\X509\Common\JobUtils; +use Icinga\Module\X509\Hook\SniHook; +use Icinga\Module\X509\Job; +use Icinga\Module\X509\Model\X509Job; +use ipl\Stdlib\Filter; +use React\EventLoop\Loop; +use Throwable; + +class ScanCommand extends Command +{ + use JobUtils; + + /** + * Scan targets to find their X.509 certificates and track changes to them. + * + * A target is an IP-port combination that is generated from the job configuration, taking into account + * configured SNI maps, so that targets with multiple certificates are also properly scanned. + * + * By default, successive calls to the scan command perform partial scans, checking both targets not yet scanned + * and targets whose scan is older than 24 hours, to ensure that all targets are rescanned over time and new + * certificates are collected. This behavior can be customized through the command options. + * + * Note that when rescanning due targets, they will be rescanned regardless of whether the target previously + * provided a certificate or not, to collect new certificates, track changed certificates, and remove + * decommissioned certificates. + * + * USAGE + * + * icingacli x509 scan --job <name> [OPTIONS] + * + * OPTIONS + * + * --job=<name> + * Scan targets that belong to the specified job. + * + * --since-last-scan=<datetime> + * Scan targets whose last scan is older than the specified date/time, + * which can also be an English textual datetime description like "2 days". + * Defaults to "-24 hours". + * + * --parallel=<number> + * Allow parallel scanning of targets up to the specified number. Defaults to 256. + * May cause **too many open files** error if set to a number higher than the configured one (ulimit). + * + * --rescan + * Rescan only targets that have been scanned before. + * + * --full + * (Re)scan all known and unknown targets. + * This will override the "rescan" and "since-last-scan" options. + * + * EXAMPLES + * + * Scan all targets that have not yet been scanned, or whose last scan is older than a certain date/time: + * + * icingacli x509 scan --job <name> --since-last-scan="3 days" + * + * Scan only unknown targets + * + * icingacli x509 scan --job <name> --since-last-scan=null + * + * Scan only known targets + * + * icingacli x509 scan --job <name> --rescan + * + * Scan only known targets whose last scan is older than a certain date/time: + * + * icingacli x509 scan --job <name> --rescan --since-last-scan="5 days" + * + * Scan all known and unknown targets: + * + * icingacli x509 scan --job <name> --full + */ + public function indexAction(): void + { + /** @var string $name */ + $name = $this->params->shiftRequired('job'); + $fullScan = (bool) $this->params->get('full', false); + $rescan = (bool) $this->params->get('rescan', false); + + /** @var string $sinceLastScan */ + $sinceLastScan = $this->params->get('since-last-scan', Job::DEFAULT_SINCE_LAST_SCAN); + if ($sinceLastScan === 'null') { + $sinceLastScan = null; + } + + /** @var int $parallel */ + $parallel = $this->params->get('parallel', Job::DEFAULT_PARALLEL); + if ($parallel <= 0) { + throw new Exception('The \'parallel\' option must be set to at least 1'); + } + + /** @var X509Job $jobConfig */ + $jobConfig = X509Job::on(Database::get()) + ->filter(Filter::equal('name', $name)) + ->first(); + if ($jobConfig === null) { + throw new Exception(sprintf('Job %s not found', $name)); + } + + if (! strlen($jobConfig->cidrs)) { + throw new Exception(sprintf('The job %s does not specify any CIDRs', $name)); + } + + $cidrs = $this->parseCIDRs($jobConfig->cidrs); + $ports = $this->parsePorts($jobConfig->ports); + $job = (new Job($name, $cidrs, $ports, SniHook::getAll())) + ->setId($jobConfig->id) + ->setFullScan($fullScan) + ->setRescan($rescan) + ->setParallel($parallel) + ->setExcludes($this->parseExcludes($jobConfig->exclude_targets)) + ->setLastScan($sinceLastScan); + + $promise = $job->run(); + $signalHandler = function () use (&$promise, $job) { + $promise->cancel(); + + Logger::info('Job %s canceled', $job->getName()); + + Loop::futureTick(function () { + Loop::stop(); + }); + }; + Loop::addSignal(SIGINT, $signalHandler); + Loop::addSignal(SIGTERM, $signalHandler); + + $promise->then(function ($targets = 0) use ($job) { + if ($targets === 0) { + Logger::warning('The job %s does not have any targets', $job->getName()); + } else { + Logger::info('Scanned %d target(s) from job %s', $targets, $job->getName()); + + try { + $verified = CertificateUtils::verifyCertificates(Database::get()); + + Logger::info('Checked %d certificate chain(s)', $verified); + } catch (Exception $err) { + Logger::error($err->getMessage()); + Logger::debug($err->getTraceAsString()); + } + } + }, function (Throwable $err) use ($job) { + Logger::error('Failed to run job %s: %s', $job->getName(), $err->getMessage()); + Logger::debug($err->getTraceAsString()); + })->always(function () { + Loop::futureTick(function () { + Loop::stop(); + }); + }); + } +} |