Move status inference out of observation layer into rules
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.
This commit is contained in:
nemunaire 2026-05-16 21:49:58 +08:00
commit 4543e9b0cf
6 changed files with 110 additions and 83 deletions

View file

@ -49,6 +49,75 @@ func orderedZones(data *DNSVizData) []string {
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":