@extends('admin.layout') @section('title', 'Server') @section('content') @php $fmt = fn($n) => number_format((float) $n, 0, ',', '.'); $ms = fn($n) => round((float) $n) . ' ms'; $mb = fn($n) => number_format($n / 1024 / 1024, 2) . ' MB'; $uptime = fn($s) => sprintf('%dd %dh %dm', $s/86400, ($s%86400)/3600, ($s%3600)/60); $pct = fn($a, $b) => $b > 0 ? round($a / $b * 100) : 0; $periods = ['24h' => 'Laatste 24 uur', 'week' => 'Laatste week', 'month' => 'Laatste maand', 'year' => 'Laatste jaar']; @endphp {{-- ── Period tabs ──────────────────────────────────────────────────────────── --}}
@foreach ($periods as $key => $label) {{ $label }} @endforeach
{{-- ── Summary cards ─────────────────────────────────────────────────────────── --}} @php $sc = fn($label, $value, $color='gray', $sub='', $clickable=false) => "

{$label}" . ($clickable ? " " : "") . "

{$value}

" . ($sub ? "

{$sub}

" : '') . "
"; @endphp
{!! $sc('Verzoeken', $fmt($summary->total ?? 0), 'blue') !!} {!! $sc('Geslaagd', $fmt($summary->success ?? 0), 'green', $pct($summary->success ?? 0, $summary->total ?? 1) . '%') !!} @if (($summary->errors_4xx ?? 0) > 0)
{!! $sc('4xx fouten', $fmt($summary->errors_4xx), 'yellow', $pct($summary->errors_4xx, $summary->total ?? 1) . '%', true) !!}
@else {!! $sc('4xx fouten', '0', 'gray', '0%') !!} @endif @if (($summary->errors_5xx ?? 0) > 0)
{!! $sc('5xx fouten', $fmt($summary->errors_5xx), 'red', $pct($summary->errors_5xx, $summary->total ?? 1) . '%', true) !!}
@else {!! $sc('5xx fouten', '0', 'gray', '0%') !!} @endif {!! $sc('Gem. tijd', $ms($summary->avg_ms ?? 0), 'violet') !!} {!! $sc('Max. tijd', $ms($summary->max_ms ?? 0), 'orange') !!} {!! $sc('Alarmen', $totalAlarms > 0 ? $totalAlarms : '✓', $totalAlarms > 0 ? 'red' : 'green', $totalAlarms > 0 ? 'beveiligings-\nwaarschuwingen' : 'geen dreigingen') !!}
{{-- ── Security alarms ──────────────────────────────────────────────────────── --}} @php $threatLabels = [ 'sqli' => ['SQL-injectie', '💉', '#ef4444', 'Aanvaller probeerde SQL-code in de URL te injecteren om de database te manipuleren of uit te lezen.'], 'xss' => ['XSS', '🔗', '#f97316', 'Cross-site scripting poging: schadelijke JavaScript of HTML in de URL.'], 'traversal' => ['Padtraversal', '📂', '#f59e0b', 'Aanvaller probeerde via "../" buiten de webroot te navigeren en bestanden te lezen.'], 'scanner' => ['Scannerprobe', '🤖', '#8b5cf6', 'Geautomatiseerde scanner zocht naar bekende kwetsbaarheden (bijv. .env, wp-admin, phpMyAdmin).'], ]; @endphp {{-- Detection legend (always visible, collapsible) --}}
🔍 Wat detecteren wij? — klik om uit te klappen
{{-- SQL injection --}}
💉 SQL-injectie Type: sqli

De aanvaller probeert SQL-commando's in de URL te verstoppen om de database te lezen, aan te passen of te verwijderen. Detectie gebeurt op patronen in het URL-pad en de querystring.

