package breachdirectory import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/anotherhadi/iknowyou/internal/tools" ) const ( name = "breachdirectory" description = "Data breach search via BreachDirectory — checks if an email, username, or phone appears in known data breaches and returns exposed passwords/hashes." link = "https://breachdirectory.org" icon = "" ) type Config struct { APIKey string `yaml:"api_key" iky:"desc=RapidAPI key for BreachDirectory (required — get one at rapidapi.com/rohan-patra/api/breachdirectory);required=true"` } type Runner struct { cfg Config } func New() tools.ToolRunner { cfg := Config{} tools.ApplyDefaults(&cfg) return &Runner{cfg: cfg} } func (r *Runner) Name() string { return name } func (r *Runner) Description() string { return description } func (r *Runner) Link() string { return link } func (r *Runner) Icon() string { return icon } func (r *Runner) InputTypes() []tools.InputType { return []tools.InputType{ tools.InputTypeEmail, tools.InputTypeUsername, } } func (r *Runner) ConfigPtr() interface{} { return &r.cfg } func (r *Runner) ConfigFields() []tools.ConfigField { return tools.ReflectConfigFields(r.cfg) } type bdResponse struct { Success bool `json:"success"` Found int `json:"found"` Result json.RawMessage `json:"result"` } type bdEntry struct { Email string `json:"email"` Password string `json:"password"` Hash string `json:"hash"` SHA1 string `json:"sha1"` Sources string `json:"sources"` HasPassword bool `json:"has_password"` } func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error { defer close(out) req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://breachdirectory.p.rapidapi.com/?func=auto&term="+target, nil) if err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } req.Header.Set("X-RapidAPI-Key", r.cfg.APIKey) req.Header.Set("X-RapidAPI-Host", "breachdirectory.p.rapidapi.com") req.Header.Set("Accept", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { if ctx.Err() != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"} } else { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()} } out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to read response"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "invalid or exhausted API key"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if resp.StatusCode != http.StatusOK { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: fmt.Sprintf("API error %d: %s", resp.StatusCode, string(body))} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } var parsed bdResponse if err := json.Unmarshal(body, &parsed); err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if !parsed.Success || parsed.Found == 0 { out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } var entries []bdEntry if err := json.Unmarshal(parsed.Result, &entries); err != nil || len(entries) == 0 { out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } var sb strings.Builder sb.WriteString(fmt.Sprintf("Found in %d breach record(s)\n\n", parsed.Found)) for _, entry := range entries { if entry.Sources != "" { sb.WriteString(fmt.Sprintf("Source: %s\n", entry.Sources)) } if entry.Password != "" { sb.WriteString(fmt.Sprintf("Password: %s\n", entry.Password)) } if entry.Hash != "" { sb.WriteString(fmt.Sprintf("Hash: %s\n", entry.Hash)) } if entry.SHA1 != "" { sb.WriteString(fmt.Sprintf("SHA1: %s\n", entry.SHA1)) } sb.WriteString("\n") } out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: parsed.Found} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil }