checker-blacklist/checker/rule.go
Pierre-Olivier Mercier c437339bda Separate observation from evaluation in blacklist sources
Each source's Query() method previously set r.Listed and r.Severity,
embedding verdict logic inside the prober. Evaluation now lives in a
dedicated Evaluate(SourceResult) (bool, string) method per source,
keeping Query() as pure observation.

A package-level EvaluateResult() helper looks up the source by ID and
delegates to its Evaluate method; rules.go, report.go, types.go, and
provider.go all call this instead of reading pre-set r.Listed/r.Severity
values. An unknownSource sentinel handles results whose source is no
longer registered.
2026-05-15 18:04:17 +08:00

131 lines
4.1 KiB
Go

package checker
import (
"context"
"fmt"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rules returns the rule set surfaced to happyDomain. After the
// registry refactor we expose a single, generic rule that emits one
// CheckState per source result: the per-source verdict lives in
// CheckState.Subject (the source name) and CheckState.Code carries the
// canonical hit / clean / disabled / error flavour.
func Rules() []sdk.CheckRule {
return []sdk.CheckRule{&sourceRule{}}
}
type sourceRule struct{}
func (*sourceRule) Name() string { return "source_listed" }
func (*sourceRule) Description() string {
return "Emits one state per reputation source: Critical/Warning when the source flags the domain, OK when clean, Info when the source is disabled, and Warning on transient query errors."
}
func (*sourceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
var data BlacklistData
if err := obs.Get(ctx, ObservationKeyBlacklist, &data); err != nil {
return []sdk.CheckState{{
Status: sdk.StatusError,
Message: fmt.Sprintf("failed to get observation: %v", err),
Code: "blacklist_obs_error",
}}
}
if len(data.Results) == 0 {
return []sdk.CheckState{{
Status: sdk.StatusInfo, Message: "No reputation sources registered.",
Code: "blacklist_no_sources",
}}
}
byID := make(map[string]Source, len(Sources()))
for _, s := range Sources() {
byID[s.ID()] = s
}
out := make([]sdk.CheckState, 0, len(data.Results))
for _, r := range data.Results {
src, ok := byID[r.SourceID]
if !ok {
src = unknownSource{}
}
out = append(out, evaluateOne(r, src))
}
return out
}
func evaluateOne(r SourceResult, src Source) sdk.CheckState {
subj := r.SourceName
if r.Subject != "" && r.Subject != r.SourceName {
subj = r.SourceName + " / " + r.Subject
}
listed, severity := src.Evaluate(r)
switch {
case !r.Enabled:
return sdk.CheckState{
Status: sdk.StatusUnknown, Subject: subj,
Message: subj + ": disabled or not configured.",
Code: "source_disabled",
}
case r.BlockedQuery:
return sdk.CheckState{
Status: sdk.StatusError,
Subject: subj,
Message: fmt.Sprintf("%s: resolver is blocked, result unreliable: %s", subj, joinNonEmpty(r.Reasons, "; ")),
Code: "source_resolver_blocked",
}
case r.Error != "":
return sdk.CheckState{
Status: sdk.StatusWarn, Subject: subj,
Message: subj + ": query failed: " + r.Error,
Code: "source_error",
}
case listed:
return sdk.CheckState{
Status: severityToStatus(severity),
Subject: subj,
Message: fmt.Sprintf("Listed in %s: %s", subj, joinNonEmpty(r.Reasons, "; ")),
Code: "source_listed",
Meta: map[string]any{
"source_id": r.SourceID,
"reasons": r.Reasons,
"lookup_url": r.LookupURL,
"removal_url": r.RemovalURL,
"reference": r.Reference,
},
}
default:
return sdk.CheckState{
Status: sdk.StatusOK, Subject: subj,
Message: subj + ": clean.",
Code: "source_clean",
}
}
}
// unknownSource is a sentinel used when a SourceResult references a source ID
// that is no longer in the registry. Evaluate always returns (false, "").
type unknownSource struct{}
func (unknownSource) ID() string { return "" }
func (unknownSource) Name() string { return "unknown" }
func (unknownSource) Options() SourceOptions { return SourceOptions{} }
func (unknownSource) Query(_ context.Context, _, _ string, _ sdk.CheckerOptions) []SourceResult { return nil }
func (unknownSource) Diagnose(_ SourceResult) Diagnosis { return Diagnosis{} }
func (unknownSource) Evaluate(_ SourceResult) (bool, string) { return false, "" }
func severityToStatus(sev string) sdk.Status {
switch sev {
case SeverityCrit:
return sdk.StatusCrit
case SeverityWarn:
return sdk.StatusWarn
case SeverityInfo:
return sdk.StatusInfo
case SeverityOK:
return sdk.StatusOK
}
return sdk.StatusCrit
}