checker-blacklist/checker/rule.go
Pierre-Olivier Mercier ce59a976d5
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Expose one rule per source with rule-scoped options
Each registered Source now becomes its own CheckRule (name = source ID)
implementing CheckRuleWithOptions, so the host can toggle blacklists
individually and the per-source option fields show up under the rule
that owns them instead of one flat global option list.

Collect honours the host's per-rule enable map (via the SDK's
RuleEnabled context helper) and skips the network call for disabled
sources entirely, not just their evaluation.
2026-05-19 22:12:31 +08:00

130 lines
3.4 KiB
Go

package checker
import (
"context"
"fmt"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rules returns one rule per registered Source. The host can enable or
// disable each independently, and each rule owns the option fields its
// Source contributes (CheckRuleWithOptions). The rule name is the
// Source ID; downstream a disabled rule both skips evaluation and
// skips the underlying network call in Collect.
func Rules() []sdk.CheckRule {
srcs := Sources()
rules := make([]sdk.CheckRule, 0, len(srcs))
for _, s := range srcs {
rules = append(rules, &sourceRule{src: s})
}
return rules
}
type sourceRule struct {
src Source
}
func (r *sourceRule) Name() string { return r.src.ID() }
func (r *sourceRule) Description() string {
return fmt.Sprintf("%s reputation lookup. Emits Critical/Warning when the source flags the domain, OK when clean, Info when the source is disabled or not configured, and Warning on transient query errors.", r.src.Name())
}
func (r *sourceRule) Options() sdk.CheckerOptionsDocumentation {
o := r.src.Options()
return sdk.CheckerOptionsDocumentation{
AdminOpts: o.Admin,
UserOpts: o.User,
}
}
func (r *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",
}}
}
var out []sdk.CheckState
for _, res := range data.Results {
if res.SourceID != r.src.ID() {
continue
}
out = append(out, evaluateOne(res, r.src))
}
if len(out) == 0 {
return []sdk.CheckState{{
Status: sdk.StatusUnknown,
Message: r.src.Name() + ": no result (source skipped or disabled).",
Code: "source_disabled",
}}
}
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",
}
}
}
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
}