132 lines
3.8 KiB
Go
132 lines
3.8 KiB
Go
package checker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// TLSRelatedKey is the observation key the downstream TLS checker
|
|
// publishes. Same value as the XMPP checker uses, by cross-checker
|
|
// convention.
|
|
const TLSRelatedKey sdk.ObservationKey = "tls_probes"
|
|
|
|
// tlsProbeView is our permissive view of a TLS checker's payload — we
|
|
// read only the fields we need.
|
|
type tlsProbeView struct {
|
|
Host string `json:"host,omitempty"`
|
|
Port uint16 `json:"port,omitempty"`
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
TLSVersion string `json:"tls_version,omitempty"`
|
|
CipherSuite string `json:"cipher_suite,omitempty"`
|
|
HostnameMatch *bool `json:"hostname_match,omitempty"`
|
|
ChainValid *bool `json:"chain_valid,omitempty"`
|
|
NotAfter time.Time `json:"not_after,omitempty"`
|
|
Issues []struct {
|
|
Code string `json:"code"`
|
|
Severity string `json:"severity"`
|
|
Message string `json:"message,omitempty"`
|
|
Fix string `json:"fix,omitempty"`
|
|
} `json:"issues,omitempty"`
|
|
}
|
|
|
|
// address returns "host:port" used as our matching key against SIP
|
|
// endpoints. Falls back to Endpoint when host/port are unset.
|
|
func (v *tlsProbeView) address() string {
|
|
if v.Endpoint != "" {
|
|
return v.Endpoint
|
|
}
|
|
if v.Host != "" && v.Port != 0 {
|
|
return net.JoinHostPort(v.Host, strconv.Itoa(int(v.Port)))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseTLSRelated(r sdk.RelatedObservation) *tlsProbeView {
|
|
var v tlsProbeView
|
|
if err := json.Unmarshal(r.Data, &v); err != nil {
|
|
return nil
|
|
}
|
|
return &v
|
|
}
|
|
|
|
// tlsIssuesFromRelated converts downstream TLS observations into Issue
|
|
// entries for the SIP aggregation. Structured issues from the TLS
|
|
// checker are forwarded with a "sip.tls." prefix so origin is obvious;
|
|
// flag-only payloads are summarised into one synthesised issue.
|
|
func tlsIssuesFromRelated(related []sdk.RelatedObservation) []Issue {
|
|
var out []Issue
|
|
for _, r := range related {
|
|
v := parseTLSRelated(r)
|
|
if v == nil {
|
|
continue
|
|
}
|
|
addr := v.address()
|
|
if len(v.Issues) > 0 {
|
|
for _, is := range v.Issues {
|
|
sev := strings.ToLower(is.Severity)
|
|
switch sev {
|
|
case SeverityCrit, SeverityWarn, SeverityInfo:
|
|
default:
|
|
continue
|
|
}
|
|
code := is.Code
|
|
if code == "" {
|
|
code = "tls.unknown"
|
|
}
|
|
out = append(out, Issue{
|
|
Code: "sip.tls." + code,
|
|
Severity: sev,
|
|
Message: strings.TrimSpace("TLS on " + addr + ": " + is.Message),
|
|
Fix: is.Fix,
|
|
Endpoint: addr,
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
// Flag-only payload: synthesise a summary issue.
|
|
sev := v.worstSeverity()
|
|
if sev == "" {
|
|
continue
|
|
}
|
|
msg := "TLS issue reported on " + addr
|
|
switch {
|
|
case v.ChainValid != nil && !*v.ChainValid:
|
|
msg = "Invalid certificate chain on " + addr
|
|
case v.HostnameMatch != nil && !*v.HostnameMatch:
|
|
msg = "Certificate does not cover the SIP host on " + addr
|
|
case !v.NotAfter.IsZero() && time.Until(v.NotAfter) < 0:
|
|
msg = "Certificate expired on " + addr + " (" + v.NotAfter.Format(time.RFC3339) + ")"
|
|
case !v.NotAfter.IsZero() && time.Until(v.NotAfter) < 14*24*time.Hour:
|
|
msg = "Certificate expiring soon on " + addr + " (" + v.NotAfter.Format(time.RFC3339) + ")"
|
|
}
|
|
out = append(out, Issue{
|
|
Code: "sip.tls.probe",
|
|
Severity: sev,
|
|
Message: msg,
|
|
Fix: "See the TLS checker report for details.",
|
|
Endpoint: addr,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (v *tlsProbeView) worstSeverity() string {
|
|
if v.ChainValid != nil && !*v.ChainValid {
|
|
return SeverityCrit
|
|
}
|
|
if v.HostnameMatch != nil && !*v.HostnameMatch {
|
|
return SeverityCrit
|
|
}
|
|
if !v.NotAfter.IsZero() && time.Until(v.NotAfter) < 0 {
|
|
return SeverityCrit
|
|
}
|
|
if !v.NotAfter.IsZero() && time.Until(v.NotAfter) < 14*24*time.Hour {
|
|
return SeverityWarn
|
|
}
|
|
return ""
|
|
}
|