Files
Atomcms-edit/app/Console/Commands/DDoSDetectionCommand.php
T

215 lines
6.7 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Services\AlertService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
class DDoSDetectionCommand extends Command
{
#[\Override]
protected $signature = 'monitor:ddos
{--threshold=100 : Minimum requests per IP om als verdacht te markeren}
{--time-window=60 : Tijd window in seconden}
{--block : Automatisch IPs blokkeren na detectie}';
#[\Override]
protected $description = 'Detecteer mogelijke DDoS aanvallen en stuur alerts';
private const string CACHE_KEY_DDOS_TRACKING = 'ddos_tracking';
private const string CACHE_KEY_BLOCKED_IPS = 'ddos_blocked_ips';
private const int TRACKING_DURATION_SECONDS = 300;
public function handle(AlertService $alertService): int
{
$threshold = (int) $this->option('threshold');
$timeWindow = (int) $this->option('time-window');
$autoBlock = (bool) $this->option('block');
$this->info("DDoS detectie gestart (threshold: {$threshold} req/IP in {$timeWindow}s)");
$ddosData = $this->getDDoSData();
$suspiciousIps = $this->analyzeTraffic($ddosData, $threshold, $timeWindow);
if ($suspiciousIps === []) {
$this->line('Geen verdachte activiteit gedetecteerd.');
return Command::SUCCESS;
}
$this->handleSuspiciousIps($suspiciousIps, $alertService, $autoBlock);
return Command::SUCCESS;
}
private function getDDoSData(): array
{
$data = Cache::get(self::CACHE_KEY_DDOS_TRACKING, []);
if (empty($data)) {
$data = $this->getManualTrackingData();
}
return $data;
}
private function getManualTrackingData(): array
{
$tracking = Cache::get('manual_ip_tracking', []);
foreach ($tracking as $ip => $data) {
$tracking[$ip]['requests'] = array_filter(
$data['requests'],
fn ($timestamp) => $timestamp > time() - self::TRACKING_DURATION_SECONDS,
);
$tracking[$ip]['count'] = count($tracking[$ip]['requests']);
if ($tracking[$ip]['count'] === 0) {
unset($tracking[$ip]);
}
}
Cache::put('manual_ip_tracking', $tracking, self::TRACKING_DURATION_SECONDS);
return $tracking;
}
private function analyzeTraffic(array $data, int $threshold, int $timeWindow): array
{
$suspicious = [];
$cutoffTime = time() - $timeWindow;
foreach ($data as $ip => $info) {
$recentRequests = array_filter(
$info['requests'],
fn ($timestamp) => $timestamp > $cutoffTime,
);
$requestCount = count($recentRequests);
if ($requestCount >= $threshold) {
$suspicious[$ip] = [
'count' => $requestCount,
'time_window' => $timeWindow,
'first_seen' => $recentRequests === [] ? time() : min($recentRequests),
'last_seen' => $recentRequests === [] ? time() : max($recentRequests),
];
}
}
return $suspicious;
}
private function handleSuspiciousIps(array $suspiciousIps, AlertService $alertService, bool $autoBlock): void
{
$blockedIps = Cache::get(self::CACHE_KEY_BLOCKED_IPS, []);
$newBlocks = [];
foreach ($suspiciousIps as $ip => $details) {
if (! $this->isValidIp($ip)) {
continue;
}
$this->error("VERDACHTE IP GEDETECTEERD: {$ip}");
$this->table(
['Metric', 'Value'],
[
['Requests', $details['count']],
['Time Window', $details['time_window'] . 's'],
['First Seen', date('H:i:s', $details['first_seen'])],
['Last Seen', date('H:i:s', $details['last_seen'])],
],
);
if ($autoBlock && ! in_array($ip, $blockedIps)) {
$this->blockIp($ip);
$blockedIps[] = $ip;
$newBlocks[] = $ip;
}
}
$totalRequests = array_sum(array_column($suspiciousIps, 'count'));
$uniqueIps = count($suspiciousIps);
$alertService->sendDDoSDetected([
'total_requests' => $totalRequests,
'unique_ips' => $uniqueIps,
'time_window' => array_first($suspiciousIps)['time_window'],
'suspicious_ips' => array_keys($suspiciousIps),
'auto_blocked' => $newBlocks,
]);
if ($newBlocks !== []) {
$this->info('Automatisch geblokkeerd: ' . implode(', ', $newBlocks));
}
Cache::put(self::CACHE_KEY_BLOCKED_IPS, $blockedIps, 3600);
}
private function isValidIp(string $ip): bool
{
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
}
private function blockIp(string $ip): void
{
if (! $this->isValidIp($ip)) {
Log::warning("Attempted to block invalid IP: {$ip}");
return;
}
try {
Process::run(['iptables', '-A', 'INPUT', '-s', $ip, '-j', 'DROP']);
Log::warning("IP blocked due to DDoS detection: {$ip}");
$this->warn("IP {$ip} geblokkeerd via iptables.");
} catch (\Exception $e) {
Log::error("Failed to block IP {$ip}: " . $e->getMessage());
$this->error("Kon IP {$ip} niet blokkeren: " . $e->getMessage());
}
}
public static function trackRequest(string $ip): void
{
if (! filter_var($ip, FILTER_VALIDATE_IP)) {
return;
}
$tracking = Cache::get('manual_ip_tracking', []);
if (! isset($tracking[$ip])) {
$tracking[$ip] = ['requests' => [], 'count' => 0];
}
$tracking[$ip]['requests'][] = time();
$tracking[$ip]['count']++;
Cache::put('manual_ip_tracking', $tracking, 300);
}
public static function getBlockedIps(): array
{
return Cache::get(self::CACHE_KEY_BLOCKED_IPS, []);
}
public static function clearBlockedIp(string $ip): void
{
if (! filter_var($ip, FILTER_VALIDATE_IP)) {
return;
}
$blocked = Cache::get(self::CACHE_KEY_BLOCKED_IPS, []);
$blocked = array_filter($blocked, fn ($blockedIp) => $blockedIp !== $ip);
Cache::put(self::CACHE_KEY_BLOCKED_IPS, array_values($blocked), 3600);
Process::run(['iptables', '-D', 'INPUT', '-s', $ip, '-j', 'DROP']);
}
}