package checker import ( "bytes" "context" "encoding/json" "fmt" "html/template" "net/http" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) const otxEndpoint = "https://otx.alienvault.com/api/v1/indicators/domain/" func init() { Register(&otxSource{endpoint: otxEndpoint}) } type otxSource struct{ endpoint string } func (*otxSource) ID() string { return "otx" } func (*otxSource) Name() string { return "AlienVault OTX" } func (*otxSource) Options() SourceOptions { return SourceOptions{ Admin: []sdk.CheckerOptionField{ { Id: "otx_api_key", Type: "string", Label: "AlienVault OTX API key", Description: "Free OTX API key from otx.alienvault.com. Leave empty to skip OTX lookups.", Secret: true, }, }, } } type otxDetails struct { PulseCount int `json:"pulse_count"` Reputation int `json:"reputation"` Pulses []otxPulse `json:"pulses"` } type otxPulse struct { Name string `json:"name"` Tags []string `json:"tags,omitempty"` MalwareFamilies []string `json:"malware_families,omitempty"` Adversary string `json:"adversary,omitempty"` Created string `json:"created,omitempty"` } func (s *otxSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult { apiKey := stringOpt(opts, "otx_api_key") if apiKey == "" { return []SourceResult{{SourceID: s.ID(), SourceName: s.Name(), Enabled: false}} } if registered == "" { return []SourceResult{{SourceID: s.ID(), SourceName: s.Name(), Enabled: true}} } res := SourceResult{ SourceID: s.ID(), SourceName: s.Name(), Enabled: true, Reference: "https://otx.alienvault.com/indicator/domain/" + registered, } reqCtx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, s.endpoint+registered+"/general", nil) if err != nil { res.Error = err.Error() return []SourceResult{res} } req.Header.Set("X-OTX-API-KEY", apiKey) req.Header.Set("Accept", "application/json") body, status, err := httpDo(req, 4<<20) if err != nil { res.Error = err.Error() return []SourceResult{res} } if status == http.StatusNotFound { 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 { Reputation int `json:"reputation"` PulseInfo struct { Count int `json:"count"` Pulses []struct { Name string `json:"name"` Tags []string `json:"tags"` MalwareFamilies []struct { DisplayName string `json:"display_name"` } `json:"malware_families"` Adversary string `json:"adversary"` Created string `json:"created"` } `json:"pulses"` } `json:"pulse_info"` } if err := json.Unmarshal(body, &parsed); err != nil { res.Error = "decode: " + err.Error() return []SourceResult{res} } d := otxDetails{ PulseCount: parsed.PulseInfo.Count, Reputation: parsed.Reputation, } seenReason := map[string]bool{} for _, p := range parsed.PulseInfo.Pulses { pulse := otxPulse{ Name: p.Name, Tags: p.Tags, Adversary: p.Adversary, Created: p.Created, } for _, mf := range p.MalwareFamilies { pulse.MalwareFamilies = append(pulse.MalwareFamilies, mf.DisplayName) if !seenReason[mf.DisplayName] { seenReason[mf.DisplayName] = true res.Reasons = append(res.Reasons, mf.DisplayName) } } if p.Adversary != "" && !seenReason[p.Adversary] { seenReason[p.Adversary] = true res.Reasons = append(res.Reasons, p.Adversary) } d.Pulses = append(d.Pulses, pulse) res.Evidence = append(res.Evidence, Evidence{ Label: "Pulse", Value: p.Name, Status: "threat", }) } res.Details = mustJSON(d) return []SourceResult{res} } func (*otxSource) Evaluate(r SourceResult) (bool, string) { if !r.Enabled || r.Error != "" || len(r.Evidence) == 0 { return false, "" } var d otxDetails _ = json.Unmarshal(r.Details, &d) if d.Reputation < -1 { return true, SeverityCrit } return true, SeverityWarn } func (*otxSource) Diagnose(res SourceResult) Diagnosis { var d otxDetails _ = json.Unmarshal(res.Details, &d) detail := fmt.Sprintf( "%d threat pulse(s) reference this domain (OTX reputation score: %d). Indicators: %s. "+ "Review the pulse details on AlienVault OTX to understand the threat context and take corrective action.", d.PulseCount, d.Reputation, joinNonEmpty(res.Reasons, ", "), ) sev := SeverityWarn if d.Reputation < -1 { sev = SeverityCrit } return Diagnosis{ Severity: sev, Title: "Listed in AlienVault OTX threat pulses", Detail: detail, Fix: res.Reference, FixIsURL: res.Reference != "", } } func (*otxSource) RenderDetail(res SourceResult) (template.HTML, error) { var d otxDetails if len(res.Details) > 0 { if err := json.Unmarshal(res.Details, &d); err != nil { return "", fmt.Errorf("otx: decode details: %w", err) } } if len(d.Pulses) == 0 { return "", nil } var b bytes.Buffer if err := otxDetailTpl.Execute(&b, d); err != nil { return "", err } return template.HTML(b.String()), nil } var otxDetailTpl = template.Must(template.New("otx_detail").Parse(`
OTX reputation score: {{.Reputation}}. Pulse count: {{.PulseCount}}.
| Pulse | Malware families | Adversary | Tags | Created |
|---|---|---|---|---|
| {{.Name}} | {{range .MalwareFamilies}}{{.}} {{end}} | {{.Adversary}} | {{range .Tags}}{{.}} {{end}} | {{.Created}} |