Add abuse.ch ThreatFox and MalwareBazaar blacklist sources
ThreatFox queries the IOC database for domain indicators (C2 servers, malware distribution, phishing); MalwareBazaar searches for malware samples tagged with the domain. Both require a free abuse.ch Auth-Key.
This commit is contained in:
parent
6b08676ec5
commit
229e7a8f02
5 changed files with 496 additions and 0 deletions
155
checker/threatfox.go
Normal file
155
checker/threatfox.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
const threatFoxEndpoint = "https://threatfox-api.abuse.ch/api/v1/"
|
||||
|
||||
func init() { Register(&threatFoxSource{endpoint: threatFoxEndpoint}) }
|
||||
|
||||
type threatFoxSource struct {
|
||||
endpoint string
|
||||
}
|
||||
|
||||
func (*threatFoxSource) ID() string { return "threatfox" }
|
||||
func (*threatFoxSource) Name() string { return "abuse.ch ThreatFox" }
|
||||
|
||||
func (*threatFoxSource) Options() SourceOptions {
|
||||
return SourceOptions{
|
||||
Admin: []sdk.CheckerOptionField{
|
||||
{
|
||||
Id: "threatfox_auth_key",
|
||||
Type: "string",
|
||||
Label: "ThreatFox Auth-Key",
|
||||
Description: "abuse.ch ThreatFox Auth-Key (free, requires an abuse.ch account). Without this key the source is disabled.",
|
||||
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 == "" {
|
||||
return []SourceResult{{SourceID: s.ID(), SourceName: s.Name(), Enabled: false}}
|
||||
}
|
||||
|
||||
res := SourceResult{
|
||||
SourceID: s.ID(), SourceName: s.Name(), Enabled: true,
|
||||
Reference: "https://threatfox.abuse.ch/browse/",
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(map[string]any{
|
||||
"query": "search_ioc",
|
||||
"search_term": 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 {
|
||||
IOCValue string `json:"ioc_value"`
|
||||
IOCType string `json:"ioc_type"`
|
||||
ThreatType string `json:"threat_type"`
|
||||
MalwarePrintable string `json:"malware_printable"`
|
||||
ConfidenceLevel int `json:"confidence_level"`
|
||||
FirstSeen string `json:"first_seen"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
res.Error = "decode: " + err.Error()
|
||||
return []SourceResult{res}
|
||||
}
|
||||
|
||||
switch parsed.QueryStatus {
|
||||
case "ok":
|
||||
threats := map[string]bool{}
|
||||
for _, ioc := range parsed.Data {
|
||||
label := ioc.MalwarePrintable
|
||||
if label == "" {
|
||||
label = ioc.ThreatType
|
||||
}
|
||||
if label != "" && !threats[label] {
|
||||
threats[label] = true
|
||||
res.Reasons = append(res.Reasons, label)
|
||||
}
|
||||
res.Evidence = append(res.Evidence, Evidence{
|
||||
Label: "IOC",
|
||||
Value: ioc.IOCValue,
|
||||
Status: ioc.ThreatType,
|
||||
Extra: map[string]string{
|
||||
"malware": ioc.MalwarePrintable,
|
||||
"confidence": fmt.Sprintf("%d%%", ioc.ConfidenceLevel),
|
||||
"first_seen": ioc.FirstSeen,
|
||||
},
|
||||
})
|
||||
}
|
||||
case "no_result":
|
||||
// Clean.
|
||||
default:
|
||||
res.Error = "query_status=" + parsed.QueryStatus
|
||||
}
|
||||
return []SourceResult{res}
|
||||
}
|
||||
|
||||
func (*threatFoxSource) Evaluate(r SourceResult) (bool, string) {
|
||||
if r.Enabled && r.Error == "" && len(r.Evidence) > 0 {
|
||||
return true, SeverityCrit
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (*threatFoxSource) Diagnose(res SourceResult) Diagnosis {
|
||||
return Diagnosis{
|
||||
Severity: SeverityCrit,
|
||||
Title: "Listed in abuse.ch ThreatFox as a threat IOC",
|
||||
Detail: fmt.Sprintf(
|
||||
"%d IOC(s) match this domain; threat(s): %s. ThreatFox tracks indicators of compromise including C2 servers, malware distribution hosts, and phishing infrastructure. Treat the host as compromised or abused: investigate recent DNS changes, audit hosted content, then request removal through the ThreatFox reference page.",
|
||||
len(res.Evidence), joinNonEmpty(res.Reasons, ", "),
|
||||
),
|
||||
Fix: res.Reference,
|
||||
FixIsURL: res.Reference != "",
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue