summaryrefslogtreecommitdiffstats
path: root/library/X509/ProvidedHook
diff options
context:
space:
mode:
Diffstat (limited to 'library/X509/ProvidedHook')
-rw-r--r--library/X509/ProvidedHook/DbMigration.php95
-rw-r--r--library/X509/ProvidedHook/HostsImportSource.php91
-rw-r--r--library/X509/ProvidedHook/ServicesImportSource.php143
-rw-r--r--library/X509/ProvidedHook/X509ImportSource.php11
4 files changed, 340 insertions, 0 deletions
diff --git a/library/X509/ProvidedHook/DbMigration.php b/library/X509/ProvidedHook/DbMigration.php
new file mode 100644
index 0000000..8314e3c
--- /dev/null
+++ b/library/X509/ProvidedHook/DbMigration.php
@@ -0,0 +1,95 @@
+<?php
+
+/* Icinga Web 2 X.509 Module | (c) 2023 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\X509\ProvidedHook;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Module\X509\Common\Database;
+use Icinga\Module\X509\Model\Schema;
+use ipl\Orm\Query;
+use ipl\Sql;
+use ipl\Sql\Adapter\Pgsql;
+
+class DbMigration extends DbMigrationHook
+{
+ public function getName(): string
+ {
+ return $this->translate('Icinga Certificate Monitoring');
+ }
+
+ public function providedDescriptions(): array
+ {
+ return [
+ '1.0.0' => $this->translate(
+ 'Adjusts the database type of several columns and changes some composed primary keys.'
+ ),
+ '1.1.0' => $this->translate(
+ 'Changes the composed x509_target index and x509_certificate valid from/to types to bigint.'
+ ),
+ '1.2.0' => $this->translate(
+ 'Changes all timestamp columns to bigint and adjusts enum types of "yes/no" to "n/y".'
+ ),
+ '1.3.0' => $this->translate(
+ 'Introduces the required tables to store jobs and job schedules in the database.'
+ )
+ ];
+ }
+
+ public function getVersion(): string
+ {
+ if ($this->version === null) {
+ $conn = $this->getDb();
+ $schema = $this->getSchemaQuery()
+ ->columns(['version', 'success'])
+ ->orderBy('id', SORT_DESC)
+ ->limit(2);
+
+ if (static::tableExists($conn, $schema->getModel()->getTableName())) {
+ /** @var Schema $version */
+ foreach ($schema as $version) {
+ if ($version->success) {
+ $this->version = $version->version;
+
+ break;
+ }
+ }
+
+ if (! $this->version) {
+ // Schema version table exist, but the user has probably deleted the entry!
+ $this->version = '1.3.0';
+ }
+ } elseif (
+ $this->getDb()->getAdapter() instanceof Pgsql
+ || static::getColumnType($conn, 'x509_certificate', 'ctime') === 'bigint(20) unsigned'
+ ) {
+ // We modified a bunch of timestamp columns to bigint in x509 version 1.2.0.
+ // We have also added Postgres support with x509 version 1.2 and never had an upgrade scripts until now.
+ $this->version = '1.2.0';
+ } elseif (static::getColumnType($conn, 'x509_certificate_subject_alt_name', 'hash') !== null) {
+ if (static::getColumnType($conn, 'x509_certificate', 'valid_from') === 'bigint(20) unsigned') {
+ $this->version = '1.0.0';
+ } else {
+ $this->version = '1.1.0';
+ }
+ } else {
+ // X509 version 1.0 was the first release of this module, but due to some reason it also contains
+ // an upgrade script and adds `hash` column. However, if this column doesn't exist yet, we need
+ // to use the lowest possible release value as the initial (last migrated) version.
+ $this->version = '0.0.0';
+ }
+ }
+
+ return $this->version;
+ }
+
+ public function getDb(): Sql\Connection
+ {
+ return Database::get();
+ }
+
+ protected function getSchemaQuery(): Query
+ {
+ return Schema::on($this->getDb());
+ }
+}
diff --git a/library/X509/ProvidedHook/HostsImportSource.php b/library/X509/ProvidedHook/HostsImportSource.php
new file mode 100644
index 0000000..70d584c
--- /dev/null
+++ b/library/X509/ProvidedHook/HostsImportSource.php
@@ -0,0 +1,91 @@
+<?php
+
+// Icinga Web 2 X.509 Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\X509\ProvidedHook;
+
+use Icinga\Module\X509\Common\Database;
+use Icinga\Module\X509\Job;
+use Icinga\Module\X509\Model\X509Target;
+use ipl\Sql;
+
+class HostsImportSource extends X509ImportSource
+{
+ public function fetchData()
+ {
+ $conn = Database::get();
+ $targets = X509Target::on($conn)
+ ->utilize('chain')
+ ->utilize('chain.certificate')
+ ->columns([
+ 'ip',
+ 'host_name' => 'hostname'
+ ]);
+
+ $targets
+ ->getSelectBase()
+ ->where(new Sql\Expression('target_chain_link.order = 0'))
+ ->groupBy(['ip', 'hostname']);
+
+ if ($conn->getAdapter() instanceof Sql\Adapter\Pgsql) {
+ $targets->withColumns([
+ 'host_ports' => new Sql\Expression("ARRAY_TO_STRING(ARRAY_AGG(DISTINCT port), ',')")
+ ]);
+ } else {
+ $targets->withColumns([
+ 'host_ports' => new Sql\Expression("GROUP_CONCAT(DISTINCT port SEPARATOR ',')")
+ ]);
+ }
+
+ $results = [];
+ $foundDupes = [];
+ /** @var X509Target $target */
+ foreach ($targets as $target) {
+ $isV6 = Job::isIPV6($target->ip);
+ $target->host_ip = $target->ip;
+ $target->host_address = $isV6 ? null : $target->ip;
+ $target->host_address6 = $isV6 ? $target->ip : null;
+
+ if (isset($foundDupes[$target->host_name])) {
+ // For load balanced systems the IP address is the better choice
+ $target->host_name_or_ip = $target->host_ip;
+ } elseif (! isset($results[$target->host_name])) {
+ // Hostnames are usually preferred, especially in the case of SNI
+ $target->host_name_or_ip = $target->host_name;
+ } else {
+ $dupe = $results[$target->host_name];
+ unset($results[$target->host_name]);
+ $foundDupes[$dupe->host_name] = true;
+ $dupe->host_name_or_ip = $dupe->host_ip;
+ $results[$dupe->host_name_or_ip] = $dupe;
+ $target->host_name_or_ip = $target->host_ip;
+ }
+
+ // Target ip is now obsolete and must not be included in the results.
+ // The relation is only used to utilize the query and must not be in the result set as well.
+ unset($target->ip);
+ unset($target->chain);
+
+ $results[$target->host_name_or_ip] = (object) iterator_to_array($target);
+ }
+
+ return $results;
+ }
+
+ public function listColumns()
+ {
+ return [
+ 'host_name_or_ip',
+ 'host_ip',
+ 'host_name',
+ 'host_ports',
+ 'host_address',
+ 'host_address6'
+ ];
+ }
+
+ public static function getDefaultKeyColumnName()
+ {
+ return 'host_name_or_ip';
+ }
+}
diff --git a/library/X509/ProvidedHook/ServicesImportSource.php b/library/X509/ProvidedHook/ServicesImportSource.php
new file mode 100644
index 0000000..7b87cd8
--- /dev/null
+++ b/library/X509/ProvidedHook/ServicesImportSource.php
@@ -0,0 +1,143 @@
+<?php
+
+// Icinga Web 2 X.509 Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\X509\ProvidedHook;
+
+use Icinga\Module\X509\Common\Database;
+use Icinga\Module\X509\Job;
+use Icinga\Module\X509\Model\X509CertificateSubjectAltName;
+use Icinga\Module\X509\Model\X509Target;
+use ipl\Sql;
+
+class ServicesImportSource extends X509ImportSource
+{
+ public function fetchData()
+ {
+ $conn = Database::get();
+ $targets = X509Target::on($conn)
+ ->with([
+ 'chain',
+ 'chain.certificate',
+ 'chain.certificate.dn',
+ 'chain.certificate.issuer_certificate'
+ ])
+ ->columns([
+ 'ip',
+ 'host_name' => 'hostname',
+ 'host_port' => 'port',
+ 'cert_subject' => 'chain.certificate.subject',
+ 'cert_issuer' => 'chain.certificate.issuer',
+ 'cert_trusted' => 'chain.certificate.trusted',
+ 'cert_valid_from' => 'chain.certificate.valid_from',
+ 'cert_valid_to' => 'chain.certificate.valid_to',
+ 'cert_self_signed' => new Sql\Expression('COALESCE(%s, %s)', [
+ 'chain.certificate.issuer_certificate.self_signed',
+ 'chain.certificate.self_signed'
+ ])
+ ]);
+
+ $targets->getWith()['target.chain.certificate.issuer_certificate']->setJoinType('LEFT');
+ $targets
+ ->getSelectBase()
+ ->where(new Sql\Expression('target_chain_link.order = 0'))
+ ->groupBy(['ip, hostname, port']);
+
+ $certAltName = X509CertificateSubjectAltName::on($conn);
+ $certAltName
+ ->getSelectBase()
+ ->where(new Sql\Expression('certificate_id = target_chain_certificate.id'))
+ ->groupBy(['alt_name.certificate_id']);
+
+ if ($conn->getAdapter() instanceof Sql\Adapter\Pgsql) {
+ $targets
+ ->withColumns([
+ 'cert_fingerprint' => new Sql\Expression("ENCODE(%s, 'hex')", [
+ 'chain.certificate.fingerprint'
+ ]),
+ 'cert_dn' => new Sql\Expression(
+ "ARRAY_TO_STRING(ARRAY_AGG(CONCAT(%s, '=', %s)), ',')",
+ [
+ 'chain.certificate.dn.key',
+ 'chain.certificate.dn.value'
+ ]
+ )
+ ])
+ ->getSelectBase()
+ ->groupBy(['target_chain_certificate.id', 'target_chain_certificate_issuer_certificate.id']);
+
+ $certAltName->columns([
+ new Sql\Expression("ARRAY_TO_STRING(ARRAY_AGG(CONCAT(%s, ':', %s)), ',')", ['type', 'value'])
+ ]);
+ } else {
+ $targets->withColumns([
+ 'cert_fingerprint' => new Sql\Expression('HEX(%s)', ['chain.certificate.fingerprint']),
+ 'cert_dn' => new Sql\Expression(
+ "GROUP_CONCAT(CONCAT(%s, '=', %s) SEPARATOR ',')",
+ [
+ 'chain.certificate.dn.key',
+ 'chain.certificate.dn.value'
+ ]
+ )
+ ]);
+
+ $certAltName->columns([
+ new Sql\Expression("GROUP_CONCAT(CONCAT(%s, ':', %s) SEPARATOR ',')", ['type', 'value'])
+ ]);
+ }
+
+ list($select, $values) = $certAltName->dump();
+ $targets->withColumns(['cert_subject_alt_name' => new Sql\Expression("$select", null, ...$values)]);
+
+ $results = [];
+ /** @var X509Target $target */
+ foreach ($targets as $target) {
+ $isV6 = Job::isIPV6($target->ip);
+ $target->host_ip = $target->ip;
+ $target->host_address = $isV6 ? null : $target->ip;
+ $target->host_address6 = $isV6 ? $target->ip : null;
+
+ $target->host_name_ip_and_port = sprintf(
+ '%s/%s:%d',
+ $target->host_name,
+ $target->host_ip,
+ $target->host_port
+ );
+
+ // Target ip is now obsolete and must not be included in the results.
+ // The relation is only used to utilize the query and must not be in the result set as well.
+ unset($target->ip);
+ unset($target->chain);
+
+ $results[$target->host_name_ip_and_port] = (object) iterator_to_array($target);
+ }
+
+ return $results;
+ }
+
+ public function listColumns()
+ {
+ return [
+ 'host_name_ip_and_port',
+ 'host_ip',
+ 'host_name',
+ 'host_port',
+ 'host_address',
+ 'host_address6',
+ 'cert_subject',
+ 'cert_issuer',
+ 'cert_self_signed',
+ 'cert_trusted',
+ 'cert_valid_from',
+ 'cert_valid_to',
+ 'cert_fingerprint',
+ 'cert_dn',
+ 'cert_subject_alt_name'
+ ];
+ }
+
+ public static function getDefaultKeyColumnName()
+ {
+ return 'host_name_ip_and_port';
+ }
+}
diff --git a/library/X509/ProvidedHook/X509ImportSource.php b/library/X509/ProvidedHook/X509ImportSource.php
new file mode 100644
index 0000000..dc280c0
--- /dev/null
+++ b/library/X509/ProvidedHook/X509ImportSource.php
@@ -0,0 +1,11 @@
+<?php
+
+// Icinga Web 2 X.509 Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\X509\ProvidedHook;
+
+use Icinga\Module\Director\Hook\ImportSourceHook;
+
+abstract class X509ImportSource extends ImportSourceHook
+{
+}