type !== 'domain') { throw new RunnerException('theHarvester only supports "domain" targets.'); } if (! preg_match('/^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/', $target->value)) { throw new RunnerException('Domain contains characters that are not safe to look up.'); } $python = config('osint.theharvester_venv').'/bin/python3'; $script = config('osint.theharvester_script'); if (! is_executable($python) || ! is_file($script)) { throw new RunnerException('theHarvester not set up. Run scripts/python/setup.sh.'); } $runDir = storage_path('app/osint/runs/'.Str::uuid()); File::ensureDirectoryExists($runDir); try { $result = Process::path($runDir) ->timeout((int) config('osint.run_timeout')) ->run([ $python, $script, '-d', $target->value, '-b', config('osint.theharvester_sources'), '-f', 'result', ]); if (! $result->successful()) { throw new RunnerException('theHarvester exited with an error: '.$result->errorOutput()); } $jsonPath = $runDir.'/result.json'; if (! File::exists($jsonPath)) { // No results at all means theHarvester skips writing a file. return new RunnerResult(raw: [], summary: 'No results found.'); } $data = json_decode(File::get($jsonPath), true) ?? []; $hosts = collect($data['hosts'] ?? []) ->map(fn (string $entry) => explode(':', $entry, 2)[0]) ->unique() ->sort() ->values(); $emails = collect($data['emails'] ?? [])->unique()->sort()->values(); $ips = collect($data['ips'] ?? [])->unique()->sort()->values(); $summary = sprintf( 'Found %d host%s, %d email%s, %d IP%s.', $hosts->count(), $hosts->count() === 1 ? '' : 's', $emails->count(), $emails->count() === 1 ? '' : 's', $ips->count(), $ips->count() === 1 ? '' : 's', ); return new RunnerResult( raw: ['hosts' => $hosts->all(), 'emails' => $emails->all(), 'ips' => $ips->all()], summary: $summary, ); } finally { File::deleteDirectory($runDir); } } }