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 }