<?php if (!defined('DASHBOARD_ROOT')) { http_response_code(403); exit; } class ChainValidator { private const MAX_CHAIN_LENGTH = 10; private const VALID_CHAINS = [ 'home -> category -> product -> cart -> checkout', 'home -> search -> product -> cart -> checkout', 'home -> category -> category -> product', 'home -> category -> product -> product', 'home -> reviews -> product -> cart', 'home -> faq -> home -> category', ]; private const SUSPICIOUS_DIRECT = [ 'checkout' => 30, 'api/order' => 50, 'api/payment' => 50, 'account' => 10, ]; public static function recordTransition(string $page): void { if (!isset($_SESSION['_page_chain'])) { $_SESSION['_page_chain'] = []; } $_SESSION['_page_chain'][] = $page; if (count($_SESSION['_page_chain']) > self::MAX_CHAIN_LENGTH) { $_SESSION['_page_chain'] = array_slice($_SESSION['_page_chain'], -self::MAX_CHAIN_LENGTH); } $_SESSION['_chain_updated'] = time(); } public static function entropyPenalty(): int { $chain = $_SESSION['_page_chain'] ?? []; $penalty = 0; if (count($chain) < 2) { return 0; } $firstPage = $chain[0] ?? ''; if (isset(self::SUSPICIOUS_DIRECT[$firstPage])) { $penalty += self::SUSPICIOUS_DIRECT[$firstPage]; } $transitions = []; for ($i = 1; $i < count($chain); $i++) { $t = $chain[$i - 1] . ' -> ' . $chain[$i]; $transitions[] = $t; } $unique = count(array_unique($transitions)); $total = count($transitions); if ($total > 3) { $repetitionRatio = 1 - ($unique / $total); $penalty += (int)($repetitionRatio * 30); } $pageCounts = array_count_values($chain); $maxHits = max($pageCounts); if ($maxHits >= 4) { $penalty += min(20, ($maxHits - 3) * 5); } $chainStart = $_SESSION['_chain_start'] ?? time(); $elapsed = time() - $chainStart; $pagesPerSecond = count($chain) / max(1, $elapsed); if ($pagesPerSecond > 2) { $penalty += 15; } return min(50, $penalty); } public static function getSessionChain(): array { return [ 'chain' => $_SESSION['_page_chain'] ?? [], 'entropy' => self::entropyPenalty(), 'length' => count($_SESSION['_page_chain'] ?? []), 'updated' => $_SESSION['_chain_updated'] ?? null, ]; } public static function analyzeIP(string $ip, int $hours = 24): array { try { $events = Database::fetchAll( "SELECT action, created_at FROM events                  WHERE ip_address = :ip AND created_at > DATE_SUB(NOW(), INTERVAL :h HOUR)                  ORDER BY created_at ASC LIMIT 200", [':ip' => $ip, ':h' => $hours] ); } catch (\Throwable $e) { $events = []; } $pages = array_column($events, 'action'); $uniquePages = count(array_unique($pages)); $totalPages = count($pages); return [ 'ip' => $ip, 'total_hits' => $totalPages, 'unique_pages' => $uniquePages, 'entropy_ratio'=> $totalPages > 0 ? round($uniquePages / $totalPages, 3) : 0, 'chain' => array_slice($pages, 0, 20), 'suspicious' => $totalPages > 10 && ($uniquePages / $totalPages) < 0.3, ]; } } 