Split monolithic rule into per-test rules, collect gathers facts only
This commit is contained in:
parent
5b71e85f49
commit
4177fcdc7b
14 changed files with 758 additions and 259 deletions
233
checker/rules_certificate.go
Normal file
233
checker/rules_certificate.go
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// peerCertificateRule flags successful handshakes in which the server sent
|
||||
// no certificate. This is distinct from chain validity: if no cert was sent,
|
||||
// hostname/chain/expiry cannot be evaluated.
|
||||
type peerCertificateRule struct{}
|
||||
|
||||
func (r *peerCertificateRule) Name() string { return "tls.peer_certificate_present" }
|
||||
func (r *peerCertificateRule) Description() string {
|
||||
return "Verifies the server presented a certificate during the TLS handshake."
|
||||
}
|
||||
|
||||
func (r *peerCertificateRule) 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.peer_certificate_present.no_endpoints")}
|
||||
}
|
||||
|
||||
var out []sdk.CheckState
|
||||
anyHandshake := false
|
||||
for _, ref := range sortedRefs(data) {
|
||||
p := data.Probes[ref]
|
||||
if !p.TLSHandshakeOK {
|
||||
continue
|
||||
}
|
||||
anyHandshake = true
|
||||
if !p.NoPeerCert {
|
||||
continue
|
||||
}
|
||||
out = append(out, sdk.CheckState{
|
||||
Status: sdk.StatusCrit,
|
||||
Code: "tls.peer_certificate_present.missing",
|
||||
Subject: subjectOf(p),
|
||||
Message: fmt.Sprintf("Server on %s completed the handshake but presented no certificate.", p.Endpoint),
|
||||
Meta: metaOf(p),
|
||||
})
|
||||
}
|
||||
if !anyHandshake {
|
||||
return []sdk.CheckState{unknownState(
|
||||
"tls.peer_certificate_present.skipped",
|
||||
"No endpoint completed a TLS handshake.",
|
||||
)}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []sdk.CheckState{passState(
|
||||
"tls.peer_certificate_present.ok",
|
||||
"Every endpoint presented a certificate.",
|
||||
)}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// chainValidityRule flags invalid certificate chains.
|
||||
type chainValidityRule struct{}
|
||||
|
||||
func (r *chainValidityRule) Name() string { return "tls.chain_validity" }
|
||||
func (r *chainValidityRule) Description() string {
|
||||
return "Verifies the presented certificate chain validates against the system trust store."
|
||||
}
|
||||
|
||||
func (r *chainValidityRule) 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.chain_validity.no_endpoints")}
|
||||
}
|
||||
|
||||
var out []sdk.CheckState
|
||||
any := false
|
||||
for _, ref := range sortedRefs(data) {
|
||||
p := data.Probes[ref]
|
||||
if p.ChainValid == nil {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
if *p.ChainValid {
|
||||
continue
|
||||
}
|
||||
msg := "Invalid certificate chain"
|
||||
if p.ChainVerifyErr != "" {
|
||||
msg = "Invalid certificate chain: " + p.ChainVerifyErr
|
||||
}
|
||||
out = append(out, sdk.CheckState{
|
||||
Status: sdk.StatusCrit,
|
||||
Code: "tls.chain_validity.invalid",
|
||||
Subject: subjectOf(p),
|
||||
Message: msg,
|
||||
Meta: metaOf(p),
|
||||
})
|
||||
}
|
||||
if !any {
|
||||
return []sdk.CheckState{unknownState(
|
||||
"tls.chain_validity.skipped",
|
||||
"No endpoint yielded a certificate chain to verify.",
|
||||
)}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []sdk.CheckState{passState(
|
||||
"tls.chain_validity.ok",
|
||||
"Every presented chain validates against the system trust store.",
|
||||
)}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// hostnameMatchRule flags endpoints whose leaf cert does not cover the SNI
|
||||
// the probe used.
|
||||
type hostnameMatchRule struct{}
|
||||
|
||||
func (r *hostnameMatchRule) Name() string { return "tls.hostname_match" }
|
||||
func (r *hostnameMatchRule) Description() string {
|
||||
return "Verifies the leaf certificate covers the probed hostname (SNI)."
|
||||
}
|
||||
|
||||
func (r *hostnameMatchRule) 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.hostname_match.no_endpoints")}
|
||||
}
|
||||
|
||||
var out []sdk.CheckState
|
||||
any := false
|
||||
for _, ref := range sortedRefs(data) {
|
||||
p := data.Probes[ref]
|
||||
if p.HostnameMatch == nil {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
if *p.HostnameMatch {
|
||||
continue
|
||||
}
|
||||
out = append(out, sdk.CheckState{
|
||||
Status: sdk.StatusCrit,
|
||||
Code: "tls.hostname_match.mismatch",
|
||||
Subject: subjectOf(p),
|
||||
Message: fmt.Sprintf("Certificate does not cover %q (SANs: %s)", p.SNI, strings.Join(p.DNSNames, ", ")),
|
||||
Meta: metaOf(p),
|
||||
})
|
||||
}
|
||||
if !any {
|
||||
return []sdk.CheckState{unknownState(
|
||||
"tls.hostname_match.skipped",
|
||||
"No endpoint yielded a certificate to hostname-match.",
|
||||
)}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []sdk.CheckState{passState(
|
||||
"tls.hostname_match.ok",
|
||||
"Every certificate covers its probed SNI.",
|
||||
)}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// expiryRule flags expired or near-expiry certificates.
|
||||
type expiryRule struct{}
|
||||
|
||||
func (r *expiryRule) Name() string { return "tls.expiry" }
|
||||
func (r *expiryRule) Description() string {
|
||||
return "Flags expired or soon-to-expire leaf certificates."
|
||||
}
|
||||
|
||||
func (r *expiryRule) 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.expiry.no_endpoints")}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var out []sdk.CheckState
|
||||
any := false
|
||||
for _, ref := range sortedRefs(data) {
|
||||
p := data.Probes[ref]
|
||||
if p.NotAfter.IsZero() {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
meta := metaOf(p)
|
||||
meta["not_after"] = p.NotAfter
|
||||
if p.NotAfter.Before(now) {
|
||||
out = append(out, sdk.CheckState{
|
||||
Status: sdk.StatusCrit,
|
||||
Code: "tls.expiry.expired",
|
||||
Subject: subjectOf(p),
|
||||
Message: "Certificate expired on " + p.NotAfter.Format(time.RFC3339),
|
||||
Meta: meta,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if p.NotAfter.Sub(now) < ExpiringSoonThreshold {
|
||||
out = append(out, sdk.CheckState{
|
||||
Status: sdk.StatusWarn,
|
||||
Code: "tls.expiry.expiring_soon",
|
||||
Subject: subjectOf(p),
|
||||
Message: "Certificate expires in less than 14 days (" + p.NotAfter.Format(time.RFC3339) + ")",
|
||||
Meta: meta,
|
||||
})
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return []sdk.CheckState{unknownState(
|
||||
"tls.expiry.skipped",
|
||||
"No endpoint yielded a certificate with an expiry to check.",
|
||||
)}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []sdk.CheckState{passState(
|
||||
"tls.expiry.ok",
|
||||
"Every leaf certificate is valid for more than 14 days.",
|
||||
)}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue