// SPDX-License-Identifier: MIT package checker import ( "context" "fmt" "sort" "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) // Rules returns the full rule set evaluated against a DNSVizData observation. // Subject is the zone FQDN so a fault at the TLD is never silently merged with a leaf fault. func Rules() []sdk.CheckRule { return []sdk.CheckRule{ &overallStatusRule{}, &perZoneStatusRule{}, &zoneErrorsRule{}, &zoneWarningsRule{}, &commonFailuresRule{}, } } func loadData(ctx context.Context, obs sdk.ObservationGetter, code string) (*DNSVizData, []sdk.CheckState) { var data DNSVizData if err := obs.Get(ctx, ObservationKeyDNSViz, &data); err != nil { return nil, []sdk.CheckState{{ Status: sdk.StatusError, Code: code, Message: fmt.Sprintf("Failed to load DNSViz observation: %v", err), }} } return &data, nil } func orderedZones(data *DNSVizData) []string { if len(data.Order) > 0 { return data.Order } keys := make([]string, 0, len(data.Zones)) for k := range data.Zones { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { return labelDepth(keys[i]) > labelDepth(keys[j]) }) return keys } // effectiveStatus returns the DNSSEC status for a zone, applying the // inference chain that the observation layer deliberately leaves to rules: // 1. delegation.status (z.Status) — the authoritative answer when present. // 2. RRSIG-based inference — for zones without a delegation block (root), // dnsviz surfaces trust-anchor validation only through per-RRSIG statuses. // 3. DNS rcode fallback (z.DNSStatus) — when the zone is unsigned or grok // did not classify it at all. func effectiveStatus(z ZoneAnalysis) string { if z.Status != "" { return z.Status } if s := inferApexDNSKEYStatus(z.Queries); s != "" { return s } return z.DNSStatus } // inferApexDNSKEYStatus returns "SECURE", "BOGUS", or "" based on the // status of RRSIGs covering the zone's apex DNSKEY rrset. dnsviz attaches // a per-RRSIG status whenever a key reaches it (either through DS from // the parent or through a configured trust anchor at this zone). For // the root, this is the only place where trust-anchor validation // surfaces in the grok output. // // queries is the value at zone["queries"], a map keyed by // "/IN/". We pick the DNSKEY query and look at every // RRSIG inside its answer. func inferApexDNSKEYStatus(queries map[string]any) string { var dnskeyQ map[string]any for k, v := range queries { if !strings.HasSuffix(k, "/IN/DNSKEY") { continue } if m, ok := v.(map[string]any); ok { dnskeyQ = m break } } if dnskeyQ == nil { return "" } answers, _ := dnskeyQ["answer"].([]any) sawValid := false for _, a := range answers { am, _ := a.(map[string]any) if am == nil { continue } rrsigs, _ := am["rrsig"].([]any) for _, rs := range rrsigs { rm, _ := rs.(map[string]any) if rm == nil { continue } s, _ := rm["status"].(string) switch strings.ToUpper(s) { case "INVALID", "BOGUS", "EXPIRED", "PREMATURE": return "BOGUS" case "VALID", "SECURE": sawValid = true } } } if sawValid { return "SECURE" } return "" } func statusFromGrok(s string) sdk.Status { switch strings.ToUpper(strings.TrimSpace(s)) { case "SECURE": return sdk.StatusOK case "INSECURE": // "INSECURE" means "no DNSSEC and no parent DS": informational, not // a failure. Rules elsewhere can still flag a missing chain. return sdk.StatusInfo case "BOGUS": return sdk.StatusCrit case "INDETERMINATE": return sdk.StatusWarn case "NON_EXISTENT": return sdk.StatusInfo case "NOERROR": // DNS-level OK with no DNSSEC chain status reported. The zone // resolves but isn't signed (or grok didn't classify it). return sdk.StatusInfo case "": return sdk.StatusUnknown default: return sdk.StatusUnknown } }