@foreach ([ ["UNION SELECT", "Gegevens uit andere tabellen ophalen"], ["SELECT … FROM", "Directe queryconstructie in de URL"], ["DROP TABLE", "Tabel verwijderen"], ["INSERT INTO / DELETE FROM", "Data manipuleren"], ["EXEC( / CAST( / CONVERT(", "Opgeslagen procedures of typeconversie misbruiken"], ["xp_cmdshell / xp_*", "SQL Server systeemcommando's uitvoeren"], ["OR 1=1 / ' --", "Authenticatie omzeilen via altijd-ware conditie"], ["/* commentaar */", "SQL-commentaar om filters te omzeilen"], ] as [$pattern, $explanation])
{{ $pattern }} {{ $explanation }}
@endforeach
{{-- XSS --}}
🔗 XSS Type: xss

Cross-site scripting: de aanvaller injecteert JavaScript of HTML via de URL, in de hoop dat die code wordt uitgevoerd in de browser van een andere gebruiker.

@foreach ([ ["<script", "Klassiek scripttag-injectie"], ["javascript: / vbscript:", "Protocol-gebaseerde scriptuitvoering"], ["data:text/html", "Inline HTML via data-URI"], ["onerror= / onload= / on*=", "HTML event handlers als aanvalsvector"], ["eval(", "JavaScript eval-uitvoering"], ["expression(", "IE-specifieke CSS expression injection"], ] as [$pattern, $explanation])
{{ $pattern }} {{ $explanation }}
@endforeach
{{-- Path traversal --}}
📂 Padtraversal Type: traversal

Via herhaalde ../ probeert de aanvaller buiten de webroot te navigeren en systeembestanden te lezen (bijv. /etc/passwd, configuratiebestanden).

@foreach ([ ["../../", "Klassieke directoryescaping"], ["..\\ (backslash)", "Windows-variant van padtraversal"], ["%2e%2e%2f", "URL-geëncodeerde versie van ../"], ["%252e%252e", "Dubbel geëncodeerde variant (bypass-poging)"], ] as [$pattern, $explanation])
{{ $pattern }} {{ $explanation }}
@endforeach
{{-- Scanner probes --}}
🤖 Scannerprobe Type: scanner

Geautomatiseerde scanners (zoals Shodan, Nuclei, Nikto) tasten servers af op bekende kwetsbare paden. Geen enkele van deze paden bestaat in ND-Link — elk verzoek ernaar is verdacht.

@foreach ([ ["/.env", "Omgevingsbestand met wachtwoorden en sleutels"], ["/.git", "Blootgestelde Git-repository"], ["/wp-admin / /wp-login.php", "WordPress-beheerpaneel (hier niet aanwezig)"], ["/phpmyadmin", "Databasebeheertool"], ["/xmlrpc.php", "WordPress XML-RPC (veelgebruikt voor aanvallen)"], ["/shell.php / /cmd.php", "Geüploade webshell of backdoor"], ["/.htaccess / /web.config", "Webserverconfiguratie uitlezen"], ["/etc/passwd / /etc/shadow", "Unix systeemgebruikersbestanden"], ["/proc/self", "Linux procesmemory uitlezen"], ["/actuator", "Spring Boot beheer-endpoint"], ["/cgi-bin", "Oude CGI-scripts (vaak kwetsbaar)"], ["/manager/html", "Tomcat-beheerpaneel"], ] as [$pattern, $explanation])
{{ $pattern }} {{ $explanation }}
@endforeach
{{-- Brute force --}}
🔑 Brute force Berekend op basis van HTTP 401/403 per IP

Een IP-adres dat ≥ 10 keer een 401 Unauthorized of 403 Forbidden ontvangt wordt als verdacht gemarkeerd. Dit kan duiden op automatische wachtwoordraapaanvallen (credential stuffing) of op aftasten van afgeschermde pagina's.

≥ 10 weigeringen → alarm (laag risico) ≥ 30 weigeringen → gemiddeld risico ≥ 100 weigeringen → hoog risico Drempel geldt per IP over de gekozen periode
{{-- Flood --}}
🌊 Verkeersoverlast (flood) Berekend op totaal verzoeken per IP per periode

