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,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 }} · {{ $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>
|
||||
Reference in New Issue
Block a user