diff --git a/checker/collect.go b/checker/collect.go index a073206..c239b35 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -76,7 +76,7 @@ func (p *danglingProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) seen[key] = true classifyExternal(&pt, zoneRegistrable) if skipResolution { - pt.Resolution = "skipped" + pt.Resolution = ResolutionSkipped } else { 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) { target = strings.TrimSuffix(target, ".") if target == "" { - return "skipped", "empty target" + return ResolutionSkipped, "empty target" } cctx, cancel := context.WithTimeout(ctx, resolverTimeout) defer cancel() @@ -295,25 +295,25 @@ func defaultResolveHost(ctx context.Context, target string) (verdict, detail str ips, err := net.DefaultResolver.LookupHost(cctx, target) if err == nil { if len(ips) == 0 { - return "no_answer", "" + return ResolutionNoAnswer, "" } - return "ok", "" + return ResolutionOK, "" } var dnsErr *net.DNSError if errors.As(err, &dnsErr) { switch { case dnsErr.IsNotFound: - return "nxdomain", dnsErr.Err + return ResolutionNXDomain, dnsErr.Err case dnsErr.IsTimeout: - return "timeout", dnsErr.Err + return ResolutionTimeout, dnsErr.Err case strings.Contains(strings.ToLower(dnsErr.Err), "servfail"): - return "servfail", dnsErr.Err + return ResolutionServFail, dnsErr.Err 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. diff --git a/checker/rule.go b/checker/rule.go index 94eff1b..296ebfa 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -141,19 +141,19 @@ func evaluatePointer(pt *Pointer, whoisByRef map[string]*whoisFacts) []SignalTri var out []SignalTrigger switch pt.Resolution { - case "nxdomain": + case ResolutionNXDomain: out = append(out, SignalTrigger{ Rrtype: pt.Rrtype, Target: pt.Target, Reason: "Target does not resolve (NXDOMAIN). The record points at a host that no longer exists.", Detail: pt.ResolutionDetail, Severity: SeverityCrit, }) - case "servfail": + case ResolutionServFail: out = append(out, SignalTrigger{ Rrtype: pt.Rrtype, Target: pt.Target, Reason: "Target lookup returned SERVFAIL. The authoritative server may be misconfigured or the delegation broken.", Detail: pt.ResolutionDetail, Severity: SeverityWarn, }) - case "no_answer": + case ResolutionNoAnswer: out = append(out, SignalTrigger{ 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.", diff --git a/checker/types.go b/checker/types.go index b76882a..4756211 100644 --- a/checker/types.go +++ b/checker/types.go @@ -8,6 +8,17 @@ import ( 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. type DanglingData struct { Zone string `json:"zone,omitempty"`