type !== 'email') { throw new RunnerException('holehe only supports "email" targets.'); } if (! filter_var($target->value, FILTER_VALIDATE_EMAIL)) { throw new RunnerException('Value is not a valid email address.'); } $binary = config('osint.python_venv').'/bin/holehe'; if (! is_executable($binary)) { throw new RunnerException("holehe binary not found or not executable at [{$binary}]. Run scripts/python/setup.sh."); } $runDir = storage_path('app/osint/runs/'.Str::uuid()); File::ensureDirectoryExists($runDir); try { // holehe's CLI calls Python's exit("message") on the success // path after writing the CSV (see core.py: export_csv), which // always yields exit code 1 — confirmed by hand, "successful" // runs and real errors are otherwise indistinguishable by exit // code alone. So: don't gate on successful(), gate on whether // the CSV actually exists. Process::path($runDir) ->timeout((int) config('osint.run_timeout')) ->run([ $binary, $target->value, '--no-color', '--no-clear', '--csv', '-T', '15', ]); // Filename is holehe___results.csv — not // predictable up front, so glob for it. $csvFiles = File::glob($runDir.'/holehe_*_results.csv'); if (empty($csvFiles)) { throw new RunnerException('holehe did not produce the expected CSV output.'); } $sites = $this->parseCsv($csvFiles[0]); $summary = count($sites) > 0 ? sprintf('Found on %d site%s: %s', count($sites), count($sites) === 1 ? '' : 's', implode(', ', $sites)) : 'No accounts found.'; return new RunnerResult(raw: ['sites' => $sites], summary: $summary); } finally { File::deleteDirectory($runDir); } } /** * @return array */ private function parseCsv(string $path): array { $handle = fopen($path, 'r'); $header = fgetcsv($handle); $sites = []; while (($row = fgetcsv($handle)) !== false) { $data = array_combine($header, $row); if (strcasecmp($data['exists'] ?? '', 'True') === 0) { $sites[] = $data['domain'] ?: $data['name']; } } fclose($handle); return $sites; } }