checker-xmpp/checker/rules.go

139 lines
5.8 KiB
Go

package checker
import (
"context"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rule returns a single aggregate rule covering the whole XMPP posture.
// Kept for backwards compatibility with callers that expect exactly one
// CheckRule; prefer Rules() which splits concerns into individual rules.
func Rule() sdk.CheckRule {
return &xmppRule{}
}
// Rules returns the full list of CheckRules exposed by the XMPP checker,
// one per concern so callers can see at a glance which checks passed and
// which did not, instead of looking up Code on a single monolithic rule.
func Rules() []sdk.CheckRule {
return []sdk.CheckRule{
&simpleXMPPConcernRule{
name: "xmpp.srv_c2s",
description: "Verifies that client-to-server SRV records (_xmpp-client / _xmpps-client / _jabber) are published and resolvable.",
codes: []string{CodeNoSRV, CodeSRVServfail, CodeLegacyJabber},
passCode: "xmpp.srv_c2s.ok",
passMessage: "Client-to-server SRV records are published and resolve cleanly.",
modeFilter: modeFilterC2S,
},
&simpleXMPPConcernRule{
name: "xmpp.srv_s2s",
description: "Verifies that server-to-server SRV records (_xmpp-server / _xmpps-server) are published and resolvable.",
codes: []string{CodeNoSRV, CodeSRVServfail},
passCode: "xmpp.srv_s2s.ok",
passMessage: "Server-to-server SRV records are published and resolve cleanly.",
modeFilter: modeFilterS2S,
},
&c2sReachableRule{},
&s2sReachableRule{},
&simpleXMPPConcernRule{
name: "xmpp.starttls_required",
description: "Verifies that STARTTLS is advertised and required on every reachable c2s/s2s endpoint.",
codes: []string{CodeStartTLSMissing, CodeStartTLSNotRequired, CodeStartTLSFailed},
passCode: "xmpp.starttls_required.ok",
passMessage: "STARTTLS is offered and required on every reachable endpoint.",
},
&simpleXMPPConcernRule{
name: "xmpp.sasl_mechanisms",
description: "Reviews the c2s SASL mechanisms offer (presence of SCRAM, absence of password-equivalent PLAIN-only).",
codes: []string{CodeSASLPlainOnly, CodeSASLNoSCRAM, CodeSASLNoSCRAMPlus},
passCode: "xmpp.sasl_mechanisms.ok",
passMessage: "c2s advertises a strong SASL mechanism (SCRAM family).",
modeFilter: modeFilterC2S,
},
&simpleXMPPConcernRule{
name: "xmpp.s2s_dialback",
description: "Verifies that s2s endpoints advertise dialback or SASL EXTERNAL after TLS (federation auth).",
codes: []string{CodeS2SNoAuth, CodeS2SProbeIncomplete},
passCode: "xmpp.s2s_dialback.ok",
passMessage: "Every reachable s2s endpoint advertises dialback or SASL EXTERNAL.",
modeFilter: modeFilterS2S,
},
&simpleXMPPConcernRule{
name: "xmpp.ipv6_reachable",
description: "Flags deployments that are only reachable over IPv4.",
codes: []string{CodeNoIPv6},
passCode: "xmpp.ipv6_reachable.ok",
passMessage: "At least one endpoint is reachable over IPv6.",
},
&simpleXMPPConcernRule{
name: "xmpp.direct_tls",
description: "Flags c2s deployments that do not publish XEP-0368 direct-TLS SRV records.",
codes: []string{CodeNoDirectTLS},
passCode: "xmpp.direct_tls.ok",
passMessage: "XEP-0368 direct-TLS SRV records are published for c2s.",
modeFilter: modeFilterC2S,
},
&tlsQualityRule{},
}
}
// modeFilter lets a rule short-circuit to "skipped" when the selected mode
// excludes the concern (e.g. c2s-specific rule running in mode=s2s).
type modeFilter func(wantC2S, wantS2S bool) bool
func modeFilterC2S(wantC2S, _ bool) bool { return wantC2S }
func modeFilterS2S(_, wantS2S bool) bool { return wantS2S }
// simpleXMPPConcernRule covers the common shape: "derive the issue list,
// keep the ones matching these codes, emit them as states or a single pass
// state when none match".
type simpleXMPPConcernRule struct {
name string
description string
codes []string
passCode string
passMessage string
modeFilter modeFilter // optional
}
func (r *simpleXMPPConcernRule) Name() string { return r.name }
func (r *simpleXMPPConcernRule) Description() string { return r.description }
func (r *simpleXMPPConcernRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
data, errSt := loadXMPPData(ctx, obs)
if errSt != nil {
return []sdk.CheckState{*errSt}
}
wantC2S, wantS2S := wantsFromOpts(opts)
if r.modeFilter != nil && !r.modeFilter(wantC2S, wantS2S) {
return []sdk.CheckState{notTestedState(r.name+".skipped", "Not applicable to the selected mode.")}
}
issues := filterIssuesByCodes(deriveIssues(data, wantC2S, wantS2S), r.codes...)
if len(issues) == 0 {
return []sdk.CheckState{passState(r.passCode, r.passMessage)}
}
return statesFromIssues(issues)
}
// tlsQualityRule folds findings from a downstream TLS checker into XMPP
// output, so cert chain / hostname / expiry problems show up on the XMPP
// service page without needing a separate glance at the TLS report.
type tlsQualityRule struct{}
func (r *tlsQualityRule) Name() string { return "xmpp.tls_quality" }
func (r *tlsQualityRule) Description() string {
return "Folds the downstream TLS checker findings (certificate chain, hostname match, expiry) onto the XMPP service."
}
func (r *tlsQualityRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
related, _ := obs.GetRelated(ctx, TLSRelatedKey)
if len(related) == 0 {
return []sdk.CheckState{notTestedState("xmpp.tls_quality.skipped", "No related TLS observation available (no TLS checker downstream, or no probe yet).")}
}
issues := tlsIssuesFromRelated(related)
if len(issues) == 0 {
return []sdk.CheckState{passState("xmpp.tls_quality.ok", "Downstream TLS checker reports no issues on the XMPP endpoints.")}
}
return statesFromIssues(issues)
}