All checks were successful
continuous-integration/drone/push Build is passing
The prober (collect.go) was calling inferApexDNSKEYStatus during zone parsing, effectively making a SECURE/BOGUS judgement inside the collection phase rather than the evaluation phase. The DNS-rcode fallback (z.Status = z.DNSStatus) was also applied at parse time.
144 lines
3.8 KiB
Go
144 lines
3.8 KiB
Go
// 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
|
|
// "<zone>/IN/<RRTYPE>". 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
|
|
}
|
|
}
|