92 lines
3.0 KiB
PHP
92 lines
3.0 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 HoleheRunner implements Runner
|
||
|
|
{
|
||
|
|
public function run(Target $target): RunnerResult
|
||
|
|
{
|
||
|
|
if ($target->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_<unix-timestamp>_<email>_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<int, string>
|
||
|
|
*/
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|