Initial commit: Laravel OSINT platform
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>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the confirm password view.
|
||||
*/
|
||||
public function show(): View
|
||||
{
|
||||
return view('auth.confirm-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the email verification prompt.
|
||||
*/
|
||||
public function __invoke(Request $request): RedirectResponse|View
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(route('dashboard', absolute: false))
|
||||
: view('auth.verify-email');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset view.
|
||||
*/
|
||||
public function create(Request $request): View
|
||||
{
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function (User $user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
return $status == Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validateWithBag('updatePassword', [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back()->with('status', 'password-updated');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset link request view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
return $status == Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GroupController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$groups = Group::query()
|
||||
->where('owner_id', $user->id)
|
||||
->orWhereHas('members', fn ($q) => $q->where('users.id', $user->id))
|
||||
->withCount('members')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('groups.index', compact('groups'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$this->authorize('create', Group::class);
|
||||
|
||||
return view('groups.create');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('create', Group::class);
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string', 'max:5000'],
|
||||
]);
|
||||
|
||||
$group = Group::create([
|
||||
...$data,
|
||||
'owner_id' => $request->user()->id,
|
||||
]);
|
||||
|
||||
return redirect()->route('groups.show', $group);
|
||||
}
|
||||
|
||||
public function show(Request $request, Group $group): View
|
||||
{
|
||||
$this->authorize('view', $group);
|
||||
|
||||
$group->load('owner', 'members');
|
||||
|
||||
return view('groups.show', [
|
||||
'group' => $group,
|
||||
'canManage' => $request->user()->can('manageMembers', $group),
|
||||
]);
|
||||
}
|
||||
|
||||
public function addMember(Request $request, Group $group): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageMembers', $group);
|
||||
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
if (! $user) {
|
||||
return back()->withErrors(['email' => 'No user found with that email address.']);
|
||||
}
|
||||
|
||||
if ($user->id === $group->owner_id) {
|
||||
return back()->withErrors(['email' => 'The group owner is already implicitly a member.']);
|
||||
}
|
||||
|
||||
$group->members()->syncWithoutDetaching([$user->id]);
|
||||
|
||||
return redirect()->route('groups.show', $group);
|
||||
}
|
||||
|
||||
public function removeMember(Request $request, Group $group, User $user): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageMembers', $group);
|
||||
|
||||
$group->members()->detach($user->id);
|
||||
|
||||
return redirect()->route('groups.show', $group);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\Investigation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class InvestigationAccessController extends Controller
|
||||
{
|
||||
public function index(Request $request, Investigation $investigation): View
|
||||
{
|
||||
$this->authorize('manageAccess', $investigation);
|
||||
|
||||
$investigation->load('users', 'groups');
|
||||
|
||||
$availableGroups = Group::query()
|
||||
->where('owner_id', $request->user()->id)
|
||||
->orWhereHas('members', fn ($q) => $q->where('users.id', $request->user()->id))
|
||||
->whereNotIn('id', $investigation->groups->pluck('id'))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('investigations.access', [
|
||||
'investigation' => $investigation,
|
||||
'availableGroups' => $availableGroups,
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeUser(Request $request, Investigation $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageAccess', $investigation);
|
||||
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'role' => ['required', 'string', 'in:viewer,editor'],
|
||||
]);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
if (! $user) {
|
||||
return back()->withErrors(['email' => 'No user found with that email address.']);
|
||||
}
|
||||
|
||||
if ($user->id === $investigation->owner_id) {
|
||||
return back()->withErrors(['email' => 'The owner already has full access.']);
|
||||
}
|
||||
|
||||
$investigation->users()->syncWithoutDetaching([$user->id => ['role' => $data['role']]]);
|
||||
|
||||
return redirect()->route('investigations.access.index', $investigation);
|
||||
}
|
||||
|
||||
public function destroyUser(Investigation $investigation, User $user): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageAccess', $investigation);
|
||||
|
||||
$investigation->users()->detach($user->id);
|
||||
|
||||
return redirect()->route('investigations.access.index', $investigation);
|
||||
}
|
||||
|
||||
public function storeGroup(Request $request, Investigation $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageAccess', $investigation);
|
||||
|
||||
$data = $request->validate([
|
||||
'group_id' => ['required', 'integer', 'exists:groups,id'],
|
||||
'role' => ['required', 'string', 'in:viewer,editor'],
|
||||
]);
|
||||
|
||||
$group = Group::findOrFail($data['group_id']);
|
||||
|
||||
abort_unless($group->isAccessibleBy($request->user()), 403);
|
||||
|
||||
$investigation->groups()->syncWithoutDetaching([$group->id => ['role' => $data['role']]]);
|
||||
|
||||
return redirect()->route('investigations.access.index', $investigation);
|
||||
}
|
||||
|
||||
public function destroyGroup(Investigation $investigation, Group $group): RedirectResponse
|
||||
{
|
||||
$this->authorize('manageAccess', $investigation);
|
||||
|
||||
$investigation->groups()->detach($group->id);
|
||||
|
||||
return redirect()->route('investigations.access.index', $investigation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Investigation;
|
||||
use App\Models\Source;
|
||||
use App\Osint\RunnerRegistry;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class InvestigationController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$investigations = Investigation::query()
|
||||
->accessibleBy($request->user())
|
||||
->withCount('targets')
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return view('investigations.index', compact('investigations'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$this->authorize('create', Investigation::class);
|
||||
|
||||
return view('investigations.create');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('create', Investigation::class);
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string', 'max:5000'],
|
||||
]);
|
||||
|
||||
$investigation = Investigation::create([
|
||||
...$data,
|
||||
'owner_id' => $request->user()->id,
|
||||
'status' => 'open',
|
||||
]);
|
||||
|
||||
return redirect()->route('investigations.show', $investigation);
|
||||
}
|
||||
|
||||
public function show(Request $request, Investigation $investigation, RunnerRegistry $registry): View
|
||||
{
|
||||
$this->authorize('view', $investigation);
|
||||
|
||||
$investigation->load(['owner', 'targets.creator', 'targets.findings.source']);
|
||||
|
||||
$sources = Source::where('enabled', true)->orderBy('name')->get();
|
||||
|
||||
return view('investigations.show', [
|
||||
'investigation' => $investigation,
|
||||
'canEdit' => $investigation->isEditableBy($request->user()),
|
||||
'canManageAccess' => $request->user()->can('manageAccess', $investigation),
|
||||
'sources' => $sources,
|
||||
'runnableSourceKeys' => $sources->filter(fn (Source $s) => $registry->has($s->key))->pluck('key'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Investigation $investigation): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $investigation);
|
||||
|
||||
$investigation->delete();
|
||||
|
||||
return redirect()->route('investigations.index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Investigation;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class InvestigationReportController extends Controller
|
||||
{
|
||||
public function show(Request $request, Investigation $investigation): View
|
||||
{
|
||||
$this->authorize('view', $investigation);
|
||||
|
||||
$investigation->load(['owner', 'targets.creator', 'targets.findings.source']);
|
||||
|
||||
return view('investigations.report', compact('investigation'));
|
||||
}
|
||||
|
||||
public function csv(Request $request, Investigation $investigation): Response
|
||||
{
|
||||
$this->authorize('view', $investigation);
|
||||
|
||||
$investigation->load(['targets.findings.source']);
|
||||
|
||||
$rows = ["target_type,target_value,source,status,summary,ran_at\n"];
|
||||
|
||||
foreach ($investigation->targets as $target) {
|
||||
foreach ($target->findings as $finding) {
|
||||
$rows[] = implode(',', array_map(
|
||||
fn ($v) => '"'.str_replace('"', '""', $this->csvSafe($v)).'"',
|
||||
[
|
||||
$target->type,
|
||||
$target->value,
|
||||
$finding->source->name,
|
||||
$finding->status,
|
||||
$finding->summary,
|
||||
$finding->ran_at,
|
||||
]
|
||||
))."\n";
|
||||
}
|
||||
}
|
||||
|
||||
$filename = 'investigation-'.$investigation->id.'-findings.csv';
|
||||
|
||||
return response(implode('', $rows), 200, [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix values that Excel/Sheets would interpret as a formula, to
|
||||
* prevent CSV injection via user-controlled target values or finding
|
||||
* summaries pulled from external tool output.
|
||||
*/
|
||||
private function csvSafe(mixed $value): string
|
||||
{
|
||||
$value = (string) $value;
|
||||
|
||||
return preg_match('/^[=+\-@\t\r]/', $value) ? "'".$value : $value;
|
||||
}
|
||||
|
||||
public function json(Request $request, Investigation $investigation): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $investigation);
|
||||
|
||||
$investigation->load(['owner', 'targets.creator', 'targets.findings.source']);
|
||||
|
||||
$filename = 'investigation-'.$investigation->id.'.json';
|
||||
|
||||
return response()->json([
|
||||
'name' => $investigation->name,
|
||||
'description' => $investigation->description,
|
||||
'status' => $investigation->status,
|
||||
'owner' => $investigation->owner->name,
|
||||
'created_at' => $investigation->created_at,
|
||||
'targets' => $investigation->targets->map(fn ($target) => [
|
||||
'type' => $target->type,
|
||||
'value' => $target->value,
|
||||
'notes' => $target->notes,
|
||||
'created_by' => $target->creator->name,
|
||||
'findings' => $target->findings->map(fn ($finding) => [
|
||||
'source' => $finding->source->name,
|
||||
'status' => $finding->status,
|
||||
'summary' => $finding->summary,
|
||||
'raw_payload' => $finding->raw_payload,
|
||||
'ran_at' => $finding->ran_at,
|
||||
]),
|
||||
]),
|
||||
])->withHeaders([
|
||||
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile form.
|
||||
*/
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
return view('profile.edit', [
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's account.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validateWithBag('userDeletion', [
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return Redirect::to('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Investigation;
|
||||
use App\Models\Target;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = trim((string) $request->query('q', ''));
|
||||
|
||||
$investigations = collect();
|
||||
$targets = collect();
|
||||
|
||||
if ($query !== '') {
|
||||
$investigations = Investigation::query()
|
||||
->accessibleBy($request->user())
|
||||
->where('name', 'like', "%{$query}%")
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$targets = Target::query()
|
||||
->whereHas('investigation', fn ($q) => $q->accessibleBy($request->user()))
|
||||
->where('value', 'like', "%{$query}%")
|
||||
->with('investigation')
|
||||
->orderBy('value')
|
||||
->limit(100)
|
||||
->get();
|
||||
}
|
||||
|
||||
return view('search.index', [
|
||||
'query' => $query,
|
||||
'investigations' => $investigations,
|
||||
'targets' => $targets,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?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\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}...");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user