Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3cda1f104 | |||
| ce59a976d5 | |||
| 6719e21b51 |
28 changed files with 286 additions and 225 deletions
|
|
@ -33,21 +33,11 @@ func (*botvrijSource) ID() string { return "botvrij" }
|
|||
func (*botvrijSource) Name() string { return "Botvrij.eu domain blocklist" }
|
||||
|
||||
func (*botvrijSource) Options() 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
return SourceOptions{}
|
||||
}
|
||||
|
||||
func (s *botvrijSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||
if !sdk.GetBoolOption(opts, "enable_botvrij", true) || registered == "" {
|
||||
if registered == "" {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
const botvrijFakeFeed = `# Botvrij.eu IOC list - domains
|
||||
|
|
@ -23,7 +21,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", sdk.CheckerOptions{"enable_botvrij": true})[0]
|
||||
r := s.Query(context.Background(), "evil.com", "evil.com", nil)[0]
|
||||
|
||||
if !r.Enabled || r.Error != "" {
|
||||
t.Fatalf("expected enabled and no error, got %+v", r)
|
||||
|
|
@ -44,7 +42,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", sdk.CheckerOptions{"enable_botvrij": true})[0]
|
||||
r := s.Query(context.Background(), "sub.example.org", "example.org", nil)[0]
|
||||
|
||||
if len(r.Evidence) != 1 || r.Evidence[0].Value != "malware.example.org" {
|
||||
t.Errorf("expected subdomain match, got %+v", r.Evidence)
|
||||
|
|
@ -61,7 +59,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", sdk.CheckerOptions{"enable_botvrij": true})[0]
|
||||
r := s.Query(context.Background(), "safe.com", "safe.com", nil)[0]
|
||||
|
||||
if !r.Enabled || r.Error != "" || len(r.Evidence) != 0 {
|
||||
t.Fatalf("expected clean result, got %+v", r)
|
||||
|
|
@ -71,14 +69,6 @@ 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)
|
||||
|
|
@ -86,7 +76,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", sdk.CheckerOptions{"enable_botvrij": true})[0]
|
||||
r := s.Query(context.Background(), "evil.com", "evil.com", nil)[0]
|
||||
|
||||
if r.Error == "" || r.Error != "botvrij HTTP 500" {
|
||||
t.Errorf("expected HTTP 500 error, got %q", r.Error)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ 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()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ 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{
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ import (
|
|||
// Version is overridden at link time by the standalone or plugin entrypoints.
|
||||
var Version = "built-in"
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
func Definition() *sdk.CheckerDefinition {
|
||||
opts := sdk.CheckerOptionsDocumentation{
|
||||
DomainOpts: []sdk.CheckerOptionDocumentation{
|
||||
|
|
@ -23,11 +22,6 @@ 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",
|
||||
|
|
|
|||
|
|
@ -26,23 +26,10 @@ func (*disconnectSource) ID() string { return "disconnect" }
|
|||
func (*disconnectSource) Name() string { return "Disconnect.me" }
|
||||
|
||||
func (*disconnectSource) Options() 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
return SourceOptions{}
|
||||
}
|
||||
|
||||
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}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
const disconnectFakeFeed = `{
|
||||
|
|
@ -42,7 +40,7 @@ func TestDisconnectSource_Listed(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
s := newDisconnectTestSource(srv)
|
||||
r := s.Query(context.Background(), "tracker.com", "tracker.com", sdk.CheckerOptions{"enable_disconnect": true})[0]
|
||||
r := s.Query(context.Background(), "tracker.com", "tracker.com", nil)[0]
|
||||
|
||||
if !r.Enabled || r.Error != "" {
|
||||
t.Fatalf("expected enabled with no error, got %+v", r)
|
||||
|
|
@ -75,7 +73,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", sdk.CheckerOptions{"enable_disconnect": true})[0]
|
||||
r := s.Query(context.Background(), "example.org", "example.org", nil)[0]
|
||||
|
||||
if !r.Enabled || r.Error != "" {
|
||||
t.Fatalf("expected enabled with no error, got %+v", r)
|
||||
|
|
@ -92,7 +90,7 @@ func TestDisconnectSource_NotListed(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
s := newDisconnectTestSource(srv)
|
||||
r := s.Query(context.Background(), "clean.example.com", "clean.example.com", sdk.CheckerOptions{"enable_disconnect": true})[0]
|
||||
r := s.Query(context.Background(), "clean.example.com", "clean.example.com", nil)[0]
|
||||
|
||||
if !r.Enabled || r.Error != "" {
|
||||
t.Fatalf("expected enabled with no error, got %+v", r)
|
||||
|
|
@ -105,14 +103,6 @@ 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)
|
||||
|
|
@ -120,7 +110,7 @@ func TestDisconnectSource_HTTPError(t *testing.T) {
|
|||
defer srv.Close()
|
||||
|
||||
s := newDisconnectTestSource(srv)
|
||||
r := s.Query(context.Background(), "tracker.com", "tracker.com", sdk.CheckerOptions{"enable_disconnect": true})[0]
|
||||
r := s.Query(context.Background(), "tracker.com", "tracker.com", nil)[0]
|
||||
if r.Error == "" {
|
||||
t.Errorf("expected error on HTTP 500, got empty error")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ 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{
|
||||
|
|
@ -33,21 +40,12 @@ 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 !sdk.GetBoolOption(opts, "enable_malwarebazaar", true) || registered == "" || authKey == "" {
|
||||
if registered == "" || authKey == "" {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{"enable_malwarebazaar": true, "malwarebazaar_auth_key": "k"})
|
||||
results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"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{"enable_malwarebazaar": true, "malwarebazaar_auth_key": "k"})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"malwarebazaar_auth_key": "k"})[0]
|
||||
if len(r.Evidence) != 1 {
|
||||
t.Fatalf("expected 1 evidence item, got %+v", r)
|
||||
}
|
||||
|
|
@ -71,23 +71,15 @@ 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{"enable_malwarebazaar": true, "malwarebazaar_auth_key": "k"})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"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", sdk.CheckerOptions{"enable_malwarebazaar": true})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", nil)[0]
|
||||
if r.Enabled {
|
||||
t.Errorf("expected disabled when no auth key, got %+v", r)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,15 +37,6 @@ 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",
|
||||
|
|
@ -59,7 +50,7 @@ func (*oisdSource) Options() SourceOptions {
|
|||
}
|
||||
|
||||
func (s *oisdSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||
if !sdk.GetBoolOption(opts, "enable_oisd", true) || registered == "" {
|
||||
if registered == "" {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
const oisdFakeFeed = `! OISD big domainswild
|
||||
|
|
@ -23,7 +21,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", sdk.CheckerOptions{"enable_oisd": true})[0]
|
||||
r := s.Query(context.Background(), "evil.com", "evil.com", nil)[0]
|
||||
|
||||
if !r.Enabled || r.Error != "" {
|
||||
t.Fatalf("expected enabled and no error, got %+v", r)
|
||||
|
|
@ -45,7 +43,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", sdk.CheckerOptions{"enable_oisd": true})[0]
|
||||
r := s.Query(context.Background(), "sub.example.org", "example.org", nil)[0]
|
||||
|
||||
if len(r.Evidence) != 1 || r.Evidence[0].Value != "malware.example.org" {
|
||||
t.Errorf("expected subdomain match, got %+v", r.Evidence)
|
||||
|
|
@ -62,7 +60,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", sdk.CheckerOptions{"enable_oisd": true})[0]
|
||||
r := s.Query(context.Background(), "safe.com", "safe.com", nil)[0]
|
||||
|
||||
if !r.Enabled || r.Error != "" || len(r.Evidence) != 0 {
|
||||
t.Fatalf("expected clean result, got %+v", r)
|
||||
|
|
@ -72,14 +70,6 @@ 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)
|
||||
|
|
@ -87,7 +77,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", sdk.CheckerOptions{"enable_oisd": true})[0]
|
||||
r := s.Query(context.Background(), "evil.com", "evil.com", nil)[0]
|
||||
|
||||
if r.Error == "" || r.Error != "oisd HTTP 500" {
|
||||
t.Errorf("expected HTTP 500 error, got %q", r.Error)
|
||||
|
|
|
|||
|
|
@ -32,21 +32,11 @@ func (*openPhishSource) ID() string { return "openphish" }
|
|||
func (*openPhishSource) Name() string { return "OpenPhish feed" }
|
||||
|
||||
func (*openPhishSource) Options() 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
return SourceOptions{}
|
||||
}
|
||||
|
||||
func (s *openPhishSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||
if !sdk.GetBoolOption(opts, "enable_openphish", true) || registered == "" {
|
||||
if registered == "" {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ 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{
|
||||
|
|
|
|||
|
|
@ -40,20 +40,11 @@ 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 !sdk.GetBoolOption(opts, "enable_phishtank", true) || registered == "" {
|
||||
if registered == "" {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
|
||||
|
|
|
|||
71
checker/precheck_test.go
Normal file
71
checker/precheck_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -20,6 +20,13 @@ 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{
|
||||
|
|
|
|||
|
|
@ -36,21 +36,11 @@ func (*quad9Source) ID() string { return "quad9" }
|
|||
func (*quad9Source) Name() string { return "Quad9 secure DNS" }
|
||||
|
||||
func (*quad9Source) Options() 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
return SourceOptions{}
|
||||
}
|
||||
|
||||
func (s *quad9Source) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||
if !sdk.GetBoolOption(opts, "enable_quad9", true) || registered == "" {
|
||||
if registered == "" {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,16 +21,17 @@ 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: diagnose(&data),
|
||||
Diagnoses: diagnoseFromStates(states, &data),
|
||||
Sections: buildSections(&data),
|
||||
CSS: template.CSS(reportCSS),
|
||||
}
|
||||
view.Headline, view.HeadlineClass = headline(view.TotalHits)
|
||||
view.Headline, view.HeadlineClass = headlineFromStates(states, view.TotalHits)
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := reportTemplate.Execute(&b, view); err != nil {
|
||||
|
|
@ -129,6 +130,54 @@ 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:
|
||||
|
|
|
|||
|
|
@ -7,23 +7,49 @@ import (
|
|||
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.
|
||||
// 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 {
|
||||
return []sdk.CheckRule{&sourceRule{}}
|
||||
srcs := Sources()
|
||||
rules := make([]sdk.CheckRule, 0, len(srcs))
|
||||
for _, s := range srcs {
|
||||
rules = append(rules, &sourceRule{src: s})
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
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."
|
||||
type sourceRule struct {
|
||||
src Source
|
||||
}
|
||||
|
||||
func (*sourceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
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{{
|
||||
|
|
@ -33,26 +59,20 @@ func (*sourceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts
|
|||
}}
|
||||
}
|
||||
|
||||
if len(data.Results) == 0 {
|
||||
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.StatusInfo, Message: "No reputation sources registered.",
|
||||
Code: "blacklist_no_sources",
|
||||
Status: sdk.StatusUnknown,
|
||||
Message: r.src.Name() + ": no result (source skipped or disabled).",
|
||||
Code: "source_disabled",
|
||||
}}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -105,17 +125,6 @@ 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:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ 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{
|
||||
|
|
|
|||
|
|
@ -57,6 +57,17 @@ 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
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ 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{
|
||||
|
|
@ -33,21 +40,12 @@ 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 !sdk.GetBoolOption(opts, "enable_threatfox", true) || registered == "" || authKey == "" {
|
||||
if registered == "" || authKey == "" {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{"enable_threatfox": true, "threatfox_auth_key": "k"})
|
||||
results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"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{"enable_threatfox": true, "threatfox_auth_key": "k"})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"threatfox_auth_key": "k"})[0]
|
||||
if len(r.Evidence) != 1 {
|
||||
t.Fatalf("expected 1 evidence item, got %+v", r)
|
||||
}
|
||||
|
|
@ -73,23 +73,15 @@ 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{"enable_threatfox": true, "threatfox_auth_key": "k"})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"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", sdk.CheckerOptions{"enable_threatfox": true})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", nil)[0]
|
||||
if r.Enabled {
|
||||
t.Errorf("expected disabled when no auth key, got %+v", r)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,15 @@ 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",
|
||||
|
|
@ -66,7 +64,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 !sdk.GetBoolOption(opts, "enable_urlhaus", true) || registered == "" || authKey == "" {
|
||||
if registered == "" || authKey == "" {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{"enable_urlhaus": true, "urlhaus_auth_key": "k"})
|
||||
results := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"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{"enable_urlhaus": true, "urlhaus_auth_key": "k"})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"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{"enable_urlhaus": true, "urlhaus_auth_key": "k"})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"urlhaus_auth_key": "k"})[0]
|
||||
if r.Error == "" || !strings.Contains(r.Error, "401") {
|
||||
t.Errorf("expected 401 error, got %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLhausSource_Disabled(t *testing.T) {
|
||||
func TestURLhausSource_NoAuthKey(t *testing.T) {
|
||||
s := &urlhausSource{endpoint: "http://nope"}
|
||||
r := s.Query(context.Background(), "example.com", "example.com", sdk.CheckerOptions{"enable_urlhaus": false})[0]
|
||||
r := s.Query(context.Background(), "example.com", "example.com", nil)[0]
|
||||
if r.Enabled {
|
||||
t.Errorf("expected disabled, got %+v", r)
|
||||
t.Errorf("expected disabled when no auth key, got %+v", r)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ 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{
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -3,6 +3,6 @@ module git.happydns.org/checker-blacklist
|
|||
go 1.25.0
|
||||
|
||||
require (
|
||||
git.happydns.org/checker-sdk-go v1.5.0
|
||||
git.happydns.org/checker-sdk-go v1.9.0
|
||||
golang.org/x/net v0.34.0
|
||||
)
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -1,4 +1,4 @@
|
|||
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=
|
||||
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=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue