checker-blacklist/checker/malwarebazaar.go
Pierre-Olivier Mercier 6b1d2e2540 Extract disabledResult and evidenceEval helpers to reduce boilerplate
Add two shared helpers to source.go and apply them across all sources:
- disabledResult(id, name) replaces the repeated inline SourceResult literal
- evidenceEval(r, severity) replaces the identical Evaluate body in 6 sources
2026-05-15 21:36:24 +08:00

146 lines
4.2 KiB
Go

package checker
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
const malwareBazaarEndpoint = "https://mb-api.abuse.ch/api/v1/"
func init() { Register(&malwareBazaarSource{endpoint: malwareBazaarEndpoint}) }
type malwareBazaarSource struct {
endpoint string
}
func (*malwareBazaarSource) ID() string { return "malwarebazaar" }
func (*malwareBazaarSource) Name() string { return "abuse.ch MalwareBazaar" }
func (*malwareBazaarSource) Options() SourceOptions {
return SourceOptions{
Admin: []sdk.CheckerOptionField{
{
Id: "malwarebazaar_auth_key",
Type: "string",
Label: "MalwareBazaar Auth-Key",
Description: "abuse.ch MalwareBazaar Auth-Key (free, requires an abuse.ch account). Without this key the source is disabled.",
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 == "" {
return disabledResult(s.ID(), s.Name())
}
res := SourceResult{
SourceID: s.ID(), SourceName: s.Name(), Enabled: true,
Reference: "https://bazaar.abuse.ch/browse/",
}
buf, err := json.Marshal(map[string]any{
"query": "search_tag",
"tag": registered,
})
if err != nil {
res.Error = err.Error()
return []SourceResult{res}
}
reqCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, s.endpoint, bytes.NewReader(buf))
if err != nil {
res.Error = err.Error()
return []SourceResult{res}
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Auth-Key", authKey)
body, status, err := httpDo(req, 4<<20)
if err != nil {
res.Error = err.Error()
return []SourceResult{res}
}
if status != http.StatusOK {
res.Error = fmt.Sprintf("HTTP %d: %s", status, truncate(string(body), 200))
return []SourceResult{res}
}
var parsed struct {
QueryStatus string `json:"query_status"`
Data []struct {
SHA256Hash string `json:"sha256_hash"`
FileName string `json:"file_name"`
FileType string `json:"file_type_mime"`
Signature string `json:"signature"`
FirstSeen string `json:"first_seen"`
} `json:"data"`
}
if err := json.Unmarshal(body, &parsed); err != nil {
res.Error = "decode: " + err.Error()
return []SourceResult{res}
}
switch parsed.QueryStatus {
case "ok":
signatures := map[string]bool{}
for _, sample := range parsed.Data {
if sample.Signature != "" && !signatures[sample.Signature] {
signatures[sample.Signature] = true
res.Reasons = append(res.Reasons, sample.Signature)
}
res.Evidence = append(res.Evidence, Evidence{
Label: "Sample",
Value: sample.SHA256Hash,
Status: sample.FileType,
Extra: map[string]string{
"filename": sample.FileName,
"signature": sample.Signature,
"first_seen": sample.FirstSeen,
},
})
}
case "no_results", "illegal_search_term":
// Clean.
default:
res.Error = "query_status=" + parsed.QueryStatus
}
return []SourceResult{res}
}
func (*malwareBazaarSource) Evaluate(r SourceResult) (bool, string) {
return evidenceEval(r, SeverityWarn)
}
func (*malwareBazaarSource) Diagnose(res SourceResult) Diagnosis {
return Diagnosis{
Severity: SeverityWarn,
Title: "Associated with malware samples in abuse.ch MalwareBazaar",
Detail: fmt.Sprintf(
"%d malware sample(s) are tagged with this domain; malware family/signature(s): %s. MalwareBazaar samples are tagged with their C2 domain or delivery host when known. Investigate the domain's hosting and DNS records to identify and remove malicious infrastructure.",
len(res.Evidence), joinNonEmpty(res.Reasons, ", "),
),
Fix: res.Reference,
FixIsURL: res.Reference != "",
}
}