Initial commit
This commit is contained in:
commit
86b92eec9d
33 changed files with 5407 additions and 0 deletions
188
checker/types.go
Normal file
188
checker/types.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
// 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"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue