commit 66a1b017be02f9a679e724644f7bcf59ef38d969 Author: Emmanuel BenoƮt Date: Sun Jan 3 10:33:21 2021 +0100 Initial import of lib + certificate validity checker diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba077a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..671bfc1 --- /dev/null +++ b/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e +cd "$(dirname $0)" +mkdir -p bin +for d in $(find cmd -mindepth 1 -maxdepth 1 -type d); do + cd $d + xn="$(basename "$d")" + go build + /bin/mv "$xn" ../../bin +done diff --git a/cmd/check_ssl_certificate/main.go b/cmd/check_ssl_certificate/main.go new file mode 100644 index 0000000..0c7f92a --- /dev/null +++ b/cmd/check_ssl_certificate/main.go @@ -0,0 +1,122 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "strings" + "time" + + "nocternity.net/monitoring/perfdata" + "nocternity.net/monitoring/plugin" +) + +type cliFlags struct { + hostname string + port int + warn int + crit int + ignoreCnOnly bool +} + +func handleCli(p *plugin.Plugin) (flags *cliFlags) { + flags = &cliFlags{} + flag.StringVar(&flags.hostname, "hostname", "", "Host name to connect to.") + flag.StringVar(&flags.hostname, "H", "", "Host name to connect to (shorthand).") + flag.IntVar(&flags.port, "port", -1, "Port to connect to.") + flag.IntVar(&flags.port, "P", -1, "Port to connect to (shorthand).") + flag.IntVar(&flags.warn, "warning", -1, "Validity threshold below which a warning state is issued, in days.") + flag.IntVar(&flags.warn, "W", -1, "Validity threshold below which a warning state is issued, in days (shorthand).") + flag.IntVar(&flags.crit, "critical", -1, "Validity threshold below which a critical state is issued, in days.") + flag.IntVar(&flags.crit, "C", -1, "Validity threshold below which a critical state is issued, in days (shorthand).") + flag.BoolVar(&flags.ignoreCnOnly, "ignore-cn-only", false, + "Do not issue warnings regarding certificates that do not use SANs at all.") + flag.Parse() + return +} + +func checkFlags(p *plugin.Plugin, flags *cliFlags) bool { + if flags.hostname == "" { + p.SetState(plugin.UNKNOWN, "no hostname specified") + return false + } + if flags.port < 1 || flags.port > 65535 { + p.SetState(plugin.UNKNOWN, "invalid or missing port number") + return false + } + if flags.warn != -1 && flags.crit != -1 && flags.warn <= flags.crit { + p.SetState(plugin.UNKNOWN, "nonsensical thresholds") + return false + } + flags.hostname = strings.ToLower(flags.hostname) + return true +} + +func findHostname(cert *x509.Certificate, hostname string) bool { + for _, name := range cert.DNSNames { + if strings.ToLower(name) == hostname { + return true + } + } + return false +} + +func main() { + p := plugin.New("Certificate check") + defer p.Done() + flags := handleCli(p) + if !checkFlags(p, flags) { + return + } + + tls_cfg := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS10, + } + tls_conn, tls_err := tls.Dial("tcp", fmt.Sprintf("%s:%d", flags.hostname, flags.port), tls_cfg) + if tls_err != nil { + p.SetState(plugin.UNKNOWN, fmt.Sprintf("connection failed: %s", tls_err)) + return + } + defer tls_conn.Close() + if hsk_err := tls_conn.Handshake(); hsk_err != nil { + p.SetState(plugin.UNKNOWN, fmt.Sprintf("handshake failed: %s", hsk_err)) + return + } + certificate := tls_conn.ConnectionState().PeerCertificates[0] + + if len(certificate.DNSNames) == 0 { + if !flags.ignoreCnOnly { + p.SetState(plugin.WARNING, "certificate doesn't have SAN domain names") + return + } + dn := strings.ToLower(certificate.Subject.String()) + if !strings.HasPrefix(dn, fmt.Sprintf("cn=%s,", flags.hostname)) { + p.SetState(plugin.ERROR, "incorrect certificate CN") + return + } + } else if !findHostname(certificate, flags.hostname) { + p.SetState(plugin.ERROR, "host name not found in SAN domain names") + return + } + timeLeft := certificate.NotAfter.Sub(time.Now()) + tlDays := int((timeLeft + 86399*time.Second) / (24 * time.Hour)) + if flags.crit > 0 && tlDays <= flags.crit { + p.SetState(plugin.ERROR, fmt.Sprintf("certificate will expire in %d days (<= %d)", tlDays, flags.crit)) + } else if flags.warn > 0 && tlDays <= flags.warn { + p.SetState(plugin.WARNING, fmt.Sprintf("certificate will expire in %d days (<= %d)", tlDays, flags.warn)) + } else { + p.SetState(plugin.OK, fmt.Sprintf("certificate will expire in %d days", tlDays)) + } + + var pdat perfdata.PerfData + pdat = perfdata.New("validity", perfdata.UOM_NONE, fmt.Sprintf("%d", tlDays)) + if flags.crit > 0 { + pdat.SetCrit(perfdata.PDRMax(fmt.Sprint(flags.crit))) + } + if flags.warn > 0 { + pdat.SetWarn(perfdata.PDRMax(fmt.Sprint(flags.warn))) + } + p.AddPerfData(pdat) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c5a9fc8 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module nocternity.net/monitoring + +go 1.15 diff --git a/perfdata/perfdata.go b/perfdata/perfdata.go new file mode 100644 index 0000000..a08b29b --- /dev/null +++ b/perfdata/perfdata.go @@ -0,0 +1,171 @@ +package perfdata + +import ( + "fmt" + "regexp" + "strings" +) + +type UnitOfMeasurement int + +const ( + UOM_NONE UnitOfMeasurement = iota + UOM_SECONDS + UOM_PERCENT + UOM_BYTES + UOM_KILOBYTES + UOM_MEGABYTES + UOM_GIGABYTES + UOM_TERABYTES + UOM_COUNTER +) + +func (u UnitOfMeasurement) String() string { + return [...]string{"", "s", "%", "B", "KB", "MB", "GB", "TB", "c"}[u] +} + +type PerfDataBits int + +const ( + PDAT_WARN PerfDataBits = 1 << iota + PDAT_CRIT + PDAT_MIN + PDAT_MAX +) + +var ( + valueCheck = regexp.MustCompile(`^-?(0(\.\d*)?|[1-9]\d*(\.\d*)?|\.\d+)$`) + rangeMinCheck = regexp.MustCompile(`^-?(0(\.\d*)?|[1-9]\d*(\.\d*)?|\.\d+)$|^~$`) +) + +type PerfDataRange struct { + start string + end string + inside bool +} + +func PDRMax(max string) *PerfDataRange { + if !valueCheck.MatchString(max) { + panic("invalid performance data range maximum value") + } + r := new(PerfDataRange) + r.start = "0" + r.end = max + return r +} + +func PDRMinMax(min, max string) *PerfDataRange { + if !valueCheck.MatchString(max) { + panic("invalid performance data range maximum value") + } + if !rangeMinCheck.MatchString(min) { + panic("invalid performance data range minimum value") + } + r := new(PerfDataRange) + r.start = min + r.end = max + return r +} + +func (r *PerfDataRange) Inside() *PerfDataRange { + r.inside = true + return r +} + +func (r PerfDataRange) String() string { + var start, inside string + if r.start == "" { + start = "~" + } else if r.start == "0" { + start = "" + } else { + start = r.start + } + if r.inside { + inside = "@" + } else { + inside = "" + } + return fmt.Sprintf("%s%s:%s", inside, start, r.end) +} + +type PerfData struct { + Label string + units UnitOfMeasurement + bits PerfDataBits + value string + warn, crit PerfDataRange + min, max string +} + +func New(label string, units UnitOfMeasurement, value string) PerfData { + if value != "" && !valueCheck.MatchString(value) { + panic("invalid value") + } + r := PerfData{} + r.Label = label + r.units = units + if value == "" { + r.value = "U" + } else { + r.value = value + } + return r +} + +func (d *PerfData) SetWarn(r *PerfDataRange) { + d.warn = *r + d.bits = d.bits | PDAT_WARN +} + +func (d *PerfData) SetCrit(r *PerfDataRange) { + d.crit = *r + d.bits = d.bits | PDAT_CRIT +} + +func (d *PerfData) SetMin(min string) { + if !valueCheck.MatchString(min) { + panic("invalid value") + } + d.min = min + d.bits = d.bits | PDAT_MIN +} + +func (d *PerfData) SetMax(max string) { + if !valueCheck.MatchString(max) { + panic("invalid value") + } + d.max = max + d.bits = d.bits | PDAT_MAX +} + +func (d PerfData) String() string { + var sb strings.Builder + needsQuotes := strings.ContainsAny(d.Label, " '=\"") + if needsQuotes { + sb.WriteString("'") + } + sb.WriteString(strings.ReplaceAll(d.Label, "'", "''")) + if needsQuotes { + sb.WriteString("'") + } + sb.WriteString("=") + sb.WriteString(fmt.Sprintf("%s%s;", d.value, d.units.String())) + if d.bits&PDAT_WARN != 0 { + sb.WriteString(d.warn.String()) + } + sb.WriteString(";") + if d.bits&PDAT_CRIT != 0 { + sb.WriteString(d.crit.String()) + } + sb.WriteString(";") + if d.bits&PDAT_MIN != 0 { + sb.WriteString(d.min) + } + sb.WriteString(";") + if d.bits&PDAT_MAX != 0 { + sb.WriteString(d.max) + } + + return sb.String() +} diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 0000000..68b1df7 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,95 @@ +package plugin + +import ( + "container/list" + "fmt" + "nocternity.net/monitoring/perfdata" + "os" + "strings" +) + +type Status int + +const ( + OK Status = iota + WARNING + ERROR + UNKNOWN +) + +func (s Status) String() string { + return [...]string{"OK", "WARNING", "ERROR", "UNKNOWN"}[s] +} + +type Plugin struct { + name string + status Status + message string + extraText *list.List + perfData map[string]perfdata.PerfData +} + +func New(name string) *Plugin { + p := new(Plugin) + p.name = name + p.status = UNKNOWN + p.message = "no status set" + p.perfData = make(map[string]perfdata.PerfData) + return p +} + +func (p *Plugin) SetState(status Status, message string) { + p.status = status + p.message = message + p.extraText = nil +} + +func (p *Plugin) AddLine(line string) { + if p.extraText == nil { + p.extraText = list.New() + } + p.extraText.PushBack(line) +} + +func (p *Plugin) AddLines(lines []string) { + for _, line := range lines { + p.AddLine(line) + } +} + +func (p *Plugin) AddPerfData(pd perfdata.PerfData) { + _, exists := p.perfData[pd.Label] + if exists { + panic(fmt.Sprintf("duplicate performance data %s", pd.Label)) + } + p.perfData[pd.Label] = pd +} + +func (p *Plugin) Done() { + var sb strings.Builder + sb.WriteString(p.name) + sb.WriteString(" ") + sb.WriteString(p.status.String()) + sb.WriteString(": ") + sb.WriteString(p.message) + if len(p.perfData) > 0 { + sb.WriteString(" | ") + needSep := false + for k := range p.perfData { + if needSep { + sb.WriteString(", ") + } else { + needSep = true + } + sb.WriteString(p.perfData[k].String()) + } + } + if p.extraText != nil { + for em := p.extraText.Front(); em != nil; em = em.Next() { + sb.WriteString("\n") + sb.WriteString(em.Value.(string)) + } + } + fmt.Println(sb.String()) + os.Exit(int(p.status)) +}