prepare(' SELECT COUNT(*) as attempts, COUNT(DISTINCT username) as unique_users, MIN(attempt_time) as first_attempt, MAX(attempt_time) as last_attempt FROM sec_failed_login_attempts WHERE ip_address = :ip AND attempt_time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) '); $stmt->execute(['ip' => $ip]); $result = $stmt->fetch(PDO::FETCH_ASSOC); $attempts = (int)$result['attempts']; $uniqueUsers = (int)$result['unique_users']; // Critères de détection $isBruteForce = false; $reason = ''; if ($attempts >= 5) { $isBruteForce = true; $reason = "Plus de 5 tentatives en 5 minutes"; } elseif ($uniqueUsers >= 3) { $isBruteForce = true; $reason = "Tentatives sur 3 usernames différents"; } if ($isBruteForce) { // Déclencher l'alerte AlertService::trigger('BRUTE_FORCE', [ 'ip' => $ip, 'username' => $username, 'attempts' => $attempts, 'unique_usernames' => $uniqueUsers, 'timeframe' => '5 minutes', 'reason' => $reason, 'message' => "Attaque brute force détectée depuis $ip : $reason" ], 'SECURITY'); return false; // Bloquer } return true; // Autoriser } /** * Enregistrer une tentative de login échouée */ public static function recordFailedLogin( string $ip, string $username = null, string $errorType = 'invalid_credentials', string $userAgent = null ): void { $db = self::getDb(); try { // Chercher si le username existe (pour stocker la version chiffrée) $encryptedUsername = null; if ($username) { $userStmt = $db->prepare(' SELECT encrypted_user_name FROM users WHERE username = :username LIMIT 1 '); $userStmt->execute(['username' => $username]); $user = $userStmt->fetch(PDO::FETCH_ASSOC); if ($user) { $encryptedUsername = $user['encrypted_user_name']; } } // Enregistrer la tentative $stmt = $db->prepare(' INSERT INTO sec_failed_login_attempts ( username, encrypted_username, ip_address, user_agent, error_type ) VALUES ( :username, :encrypted, :ip, :agent, :error ) '); $stmt->execute([ 'username' => $username, 'encrypted' => $encryptedUsername, 'ip' => $ip, 'agent' => $userAgent, 'error' => $errorType ]); // Vérifier si on doit bloquer l'IP if (!self::checkBruteForce($ip, $username)) { // L'IP sera bloquée par AlertService via les actions automatiques } } catch (\Exception $e) { error_log("Failed to record login attempt: " . $e->getMessage()); } } /** * Vérifier les patterns d'injection SQL */ public static function checkSQLInjection(string $value): bool { // Nettoyer la valeur pour l'analyse $decoded = urldecode($value); foreach (self::SQL_INJECTION_PATTERNS as $pattern) { if (preg_match($pattern, $decoded)) { // Injection SQL détectée ! $context = [ 'pattern_matched' => $pattern, 'value' => substr($value, 0, 500), // Limiter la taille 'decoded_value' => substr($decoded, 0, 500), 'message' => 'Tentative d\'injection SQL détectée' ]; // Enrichir avec le contexte de requête if (isset($_SERVER['REQUEST_URI'])) { $context['endpoint'] = $_SERVER['REQUEST_URI']; $context['method'] = $_SERVER['REQUEST_METHOD'] ?? 'unknown'; $context['ip'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; } AlertService::trigger('SQL_INJECTION', $context, 'SECURITY'); return false; // Bloquer la requête } } return true; // Pas d'injection détectée } /** * Vérifier tous les paramètres d'une requête */ public static function checkRequestParameters(array $params): bool { $suspicious = []; foreach ($params as $key => $value) { if (is_string($value)) { if (!self::checkSQLInjection($value)) { $suspicious[$key] = $value; } } elseif (is_array($value)) { // Vérifier récursivement if (!self::checkRequestParameters($value)) { $suspicious[$key] = $value; } } } if (!empty($suspicious)) { // Paramètres suspects détectés return false; } return true; } /** * Détecter les patterns de scan/exploration */ public static function checkScanPattern(string $uri): bool { $lowerUri = strtolower($uri); foreach (self::SCAN_PATTERNS as $pattern) { if (strpos($lowerUri, $pattern) !== false) { // Pattern de scan détecté AlertService::trigger('SUSPICIOUS_PATTERN', [ 'pattern' => $pattern, 'uri' => $uri, 'message' => "Tentative d'accès à un fichier sensible : $pattern" ], 'WARNING'); return false; } } return true; } /** * Vérifier les accès non autorisés */ public static function checkUnauthorizedAccess(string $endpoint, bool $hasAuth): bool { // Endpoints publics qui n'ont pas besoin d'auth $publicEndpoints = [ '/api/login', '/api/users/check-username', '/api/logs', '/api/health', '/api/status' ]; // Vérifier si l'endpoint nécessite une auth $isPublic = false; foreach ($publicEndpoints as $public) { if (strpos($endpoint, $public) === 0) { $isPublic = true; break; } } if (!$isPublic && !$hasAuth) { // Accès non autorisé AlertService::trigger('UNAUTHORIZED_ACCESS', [ 'endpoint' => $endpoint, 'message' => "Tentative d'accès sans authentification à : $endpoint" ], 'WARNING'); return false; } return true; } /** * Analyser les patterns 404 */ public static function check404Pattern(string $ip): void { $db = self::getDb(); // Compter les 404 récents de cette IP $stmt = $db->prepare(' SELECT COUNT(*) as count, COUNT(DISTINCT endpoint) as unique_endpoints FROM sec_performance_metrics WHERE ip_address = :ip AND http_status = 404 AND created_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE) '); $stmt->execute(['ip' => $ip]); $result = $stmt->fetch(PDO::FETCH_ASSOC); $count404 = (int)$result['count']; $uniqueEndpoints = (int)$result['unique_endpoints']; // Si trop de 404, c'est suspect if ($count404 >= 10 || $uniqueEndpoints >= 5) { AlertService::trigger('HTTP_404_PATTERN', [ 'ip' => $ip, 'count_404' => $count404, 'unique_endpoints' => $uniqueEndpoints, 'timeframe' => '10 minutes', 'message' => "Pattern de scan détecté : $count404 erreurs 404 sur $uniqueEndpoints endpoints" ], 'WARNING'); } } /** * Vérifier la vitesse des requêtes (rate limiting) */ public static function checkRateLimit(string $ip): bool { $db = self::getDb(); // Compter les requêtes de la dernière minute $stmt = $db->prepare(' SELECT COUNT(*) as count FROM sec_performance_metrics WHERE ip_address = :ip AND created_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) '); $stmt->execute(['ip' => $ip]); $result = $stmt->fetch(PDO::FETCH_ASSOC); $requestsPerMinute = (int)$result['count']; // Limite : 60 requêtes par minute if ($requestsPerMinute > 60) { AlertService::trigger('RATE_LIMIT_EXCEEDED', [ 'ip' => $ip, 'requests_per_minute' => $requestsPerMinute, 'limit' => 60, 'message' => "Limite de taux dépassée : $requestsPerMinute requêtes/minute" ], 'WARNING'); return false; } return true; } /** * Analyser une erreur SQL pour détecter les problèmes */ public static function analyzeSQLError(\PDOException $e, string $query = null): void { $message = $e->getMessage(); $code = $e->getCode(); // Classifier l'erreur $level = 'ERROR'; $type = 'SQL_ERROR'; if (strpos($message, 'Unknown column') !== false) { $type = 'SQL_ERROR'; $level = 'ERROR'; } elseif (strpos($message, 'Table') !== false && strpos($message, 'doesn\'t exist') !== false) { $type = 'SQL_ERROR'; $level = 'CRITICAL'; } elseif (strpos($message, 'Connection refused') !== false || strpos($message, 'Can\'t connect') !== false) { $type = 'DB_CONNECTION'; $level = 'CRITICAL'; } elseif (strpos($message, 'Too many connections') !== false) { $type = 'DB_CONNECTION'; $level = 'CRITICAL'; } elseif (strpos($message, 'Deadlock') !== false) { $type = 'DB_DEADLOCK'; $level = 'ERROR'; } // Créer l'alerte AlertService::trigger($type, [ 'sql_error' => $message, 'sql_code' => $code, 'query' => $query ? substr($query, 0, 500) : null, 'message' => "Erreur SQL : $message" ], $level); } /** * Obtenir les statistiques de sécurité */ public static function getSecurityStats(): array { $db = self::getDb(); // Statistiques des dernières 24h $stats = []; // Tentatives de login $loginStmt = $db->prepare(' SELECT COUNT(*) as total_attempts, COUNT(DISTINCT ip_address) as unique_ips, COUNT(DISTINCT username) as unique_usernames FROM sec_failed_login_attempts WHERE attempt_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR) '); $loginStmt->execute(); $stats['failed_logins'] = $loginStmt->fetch(PDO::FETCH_ASSOC); // IPs bloquées $blockedStmt = $db->prepare(' SELECT COUNT(*) as total, SUM(CASE WHEN permanent = 1 THEN 1 ELSE 0 END) as permanent, SUM(CASE WHEN blocked_until > NOW() THEN 1 ELSE 0 END) as active FROM sec_blocked_ips '); $blockedStmt->execute(); $stats['blocked_ips'] = $blockedStmt->fetch(PDO::FETCH_ASSOC); // Alertes par type $alertsStmt = $db->prepare(' SELECT alert_type, alert_level, COUNT(*) as count, MAX(last_seen) as last_seen FROM sec_alerts WHERE last_seen >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY alert_type, alert_level ORDER BY count DESC '); $alertsStmt->execute(); $stats['alerts_by_type'] = $alertsStmt->fetchAll(PDO::FETCH_ASSOC); return $stats; } /** * Obtenir l'instance de base de données */ private static function getDb(): PDO { if (self::$db === null) { self::$db = Database::getInstance(); } return self::$db; } }