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:
cedry2k1992
2026-07-20 10:22:05 +03:00
co-authored by Claude Sonnet 5
commit 873699e23e
165 changed files with 18507 additions and 0 deletions
@@ -0,0 +1,117 @@
<x-app-layout>
<x-slot name="header">
<div>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Manage Access') }} &mdash; {{ $investigation->name }}
</h2>
<a href="{{ route('investigations.show', $investigation) }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">
{{ __('Back to investigation') }}
</a>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8 space-y-6">
@if ($errors->any())
<div class="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 text-sm rounded-md p-4">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg p-6">
<h3 class="font-semibold text-gray-800 dark:text-gray-200 mb-4">{{ __('Users') }}</h3>
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
<li class="py-2 flex justify-between items-center">
<span>{{ $investigation->owner->name }} <span class="text-xs text-gray-500 dark:text-gray-400">({{ $investigation->owner->email }})</span></span>
<span class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('owner') }}</span>
</li>
@forelse ($investigation->users as $user)
<li class="py-2 flex justify-between items-center">
<span>{{ $user->name }} <span class="text-xs text-gray-500 dark:text-gray-400">({{ $user->email }})</span></span>
<div class="flex items-center gap-3">
<span class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $user->pivot->role }}</span>
<form method="POST" action="{{ route('investigations.access.users.destroy', [$investigation, $user]) }}">
@csrf
@method('DELETE')
<button type="submit" class="text-xs text-red-600 dark:text-red-400 hover:underline">{{ __('Revoke') }}</button>
</form>
</div>
</li>
@empty
@endforelse
</ul>
<form method="POST" action="{{ route('investigations.access.users.store', $investigation) }}" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700 flex items-end gap-3">
@csrf
<div class="flex-1">
<x-input-label for="email" :value="__('User email')" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" required />
</div>
<div>
<x-input-label for="role" :value="__('Role')" />
<select id="role" name="role" class="mt-1 text-sm border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 rounded-md">
<option value="viewer">{{ __('viewer') }}</option>
<option value="editor">{{ __('editor') }}</option>
</select>
</div>
<x-primary-button type="submit">{{ __('Grant') }}</x-primary-button>
</form>
</div>
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg p-6">
<h3 class="font-semibold text-gray-800 dark:text-gray-200 mb-4">{{ __('Groups') }}</h3>
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse ($investigation->groups as $group)
<li class="py-2 flex justify-between items-center">
<a href="{{ route('groups.show', $group) }}" class="hover:underline">{{ $group->name }}</a>
<div class="flex items-center gap-3">
<span class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $group->pivot->role }}</span>
<form method="POST" action="{{ route('investigations.access.groups.destroy', [$investigation, $group]) }}">
@csrf
@method('DELETE')
<button type="submit" class="text-xs text-red-600 dark:text-red-400 hover:underline">{{ __('Revoke') }}</button>
</form>
</div>
</li>
@empty
<li class="py-2 text-sm text-gray-500 dark:text-gray-400">{{ __('No groups granted access yet.') }}</li>
@endforelse
</ul>
@if ($availableGroups->isNotEmpty())
<form method="POST" action="{{ route('investigations.access.groups.store', $investigation) }}" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700 flex items-end gap-3">
@csrf
<div class="flex-1">
<x-input-label for="group_id" :value="__('Group')" />
<select id="group_id" name="group_id" class="mt-1 block w-full text-sm border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 rounded-md">
@foreach ($availableGroups as $group)
<option value="{{ $group->id }}">{{ $group->name }}</option>
@endforeach
</select>
</div>
<div>
<x-input-label for="group_role" :value="__('Role')" />
<select id="group_role" name="role" class="mt-1 text-sm border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 rounded-md">
<option value="viewer">{{ __('viewer') }}</option>
<option value="editor">{{ __('editor') }}</option>
</select>
</div>
<x-primary-button type="submit">{{ __('Grant') }}</x-primary-button>
</form>
@else
<p class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700 text-sm text-gray-500 dark:text-gray-400">
{{ __("You don't own or belong to any other groups to grant access to.") }}
<a href="{{ route('groups.create') }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ __('Create one') }}</a>.
</p>
@endif
</div>
</div>
</div>
</x-app-layout>
@@ -0,0 +1,37 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('New Investigation') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<form method="POST" action="{{ route('investigations.store') }}" class="p-6 space-y-6">
@csrf
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name')" required autofocus />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<div>
<x-input-label for="description" :value="__('Description')" />
<textarea id="description" name="description" rows="4"
class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 rounded-md shadow-sm">{{ old('description') }}</textarea>
<x-input-error :messages="$errors->get('description')" class="mt-2" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Create') }}</x-primary-button>
<a href="{{ route('investigations.index') }}" class="text-sm text-gray-500 dark:text-gray-400 hover:underline">
{{ __('Cancel') }}
</a>
</div>
</form>
</div>
</div>
</div>
</x-app-layout>
@@ -0,0 +1,42 @@
<x-app-layout>
<x-slot name="header">
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Investigations') }}
</h2>
<a href="{{ route('investigations.create') }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">
{{ __('New investigation') }}
</a>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
@if ($investigations->isEmpty())
<p class="text-gray-500 dark:text-gray-400">
{{ __("You don't have access to any investigations yet.") }}
</p>
@else
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach ($investigations as $investigation)
<li class="py-3">
<a href="{{ route('investigations.show', $investigation) }}" class="flex justify-between items-center">
<div>
<span class="font-medium">{{ $investigation->name }}</span>
<span class="ml-2 text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $investigation->status }}</span>
</div>
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ $investigation->targets_count }} {{ Str::plural('target', $investigation->targets_count) }}
</span>
</a>
</li>
@endforeach
</ul>
@endif
</div>
</div>
</div>
</div>
</x-app-layout>
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $investigation->name }} — {{ __('Report') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
@media print {
.no-print { display: none !important; }
}
</style>
</head>
<body class="font-sans antialiased bg-white text-gray-900 p-8 max-w-4xl mx-auto">
<div class="no-print mb-6 flex justify-between items-center">
<a href="{{ route('investigations.show', $investigation) }}" class="text-sm text-indigo-600 hover:underline">
{{ __('Back to investigation') }}
</a>
<button onclick="window.print()" class="text-sm bg-gray-800 text-white px-3 py-1.5 rounded-md">
{{ __('Print / Save as PDF') }}
</button>
</div>
<h1 class="text-2xl font-bold">{{ $investigation->name }}</h1>
<p class="text-sm text-gray-500 mt-1">
{{ __('Owner') }}: {{ $investigation->owner->name }} &middot;
{{ __('Status') }}: {{ $investigation->status }} &middot;
{{ __('Generated') }}: {{ now()->toDayDateTimeString() }}
</p>
@if ($investigation->description)
<p class="mt-4 text-gray-700">{{ $investigation->description }}</p>
@endif
<h2 class="text-lg font-semibold mt-8 mb-3 border-b border-gray-300 pb-1">{{ __('Targets & Findings') }}</h2>
@forelse ($investigation->targets as $target)
<div class="mt-5 break-inside-avoid">
<p class="font-medium">
<span class="text-xs uppercase tracking-wide text-gray-500">{{ $target->type }}</span>
{{ $target->value }}
</p>
@if ($target->notes)
<p class="text-sm text-gray-600">{{ $target->notes }}</p>
@endif
@forelse ($target->findings as $finding)
<div class="ml-4 mt-2 text-sm">
<p>
<span class="font-medium">{{ $finding->source->name }}</span>
<span class="text-gray-500">({{ $finding->status }})</span>
</p>
@if ($finding->summary)
<p class="text-gray-700">{{ $finding->summary }}</p>
@endif
@if ($finding->error)
<p class="text-red-600">{{ $finding->error }}</p>
@endif
</div>
@empty
<p class="ml-4 mt-2 text-sm text-gray-400">{{ __('No lookups run yet.') }}</p>
@endforelse
</div>
@empty
<p class="text-gray-500">{{ __('No targets in this investigation.') }}</p>
@endforelse
</body>
</html>
@@ -0,0 +1,186 @@
<x-app-layout>
<x-slot name="header">
<div class="flex justify-between items-start">
<div>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ $investigation->name }}
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ __('Owned by') }} {{ $investigation->owner->name }} &middot; {{ $investigation->status }}
</p>
</div>
<div class="flex items-center gap-4">
<a href="{{ route('investigations.report', $investigation) }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">
{{ __('Report') }}
</a>
<a href="{{ route('investigations.export.csv', $investigation) }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">
{{ __('Export CSV') }}
</a>
<a href="{{ route('investigations.export.json', $investigation) }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">
{{ __('Export JSON') }}
</a>
@if ($canManageAccess)
<a href="{{ route('investigations.access.index', $investigation) }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">
{{ __('Manage access') }}
</a>
<form method="POST" action="{{ route('investigations.destroy', $investigation) }}" onsubmit="return confirm('{{ __('Delete this investigation and all its targets/findings?') }}');">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-600 dark:text-red-400 hover:underline">{{ __('Delete') }}</button>
</form>
@endif
</div>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8 space-y-6">
@if (session('status'))
<div class="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-sm rounded-md p-4">
{{ session('status') }}
</div>
@endif
@if ($errors->any())
<div class="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 text-sm rounded-md p-4">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if ($investigation->description)
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg p-6 text-gray-700 dark:text-gray-300">
{{ $investigation->description }}
</div>
@endif
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg p-6">
<h3 class="font-semibold text-gray-800 dark:text-gray-200 mb-4">{{ __('Targets') }}</h3>
@forelse ($investigation->targets as $target)
<div class="border-t border-gray-200 dark:border-gray-700 py-4 first:border-t-0 first:pt-0">
<div class="flex justify-between items-start">
<div>
<span class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $target->type }}</span>
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $target->value }}</p>
@if ($target->notes)
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $target->notes }}</p>
@endif
</div>
@if ($canEdit)
@php
$applicableSources = $sources->filter(fn ($s) => in_array($target->type, $s->target_types, true));
@endphp
<div class="flex items-center gap-3">
@if ($applicableSources->isNotEmpty())
<form method="POST" action="{{ route('investigations.targets.run', [$investigation, $target]) }}" class="flex items-center gap-2">
@csrf
<select name="source_id" class="text-sm border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 rounded-md">
@foreach ($applicableSources as $source)
<option value="{{ $source->id }}" @disabled(!$runnableSourceKeys->contains($source->key))>
{{ $source->name }} @if (!$runnableSourceKeys->contains($source->key)) ({{ __('not yet integrated') }}) @endif
</option>
@endforeach
</select>
<x-secondary-button type="submit">{{ __('Run') }}</x-secondary-button>
</form>
@endif
<form method="POST" action="{{ route('investigations.targets.destroy', [$investigation, $target]) }}" onsubmit="return confirm('{{ __('Delete this target and its findings?') }}');">
@csrf
@method('DELETE')
<button type="submit" class="text-xs text-red-600 dark:text-red-400 hover:underline">{{ __('Remove') }}</button>
</form>
</div>
@endif
</div>
@if ($target->findings->isNotEmpty())
<ul class="mt-3 space-y-2">
@foreach ($target->findings->sortByDesc('id') as $finding)
<li class="text-sm bg-gray-50 dark:bg-gray-900 rounded-md p-3">
<div class="flex justify-between items-center">
<span class="font-medium">{{ $finding->source->name }}</span>
<span class="text-xs uppercase tracking-wide
{{ match($finding->status) {
'completed' => 'text-green-600 dark:text-green-400',
'failed' => 'text-red-600 dark:text-red-400',
default => 'text-gray-500 dark:text-gray-400',
} }}">
{{ $finding->status }}
</span>
</div>
@if ($finding->summary)
<p class="text-gray-700 dark:text-gray-300 mt-1">{{ $finding->summary }}</p>
@endif
@if ($finding->error)
<p class="text-red-600 dark:text-red-400 mt-1">{{ $finding->error }}</p>
@endif
@if ($finding->status === 'completed' && !empty($finding->raw_payload['matches']))
<ul class="mt-2 space-y-1">
@foreach ($finding->raw_payload['matches'] as $match)
<li>
<a href="{{ $match['url'] }}" target="_blank" rel="noopener" class="text-indigo-600 dark:text-indigo-400 hover:underline">
{{ $match['name'] }}
</a>
</li>
@endforeach
</ul>
@endif
@if ($finding->status === 'completed')
@foreach (($finding->raw_payload ?? []) as $field => $items)
@continue($field === 'matches' || !is_array($items) || empty($items) || !is_string($items[0] ?? null))
<details class="mt-2">
<summary class="cursor-pointer text-gray-600 dark:text-gray-400">
{{ ucfirst($field) }} ({{ count($items) }})
</summary>
<ul class="mt-1 ml-4 list-disc space-y-0.5">
@foreach (array_slice($items, 0, 50) as $item)
<li class="text-gray-700 dark:text-gray-300">{{ $item }}</li>
@endforeach
</ul>
@if (count($items) > 50)
<p class="text-xs text-gray-500 dark:text-gray-400 ml-4">{{ __('+ :count more', ['count' => count($items) - 50]) }}</p>
@endif
</details>
@endforeach
@endif
</li>
@endforeach
</ul>
@endif
</div>
@empty
<p class="text-gray-500 dark:text-gray-400">{{ __('No targets yet.') }}</p>
@endforelse
@if ($canEdit)
<form method="POST" action="{{ route('investigations.targets.store', $investigation) }}" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700 flex flex-wrap items-end gap-3">
@csrf
<div>
<x-input-label for="type" :value="__('Type')" />
<select id="type" name="type" class="mt-1 text-sm border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 rounded-md">
<option value="username">username</option>
<option value="domain">domain</option>
<option value="email">email</option>
<option value="ip">ip</option>
<option value="person">person</option>
<option value="org">org</option>
</select>
</div>
<div class="flex-1 min-w-[12rem]">
<x-input-label for="value" :value="__('Value')" />
<x-text-input id="value" name="value" type="text" class="mt-1 block w-full" required />
</div>
<x-primary-button type="submit">{{ __('Add target') }}</x-primary-button>
</form>
@endif
</div>
</div>
</div>
</x-app-layout>