191 lines
4.8 KiB
Go
191 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"net/mail"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
cli "github.com/urfave/cli/v3"
|
|
)
|
|
|
|
func main() {
|
|
cmd := &cli.Command{
|
|
Name: "too-soon",
|
|
Usage: "check if certificates expire too soon",
|
|
Version: "0.1",
|
|
Authors: []any{
|
|
&mail.Address{Name: "Vincent Batts", Address: "vbatts@hashbangbash.com"},
|
|
},
|
|
Flags: []cli.Flag{
|
|
&cli.IntFlag{
|
|
Name: "days",
|
|
Value: 20,
|
|
Usage: "days within range to alert about",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "debug",
|
|
Aliases: []string{"D"},
|
|
Value: false,
|
|
Usage: "output debug verbose info",
|
|
},
|
|
},
|
|
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
|
if cmd.Bool("debug") {
|
|
log.SetLevel(log.DebugLevel)
|
|
}
|
|
return ctx, nil
|
|
},
|
|
Commands: []*cli.Command{
|
|
&cli.Command{
|
|
Name: "pem",
|
|
Usage: "check if PEM certificate files expire too soon",
|
|
Action: fPEMCheck,
|
|
ArgsUsage: "[PEM files...]",
|
|
},
|
|
&cli.Command{
|
|
Name: "remote",
|
|
Usage: "check if certificate on a remote host expires too soon",
|
|
Action: fHostCheck,
|
|
ArgsUsage: "[host/host:port...]",
|
|
},
|
|
},
|
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
|
cli.ShowAppHelpAndExit(cmd, 1)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func fHostCheck(ctx context.Context, cmd *cli.Command) error {
|
|
numAlert := 0
|
|
numHostErr := 0
|
|
for i := 0; i <= cmd.Args().Len(); i++ {
|
|
host := cmd.Args().Get(i)
|
|
if host == "" {
|
|
break
|
|
}
|
|
|
|
// TODO use the url.Parse package to do this, incase there is a protocol and path on there as well
|
|
port := "443"
|
|
if strings.Contains(host, ":") {
|
|
chunks := strings.Split(host, ":")
|
|
host = chunks[0]
|
|
port = chunks[1]
|
|
}
|
|
// connect to the domain:port/tcp
|
|
dest := fmt.Sprintf("%s:%s", host, port)
|
|
conn, err := tls.Dial("tcp", dest, &tls.Config{
|
|
InsecureSkipVerify: true, // TODO add a flag to force this
|
|
})
|
|
if err != nil {
|
|
numHostErr++
|
|
log.Errorf("%q connection failed: %s", dest, err)
|
|
continue
|
|
}
|
|
defer conn.Close()
|
|
|
|
log.Debugf("server: %s", conn.ConnectionState().ServerName)
|
|
log.Debugf("version: %d", conn.ConnectionState().Version)
|
|
for _, cert := range conn.ConnectionState().PeerCertificates {
|
|
log.Debugf(" -- cert serial: %x", cert.SerialNumber)
|
|
if len(cert.DNSNames) == 0 {
|
|
log.Debugf(" -- cert: skipping as there are no DNS names")
|
|
continue
|
|
}
|
|
|
|
hours := time.Duration(cmd.Int("days") * -24)
|
|
alertTime := cert.NotAfter.Add(hours * time.Hour)
|
|
today := time.Now()
|
|
if today.After(alertTime) {
|
|
if today.After(cert.NotAfter) {
|
|
log.Warnf("%q(%x) : TIME TO RENEW CERTIFICATE (already expired!)", dest, cert.SerialNumber)
|
|
} else {
|
|
log.Warnf("%q(%x) : TIME TO RENEW CERTIFICATE (expires in less than %d days)", dest, cert.SerialNumber, cmd.Int("days"))
|
|
}
|
|
log.Warnf("%q(%x) : %v", dest, cert.SerialNumber, cert.NotAfter)
|
|
log.Warnf("%q(%x) : %v", dest, cert.SerialNumber, cert.DNSNames)
|
|
numAlert++
|
|
} else {
|
|
log.Debugf("%q(%x) : %v", dest, cert.SerialNumber, cert.NotAfter)
|
|
log.Debugf("%q(%x) : %v", dest, cert.SerialNumber, cert.DNSNames)
|
|
}
|
|
}
|
|
}
|
|
if numAlert != 0 {
|
|
return cli.Exit("domain certificates need to be renewed", numAlert)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fPEMCheck(ctx context.Context, cmd *cli.Command) error {
|
|
numAlert := 0
|
|
for i := 0; i <= cmd.Args().Len(); i++ {
|
|
file := cmd.Args().Get(i)
|
|
if file == "" {
|
|
break
|
|
}
|
|
|
|
var certs []*x509.Certificate
|
|
|
|
buf, err := os.ReadFile(file)
|
|
if err != nil {
|
|
log.Errorf("%q could not be read: %s", file, err)
|
|
continue
|
|
}
|
|
|
|
more := true
|
|
for more {
|
|
block, rest := pem.Decode(buf)
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
log.Errorf("%q cert could not be parsed: %s", file, err)
|
|
continue
|
|
}
|
|
certs = append(certs, cert)
|
|
|
|
if len(rest) == 0 {
|
|
more = false
|
|
}
|
|
// reset the buffer if there is more
|
|
buf = rest
|
|
}
|
|
|
|
for _, cert := range certs {
|
|
if len(cert.DNSNames) == 0 {
|
|
continue
|
|
}
|
|
|
|
hours := time.Duration(cmd.Int("days") * -24)
|
|
alertTime := cert.NotAfter.Add(hours * time.Hour)
|
|
today := time.Now()
|
|
if today.After(alertTime) {
|
|
if today.After(cert.NotAfter) {
|
|
log.Warnf("%q : TIME TO RENEW CERTIFICATE (already expired!)", file)
|
|
} else {
|
|
log.Warnf("%q : TIME TO RENEW CERTIFICATE (expires in less than %d days)", file, cmd.Int("days"))
|
|
}
|
|
log.Warnf("%q : %v", file, cert.NotAfter)
|
|
log.Warnf("%q : %v", file, cert.DNSNames)
|
|
numAlert++
|
|
} else {
|
|
log.Debugf("%q : %v", file, cert.NotAfter)
|
|
log.Debugf("%q : %v", file, cert.DNSNames)
|
|
}
|
|
}
|
|
}
|
|
if numAlert != 0 {
|
|
return cli.Exit("certificates need to be renewed", numAlert)
|
|
}
|
|
return nil
|
|
}
|