Sources that always work (botvrij, disconnect, oisd, openphish, phishtank, quad9) drop their user-facing enable_* option; the rule's enabled/disabled state is now solely controlled by the SDK rule toggle. Sources that require credentials (criminalip, malwarebazaar, otx, pulsedive, safebrowsing, threatfox, urlhaus, virustotal) instead implement the new SourcePrecheck interface so the host UI can surface "not configured" before attempting a query.
140 lines
3.8 KiB
Go
140 lines
3.8 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())
|
|
}
|
|
|
|
// Precheck satisfies sdk.RulePrecheck for every sourceRule. Sources
|
|
// that need credentials (or any other runtime prerequisite) opt in via
|
|
// SourcePrecheck; sources that always work return nil here.
|
|
func (r *sourceRule) Precheck(ctx context.Context, opts sdk.CheckerOptions) error {
|
|
if p, ok := r.src.(SourcePrecheck); ok {
|
|
return p.Precheck(ctx, opts)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|