Een IP-adres dat een abnormaal hoog volume verzoeken verstuurt kan duiden op een DDoS-aanval, agressieve webscraper of kapotte client. Drempelwaarden per periode:

@foreach (['24 uur' => 300, 'Week' => 1500, 'Maand' => 5000, 'Jaar' => 15000] as $p => $threshold)
{{ $p }} ≥ {{ number_format($threshold, 0, ',', '.') }} verzoeken → alarm
@endforeach
@if ($totalAlarms > 0)
{{-- Section header --}}
⚠ Beveiligingsalarmen {{ $totalAlarms }} alarm{{ $totalAlarms !== 1 ? 'en' : '' }} — {{ $periods[$period] }}
{{-- Threat probes (sqli, xss, traversal, scanner) --}} @if ($threatAlarms->isNotEmpty())

Injectie- en scanpogingen

Gedetecteerd op basis van verdachte patronen in de URL (pad + querystring).

@foreach ($threatAlarms as $t) @php [$label, $icon, $color, $desc] = $threatLabels[$t->threat] ?? [$t->threat, '⚠', '#6b7280', '']; @endphp @endforeach
Type Betekenis IP-adres Pogingen Laatste activiteit
{{ $icon }} {{ $label }} {{ Str::limit($desc, 80) }} {{ $t->ip ?? '–' }} {{ $t->total }} {{ \Carbon\Carbon::parse($t->last_seen)->format('d/m H:i:s') }}
@endif {{-- Brute force --}} @if ($bruteForceAlarms->isNotEmpty())

🔑 Brute force / toegangspogingen

IP-adressen met ≥ 10 geweigerde verzoeken (HTTP 401/403). Dit kan duiden op herhaalde inlogpogingen of geautomatiseerde aanvallen op afgeschermde pagina's.

@foreach ($bruteForceAlarms as $bf) @php $risk = $bf->total >= 100 ? ['Hoog', '#ef4444'] : ($bf->total >= 30 ? ['Gemiddeld', '#f59e0b'] : ['Laag', '#84cc16']); @endphp @endforeach
IP-adres Weigeringen Unieke paden Laatste poging Risico
{{ $bf->ip ?? '–' }} {{ $bf->total }} {{ $bf->unique_paths }} {{ \Carbon\Carbon::parse($bf->last_seen)->format('d/m H:i:s') }} {{ $risk[0] }}
@endif {{-- Flood --}} @if ($floodAlarms->isNotEmpty())

🌊 Verkeersoverlast (flood)

IP-adressen met een abnormaal hoog aantal verzoeken in de gekozen periode. Kan wijzen op een DDoS-aanval, scraper of defecte client.

@foreach ($floodAlarms as $fl) @endforeach
IP-adres Verzoeken Laatste activiteit
{{ $fl->ip ?? '–' }} {{ number_format($fl->total, 0, ',', '.') }} {{ \Carbon\Carbon::parse($fl->last_seen)->format('d/m H:i:s') }}
@endif
@else
Geen beveiligingsalarmen in {{ $periods[$period] }}
@endif {{-- ── Chart ────────────────────────────────────────────────────────────────── --}}

Verzoeken per {{ $bucketLabel }} — {{ $periods[$period] }}

@php $maxVal = max($chart->max('total'), 1); @endphp
@foreach ($chart as $row) @php $total = $row->total ?? 0; $err = ($row->errors_5xx ?? 0) + ($row->errors_4xx ?? 0); $barH = max($total > 0 ? 4 : 0, round($total / $maxVal * 68)); $label = match($period) { '24h' => \Carbon\Carbon::parse($row->bucket)->format('H') . 'h', 'year' => \Carbon\Carbon::parse($row->bucket . '-01')->format('M'), default => \Carbon\Carbon::parse($row->bucket)->format('d/m'), }; $tooltip = $label . ': ' . $total . ' verzoeken' . ($err ? ', ' . $err . ' fouten' : ''); @endphp
@if ($loop->count <= 24 || $loop->index % max(1, intdiv($loop->count, 12)) === 0) {{ $label }} @endif
@endforeach
{{-- Avg response time as a secondary line annotation --}}

