package ghunt import ( "context" "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" "github.com/anotherhadi/iknowyou/internal/tools" ) var ansiRe = regexp.MustCompile(`\x1b[\x5b-\x5f][0-9;]*[A-Za-z]|\x1b[^[\x5b-\x5f]`) const ( name = "ghunt" description = "Google account OSINT via GHunt. Extracts profile info, linked services, and activity from a Google email address." link = "https://github.com/mxrch/GHunt" icon = "google" ) type Config struct { Creds string `yaml:"creds" iky:"desc=GHunt credentials (content of ~/.malfrats/ghunt/creds.m). To obtain: (1) install GHunt and run 'ghunt login' on your machine, (2) copy the full content of ~/.malfrats/ghunt/creds.m, (3) paste it here.;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, } } func (r *Runner) ConfigPtr() interface{} { return &r.cfg } func (r *Runner) ConfigFields() []tools.ConfigField { return tools.ReflectConfigFields(r.cfg) } func (r *Runner) Available() (bool, string) { if _, err := exec.LookPath("ghunt"); err != nil { return false, "ghunt binary not found in PATH" } return true, "" } func (r *Runner) Dependencies() []string { return []string{"ghunt"} } func (r *Runner) writeCreds() error { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("cannot determine home directory: %w", err) } dir := filepath.Join(home, ".malfrats", "ghunt") if err := os.MkdirAll(dir, 0700); err != nil { return fmt.Errorf("cannot create ghunt dir: %w", err) } return os.WriteFile(filepath.Join(dir, "creds.m"), []byte(r.cfg.Creds), 0600) } func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error { defer close(out) if err := r.writeCreds(); err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } cmd := exec.CommandContext(ctx, "ghunt", "email", target) output, err := tools.RunWithPTY(ctx, cmd) output = strings.ReplaceAll(output, "\r\n", "\n") output = strings.ReplaceAll(output, "\r", "\n") lines := strings.Split(output, "\n") parsed := make([]string, len(lines)) for i, l := range lines { parsed[i] = ansiRe.ReplaceAllString(l, "") } start := -1 for i, l := range parsed { if strings.Contains(l, "[+] Authenticated !") { start = i + 1 break } } if start == -1 { // Banner printed but auth line never appeared — bad/expired credentials. out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "GHunt authentication failed — credentials may be missing or expired (run 'ghunt login' and update your creds in Settings)"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } end := len(lines) for i := start; i < len(parsed); i++ { if strings.Contains(parsed[i], "Traceback (most recent call last)") { end = i break } } output = strings.TrimSpace(strings.Join(lines[start:end], "\n")) if err != nil && ctx.Err() != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"} } else if output != "" { out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: output} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 1} } else { out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} } out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil }