Multi-user OSINT investigation tool with owner-controlled per-user and per-group access grants. Includes Sherlock, theHarvester, crt.sh, WHOIS, and ip-api.com collectors wired through a pluggable Runner/queue pipeline, investigation search, and printable/CSV/JSON reporting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
82 lines
2.8 KiB
PHP
82 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Osint\Runners;
|
|
|
|
use App\Models\Target;
|
|
use App\Osint\Runner;
|
|
use App\Osint\RunnerException;
|
|
use App\Osint\RunnerResult;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Process;
|
|
use Illuminate\Support\Str;
|
|
|
|
class TheHarvesterRunner implements Runner
|
|
{
|
|
public function run(Target $target): RunnerResult
|
|
{
|
|
if ($target->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);
|
|
}
|
|
}
|
|
}
|