From 79b0eb417b0431e316964e003a9efe6b6725b94a Mon Sep 17 00:00:00 2001 From: Sergey Poznyakoff Date: Wed, 9 Oct 2019 09:09:58 +0300 Subject: Rename to certmon. Eliminate alternate names to avoid spurious polling. --- .gitignore | 1 + certmon.go | 245 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ certwatch.go | 207 -------------------------------------------------- 3 files changed, 246 insertions(+), 207 deletions(-) create mode 100644 .gitignore create mode 100644 certmon.go delete mode 100644 certwatch.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0185e2f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +certmon diff --git a/certmon.go b/certmon.go new file mode 100644 index 0000000..155ff0f --- /dev/null +++ b/certmon.go @@ -0,0 +1,245 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "os" + "strings" + "fmt" + "time" + "flag" +) + +// Nagios status constants +const ( + StatusOK = iota + StatusWarning + StatusCritical + StatusUnknown +) + +var statusString = []string{StatusOK: `OK`, + StatusWarning: `WARNING`, + StatusCritical: `CRITICAL`, + StatusUnknown: `UNKNOWN`,} + +type CertResult struct { + Subject string + Status int + Ttl time.Duration + Error string +} + +type CertResultList struct { + Address string + Status int + Result []CertResult +} + +func CertMatch(cert *x509.Certificate, cn string) bool { + if cn == `` || cert.Subject.CommonName == cn { + return true + } + for _, name := range cert.DNSNames { + if cn == name { + return true + } + } + return false +} + +// Argument list +type ArgList struct { + args []string +} + +func NewArgList(a []string) *ArgList { + var args ArgList + if len(a) > 0 { + args.args = a + } else { + args.args = []string{``} + } + return &args +} + +func (a *ArgList) Next() (string) { + s := a.args[0] + a.args = a.args[1:] + return s +} + +func (a *ArgList) DropMatches(cert *x509.Certificate) { + for i := 0; i < len(a.args); { + if CertMatch(cert, a.args[i]) { + a.args = append(a.args[:i], a.args[i+1:]...) + } else { + i++ + } + } +} + +func (a *ArgList) More() bool { + return len(a.args) > 0 +} + +// Command line options +var warnLimit time.Duration +var critLimit time.Duration +var verboseOption bool +var helpOption bool +var quietOption bool +var host string + +// Intitialize command line parser +func init() { + flag.DurationVar(&warnLimit, `w`, 0, `warning threshold`) + flag.DurationVar(&critLimit, `c`, 0, `critical threshold`) + flag.BoolVar(&verboseOption, `v`, false, `verbose mode`) + flag.BoolVar(&helpOption, `h`, false, `show help summary`) + flag.StringVar(&host, `H`, ``, `host name`) + flag.BoolVar(&quietOption, `q`, false, `quiet mode: print nothing, exit with a meaningful status`) + flag.Usage = func() { + if helpOption { + flag.CommandLine.SetOutput(os.Stdout) + } + fmt.Fprintf(flag.CommandLine.Output(), + "Usage: %s [OPTIONS] [CN...]\n", + os.Args[0]) + fmt.Fprintln(flag.CommandLine.Output(), `OPTIONS are:`) + flag.PrintDefaults() + } +} + +func main() { + flag.Parse() + + if helpOption { + flag.Usage() + os.Exit(0) + } + if host == `` { + fmt.Fprintf(os.Stderr, "-H option is mandatory\n") + flag.Usage() + os.Exit(2) + } + + res := CertResultList{Address: host, Status: StatusOK} + + for args := NewArgList(flag.Args()); args.More(); { + res.Check(args) + } + if !quietOption { + res.Format() + } + os.Exit(res.Status) +} + +func (res CertResult) FormatHR() { + if res.Status == StatusUnknown { + fmt.Printf("%s - %s;", res.Subject, res.Error) + } else { + fmt.Printf("%s TTL %s;", res.Subject, res.Ttl.String()) + } +} + +func (res CertResult) FormatPerfData() { + fmt.Printf("%s=%d;%d;%d;;", + res.Subject, + int(res.Ttl.Seconds()), + int(warnLimit.Seconds()), + int(critLimit.Seconds())) +} + +func (rl CertResultList) Format() { + //'label'=value[UOM];[warn];[crit];[min];[max] + fmt.Printf("%s - %s ", statusString[rl.Status], rl.Address) + rl.Result[0].FormatHR() + fmt.Printf(" | ") + rl.Result[0].FormatPerfData() + if len(rl.Result) > 1 { + for _, res := range rl.Result[1:] { + fmt.Println() + res.FormatHR() + } + fmt.Printf(`|`) + for _, res := range rl.Result[1:] { + res.FormatPerfData() + fmt.Println() + } + } else { + fmt.Println() + } +} + +func (rl *CertResultList) Append(res CertResult) { + rl.Result = append(rl.Result, res) + if res.Status > rl.Status { + rl.Status = res.Status + } +} + +func (rl *CertResultList) Check(args *ArgList) { + cn := args.Next() + addr := rl.Address; + a := strings.Split(addr, `:`) + switch (len(a)) { + case 1: + addr += `:443` + case 2: + break + default: + rl.Append(CertResult{Status: StatusUnknown, + Error: `bad address`}) + return + } + + conf := &tls.Config { + InsecureSkipVerify: true, + ServerName: cn, + } + + conn, err := tls.Dial("tcp", addr, conf) + if err != nil { + rl.Append(CertResult{Subject: cn, + Status: StatusUnknown, + Error: err.Error()}) + return + } + defer conn.Close() + + state := conn.ConnectionState() + + for _, cert := range state.PeerCertificates { + if cert.IsCA { + continue + } + if cn == `` { + cn = cert.Subject.CommonName + } + if !CertMatch(cert, cn) { + continue + } + args.DropMatches(cert) + res := CertResult{Subject: cn, Status: StatusOK} + res.Ttl = time.Until(cert.NotAfter) + if res.Ttl < critLimit { + res.Status = StatusCritical + } else if res.Ttl < warnLimit { + res.Status = StatusWarning + } + rl.Append(res) + if (verboseOption) { + fmt.Printf("Host: %s\n", addr) + fmt.Printf("CN: %s\n", cert.Subject.CommonName) + fmt.Printf("DNS: %s\n", strings.Join(cert.DNSNames, `,`)) + fmt.Printf("Expires: %s\n", cert.NotAfter.String()) + fmt.Printf("Status: %s\n", statusString[res.Status]) + fmt.Println() + } + return + } + rl.Append(CertResult{Status: StatusUnknown, + Subject: cn, + Error: `No such CN`}) +} diff --git a/certwatch.go b/certwatch.go deleted file mode 100644 index 188de0c..0000000 --- a/certwatch.go +++ /dev/null @@ -1,207 +0,0 @@ -package main - -import ( - "crypto/tls" - "crypto/x509" - "os" - "strings" - "fmt" - "time" - "flag" -) - -// Nagios status constants -const ( - StatusOK = iota - StatusWarning - StatusCritical - StatusUnknown -) - -var statusString = []string{StatusOK: `OK`, - StatusWarning: `WARNING`, - StatusCritical: `CRITICAL`, - StatusUnknown: `UNKNOWN`,} - -type CertResult struct { - Subject string - Status int - Ttl time.Duration - Error string -} - -type CertResultList struct { - Address string - Status int - Result []CertResult -} - -func CertMatch(cert *x509.Certificate, cn string) bool { - if cn == `` || cert.Subject.CommonName == cn { - return true - } - for _, name := range cert.DNSNames { - if cn == name { - return true - } - } - return false -} - -// Command line options -var warnLimit time.Duration -var critLimit time.Duration -var verboseOption bool -var helpOption bool -var host string - -// Intitialize command line parser -func init() { - flag.DurationVar(&warnLimit, `w`, 0, `warning threshold`) - flag.DurationVar(&critLimit, `c`, 0, `critical threshold`) - flag.BoolVar(&verboseOption, `v`, false, `verbose mode`) - flag.BoolVar(&helpOption, `h`, false, `show help summary`) - flag.StringVar(&host, `H`, ``, `host name`) - flag.Usage = func() { - if helpOption { - flag.CommandLine.SetOutput(os.Stdout) - } - fmt.Fprintf(flag.CommandLine.Output(), - "Usage: %s [OPTIONS] [CN...]\n", - os.Args[0]) - fmt.Fprintln(flag.CommandLine.Output(), `OPTIONS are:`) - flag.PrintDefaults() - } -} - -func main() { - flag.Parse() - - if helpOption { - flag.Usage() - os.Exit(0) - } - if host == `` { - fmt.Fprintf(os.Stderr, "-H option is mandatory\n") - flag.Usage() - os.Exit(2) - } - - res := CertResultList{Address: host, Status: StatusOK} - if len(flag.Args()) > 0 { - for _, cn := range flag.Args() { - res.Check(cn) - } - } else { - res.Check(``) - } - res.Format() - os.Exit(res.Status) -} - -func (res CertResult) FormatHR() { - if res.Status == StatusUnknown { - fmt.Printf("%s - %s;", res.Subject, res.Error) - } else { - fmt.Printf("%s TTL %s;", res.Subject, res.Ttl.String()) - } -} - -func (res CertResult) FormatPerfData() { - fmt.Printf("%s=%d;%d;%d;;", - res.Subject, - int(res.Ttl.Seconds()), - int(warnLimit.Seconds()), - int(critLimit.Seconds())) -} - -func (rl CertResultList) Format() { - //'label'=value[UOM];[warn];[crit];[min];[max] - fmt.Printf("%s - %s ", statusString[rl.Status], rl.Address) - rl.Result[0].FormatHR() - fmt.Printf(" | ") - rl.Result[0].FormatPerfData() - if len(rl.Result) > 1 { - for _, res := range rl.Result[1:] { - fmt.Println() - res.FormatHR() - } - fmt.Printf(`|`) - for _, res := range rl.Result[1:] { - res.FormatPerfData() - fmt.Println() - } - } else { - fmt.Println() - } -} - -func (rl *CertResultList) Append(res CertResult) { - rl.Result = append(rl.Result, res) - if res.Status > rl.Status { - rl.Status = res.Status - } -} - -func (rl *CertResultList) Check(cn string) { - addr := rl.Address; - a := strings.Split(addr, `:`) - switch (len(a)) { - case 1: - addr += `:443` - case 2: - break - default: - rl.Append(CertResult{Status: StatusUnknown, - Error: `bad address`}) - return - } - - conf := &tls.Config { - InsecureSkipVerify: true, - ServerName: cn, - } - - conn, err := tls.Dial("tcp", addr, conf) - if err != nil { - rl.Append(CertResult{Subject: cn, - Status: StatusUnknown, - Error: err.Error()}) - return - } - defer conn.Close() - - state := conn.ConnectionState() - - for _, cert := range state.PeerCertificates { - if cert.IsCA { - continue - } - if cn == `` { - cn = cert.Subject.CommonName - } - if !CertMatch(cert, cn) { - continue - } - res := CertResult{Subject: cn, Status: StatusOK} - res.Ttl = time.Until(cert.NotAfter) - if res.Ttl < critLimit { - res.Status = StatusCritical - } else if res.Ttl < warnLimit { - res.Status = StatusWarning - } - rl.Append(res) - if (verboseOption) { - fmt.Printf("Host: %s\n", addr) - fmt.Printf("CN: %s\n", cert.Subject.CommonName) - fmt.Printf("DNS: %s\n", strings.Join(cert.DNSNames, `,`)) - fmt.Printf("Expires: %s\n", cert.NotAfter.String()) - fmt.Printf("Status: %s\n", statusString[res.Status]) - fmt.Println() - } - return - } - rl.Append(CertResult{Status: StatusUnknown, - Subject: cn, - Error: `No such CN`}) -} -- cgit v1.2.1