// Package checker implements the happyDomain "dangling records" // checker: it walks the working zone, identifies every pointer record // (CNAME / MX / SRV / NS) whose target lives outside the zone, performs // a light DNS resolution to detect immediate breakage (NXDOMAIN), and // publishes DiscoveryEntry records so a companion checker (typically // the host's domain_expiry) can verify each external registrable domain // via RDAP/WHOIS. The rule layer joins both signals to surface // subdomains at risk of takeover (the "dangling CNAME" attack class // publicised by Ars Technica in 2017). package checker import ( "encoding/json" ) const ObservationKeyDangling = "dangling_records" // DanglingData is the raw observation payload. It carries one Pointer // entry per (owner, rrtype, target) triple found in the zone, including // targets resolved to their DNS verdict. Aggregation by owner happens // in the rule layer. type DanglingData struct { // Zone is the zone apex, without trailing dot. Empty when the host // did not provide a domain_name option. Zone string `json:"zone,omitempty"` // ServicesScanned counts every service inspected (matches the same // field in checker-legacy-records, anchoring the report). ServicesScanned int `json:"services_scanned"` // Pointers lists every pointer record encountered. One entry per // distinct (owner, rrtype, target). External pointers carry a // non-empty Registrable; in-zone pointers leave it empty so the // rule does not request RDAP on the user's own apex. Pointers []Pointer `json:"pointers,omitempty"` // CollectErrors records non-fatal problems encountered during the // zone walk, surfaced in the report so silent skips do not // masquerade as a clean pass. CollectErrors []string `json:"collect_errors,omitempty"` } // Pointer is the unit of observation: one (owner, rrtype, target) seen // in the zone, plus the result of the local DNS resolution. type Pointer struct { // Owner is the FQDN that carries the pointer record (CNAME owner, // MX/SRV owner, NS apex, …). No trailing dot. Owner string `json:"owner"` // Subdomain is Owner relative to the zone apex. "" means apex // (rendered as "@" in the report). Subdomain string `json:"subdomain"` // Rrtype is the textual record type ("CNAME", "MX", "SRV", "NS"). Rrtype string `json:"rrtype"` // Target is the FQDN the record points at. No trailing dot. Target string `json:"target"` // External is true when Target's registrable domain differs from // the zone's registrable domain (the takeover-risk case). External bool `json:"external"` // Registrable is the eTLD+1 of Target. Empty when External is false // or when public-suffix lookup failed. Registrable string `json:"registrable,omitempty"` // ServiceType is the happyDomain service that exposed the record // ("svcs.CNAME", "svcs.MXs", …). Useful for navigating users back // to the right edit screen in the report. ServiceType string `json:"service_type,omitempty"` // Resolution is the verdict of the local DNS lookup of Target: // "ok", "nxdomain", "no_answer", "servfail", "timeout", "skipped". // "skipped" is used when the collector chose not to resolve (for // example, because lookups are disabled at runtime). Resolution string `json:"resolution"` // ResolutionDetail is a free-form sentence describing the // resolution outcome (e.g. the underlying error string). Optional. ResolutionDetail string `json:"resolution_detail,omitempty"` } // rawZone is the minimal slice of happyDomain's *Zone JSON we consume. // Like checker-legacy-records, we redeclare just the fields we need so // this checker compiles without depending on the happyDomain module. type rawZone struct { DomainName string `json:"domain_name,omitempty"` Services map[string][]rawService `json:"services"` } type rawService struct { Type string `json:"_svctype"` Domain string `json:"_domain"` Service json.RawMessage `json:"Service"` } // Below: minimal JSON shapes for each service body we extract pointers // from. We only need fields that point at a host name, so the // definitions are deliberately partial. type cnameBody struct { Record struct { Hdr struct { Name string `json:"Name"` } `json:"Hdr"` Target string `json:"Target"` } `json:"cname"` } type mxRecord struct { Hdr struct { Name string `json:"Name"` } `json:"Hdr"` Mx string `json:"Mx"` } type mxsBody struct { MXs []mxRecord `json:"mx"` } type srvRecord struct { Hdr struct { Name string `json:"Name"` } `json:"Hdr"` Target string `json:"Target"` } type srvsBody struct { Records []srvRecord `json:"srv"` } // orphanRecord covers the body shape used by svcs.Orphan when the // embedded RR is a CNAME, NS, MX, or SRV. We sniff Hdr.Rrtype before // committing to a specific decoder. type orphanRecord struct { Record struct { Hdr struct { Name string `json:"Name"` Rrtype uint16 `json:"Rrtype"` } `json:"Hdr"` // Optional fields, populated for the relevant rrtype. Target string `json:"Target,omitempty"` Mx string `json:"Mx,omitempty"` Ns string `json:"Ns,omitempty"` } `json:"record"` }