checker-blacklist/checker/threatfox.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

152 lines
4.4 KiB
Go

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 disabledResult(s.ID(), s.Name())
}
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) {
return evidenceEval(r, SeverityCrit)
}
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 != "",
}
}