You've already forked Atomcms-edit
215 lines
6.7 KiB
PHP
Executable File
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']);
|
|
}
|
|
}
|