checker-blacklist/checker/rule.go

110 lines
3 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",
}}
}
out := make([]sdk.CheckState, 0, len(data.Results))
for _, r := range data.Results {
out = append(out, evaluateOne(r))
}
return out
}
func evaluateOne(r SourceResult) sdk.CheckState {
subj := r.SourceName
if r.Subject != "" && r.Subject != r.SourceName {
subj = r.SourceName + " / " + r.Subject
}
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 r.Listed:
return sdk.CheckState{
Status: severityToStatus(r.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
}