package checker import "strings" // deriveIssues walks a raw XMPPData and returns the full list of findings // that rules (and the HTML report) may surface. // // It does not mutate data. It is intentionally pure so that rules can // recompute their slice of the findings without having to stash anything // into the observation payload. wantC2S / wantS2S restrict mode-scoped // checks (SASL / direct-TLS / working-endpoint-coverage); SRV and per- // endpoint findings are always emitted. func deriveIssues(data *XMPPData, wantC2S, wantS2S bool) []Issue { var issues []Issue // 1. No SRV published. if data.SRV.FallbackProbed { issues = append(issues, Issue{ Code: CodeNoSRV, Severity: SeverityCrit, Message: "No XMPP SRV records found for " + data.Domain + ".", Fix: "Publish _xmpp-client._tcp." + data.Domain + " and _xmpp-server._tcp." + data.Domain + " SRV records.", }) } // 2. Legacy _jabber. if len(data.SRV.Jabber) > 0 { issues = append(issues, Issue{ Code: CodeLegacyJabber, Severity: SeverityWarn, Message: "Obsolete _jabber._tcp SRV record still published.", Fix: "Remove _jabber._tcp records; _xmpp-client._tcp supersedes them.", }) } // 3. SRV lookup errors (real DNS failures, not NXDOMAIN). for prefix, msg := range data.SRV.Errors { issues = append(issues, Issue{ Code: CodeSRVServfail, Severity: SeverityWarn, Message: "DNS lookup failed for " + prefix + data.Domain + ": " + msg, Fix: "Check the authoritative DNS servers for this domain.", }) } // 4. Endpoint-level issues. allDown := true sawSCRAM := map[XMPPMode]bool{} sawSCRAMPlus := map[XMPPMode]bool{} sawPlainOnly := map[XMPPMode]bool{} sawAnyWorking := map[XMPPMode]bool{} for _, ep := range data.Endpoints { if ep.TCPConnected && ep.STARTTLSUpgraded { allDown = false sawAnyWorking[ep.Mode] = true } if ep.TCPConnected && ep.StreamOpened && !ep.DirectTLS { if !ep.STARTTLSOffered { issues = append(issues, Issue{ Code: CodeStartTLSMissing, Severity: SeverityCrit, Message: "STARTTLS not advertised on " + ep.Address + " (" + ep.SRVPrefix + ").", Fix: "Enable STARTTLS in the XMPP server configuration and require it for all connections.", Endpoint: ep.Address, }) } else if !ep.STARTTLSRequired { issues = append(issues, Issue{ Code: CodeStartTLSNotRequired, Severity: SeverityWarn, Message: "STARTTLS offered but not on " + ep.Address + ".", Fix: "Set the server to require TLS (e.g. `c2s_require_encryption = true` in Prosody, `starttls_required` in ejabberd).", Endpoint: ep.Address, }) } } if ep.TCPConnected && !ep.STARTTLSUpgraded && ep.STARTTLSOffered && ep.Error != "" { issues = append(issues, Issue{ Code: CodeStartTLSFailed, Severity: SeverityCrit, Message: "STARTTLS handshake failed on " + ep.Address + ": " + ep.Error + ".", Fix: "Run the TLS checker on this port for cert and cipher details.", Endpoint: ep.Address, }) } if !ep.TCPConnected && ep.Error != "" { issues = append(issues, Issue{ Code: CodeTCPUnreachable, Severity: SeverityWarn, Message: "Cannot reach " + ep.Address + ": " + ep.Error + ".", Fix: "Verify firewall rules and that the XMPP server is listening on this address.", Endpoint: ep.Address, }) } // SASL posture (c2s only). if ep.Mode == ModeClient && ep.STARTTLSUpgraded && len(ep.SASLMechanisms) > 0 { hasSCRAM := false hasSCRAMPlus := false hasPlain := false nonPlain := false for _, m := range ep.SASLMechanisms { u := strings.ToUpper(m) if strings.HasPrefix(u, "SCRAM-") { hasSCRAM = true if strings.HasSuffix(u, "-PLUS") { hasSCRAMPlus = true } } if u == "PLAIN" { hasPlain = true } else { nonPlain = true } } if hasSCRAM { sawSCRAM[ep.Mode] = true } if hasSCRAMPlus { sawSCRAMPlus[ep.Mode] = true } if hasPlain && !nonPlain { sawPlainOnly[ep.Mode] = true } } // S2S auth posture, only meaningful if we actually parsed the // post-TLS features. if ep.Mode == ModeServer && ep.STARTTLSUpgraded { if !ep.FeaturesRead { issues = append(issues, Issue{ Code: CodeS2SProbeIncomplete, Severity: SeverityInfo, Message: "Could not read post-TLS stream features on " + ep.Address + "; server may require an authenticated origin for s2s.", Fix: "This is often benign for well-run public servers. Try from a real federating host if in doubt.", Endpoint: ep.Address, }) } else if !ep.DialbackOffered && !ep.SASLExternal { issues = append(issues, Issue{ Code: CodeS2SNoAuth, Severity: SeverityCrit, Message: "No dialback or SASL EXTERNAL advertised on " + ep.Address + " after TLS; federation will fail.", Fix: "Enable server-to-server dialback, or provision a cert usable for SASL EXTERNAL.", Endpoint: ep.Address, }) } } } if len(data.Endpoints) > 0 && allDown { issues = append(issues, Issue{ Code: CodeAllEndpointsDown, Severity: SeverityCrit, Message: "None of the XMPP endpoints could complete STARTTLS.", Fix: "Verify the server is running and reachable on the published SRV ports.", }) } if wantC2S && sawAnyWorking[ModeClient] { if !sawSCRAM[ModeClient] { issues = append(issues, Issue{ Code: CodeSASLNoSCRAM, Severity: SeverityWarn, Message: "No SCRAM-SHA-* SASL mechanism offered on c2s.", Fix: "Enable SCRAM-SHA-256 (and SCRAM-SHA-1 for compatibility).", }) } if !sawSCRAMPlus[ModeClient] { issues = append(issues, Issue{ Code: CodeSASLNoSCRAMPlus, Severity: SeverityInfo, Message: "No SCRAM-SHA-*-PLUS offered (channel binding).", Fix: "Enable SCRAM-SHA-256-PLUS to protect against TLS MITM.", }) } if sawPlainOnly[ModeClient] { issues = append(issues, Issue{ Code: CodeSASLPlainOnly, Severity: SeverityCrit, Message: "Only SASL PLAIN is offered on c2s.", Fix: "Enable SCRAM-SHA-256 so credentials are not sent as a password-equivalent hash.", }) } } // IPv6 coverage. if data.Coverage.HasIPv4 && !data.Coverage.HasIPv6 { issues = append(issues, Issue{ Code: CodeNoIPv6, Severity: SeverityInfo, Message: "No IPv6 endpoint reachable.", Fix: "Publish AAAA records for the SRV targets.", }) } // XEP-0368 direct TLS coverage. if wantC2S && sawAnyWorking[ModeClient] && len(data.SRV.ClientSecure) == 0 { issues = append(issues, Issue{ Code: CodeNoDirectTLS, Severity: SeverityInfo, Message: "No XEP-0368 direct-TLS SRV record (_xmpps-client._tcp) published.", Fix: "Publish _xmpps-client._tcp SRV records pointing at port 5223 to allow TLS from the first byte.", }) } _ = wantS2S // kept for signature symmetry; s2s-specific rules are expressed via per-endpoint mode checks above return issues }