89 lines
2.8 KiB
PHP
89 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 SherlockRunner implements Runner
|
||
|
|
{
|
||
|
|
public function run(Target $target): RunnerResult
|
||
|
|
{
|
||
|
|
if ($target->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<int, array{name: string, url: string, http_status: int|null}>
|
||
|
|
*/
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|