Gemiddelde responstijd: {{ $ms($summary->avg_ms ?? 0) }} · Max: {{ $ms($summary->max_ms ?? 0) }}

{{-- ── Status distribution + Endpoints ────────────────────────────────────── --}}
{{-- Status distribution --}}

HTTP statuscodes

@php $statusColors = [200 => '#34d399', 300 => '#60a5fa', 400 => '#fbbf24', 500 => '#f87171']; $total = $summary->total ?: 1; @endphp @forelse ($statusDist as $row) @php $color = $statusColors[$row->code_group] ?? '#9ca3af'; @endphp
{{ $row->code_group }}–{{ $row->code_group + 99 }} {{ $fmt($row->total) }} ({{ $pct($row->total, $total) }}%)
@empty

Nog geen data.

@endforelse
{{-- Top endpoints --}}

Meest bezocht

@if ($topEndpoints->isEmpty())

Nog geen data.

@else @php $maxHits = max($topEndpoints->max('hits'), 1); @endphp @endif
{{-- ── Slowest endpoints ────────────────────────────────────────────────────── --}}

Traagste endpoints (gem. responstijd)

@if ($slowestEndpoints->isEmpty())

Nog geen data.

@else @php $maxMs = max($slowestEndpoints->max('avg_ms'), 1); @endphp
@foreach ($slowestEndpoints as $ep)
{{ $ep->endpoint }} gem. {{ $ms($ep->avg_ms) }} · max {{ $ms($ep->max_ms) }}
@endforeach
@endif
{{-- ── Error drill-down ────────────────────────────────────────────────────── --}} @if ($errorBreakdown->isNotEmpty())

Foutanalyse

{{-- Legend: 4xx vs 5xx explanation --}}
4xx = clientfout — verzoek is ongeldig of niet toegestaan (bijv. pagina niet gevonden, niet ingelogd, CSRF verlopen) 5xx = serverfout — de server kon het verzoek niet verwerken (bijv. crash, configuratiefout)
{{-- Status code breakdown mini-grid --}} @php $statusDesc = [ 400 => 'Ongeldig verzoek (Bad Request)', 401 => 'Niet ingelogd (Unauthorized)', 403 => 'Geen toegang (Forbidden)', 404 => 'Pagina niet gevonden (Not Found)', 405 => 'Methode niet toegestaan (Method Not Allowed)', 419 => 'CSRF-token verlopen (Page Expired)', 422 => 'Validatiefout (Unprocessable Entity)', 429 => 'Te veel verzoeken (Too Many Requests)', 500 => 'Interne serverfout (Internal Server Error)', 502 => 'Slechte gateway (Bad Gateway)', 503 => 'Service niet beschikbaar (Service Unavailable)', 504 => 'Gateway timeout (Gateway Timeout)', ]; @endphp
@foreach ($errorBreakdown as $eb) @php $ebColor = $eb->status >= 500 ? '#f87171' : ($eb->status >= 400 ? '#fbbf24' : '#60a5fa'); $ebPct = $summary->total > 0 ? round($eb->total / $summary->total * 100, 1) : 0; $ebDesc = $statusDesc[$eb->status] ?? 'HTTP ' . $eb->status; @endphp @endforeach
{{-- Error log table --}}

Recentste fouten (max 100 getoond)

@foreach ($recentErrors as $err) @php $is5xx = $err->status >= 500; $rowColor = $is5xx ? '#fef2f2' : '#fffbeb'; $badgeCol = $is5xx ? '#ef4444' : '#f59e0b'; @endphp @endforeach
Tijd Methode Pad Status Duur IP
{{ $err->created_at->format('d/m H:i:s') }} {{ $err->method }} {{ $err->path }} {{ $err->status }} {{ $ms($err->duration_ms) }} {{ $err->ip }}
@if ($recentErrors->isEmpty())

Geen fouten in deze periode.

@endif
@endif @endsection