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) }