diff options
Diffstat (limited to 'src/go/plugin/go.d/modules/x509check')
l--------- | src/go/plugin/go.d/modules/x509check/README.md | 1 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/charts.go | 43 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/collect.go | 58 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/config_schema.json | 114 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/init.go | 38 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/integrations/x.509_certificate.md | 260 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/metadata.yaml | 172 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/provider.go | 131 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/testdata/config.json | 12 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/testdata/config.yaml | 10 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/x509check.go | 111 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/x509check/x509check_test.go | 177 |
12 files changed, 1127 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/x509check/README.md b/src/go/plugin/go.d/modules/x509check/README.md new file mode 120000 index 00000000..28978ccf --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/README.md @@ -0,0 +1 @@ +integrations/x.509_certificate.md
\ No newline at end of file diff --git a/src/go/plugin/go.d/modules/x509check/charts.go b/src/go/plugin/go.d/modules/x509check/charts.go new file mode 100644 index 00000000..5105c6d1 --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/charts.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package x509check + +import "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + +var ( + baseCharts = module.Charts{ + timeUntilExpirationChart.Copy(), + } + withRevocationCharts = module.Charts{ + timeUntilExpirationChart.Copy(), + revocationStatusChart.Copy(), + } + + timeUntilExpirationChart = module.Chart{ + ID: "time_until_expiration", + Title: "Time Until Certificate Expiration", + Units: "seconds", + Fam: "expiration time", + Ctx: "x509check.time_until_expiration", + Opts: module.Opts{StoreFirst: true}, + Dims: module.Dims{ + {ID: "expiry"}, + }, + Vars: module.Vars{ + {ID: "days_until_expiration_warning"}, + {ID: "days_until_expiration_critical"}, + }, + } + revocationStatusChart = module.Chart{ + ID: "revocation_status", + Title: "Revocation Status", + Units: "boolean", + Fam: "revocation", + Ctx: "x509check.revocation_status", + Opts: module.Opts{StoreFirst: true}, + Dims: module.Dims{ + {ID: "not_revoked"}, + {ID: "revoked"}, + }, + } +) diff --git a/src/go/plugin/go.d/modules/x509check/collect.go b/src/go/plugin/go.d/modules/x509check/collect.go new file mode 100644 index 00000000..fc98e3a2 --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/collect.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package x509check + +import ( + "crypto/x509" + "fmt" + "time" + + "github.com/cloudflare/cfssl/revoke" +) + +func (x *X509Check) collect() (map[string]int64, error) { + certs, err := x.prov.certificates() + if err != nil { + return nil, err + } + + if len(certs) == 0 { + return nil, fmt.Errorf("no certificate was provided by '%s'", x.Config.Source) + } + + mx := make(map[string]int64) + + x.collectExpiration(mx, certs) + if x.CheckRevocation { + x.collectRevocation(mx, certs) + } + + return mx, nil +} + +func (x *X509Check) collectExpiration(mx map[string]int64, certs []*x509.Certificate) { + expiry := time.Until(certs[0].NotAfter).Seconds() + mx["expiry"] = int64(expiry) + mx["days_until_expiration_warning"] = x.DaysUntilWarn + mx["days_until_expiration_critical"] = x.DaysUntilCritical + +} + +func (x *X509Check) collectRevocation(mx map[string]int64, certs []*x509.Certificate) { + rev, ok, err := revoke.VerifyCertificateError(certs[0]) + if err != nil { + x.Debug(err) + } + if !ok { + return + } + + mx["revoked"] = 0 + mx["not_revoked"] = 0 + + if rev { + mx["revoked"] = 1 + } else { + mx["not_revoked"] = 1 + } +} diff --git a/src/go/plugin/go.d/modules/x509check/config_schema.json b/src/go/plugin/go.d/modules/x509check/config_schema.json new file mode 100644 index 00000000..7246cfa7 --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/config_schema.json @@ -0,0 +1,114 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "X509Check collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "source": { + "title": "Certificate source", + "description": "The source of the certificate. Allowed schemes: https, tcp, tcp4, tcp6, udp, udp4, udp6, file, smtp.", + "type": "string" + }, + "timeout": { + "title": "Timeout", + "description": "The timeout in seconds for the certificate retrieval.", + "type": "number", + "minimum": 0.5, + "default": 1 + }, + "check_revocation_status": { + "title": "Revocation status check", + "description": "Whether to check the revocation status of the certificate.", + "type": "boolean" + }, + "days_until_expiration_warning": { + "title": "Days until warning", + "description": "Number of days before the alarm status is set to warning.", + "type": "integer", + "minimum": 1, + "default": 14 + }, + "days_until_expiration_critical": { + "title": "Days until critical", + "description": "Number of days before the alarm status is set to critical.", + "type": "integer", + "minimum": 1, + "default": 7 + }, + "tls_skip_verify": { + "title": "Skip TLS verification", + "description": "If set, TLS certificate verification will be skipped.", + "type": "boolean" + }, + "tls_ca": { + "title": "TLS CA", + "description": "The path to the CA certificate file for TLS verification.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_cert": { + "title": "TLS certificate", + "description": "The path to the client certificate file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_key": { + "title": "TLS key", + "description": "The path to the client key file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + } + }, + "required": [ + "source" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "uiOptions": { + "fullPage": true + }, + "source": { + "ui:placeholder": "https://example.com:443", + "ui:help": " Website: `https://domainName:443`. Local file: `file:///path/to/cert.pem`. SMTP: `smtp://smtp.example.com:587`." + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + }, + "ui:flavour": "tabs", + "ui:options": { + "tabs": [ + { + "title": "Base", + "fields": [ + "update_every", + "source", + "timeout", + "check_revocation_status", + "days_until_expiration_warning", + "days_until_expiration_critical" + ] + }, + { + "title": "TLS", + "fields": [ + "tls_skip_verify", + "tls_ca", + "tls_cert", + "tls_key" + ] + } + ] + } + } +} diff --git a/src/go/plugin/go.d/modules/x509check/init.go b/src/go/plugin/go.d/modules/x509check/init.go new file mode 100644 index 00000000..8d6dece2 --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/init.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package x509check + +import ( + "errors" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" +) + +func (x *X509Check) validateConfig() error { + if x.Source == "" { + return errors.New("source is not set") + } + return nil +} + +func (x *X509Check) initProvider() (provider, error) { + return newProvider(x.Config) +} + +func (x *X509Check) initCharts() *module.Charts { + var charts *module.Charts + if x.CheckRevocation { + charts = withRevocationCharts.Copy() + } else { + charts = baseCharts.Copy() + } + + for _, chart := range *charts { + chart.Labels = []module.Label{ + {Key: "source", Value: x.Source}, + } + } + + return charts + +} diff --git a/src/go/plugin/go.d/modules/x509check/integrations/x.509_certificate.md b/src/go/plugin/go.d/modules/x509check/integrations/x.509_certificate.md new file mode 100644 index 00000000..ccbe1294 --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/integrations/x.509_certificate.md @@ -0,0 +1,260 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/x509check/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/x509check/metadata.yaml" +sidebar_label: "X.509 certificate" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/Synthetic Checks" +most_popular: False +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# X.509 certificate + + +<img src="https://netdata.cloud/img/lock.svg" width="150"/> + + +Plugin: go.d.plugin +Module: x509check + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + + + +This collectors monitors x509 certificates expiration time and revocation status. + + +This collector is supported on all platforms. + +This collector supports collecting metrics from multiple instances of this integration, including remote instances. + + +### Default Behavior + +#### Auto-Detection + +This integration doesn't support auto-detection. + +#### Limits + +The default configuration for this integration does not impose any limits on data collection. + +#### Performance Impact + +The default configuration for this integration is not expected to impose a significant performance impact on the system. + + +## Metrics + +Metrics grouped by *scope*. + +The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels. + + + +### Per source + +These metrics refer to the configured source. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| source | Configured source. | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| x509check.time_until_expiration | expiry | seconds | +| x509check.revocation_status | not_revoked, revoked | boolean | + + + +## Alerts + + +The following alerts are available: + +| Alert name | On metric | Description | +|:------------|:----------|:------------| +| [ x509check_days_until_expiration ](https://github.com/netdata/netdata/blob/master/src/health/health.d/x509check.conf) | x509check.time_until_expiration | Time until x509 certificate expires for ${label:source} | +| [ x509check_revocation_status ](https://github.com/netdata/netdata/blob/master/src/health/health.d/x509check.conf) | x509check.revocation_status | x509 certificate revocation status for ${label:source} | + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/x509check.conf`. + + +You can edit the configuration file using the `edit-config` script from the +Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory). + +```bash +cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata +sudo ./edit-config go.d/x509check.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details open><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 1 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| source | Certificate source. Allowed schemes: https, tcp, tcp4, tcp6, udp, udp4, udp6, file, smtp. | | no | +| days_until_expiration_warning | Number of days before the alarm status is warning. | 30 | no | +| days_until_expiration_critical | Number of days before the alarm status is critical. | 15 | no | +| check_revocation_status | Whether to check the revocation status of the certificate. | no | no | +| timeout | SSL connection timeout. | 2 | no | +| tls_skip_verify | Server certificate chain and hostname validation policy. Controls whether the client performs this check. | no | no | +| tls_ca | Certification authority that the client uses when verifying the server's certificates. | | no | +| tls_cert | Client TLS certificate. | | no | +| tls_key | Client TLS key. | | no | + +</details> + +#### Examples + +##### Website certificate + +Website certificate. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: my_site_cert + source: https://my_site.org:443 + +``` +</details> + +##### Local file certificate + +Local file certificate. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: my_file_cert + source: file:///home/me/cert.pem + +``` +</details> + +##### SMTP certificate + +SMTP certificate. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: my_smtp_cert + source: smtp://smtp.my_mail.org:587 + +``` +</details> + +##### Multi-instance + +> **Note**: When you define more than one job, their names must be unique. + +Check the expiration status of the multiple websites' certificates. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: my_site_cert1 + source: https://my_site1.org:443 + + - name: my_site_cert2 + source: https://my_site1.org:443 + + - name: my_site_cert3 + source: https://my_site3.org:443 + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +**Important**: Debug mode is not supported for data collection jobs created via the UI using the Dyncfg feature. + +To troubleshoot issues with the `x509check` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on + your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`. + + ```bash + cd /usr/libexec/netdata/plugins.d/ + ``` + +- Switch to the `netdata` user. + + ```bash + sudo -u netdata -s + ``` + +- Run the `go.d.plugin` to debug the collector: + + ```bash + ./go.d.plugin -d -m x509check + ``` + +### Getting Logs + +If you're encountering problems with the `x509check` collector, follow these steps to retrieve logs and identify potential issues: + +- **Run the command** specific to your system (systemd, non-systemd, or Docker container). +- **Examine the output** for any warnings or error messages that might indicate issues. These messages should provide clues about the root cause of the problem. + +#### System with systemd + +Use the following command to view logs generated since the last Netdata service restart: + +```bash +journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep x509check +``` + +#### System without systemd + +Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name: + +```bash +grep x509check /var/log/netdata/collector.log +``` + +**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues. + +#### Docker Container + +If your Netdata runs in a Docker container named "netdata" (replace if different), use this command: + +```bash +docker logs netdata 2>&1 | grep x509check +``` + + diff --git a/src/go/plugin/go.d/modules/x509check/metadata.yaml b/src/go/plugin/go.d/modules/x509check/metadata.yaml new file mode 100644 index 00000000..e373f33d --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/metadata.yaml @@ -0,0 +1,172 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-x509check + plugin_name: go.d.plugin + module_name: x509check + monitored_instance: + name: X.509 certificate + link: "" + categories: + - data-collection.synthetic-checks + icon_filename: lock.svg + keywords: + - x509 + - certificate + most_popular: false + info_provided_to_referring_integrations: + description: "" + related_resources: + integrations: + list: [] + overview: + data_collection: + metrics_description: "" + method_description: | + This collectors monitors x509 certificates expiration time and revocation status. + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + additional_permissions: + description: "" + multi_instance: true + supported_platforms: + include: [] + exclude: [] + setup: + prerequisites: + list: [] + configuration: + file: + name: "go.d/x509check.conf" + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 1 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: source + description: "Certificate source. Allowed schemes: https, tcp, tcp4, tcp6, udp, udp4, udp6, file, smtp." + default_value: "" + required: false + - name: days_until_expiration_warning + description: Number of days before the alarm status is warning. + default_value: 30 + required: false + - name: days_until_expiration_critical + description: Number of days before the alarm status is critical. + default_value: 15 + required: false + - name: check_revocation_status + description: Whether to check the revocation status of the certificate. + default_value: false + required: false + - name: timeout + description: SSL connection timeout. + default_value: 2 + required: false + - name: tls_skip_verify + description: Server certificate chain and hostname validation policy. Controls whether the client performs this check. + default_value: false + required: false + - name: tls_ca + description: Certification authority that the client uses when verifying the server's certificates. + default_value: "" + required: false + - name: tls_cert + description: Client TLS certificate. + default_value: "" + required: false + - name: tls_key + description: Client TLS key. + default_value: "" + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Website certificate + description: Website certificate. + config: | + jobs: + - name: my_site_cert + source: https://my_site.org:443 + - name: Local file certificate + description: Local file certificate. + config: | + jobs: + - name: my_file_cert + source: file:///home/me/cert.pem + - name: SMTP certificate + description: SMTP certificate. + config: | + jobs: + - name: my_smtp_cert + source: smtp://smtp.my_mail.org:587 + - name: Multi-instance + description: | + > **Note**: When you define more than one job, their names must be unique. + + Check the expiration status of the multiple websites' certificates. + config: | + jobs: + - name: my_site_cert1 + source: https://my_site1.org:443 + + - name: my_site_cert2 + source: https://my_site1.org:443 + + - name: my_site_cert3 + source: https://my_site3.org:443 + troubleshooting: + problems: + list: [] + alerts: + - name: x509check_days_until_expiration + metric: x509check.time_until_expiration + info: "Time until x509 certificate expires for ${label:source}" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/x509check.conf + - name: x509check_revocation_status + metric: x509check.revocation_status + info: "x509 certificate revocation status for ${label:source}" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/x509check.conf + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: source + description: These metrics refer to the configured source. + labels: + - name: source + description: Configured source. + metrics: + - name: x509check.time_until_expiration + description: Time Until Certificate Expiration + unit: seconds + chart_type: line + dimensions: + - name: expiry + - name: x509check.revocation_status + description: Revocation Status + unit: boolean + chart_type: line + dimensions: + - name: not_revoked + - name: revoked diff --git a/src/go/plugin/go.d/modules/x509check/provider.go b/src/go/plugin/go.d/modules/x509check/provider.go new file mode 100644 index 00000000..4a063570 --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/provider.go @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package x509check + +import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "net" + "net/smtp" + "net/url" + "os" + "time" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/tlscfg" +) + +type provider interface { + certificates() ([]*x509.Certificate, error) +} + +type fromFile struct { + path string +} + +type fromNet struct { + url *url.URL + tlsConfig *tls.Config + timeout time.Duration +} + +type fromSMTP struct { + url *url.URL + tlsConfig *tls.Config + timeout time.Duration +} + +func newProvider(config Config) (provider, error) { + sourceURL, err := url.Parse(config.Source) + if err != nil { + return nil, fmt.Errorf("source parse: %v", err) + } + + tlsCfg, err := tlscfg.NewTLSConfig(config.TLSConfig) + if err != nil { + return nil, fmt.Errorf("create tls config: %v", err) + } + + if tlsCfg == nil { + tlsCfg = &tls.Config{} + } + tlsCfg.ServerName = sourceURL.Hostname() + + switch sourceURL.Scheme { + case "file": + return &fromFile{path: sourceURL.Path}, nil + case "https", "udp", "udp4", "udp6", "tcp", "tcp4", "tcp6": + if sourceURL.Scheme == "https" { + sourceURL.Scheme = "tcp" + } + return &fromNet{url: sourceURL, tlsConfig: tlsCfg, timeout: config.Timeout.Duration()}, nil + case "smtp": + sourceURL.Scheme = "tcp" + return &fromSMTP{url: sourceURL, tlsConfig: tlsCfg, timeout: config.Timeout.Duration()}, nil + default: + return nil, fmt.Errorf("unsupported scheme '%s'", sourceURL) + } +} + +func (f fromFile) certificates() ([]*x509.Certificate, error) { + content, err := os.ReadFile(f.path) + if err != nil { + return nil, fmt.Errorf("error on reading '%s': %v", f.path, err) + } + + block, _ := pem.Decode(content) + if block == nil { + return nil, fmt.Errorf("error on decoding '%s': %v", f.path, err) + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("error on parsing certificate '%s': %v", f.path, err) + } + + return []*x509.Certificate{cert}, nil +} + +func (f fromNet) certificates() ([]*x509.Certificate, error) { + ipConn, err := net.DialTimeout(f.url.Scheme, f.url.Host, f.timeout) + if err != nil { + return nil, fmt.Errorf("error on dial to '%s': %v", f.url, err) + } + defer func() { _ = ipConn.Close() }() + + conn := tls.Client(ipConn, f.tlsConfig.Clone()) + defer func() { _ = conn.Close() }() + if err := conn.Handshake(); err != nil { + return nil, fmt.Errorf("error on SSL handshake with '%s': %v", f.url, err) + } + + certs := conn.ConnectionState().PeerCertificates + return certs, nil +} + +func (f fromSMTP) certificates() ([]*x509.Certificate, error) { + ipConn, err := net.DialTimeout(f.url.Scheme, f.url.Host, f.timeout) + if err != nil { + return nil, fmt.Errorf("error on dial to '%s': %v", f.url, err) + } + defer func() { _ = ipConn.Close() }() + + host, _, _ := net.SplitHostPort(f.url.Host) + smtpClient, err := smtp.NewClient(ipConn, host) + if err != nil { + return nil, fmt.Errorf("error on creating SMTP client: %v", err) + } + defer func() { _ = smtpClient.Quit() }() + + err = smtpClient.StartTLS(f.tlsConfig.Clone()) + if err != nil { + return nil, fmt.Errorf("error on startTLS with '%s': %v", f.url, err) + } + + conn, ok := smtpClient.TLSConnectionState() + if !ok { + return nil, fmt.Errorf("startTLS didn't succeed") + } + return conn.PeerCertificates, nil +} diff --git a/src/go/plugin/go.d/modules/x509check/testdata/config.json b/src/go/plugin/go.d/modules/x509check/testdata/config.json new file mode 100644 index 00000000..9bb2dade --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/testdata/config.json @@ -0,0 +1,12 @@ +{ + "update_every": 123, + "source": "ok", + "timeout": 123.123, + "tls_ca": "ok", + "tls_cert": "ok", + "tls_key": "ok", + "tls_skip_verify": true, + "days_until_expiration_warning": 123, + "days_until_expiration_critical": 123, + "check_revocation_status": true +} diff --git a/src/go/plugin/go.d/modules/x509check/testdata/config.yaml b/src/go/plugin/go.d/modules/x509check/testdata/config.yaml new file mode 100644 index 00000000..e1f273f5 --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/testdata/config.yaml @@ -0,0 +1,10 @@ +update_every: 123 +source: "ok" +timeout: 123.123 +tls_ca: "ok" +tls_cert: "ok" +tls_key: "ok" +tls_skip_verify: yes +days_until_expiration_warning: 123 +days_until_expiration_critical: 123 +check_revocation_status: yes diff --git a/src/go/plugin/go.d/modules/x509check/x509check.go b/src/go/plugin/go.d/modules/x509check/x509check.go new file mode 100644 index 00000000..c4fa70ea --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/x509check.go @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package x509check + +import ( + _ "embed" + "errors" + "time" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/tlscfg" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" + + cfssllog "github.com/cloudflare/cfssl/log" + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + cfssllog.Level = cfssllog.LevelFatal + module.Register("x509check", module.Creator{ + JobConfigSchema: configSchema, + Defaults: module.Defaults{ + UpdateEvery: 60, + }, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *X509Check { + return &X509Check{ + Config: Config{ + Timeout: web.Duration(time.Second * 2), + DaysUntilWarn: 14, + DaysUntilCritical: 7, + }, + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + Source string `yaml:"source" json:"source"` + Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"` + DaysUntilWarn int64 `yaml:"days_until_expiration_warning,omitempty" json:"days_until_expiration_warning"` + DaysUntilCritical int64 `yaml:"days_until_expiration_critical,omitempty" json:"days_until_expiration_critical"` + CheckRevocation bool `yaml:"check_revocation_status" json:"check_revocation_status"` + tlscfg.TLSConfig `yaml:",inline" json:""` +} + +type X509Check struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + prov provider +} + +func (x *X509Check) Configuration() any { + return x.Config +} + +func (x *X509Check) Init() error { + if err := x.validateConfig(); err != nil { + x.Errorf("config validation: %v", err) + return err + } + + prov, err := x.initProvider() + if err != nil { + x.Errorf("certificate provider init: %v", err) + return err + } + x.prov = prov + + x.charts = x.initCharts() + + return nil +} + +func (x *X509Check) Check() error { + mx, err := x.collect() + if err != nil { + x.Error(err) + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + } + return nil +} + +func (x *X509Check) Charts() *module.Charts { + return x.charts +} + +func (x *X509Check) Collect() map[string]int64 { + mx, err := x.collect() + if err != nil { + x.Error(err) + } + + if len(mx) == 0 { + return nil + } + return mx +} + +func (x *X509Check) Cleanup() {} diff --git a/src/go/plugin/go.d/modules/x509check/x509check_test.go b/src/go/plugin/go.d/modules/x509check/x509check_test.go new file mode 100644 index 00000000..e0b28725 --- /dev/null +++ b/src/go/plugin/go.d/modules/x509check/x509check_test.go @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package x509check + +import ( + "crypto/x509" + "errors" + "os" + "testing" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/tlscfg" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + } { + assert.NotNil(t, data, name) + } +} + +func TestX509Check_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &X509Check{}, dataConfigJSON, dataConfigYAML) +} + +func TestX509Check_Cleanup(t *testing.T) { + assert.NotPanics(t, New().Cleanup) +} + +func TestX509Check_Charts(t *testing.T) { + x509Check := New() + x509Check.Source = "https://example.com" + require.NoError(t, x509Check.Init()) + assert.NotNil(t, x509Check.Charts()) +} + +func TestX509Check_Init(t *testing.T) { + const ( + file = iota + net + smtp + ) + tests := map[string]struct { + config Config + providerType int + err bool + }{ + "ok from net https": { + config: Config{Source: "https://example.org"}, + providerType: net, + }, + "ok from net tcp": { + config: Config{Source: "tcp://example.org"}, + providerType: net, + }, + "ok from file": { + config: Config{Source: "file:///home/me/cert.pem"}, + providerType: file, + }, + "ok from smtp": { + config: Config{Source: "smtp://smtp.my_mail.org:587"}, + providerType: smtp, + }, + "empty source": { + config: Config{Source: ""}, + err: true}, + "unknown provider": { + config: Config{Source: "http://example.org"}, + err: true, + }, + "nonexistent TLSCA": { + config: Config{Source: "https://example.org", TLSConfig: tlscfg.TLSConfig{TLSCA: "testdata/tls"}}, + err: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + x509Check := New() + x509Check.Config = test.config + + if test.err { + assert.Error(t, x509Check.Init()) + } else { + require.NoError(t, x509Check.Init()) + + var typeOK bool + switch test.providerType { + case file: + _, typeOK = x509Check.prov.(*fromFile) + case net: + _, typeOK = x509Check.prov.(*fromNet) + case smtp: + _, typeOK = x509Check.prov.(*fromSMTP) + } + + assert.True(t, typeOK) + } + }) + } +} + +func TestX509Check_Check(t *testing.T) { + x509Check := New() + x509Check.prov = &mockProvider{certs: []*x509.Certificate{{}}} + + assert.NoError(t, x509Check.Check()) +} + +func TestX509Check_Check_ReturnsFalseOnProviderError(t *testing.T) { + x509Check := New() + x509Check.prov = &mockProvider{err: true} + + assert.Error(t, x509Check.Check()) +} + +func TestX509Check_Collect(t *testing.T) { + x509Check := New() + x509Check.Source = "https://example.com" + require.NoError(t, x509Check.Init()) + x509Check.prov = &mockProvider{certs: []*x509.Certificate{{}}} + + collected := x509Check.Collect() + + assert.NotZero(t, collected) + ensureCollectedHasAllChartsDimsVarsIDs(t, x509Check, collected) +} + +func TestX509Check_Collect_ReturnsNilOnProviderError(t *testing.T) { + x509Check := New() + x509Check.prov = &mockProvider{err: true} + + assert.Nil(t, x509Check.Collect()) +} + +func TestX509Check_Collect_ReturnsNilOnZeroCertificates(t *testing.T) { + x509Check := New() + x509Check.prov = &mockProvider{certs: []*x509.Certificate{}} + mx := x509Check.Collect() + + assert.Nil(t, mx) +} + +func ensureCollectedHasAllChartsDimsVarsIDs(t *testing.T, x509Check *X509Check, collected map[string]int64) { + for _, chart := range *x509Check.Charts() { + for _, dim := range chart.Dims { + _, ok := collected[dim.ID] + assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID) + } + for _, v := range chart.Vars { + _, ok := collected[v.ID] + assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID) + } + } +} + +type mockProvider struct { + certs []*x509.Certificate + err bool +} + +func (m mockProvider) certificates() ([]*x509.Certificate, error) { + if m.err { + return nil, errors.New("mock certificates error") + } + return m.certs, nil +} |