From fcb76b4a3ac3032903ae5048ff119d42198cff24 Mon Sep 17 00:00:00 2001 From: Michael Cortese <46465065+cortesem@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:36:48 -0500 Subject: [PATCH] add ssl certificate metrics (#130) Co-authored-by: Peiman Jafari <18074432+peimanja@users.noreply.github.com> --- README.md | 1 + artifactory/security.go | 40 +++++++++++++++++++++++++++++++++-- collector/collector.go | 11 ++++++++-- collector/security.go | 46 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cf9d432..d18254b 100755 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ Some metrics are not available with Artifactory OSS license. The exporter return | artifactory_exporter_total_api_errors | Current total Artifactory API errors when scraping for stats. | | ✅ | | artifactory_exporter_json_parse_failures |Number of errors while parsing Json. | | ✅ | | artifactory_replication_enabled | Replication status for an Artifactory repository (1 = enabled). | `name`, `type`, `cron_exp`, `status` | | +| artifactory_security_certificates | SSL certificate name and expiry as labels, seconds to expiration as value | `alias`, `expires`, `issued_by` | | | artifactory_security_groups | Number of Artifactory groups. | | | | artifactory_security_users | Number of Artifactory users for each realm. | `realm` | | | artifactory_storage_artifacts | Total artifacts count stored in Artifactory. | | ✅ | diff --git a/artifactory/security.go b/artifactory/security.go index 5adef03..7a8753e 100644 --- a/artifactory/security.go +++ b/artifactory/security.go @@ -5,8 +5,9 @@ import ( ) const ( - usersEndpoint = "security/users" - groupsEndpoint = "security/groups" + usersEndpoint = "security/users" + groupsEndpoint = "security/groups" + certificatesEndpoint = "system/security/certificates" ) // User represents single element of API respond from users endpoint @@ -67,3 +68,38 @@ func (c *Client) FetchGroups() (Groups, error) { return groups, nil } + +// Certificate represents a single element of an API response from the certificates endpoint +type Certificate struct { + CertificateAlias string `json:"certificateAlias"` + IssuedTo string `json:"issuedTo"` + IssuedBy string `json:"issuedBy"` + IssuedOn string `json:"issuedOn"` + ValidUntil string `json:"validUntil"` + Fingerprint string `json:"fingerprint"` +} + +type Certificates struct { + Certificates []Certificate + NodeId string +} + +// FetchCertificates makes the API call to the certificates endpoint and returns []Certificates +func (c *Client) FetchCertificates() (Certificates, error) { + var certs Certificates + c.logger.Debug("Fetching certificate stats") + resp, err := c.FetchHTTP(certificatesEndpoint) + if err != nil { + return certs, err + } + certs.NodeId = resp.NodeId + if err := json.Unmarshal(resp.Body, &certs.Certificates); err != nil { + c.logger.Error("There was an issue when try to unmarshal certificates response") + return certs, &UnmarshalError{ + message: err.Error(), + endpoint: certificatesEndpoint, + } + } + + return certs, nil +} diff --git a/collector/collector.go b/collector/collector.go index 086cd33..9d55ee4 100755 --- a/collector/collector.go +++ b/collector/collector.go @@ -17,6 +17,7 @@ var ( repoLabelNames = append([]string{"name", "type", "package_type"}, defaultLabelNames...) replicationLabelNames = append([]string{"name", "type", "url", "cron_exp", "status"}, defaultLabelNames...) federationLabelNames = append([]string{"name", "remote_url", "remote_name"}, defaultLabelNames...) + certificateLabelNames = append([]string{"alias", "issued_by", "expires"}, defaultLabelNames...) ) func newMetric(metricName string, subsystem string, docString string, labelNames []string) *prometheus.Desc { @@ -31,8 +32,9 @@ var ( } securityMetrics = metrics{ - "users": newMetric("users", "security", "Number of Artifactory users for each realm.", append([]string{"realm"}, defaultLabelNames...)), - "groups": newMetric("groups", "security", "Number of Artifactory groups", defaultLabelNames), + "users": newMetric("users", "security", "Number of Artifactory users for each realm.", append([]string{"realm"}, defaultLabelNames...)), + "groups": newMetric("groups", "security", "Number of Artifactory groups", defaultLabelNames), + "certificates": newMetric("certificates", "security", "Internal SSL certificate information, seconds to expiration as value", certificateLabelNames), } storageMetrics = metrics{ @@ -154,6 +156,11 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) (up float64) { if err != nil { return 0 } + case "certificates": + err := e.exportCertificates(metricName, metric, ch) + if err != nil { + return 0 + } } } err = e.exportReplications(ch) diff --git a/collector/security.go b/collector/security.go index a300e24..efa86a8 100644 --- a/collector/security.go +++ b/collector/security.go @@ -2,6 +2,7 @@ package collector import ( "fmt" + "time" "github.com/prometheus/client_golang/prometheus" @@ -85,3 +86,48 @@ func (e *Exporter) exportGroups(metricName string, metric *prometheus.Desc, ch c ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, float64(len(groups.Groups)), groups.NodeId) return nil } + +func (e *Exporter) exportCertificates(metricName string, metric *prometheus.Desc, ch chan<- prometheus.Metric) error { + // Fetch Artifactory certificates + certs, err := e.client.FetchCertificates() + if err != nil { + e.logger.Error( + "Couldn't scrape Artifactory when fetching system/security/certificates", + "err", err.Error(), + ) + e.totalAPIErrors.Inc() + return err + } + if len(certs.Certificates) == 0 { + e.logger.Debug("No certificates found") + return nil + } + + for _, certificate := range certs.Certificates { + var validThrough float64 + timeNow := float64(time.Now().Unix()) + if validThroughTime, err := time.Parse(time.RFC3339, certificate.ValidUntil); err != nil { + e.logger.Warn( + "Couldn't parse certificate ValidThrough", + "err", err.Error(), + ) + validThrough = timeNow + } else { + validThrough = float64(validThroughTime.Unix()) + } + + alias := certificate.CertificateAlias + issued_by := certificate.IssuedBy + valid_until := certificate.ValidUntil + e.logger.Debug( + "Registering metric", + "metric", metricName, + "alias", alias, + "issued_by", issued_by, + "valid_until", valid_until, + ) + ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, validThrough-timeNow, alias, issued_by, valid_until, certs.NodeId) + } + + return nil +}