// SPDX-License-Identifier: MIT // Package checker implements a happyDomain checker that wraps DNSViz // (https://github.com/dnsviz/dnsviz). It runs `dnsviz probe` followed by // `dnsviz grok` against a domain, stores the structured analysis as the // observation, and turns the per-zone errors/warnings into CheckStates. // // The container ships dnsviz alongside this binary, so the checker has no // external dependency at runtime besides the network. package checker import ( sdk "git.happydns.org/checker-sdk-go/checker" ) // ObservationKeyDNSViz is the observation key for DNSViz analysis output. const ObservationKeyDNSViz sdk.ObservationKey = "dnsviz" // DNSVizData is what Collect stores. It carries the full grok output // (parsed into a permissive structure) plus the raw bytes for the report. // // DNSViz emits a single top-level object whose keys are zone FQDNs (with // trailing dot), one per level of the chain. Inside each zone object the // shape is permissive: many fields are conditional, so we keep most of them // as map[string]any and only pluck out what the rules need. type DNSVizData struct { // Domain is the queried FQDN, with trailing dot stripped. Domain string `json:"domain"` // Zones is the per-zone analysis, keyed by zone FQDN (with trailing dot // preserved, matching DNSViz's output). Zones map[string]ZoneAnalysis `json:"zones"` // Order is Zones' keys, sorted from the queried name up to the root. // We surface it explicitly so the report can render in a stable order // without having to re-sort on every render. Order []string `json:"order,omitempty"` // Raw is the unmodified `dnsviz grok` JSON. Kept around so the report // can fall back on it for fields the typed view does not capture. Raw []byte `json:"raw,omitempty"` // ProbeStderr / GrokStderr capture the diagnostics dnsviz prints to // stderr. Useful when collection succeeds but the analysis is partial. ProbeStderr string `json:"probe_stderr,omitempty"` GrokStderr string `json:"grok_stderr,omitempty"` } // ZoneAnalysis is a permissive view over one zone's grok block. // // DNSViz output puts the DNSSEC chain status at delegation.status (one of // "SECURE", "BOGUS", "INSECURE", "INDETERMINATE") while the top-level // "status" field carries the DNS rcode for the zone apex (e.g. "NOERROR"). // Errors and warnings are not surfaced as a flat per-zone array; instead // they appear as nested "errors"/"warnings" arrays attached to the record // where the problem was found (DS, DNSKEY, RRSIG, NSEC proof, query // response, server, …). We walk the whole zone subtree to collect them. type ZoneAnalysis struct { // Status is the DNSSEC chain status taken from delegation.status when // available, falling back to the top-level "status" field otherwise. Status string `json:"status,omitempty"` // DNSStatus is the raw top-level "status" field (DNS rcode such as // "NOERROR"). Kept for the report so we can distinguish "DNS resolved // fine" from "DNSSEC chain validates". DNSStatus string `json:"dns_status,omitempty"` Errors []Finding `json:"errors,omitempty"` Warnings []Finding `json:"warnings,omitempty"` } // Finding mirrors the shape DNSViz uses for entries in errors/warnings. // Producers occasionally use slightly different field names across versions // of dnsviz; we accept both `description`/`message` for the human text and // fall back to a generic stringification at parse time. type Finding struct { Code string `json:"code,omitempty"` Description string `json:"description"` Servers []string `json:"servers,omitempty"` // Path is a slash-separated pointer to the JSON node where the finding // was attached (e.g. "delegation/ds[0]" or // "queries/example.com./IN/A/answer[0]/rrsig[0]"). Useful in the // report so a generic "signature_invalid" can be located precisely. Path string `json:"path,omitempty"` // Extra holds the raw finding payload when no human description could // be extracted. Surfaced by the report as a debug fallback. Extra map[string]any `json:"extra,omitempty"` }