Files
osint/app/Http/Controllers/TargetController.php
T
cedry2k1992andClaude Sonnet 5 e4fa57c3d6 Add bulk-run actions and per-source rate limiting
Run-all buttons (per target and per investigation) queue every applicable,
not-already-in-flight source via a new FindingDispatcher, re-attempting
prior failures automatically. RunFinding now applies a per-source
RateLimited queue middleware so a bulk run can't hammer the free public
APIs (ip-api, crt.sh, whois, sherlock, theharvester) it depends on;
$tries raised to 30 to survive rate-limiter releases, with a failed()
handler so exhausted retries still resolve the Finding instead of leaving
it stuck pending.

Also fixes CrtShRunner treating crt.sh's intermittent 404/502 (a known
flaky free service) as fatal on the first try — added a bounded retry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:29:26 +03:00

93 lines
3.0 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Jobs\RunFinding;
use App\Models\Finding;
use App\Models\Investigation;
use App\Models\Source;
use App\Models\Target;
use App\Osint\FindingDispatcher;
use App\Osint\RunnerRegistry;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class TargetController extends Controller
{
public function store(Request $request, Investigation $investigation): RedirectResponse
{
$this->authorize('update', $investigation);
$type = $request->validate([
'type' => ['required', 'string', 'in:'.implode(',', Target::TYPES)],
])['type'];
$data = $request->validate([
'value' => Target::valueRulesFor($type),
'notes' => ['nullable', 'string', 'max:5000'],
]);
$investigation->targets()->create([
...$data,
'type' => $type,
'created_by' => $request->user()->id,
]);
return redirect()->route('investigations.show', $investigation);
}
public function destroy(Investigation $investigation, Target $target): RedirectResponse
{
$this->authorize('update', $investigation);
abort_if($target->investigation_id !== $investigation->id, 404);
$target->delete();
return redirect()->route('investigations.show', $investigation);
}
public function run(Request $request, Investigation $investigation, Target $target, RunnerRegistry $registry): RedirectResponse
{
$this->authorize('update', $investigation);
abort_if($target->investigation_id !== $investigation->id, 404);
$data = $request->validate([
'source_id' => ['required', 'integer', 'exists:sources,id'],
]);
$source = Source::findOrFail($data['source_id']);
if (! in_array($target->type, $source->target_types, true) || ! $registry->has($source->key)) {
return back()->withErrors(['source_id' => "{$source->name} can't be run against this target."]);
}
$finding = Finding::create([
'target_id' => $target->id,
'source_id' => $source->id,
'run_by' => $request->user()->id,
'status' => 'pending',
]);
RunFinding::dispatch($finding->id);
return redirect()->route('investigations.show', $investigation)
->with('status', "Running {$source->name} against {$target->value}...");
}
public function runAll(Request $request, Investigation $investigation, Target $target, FindingDispatcher $dispatcher): RedirectResponse
{
$this->authorize('update', $investigation);
abort_if($target->investigation_id !== $investigation->id, 404);
$queued = $dispatcher->dispatchAllApplicable($target, $request->user());
return redirect()->route('investigations.show', $investigation)
->with('status', $queued > 0
? "Queued {$queued} lookup(s) for {$target->value}..."
: "Nothing to queue for {$target->value} — already up to date.");
}
}