Replace per-source enable booleans with SourcePrecheck and bump SDK to v1.9.0
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.
This commit is contained in:
parent
ce59a976d5
commit
c3cda1f104
25 changed files with 189 additions and 175 deletions
|
|
@ -33,21 +33,11 @@ func (*botvrijSource) ID() string { return "botvrij" }
|
||||||
func (*botvrijSource) Name() string { return "Botvrij.eu domain blocklist" }
|
func (*botvrijSource) Name() string { return "Botvrij.eu domain blocklist" }
|
||||||
|
|
||||||
func (*botvrijSource) Options() SourceOptions {
|
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 {
|
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())
|
return disabledResult(s.ID(), s.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const botvrijFakeFeed = `# Botvrij.eu IOC list - domains
|
const botvrijFakeFeed = `# Botvrij.eu IOC list - domains
|
||||||
|
|
@ -23,7 +21,7 @@ func TestBotvrijSource_Listed_ExactMatch(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &botvrijSource{cache: newBotvrijCache(srv.URL)}
|
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 != "" {
|
if !r.Enabled || r.Error != "" {
|
||||||
t.Fatalf("expected enabled and no error, got %+v", r)
|
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.
|
// Feed has "malware.example.org"; querying registered "example.org" should match.
|
||||||
s := &botvrijSource{cache: newBotvrijCache(srv.URL)}
|
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" {
|
if len(r.Evidence) != 1 || r.Evidence[0].Value != "malware.example.org" {
|
||||||
t.Errorf("expected subdomain match, got %+v", r.Evidence)
|
t.Errorf("expected subdomain match, got %+v", r.Evidence)
|
||||||
|
|
@ -61,7 +59,7 @@ func TestBotvrijSource_NotListed(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &botvrijSource{cache: newBotvrijCache(srv.URL)}
|
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 {
|
if !r.Enabled || r.Error != "" || len(r.Evidence) != 0 {
|
||||||
t.Fatalf("expected clean result, got %+v", r)
|
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) {
|
func TestBotvrijSource_HTTPError(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
@ -86,7 +76,7 @@ func TestBotvrijSource_HTTPError(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &botvrijSource{cache: newBotvrijCache(srv.URL)}
|
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" {
|
if r.Error == "" || r.Error != "botvrij HTTP 500" {
|
||||||
t.Errorf("expected HTTP 500 error, got %q", r.Error)
|
t.Errorf("expected HTTP 500 error, got %q", r.Error)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ type criminalIPSource struct{ endpoint string }
|
||||||
func (*criminalIPSource) ID() string { return "criminal_ip" }
|
func (*criminalIPSource) ID() string { return "criminal_ip" }
|
||||||
func (*criminalIPSource) Name() 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 {
|
func (*criminalIPSource) Options() SourceOptions {
|
||||||
return SourceOptions{
|
return SourceOptions{
|
||||||
Admin: []sdk.CheckerOptionField{
|
Admin: []sdk.CheckerOptionField{
|
||||||
|
|
|
||||||
|
|
@ -26,23 +26,10 @@ func (*disconnectSource) ID() string { return "disconnect" }
|
||||||
func (*disconnectSource) Name() string { return "Disconnect.me" }
|
func (*disconnectSource) Name() string { return "Disconnect.me" }
|
||||||
|
|
||||||
func (*disconnectSource) Options() SourceOptions {
|
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 {
|
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 == "" {
|
if registered == "" {
|
||||||
return []SourceResult{{SourceID: s.ID(), SourceName: s.Name(), Enabled: true}}
|
return []SourceResult{{SourceID: s.ID(), SourceName: s.Name(), Enabled: true}}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const disconnectFakeFeed = `{
|
const disconnectFakeFeed = `{
|
||||||
|
|
@ -42,7 +40,7 @@ func TestDisconnectSource_Listed(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := newDisconnectTestSource(srv)
|
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 != "" {
|
if !r.Enabled || r.Error != "" {
|
||||||
t.Fatalf("expected enabled with no error, got %+v", r)
|
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.
|
// Feed has "sub.example.org"; querying registered domain "example.org" should match.
|
||||||
s := newDisconnectTestSource(srv)
|
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 != "" {
|
if !r.Enabled || r.Error != "" {
|
||||||
t.Fatalf("expected enabled with no error, got %+v", r)
|
t.Fatalf("expected enabled with no error, got %+v", r)
|
||||||
|
|
@ -92,7 +90,7 @@ func TestDisconnectSource_NotListed(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := newDisconnectTestSource(srv)
|
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 != "" {
|
if !r.Enabled || r.Error != "" {
|
||||||
t.Fatalf("expected enabled with no error, got %+v", r)
|
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) {
|
func TestDisconnectSource_HTTPError(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
@ -120,7 +110,7 @@ func TestDisconnectSource_HTTPError(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := newDisconnectTestSource(srv)
|
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 == "" {
|
if r.Error == "" {
|
||||||
t.Errorf("expected error on HTTP 500, got empty 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) ID() string { return "malwarebazaar" }
|
||||||
func (*malwareBazaarSource) Name() string { return "abuse.ch 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 {
|
func (*malwareBazaarSource) Options() SourceOptions {
|
||||||
return SourceOptions{
|
return SourceOptions{
|
||||||
Admin: []sdk.CheckerOptionField{
|
Admin: []sdk.CheckerOptionField{
|
||||||
|
|
@ -33,21 +40,12 @@ func (*malwareBazaarSource) Options() SourceOptions {
|
||||||
Secret: true,
|
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 {
|
func (s *malwareBazaarSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||||
authKey := stringOpt(opts, "malwarebazaar_auth_key")
|
authKey := stringOpt(opts, "malwarebazaar_auth_key")
|
||||||
if !sdk.GetBoolOption(opts, "enable_malwarebazaar", true) || registered == "" || authKey == "" {
|
if registered == "" || authKey == "" {
|
||||||
return disabledResult(s.ID(), s.Name())
|
return disabledResult(s.ID(), s.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ func TestMalwareBazaarSource_NoResults(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &malwareBazaarSource{endpoint: srv.URL}
|
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 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("expected 1 result, got %d", len(results))
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ func TestMalwareBazaarSource_Listed(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &malwareBazaarSource{endpoint: srv.URL}
|
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 {
|
if len(r.Evidence) != 1 {
|
||||||
t.Fatalf("expected 1 evidence item, got %+v", r)
|
t.Fatalf("expected 1 evidence item, got %+v", r)
|
||||||
}
|
}
|
||||||
|
|
@ -71,23 +71,15 @@ func TestMalwareBazaarSource_HTTPError(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &malwareBazaarSource{endpoint: srv.URL}
|
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 == "" {
|
if r.Error == "" {
|
||||||
t.Errorf("expected error, got %+v", r)
|
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) {
|
func TestMalwareBazaarSource_NoAuthKey(t *testing.T) {
|
||||||
s := &malwareBazaarSource{endpoint: "http://nope"}
|
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 {
|
if r.Enabled {
|
||||||
t.Errorf("expected disabled when no auth key, got %+v", r)
|
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 {
|
func (*oisdSource) Options() SourceOptions {
|
||||||
return 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{
|
Admin: []sdk.CheckerOptionField{
|
||||||
{
|
{
|
||||||
Id: "oisd_variant",
|
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 {
|
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())
|
return disabledResult(s.ID(), s.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const oisdFakeFeed = `! OISD big domainswild
|
const oisdFakeFeed = `! OISD big domainswild
|
||||||
|
|
@ -23,7 +21,7 @@ func TestOisdSource_Listed_ExactMatch(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &oisdSource{bigCache: newOisdCache(srv.URL)}
|
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 != "" {
|
if !r.Enabled || r.Error != "" {
|
||||||
t.Fatalf("expected enabled and no error, got %+v", r)
|
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
|
// Feed has "*.malware.example.org" → stored as "malware.example.org"; querying
|
||||||
// registered "example.org" should match via suffix check.
|
// registered "example.org" should match via suffix check.
|
||||||
s := &oisdSource{bigCache: newOisdCache(srv.URL)}
|
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" {
|
if len(r.Evidence) != 1 || r.Evidence[0].Value != "malware.example.org" {
|
||||||
t.Errorf("expected subdomain match, got %+v", r.Evidence)
|
t.Errorf("expected subdomain match, got %+v", r.Evidence)
|
||||||
|
|
@ -62,7 +60,7 @@ func TestOisdSource_NotListed(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &oisdSource{bigCache: newOisdCache(srv.URL)}
|
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 {
|
if !r.Enabled || r.Error != "" || len(r.Evidence) != 0 {
|
||||||
t.Fatalf("expected clean result, got %+v", r)
|
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) {
|
func TestOisdSource_HTTPError(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
@ -87,7 +77,7 @@ func TestOisdSource_HTTPError(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &oisdSource{bigCache: newOisdCache(srv.URL)}
|
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" {
|
if r.Error == "" || r.Error != "oisd HTTP 500" {
|
||||||
t.Errorf("expected HTTP 500 error, got %q", r.Error)
|
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) Name() string { return "OpenPhish feed" }
|
||||||
|
|
||||||
func (*openPhishSource) Options() SourceOptions {
|
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 {
|
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())
|
return disabledResult(s.ID(), s.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@ type otxSource struct{ endpoint string }
|
||||||
func (*otxSource) ID() string { return "otx" }
|
func (*otxSource) ID() string { return "otx" }
|
||||||
func (*otxSource) Name() string { return "AlienVault 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 {
|
func (*otxSource) Options() SourceOptions {
|
||||||
return SourceOptions{
|
return SourceOptions{
|
||||||
Admin: []sdk.CheckerOptionField{
|
Admin: []sdk.CheckerOptionField{
|
||||||
|
|
|
||||||
|
|
@ -40,20 +40,11 @@ func (*phishTankSource) Options() SourceOptions {
|
||||||
Default: "12",
|
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 {
|
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())
|
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) ID() string { return "pulsedive" }
|
||||||
func (*pulsediveSource) Name() 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 {
|
func (*pulsediveSource) Options() SourceOptions {
|
||||||
return SourceOptions{
|
return SourceOptions{
|
||||||
Admin: []sdk.CheckerOptionField{
|
Admin: []sdk.CheckerOptionField{
|
||||||
|
|
|
||||||
|
|
@ -36,21 +36,11 @@ func (*quad9Source) ID() string { return "quad9" }
|
||||||
func (*quad9Source) Name() string { return "Quad9 secure DNS" }
|
func (*quad9Source) Name() string { return "Quad9 secure DNS" }
|
||||||
|
|
||||||
func (*quad9Source) Options() SourceOptions {
|
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 {
|
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())
|
return disabledResult(s.ID(), s.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@ 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())
|
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 {
|
func (r *sourceRule) Options() sdk.CheckerOptionsDocumentation {
|
||||||
o := r.src.Options()
|
o := r.src.Options()
|
||||||
return sdk.CheckerOptionsDocumentation{
|
return sdk.CheckerOptionsDocumentation{
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ type safeBrowsingSource struct {
|
||||||
func (*safeBrowsingSource) ID() string { return "google_safe_browsing" }
|
func (*safeBrowsingSource) ID() string { return "google_safe_browsing" }
|
||||||
func (*safeBrowsingSource) Name() 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 {
|
func (*safeBrowsingSource) Options() SourceOptions {
|
||||||
return SourceOptions{
|
return SourceOptions{
|
||||||
Admin: []sdk.CheckerOptionField{
|
Admin: []sdk.CheckerOptionField{
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,17 @@ type Source interface {
|
||||||
Evaluate(r SourceResult) (listed bool, severity string)
|
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
|
// DetailRenderer is an optional interface a Source can implement when
|
||||||
// the generic SourceResult shape (Reasons + Evidence + URLs) cannot
|
// the generic SourceResult shape (Reasons + Evidence + URLs) cannot
|
||||||
// fully express its output. Examples: VirusTotal's per-vendor verdict
|
// 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) ID() string { return "threatfox" }
|
||||||
func (*threatFoxSource) Name() string { return "abuse.ch 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 {
|
func (*threatFoxSource) Options() SourceOptions {
|
||||||
return SourceOptions{
|
return SourceOptions{
|
||||||
Admin: []sdk.CheckerOptionField{
|
Admin: []sdk.CheckerOptionField{
|
||||||
|
|
@ -33,21 +40,12 @@ func (*threatFoxSource) Options() SourceOptions {
|
||||||
Secret: true,
|
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 {
|
func (s *threatFoxSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||||
authKey := stringOpt(opts, "threatfox_auth_key")
|
authKey := stringOpt(opts, "threatfox_auth_key")
|
||||||
if !sdk.GetBoolOption(opts, "enable_threatfox", true) || registered == "" || authKey == "" {
|
if registered == "" || authKey == "" {
|
||||||
return disabledResult(s.ID(), s.Name())
|
return disabledResult(s.ID(), s.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ func TestThreatFoxSource_NoResult(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &threatFoxSource{endpoint: srv.URL}
|
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 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("expected 1 result, got %d", len(results))
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ func TestThreatFoxSource_Listed(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &threatFoxSource{endpoint: srv.URL}
|
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 {
|
if len(r.Evidence) != 1 {
|
||||||
t.Fatalf("expected 1 evidence item, got %+v", r)
|
t.Fatalf("expected 1 evidence item, got %+v", r)
|
||||||
}
|
}
|
||||||
|
|
@ -73,23 +73,15 @@ func TestThreatFoxSource_HTTPError(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &threatFoxSource{endpoint: srv.URL}
|
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 == "" {
|
if r.Error == "" {
|
||||||
t.Errorf("expected error, got %+v", r)
|
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) {
|
func TestThreatFoxSource_NoAuthKey(t *testing.T) {
|
||||||
s := &threatFoxSource{endpoint: "http://nope"}
|
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 {
|
if r.Enabled {
|
||||||
t.Errorf("expected disabled when no auth key, got %+v", r)
|
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) ID() string { return "urlhaus" }
|
||||||
func (*urlhausSource) Name() string { return "abuse.ch 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 {
|
func (*urlhausSource) Options() SourceOptions {
|
||||||
return 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{
|
Admin: []sdk.CheckerOptionField{
|
||||||
{
|
{
|
||||||
Id: "urlhaus_auth_key",
|
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 {
|
func (s *urlhausSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||||
authKey := stringOpt(opts, "urlhaus_auth_key")
|
authKey := stringOpt(opts, "urlhaus_auth_key")
|
||||||
if !sdk.GetBoolOption(opts, "enable_urlhaus", true) || registered == "" || authKey == "" {
|
if registered == "" || authKey == "" {
|
||||||
return disabledResult(s.ID(), s.Name())
|
return disabledResult(s.ID(), s.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func TestURLhausSource_NoResults(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &urlhausSource{endpoint: srv.URL}
|
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 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("expected 1 result, got %d", len(results))
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ func TestURLhausSource_Listed(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &urlhausSource{endpoint: srv.URL}
|
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 {
|
if len(r.Evidence) != 1 {
|
||||||
t.Fatalf("expected 1 evidence item, got %+v", r)
|
t.Fatalf("expected 1 evidence item, got %+v", r)
|
||||||
}
|
}
|
||||||
|
|
@ -80,16 +80,16 @@ func TestURLhausSource_HTTPError(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
s := &urlhausSource{endpoint: srv.URL}
|
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") {
|
if r.Error == "" || !strings.Contains(r.Error, "401") {
|
||||||
t.Errorf("expected 401 error, got %+v", r)
|
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"}
|
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 {
|
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) ID() string { return "virustotal" }
|
||||||
func (*virusTotalSource) Name() 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 {
|
func (*virusTotalSource) Options() SourceOptions {
|
||||||
return SourceOptions{
|
return SourceOptions{
|
||||||
Admin: []sdk.CheckerOptionField{
|
Admin: []sdk.CheckerOptionField{
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -3,6 +3,6 @@ module git.happydns.org/checker-blacklist
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.happydns.org/checker-sdk-go v1.8.0
|
git.happydns.org/checker-sdk-go v1.9.0
|
||||||
golang.org/x/net v0.34.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.8.0 h1:2lhcSc16rnCaszdQi1nerszb2c3fVh5XNS11pLrXuK4=
|
git.happydns.org/checker-sdk-go v1.9.0 h1:orBRymir+p6PMHVa4focryPKhTVWT7JAv6u9Ido5KF0=
|
||||||
git.happydns.org/checker-sdk-go v1.8.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
|
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 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue