1
0
mirror of https://github.com/ribbybibby/ssl_exporter.git synced 2024-11-24 08:22:17 +02:00

Add file prober

This commit is contained in:
Rob Best 2020-11-09 12:57:28 +00:00
parent c74c0de901
commit 0506638f63
24 changed files with 1499 additions and 130 deletions

137
README.md
View File

@ -28,14 +28,16 @@ meaningful visualisations and consoles.
- [Usage](#usage)
- [Metrics](#metrics)
- [Configuration](#configuration)
- [Configuration file](#configuration-file)
- [<module>](#module)
- [<tls_config>](#tls_config)
- [<https_probe>](#https_probe)
- [<tcp_probe>](#tcp_probe)
- [TCP](#tcp)
- [HTTPS](#https)
- [File](#file)
- [Configuration file](#configuration-file)
- [<module>](#module)
- [<tls_config>](#tls_config)
- [<https_probe>](#https_probe)
- [<tcp_probe>](#tcp_probe)
- [Example Queries](#example-queries)
- [Peer Cerificates vs Verified Chain Certificates](#peer-cerificates-vs-verified-chain-certificates)
- [Proxying](#proxying)
- [Peer Certificates vs Verified Chain Certificates](#peer-certificates-vs-verified-chain-certificates)
- [Grafana](#grafana)
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
@ -47,7 +49,7 @@ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
Similarly to the blackbox_exporter, visiting
[http://localhost:9219/probe?target=example.com:443](http://localhost:9219/probe?target=example.com:443)
will return certificate metrics for example.com. The `ssl_tls_connect_success`
will return certificate metrics for example.com. The `ssl_probe_success`
metric indicates if the probe has been successful.
### Docker
@ -88,24 +90,28 @@ Flags:
## Metrics
| Metric | Meaning | Labels |
| ----------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| ssl_cert_not_after | The date after which a peer certificate expires. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_cert_not_before | The date before which a peer certificate is not valid. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_ocsp_response_next_update | The nextUpdate value in the OCSP response. Expressed as a Unix Epoch Time | |
| ssl_ocsp_response_produced_at | The producedAt value in the OCSP response. Expressed as a Unix Epoch Time | |
| ssl_ocsp_response_revoked_at | The revocationTime value in the OCSP response. Expressed as a Unix Epoch Time | |
| ssl_ocsp_response_status | The status in the OCSP response. 0=Good 1=Revoked 2=Unknown | |
| ssl_ocsp_response_stapled | Does the connection state contain a stapled OCSP response? Boolean. | |
| ssl_ocsp_response_this_update | The thisUpdate value in the OCSP response. Expressed as a Unix Epoch Time | |
| ssl_prober | The prober used by the exporter to connect to the target. Boolean. | prober |
| ssl_tls_connect_success | Was the TLS connection successful? Boolean. | |
| ssl_tls_version_info | The TLS version used. Always 1. | version |
| ssl_verified_cert_not_after | The date after which a certificate in the verified chain expires. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_verified_cert_not_before | The date before which a certificate in the verified chain is not valid. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| Metric | Meaning | Labels |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| ssl_cert_not_after | The date after which a peer certificate expires. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_cert_not_before | The date before which a peer certificate is not valid. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_file_cert_not_after | The date after which a certificate found by the file prober expires. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_file_cert_not_before | The date before which a certificate found by the file prober is not valid. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_ocsp_response_next_update | The nextUpdate value in the OCSP response. Expressed as a Unix Epoch Time | |
| ssl_ocsp_response_produced_at | The producedAt value in the OCSP response. Expressed as a Unix Epoch Time | |
| ssl_ocsp_response_revoked_at | The revocationTime value in the OCSP response. Expressed as a Unix Epoch Time | |
| ssl_ocsp_response_status | The status in the OCSP response. 0=Good 1=Revoked 2=Unknown | |
| ssl_ocsp_response_stapled | Does the connection state contain a stapled OCSP response? Boolean. | |
| ssl_ocsp_response_this_update | The thisUpdate value in the OCSP response. Expressed as a Unix Epoch Time | |
| ssl_probe_success | Was the probe successful? Boolean. | |
| ssl_prober | The prober used by the exporter to connect to the target. Boolean. | prober |
| ssl_tls_version_info | The TLS version used. Always 1. | version |
| ssl_verified_cert_not_after | The date after which a certificate in the verified chain expires. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_verified_cert_not_before | The date before which a certificate in the verified chain is not valid. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
## Configuration
### TCP
Just like with the blackbox_exporter, you should pass the targets to a single
instance of the exporter in a scrape config with a clever bit of relabelling.
This allows you to leverage service discovery and keeps configuration
@ -128,8 +134,11 @@ scrape_configs:
replacement: 127.0.0.1:9219 # SSL exporter.
```
By default the exporter will make a TCP connection to the target. You can change
this to https by setting the module parameter:
### HTTPS
By default the exporter will make a TCP connection to the target. This will be
suitable for most cases but if you want to take advantage of http proxying you
can use a HTTPS client by setting the `https` module parameter:
```yml
scrape_configs:
@ -150,7 +159,53 @@ scrape_configs:
replacement: 127.0.0.1:9219
```
### Configuration file
This will use proxy servers discovered by the environment variables `HTTP_PROXY`,
`HTTPS_PROXY` and `ALL_PROXY`. Or, you can set the `proxy_url` option in the module
configuration.
The latter takes precedence.
### File
The `file` prober exports `ssl_file_cert_not_after` and
`ssl_file_cert_not_before` for PEM encoded certificates found in local files.
Files local to the exporter can be scraped by providing them as the target
parameter:
```
curl "localhost:9219/probe?module=file&target=/etc/ssl/cert.pem"
```
The target parameter supports globbing (as provided by the
[doublestar](https://github.com/bmatcuk/doublestar) package),
which allows you to capture multiple files at once:
```
curl "localhost:9219/probe?module=file&target=/etc/ssl/**/*.pem"
```
One specific usage of this prober could be to run the exporter as a DaemonSet in
Kubernetes and then scrape each instance to check the expiry of certificates on
each node:
```
scrape_configs:
- job_name: "ssl"
metrics_path: /probe
params:
module: ["file"]
target: ["/etc/kubernetes/**/*.crt"]
kubernetes_sd_configs:
- role: node
relabel_configs:
- source_labels: [__address__]
regex: ^(.*):(.*)$
target_label: __address__
replacement: ${1}:9219
```
## Configuration file
You can provide further module configuration by providing the path to a
configuration file with `--config.file`. The file is written in yaml format,
@ -160,10 +215,10 @@ defined by the schema below.
modules: [<module>]
```
#### \<module\>
### \<module\>
```
# The protocol over which the probe will take place (https, tcp)
# The type of probe (https, tcp, file)
prober: <prober_string>
# How long the probe will wait before giving up.
@ -177,7 +232,7 @@ prober: <prober_string>
[ tcp: <tcp_probe> ]
```
#### <tls_config>
### <tls_config>
```
# Disable target certificate validation.
@ -196,14 +251,14 @@ prober: <prober_string>
[ server_name: <string> ]
```
#### <https_probe>
### <https_probe>
```
# HTTP proxy server to use to connect to the targets.
[ proxy_url: <string> ]
```
#### <tcp_probe>
### <tcp_probe>
```
# Use the STARTTLS command before starting TLS for those protocols that support it (smtp, ftp, imap)
@ -237,13 +292,13 @@ Number of certificates presented by the server:
count(ssl_cert_not_after) by (instance)
```
Identify instances that have failed to create a valid SSL connection:
Identify failed probes:
```
ssl_tls_connect_success == 0
ssl_probe_success == 0
```
## Peer Cerificates vs Verified Chain Certificates
## Peer Certificates vs Verified Chain Certificates
Metrics are exported for the `NotAfter` and `NotBefore` fields for peer
certificates as well as for the verified chain that is
@ -277,20 +332,6 @@ of trust between the exporter and the target. Genuine clients may hold different
root certs than the exporter and therefore have different verified chains of
trust.
## Proxying
The `https` prober supports the use of proxy servers discovered by the
environment variables `HTTP_PROXY`, `HTTPS_PROXY` and `ALL_PROXY`.
For instance:
$ export HTTPS_PROXY=localhost:8888
$ ./ssl_exporter
Or, you can set the `proxy_url` option in the module.
The latter takes precedence.
## Grafana
You can find a simple dashboard [here](grafana/dashboard.json) that tracks

View File

@ -22,6 +22,9 @@ var (
"https": Module{
Prober: "https",
},
"file": Module{
Prober: "file",
},
},
}
)
@ -42,7 +45,6 @@ func LoadConfig(confFile string) (*Config, error) {
}
return c, nil
}
type Config struct {

View File

@ -17,4 +17,12 @@ scrape_configs:
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9219 # SSL exporter.
replacement: 127.0.0.1:9219 # SSL exporter.
- job_name: 'ssl-files'
metrics_path: /probe
params:
module: ["file"]
target: ["/etc/ssl/**/*.pem"]
static_configs:
- targets:
- 127.0.0.1:9219

2
go.mod
View File

@ -1,7 +1,9 @@
module github.com/ribbybibby/ssl_exporter
require (
github.com/bmatcuk/doublestar/v2 v2.0.3
github.com/prometheus/client_golang v1.8.0
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.14.0
github.com/sirupsen/logrus v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9

2
go.sum
View File

@ -30,6 +30,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bmatcuk/doublestar/v2 v2.0.3 h1:D6SI8MzWzXXBXZFS87cFL6s/n307lEU+thM2SUnge3g=
github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

View File

@ -240,7 +240,7 @@
"steppedLine": false,
"targets": [
{
"expr": "(up{job=~\"$job\", instance=~\"$instance\"} == 0 or ssl_tls_connect_success{job=~\"$job\", instance=~\"$instance\"} == 0)^0",
"expr": "(up{job=~\"$job\", instance=~\"$instance\"} == 0 or ssl_probe_success{job=~\"$job\", instance=~\"$instance\"} == 0)^0",
"format": "time_series",
"instant": false,
"legendFormat": "{{instance}}",
@ -407,7 +407,7 @@
],
"targets": [
{
"expr": "ssl_tls_connect_success{instance=~\"$instance\",job=~\"$job\"} == 0",
"expr": "ssl_probe_success{instance=~\"$instance\",job=~\"$job\"} == 0",
"format": "table",
"instant": true,
"intervalFactor": 1,
@ -585,14 +585,14 @@
"value": ["$__all"]
},
"datasource": "Prometheus",
"definition": "label_values(ssl_tls_connect_success, job)",
"definition": "label_values(ssl_probe_success, job)",
"hide": 0,
"includeAll": true,
"label": "Job",
"multi": true,
"name": "job",
"options": [],
"query": "label_values(ssl_tls_connect_success, job)",
"query": "label_values(ssl_probe_success, job)",
"refresh": 1,
"regex": "",
"skipUrlSync": false,

35
prober/file.go Normal file
View File

@ -0,0 +1,35 @@
package prober
import (
"context"
"fmt"
"github.com/bmatcuk/doublestar/v2"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/config"
)
func ProbeFile(ctx context.Context, target string, module config.Module, registry *prometheus.Registry) error {
errCh := make(chan error, 1)
go func() {
files, err := doublestar.Glob(target)
if err != nil {
errCh <- err
return
}
if len(files) == 0 {
errCh <- fmt.Errorf("No files found")
} else {
errCh <- collectFileMetrics(files, registry)
}
}()
select {
case <-ctx.Done():
return fmt.Errorf("context timeout, ran out of time")
case err := <-errCh:
return err
}
}

279
prober/file_test.go Normal file
View File

@ -0,0 +1,279 @@
package prober
import (
"context"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/ribbybibby/ssl_exporter/config"
"github.com/ribbybibby/ssl_exporter/test"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
// TestProbeFile tests a file
func TestProbeFile(t *testing.T) {
cert, certFile, err := createTestFile("", "tls*.crt")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeFile(ctx, certFile, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkFileMetrics(cert, certFile, registry, t)
}
// TestProbeFileGlob tests matching a file with a glob
func TestProbeFileGlob(t *testing.T) {
cert, certFile, err := createTestFile("", "tls*.crt")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
glob := filepath.Dir(certFile) + "/*.crt"
if err := ProbeFile(ctx, glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkFileMetrics(cert, certFile, registry, t)
}
// TestProbeFileGlobDoubleStar tests matching a file with a ** glob
func TestProbeFileGlobDoubleStar(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert, certFile, err := createTestFile(tmpDir, "tls*.crt")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
glob := filepath.Dir(filepath.Dir(certFile)) + "/**/*.crt"
if err := ProbeFile(ctx, glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkFileMetrics(cert, certFile, registry, t)
}
// TestProbeFileGlobDoubleStarMultiple tests matching multiple files with a ** glob
func TestProbeFileGlobDoubleStarMultiple(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "testdir")
if err != nil {
t.Fatalf(err.Error())
}
defer os.RemoveAll(tmpDir)
tmpDir1, err := ioutil.TempDir(tmpDir, "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert1, certFile1, err := createTestFile(tmpDir1, "1*.crt")
if err != nil {
t.Fatalf(err.Error())
}
tmpDir2, err := ioutil.TempDir(tmpDir, "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert2, certFile2, err := createTestFile(tmpDir2, "2*.crt")
if err != nil {
t.Fatalf(err.Error())
}
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
glob := tmpDir + "/**/*.crt"
if err := ProbeFile(ctx, glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkFileMetrics(cert1, certFile1, registry, t)
checkFileMetrics(cert2, certFile2, registry, t)
}
// Create a certificate and write it to a file
func createTestFile(dir, filename string) (*x509.Certificate, string, error) {
certPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1))
block, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, "", err
}
tmpFile, err := ioutil.TempFile(dir, filename)
if err != nil {
return nil, tmpFile.Name(), err
}
if _, err := tmpFile.Write(certPEM); err != nil {
return nil, tmpFile.Name(), err
}
if err := tmpFile.Close(); err != nil {
return nil, tmpFile.Name(), err
}
return cert, tmpFile.Name(), nil
}
// Check metrics
func checkFileMetrics(cert *x509.Certificate, certFile string, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
ips := ","
for _, ip := range cert.IPAddresses {
ips = ips + ip.String() + ","
}
expectedLabels := map[string]map[string]map[string]string{
certFile: {
"ssl_file_cert_not_after": {
"file": certFile,
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
"ssl_file_cert_not_before": {
"file": certFile,
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
},
}
checkFileRegistryLabels(expectedLabels, mfs, t)
expectedResults := map[string]map[string]float64{
certFile: {
"ssl_file_cert_not_after": float64(cert.NotAfter.Unix()),
"ssl_file_cert_not_before": float64(cert.NotBefore.Unix()),
},
}
checkFileRegistryResults(expectedResults, mfs, t)
}
// Check if expected results are in the registry
func checkFileRegistryResults(expRes map[string]map[string]float64, mfs []*dto.MetricFamily, t *testing.T) {
results := make(map[string]map[string]float64)
for _, mf := range mfs {
for _, metric := range mf.Metric {
for _, l := range metric.GetLabel() {
if l.GetName() == "file" {
if _, ok := results[l.GetValue()]; !ok {
results[l.GetValue()] = make(map[string]float64)
}
results[l.GetValue()][mf.GetName()] = metric.GetGauge().GetValue()
}
}
}
}
for expf, expr := range expRes {
for expm, expv := range expr {
if _, ok := results[expf]; !ok {
t.Fatalf("Could not find results for file %v", expf)
}
v, ok := results[expf][expm]
if !ok {
t.Fatalf("Expected metric %v not found in returned metrics for file %v", expm, expf)
}
if v != expv {
t.Fatalf("Expected: %v: %v, got: %v: %v for file %v", expm, expv, expm, v, expf)
}
}
}
}
// Check if expected labels are in the registry
func checkFileRegistryLabels(expRes map[string]map[string]map[string]string, mfs []*dto.MetricFamily, t *testing.T) {
results := make(map[string]map[string]map[string]string)
for _, mf := range mfs {
for _, metric := range mf.Metric {
for _, l := range metric.GetLabel() {
if l.GetName() == "file" {
if _, ok := results[l.GetValue()]; !ok {
results[l.GetValue()] = make(map[string]map[string]string)
}
results[l.GetValue()][mf.GetName()] = make(map[string]string)
for _, sl := range metric.GetLabel() {
results[l.GetValue()][mf.GetName()][sl.GetName()] = sl.GetValue()
}
}
}
}
}
for expf, expr := range expRes {
for expm, expl := range expr {
if _, ok := results[expf]; !ok {
t.Fatalf("Could not find results for file %v", expf)
}
l, ok := results[expf][expm]
if !ok {
t.Fatalf("Expected metric %v not found in returned metrics for file %v", expm, expf)
}
for expk, expv := range expl {
v, ok := l[expk]
if !ok {
t.Fatalf("Expected label %v for metric %v not found in returned metrics for file %v", expk, expm, expf)
}
if v != expv {
t.Fatalf("Expected %v{%q=%q}, got: %v{%q=%q} for file %v", expm, expk, expv, expm, expk, v, expf)
}
}
if len(l) != len(expl) {
t.Fatalf("Expected %v labels but got %v for metric %v and file %v", len(expl), len(l), expm, expf)
}
}
}
}

View File

@ -1,13 +1,13 @@
package prober
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log"
@ -15,7 +15,7 @@ import (
)
// ProbeHTTPS performs a https probe
func ProbeHTTPS(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error {
func ProbeHTTPS(ctx context.Context, target string, module config.Module, registry *prometheus.Registry) error {
tlsConfig, err := newTLSConfig("", registry, &module.TLSConfig)
if err != nil {
return err
@ -48,11 +48,15 @@ func ProbeHTTPS(target string, module config.Module, timeout time.Duration, regi
Proxy: proxy,
DisableKeepAlives: true,
},
Timeout: timeout,
}
// Issue a GET request to the target
resp, err := client.Get(targetURL.String())
request, err := http.NewRequest(http.MethodGet, targetURL.String(), nil)
if err != nil {
return err
}
request = request.WithContext(ctx)
resp, err := client.Do(request)
if err != nil {
return err
}

View File

@ -1,6 +1,7 @@
package prober
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
@ -37,7 +38,10 @@ func TestProbeHTTPS(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
@ -69,7 +73,10 @@ func TestProbeHTTPSInvalidName(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS("https://localhost:"+u.Port(), module, 5*time.Second, registry); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, "https://localhost:"+u.Port(), module, registry); err == nil {
t.Fatalf("expected error, but err was nil")
}
}
@ -100,7 +107,10 @@ func TestProbeHTTPSNoScheme(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS(u.Host, module, 5*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, u.Host, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}
@ -132,7 +142,10 @@ func TestProbeHTTPSServerName(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS("https://localhost:"+u.Port(), module, 5*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, "https://localhost:"+u.Port(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}
@ -147,7 +160,10 @@ func TestProbeHTTPSHTTP(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS(server.URL, config.Module{}, 5*time.Second, registry); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, server.URL, config.Module{}, registry); err == nil {
t.Fatalf("expected error, but err was nil")
}
}
@ -196,7 +212,10 @@ func TestProbeHTTPSClientAuth(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}
@ -249,7 +268,10 @@ func TestProbeHTTPSClientAuthWrongClientCert(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, server.URL, module, registry); err == nil {
t.Fatalf("expected error but err is nil")
}
}
@ -282,7 +304,10 @@ func TestProbeHTTPSExpired(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, server.URL, module, registry); err == nil {
t.Fatalf("expected error but err is nil")
}
}
@ -316,7 +341,10 @@ func TestProbeHTTPSExpiredInsecure(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}
@ -362,14 +390,17 @@ func TestProbeHTTPSProxy(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, server.URL, module, registry); err == nil {
t.Fatalf("expected error but err was nil")
}
// Test with the proxy url, this shouldn't return an error
module.HTTPS.ProxyURL = config.URL{URL: proxyURL}
if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil {
if err := ProbeHTTPS(ctx, server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}

View File

@ -3,12 +3,16 @@ package prober
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"sort"
"strconv"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log"
"golang.org/x/crypto/ocsp"
)
@ -82,7 +86,13 @@ func collectCertificateMetrics(certs []*x509.Certificate, registry *prometheus.R
)
registry.MustRegister(notAfter, notBefore)
for _, cert := range uniq(certs) {
certs = uniq(certs)
if len(certs) == 0 {
return fmt.Errorf("No certificates found")
}
for _, cert := range certs {
labels := labelValues(cert)
if !cert.NotAfter.IsZero() {
@ -219,6 +229,65 @@ func collectOCSPMetrics(ocspResponse []byte, registry *prometheus.Registry) erro
return nil
}
func collectFileMetrics(files []string, registry *prometheus.Registry) error {
var (
totalCerts []*x509.Certificate
fileNotAfter = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "file_cert_not_after"),
Help: "NotAfter expressed as a Unix Epoch Time for a certificate found in a file",
},
[]string{"file", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
fileNotBefore = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "file_cert_not_before"),
Help: "NotBefore expressed as a Unix Epoch Time for a certificate found in a file",
},
[]string{"file", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
)
registry.MustRegister(fileNotAfter, fileNotBefore)
for _, f := range files {
data, err := ioutil.ReadFile(f)
if err != nil {
log.Debugf("Error reading file: %s error=%s", f, err)
continue
}
var certs []*x509.Certificate
for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) {
if block.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
if !contains(certs, cert) {
certs = append(certs, cert)
}
}
}
totalCerts = append(totalCerts, certs...)
for _, cert := range certs {
labels := append([]string{f}, labelValues(cert)...)
if !cert.NotAfter.IsZero() {
fileNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
}
if !cert.NotBefore.IsZero() {
fileNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
}
}
}
if len(totalCerts) == 0 {
return fmt.Errorf("No certificates found")
}
return nil
}
func labelValues(cert *x509.Certificate) []string {
return []string{
cert.SerialNumber.String(),

View File

@ -1,7 +1,7 @@
package prober
import (
"time"
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/config"
@ -13,8 +13,9 @@ var (
"https": ProbeHTTPS,
"http": ProbeHTTPS,
"tcp": ProbeTCP,
"file": ProbeFile,
}
)
// ProbeFn probes
type ProbeFn func(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error
type ProbeFn func(ctx context.Context, target string, module config.Module, registry *prometheus.Registry) error

View File

@ -2,11 +2,11 @@ package prober
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"net"
"regexp"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log"
@ -14,21 +14,21 @@ import (
)
// ProbeTCP performs a tcp probe
func ProbeTCP(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error {
func ProbeTCP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry) error {
tlsConfig, err := newTLSConfig(target, registry, &module.TLSConfig)
if err != nil {
return err
}
dialer := &net.Dialer{Timeout: timeout}
conn, err := dialer.Dial("tcp", target)
dialer := &net.Dialer{}
conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil {
return err
}
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
deadline, _ := ctx.Deadline()
if err := conn.SetDeadline(deadline); err != nil {
return fmt.Errorf("Error setting deadline")
}

View File

@ -1,6 +1,7 @@
package prober
import (
"context"
"crypto/tls"
"net"
"testing"
@ -33,7 +34,10 @@ func TestProbeTCP(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}
@ -61,7 +65,10 @@ func TestProbeTCPInvalidName(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeTCP("localhost:"+listenPort, module, 10*time.Second, registry); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, "localhost:"+listenPort, module, registry); err == nil {
t.Fatalf("expected error but err was nil")
}
}
@ -90,7 +97,10 @@ func TestProbeTCPServerName(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeTCP("localhost:"+listenPort, module, 10*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, "localhost:"+listenPort, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}
@ -123,7 +133,10 @@ func TestProbeTCPExpired(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeTCP(server.Listener.Addr().String(), module, 5*time.Second, registry); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err == nil {
t.Fatalf("expected error but err is nil")
}
}
@ -157,7 +170,10 @@ func TestProbeTCPExpiredInsecure(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeTCP(server.Listener.Addr().String(), module, 5*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
@ -186,7 +202,10 @@ func TestProbeTCPStartTLSSMTP(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}
@ -214,7 +233,10 @@ func TestProbeTCPStartTLSFTP(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}
@ -242,7 +264,10 @@ func TestProbeTCPStartTLSIMAP(t *testing.T) {
registry := prometheus.NewRegistry()
if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
}

View File

@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"net/http"
"strconv"
@ -52,6 +53,9 @@ func probeHandler(w http.ResponseWriter, r *http.Request, conf *config.Config) {
timeout = time.Duration((timeoutSeconds) * 1e9)
}
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
target := r.URL.Query().Get("target")
if target == "" {
http.Error(w, "Target parameter is missing", http.StatusBadRequest)
@ -65,10 +69,10 @@ func probeHandler(w http.ResponseWriter, r *http.Request, conf *config.Config) {
}
var (
tlsConnectSuccess = prometheus.NewGauge(
probeSuccess = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "tls_connect_success"),
Help: "If the TLS connection was a success",
Name: prometheus.BuildFQName(namespace, "", "probe_success"),
Help: "If the probe was a success",
},
)
proberType = prometheus.NewGaugeVec(
@ -81,16 +85,15 @@ func probeHandler(w http.ResponseWriter, r *http.Request, conf *config.Config) {
)
registry := prometheus.NewRegistry()
registry.MustRegister(tlsConnectSuccess, proberType)
registry.MustRegister(probeSuccess, proberType)
proberType.WithLabelValues(module.Prober).Set(1)
err := probeFunc(target, module, timeout, registry)
err := probeFunc(ctx, target, module, registry)
if err != nil {
log.Errorf("error=%s target=%s prober=%s timeout=%s", err, target, module.Prober, timeout)
tlsConnectSuccess.Set(0)
probeSuccess.Set(0)
} else {
tlsConnectSuccess.Set(1)
probeSuccess.Set(1)
}
// Serve

View File

@ -52,8 +52,8 @@ func TestProbeHandlerHTTPS(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok {
t.Errorf("expected `ssl_tls_connect_success 1`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
// Check probe metric
@ -112,8 +112,8 @@ func TestProbeHandlerHTTPSTimeout(t *testing.T) {
t.Fatalf(err.Error())
}
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok {
t.Errorf("expected `ssl_probe_success 0`")
}
if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"https\"} 1"); !ok {
@ -211,8 +211,8 @@ func TestProbeHandlerHTTPSNoServer(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok {
t.Errorf("expected `ssl_probe_success 0`")
}
}
@ -235,9 +235,9 @@ func TestProbeHandlerHTTPSSpaces(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0")
if !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
t.Errorf("expected `ssl_probe_success 0`")
}
}
@ -260,9 +260,9 @@ func TestProbeHandlerHTTPSHTTP(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0")
if !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
t.Errorf("expected `ssl_probe_success 0`")
}
}
@ -319,9 +319,9 @@ func TestProbeHandlerHTTPSClientAuthWrongClientCert(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0")
if !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
t.Errorf("expected `ssl_probe_success 0`")
}
}
@ -353,8 +353,8 @@ func TestProbeHandlerTCP(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok {
t.Errorf("expected `ssl_tls_connect_success 1`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
// Check probe metric
@ -402,8 +402,8 @@ func TestProbeHandlerTCPTimeout(t *testing.T) {
t.Fatalf(err.Error())
}
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok {
t.Errorf("expected `ssl_probe_success 0`")
}
if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"tcp\"} 1"); !ok {
@ -583,8 +583,8 @@ func TestProbeHandlerTCPNoServer(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok {
t.Errorf("expected `ssl_probe_success 0`")
}
}
@ -607,9 +607,9 @@ func TestProbeHandlerTCPSpaces(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0")
if !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
t.Errorf("expected `ssl_probe_success 0`")
}
}
@ -632,9 +632,9 @@ func TestProbeHandlerTCPHTTP(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0")
if !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
t.Errorf("expected `ssl_probe_success 0`")
}
}
@ -672,9 +672,9 @@ func TestProbeHandlerTCPExpired(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0")
if !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
t.Errorf("expected `ssl_probe_success 0`")
}
}
@ -713,9 +713,9 @@ func TestProbeHandlerTCPExpiredInsecure(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1")
ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1")
if !ok {
t.Errorf("expected `ssl_tls_connect_success 1`")
t.Errorf("expected `ssl_probe_success 1`")
}
}
@ -769,8 +769,8 @@ func TestProbeHandlerProxy(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok {
t.Errorf("expected `ssl_tls_connect_success 0`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok {
t.Errorf("expected `ssl_probe_success 0`")
}
// Test with an actual proxy server
@ -808,8 +808,8 @@ func TestProbeHandlerProxy(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok {
t.Errorf("expected `ssl_tls_connect_success 1`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
}
@ -844,8 +844,8 @@ func TestProbeHandlerTCPStartTLSSMTP(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok {
t.Errorf("expected `ssl_tls_connect_success 1`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
// Check probe metric
@ -890,8 +890,8 @@ func TestProbeHandlerTCPStartTLSFTP(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok {
t.Errorf("expected `ssl_tls_connect_success 1`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
// Check probe metric
@ -936,8 +936,8 @@ func TestProbeHandlerTCPStartTLSIMAP(t *testing.T) {
}
// Check success metric
if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok {
t.Errorf("expected `ssl_tls_connect_success 1`")
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
// Check probe metric

32
vendor/github.com/bmatcuk/doublestar/v2/.gitignore generated vendored Normal file
View File

@ -0,0 +1,32 @@
# vi
*~
*.swp
*.swo
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# test directory
test/

20
vendor/github.com/bmatcuk/doublestar/v2/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,20 @@
language: go
go:
- 1.12
- 1.13
- 1.14
os:
- linux
- windows
before_install:
- go get -t -v ./...
script:
- go test -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)

22
vendor/github.com/bmatcuk/doublestar/v2/LICENSE generated vendored Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Bob Matcuk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

143
vendor/github.com/bmatcuk/doublestar/v2/README.md generated vendored Normal file
View File

@ -0,0 +1,143 @@
# doublestar
Path pattern matching and globbing supporting `doublestar` (`**`) patterns.
[![PkgGoDev](https://pkg.go.dev/badge/github.com/bmatcuk/doublestar)](https://pkg.go.dev/github.com/bmatcuk/doublestar/v2)
[![Release](https://img.shields.io/github/release/bmatcuk/doublestar.svg?branch=master)](https://github.com/bmatcuk/doublestar/releases)
[![Build Status](https://travis-ci.org/bmatcuk/doublestar.svg?branch=master)](https://travis-ci.org/bmatcuk/doublestar)
[![codecov.io](https://img.shields.io/codecov/c/github/bmatcuk/doublestar.svg?branch=master)](https://codecov.io/github/bmatcuk/doublestar?branch=master)
## About
#### [Updating from v1 to v2?](UPGRADING.md)
**doublestar** is a [golang](http://golang.org/) implementation of path pattern
matching and globbing with support for "doublestar" (aka globstar: `**`)
patterns.
doublestar patterns match files and directories recursively. For example, if
you had the following directory structure:
```bash
grandparent
`-- parent
|-- child1
`-- child2
```
You could find the children with patterns such as: `**/child*`,
`grandparent/**/child?`, `**/parent/*`, or even just `**` by itself (which will
return all files and directories recursively).
Bash's globstar is doublestar's inspiration and, as such, works similarly.
Note that the doublestar must appear as a path component by itself. A pattern
such as `/path**` is invalid and will be treated the same as `/path*`, but
`/path*/**` should achieve the desired result. Additionally, `/path/**` will
match all directories and files under the path directory, but `/path/**/` will
only match directories.
## Installation
**doublestar** can be installed via `go get`:
```bash
go get github.com/bmatcuk/doublestar/v2
```
To use it in your code, you must import it:
```go
import "github.com/bmatcuk/doublestar/v2"
```
## Usage
### Match
```go
func Match(pattern, name string) (bool, error)
```
Match returns true if `name` matches the file name `pattern`
([see below](#patterns)). `name` and `pattern` are split on forward slash (`/`)
characters and may be relative or absolute.
Note: `Match()` is meant to be a drop-in replacement for `path.Match()`. As
such, it always uses `/` as the path separator. If you are writing code that
will run on systems where `/` is not the path separator (such as Windows), you
want to use `PathMatch()` (below) instead.
### PathMatch
```go
func PathMatch(pattern, name string) (bool, error)
```
PathMatch returns true if `name` matches the file name `pattern`
([see below](#patterns)). The difference between Match and PathMatch is that
PathMatch will automatically use your system's path separator to split `name`
and `pattern`.
`PathMatch()` is meant to be a drop-in replacement for `filepath.Match()`.
### Glob
```go
func Glob(pattern string) ([]string, error)
```
Glob finds all files and directories in the filesystem that match `pattern`
([see below](#patterns)). `pattern` may be relative (to the current working
directory), or absolute.
`Glob()` is meant to be a drop-in replacement for `filepath.Glob()`.
### Patterns
**doublestar** supports the following special terms in the patterns:
Special Terms | Meaning
------------- | -------
`*` | matches any sequence of non-path-separators
`**` | matches any sequence of characters, including path separators
`?` | matches any single non-path-separator character
`[class]` | matches any single non-path-separator character against a class of characters ([see below](#character-classes))
`{alt1,...}` | matches a sequence of characters if one of the comma-separated alternatives matches
Any character with a special meaning can be escaped with a backslash (`\`).
#### Character Classes
Character classes support the following:
Class | Meaning
---------- | -------
`[abc]` | matches any single character within the set
`[a-z]` | matches any single character in the range
`[^class]` | matches any single character which does *not* match the class
### Abstracting the `os` package
**doublestar** by default uses the `Open`, `Stat`, and `Lstat`, functions and
`PathSeparator` value from the standard library's `os` package. To abstract
this, for example to be able to perform tests of Windows paths on Linux, or to
interoperate with your own filesystem code, it includes the functions `GlobOS`
and `PathMatchOS` which are identical to `Glob` and `PathMatch` except that they
operate on an `OS` interface:
```go
type OS interface {
Lstat(name string) (os.FileInfo, error)
Open(name string) (*os.File, error)
PathSeparator() rune
Stat(name string) (os.FileInfo, error)
}
```
`StandardOS` is a value that implements this interface by calling functions in
the standard library's `os` package.
## License
[MIT License](LICENSE)

13
vendor/github.com/bmatcuk/doublestar/v2/UPGRADING.md generated vendored Normal file
View File

@ -0,0 +1,13 @@
# Upgrading from v1 to v2
The change from v1 to v2 was fairly minor: the return type of the `Open` method
on the `OS` interface was changed from `*os.File` to `File`, a new interface
exported by doublestar. The new `File` interface only defines the functionality
doublestar actually needs (`io.Closer` and `Readdir`), making it easier to use
doublestar with [go-billy](https://github.com/src-d/go-billy),
[afero](https://github.com/spf13/afero), or something similar. If you were
using this functionality, updating should be as easy as updating `Open's`
return type, since `os.File` already implements `doublestar.File`.
If you weren't using this functionality, updating should be as easy as changing
your dependencies to point to v2.

630
vendor/github.com/bmatcuk/doublestar/v2/doublestar.go generated vendored Normal file
View File

@ -0,0 +1,630 @@
package doublestar
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
"unicode/utf8"
)
// File defines a subset of file operations
type File interface {
io.Closer
Readdir(count int) ([]os.FileInfo, error)
}
// An OS abstracts functions in the standard library's os package.
type OS interface {
Lstat(name string) (os.FileInfo, error)
Open(name string) (File, error)
PathSeparator() rune
Stat(name string) (os.FileInfo, error)
}
// A standardOS implements OS by calling functions in the standard library's os
// package.
type standardOS struct{}
func (standardOS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) }
func (standardOS) Open(name string) (File, error) { return os.Open(name) }
func (standardOS) PathSeparator() rune { return os.PathSeparator }
func (standardOS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) }
// StandardOS is a value that implements the OS interface by calling functions
// in the standard libray's os package.
var StandardOS OS = standardOS{}
// ErrBadPattern indicates a pattern was malformed.
var ErrBadPattern = path.ErrBadPattern
// Find the first index of a rune in a string,
// ignoring any times the rune is escaped using "\".
func indexRuneWithEscaping(s string, r rune) int {
end := strings.IndexRune(s, r)
if end == -1 || r == '\\' {
return end
}
if end > 0 && s[end-1] == '\\' {
start := end + utf8.RuneLen(r)
end = indexRuneWithEscaping(s[start:], r)
if end != -1 {
end += start
}
}
return end
}
// Find the last index of a rune in a string,
// ignoring any times the rune is escaped using "\".
func lastIndexRuneWithEscaping(s string, r rune) int {
end := strings.LastIndex(s, string(r))
if end == -1 {
return -1
}
if end > 0 && s[end-1] == '\\' {
end = lastIndexRuneWithEscaping(s[:end-1], r)
}
return end
}
// Find the index of the first instance of one of the unicode characters in
// chars, ignoring any times those characters are escaped using "\".
func indexAnyWithEscaping(s, chars string) int {
end := strings.IndexAny(s, chars)
if end == -1 {
return -1
}
if end > 0 && s[end-1] == '\\' {
_, adj := utf8.DecodeRuneInString(s[end:])
start := end + adj
end = indexAnyWithEscaping(s[start:], chars)
if end != -1 {
end += start
}
}
return end
}
// Split a set of alternatives such as {alt1,alt2,...} and returns the index of
// the rune after the closing curly brace. Respects nested alternatives and
// escaped runes.
func splitAlternatives(s string) (ret []string, idx int) {
ret = make([]string, 0, 2)
idx = 0
slen := len(s)
braceCnt := 1
esc := false
start := 0
for braceCnt > 0 {
if idx >= slen {
return nil, -1
}
sRune, adj := utf8.DecodeRuneInString(s[idx:])
if esc {
esc = false
} else if sRune == '\\' {
esc = true
} else if sRune == '{' {
braceCnt++
} else if sRune == '}' {
braceCnt--
} else if sRune == ',' && braceCnt == 1 {
ret = append(ret, s[start:idx])
start = idx + adj
}
idx += adj
}
ret = append(ret, s[start:idx-1])
return
}
// Returns true if the pattern is "zero length", meaning
// it could match zero or more characters.
func isZeroLengthPattern(pattern string) (ret bool, err error) {
// * can match zero
if pattern == "" || pattern == "*" || pattern == "**" {
return true, nil
}
// an alternative with zero length can match zero, for example {,x} - the
// first alternative has zero length
r, adj := utf8.DecodeRuneInString(pattern)
if r == '{' {
options, endOptions := splitAlternatives(pattern[adj:])
if endOptions == -1 {
return false, ErrBadPattern
}
if ret, err = isZeroLengthPattern(pattern[adj+endOptions:]); !ret || err != nil {
return
}
for _, o := range options {
if ret, err = isZeroLengthPattern(o); ret || err != nil {
return
}
}
}
return false, nil
}
// Match returns true if name matches the shell file name pattern.
// The pattern syntax is:
//
// pattern:
// { term }
// term:
// '*' matches any sequence of non-path-separators
// '**' matches any sequence of characters, including
// path separators.
// '?' matches any single non-path-separator character
// '[' [ '^' ] { character-range } ']'
// character class (must be non-empty)
// '{' { term } [ ',' { term } ... ] '}'
// c matches character c (c != '*', '?', '\\', '[')
// '\\' c matches character c
//
// character-range:
// c matches character c (c != '\\', '-', ']')
// '\\' c matches character c
// lo '-' hi matches character c for lo <= c <= hi
//
// Match requires pattern to match all of name, not just a substring.
// The path-separator defaults to the '/' character. The only possible
// returned error is ErrBadPattern, when pattern is malformed.
//
// Note: this is meant as a drop-in replacement for path.Match() which
// always uses '/' as the path separator. If you want to support systems
// which use a different path separator (such as Windows), what you want
// is the PathMatch() function below.
//
func Match(pattern, name string) (bool, error) {
return doMatching(pattern, name, '/')
}
// PathMatch is like Match except that it uses your system's path separator.
// For most systems, this will be '/'. However, for Windows, it would be '\\'.
// Note that for systems where the path separator is '\\', escaping is
// disabled.
//
// Note: this is meant as a drop-in replacement for filepath.Match().
//
func PathMatch(pattern, name string) (bool, error) {
return PathMatchOS(StandardOS, pattern, name)
}
// PathMatchOS is like PathMatch except that it uses vos's path separator.
func PathMatchOS(vos OS, pattern, name string) (bool, error) {
pattern = filepath.ToSlash(pattern)
return doMatching(pattern, name, vos.PathSeparator())
}
func doMatching(pattern, name string, separator rune) (matched bool, err error) {
// check for some base-cases
patternLen, nameLen := len(pattern), len(name)
if patternLen == 0 {
return nameLen == 0, nil
} else if nameLen == 0 {
return isZeroLengthPattern(pattern)
}
separatorAdj := utf8.RuneLen(separator)
patIdx := indexRuneWithEscaping(pattern, '/')
lastPat := patIdx == -1
if lastPat {
patIdx = len(pattern)
}
if pattern[:patIdx] == "**" {
// if our last pattern component is a doublestar, we're done -
// doublestar will match any remaining name components, if any.
if lastPat {
return true, nil
}
// otherwise, try matching remaining components
nameIdx := 0
patIdx += 1
for {
if m, _ := doMatching(pattern[patIdx:], name[nameIdx:], separator); m {
return true, nil
}
nextNameIdx := 0
if nextNameIdx = indexRuneWithEscaping(name[nameIdx:], separator); nextNameIdx == -1 {
break
}
nameIdx += separatorAdj + nextNameIdx
}
return false, nil
}
nameIdx := indexRuneWithEscaping(name, separator)
lastName := nameIdx == -1
if lastName {
nameIdx = nameLen
}
var matches []string
matches, err = matchComponent(pattern, name[:nameIdx])
if matches == nil || err != nil {
return
}
if len(matches) == 0 && lastName {
return true, nil
}
if !lastName {
nameIdx += separatorAdj
for _, alt := range matches {
matched, err = doMatching(alt, name[nameIdx:], separator)
if matched || err != nil {
return
}
}
}
return false, nil
}
// Glob returns the names of all files matching pattern or nil
// if there is no matching file. The syntax of pattern is the same
// as in Match. The pattern may describe hierarchical names such as
// /usr/*/bin/ed (assuming the Separator is '/').
//
// Glob ignores file system errors such as I/O errors reading directories.
// The only possible returned error is ErrBadPattern, when pattern
// is malformed.
//
// Your system path separator is automatically used. This means on
// systems where the separator is '\\' (Windows), escaping will be
// disabled.
//
// Note: this is meant as a drop-in replacement for filepath.Glob().
//
func Glob(pattern string) (matches []string, err error) {
return GlobOS(StandardOS, pattern)
}
// GlobOS is like Glob except that it operates on vos.
func GlobOS(vos OS, pattern string) (matches []string, err error) {
if len(pattern) == 0 {
return nil, nil
}
// if the pattern starts with alternatives, we need to handle that here - the
// alternatives may be a mix of relative and absolute
if pattern[0] == '{' {
options, endOptions := splitAlternatives(pattern[1:])
if endOptions == -1 {
return nil, ErrBadPattern
}
for _, o := range options {
m, e := Glob(o + pattern[endOptions+1:])
if e != nil {
return nil, e
}
matches = append(matches, m...)
}
return matches, nil
}
// If the pattern is relative or absolute and we're on a non-Windows machine,
// volumeName will be an empty string. If it is absolute and we're on a
// Windows machine, volumeName will be a drive letter ("C:") for filesystem
// paths or \\<server>\<share> for UNC paths.
isAbs := filepath.IsAbs(pattern) || pattern[0] == '\\' || pattern[0] == '/'
volumeName := filepath.VolumeName(pattern)
isWindowsUNC := strings.HasPrefix(volumeName, `\\`)
if isWindowsUNC || isAbs {
startIdx := len(volumeName) + 1
return doGlob(vos, fmt.Sprintf("%s%s", volumeName, string(vos.PathSeparator())), filepath.ToSlash(pattern[startIdx:]), matches)
}
// otherwise, it's a relative pattern
return doGlob(vos, ".", filepath.ToSlash(pattern), matches)
}
// Perform a glob
func doGlob(vos OS, basedir, pattern string, matches []string) (m []string, e error) {
m = matches
e = nil
// if the pattern starts with any path components that aren't globbed (ie,
// `path/to/glob*`), we can skip over the un-globbed components (`path/to` in
// our example).
globIdx := indexAnyWithEscaping(pattern, "*?[{\\")
if globIdx > 0 {
globIdx = lastIndexRuneWithEscaping(pattern[:globIdx], '/')
} else if globIdx == -1 {
globIdx = lastIndexRuneWithEscaping(pattern, '/')
}
if globIdx > 0 {
basedir = filepath.Join(basedir, pattern[:globIdx])
pattern = pattern[globIdx+1:]
}
// Lstat will return an error if the file/directory doesn't exist
fi, err := vos.Lstat(basedir)
if err != nil {
return
}
// if the pattern is empty, we've found a match
if len(pattern) == 0 {
m = append(m, basedir)
return
}
// otherwise, we need to check each item in the directory...
// first, if basedir is a symlink, follow it...
if (fi.Mode() & os.ModeSymlink) != 0 {
fi, err = vos.Stat(basedir)
if err != nil {
return
}
}
// confirm it's a directory...
if !fi.IsDir() {
return
}
files, err := filesInDir(vos, basedir)
if err != nil {
return
}
sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() })
slashIdx := indexRuneWithEscaping(pattern, '/')
lastComponent := slashIdx == -1
if lastComponent {
slashIdx = len(pattern)
}
if pattern[:slashIdx] == "**" {
// if the current component is a doublestar, we'll try depth-first
for _, file := range files {
// if symlink, we may want to follow
if (file.Mode() & os.ModeSymlink) != 0 {
file, err = vos.Stat(filepath.Join(basedir, file.Name()))
if err != nil {
continue
}
}
if file.IsDir() {
// recurse into directories
if lastComponent {
m = append(m, filepath.Join(basedir, file.Name()))
}
m, e = doGlob(vos, filepath.Join(basedir, file.Name()), pattern, m)
} else if lastComponent {
// if the pattern's last component is a doublestar, we match filenames, too
m = append(m, filepath.Join(basedir, file.Name()))
}
}
if lastComponent {
return // we're done
}
pattern = pattern[slashIdx+1:]
}
// check items in current directory and recurse
var match []string
for _, file := range files {
match, e = matchComponent(pattern, file.Name())
if e != nil {
return
}
if match != nil {
if len(match) == 0 {
m = append(m, filepath.Join(basedir, file.Name()))
} else {
for _, alt := range match {
m, e = doGlob(vos, filepath.Join(basedir, file.Name()), alt, m)
}
}
}
}
return
}
func filesInDir(vos OS, dirPath string) (files []os.FileInfo, e error) {
dir, err := vos.Open(dirPath)
if err != nil {
return nil, nil
}
defer func() {
if err := dir.Close(); e == nil {
e = err
}
}()
files, err = dir.Readdir(-1)
if err != nil {
return nil, nil
}
return
}
// Attempt to match a single path component with a pattern. Note that the
// pattern may include multiple components but that the "name" is just a single
// path component. The return value is a slice of patterns that should be
// checked against subsequent path components or nil, indicating that the
// pattern does not match this path. It is assumed that pattern components are
// separated by '/'
func matchComponent(pattern, name string) ([]string, error) {
// check for matches one rune at a time
patternLen, nameLen := len(pattern), len(name)
patIdx, nameIdx := 0, 0
for patIdx < patternLen && nameIdx < nameLen {
patRune, patAdj := utf8.DecodeRuneInString(pattern[patIdx:])
nameRune, nameAdj := utf8.DecodeRuneInString(name[nameIdx:])
if patRune == '/' {
patIdx++
break
} else if patRune == '\\' {
// handle escaped runes, only if separator isn't '\\'
patIdx += patAdj
patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:])
if patRune == utf8.RuneError {
return nil, ErrBadPattern
} else if patRune == nameRune {
patIdx += patAdj
nameIdx += nameAdj
} else {
return nil, nil
}
} else if patRune == '*' {
// handle stars - a star at the end of the pattern or before a separator
// will always match the rest of the path component
if patIdx += patAdj; patIdx >= patternLen {
return []string{}, nil
}
if patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:]); patRune == '/' {
return []string{pattern[patIdx+patAdj:]}, nil
}
// check if we can make any matches
for ; nameIdx < nameLen; nameIdx += nameAdj {
if m, e := matchComponent(pattern[patIdx:], name[nameIdx:]); m != nil || e != nil {
return m, e
}
_, nameAdj = utf8.DecodeRuneInString(name[nameIdx:])
}
return nil, nil
} else if patRune == '[' {
// handle character sets
patIdx += patAdj
endClass := indexRuneWithEscaping(pattern[patIdx:], ']')
if endClass == -1 {
return nil, ErrBadPattern
}
endClass += patIdx
classRunes := []rune(pattern[patIdx:endClass])
classRunesLen := len(classRunes)
if classRunesLen > 0 {
classIdx := 0
matchClass := false
if classRunes[0] == '^' {
classIdx++
}
for classIdx < classRunesLen {
low := classRunes[classIdx]
if low == '-' {
return nil, ErrBadPattern
}
classIdx++
if low == '\\' {
if classIdx < classRunesLen {
low = classRunes[classIdx]
classIdx++
} else {
return nil, ErrBadPattern
}
}
high := low
if classIdx < classRunesLen && classRunes[classIdx] == '-' {
// we have a range of runes
if classIdx++; classIdx >= classRunesLen {
return nil, ErrBadPattern
}
high = classRunes[classIdx]
if high == '-' {
return nil, ErrBadPattern
}
classIdx++
if high == '\\' {
if classIdx < classRunesLen {
high = classRunes[classIdx]
classIdx++
} else {
return nil, ErrBadPattern
}
}
}
if low <= nameRune && nameRune <= high {
matchClass = true
}
}
if matchClass == (classRunes[0] == '^') {
return nil, nil
}
} else {
return nil, ErrBadPattern
}
patIdx = endClass + 1
nameIdx += nameAdj
} else if patRune == '{' {
// handle alternatives such as {alt1,alt2,...}
patIdx += patAdj
options, endOptions := splitAlternatives(pattern[patIdx:])
if endOptions == -1 {
return nil, ErrBadPattern
}
patIdx += endOptions
results := make([][]string, 0, len(options))
totalResults := 0
for _, o := range options {
m, e := matchComponent(o+pattern[patIdx:], name[nameIdx:])
if e != nil {
return nil, e
}
if m != nil {
results = append(results, m)
totalResults += len(m)
}
}
if len(results) > 0 {
lst := make([]string, 0, totalResults)
for _, m := range results {
lst = append(lst, m...)
}
return lst, nil
}
return nil, nil
} else if patRune == '?' || patRune == nameRune {
// handle single-rune wildcard
patIdx += patAdj
nameIdx += nameAdj
} else {
return nil, nil
}
}
if nameIdx >= nameLen {
if patIdx >= patternLen {
return []string{}, nil
}
pattern = pattern[patIdx:]
slashIdx := indexRuneWithEscaping(pattern, '/')
testPattern := pattern
if slashIdx >= 0 {
testPattern = pattern[:slashIdx]
}
zeroLength, err := isZeroLengthPattern(testPattern)
if err != nil {
return nil, err
}
if zeroLength {
if slashIdx == -1 {
return []string{}, nil
} else {
return []string{pattern[slashIdx+1:]}, nil
}
}
}
return nil, nil
}

3
vendor/github.com/bmatcuk/doublestar/v2/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/bmatcuk/doublestar/v2
go 1.12

4
vendor/modules.txt vendored
View File

@ -5,6 +5,9 @@ github.com/alecthomas/template/parse
github.com/alecthomas/units
# github.com/beorn7/perks v1.0.1
github.com/beorn7/perks/quantile
# github.com/bmatcuk/doublestar/v2 v2.0.3
## explicit
github.com/bmatcuk/doublestar/v2
# github.com/cespare/xxhash/v2 v2.1.1
github.com/cespare/xxhash/v2
# github.com/golang/protobuf v1.4.3
@ -25,6 +28,7 @@ github.com/prometheus/client_golang/prometheus
github.com/prometheus/client_golang/prometheus/internal
github.com/prometheus/client_golang/prometheus/promhttp
# github.com/prometheus/client_model v0.2.0
## explicit
github.com/prometheus/client_model/go
# github.com/prometheus/common v0.14.0
## explicit