79 lines
1.6 KiB
Go
79 lines
1.6 KiB
Go
package ssl
|
||
|
||
import (
|
||
"crypto/tls"
|
||
"fmt"
|
||
"net"
|
||
"time"
|
||
)
|
||
|
||
// Result holds the outcome of a single cert check.
|
||
type Result struct {
|
||
Domain string
|
||
ExpiresAt time.Time
|
||
DaysRemaining int
|
||
IsValid bool
|
||
Error string
|
||
}
|
||
|
||
// Severity returns a CSS class name for the expiry status.
|
||
//
|
||
// "ok" — > 30 days
|
||
// "warn" — 14–30 days
|
||
// "crit" — < 14 days or invalid
|
||
func (r Result) Severity() string {
|
||
if !r.IsValid {
|
||
return "crit"
|
||
}
|
||
switch {
|
||
case r.DaysRemaining > 30:
|
||
return "ok"
|
||
case r.DaysRemaining >= 14:
|
||
return "warn"
|
||
default:
|
||
return "crit"
|
||
}
|
||
}
|
||
|
||
// Check dials domain:443, retrieves the TLS certificate chain, and returns
|
||
// the expiry of the leaf certificate.
|
||
func Check(domain string) Result {
|
||
r := Result{Domain: domain}
|
||
|
||
conn, err := tls.DialWithDialer(
|
||
&net.Dialer{Timeout: 10 * time.Second},
|
||
"tcp",
|
||
net.JoinHostPort(domain, "443"),
|
||
&tls.Config{ServerName: domain},
|
||
)
|
||
if err != nil {
|
||
r.Error = fmt.Sprintf("tls dial: %s", err)
|
||
return r
|
||
}
|
||
defer conn.Close()
|
||
|
||
certs := conn.ConnectionState().PeerCertificates
|
||
if len(certs) == 0 {
|
||
r.Error = "no certificates in chain"
|
||
return r
|
||
}
|
||
|
||
leaf := certs[0]
|
||
now := time.Now()
|
||
|
||
r.ExpiresAt = leaf.NotAfter
|
||
r.DaysRemaining = int(leaf.NotAfter.Sub(now).Hours() / 24)
|
||
|
||
if now.Before(leaf.NotBefore) {
|
||
r.Error = fmt.Sprintf("certificate not yet valid (valid from %s)", leaf.NotBefore.Format("2006-01-02"))
|
||
return r
|
||
}
|
||
if now.After(leaf.NotAfter) {
|
||
r.Error = fmt.Sprintf("certificate expired %s", leaf.NotAfter.Format("2006-01-02"))
|
||
return r
|
||
}
|
||
|
||
r.IsValid = true
|
||
return r
|
||
}
|