Initial import of lib + certificate validity checker
This commit is contained in:
commit
66a1b017be
6 changed files with 403 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
bin
|
11
build.sh
Executable file
11
build.sh
Executable file
|
@ -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
|
122
cmd/check_ssl_certificate/main.go
Normal file
122
cmd/check_ssl_certificate/main.go
Normal file
|
@ -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)
|
||||||
|
}
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module nocternity.net/monitoring
|
||||||
|
|
||||||
|
go 1.15
|
171
perfdata/perfdata.go
Normal file
171
perfdata/perfdata.go
Normal file
|
@ -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()
|
||||||
|
}
|
95
plugin/plugin.go
Normal file
95
plugin/plugin.go
Normal file
|
@ -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))
|
||||||
|
}
|
Loading…
Reference in a new issue