diff --git a/checker/botvrij.go b/checker/botvrij.go index 05c09c4..dbc4348 100644 --- a/checker/botvrij.go +++ b/checker/botvrij.go @@ -33,11 +33,21 @@ func (*botvrijSource) ID() string { return "botvrij" } func (*botvrijSource) Name() string { return "Botvrij.eu domain blocklist" } func (*botvrijSource) Options() SourceOptions { - return SourceOptions{} + return SourceOptions{ + User: []sdk.CheckerOptionField{ + { + Id: "enable_botvrij", + Type: "bool", + Label: "Use Botvrij.eu domain blocklist", + Description: "Download the Botvrij.eu public domain blocklist (refreshed every 6h) and check the domain against it.", + Default: true, + }, + }, + } } func (s *botvrijSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { - if registered == "" { + if !sdk.GetBoolOption(opts, "enable_botvrij", true) || registered == "" { return disabledResult(s.ID(), s.Name()) } diff --git a/checker/botvrij_test.go b/checker/botvrij_test.go index b9c466f..74366b4 100644 --- a/checker/botvrij_test.go +++ b/checker/botvrij_test.go @@ -5,6 +5,8 @@ import ( "net/http" "net/http/httptest" "testing" + + sdk "git.happydns.org/checker-sdk-go/checker" ) const botvrijFakeFeed = `# Botvrij.eu IOC list - domains @@ -21,7 +23,7 @@ func TestBotvrijSource_Listed_ExactMatch(t *testing.T) { defer srv.Close() s := &botvrijSource{cache: newBotvrijCache(srv.URL)} - r := s.Query(context.Background(), "evil.com", "evil.com", nil)[0] + r := s.Query(context.Background(), "evil.com", "evil.com", sdk.CheckerOptions{"enable_botvrij": true})[0] if !r.Enabled || r.Error != "" { t.Fatalf("expected enabled and no error, got %+v", r) @@ -42,7 +44,7 @@ func TestBotvrijSource_Listed_SubdomainInFeed(t *testing.T) { // Feed has "malware.example.org"; querying registered "example.org" should match. s := &botvrijSource{cache: newBotvrijCache(srv.URL)} - r := s.Query(context.Background(), "sub.example.org", "example.org", nil)[0] + r := s.Query(context.Background(), "sub.example.org", "example.org", sdk.CheckerOptions{"enable_botvrij": true})[0] if len(r.Evidence) != 1 || r.Evidence[0].Value != "malware.example.org" { t.Errorf("expected subdomain match, got %+v", r.Evidence) @@ -59,7 +61,7 @@ func TestBotvrijSource_NotListed(t *testing.T) { defer srv.Close() s := &botvrijSource{cache: newBotvrijCache(srv.URL)} - r := s.Query(context.Background(), "safe.com", "safe.com", nil)[0] + r := s.Query(context.Background(), "safe.com", "safe.com", sdk.CheckerOptions{"enable_botvrij": true})[0] if !r.Enabled || r.Error != "" || len(r.Evidence) != 0 { t.Fatalf("expected clean result, got %+v", r) @@ -69,6 +71,14 @@ func TestBotvrijSource_NotListed(t *testing.T) { } } +func TestBotvrijSource_Disabled(t *testing.T) { + s := &botvrijSource{cache: newBotvrijCache("http://nope")} + r := s.Query(context.Background(), "evil.com", "evil.com", sdk.CheckerOptions{"enable_botvrij": false})[0] + if r.Enabled { + t.Errorf("expected disabled, got %+v", r) + } +} + func TestBotvrijSource_HTTPError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) @@ -76,7 +86,7 @@ func TestBotvrijSource_HTTPError(t *testing.T) { defer srv.Close() s := &botvrijSource{cache: newBotvrijCache(srv.URL)} - r := s.Query(context.Background(), "evil.com", "evil.com", nil)[0] + r := s.Query(context.Background(), "evil.com", "evil.com", sdk.CheckerOptions{"enable_botvrij": true})[0] if r.Error == "" || r.Error != "botvrij HTTP 500" { t.Errorf("expected HTTP 500 error, got %q", r.Error) diff --git a/checker/criminalip.go b/checker/criminalip.go index d15371d..d98f2b5 100644 --- a/checker/criminalip.go +++ b/checker/criminalip.go @@ -19,13 +19,6 @@ type criminalIPSource struct{ endpoint string } func (*criminalIPSource) ID() string { return "criminal_ip" } func (*criminalIPSource) Name() string { return "Criminal IP" } -func (s *criminalIPSource) Precheck(ctx context.Context, opts sdk.CheckerOptions) error { - if stringOpt(opts, "criminal_ip_api_key") == "" { - return fmt.Errorf("Criminal IP API key is not configured") - } - return nil -} - func (*criminalIPSource) Options() SourceOptions { return SourceOptions{ Admin: []sdk.CheckerOptionField{ diff --git a/checker/disconnect.go b/checker/disconnect.go index 73861b3..ae93adf 100644 --- a/checker/disconnect.go +++ b/checker/disconnect.go @@ -26,10 +26,23 @@ func (*disconnectSource) ID() string { return "disconnect" } func (*disconnectSource) Name() string { return "Disconnect.me" } func (*disconnectSource) Options() SourceOptions { - return SourceOptions{} + return SourceOptions{ + User: []sdk.CheckerOptionField{ + { + Id: "enable_disconnect", + Type: "bool", + Label: "Use the Disconnect.me tracking-protection list", + Description: "Check the domain against the Disconnect.me blocklist used by Firefox Enhanced Tracking Protection, Brave, and uBlock Origin.", + Default: true, + }, + }, + } } func (s *disconnectSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { + if !sdk.GetBoolOption(opts, "enable_disconnect", true) { + return disabledResult(s.ID(), s.Name()) + } if registered == "" { return []SourceResult{{SourceID: s.ID(), SourceName: s.Name(), Enabled: true}} } diff --git a/checker/disconnect_test.go b/checker/disconnect_test.go index fc17c55..7a5e325 100644 --- a/checker/disconnect_test.go +++ b/checker/disconnect_test.go @@ -6,6 +6,8 @@ import ( "net/http/httptest" "testing" "time" + + sdk "git.happydns.org/checker-sdk-go/checker" ) const disconnectFakeFeed = `{ @@ -40,7 +42,7 @@ func TestDisconnectSource_Listed(t *testing.T) { defer srv.Close() s := newDisconnectTestSource(srv) - r := s.Query(context.Background(), "tracker.com", "tracker.com", nil)[0] + r := s.Query(context.Background(), "tracker.com", "tracker.com", sdk.CheckerOptions{"enable_disconnect": true})[0] if !r.Enabled || r.Error != "" { t.Fatalf("expected enabled with no error, got %+v", r) @@ -73,7 +75,7 @@ func TestDisconnectSource_SubdomainInFeed(t *testing.T) { // Feed has "sub.example.org"; querying registered domain "example.org" should match. s := newDisconnectTestSource(srv) - r := s.Query(context.Background(), "example.org", "example.org", nil)[0] + r := s.Query(context.Background(), "example.org", "example.org", sdk.CheckerOptions{"enable_disconnect": true})[0] if !r.Enabled || r.Error != "" { t.Fatalf("expected enabled with no error, got %+v", r) @@ -90,7 +92,7 @@ func TestDisconnectSource_NotListed(t *testing.T) { defer srv.Close() s := newDisconnectTestSource(srv) - r := s.Query(context.Background(), "clean.example.com", "clean.example.com", nil)[0] + r := s.Query(context.Background(), "clean.example.com", "clean.example.com", sdk.CheckerOptions{"enable_disconnect": true})[0] if !r.Enabled || r.Error != "" { t.Fatalf("expected enabled with no error, got %+v", r) @@ -103,6 +105,14 @@ func TestDisconnectSource_NotListed(t *testing.T) { } } +func TestDisconnectSource_Disabled(t *testing.T) { + s := &disconnectSource{cache: newFeedCache(time.Hour, disconnectFetch("http://nope"))} + r := s.Query(context.Background(), "tracker.com", "tracker.com", sdk.CheckerOptions{"enable_disconnect": false})[0] + if r.Enabled { + t.Errorf("expected disabled result, got %+v", r) + } +} + func TestDisconnectSource_HTTPError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) @@ -110,7 +120,7 @@ func TestDisconnectSource_HTTPError(t *testing.T) { defer srv.Close() s := newDisconnectTestSource(srv) - r := s.Query(context.Background(), "tracker.com", "tracker.com", nil)[0] + r := s.Query(context.Background(), "tracker.com", "tracker.com", sdk.CheckerOptions{"enable_disconnect": true})[0] if r.Error == "" { t.Errorf("expected error on HTTP 500, got empty error") } diff --git a/checker/malwarebazaar.go b/checker/malwarebazaar.go index 6bd5b71..9e1af6b 100644 --- a/checker/malwarebazaar.go +++ b/checker/malwarebazaar.go @@ -22,13 +22,6 @@ type malwareBazaarSource struct { func (*malwareBazaarSource) ID() string { return "malwarebazaar" } func (*malwareBazaarSource) Name() string { return "abuse.ch MalwareBazaar" } -func (s *malwareBazaarSource) Precheck(ctx context.Context, opts sdk.CheckerOptions) error { - if stringOpt(opts, "malwarebazaar_auth_key") == "" { - return fmt.Errorf("MalwareBazaar Auth-Key is not configured") - } - return nil -} - func (*malwareBazaarSource) Options() SourceOptions { return SourceOptions{ Admin: []sdk.CheckerOptionField{ @@ -40,12 +33,21 @@ func (*malwareBazaarSource) Options() SourceOptions { Secret: true, }, }, + User: []sdk.CheckerOptionField{ + { + Id: "enable_malwarebazaar", + Type: "bool", + Label: "Use abuse.ch MalwareBazaar", + Description: "Search MalwareBazaar for malware samples tagged with the domain (typically C2 infrastructure or delivery hosts).", + Default: true, + }, + }, } } func (s *malwareBazaarSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { authKey := stringOpt(opts, "malwarebazaar_auth_key") - if registered == "" || authKey == "" { + if !sdk.GetBoolOption(opts, "enable_malwarebazaar", true) || registered == "" || authKey == "" { return disabledResult(s.ID(), s.Name()) } diff --git a/checker/malwarebazaar_test.go b/checker/malwarebazaar_test.go index 3988ed4..62bc24a 100644 --- a/checker/malwarebazaar_test.go +++ b/checker/malwarebazaar_test.go @@ -17,7 +17,7 @@ func TestMalwareBazaarSource_NoResults(t *testing.T) { defer srv.Close() s := &malwareBazaarSource{endpoint: srv.URL} - results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"malwarebazaar_auth_key": "k"}) + results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_malwarebazaar": true, "malwarebazaar_auth_key": "k"}) if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } @@ -48,7 +48,7 @@ func TestMalwareBazaarSource_Listed(t *testing.T) { defer srv.Close() s := &malwareBazaarSource{endpoint: srv.URL} - r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"malwarebazaar_auth_key": "k"})[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_malwarebazaar": true, "malwarebazaar_auth_key": "k"})[0] if len(r.Evidence) != 1 { t.Fatalf("expected 1 evidence item, got %+v", r) } @@ -71,15 +71,23 @@ func TestMalwareBazaarSource_HTTPError(t *testing.T) { defer srv.Close() s := &malwareBazaarSource{endpoint: srv.URL} - r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"malwarebazaar_auth_key": "k"})[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_malwarebazaar": true, "malwarebazaar_auth_key": "k"})[0] if r.Error == "" { t.Errorf("expected error, got %+v", r) } } +func TestMalwareBazaarSource_Disabled(t *testing.T) { + s := &malwareBazaarSource{endpoint: "http://nope"} + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_malwarebazaar": false})[0] + if r.Enabled { + t.Errorf("expected disabled, got %+v", r) + } +} + func TestMalwareBazaarSource_NoAuthKey(t *testing.T) { s := &malwareBazaarSource{endpoint: "http://nope"} - r := s.Query(context.Background(), "example.com", "example.com", nil)[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_malwarebazaar": true})[0] if r.Enabled { t.Errorf("expected disabled when no auth key, got %+v", r) } diff --git a/checker/oisd.go b/checker/oisd.go index 4b3dc0d..39773a1 100644 --- a/checker/oisd.go +++ b/checker/oisd.go @@ -37,6 +37,15 @@ func (*oisdSource) Name() string { return "OISD domain blocklist" } func (*oisdSource) Options() SourceOptions { return SourceOptions{ + User: []sdk.CheckerOptionField{ + { + Id: "enable_oisd", + Type: "bool", + Label: "Use the OISD domain blocklist", + Description: "Download the OISD domain blocklist (refreshed every 24h) and check the domain against it.", + Default: true, + }, + }, Admin: []sdk.CheckerOptionField{ { Id: "oisd_variant", @@ -50,7 +59,7 @@ func (*oisdSource) Options() SourceOptions { } func (s *oisdSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { - if registered == "" { + if !sdk.GetBoolOption(opts, "enable_oisd", true) || registered == "" { return disabledResult(s.ID(), s.Name()) } diff --git a/checker/oisd_test.go b/checker/oisd_test.go index 12db986..93655b2 100644 --- a/checker/oisd_test.go +++ b/checker/oisd_test.go @@ -5,6 +5,8 @@ import ( "net/http" "net/http/httptest" "testing" + + sdk "git.happydns.org/checker-sdk-go/checker" ) const oisdFakeFeed = `! OISD big domainswild @@ -21,7 +23,7 @@ func TestOisdSource_Listed_ExactMatch(t *testing.T) { defer srv.Close() s := &oisdSource{bigCache: newOisdCache(srv.URL)} - r := s.Query(context.Background(), "evil.com", "evil.com", nil)[0] + r := s.Query(context.Background(), "evil.com", "evil.com", sdk.CheckerOptions{"enable_oisd": true})[0] if !r.Enabled || r.Error != "" { t.Fatalf("expected enabled and no error, got %+v", r) @@ -43,7 +45,7 @@ func TestOisdSource_Listed_SubdomainInFeed(t *testing.T) { // Feed has "*.malware.example.org" → stored as "malware.example.org"; querying // registered "example.org" should match via suffix check. s := &oisdSource{bigCache: newOisdCache(srv.URL)} - r := s.Query(context.Background(), "sub.example.org", "example.org", nil)[0] + r := s.Query(context.Background(), "sub.example.org", "example.org", sdk.CheckerOptions{"enable_oisd": true})[0] if len(r.Evidence) != 1 || r.Evidence[0].Value != "malware.example.org" { t.Errorf("expected subdomain match, got %+v", r.Evidence) @@ -60,7 +62,7 @@ func TestOisdSource_NotListed(t *testing.T) { defer srv.Close() s := &oisdSource{bigCache: newOisdCache(srv.URL)} - r := s.Query(context.Background(), "safe.com", "safe.com", nil)[0] + r := s.Query(context.Background(), "safe.com", "safe.com", sdk.CheckerOptions{"enable_oisd": true})[0] if !r.Enabled || r.Error != "" || len(r.Evidence) != 0 { t.Fatalf("expected clean result, got %+v", r) @@ -70,6 +72,14 @@ func TestOisdSource_NotListed(t *testing.T) { } } +func TestOisdSource_Disabled(t *testing.T) { + s := &oisdSource{bigCache: newOisdCache("http://nope")} + r := s.Query(context.Background(), "evil.com", "evil.com", sdk.CheckerOptions{"enable_oisd": false})[0] + if r.Enabled { + t.Errorf("expected disabled, got %+v", r) + } +} + func TestOisdSource_HTTPError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) @@ -77,7 +87,7 @@ func TestOisdSource_HTTPError(t *testing.T) { defer srv.Close() s := &oisdSource{bigCache: newOisdCache(srv.URL)} - r := s.Query(context.Background(), "evil.com", "evil.com", nil)[0] + r := s.Query(context.Background(), "evil.com", "evil.com", sdk.CheckerOptions{"enable_oisd": true})[0] if r.Error == "" || r.Error != "oisd HTTP 500" { t.Errorf("expected HTTP 500 error, got %q", r.Error) diff --git a/checker/openphish.go b/checker/openphish.go index 042f4f2..e175f73 100644 --- a/checker/openphish.go +++ b/checker/openphish.go @@ -32,11 +32,21 @@ func (*openPhishSource) ID() string { return "openphish" } func (*openPhishSource) Name() string { return "OpenPhish feed" } func (*openPhishSource) Options() SourceOptions { - return SourceOptions{} + return SourceOptions{ + User: []sdk.CheckerOptionField{ + { + Id: "enable_openphish", + Type: "bool", + Label: "Use the OpenPhish public feed", + Description: "Download the OpenPhish public feed (refreshed every 12h) and check the domain against it.", + Default: true, + }, + }, + } } func (s *openPhishSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { - if registered == "" { + if !sdk.GetBoolOption(opts, "enable_openphish", true) || registered == "" { return disabledResult(s.ID(), s.Name()) } diff --git a/checker/otx.go b/checker/otx.go index 41300f5..d4e39cf 100644 --- a/checker/otx.go +++ b/checker/otx.go @@ -21,13 +21,6 @@ type otxSource struct{ endpoint string } func (*otxSource) ID() string { return "otx" } func (*otxSource) Name() string { return "AlienVault OTX" } -func (s *otxSource) Precheck(ctx context.Context, opts sdk.CheckerOptions) error { - if stringOpt(opts, "otx_api_key") == "" { - return fmt.Errorf("AlienVault OTX API key is not configured") - } - return nil -} - func (*otxSource) Options() SourceOptions { return SourceOptions{ Admin: []sdk.CheckerOptionField{ diff --git a/checker/phishtank.go b/checker/phishtank.go index 12d808e..d3505b4 100644 --- a/checker/phishtank.go +++ b/checker/phishtank.go @@ -40,11 +40,20 @@ func (*phishTankSource) Options() SourceOptions { Default: "12", }, }, + User: []sdk.CheckerOptionField{ + { + Id: "enable_phishtank", + Type: "bool", + Label: "Use the PhishTank feed", + Description: "Download the PhishTank verified phishing list and check the domain against it.", + Default: true, + }, + }, } } func (s *phishTankSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { - if registered == "" { + if !sdk.GetBoolOption(opts, "enable_phishtank", true) || registered == "" { return disabledResult(s.ID(), s.Name()) } diff --git a/checker/precheck_test.go b/checker/precheck_test.go deleted file mode 100644 index 79d159e..0000000 --- a/checker/precheck_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package checker - -import ( - "context" - "strings" - "testing" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -// TestSourcePrechecks covers every Source that implements SourcePrecheck: -// without the required option Precheck must return a non-nil error, and -// with the option set it must return nil. -func TestSourcePrechecks(t *testing.T) { - cases := []struct { - name string - src SourcePrecheck - optKey string - hint string // substring expected in the error message - }{ - {"safebrowsing", &safeBrowsingSource{}, "google_safe_browsing_api_key", "Safe Browsing"}, - {"virustotal", &virusTotalSource{}, "virustotal_api_key", "VirusTotal"}, - {"otx", &otxSource{}, "otx_api_key", "OTX"}, - {"pulsedive", &pulsediveSource{}, "pulsedive_api_key", "Pulsedive"}, - {"criminalip", &criminalIPSource{}, "criminal_ip_api_key", "Criminal IP"}, - {"malwarebazaar", &malwareBazaarSource{endpoint: "http://nope"}, "malwarebazaar_auth_key", "MalwareBazaar"}, - {"threatfox", &threatFoxSource{endpoint: "http://nope"}, "threatfox_auth_key", "ThreatFox"}, - {"urlhaus", &urlhausSource{endpoint: "http://nope"}, "urlhaus_auth_key", "URLhaus"}, - } - - for _, c := range cases { - t.Run(c.name+"/missing", func(t *testing.T) { - err := c.src.Precheck(context.Background(), nil) - if err == nil { - t.Fatalf("expected error when %q is missing, got nil", c.optKey) - } - if !strings.Contains(err.Error(), c.hint) { - t.Errorf("error %q does not mention %q", err.Error(), c.hint) - } - }) - t.Run(c.name+"/set", func(t *testing.T) { - err := c.src.Precheck(context.Background(), sdk.CheckerOptions{c.optKey: "k"}) - if err != nil { - t.Errorf("expected nil when %q is set, got %v", c.optKey, err) - } - }) - } -} - -// TestSourceRule_PrecheckDelegation ensures sourceRule satisfies -// sdk.RulePrecheck and that the delegation through SourcePrecheck -// works end-to-end. Sources that do not implement SourcePrecheck must -// report "available" (nil error). -func TestSourceRule_PrecheckDelegation(t *testing.T) { - gated := &sourceRule{src: &urlhausSource{endpoint: "http://nope"}} - if err := gated.Precheck(context.Background(), nil); err == nil { - t.Errorf("urlhaus sourceRule.Precheck with empty opts: expected error, got nil") - } - if err := gated.Precheck(context.Background(), sdk.CheckerOptions{"urlhaus_auth_key": "k"}); err != nil { - t.Errorf("urlhaus sourceRule.Precheck with key set: expected nil, got %v", err) - } - - open := &sourceRule{src: &botvrijSource{cache: newBotvrijCache("http://nope")}} - if err := open.Precheck(context.Background(), nil); err != nil { - t.Errorf("botvrij sourceRule.Precheck: expected nil (no SourcePrecheck), got %v", err) - } - - // Confirm the type assertion the SDK server relies on succeeds. - var _ sdk.RulePrecheck = gated - var _ sdk.RulePrecheck = open -} diff --git a/checker/pulsedive.go b/checker/pulsedive.go index 987d7bd..edfeb1d 100644 --- a/checker/pulsedive.go +++ b/checker/pulsedive.go @@ -20,13 +20,6 @@ type pulsediveSource struct{ endpoint string } func (*pulsediveSource) ID() string { return "pulsedive" } func (*pulsediveSource) Name() string { return "Pulsedive" } -func (s *pulsediveSource) Precheck(ctx context.Context, opts sdk.CheckerOptions) error { - if stringOpt(opts, "pulsedive_api_key") == "" { - return fmt.Errorf("Pulsedive API key is not configured") - } - return nil -} - func (*pulsediveSource) Options() SourceOptions { return SourceOptions{ Admin: []sdk.CheckerOptionField{ diff --git a/checker/quad9.go b/checker/quad9.go index 6f92782..7bf6094 100644 --- a/checker/quad9.go +++ b/checker/quad9.go @@ -36,11 +36,21 @@ func (*quad9Source) ID() string { return "quad9" } func (*quad9Source) Name() string { return "Quad9 secure DNS" } func (*quad9Source) Options() SourceOptions { - return SourceOptions{} + return SourceOptions{ + User: []sdk.CheckerOptionField{ + { + Id: "enable_quad9", + Type: "bool", + Label: "Use Quad9 secure DNS check", + Description: "Compare Quad9's secure resolver (9.9.9.9) against its unsecured peer (9.9.9.10). A domain that resolves on the unsecured but returns NXDOMAIN on the secure resolver is blocked by Quad9's threat intelligence.", + Default: true, + }, + }, + } } func (s *quad9Source) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { - if registered == "" { + if !sdk.GetBoolOption(opts, "enable_quad9", true) || registered == "" { return disabledResult(s.ID(), s.Name()) } diff --git a/checker/rule.go b/checker/rule.go index ce0cc70..d904836 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -31,16 +31,6 @@ 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{ diff --git a/checker/safebrowsing.go b/checker/safebrowsing.go index efaa17f..f04ec70 100644 --- a/checker/safebrowsing.go +++ b/checker/safebrowsing.go @@ -25,13 +25,6 @@ type safeBrowsingSource struct { func (*safeBrowsingSource) ID() string { return "google_safe_browsing" } func (*safeBrowsingSource) Name() string { return "Google Safe Browsing" } -func (s *safeBrowsingSource) Precheck(ctx context.Context, opts sdk.CheckerOptions) error { - if stringOpt(opts, "google_safe_browsing_api_key") == "" { - return fmt.Errorf("Google Safe Browsing API key is not configured") - } - return nil -} - func (*safeBrowsingSource) Options() SourceOptions { return SourceOptions{ Admin: []sdk.CheckerOptionField{ diff --git a/checker/source.go b/checker/source.go index 1320994..03f47fc 100644 --- a/checker/source.go +++ b/checker/source.go @@ -57,17 +57,6 @@ type Source interface { Evaluate(r SourceResult) (listed bool, severity string) } -// SourcePrecheck is an optional interface a Source can implement to -// declare whether the current options are sufficient for it to run. -// Used to surface "rule unavailable because the operator hasn't -// configured the credentials yet" in the host UI via the SDK's -// RulePrecheck contract. Returning nil means "ready to run"; any error -// is shown verbatim to the operator. -type SourcePrecheck interface { - Source - Precheck(ctx context.Context, opts sdk.CheckerOptions) error -} - // DetailRenderer is an optional interface a Source can implement when // the generic SourceResult shape (Reasons + Evidence + URLs) cannot // fully express its output. Examples: VirusTotal's per-vendor verdict diff --git a/checker/threatfox.go b/checker/threatfox.go index f38bd8f..4df96fa 100644 --- a/checker/threatfox.go +++ b/checker/threatfox.go @@ -22,13 +22,6 @@ type threatFoxSource struct { func (*threatFoxSource) ID() string { return "threatfox" } func (*threatFoxSource) Name() string { return "abuse.ch ThreatFox" } -func (s *threatFoxSource) Precheck(ctx context.Context, opts sdk.CheckerOptions) error { - if stringOpt(opts, "threatfox_auth_key") == "" { - return fmt.Errorf("ThreatFox Auth-Key is not configured") - } - return nil -} - func (*threatFoxSource) Options() SourceOptions { return SourceOptions{ Admin: []sdk.CheckerOptionField{ @@ -40,12 +33,21 @@ func (*threatFoxSource) Options() SourceOptions { Secret: true, }, }, + User: []sdk.CheckerOptionField{ + { + Id: "enable_threatfox", + Type: "bool", + Label: "Use abuse.ch ThreatFox", + Description: "Query ThreatFox for indicators of compromise (C2 servers, malware, phishing) associated with the domain.", + Default: true, + }, + }, } } func (s *threatFoxSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { authKey := stringOpt(opts, "threatfox_auth_key") - if registered == "" || authKey == "" { + if !sdk.GetBoolOption(opts, "enable_threatfox", true) || registered == "" || authKey == "" { return disabledResult(s.ID(), s.Name()) } diff --git a/checker/threatfox_test.go b/checker/threatfox_test.go index a1ff480..5e5f12f 100644 --- a/checker/threatfox_test.go +++ b/checker/threatfox_test.go @@ -17,7 +17,7 @@ func TestThreatFoxSource_NoResult(t *testing.T) { defer srv.Close() s := &threatFoxSource{endpoint: srv.URL} - results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"threatfox_auth_key": "k"}) + results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_threatfox": true, "threatfox_auth_key": "k"}) if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } @@ -50,7 +50,7 @@ func TestThreatFoxSource_Listed(t *testing.T) { defer srv.Close() s := &threatFoxSource{endpoint: srv.URL} - r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"threatfox_auth_key": "k"})[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_threatfox": true, "threatfox_auth_key": "k"})[0] if len(r.Evidence) != 1 { t.Fatalf("expected 1 evidence item, got %+v", r) } @@ -73,15 +73,23 @@ func TestThreatFoxSource_HTTPError(t *testing.T) { defer srv.Close() s := &threatFoxSource{endpoint: srv.URL} - r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"threatfox_auth_key": "k"})[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_threatfox": true, "threatfox_auth_key": "k"})[0] if r.Error == "" { t.Errorf("expected error, got %+v", r) } } +func TestThreatFoxSource_Disabled(t *testing.T) { + s := &threatFoxSource{endpoint: "http://nope"} + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_threatfox": false})[0] + if r.Enabled { + t.Errorf("expected disabled, got %+v", r) + } +} + func TestThreatFoxSource_NoAuthKey(t *testing.T) { s := &threatFoxSource{endpoint: "http://nope"} - r := s.Query(context.Background(), "example.com", "example.com", nil)[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_threatfox": true})[0] if r.Enabled { t.Errorf("expected disabled when no auth key, got %+v", r) } diff --git a/checker/urlhaus.go b/checker/urlhaus.go index 8f00305..1bac676 100644 --- a/checker/urlhaus.go +++ b/checker/urlhaus.go @@ -25,15 +25,17 @@ type urlhausSource struct { func (*urlhausSource) ID() string { return "urlhaus" } func (*urlhausSource) Name() string { return "abuse.ch URLhaus" } -func (s *urlhausSource) Precheck(ctx context.Context, opts sdk.CheckerOptions) error { - if stringOpt(opts, "urlhaus_auth_key") == "" { - return fmt.Errorf("URLhaus Auth-Key is not configured") - } - return nil -} - func (*urlhausSource) Options() SourceOptions { return SourceOptions{ + User: []sdk.CheckerOptionField{ + { + Id: "enable_urlhaus", + Type: "bool", + Label: "Use abuse.ch URLhaus", + Description: "Query the URLhaus host endpoint for active malware-distribution URLs hosted on the domain.", + Default: true, + }, + }, Admin: []sdk.CheckerOptionField{ { Id: "urlhaus_auth_key", @@ -64,7 +66,7 @@ type urlhausURL struct { func (s *urlhausSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { authKey := stringOpt(opts, "urlhaus_auth_key") - if registered == "" || authKey == "" { + if !sdk.GetBoolOption(opts, "enable_urlhaus", true) || registered == "" || authKey == "" { return disabledResult(s.ID(), s.Name()) } diff --git a/checker/urlhaus_test.go b/checker/urlhaus_test.go index 045322f..9128efd 100644 --- a/checker/urlhaus_test.go +++ b/checker/urlhaus_test.go @@ -19,7 +19,7 @@ func TestURLhausSource_NoResults(t *testing.T) { defer srv.Close() s := &urlhausSource{endpoint: srv.URL} - results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"urlhaus_auth_key": "k"}) + results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_urlhaus": true, "urlhaus_auth_key": "k"}) if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } @@ -48,7 +48,7 @@ func TestURLhausSource_Listed(t *testing.T) { defer srv.Close() s := &urlhausSource{endpoint: srv.URL} - r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"urlhaus_auth_key": "k"})[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_urlhaus": true, "urlhaus_auth_key": "k"})[0] if len(r.Evidence) != 1 { t.Fatalf("expected 1 evidence item, got %+v", r) } @@ -80,16 +80,16 @@ func TestURLhausSource_HTTPError(t *testing.T) { defer srv.Close() s := &urlhausSource{endpoint: srv.URL} - r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"urlhaus_auth_key": "k"})[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_urlhaus": true, "urlhaus_auth_key": "k"})[0] if r.Error == "" || !strings.Contains(r.Error, "401") { t.Errorf("expected 401 error, got %+v", r) } } -func TestURLhausSource_NoAuthKey(t *testing.T) { +func TestURLhausSource_Disabled(t *testing.T) { s := &urlhausSource{endpoint: "http://nope"} - r := s.Query(context.Background(), "example.com", "example.com", nil)[0] + r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_urlhaus": false})[0] if r.Enabled { - t.Errorf("expected disabled when no auth key, got %+v", r) + t.Errorf("expected disabled, got %+v", r) } } diff --git a/checker/virustotal.go b/checker/virustotal.go index c245cd2..14d1132 100644 --- a/checker/virustotal.go +++ b/checker/virustotal.go @@ -24,13 +24,6 @@ type virusTotalSource struct { func (*virusTotalSource) ID() string { return "virustotal" } func (*virusTotalSource) Name() string { return "VirusTotal" } -func (s *virusTotalSource) Precheck(ctx context.Context, opts sdk.CheckerOptions) error { - if stringOpt(opts, "virustotal_api_key") == "" { - return fmt.Errorf("VirusTotal API key is not configured") - } - return nil -} - func (*virusTotalSource) Options() SourceOptions { return SourceOptions{ Admin: []sdk.CheckerOptionField{ diff --git a/go.mod b/go.mod index f161980..cc71d5d 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.9.0 + git.happydns.org/checker-sdk-go v1.8.0 golang.org/x/net v0.34.0 ) diff --git a/go.sum b/go.sum index 9c26370..d14c68e 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -git.happydns.org/checker-sdk-go v1.9.0 h1:orBRymir+p6PMHVa4focryPKhTVWT7JAv6u9Ido5KF0= -git.happydns.org/checker-sdk-go v1.9.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-sdk-go v1.8.0 h1:2lhcSc16rnCaszdQi1nerszb2c3fVh5XNS11pLrXuK4= +git.happydns.org/checker-sdk-go v1.8.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=