120 lines
3.4 KiB
Go
120 lines
3.4 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// tlsVersionRule flags endpoints negotiating a protocol version below the
|
|
// recommended TLS 1.2 floor.
|
|
type tlsVersionRule struct{}
|
|
|
|
func (r *tlsVersionRule) Name() string { return "tls.version" }
|
|
func (r *tlsVersionRule) Description() string {
|
|
return "Flags endpoints negotiating a TLS version below the recommended TLS 1.2."
|
|
}
|
|
|
|
func (r *tlsVersionRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
if len(data.Probes) == 0 {
|
|
return []sdk.CheckState{emptyCaseState("tls.version.no_endpoints")}
|
|
}
|
|
|
|
var out []sdk.CheckState
|
|
any := false
|
|
for _, ref := range sortedRefs(data) {
|
|
p := data.Probes[ref]
|
|
if p.TLSVersionNum == 0 {
|
|
continue
|
|
}
|
|
any = true
|
|
if p.TLSVersionNum >= tls.VersionTLS12 {
|
|
continue
|
|
}
|
|
out = append(out, sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Code: "tls.version.weak",
|
|
Subject: subjectOf(p),
|
|
Message: fmt.Sprintf("Negotiated TLS version %s is below the recommended TLS 1.2.", p.TLSVersion),
|
|
Meta: metaOf(p),
|
|
})
|
|
}
|
|
if !any {
|
|
return []sdk.CheckState{unknownState(
|
|
"tls.version.skipped",
|
|
"No endpoint completed a TLS handshake.",
|
|
)}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{passState(
|
|
"tls.version.ok",
|
|
"Every endpoint negotiates TLS 1.2 or higher.",
|
|
)}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// cipherSuiteRule reports the negotiated cipher suite for visibility.
|
|
// It does not currently classify suites as weak/strong: go's crypto/tls
|
|
// refuses to negotiate the known-weak suites anyway. The rule exists so the
|
|
// UI can expose the suite in the passing-list rather than leaving it buried
|
|
// in the raw observation.
|
|
type cipherSuiteRule struct{}
|
|
|
|
func (r *cipherSuiteRule) Name() string { return "tls.cipher_suite" }
|
|
func (r *cipherSuiteRule) Description() string {
|
|
return "Reports the cipher suite negotiated on each endpoint."
|
|
}
|
|
|
|
func (r *cipherSuiteRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
if len(data.Probes) == 0 {
|
|
return []sdk.CheckState{emptyCaseState("tls.cipher_suite.no_endpoints")}
|
|
}
|
|
|
|
// Collapse per-endpoint cipher suites into a single info state. One
|
|
// row per endpoint drowns out actionable rules in the UI on domains
|
|
// with many endpoints; an aggregated list is enough for visibility.
|
|
suites := map[string]int{}
|
|
endpoints := map[string][]string{}
|
|
for _, ref := range sortedRefs(data) {
|
|
p := data.Probes[ref]
|
|
if p.CipherSuite == "" {
|
|
continue
|
|
}
|
|
suites[p.CipherSuite]++
|
|
endpoints[p.CipherSuite] = append(endpoints[p.CipherSuite], p.Endpoint)
|
|
}
|
|
if len(suites) == 0 {
|
|
return []sdk.CheckState{unknownState(
|
|
"tls.cipher_suite.skipped",
|
|
"No endpoint completed a TLS handshake.",
|
|
)}
|
|
}
|
|
names := make([]string, 0, len(suites))
|
|
for s := range suites {
|
|
names = append(names, s)
|
|
}
|
|
sort.Strings(names)
|
|
parts := make([]string, 0, len(names))
|
|
for _, n := range names {
|
|
parts = append(parts, fmt.Sprintf("%s (%d)", n, suites[n]))
|
|
}
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusInfo,
|
|
Code: "tls.cipher_suite.negotiated",
|
|
Message: "Negotiated cipher suites: " + strings.Join(parts, ", "),
|
|
Meta: map[string]any{"suites": endpoints},
|
|
}}
|
|
}
|