You've already forked Atomcms-edit
222 lines
6.9 KiB
PHP
Executable File
222 lines
6.9 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;
|
|
|
|
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->fetchApacheTrafficData();
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function fetchApacheTrafficData(): array
|
|
{
|
|
$this->warn('Gebruik handmatige IP tracking (geen server logs toegang).');
|
|
|
|
return $this->getManualTrackingData();
|
|
}
|
|
|
|
private function tailFile(string $path, int $lines = 100): array
|
|
{
|
|
if (! file_exists($path)) {
|
|
return [];
|
|
}
|
|
|
|
$file = new \SplFileObject($path, 'r');
|
|
$file->seek(PHP_INT_MAX);
|
|
$totalLines = $file->key() + 1;
|
|
|
|
$startLine = max(0, $totalLines - $lines);
|
|
$result = [];
|
|
|
|
$file->seek($startLine);
|
|
while (! $file->eof()) {
|
|
$result[] = rtrim($file->current(), "\r\n");
|
|
$file->next();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
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) {
|
|
$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 blockIp(string $ip): void
|
|
{
|
|
try {
|
|
$escapedIp = escapeshellarg($ip);
|
|
exec("iptables -A INPUT -s {$escapedIp} -j DROP 2>/dev/null");
|
|
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
|
|
{
|
|
$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, self::TRACKING_DURATION_SECONDS);
|
|
}
|
|
|
|
public static function getBlockedIps(): array
|
|
{
|
|
return Cache::get(self::CACHE_KEY_BLOCKED_IPS, []);
|
|
}
|
|
|
|
public static function clearBlockedIp(string $ip): void
|
|
{
|
|
$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);
|
|
|
|
$escapedIp = escapeshellarg($ip);
|
|
exec("iptables -D INPUT -s {$escapedIp} -j DROP 2>/dev/null");
|
|
}
|
|
}
|