Initial commit
This commit is contained in:
commit
485c5a4a1d
33 changed files with 5407 additions and 0 deletions
214
checker/rule.go
Normal file
214
checker/rule.go
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Rules returns the full list of CheckRules exposed by the SMTP checker.
|
||||
// Each rule covers a single concern (MX present, STARTTLS offered, open
|
||||
// relay, PTR/FCrDNS, …) so each shows up as an independent pass/fail line
|
||||
// in the UI, instead of being buried under a single monolithic rule.
|
||||
func Rules() []sdk.CheckRule {
|
||||
return []sdk.CheckRule{
|
||||
&nullMXRule{},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.mx_present",
|
||||
description: "Verifies the domain publishes at least one MX record (or a null MX).",
|
||||
codes: []string{CodeMXLookupFailed, CodeImplicitMX, CodeNoMX},
|
||||
passCode: "smtp.mx_present.ok",
|
||||
passMessage: "Domain publishes explicit MX records.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.mx_sanity",
|
||||
description: "Flags MX targets that violate RFC 5321 § 5.1 (IP literals, CNAME chains, unresolved names).",
|
||||
codes: []string{CodeMXIPLiteral, CodeMXCNAME, CodeMXResolveFailed, CodeNoAddresses},
|
||||
passCode: "smtp.mx_sanity.ok",
|
||||
passMessage: "MX targets resolve cleanly and are regular hostnames.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.endpoint_reachable",
|
||||
description: "Verifies every MX endpoint accepts a TCP connection on port 25.",
|
||||
codes: []string{CodeTCPUnreachable, CodeAllEndpointsDown},
|
||||
passCode: "smtp.endpoint_reachable.ok",
|
||||
passMessage: "All MX endpoints are reachable on port 25.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.banner_sanity",
|
||||
description: "Verifies every reachable endpoint emits a 220 SMTP greeting.",
|
||||
codes: []string{CodeBannerMissing, CodeBannerInvalid},
|
||||
passCode: "smtp.banner_sanity.ok",
|
||||
passMessage: "Every reachable endpoint presents a valid 220 banner.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.ehlo_supported",
|
||||
description: "Verifies every endpoint accepts EHLO (required for STARTTLS, PIPELINING, SIZE, …).",
|
||||
codes: []string{CodeEHLOFailed, CodeEHLOFallback},
|
||||
passCode: "smtp.ehlo_supported.ok",
|
||||
passMessage: "Every endpoint accepts EHLO.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.starttls_offered",
|
||||
description: "Verifies every endpoint advertises the STARTTLS extension.",
|
||||
codes: []string{CodeSTARTTLSMissing, CodeAllNoSTARTTLS},
|
||||
passCode: "smtp.starttls_offered.ok",
|
||||
passMessage: "Every endpoint advertises STARTTLS.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.starttls_handshake",
|
||||
description: "Verifies the STARTTLS handshake succeeds wherever STARTTLS is advertised.",
|
||||
codes: []string{CodeSTARTTLSFailed},
|
||||
passCode: "smtp.starttls_handshake.ok",
|
||||
passMessage: "STARTTLS handshake succeeds on every endpoint that offers it.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.auth_posture",
|
||||
description: "Flags endpoints that advertise SMTP AUTH before STARTTLS (cleartext credentials).",
|
||||
codes: []string{CodeAUTHOverPlain},
|
||||
passCode: "smtp.auth_posture.ok",
|
||||
passMessage: "No endpoint advertises SMTP AUTH in cleartext.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.reverse_dns",
|
||||
description: "Verifies every endpoint has a matching PTR record (FCrDNS).",
|
||||
codes: []string{CodePTRMissing, CodeFCrDNSMismatch},
|
||||
passCode: "smtp.reverse_dns.ok",
|
||||
passMessage: "Every endpoint has a PTR record that forward-confirms.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.null_sender",
|
||||
description: "Verifies endpoints accept the null sender MAIL FROM:<> (required for DSNs).",
|
||||
codes: []string{CodeNullSenderReject},
|
||||
passCode: "smtp.null_sender.ok",
|
||||
passMessage: "Endpoints accept the RFC 5321 null sender.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.postmaster",
|
||||
description: "Verifies endpoints accept RCPT TO:<postmaster@domain> (RFC 5321 § 4.5.1).",
|
||||
codes: []string{CodePostmasterReject},
|
||||
passCode: "smtp.postmaster.ok",
|
||||
passMessage: "Endpoints accept mail for <postmaster>.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.open_relay",
|
||||
description: "Flags endpoints that relay mail for recipients outside the tested domain.",
|
||||
codes: []string{CodeOpenRelay},
|
||||
passCode: "smtp.open_relay.ok",
|
||||
passMessage: "No endpoint accepts relay for foreign recipients.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.extension_posture",
|
||||
description: "Reports ESMTP extension posture (PIPELINING, 8BITMIME).",
|
||||
codes: []string{CodeNoPipelining, CodeNo8BITMIME},
|
||||
passCode: "smtp.extension_posture.ok",
|
||||
passMessage: "Endpoints advertise the common ESMTP extensions.",
|
||||
},
|
||||
&simpleConcernRule{
|
||||
name: "smtp.ipv6_reachable",
|
||||
description: "Verifies at least one MX endpoint is reachable over IPv6.",
|
||||
codes: []string{CodeNoIPv6},
|
||||
passCode: "smtp.ipv6_reachable.ok",
|
||||
passMessage: "At least one MX endpoint is reachable over IPv6.",
|
||||
},
|
||||
&tlsQualityRule{},
|
||||
}
|
||||
}
|
||||
|
||||
// loadSMTPData fetches the SMTP observation. On error, returns a CheckState
|
||||
// the caller should emit to short-circuit its rule.
|
||||
func loadSMTPData(ctx context.Context, obs sdk.ObservationGetter) (*SMTPData, *sdk.CheckState) {
|
||||
var data SMTPData
|
||||
if err := obs.Get(ctx, ObservationKeySMTP, &data); err != nil {
|
||||
return nil, &sdk.CheckState{
|
||||
Status: sdk.StatusError,
|
||||
Message: fmt.Sprintf("failed to load SMTP observation: %v", err),
|
||||
Code: "smtp.observation_error",
|
||||
}
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// issuesByCodes returns derived issues whose Code is in the given set,
|
||||
// preserving the order deriveIssues produces.
|
||||
func issuesByCodes(data *SMTPData, codes ...string) []Issue {
|
||||
if len(codes) == 0 {
|
||||
return nil
|
||||
}
|
||||
set := make(map[string]struct{}, len(codes))
|
||||
for _, c := range codes {
|
||||
set[c] = struct{}{}
|
||||
}
|
||||
var out []Issue
|
||||
for _, is := range deriveIssues(data) {
|
||||
if _, ok := set[is.Code]; ok {
|
||||
out = append(out, is)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func statesFromIssues(issues []Issue) []sdk.CheckState {
|
||||
out := make([]sdk.CheckState, 0, len(issues))
|
||||
for _, is := range issues {
|
||||
out = append(out, issueToState(is))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func issueToState(is Issue) sdk.CheckState {
|
||||
subject := is.Endpoint
|
||||
if subject == "" {
|
||||
subject = is.Target
|
||||
}
|
||||
meta := map[string]any{}
|
||||
if is.Fix != "" {
|
||||
meta["fix"] = is.Fix
|
||||
}
|
||||
if is.Endpoint != "" {
|
||||
meta["endpoint"] = is.Endpoint
|
||||
}
|
||||
if is.Target != "" {
|
||||
meta["target"] = is.Target
|
||||
}
|
||||
st := sdk.CheckState{
|
||||
Status: severityToStatus(is.Severity),
|
||||
Message: is.Message,
|
||||
Code: is.Code,
|
||||
Subject: subject,
|
||||
}
|
||||
if len(meta) > 0 {
|
||||
st.Meta = meta
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func severityToStatus(sev string) sdk.Status {
|
||||
switch sev {
|
||||
case SeverityCrit:
|
||||
return sdk.StatusCrit
|
||||
case SeverityWarn:
|
||||
return sdk.StatusWarn
|
||||
case SeverityInfo:
|
||||
return sdk.StatusInfo
|
||||
default:
|
||||
return sdk.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
func passState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{
|
||||
Status: sdk.StatusOK,
|
||||
Message: message,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
func notTestedState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{
|
||||
Status: sdk.StatusUnknown,
|
||||
Message: message,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue