@anonympins/fingerprint 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -186,7 +186,7 @@ class RequestUtils
186
186
  public static function getHeaderAnomalies(RequestContext $context): array
187
187
  {
188
188
  $anomalyScore = 0;
189
- $ua = $context->getHeader('user-agent');
189
+ $ua = $context->getHeader('user-agent') ?? '';
190
190
  if (empty($ua) || strlen($ua) < 10) {
191
191
  $anomalyScore += 60;
192
192
  }
@@ -197,6 +197,17 @@ class RequestUtils
197
197
  $anomalyScore += 15;
198
198
  }
199
199
 
200
+ // TE: trailers check for Firefox on Desktop
201
+ $uaParts = self::parseUserAgent($ua);
202
+ $isFirefoxDesktop = isset($uaParts['browser']) && str_starts_with($uaParts['browser'], 'Firefox') && ($uaParts['device'] ?? 'desktop') === 'desktop';
203
+ $te = strtolower($context->getHeader('te') ?? '');
204
+
205
+ if ($isFirefoxDesktop && $te !== 'trailers') {
206
+ $anomalyScore += 30;
207
+ } elseif (!$isFirefoxDesktop && ($uaParts['device'] ?? 'desktop') === 'desktop' && $te === 'trailers') {
208
+ $anomalyScore += 30;
209
+ }
210
+
200
211
  return ['headerAnomalyScore' => min(100.0, $anomalyScore)];
201
212
  }
202
213
 
@@ -520,7 +531,7 @@ class RequestUtils
520
531
  $deviceData['lastChangeTimestamp'] = $now;
521
532
  }
522
533
  $deviceData['lastFpHash'] = $currentFpHash;
523
-
534
+
524
535
  // Enregistrement de l'IP
525
536
  if (!in_array($clientIp, $deviceData['ips'])) {
526
537
  $deviceData['ips'][] = $clientIp;
@@ -620,6 +631,25 @@ class RequestUtils
620
631
  }
621
632
  }
622
633
 
634
+ // Détection d'énumération de chemins (crawling/scraping de ressources séquentielles)
635
+ $enumerationScore = 0;
636
+ if (count($history) >= 3) {
637
+ $templates = array_map(function($h) {
638
+ return preg_replace('/\d+/', '{num}', $h['path']);
639
+ }, $history);
640
+
641
+ $uniquePaths = array_unique(array_map(function($h) {
642
+ return $h['path'];
643
+ }, $history));
644
+
645
+ $templateCounts = array_count_values($templates);
646
+ $maxTemplateRepetition = !empty($templateCounts) ? max($templateCounts) : 0;
647
+
648
+ if ($maxTemplateRepetition >= 3 && count($uniquePaths) === count($history)) {
649
+ $enumerationScore = $patternWeight * 0.8;
650
+ }
651
+ }
652
+
623
653
  // Logique de décroissance et de score final
624
654
  $newPatternScore = $deviceData['lastPatternScore'] ?? 0;
625
655
 
@@ -630,7 +660,7 @@ class RequestUtils
630
660
  }
631
661
  $newPatternScore = max(0, $newPatternScore);
632
662
 
633
- $deviceData['lastPatternScore'] = $newPatternScore + $instantScore;
663
+ $deviceData['lastPatternScore'] = $newPatternScore + $instantScore + $enumerationScore;
634
664
 
635
665
  return ['requestPatternScore' => min(100.0, $deviceData['lastPatternScore'])];
636
666
  }
@@ -749,7 +779,7 @@ class RequestUtils
749
779
  * Parse une chaîne de requête GraphQL pour extraire le type et le nom de l'opération.
750
780
  * @param array<string, mixed> $body Le corps de la requête.
751
781
  * @return array{type: string, name: string}|null
752
- */
782
+ */
753
783
  public static function parseGraphQLQuery(array $body): ?array
754
784
  {
755
785
  $query = $body['query'] ?? null;
@@ -841,7 +871,7 @@ class RequestUtils
841
871
  if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
842
872
  $ipBinary = inet_pton($ip);
843
873
  if ($ipBinary === false) return null;
844
-
874
+
845
875
  $mask = self::generateMask($ipv4Prefix, 4);
846
876
  if ($mask === null) return null;
847
877
 
@@ -850,7 +880,7 @@ class RequestUtils
850
880
  } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
851
881
  $ipBinary = inet_pton($ip);
852
882
  if ($ipBinary === false) return null;
853
-
883
+
854
884
  $mask = self::generateMask($ipv6Prefix, 16);
855
885
  if ($mask === null) return null;
856
886
 
@@ -959,4 +989,155 @@ class RequestUtils
959
989
  $newScore = min(100.0, max(0.0, $current + $change));
960
990
  $store->set($key, ['score' => $newScore, 'lastUpdate' => time()], 86400 * 7); // TTL de 7 jours
961
991
  }
992
+
993
+
994
+ /**
995
+ * Assainit les données de trafic pour l'auto-tuner afin de prévenir les attaques par empoisonnement.
996
+ * Limite la contribution de chaque deviceId à un pourcentage maximum (ex: 2%) du jeu de données total.
997
+ *
998
+ * @param array<int, array<string, mixed>> $trafficData
999
+ * @return array<int, array<string, mixed>>
1000
+ */
1001
+ public static function sanitizeTrafficData(array $trafficData): array
1002
+ {
1003
+ if (empty($trafficData)) {
1004
+ return [];
1005
+ }
1006
+
1007
+ $tempSanitized = [];
1008
+ $deviceCounts = [];
1009
+ $maxLogsPerDevice = max(3, (int)floor(count($trafficData) * 0.02));
1010
+
1011
+ foreach ($trafficData as $log) {
1012
+ $deviceId = $log['deviceId'] ?? 'anonymous';
1013
+ if (!isset($deviceCounts[$deviceId])) {
1014
+ $deviceCounts[$deviceId] = 0;
1015
+ }
1016
+ if ($deviceCounts[$deviceId] < $maxLogsPerDevice) {
1017
+ $deviceCounts[$deviceId]++;
1018
+ $tempSanitized[] = $log;
1019
+ }
1020
+ }
1021
+
1022
+ $passedLogs = [];
1023
+ $suspiciousLogs = [];
1024
+ foreach ($tempSanitized as $log) {
1025
+ if (($log['type'] ?? '') === 'request_passed') {
1026
+ $passedLogs[] = $log;
1027
+ } else {
1028
+ $suspiciousLogs[] = $log;
1029
+ }
1030
+ }
1031
+
1032
+ $minDataPoints = 200; // Seuil par défaut
1033
+ $maxPassedAllowed = max($minDataPoints, count($suspiciousLogs) * 9);
1034
+
1035
+ if (count($passedLogs) > $maxPassedAllowed) {
1036
+ shuffle($passedLogs);
1037
+ $passedLogs = array_slice($passedLogs, 0, $maxPassedAllowed);
1038
+ }
1039
+
1040
+ return array_merge($suspiciousLogs, $passedLogs);
1041
+ }
1042
+
1043
+ /**
1044
+ * Génère une signature HMAC-SHA256 pour sécuriser les données du challenge stockées.
1045
+ * @param string $secret Le secret global (POW_SECRET).
1046
+ * @param array<string, mixed> $payload Les données du challenge.
1047
+ * @param string $clientIp L'IP du client pour lier la signature.
1048
+ * @return string
1049
+ */
1050
+ public static function signChallengePayload(string $secret, array $payload, string $clientIp): string
1051
+ {
1052
+ $dataToSign = implode(':', [
1053
+ $payload['clientSecret'] ?? '',
1054
+ $payload['cpuTarget'] ?? '',
1055
+ $payload['fingerprint'] ?? '',
1056
+ $payload['memDifficulty'] ?? '',
1057
+ $payload['originalPath'] ?? '',
1058
+ $clientIp
1059
+ ]);
1060
+
1061
+ return hash_hmac('sha256', $dataToSign, $secret);
1062
+ }
1063
+
1064
+ /**
1065
+ * Vérifie la signature HMAC-SHA256 des données de challenge récupérées du store.
1066
+ * @param string $secret Le secret global (POW_SECRET).
1067
+ * @param array<string, mixed> $payload Les données du challenge contenant la signature.
1068
+ * @param string $clientIp L'IP du client.
1069
+ * @return bool True si la signature est valide, false sinon.
1070
+ */
1071
+ public static function verifyChallengePayload(string $secret, array $payload, string $clientIp): bool
1072
+ {
1073
+ if (empty($payload['signature'])) {
1074
+ return false;
1075
+ }
1076
+
1077
+ $storedSignature = $payload['signature'];
1078
+ $payloadWithoutSig = $payload;
1079
+ unset($payloadWithoutSig['signature']);
1080
+
1081
+ $expectedSignature = self::signChallengePayload($secret, $payloadWithoutSig, $clientIp);
1082
+
1083
+ return hash_equals($expectedSignature, $storedSignature);
1084
+ }
1085
+
1086
+ /**
1087
+ * Vérifie si un ticket de clearance (PoW) est valide, en supportant la tolérance au roaming.
1088
+ *
1089
+ * @param string $ip L'adresse IP de la requête courante.
1090
+ * @param string|null $ticket Le ticket de clearance extrait du cookie.
1091
+ * @param string $deviceId L'identifiant du cookie de l'appareil.
1092
+ * @param string $deviceHash L'empreinte matérielle calculée côté serveur.
1093
+ * @param string $secret La clé secrète (POW_SECRET).
1094
+ * @return bool True si le ticket est valide et correspond aux contraintes de sécurité.
1095
+ */
1096
+ public static function isTicketValid(string $ip, ?string $ticket, string $deviceId = '', string $deviceHash = '', string $secret = ''): bool
1097
+ {
1098
+ if (empty($ticket)) {
1099
+ return false;
1100
+ }
1101
+
1102
+ if (str_contains($ticket, '|')) {
1103
+ $parts = explode('|', $ticket);
1104
+ if (count($parts) < 3) return false;
1105
+ [$expiry, $originalIp, $sig] = $parts;
1106
+ } elseif (str_contains($ticket, ':')) {
1107
+ // Fallback rétrocompatible pour les anciens tickets
1108
+ $parts = explode(':', $ticket);
1109
+ if (count($parts) < 2) return false;
1110
+ [$expiry, $sig] = $parts;
1111
+ $originalIp = $ip;
1112
+ } else {
1113
+ return false;
1114
+ }
1115
+
1116
+ if (empty($expiry) || empty($sig) || (time() * 1000) > (int)$expiry) {
1117
+ return false;
1118
+ }
1119
+
1120
+ if (str_contains($ticket, '|')) {
1121
+ $expectedSig = hash_hmac('sha256', "{$expiry}:{$originalIp}:{$deviceId}:{$deviceHash}", $secret);
1122
+ } else {
1123
+ $expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", $secret);
1124
+ }
1125
+
1126
+ if (!hash_equals($expectedSig, $sig)) {
1127
+ return false;
1128
+ }
1129
+
1130
+ if (!str_contains($ticket, '|')) {
1131
+ return $ip === $originalIp;
1132
+ }
1133
+
1134
+ if ($ip === $originalIp) return true;
1135
+ $currentSubnet = self::getIpSubnet($ip);
1136
+ $originalSubnet = self::getIpSubnet($originalIp);
1137
+ if ($currentSubnet !== null && $originalSubnet !== null && $currentSubnet === $originalSubnet) {
1138
+ return true;
1139
+ }
1140
+
1141
+ return !empty($deviceId) && !empty($deviceHash); // Match d'identité matérielle stricte (deviceId + deviceHash validés par HMAC)
1142
+ }
962
1143
  }
@@ -0,0 +1,118 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * CLI Script d'auto-tuning périodique des configurations de sécurité.
7
+ * Ce script lit la configuration courante, récupère les logs, les assainit,
8
+ * exécute l'optimiseur génétique, et écrase le fichier JSON d'origine.
9
+ */
10
+
11
+ $autoloaderPaths = [
12
+ __DIR__ . '/../../vendor/autoload.php',
13
+ __DIR__ . '/../vendor/autoload.php',
14
+ __DIR__ . '/../../../autoload.php',
15
+ ];
16
+
17
+ $autoloaded = false;
18
+ foreach ($autoloaderPaths as $path) {
19
+ if (file_exists($path)) {
20
+ require_once $path;
21
+ $autoloaded = true;
22
+ break;
23
+ }
24
+ }
25
+
26
+ if (!$autoloaded) {
27
+ fwrite(STDERR, "Erreur : Impossible de charger l'autoloader. Exécutez 'composer install'.\n");
28
+ exit(1);
29
+ }
30
+
31
+ use Anonympins\Fingerprint\Utils\RequestUtils;
32
+ use Anonympins\Fingerprint\Store\StoreManager;
33
+ use Anonympins\Fingerprint\Optimization\Optimization;
34
+
35
+ // 1. Récupération des arguments CLI
36
+ $configPath = $argv[1] ?? null;
37
+ if (!$configPath) {
38
+ echo "Usage: php auto-tune.php [chemin_vers_security-config.json]\n";
39
+ exit(1);
40
+ }
41
+
42
+ if (!file_exists($configPath)) {
43
+ fwrite(STDERR, "Erreur : Fichier de configuration introuvable : {$configPath}\n");
44
+ exit(1);
45
+ }
46
+
47
+ $config = json_decode(file_get_contents($configPath), true);
48
+ if (json_last_error() !== JSON_ERROR_NONE) {
49
+ fwrite(STDERR, "Erreur : Fichier de configuration JSON invalide.\n");
50
+ exit(1);
51
+ }
52
+
53
+ // 2. Connexion au store pour récupérer les logs accumulés
54
+ $store = StoreManager::getStore();
55
+ if (!$store) {
56
+ fwrite(STDERR, "Erreur : Aucun store de persistance actif.\n");
57
+ exit(1);
58
+ }
59
+
60
+ $rawTrafficLogs = $store->get('traffic_logs') ?? [];
61
+ if (empty($rawTrafficLogs)) {
62
+ echo "[AutoTuning] Aucun log de trafic disponible pour l'optimisation.\n";
63
+ exit(0);
64
+ }
65
+
66
+ // 3. Nettoyage des données pour prévenir l'empoisonnement (Sybil attacks)
67
+ $sanitizedLogs = RequestUtils::sanitizeTrafficData($rawTrafficLogs);
68
+
69
+ $minDataPoints = $config['autotuning']['minDataPoints'] ?? 200;
70
+ if (count($sanitizedLogs) < $minDataPoints) {
71
+ echo "[AutoTuning] Reporté : Pas assez de données assainies (" . count($sanitizedLogs) . "/{$minDataPoints}).\n";
72
+ exit(0);
73
+ }
74
+
75
+ echo "[AutoTuning] Lancement de l'optimisation sur " . count($sanitizedLogs) . " données de trafic...\n";
76
+
77
+ // 4. Résolution du Front de Pareto
78
+ $paretoFront = Optimization::solveFullSecurityTuning(['trafficData' => $sanitizedLogs]);
79
+ if (empty($paretoFront)) {
80
+ fwrite(STDERR, "[AutoTuning] L'optimisation n'a retourné aucun résultat.\n");
81
+ exit(1);
82
+ }
83
+
84
+ // 5. Sélection de la solution la plus équilibrée
85
+ $bestSolution = $paretoFront[0];
86
+ $minDistance = sqrt(pow($bestSolution['objectives'][0], 2) + pow($bestSolution['objectives'][1], 2));
87
+ foreach ($paretoFront as $candidate) {
88
+ $distance = sqrt(pow($candidate['objectives'][0], 2) + pow($candidate['objectives'][1], 2));
89
+ if ($distance < $minDistance) {
90
+ $minDistance = $distance;
91
+ $bestSolution = $candidate;
92
+ }
93
+ }
94
+
95
+ $newConfig = $bestSolution['solution'];
96
+ $maxChangeVelocity = 0.15; // Inertie de 15% maximum par cycle
97
+
98
+ $applyInertialUpdate = function (array &$current, array $target) use ($maxChangeVelocity) {
99
+ $sumCurrent = array_sum($current);
100
+ if ($sumCurrent === 0) return;
101
+ $sumTarget = 0;
102
+ foreach ($current as $k => $v) {
103
+ if (isset($target[$k])) $sumTarget += $target[$k];
104
+ }
105
+ $ratio = ($sumTarget - $sumCurrent) / $sumCurrent;
106
+ $factor = 1 + max(-$maxChangeVelocity, min($maxChangeVelocity, $ratio));
107
+ foreach ($current as $k => &$v) {
108
+ if (isset($target[$k])) $v *= $factor;
109
+ }
110
+ };
111
+
112
+ $applyInertialUpdate($config['thresholds'], $newConfig['thresholds']);
113
+ $applyInertialUpdate($config['weights'], $newConfig['weights']);
114
+ $applyInertialUpdate($config['patterns'], $newConfig['patterns']);
115
+
116
+ // 6. Écrasement propre du fichier de configuration original
117
+ file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
118
+ echo "[AutoTuning] Succès : Fichier {$configPath} mis à jour avec les paramètres optimisés.\n";