checker: define Resolution verdict constants to make the collect/evaluate contract explicit
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
nemunaire 2026-05-16 13:10:01 +08:00
commit 62e85a155e
3 changed files with 23 additions and 12 deletions

View file

@ -76,7 +76,7 @@ func (p *danglingProvider) Collect(ctx context.Context, opts sdk.CheckerOptions)
seen[key] = true seen[key] = true
classifyExternal(&pt, zoneRegistrable) classifyExternal(&pt, zoneRegistrable)
if skipResolution { if skipResolution {
pt.Resolution = "skipped" pt.Resolution = ResolutionSkipped
} else { } else {
pt.Resolution, pt.ResolutionDetail = resolveHost(ctx, pt.Target) pt.Resolution, pt.ResolutionDetail = resolveHost(ctx, pt.Target)
} }
@ -287,7 +287,7 @@ func classifyExternal(pt *Pointer, zoneRegistrable string) {
func defaultResolveHost(ctx context.Context, target string) (verdict, detail string) { func defaultResolveHost(ctx context.Context, target string) (verdict, detail string) {
target = strings.TrimSuffix(target, ".") target = strings.TrimSuffix(target, ".")
if target == "" { if target == "" {
return "skipped", "empty target" return ResolutionSkipped, "empty target"
} }
cctx, cancel := context.WithTimeout(ctx, resolverTimeout) cctx, cancel := context.WithTimeout(ctx, resolverTimeout)
defer cancel() defer cancel()
@ -295,25 +295,25 @@ func defaultResolveHost(ctx context.Context, target string) (verdict, detail str
ips, err := net.DefaultResolver.LookupHost(cctx, target) ips, err := net.DefaultResolver.LookupHost(cctx, target)
if err == nil { if err == nil {
if len(ips) == 0 { if len(ips) == 0 {
return "no_answer", "" return ResolutionNoAnswer, ""
} }
return "ok", "" return ResolutionOK, ""
} }
var dnsErr *net.DNSError var dnsErr *net.DNSError
if errors.As(err, &dnsErr) { if errors.As(err, &dnsErr) {
switch { switch {
case dnsErr.IsNotFound: case dnsErr.IsNotFound:
return "nxdomain", dnsErr.Err return ResolutionNXDomain, dnsErr.Err
case dnsErr.IsTimeout: case dnsErr.IsTimeout:
return "timeout", dnsErr.Err return ResolutionTimeout, dnsErr.Err
case strings.Contains(strings.ToLower(dnsErr.Err), "servfail"): case strings.Contains(strings.ToLower(dnsErr.Err), "servfail"):
return "servfail", dnsErr.Err return ResolutionServFail, dnsErr.Err
default: default:
return "error", dnsErr.Err return ResolutionError, dnsErr.Err
} }
} }
return "error", err.Error() return ResolutionError, err.Error()
} }
// ownerFQDN returns the record owner FQDN, preferring the service's _domain field over subdomain+apex. // ownerFQDN returns the record owner FQDN, preferring the service's _domain field over subdomain+apex.

View file

@ -141,19 +141,19 @@ func evaluatePointer(pt *Pointer, whoisByRef map[string]*whoisFacts) []SignalTri
var out []SignalTrigger var out []SignalTrigger
switch pt.Resolution { switch pt.Resolution {
case "nxdomain": case ResolutionNXDomain:
out = append(out, SignalTrigger{ out = append(out, SignalTrigger{
Rrtype: pt.Rrtype, Target: pt.Target, Rrtype: pt.Rrtype, Target: pt.Target,
Reason: "Target does not resolve (NXDOMAIN). The record points at a host that no longer exists.", Reason: "Target does not resolve (NXDOMAIN). The record points at a host that no longer exists.",
Detail: pt.ResolutionDetail, Severity: SeverityCrit, Detail: pt.ResolutionDetail, Severity: SeverityCrit,
}) })
case "servfail": case ResolutionServFail:
out = append(out, SignalTrigger{ out = append(out, SignalTrigger{
Rrtype: pt.Rrtype, Target: pt.Target, Rrtype: pt.Rrtype, Target: pt.Target,
Reason: "Target lookup returned SERVFAIL. The authoritative server may be misconfigured or the delegation broken.", Reason: "Target lookup returned SERVFAIL. The authoritative server may be misconfigured or the delegation broken.",
Detail: pt.ResolutionDetail, Severity: SeverityWarn, Detail: pt.ResolutionDetail, Severity: SeverityWarn,
}) })
case "no_answer": case ResolutionNoAnswer:
out = append(out, SignalTrigger{ out = append(out, SignalTrigger{
Rrtype: pt.Rrtype, Target: pt.Target, Rrtype: pt.Rrtype, Target: pt.Target,
Reason: "Target resolves to no address (NOERROR with empty answer). Rarely the operator's intent for a pointer record.", Reason: "Target resolves to no address (NOERROR with empty answer). Rarely the operator's intent for a pointer record.",

View file

@ -8,6 +8,17 @@ import (
const ObservationKeyDangling = "dangling_records" const ObservationKeyDangling = "dangling_records"
// Resolution verdict constants — the shared contract between Collect and Evaluate.
const (
ResolutionOK = "ok"
ResolutionNXDomain = "nxdomain"
ResolutionServFail = "servfail"
ResolutionNoAnswer = "no_answer"
ResolutionTimeout = "timeout"
ResolutionError = "error"
ResolutionSkipped = "skipped"
)
// DanglingData is the raw observation payload; one Pointer per (owner, rrtype, target) triple. // DanglingData is the raw observation payload; one Pointer per (owner, rrtype, target) triple.
type DanglingData struct { type DanglingData struct {
Zone string `json:"zone,omitempty"` Zone string `json:"zone,omitempty"`