checker-smtp/checker/types.go

188 lines
7.1 KiB
Go

// Package checker implements the SMTP (MX) server checker for happyDomain.
//
// It probes a domain's inbound-mail deployment (MX discovery, TCP
// reachability, ESMTP banner & EHLO, STARTTLS negotiation, open-relay
// posture, RFC 5321 § 4.5.1 postmaster acceptance, PTR / FCrDNS) and
// reports actionable findings.
//
// TLS certificate chain / SAN / expiry / cipher posture is intentionally
// out of scope: we publish each MX target as a DiscoveryEntry of type
// tls.endpoint.v1 with STARTTLS="smtp" so checker-tls picks up the
// connection and runs the TLS posture checks itself. The resulting
// observations flow back into our rule and HTML report via the SDK's
// ObservationGetter.GetRelated / ReportContext.Related path.
package checker
import (
sdk "git.happydns.org/checker-sdk-go/checker"
)
// ObservationKeySMTP is the key under which this checker's observation
// payload is stored.
const ObservationKeySMTP sdk.ObservationKey = "smtp"
// SMTPData is the full observation stored per run.
type SMTPData struct {
Domain string `json:"domain"`
RunAt string `json:"run_at"`
MX MXLookup `json:"mx"`
Endpoints []EndpointProbe `json:"endpoints"`
Coverage Coverage `json:"coverage"`
}
// MXLookup captures the MX discovery step.
type MXLookup struct {
Records []MXRecord `json:"records,omitempty"`
// Error is a non-NXDOMAIN DNS failure (servfail, timeout, …).
Error string `json:"error,omitempty"`
// NullMX is true when the sole record is "." with preference 0 (RFC 7505).
NullMX bool `json:"null_mx,omitempty"`
// ImplicitMX is true when no MX was published and we fell back to the
// A/AAAA of the domain itself (RFC 5321 § 5.1, implicit MX).
ImplicitMX bool `json:"implicit_mx,omitempty"`
}
// MXRecord is a single MX RR target, expanded with per-target DNS checks.
type MXRecord struct {
Preference uint16 `json:"preference"`
Target string `json:"target"`
// Resolution.
IPv4 []string `json:"ipv4,omitempty"`
IPv6 []string `json:"ipv6,omitempty"`
// Error from A/AAAA resolution (empty string when OK).
ResolveError string `json:"resolve_error,omitempty"`
// IsCNAME flags targets pointed at via a CNAME chain, forbidden by
// RFC 5321 § 5.1 ("the domain name that appears in the RDATA SHOULD
// have an associated address record"): senders MAY reject or fail.
IsCNAME bool `json:"is_cname,omitempty"`
CNAMEChain []string `json:"cname_chain,omitempty"`
// IsIPLiteral flags targets that look like an IP address instead of a
// hostname (RFC 5321 § 5.1 forbids it).
IsIPLiteral bool `json:"is_ip_literal,omitempty"`
}
// EndpointProbe is the result of probing one (target, ip, port=25) tuple.
type EndpointProbe struct {
Target string `json:"target"`
Port uint16 `json:"port"`
IP string `json:"ip"`
IsIPv6 bool `json:"is_ipv6,omitempty"`
// Address is "ip:port"; used as a stable key.
Address string `json:"address"`
// Timing & errors.
ElapsedMS int64 `json:"elapsed_ms"`
Error string `json:"error,omitempty"`
// Connection stages.
TCPConnected bool `json:"tcp_connected"`
BannerReceived bool `json:"banner_received"`
BannerLine string `json:"banner_line,omitempty"`
BannerHostname string `json:"banner_hostname,omitempty"`
BannerCode int `json:"banner_code,omitempty"`
EHLOReceived bool `json:"ehlo_received"`
EHLOHostname string `json:"ehlo_hostname,omitempty"`
EHLOFallbackHELO bool `json:"ehlo_fallback_helo,omitempty"`
// Pre-TLS extensions.
Extensions []string `json:"extensions,omitempty"`
STARTTLSOffered bool `json:"starttls_offered"`
AUTHPreTLS []string `json:"auth_pre_tls,omitempty"`
SizeLimit uint64 `json:"size_limit,omitempty"`
HasPipelining bool `json:"has_pipelining,omitempty"`
Has8BITMIME bool `json:"has_8bitmime,omitempty"`
HasSMTPUTF8 bool `json:"has_smtputf8,omitempty"`
HasCHUNKING bool `json:"has_chunking,omitempty"`
HasDSN bool `json:"has_dsn,omitempty"`
HasENHANCEDCODE bool `json:"has_enhancedstatuscodes,omitempty"`
// STARTTLS negotiation.
STARTTLSUpgraded bool `json:"starttls_upgraded,omitempty"`
TLSVersion string `json:"tls_version,omitempty"`
TLSCipher string `json:"tls_cipher,omitempty"`
// Post-TLS extensions (typically identical or larger than pre-TLS).
PostTLSExtensions []string `json:"post_tls_extensions,omitempty"`
AUTHPostTLS []string `json:"auth_post_tls,omitempty"`
// Reverse DNS / FCrDNS.
PTR string `json:"ptr,omitempty"`
PTRError string `json:"ptr_error,omitempty"`
FCrDNSPass bool `json:"fcrdns_pass,omitempty"`
// Mail transaction probes.
NullSenderAccepted *bool `json:"null_sender_accepted,omitempty"`
NullSenderResponse string `json:"null_sender_response,omitempty"`
PostmasterAccepted *bool `json:"postmaster_accepted,omitempty"`
PostmasterResponse string `json:"postmaster_response,omitempty"`
OpenRelay *bool `json:"open_relay,omitempty"`
OpenRelayResponse string `json:"open_relay_response,omitempty"`
OpenRelayRecipient string `json:"open_relay_recipient,omitempty"`
}
// Coverage summarises which axes are working at the domain level.
type Coverage struct {
HasIPv4 bool `json:"has_ipv4"`
HasIPv6 bool `json:"has_ipv6"`
AnyReachable bool `json:"any_reachable"`
AnyBanner bool `json:"any_banner"`
AnyEHLO bool `json:"any_ehlo"`
AnySTARTTLS bool `json:"any_starttls"`
AllSTARTTLS bool `json:"all_starttls"`
AllAcceptMail bool `json:"all_accept_mail"`
}
// Issue is a structured finding, consumed by both the rule and the HTML report.
type Issue struct {
Code string `json:"code"`
Severity string `json:"severity"` // "info" | "warn" | "crit"
Message string `json:"message"`
Fix string `json:"fix,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Target string `json:"target,omitempty"`
}
// Severities (string for stable JSON, independent of sdk.Status numeric values).
const (
SeverityInfo = "info"
SeverityWarn = "warn"
SeverityCrit = "crit"
)
// Issue codes.
const (
CodeNoMX = "smtp.no_mx"
CodeMXLookupFailed = "smtp.mx.lookup_failed"
CodeNullMX = "smtp.null_mx"
CodeImplicitMX = "smtp.mx.implicit"
CodeMXCNAME = "smtp.mx.cname"
CodeMXIPLiteral = "smtp.mx.ip_literal"
CodeMXResolveFailed = "smtp.mx.resolve_failed"
CodeNoAddresses = "smtp.mx.no_addresses"
CodeTCPUnreachable = "smtp.tcp.unreachable"
CodeBannerMissing = "smtp.banner.missing"
CodeBannerInvalid = "smtp.banner.invalid"
CodeEHLOFailed = "smtp.ehlo.failed"
CodeEHLOFallback = "smtp.ehlo.fallback_helo"
CodeSTARTTLSMissing = "smtp.starttls.missing"
CodeSTARTTLSFailed = "smtp.starttls.failed"
CodeAUTHOverPlain = "smtp.auth.plaintext"
CodePTRMissing = "smtp.ptr.missing"
CodeFCrDNSMismatch = "smtp.fcrdns.mismatch"
CodeNullSenderReject = "smtp.null_sender.rejected"
CodePostmasterReject = "smtp.postmaster.rejected"
CodeOpenRelay = "smtp.open_relay"
CodeNoPipelining = "smtp.no_pipelining"
CodeNo8BITMIME = "smtp.no_8bitmime"
CodeNoIPv6 = "smtp.no_ipv6"
CodeAllEndpointsDown = "smtp.all_endpoints_down"
CodeAllNoSTARTTLS = "smtp.all_no_starttls"
)