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.
150 lines
4.2 KiB
Go
150 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 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 (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 {
|
|
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,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *threatFoxSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
|
authKey := stringOpt(opts, "threatfox_auth_key")
|
|
if 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 != "",
|
|
}
|
|
}
|