Merge "Add certificates expiration date checker"
diff --git a/admin/certificates-validity-checker-1.0.groovy b/admin/certificates-validity-checker-1.0.groovy
new file mode 100644
index 0000000..841efdf
--- /dev/null
+++ b/admin/certificates-validity-checker-1.0.groovy
@@ -0,0 +1,186 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import com.google.common.base.Strings
+import com.google.common.flogger.FluentLogger
+import com.google.gerrit.extensions.annotations.Listen
+import com.google.gerrit.extensions.annotations.PluginName
+import com.google.gerrit.extensions.events.LifecycleListener
+import com.google.gerrit.metrics.CallbackMetric1
+import com.google.gerrit.metrics.Description
+import com.google.gerrit.metrics.Field
+import com.google.gerrit.metrics.MetricMaker
+import com.google.gerrit.server.config.ConfigUtil
+import com.google.gerrit.server.config.PluginConfigFactory
+import com.google.gerrit.server.git.WorkQueue
+import com.google.gerrit.server.logging.Metadata
+import com.google.inject.Inject
+import com.google.inject.Singleton
+import sun.security.x509.GeneralNameInterface
+
+import javax.net.ssl.SSLSocket
+import javax.net.ssl.SSLSocketFactory
+import java.security.cert.Certificate
+import java.security.cert.X509Certificate
+import java.time.Duration
+import java.util.concurrent.ScheduledFuture
+
+import static java.util.concurrent.TimeUnit.HOURS
+import static java.util.concurrent.TimeUnit.MILLISECONDS
+import static java.util.concurrent.TimeUnit.SECONDS
+
+@Singleton
+@Listen
+class CertificatesValidityChecker implements LifecycleListener {
+ private static final int DEFAULT_CHECK_INTERVAL_HOURS = 24
+ private final WorkQueue queue
+ private final PluginConfigFactory config
+ private final String pluginName
+ private final CertificatesCheckMetrics metrics
+
+ private ScheduledFuture<?> certificatesValidityChecksTask
+ private List<String> endpoints
+ private Long checkIntervalInMillis
+
+ @Inject
+ CertificatesValidityChecker(WorkQueue queue, PluginConfigFactory cfg,
+ CertificatesCheckMetrics metrics,
+ @PluginName String pluginName) {
+ this.metrics = metrics
+ this.queue = queue
+ this.config = cfg
+ this.pluginName = pluginName
+ }
+
+ @Override
+ void start() {
+ endpoints = getEndpointsList(config, pluginName)
+ checkIntervalInMillis = getCheckIntervalMillis(config, pluginName)
+ certificatesValidityChecksTask = queue.getDefaultQueue()
+ .scheduleAtFixedRate(
+ new CheckCertificatesValidityTask(metrics, endpoints),
+ SECONDS.toMillis(1),
+ checkIntervalInMillis,
+ MILLISECONDS)
+ }
+
+ @Override
+ void stop() {
+ if (certificatesValidityChecksTask != null) {
+ certificatesValidityChecksTask.cancel(true)
+ certificatesValidityChecksTask = null
+ }
+ }
+
+ private Long getCheckIntervalMillis(PluginConfigFactory cfg, String pluginName) {
+ String fromConfig =
+ Strings.nullToEmpty(cfg.getGlobalPluginConfig(pluginName).getString("validation",null,"checkInterval"))
+ return HOURS.toMillis(ConfigUtil.getTimeUnit(fromConfig, DEFAULT_CHECK_INTERVAL_HOURS, HOURS))
+ }
+
+ private List<String> getEndpointsList(PluginConfigFactory cfg, String pluginName) {
+ return cfg.getGlobalPluginConfig(pluginName).getStringList("validation",null,"endpoint")
+ }
+
+ private static class CertificatesCheckMetrics {
+ private static final Field<String> ENDPOINT_NAME =
+ Field.ofString("endpoint_name", Metadata.Builder.&cacheName).build()
+ private final CallbackMetric1<String, Integer> metrics
+
+ @Inject
+ CertificatesCheckMetrics(MetricMaker metricMaker) {
+ this.metrics =
+ metricMaker.newCallbackMetric(
+ "certificates/number_of_day_to_expire/per_endpoint",
+ Integer.class,
+ new Description("Per-endpoint certificate expiration date")
+ .setGauge()
+ .setUnit("days"),
+ ENDPOINT_NAME)
+ }
+
+ def setMetric(String endpoint, int numberOfDays) {
+ metrics.set(endpoint, numberOfDays)
+ }
+ }
+
+ private static class CheckCertificatesValidityTask implements Runnable {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass()
+ private final CertificatesCheckMetrics metrics
+ private final List<String> endpoints
+
+ CheckCertificatesValidityTask(CertificatesCheckMetrics metrics, List<String> endpoints) {
+ this.endpoints = endpoints
+ this.metrics = metrics
+ }
+
+ @Override
+ void run() {
+ for (String endpoint : endpoints) {
+ logger.atInfo().log("Checking certificate expiry date for %s endpoint", endpoint)
+ SSLSocket conn
+ try {
+ def (hostname, port) = parseEndpoint(endpoint)
+ conn = openConnection(hostname as String, port as int)
+ conn.startHandshake();
+ Certificate[] certs = conn.getSession().getPeerCertificates();
+ for (Certificate cert : certs) {
+ if (cert instanceof X509Certificate &&
+ cert.getSubjectAlternativeNames().findAll{it[0] == GeneralNameInterface.NAME_DNS}
+ .any {isHostnameMatching(hostname as String, it.get(1) as String) }) {
+ def numberOfDaysToExpire = Duration
+ .between(new Date().toInstant(), cert.notAfter.toInstant()).toDays()
+ metrics
+ .setMetric(
+ hostname as String,
+ numberOfDaysToExpire.intValue())
+ } else {
+ logger.atFine().log("Certificate type %s is not a valid X.509 certificate for the specified endpoint: %s. Skipping!", cert.getType(), endpoint)
+ }
+ }
+ } catch(e) {
+ logger.atSevere()
+ .withCause(e)
+ .log("Cannot check certificates expiry date for %s endpoint", endpoint)
+ } finally {
+ conn?.close()
+ }
+ }
+ }
+
+ def parseEndpoint(String endpoint) {
+ def hostAndPort = endpoint.split(':')
+ if (hostAndPort.size() != 2) {
+ throw new IllegalArgumentException("Wrong endpoint format, expected <host>:<port> but was ${endpoint}")
+ }
+
+ hostAndPort
+ }
+
+ private boolean isHostnameMatching(String hostname, String certName) {
+ // Replace the wildcard (*) with a regex wildcard (.*)
+ def certPattern = certName.replaceFirst("\\*", ".*")
+ return hostname.matches(certPattern)
+ }
+
+ private SSLSocket openConnection(String hostname, int port) {
+ logger
+ .atInfo()
+ .log("Opening connection for %s endpoint successful",
+ hostname)
+ (SSLSocket) SSLSocketFactory.getDefault()
+ .createSocket(hostname, port);
+ }
+ }
+}
diff --git a/admin/certificates-validity-checker.md b/admin/certificates-validity-checker.md
new file mode 100644
index 0000000..1f0b374
--- /dev/null
+++ b/admin/certificates-validity-checker.md
@@ -0,0 +1,46 @@
+Certificates Validity Check utility
+==============================
+
+DESCRIPTION
+-----------
+Check SSL Certificates expiry date and expose them as a Gerrit metrics.
+
+Configuration
+=========================
+
+The certificates-validity-checker plugin is configured in
+$site_path/etc/@PLUGIN@.config` files, example:
+
+```text
+[validation]
+ endpoint = hostname:443
+ endpoint = mail.hostname.com:993
+ checkInterval = 1 day
+```
+
+Configuration parameters
+---------------------
+
+```validation.checkInterval```
+: Frequency of the SSL certificates expiry date check operation
+ Value should use common time unit suffixes to express their setting:
+ * h, hr, hour, hours
+ * d, day, days
+ * w, week, weeks (`1 week` is treated as `7 days`)
+ * mon, month, months (`1 month` is treated as `30 days`)
+ * y, year, years (`1 year` is treated as `365 days`)
+ If a time unit suffix is not specified, `hours` is assumed.
+ Time intervals smaller than one hour are not supported.
+ Default: 24 hours
+
+```validation.endpoint```
+: Specify for which endpoint SSL certificates expiry date should be
+ checked and expose as a Gerrit metric.
+ Endpoint format is <host>:<port>
+ It can be provided more than once.
+
+Metrics
+---------------------
+Currently, the metrics exposed are the following:
+
+```groovy_certificates_validity_checker_certificates_number_of_day_to_expire_per_endpoint_<hostname>```