package leakcheck import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/anotherhadi/iknowyou/internal/tools" ) const ( name = "leakcheck" description = "Data breach lookup via LeakCheck.io — searches 7B+ leaked records for email addresses, usernames, and phone numbers across hundreds of breaches." link = "https://leakcheck.io" icon = "" ) type Config struct { APIKey string `yaml:"api_key" iky:"desc=LeakCheck API key (required — get one at leakcheck.io);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, tools.InputTypePhone, } } func (r *Runner) ConfigPtr() interface{} { return &r.cfg } func (r *Runner) ConfigFields() []tools.ConfigField { return tools.ReflectConfigFields(r.cfg) } type leakCheckResponse struct { Success bool `json:"success"` Found int `json:"found"` Result []struct { Email string `json:"email"` Username string `json:"username"` Phone string `json:"phone"` Password string `json:"password"` Hash string `json:"hash"` Sources []string `json:"sources"` Fields []string `json:"fields"` } `json:"result"` Error string `json:"error"` } func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error { defer close(out) queryType := "auto" switch inputType { case tools.InputTypeEmail: queryType = "email" case tools.InputTypeUsername: queryType = "login" case tools.InputTypePhone: queryType = "phone" } url := fmt.Sprintf("https://leakcheck.io/api/v2/query/%s?type=%s", target, queryType) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, 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-API-Key", r.cfg.APIKey) 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 } var result leakCheckResponse if err := json.Unmarshal(body, &result); 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 !result.Success { msg := result.Error if msg == "" { msg = "API returned failure" } out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: msg} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if result.Found == 0 || len(result.Result) == 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(es)\n\n", result.Found)) for _, entry := range result.Result { if len(entry.Sources) > 0 { sb.WriteString(fmt.Sprintf("Sources: %s\n", strings.Join(entry.Sources, ", "))) } if entry.Email != "" { sb.WriteString(fmt.Sprintf(" Email: %s\n", entry.Email)) } if entry.Username != "" { sb.WriteString(fmt.Sprintf(" Username: %s\n", entry.Username)) } if entry.Phone != "" { sb.WriteString(fmt.Sprintf(" Phone: %s\n", entry.Phone)) } 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 len(entry.Fields) > 0 { sb.WriteString(fmt.Sprintf(" Fields: %s\n", strings.Join(entry.Fields, ", "))) } 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: result.Found} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil }