170 lines
8.9 KiB
PHP
170 lines
8.9 KiB
PHP
<?php
|
|
|
|
use App\Models\Certificate;
|
|
use App\Models\SecurityEvent;
|
|
use App\Models\TeamMember;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Volt\Component;
|
|
|
|
new #[Layout('layouts.app')] class extends Component {
|
|
public function with(): array
|
|
{
|
|
$events24h = SecurityEvent::where('occurred_at', '>=', now()->subDay())->count();
|
|
$blocked24h = SecurityEvent::where('occurred_at', '>=', now()->subDay())->where('blocked', true)->count();
|
|
|
|
$highEvents = SecurityEvent::query()
|
|
->whereIn('severity', ['high', 'critical'])
|
|
->orderByDesc('occurred_at')
|
|
->limit(8)
|
|
->get();
|
|
|
|
$feedEvents = SecurityEvent::query()
|
|
->orderByDesc('occurred_at')
|
|
->limit(12)
|
|
->get();
|
|
|
|
$certs = Certificate::query()
|
|
->with('site')
|
|
->orderBy('expires_at')
|
|
->limit(10)
|
|
->get();
|
|
|
|
$totalMembers = TeamMember::count() ?: 1;
|
|
$with2fa = TeamMember::where('two_fa_enabled', true)->count();
|
|
|
|
return [
|
|
'eventsTotal' => $events24h,
|
|
'blockedCount' => $blocked24h,
|
|
'advisories' => $highEvents,
|
|
'feedEvents' => $feedEvents,
|
|
'certs' => $certs,
|
|
'twoFaCoverage' => (int) round($with2fa / $totalMembers * 100),
|
|
'rulesActive' => 42,
|
|
'sslExpiringSoon' => Certificate::where('expires_at', '<=', now()->addDays(30))->count(),
|
|
];
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
<div class="clu-topbar">
|
|
<x-clu.burger />
|
|
<div>
|
|
<h1 class="clu-page-title">Sicherheit</h1>
|
|
<div class="clu-page-sub">{{ $blockedCount }} blockiert / 24h · {{ $advisories->count() }} advisories</div>
|
|
</div>
|
|
<span class="clu-status-chip"><span class="clu-pulse-dot"></span> firewall aktiv</span>
|
|
<div class="spacer"></div>
|
|
<button class="clu-btn-primary" style="height:36px;padding:0 14px;">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M11 5l-2 7h6l-2 7"/><circle cx="12" cy="12" r="9"/></svg>
|
|
Scan starten
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid gap-[14px] mb-[18px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
|
|
<div class="clu-metric danger">
|
|
<div class="clu-metric-row"><span class="clu-metric-label">Blockiert / 24h</span>
|
|
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M5 5l14 14"/></svg></span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $blockedCount }}</div>
|
|
<div class="clu-metric-delta bad">↑ brute-force trend</div>
|
|
</div>
|
|
<div class="clu-metric">
|
|
<div class="clu-metric-row"><span class="clu-metric-label">Firewall Rules</span>
|
|
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg></span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $rulesActive }}</div>
|
|
<div class="clu-metric-delta">aktiv</div>
|
|
</div>
|
|
<div class="clu-metric @if($sslExpiringSoon > 0) warning @endif">
|
|
<div class="clu-metric-row"><span class="clu-metric-label">SSL läuft bald ab</span>
|
|
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $sslExpiringSoon }}</div>
|
|
<div class="clu-metric-delta @if($sslExpiringSoon > 0) warn @endif">in den nächsten 30 Tagen</div>
|
|
</div>
|
|
<div class="clu-metric success">
|
|
<div class="clu-metric-row"><span class="clu-metric-label">2FA Coverage</span>
|
|
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg></span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $twoFaCoverage }}<span class="unit">%</span></div>
|
|
<div class="clu-metric-delta">Team mit 2FA</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if ($advisories->isNotEmpty())
|
|
<div class="clu-card mb-[18px]">
|
|
<div class="clu-card-head">
|
|
<h3>Offene Advisories</h3>
|
|
<span class="meta">{{ $advisories->count() }} aktiv</span>
|
|
</div>
|
|
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:10px;">
|
|
@foreach ($advisories as $a)
|
|
<div class="flex items-start gap-[12px] p-[12px] rounded-[11px]"
|
|
style="background:rgba(219,59,59,0.06);border:1px solid rgba(219,59,59,0.18);">
|
|
<div class="clu-feed-ico error" style="flex:none;">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.3 3.86l-8.55 14.83A2 2 0 0 0 3.46 22h17.08a2 2 0 0 0 1.71-3.31L13.7 3.86a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="font-semibold text-[13px]">{{ $a->message ?? ucfirst(str_replace('_', ' ', $a->type)) }}</div>
|
|
<div class="clu-feed-meta">{{ strtoupper($a->severity) }} · {{ $a->source_ip ?? '—' }} · {{ $a->occurred_at?->diffForHumans() }}</div>
|
|
</div>
|
|
<button class="clu-ghost-btn" type="button">Action</button>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="grid gap-[14px]" style="grid-template-columns:minmax(0,1fr) minmax(280px,1fr);">
|
|
<div class="clu-card">
|
|
<div class="clu-card-head"><h3>Threat Feed</h3><span class="meta">live</span></div>
|
|
<div class="clu-card-body tight">
|
|
<div class="clu-feed">
|
|
@forelse ($feedEvents as $e)
|
|
<div class="clu-feed-row">
|
|
<div class="clu-feed-ico @if($e->severity === 'critical') error @elseif($e->severity === 'high') warning @endif">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg>
|
|
</div>
|
|
<div class="clu-feed-body">
|
|
<div class="clu-feed-msg">{{ $e->message ?? ucfirst(str_replace('_', ' ', $e->type)) }}</div>
|
|
<div class="clu-feed-meta">{{ $e->source_ip ?? '—' }} · {{ $e->country ?? '—' }} · {{ $e->occurred_at?->diffForHumans() }}</div>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="clu-card-body" style="text-align:center;padding:30px;color:var(--color-muted);">Keine Events.</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="clu-card">
|
|
<div class="clu-card-head"><h3>SSL Zertifikate</h3><span class="meta">{{ $certs->count() }} nächste</span></div>
|
|
<div class="clu-card-body tight" style="overflow-x:auto;">
|
|
<table class="clu-table">
|
|
<thead><tr><th>Domain</th><th>Aussteller</th><th>Tage</th><th>Status</th></tr></thead>
|
|
<tbody>
|
|
@forelse ($certs as $c)
|
|
<tr>
|
|
<td>{{ $c->domain }}</td>
|
|
<td class="font-mono text-[11.5px]">{{ $c->issuer }}</td>
|
|
<td class="font-mono">{{ $c->days_to_expiry }}d</td>
|
|
<td>
|
|
@if ($c->status === 'critical')
|
|
<span class="clu-health bad">kritisch</span>
|
|
@elseif ($c->status === 'warning')
|
|
<span class="clu-health warn">bald ablauf</span>
|
|
@else
|
|
<span class="clu-health">ok</span>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr><td colspan="4" style="text-align:center;padding:30px;color:var(--color-muted);">Keine Zertifikate.</td></tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|