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 != "", } }