type !== 'username') { throw new RunnerException('Sherlock only supports "username" targets.'); } if (! preg_match('/^[a-zA-Z0-9._-]+$/', $target->value)) { throw new RunnerException('Username contains characters that are not safe to check.'); } $binary = config('osint.python_venv').'/bin/sherlock'; if (! is_executable($binary)) { throw new RunnerException("Sherlock binary not found or not executable at [{$binary}]. Run the setup in scripts/python/README.md."); } $runDir = storage_path('app/osint/runs/'.Str::uuid()); File::ensureDirectoryExists($runDir); try { $result = Process::path($runDir) ->timeout((int) config('osint.run_timeout')) ->run([ $binary, $target->value, '--csv', '--no-color', '--no-txt', '--timeout', '30', ]); if (! $result->successful()) { throw new RunnerException('Sherlock exited with an error: '.$result->errorOutput()); } $csvPath = $runDir.'/'.$target->value.'.csv'; if (! File::exists($csvPath)) { throw new RunnerException('Sherlock did not produce the expected CSV output.'); } $rows = $this->parseCsv($csvPath); $summary = count($rows) > 0 ? sprintf('Found on %d site%s: %s', count($rows), count($rows) === 1 ? '' : 's', collect($rows)->pluck('name')->implode(', ')) : 'No matching profiles found.'; return new RunnerResult(raw: ['matches' => $rows], summary: $summary); } finally { File::deleteDirectory($runDir); } } /** * @return array */ private function parseCsv(string $path): array { $handle = fopen($path, 'r'); $header = fgetcsv($handle); $rows = []; while (($row = fgetcsv($handle)) !== false) { $data = array_combine($header, $row); $rows[] = [ 'name' => $data['name'], 'url' => $data['url_user'], 'http_status' => $data['http_status'] !== '' ? (int) $data['http_status'] : null, ]; } fclose($handle); return $rows; } }