diff --git a/checker/collect.go b/checker/collect.go index 076f98e..446a195 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -38,11 +38,6 @@ func (p *blacklistProvider) Collect(ctx context.Context, opts sdk.CheckerOptions var wg sync.WaitGroup for i, s := range sources { - // The host disables a source by disabling its rule (rule name == source ID). - // Skip the network call entirely; Evaluate is short-circuited host-side. - if !sdk.RuleEnabled(ctx, s.ID()) { - continue - } wg.Add(1) go func(i int, s Source) { defer wg.Done() diff --git a/checker/definition.go b/checker/definition.go index fcbd9aa..14c67e9 100644 --- a/checker/definition.go +++ b/checker/definition.go @@ -9,9 +9,10 @@ import ( // Version is overridden at link time by the standalone or plugin entrypoints. var Version = "built-in" -// Definition assembles the checker definition. Per-source option fields -// live on each per-source rule (CheckRuleWithOptions); the global Options -// only carries the shared domain target. +// Definition assembles the checker definition by aggregating each +// registered Source's options into the SDK's audience-grouped layout. +// Adding a source automatically adds its option fields here: no edit +// to this file needed. func Definition() *sdk.CheckerDefinition { opts := sdk.CheckerOptionsDocumentation{ DomainOpts: []sdk.CheckerOptionDocumentation{ @@ -22,6 +23,11 @@ func Definition() *sdk.CheckerDefinition { }, }, } + for _, s := range Sources() { + o := s.Options() + opts.AdminOpts = append(opts.AdminOpts, o.Admin...) + opts.UserOpts = append(opts.UserOpts, o.User...) + } return &sdk.CheckerDefinition{ ID: "blacklist", diff --git a/checker/report.go b/checker/report.go index 5abd8a8..c823cf7 100644 --- a/checker/report.go +++ b/checker/report.go @@ -21,17 +21,16 @@ func (p *blacklistProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) return "", fmt.Errorf("decode blacklist data: %w", err) } - states := ctx.States() view := reportView{ Domain: data.Domain, RegisteredDomain: data.RegisteredDomain, CollectedAt: data.CollectedAt.Format("2006-01-02 15:04 MST"), TotalHits: data.TotalHits(), - Diagnoses: diagnoseFromStates(states, &data), + Diagnoses: diagnose(&data), Sections: buildSections(&data), CSS: template.CSS(reportCSS), } - view.Headline, view.HeadlineClass = headlineFromStates(states, view.TotalHits) + view.Headline, view.HeadlineClass = headline(view.TotalHits) var b bytes.Buffer if err := reportTemplate.Execute(&b, view); err != nil { @@ -130,54 +129,6 @@ func diagnose(d *BlacklistData) []Diagnosis { return out } -func headlineFromStates(states []sdk.CheckState, fallbackHits int) (string, string) { - if len(states) == 0 { - return headline(fallbackHits) - } - listed := 0 - for _, st := range states { - if st.Code == "source_listed" { - listed++ - } - } - return headline(listed) -} - -func diagnoseFromStates(states []sdk.CheckState, d *BlacklistData) []Diagnosis { - if len(states) == 0 { - return diagnose(d) - } - var out []Diagnosis - for _, st := range states { - if st.Status != sdk.StatusCrit && st.Status != sdk.StatusWarn { - continue - } - switch st.Code { - case "source_listed": - diag := Diagnosis{ - Severity: SeverityCrit, - Title: "Listed in " + st.Subject, - Detail: st.Message, - } - if v, ok := st.Meta["lookup_url"].(string); ok { - diag.LookupURL = v - } - if v, ok := st.Meta["removal_url"].(string); ok { - diag.RemovalURL = v - } - out = append(out, diag) - case "source_error", "source_resolver_blocked": - out = append(out, Diagnosis{ - Severity: SeverityWarn, - Title: "Could not query " + st.Subject, - Detail: st.Message, - }) - } - } - sort.SliceStable(out, func(i, j int) bool { return sevRank(out[i].Severity) < sevRank(out[j].Severity) }) - return out -} - func sevRank(s string) int { switch s { case SeverityCrit: diff --git a/checker/rule.go b/checker/rule.go index d904836..72d5c38 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -7,39 +7,23 @@ import ( 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. +// 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 { - srcs := Sources() - rules := make([]sdk.CheckRule, 0, len(srcs)) - for _, s := range srcs { - rules = append(rules, &sourceRule{src: s}) - } - return rules + return []sdk.CheckRule{&sourceRule{}} } -type sourceRule struct { - src Source +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 (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 { +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{{ @@ -49,20 +33,26 @@ func (r *sourceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, op }} } - 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 { + if len(data.Results) == 0 { return []sdk.CheckState{{ - Status: sdk.StatusUnknown, - Message: r.src.Name() + ": no result (source skipped or disabled).", - Code: "source_disabled", + 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 } @@ -115,6 +105,17 @@ func evaluateOne(r SourceResult, src Source) sdk.CheckState { } } +// 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: diff --git a/go.mod b/go.mod index cc71d5d..042aaf7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module git.happydns.org/checker-blacklist go 1.25.0 require ( - git.happydns.org/checker-sdk-go v1.8.0 + git.happydns.org/checker-sdk-go v1.5.0 golang.org/x/net v0.34.0 ) diff --git a/go.sum b/go.sum index d14c68e..5631c56 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -git.happydns.org/checker-sdk-go v1.8.0 h1:2lhcSc16rnCaszdQi1nerszb2c3fVh5XNS11pLrXuK4= -git.happydns.org/checker-sdk-go v1.8.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY= +git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=