1
0
Fork 0
mirror of https://github.com/Luzifer/promcertcheck.git synced 2024-11-09 08:20:05 +00:00

Initial version

This commit is contained in:
Knut Ahlers 2015-09-04 15:36:49 +02:00
commit f109a5b41d
7 changed files with 568 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
certcheck

11
Makefile Normal file
View file

@ -0,0 +1,11 @@
test:
go generate
go run *.go \
--probe="https://www.cloudkeys.de/" \
--probe="https://foo.hub.luzifer.io/" \
--probe="https://registry.luzifer.io/" \
--probe="https://blog.knut.me/" \
--probe="https://gobuilder.me/" \
--probe="https://pwd.luzifer.io/" \
--probe="https://www.itpad.de/" \
--probe="https://mondash.org/"

236
bindata.go Normal file
View file

@ -0,0 +1,236 @@
// Code generated by go-bindata.
// sources:
// display.html
// DO NOT EDIT!
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
"os"
"time"
"io/ioutil"
"path/filepath"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _displayHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x9c\x56\x6d\x73\xdb\x36\x0c\xfe\xde\x5f\x81\xe8\xae\xd7\x24\x17\x49\x4b\xb2\xdc\xba\x4c\xf2\x5d\x96\xe5\xd6\x6c\x5d\xda\x25\xd9\xdb\xf5\xfa\x81\x16\x21\x8b\x0e\x45\xaa\x24\x65\xc7\xcb\xe5\xbf\x0f\x94\x64\x5b\xf1\x4b\xda\xee\x8b\x2d\x90\xc0\x03\xe0\x21\x40\x30\xd9\xf9\xe9\xdd\xf9\xed\x3f\xef\x2f\xa0\x70\xa5\x1c\xbc\x48\xfc\x1f\x48\xa6\x46\x69\x80\x2a\x18\xbc\x00\x48\x0a\x64\xdc\x7f\xd0\x67\x89\x8e\x41\x56\x30\x63\xd1\xa5\x41\xed\xf2\xf0\x75\xd0\xdf\x2a\x9c\xab\x42\xfc\x54\x8b\x49\x1a\xfc\x1d\xfe\x71\x16\x9e\xeb\xb2\x62\x4e\x0c\x25\x06\x90\x69\xe5\x50\x91\xdd\xe5\x45\x8a\x7c\x84\x4f\x2c\x15\x2b\x31\x0d\x26\x02\xa7\x95\x36\xae\xa7\x3c\x15\xdc\x15\x29\xc7\x89\xc8\x30\x6c\x84\x03\x10\x4a\x38\xc1\x64\x68\x33\x26\x31\x3d\x9c\x03\xed\x84\x21\xdc\x16\x08\x6c\xa8\x27\x08\xc7\xd0\x00\x3b\x36\xb2\xb0\x5f\xd6\xd6\xed\x13\x68\x89\x90\x0b\x63\x1d\x41\x80\x23\x55\x9f\xdb\x0f\xc0\xd4\x0c\x34\x89\xa6\x91\xe7\xbe\xc1\x1b\xb5\x36\xfb\x2c\x77\x68\xf6\xbd\x89\xc5\x16\x32\x0c\x3b\xaf\x4e\x38\x89\x83\x73\x34\x4e\xe4\x22\x63\x0e\x61\xc2\xa4\xe0\x94\xb5\x56\x60\xd0\xd6\xd2\xd9\x24\x6e\xb5\x5e\x2c\x03\xfd\x51\x6b\x67\x9d\x61\xd5\x12\x49\x0a\x75\x47\x16\x32\x0d\xac\x9b\x49\xb4\x05\x22\x31\x51\x18\xcc\xd3\x20\x8e\x4b\x76\x9f\x71\x15\x0d\xe7\x76\x5e\xa0\xe0\xe2\xc5\x42\x7c\x1c\x1d\x47\x27\x71\x66\xed\x72\x2d\x2a\x05\x69\x59\x1b\xf4\x5d\xbf\xb9\xfd\xed\xed\x09\xd8\x42\x94\x94\x39\x87\x6b\xb4\x95\x56\x3c\x1a\x5b\xc8\xb5\x81\xcb\x8b\xd7\x60\xeb\xca\x1f\x03\xe8\xbc\x53\x46\x89\x25\x51\x62\x1b\x83\x12\xb9\x60\xf0\xa9\x46\x23\xb0\x47\x84\x87\xfe\xeb\xec\xfa\xea\xf2\xea\xe7\xd3\x3e\x28\xd7\x68\xd5\x2b\x07\x53\x6d\xee\x40\xe4\x30\xd3\x35\xf8\x83\x6e\x0e\xa0\x62\x23\x22\x8c\xe0\x72\x21\xf1\x34\x8e\x9f\xc0\x7d\x20\x6d\xe9\x28\x22\xf8\xfe\x63\xbb\x4a\xeb\x36\x33\xa2\x72\x60\x4d\x96\x06\xbe\xde\x2c\x59\x69\x6b\xa3\x8e\x1f\x4f\x89\x2f\xe2\x13\xca\x6f\x42\x94\x7c\x17\x1d\x2d\xe5\x86\x8e\x31\xb1\x91\xc4\x2d\xcc\xd7\xa0\x9a\x36\xa5\xf8\x30\xfa\x96\x30\x3b\x69\x0b\x62\xb2\xf3\x01\x15\x17\xf9\xc7\x36\x9d\x24\x9e\x37\x51\x32\xd4\x7c\xd6\xe9\x70\x31\x81\x4c\x32\x6b\xd3\xc0\x97\x1c\x13\x0a\x4d\xb0\x88\xa8\xb7\x6b\xf4\x34\x80\xa6\x26\x28\x38\x14\xa3\xc2\x9d\x1e\x7d\x53\xdd\x7b\xa7\xa4\xd5\x1d\xed\xba\xc9\x62\x63\xd5\x97\x0c\x4b\x1e\x1e\x1e\x2d\x7c\xad\x6a\x54\x4c\xa1\x84\xe6\x37\xe4\x98\x33\x2a\xe1\x27\xba\x1b\xb4\x43\x9f\xa0\x50\xa3\x15\x3d\x80\xe7\x1b\xe3\x29\x68\x9b\xcd\xf3\x7e\x3c\x7f\x6b\x4e\x12\xc7\xe8\x8a\x99\x2b\xb6\x42\xf3\x1b\x52\x17\x88\x0a\xf9\x9a\x85\xb7\x31\xeb\x8b\x7e\xb9\x18\xbc\xd1\xd6\x51\xcb\x16\x03\x2f\x5c\x5a\x4b\x95\xbe\x10\xff\xf4\x39\x40\xad\x9c\x90\x8b\xb5\xeb\x26\x99\x46\x5c\x77\x13\x6f\xf2\xf3\xf0\xb2\xe9\xb5\x82\x1c\x1d\x78\x2e\xfc\x7d\xd4\x51\x02\x96\x3a\x0f\x39\xbc\x7c\xdc\x10\x1d\xd9\x51\x4f\x90\x66\x74\xe3\x98\xab\x2d\xa4\x29\x64\x4b\x86\xdf\xfd\xba\xd9\x6c\x6b\xb6\x84\x87\xf2\x39\xc4\x8b\xfb\x4a\xd0\xe6\x8d\xa6\x33\xdb\x0e\x3d\x67\x7e\xca\x8c\xda\x54\x04\x0b\x57\x74\x7b\x7e\x1e\x85\xd3\xf8\xe9\x75\xc2\x2a\x88\xef\xab\xed\x28\x7c\xf0\xf0\xd0\xf0\x0a\x8f\x8f\xc4\x3d\xdf\x84\xb2\xd0\xf3\x69\xf7\x0a\x34\x6a\xcf\x3a\xa2\xa9\x55\x6a\x75\x45\x23\xe9\xff\x80\x5c\x69\x77\xe6\xc7\xc5\x97\xda\xb6\xbc\xd3\x9f\x21\xea\x76\xf7\x9e\x31\xdb\x5a\x4b\xc4\x89\x2f\xa7\x35\x52\xc8\xc0\x77\xc1\xe0\xeb\xdb\x2c\xa7\x09\xb2\xe1\x0c\x12\xd6\xcd\xa3\xf9\x1d\x39\x12\xae\xa8\x87\xcd\xfd\xf8\xb6\xfe\x57\xe4\x68\xe2\xca\xe8\xd2\x57\x50\x56\x60\x76\x17\x0c\xde\x93\x78\x3e\x17\x81\x72\x9e\xa0\xb1\xfe\x0a\xf0\x79\xb2\xcf\x84\xb6\xb2\xb0\x72\xdf\x2d\x37\xfb\x1b\xcd\x20\x1a\xff\x4e\x07\x39\x83\x5d\x85\x19\x5a\xcb\xe8\xd3\x13\xb4\x18\xba\xaf\x2c\xfc\xc2\x26\xec\xa6\xbd\xf5\x2b\x59\x8f\x84\xb2\x7b\xcb\xe1\xd3\x1f\x07\x71\xcc\xc6\xec\x3e\x1a\x69\x3d\x92\xc8\x2a\x61\x9b\x6c\xfd\x5a\x2c\xc5\xd0\xc6\x63\x3f\x09\x67\x34\x16\x0e\x0f\xa3\xe3\x4e\xda\x3a\x16\x28\xb4\x4b\x95\xc9\x9a\xd3\x33\x45\x4a\xff\xba\xa8\x68\xf0\xf1\x79\x08\xb0\x3b\x44\xa9\xa7\x7b\x07\x40\xd1\x8a\x4e\x51\x50\xc5\x4f\x04\xaf\x99\x6c\xa6\x24\x0d\x61\x0b\x0a\x91\x93\xd9\x96\x80\xbf\xf4\xad\x30\x5e\x7d\x2a\xac\x86\x9c\xc4\xed\xbc\x4a\xe2\xf6\x7d\xf8\x5f\x00\x00\x00\xff\xff\x21\xaf\x29\x78\x30\x0a\x00\x00")
func displayHtmlBytes() ([]byte, error) {
return bindataRead(
_displayHtml,
"display.html",
)
}
func displayHtml() (*asset, error) {
bytes, err := displayHtmlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "display.html", size: 2608, mode: os.FileMode(420), modTime: time.Unix(1441373785, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if (err != nil) {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"display.html": displayHtml,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"display.html": &bintree{displayHtml, map[string]*bintree{
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

86
cert.go Normal file
View file

@ -0,0 +1,86 @@
package main
import (
"crypto/x509"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
type probeResult uint
const (
certificateOK probeResult = iota
certificateNotFound
certificateExpiresSoon
certificateInvalid
generalFailure
)
func (p probeResult) String() string {
switch p {
case certificateOK:
return "Certificate OK"
case certificateExpiresSoon:
return fmt.Sprintf("Certificate expires within %s", config.ExpireWarning)
case certificateInvalid:
return "Certificate invalid / intermediate certificates not present"
case certificateNotFound:
return "Did not find a certificate valid for this domain"
case generalFailure:
return "Something went wrong in the request"
}
return "" // This does not happen.
}
func checkCertificate(probeURL *url.URL) (probeResult, *x509.Certificate) {
req, _ := http.NewRequest("HEAD", probeURL.String(), nil)
req.Header.Set("User-Agent", fmt.Sprintf("Mozilla/5.0 (compatible; PromCertcheck/%s; +https://github.com/Luzifer/promcertcheck)", version))
resp, err := http.DefaultClient.Do(req)
switch err.(type) {
case nil, redirectFoundError:
default:
if !strings.Contains(err.Error(), "Found a redirect.") {
return generalFailure, nil
}
}
resp.Body.Close()
intermediatePool := x509.NewCertPool()
var verifyCert *x509.Certificate
for _, cert := range resp.TLS.PeerCertificates {
wildHost := "*" + probeURL.Host[strings.Index(probeURL.Host, "."):]
if !inSlice(cert.DNSNames, probeURL.Host) && !inSlice(cert.DNSNames, wildHost) {
intermediatePool.AddCert(cert)
continue
}
verifyCert = cert
}
if verifyCert == nil {
return certificateNotFound, nil
}
verificationResult := false
if _, err := verifyCert.Verify(x509.VerifyOptions{
Intermediates: intermediatePool,
}); err == nil {
verificationResult = true
}
if !verificationResult {
return certificateInvalid, verifyCert
}
if verifyCert.NotAfter.Sub(time.Now()) < config.expireWarning {
return certificateExpiresSoon, verifyCert
}
return certificateOK, verifyCert
}

66
display.html Normal file
View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Certificate validation results</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="row" style="height:20px"></div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
Certificate validation results
</div>
<div class="panel-body">
<table class="table table-striped">
<tr>
<th>Host</th><th>Issuer</th><th>Valid until</th><th>Result</th>
</tr>
{% for host, res in results sorted %}
{% if res.Status == certificateOK %}
<tr>
{% elif res.Status == certificateExpiresSoon %}
<tr class="warning">
{% else %}
<tr class="danger">
{% endif %}
<td>{{ host }}</td>
<td>{{ res.Certificate.Issuer.CommonName }}</td>
<td>{{ res.Certificate.NotAfter }}</td>
<td>{{ res.Status.String() }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="panel-footer">
<a href="https://github.com/Luzifer/promcertcheck">PromCertcheck {{ version }}</a>
</div>
</div>
</div>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>

26
http.go Normal file
View file

@ -0,0 +1,26 @@
package main
//go:generate go-bindata display.html
import (
"log"
"net/http"
"github.com/flosch/pongo2"
)
func httpHandler(res http.ResponseWriter, r *http.Request) {
tplsrc, _ := Asset("display.html")
template, err := pongo2.FromString(string(tplsrc))
if err != nil {
log.Fatal(err)
}
template.ExecuteWriter(pongo2.Context{
"results": probeMonitors,
"certificateOK": certificateOK,
"certificateExpiresSoon": certificateExpiresSoon,
"version": version,
}, res)
}

142
main.go Normal file
View file

@ -0,0 +1,142 @@
package main
import (
"crypto/x509"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/Luzifer/rconfig"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/robfig/cron"
)
var (
config = struct {
Debug bool `flag:"debug" default:"false" description:"Output debugging data"`
ExpireWarning string `flag:"expire-warning" default:"744h" description:"When to warn about a soon expiring certificate"`
Probes []string `flag:"probe" default:"" description:"URLs to check for certificate issues"`
expireWarning time.Duration
}{}
version = "dev"
probeMonitors = map[string]*probeMonitor{}
)
type probeMonitor struct {
IsValid prometheus.Gauge
Expires prometheus.Gauge
Status probeResult
Certificate *x509.Certificate
}
type redirectFoundError struct{}
func (r redirectFoundError) Error() string {
return "Found a redirect."
}
func init() {
var err error
rconfig.Parse(&config)
config.expireWarning, err = time.ParseDuration(config.ExpireWarning)
if err != nil {
log.Fatalf("You need to specify a valid expire-warning: %s", err)
}
}
func main() {
http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return redirectFoundError{}
}
registerProbes()
refreshCertificateStatus()
c := cron.New()
c.AddFunc("0 0 * * * *", refreshCertificateStatus)
c.Start()
r := mux.NewRouter()
r.Handle("/metrics", prometheus.Handler())
r.HandleFunc("/", httpHandler)
http.ListenAndServe(":3000", r)
}
func registerProbes() {
for _, probe := range config.Probes {
probeURL, _ := url.Parse(probe)
monitors := &probeMonitor{}
monitors.Expires = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "certcheck_expires",
Help: "Expiration date in unix timestamp (UTC)",
ConstLabels: prometheus.Labels{
"host": probeURL.Host,
},
})
monitors.IsValid = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "certcheck_valid",
Help: "Validity of the certificate (0/1)",
ConstLabels: prometheus.Labels{
"host": probeURL.Host,
},
})
prometheus.MustRegister(monitors.Expires)
prometheus.MustRegister(monitors.IsValid)
probeMonitors[probeURL.Host] = monitors
}
}
func refreshCertificateStatus() {
for _, probe := range config.Probes {
probeURL, _ := url.Parse(probe)
verificationResult, verifyCert := checkCertificate(probeURL)
if config.Debug {
fmt.Printf("---\nProbe: %s\nResult: %s\n",
probeURL.Host,
verificationResult,
)
if verifyCert != nil {
fmt.Printf("Version: %d\nSerial: %d\nSubject: %s\nExpires: %s\nIssuer: %s\nAlt Names: %s\n",
verifyCert.Version,
verifyCert.SerialNumber,
verifyCert.Subject.CommonName,
verifyCert.NotAfter,
verifyCert.Issuer.CommonName,
strings.Join(verifyCert.DNSNames, ", "),
)
}
}
probeMonitors[probeURL.Host].Expires.Set(float64(verifyCert.NotAfter.UTC().Unix()))
switch verificationResult {
case certificateExpiresSoon, certificateOK:
probeMonitors[probeURL.Host].IsValid.Set(1)
case certificateInvalid, certificateNotFound:
probeMonitors[probeURL.Host].IsValid.Set(0)
default:
probeMonitors[probeURL.Host].IsValid.Set(0)
}
probeMonitors[probeURL.Host].Status = verificationResult
probeMonitors[probeURL.Host].Certificate = verifyCert
}
}
func inSlice(slice []string, needle string) bool {
for _, i := range slice {
if i == needle {
return true
}
}
return false
}