@anonympins/fingerprint 0.6.3 → 0.6.4

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.
@@ -237,6 +237,9 @@
237
237
  $this->logger = isset($securityConfig['logger']) && is_callable($securityConfig['logger']) ? new Logger($securityConfig['logger']) : null;
238
238
  $this->dryRun = $securityConfig['dryRun'] ?? false;
239
239
  $this->validateConfig($securityConfig);
240
+ if ($this->securityConfig['reset'] ?? false) {
241
+ $this->resetStore();
242
+ }
240
243
  }
241
244
 
242
245
  /**
@@ -252,6 +255,18 @@
252
255
  $this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
253
256
  }
254
257
 
258
+ /**
259
+ * Réinitialise le store de persistance actif.
260
+ */
261
+ public function resetStore(): void
262
+ {
263
+ $store = StoreManager::getStore();
264
+ if (method_exists($store, 'clear')) {
265
+ $store->clear();
266
+ }
267
+ $this->log('Store has been reset/cleared.');
268
+ }
269
+
255
270
  private function validateConfig(array $config): void
256
271
  {
257
272
  if (empty($config)) {
@@ -266,7 +281,7 @@
266
281
  'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
267
282
  'similarityThreshold', 'summary', 'description',
268
283
  'ed25519_private_key', 'ed25519_public_key',
269
- 'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret'
284
+ 'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset'
270
285
  ];
271
286
 
272
287
  if (empty($config['weights'])) {
@@ -497,28 +512,50 @@
497
512
  */
498
513
  private function checkAllowlists(RequestContext $context): bool
499
514
  {
515
+ $whitelisted = false;
516
+ $type = '';
517
+
500
518
  if ($this->isIpInAllowlist($context->clientIp)) {
501
- $this->log('IP in allowlist - allowing request', ['clientIp' => $context->clientIp]);
502
- return true;
503
- }
504
- if ($this->isPathInAllowlist($context->path)) {
505
- $this->log('Path in allowlist - allowing request', ['path' => $context->path]);
506
- return true;
507
- }
508
- if ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
509
- $this->log('Host and path in allowlist - allowing request', ['host' => $context->getHeader('host'), 'path' => $context->path]);
510
- return true;
511
- }
512
- // NOUVEAU: Vérifier la liste blanche GraphQL
513
- if ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
514
- $this->log('GraphQL operation in allowlist - allowing request', ['operation' => "{$context->graphqlOperation['type']}:{$context->graphqlOperation['name']}"]);
515
- return true;
516
- }
517
- if ($this->verifyWhitelistedBot($context)) {
518
- $this->log('Whitelisted bot verified - allowing request', ['clientIp' => $context->clientIp]);
519
+ $whitelisted = true;
520
+ $type = 'allowlist';
521
+ } elseif ($this->isPathInAllowlist($context->path)) {
522
+ $whitelisted = true;
523
+ $type = 'path_allowlist';
524
+ } elseif ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
525
+ $whitelisted = true;
526
+ $type = 'host_path_allowlist';
527
+ } elseif ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
528
+ $whitelisted = true;
529
+ $type = 'graphql_operation_allowlist';
530
+ } elseif ($this->verifyWhitelistedBot($context)) {
531
+ $whitelisted = true;
532
+ $type = 'bot';
533
+ }
534
+
535
+ if ($whitelisted) {
536
+ $filterWhitelist = $this->securityConfig['filterWhitelist'] ?? false;
537
+ $bypassWhitelist = false;
538
+ if ($filterWhitelist === true) {
539
+ $bypassWhitelist = $this->hasCertainAttack($context);
540
+ } elseif (is_numeric($filterWhitelist)) {
541
+ if ($context->preCalculatedScore === null) {
542
+ $suspicionVector = [];
543
+ $context->preCalculatedVector = $this->getSuspicionVector($context, $suspicionVector);
544
+ $context->preCalculatedScore = $this->calculateFinalScore($context->preCalculatedVector);
545
+ }
546
+ if ($context->preCalculatedScore > $filterWhitelist) {
547
+ $bypassWhitelist = true;
548
+ }
549
+ }
550
+
551
+ if ($bypassWhitelist) {
552
+ $this->log('Whitelisted request exceeds filter threshold - bypassing whitelist bypass', ['clientIp' => $context->clientIp, 'path' => $context->path]);
553
+ return false;
554
+ }
555
+ $this->log("IP/Path in allowlist ({$type}) - allowing request", ['clientIp' => $context->clientIp, 'path' => $context->path]);
519
556
  return true;
520
557
  }
521
-
558
+
522
559
  return false;
523
560
  }
524
561
 
@@ -908,6 +945,23 @@
908
945
  }
909
946
  }
910
947
 
948
+ /**
949
+ * Évalue si la requête présente des caractéristiques d'attaque flagrantes ou a déclenché un honeypot.
950
+ */
951
+ private function hasCertainAttack(RequestContext $context): bool
952
+ {
953
+ $honeypotConfig = $this->securityConfig['honeypot'] ?? [];
954
+ $honeypot = RequestUtils::getHoneypotScore($context, $honeypotConfig);
955
+ if (($honeypot['honeypotScore'] ?? 0.0) >= 100.0) {
956
+ return true;
957
+ }
958
+ $bot = RequestUtils::getBotScore($context);
959
+ if (($bot['botScore'] ?? 0.0) >= 100.0) {
960
+ return true;
961
+ }
962
+ return false;
963
+ }
964
+
911
965
  /**
912
966
  * Traite une requête entrante et retourne une décision.
913
967
  * @param RequestContext $context Le contexte de la requête.
@@ -1032,6 +1086,28 @@
1032
1086
  $problemManager = \Anonympins\Fingerprint\ProblemManager::getInstance($configPath, $store);
1033
1087
  // FIX: La solution est directement le $workResult, pas une sous-propriété.
1034
1088
  $problemManager->integrateSolution($problemId, $workResult);
1089
+
1090
+ // Si le problème résolu est l'auto-tuning de sécurité et que l'auto-tuning est activé,
1091
+ // on applique directement la meilleure solution calculée au moteur en direct.
1092
+ if ($problemId === 'security_auto_tuning' && ($this->securityConfig['autotuning']['enabled'] ?? false)) {
1093
+ $paretoFront = $workResult['paretoFront'] ?? null;
1094
+ if (is_array($paretoFront) && !empty($paretoFront)) {
1095
+ $bestSolution = $paretoFront[0];
1096
+ $minDistance = sqrt(pow((float)$bestSolution['objectives'][0], 2) + pow((float)$bestSolution['objectives'][1], 2));
1097
+ for ($i = 1; $i < count($paretoFront); $i++) {
1098
+ $distance = sqrt(pow((float)$paretoFront[$i]['objectives'][0], 2) + pow((float)$paretoFront[$i]['objectives'][1], 2));
1099
+ if ($distance < $minDistance) {
1100
+ $minDistance = $distance;
1101
+ $bestSolution = $paretoFront[$i];
1102
+ }
1103
+ }
1104
+ if (isset($bestSolution['solution'])) {
1105
+ $this->updateConfig($bestSolution['solution']);
1106
+ $this->log('Useful Work auto-tuning applied successfully to live config.');
1107
+ }
1108
+ }
1109
+ }
1110
+
1035
1111
  $isValid = true;
1036
1112
  // FIX: Générer un vrai ticket pour uPoW, comme pour un PoW normal.
1037
1113
  $ticketTtl = $this->securityConfig['ticketMaxAge'] ?? 3600000; // 1 heure par défaut
@@ -1113,8 +1189,8 @@
1113
1189
  return $decision;
1114
1190
  }
1115
1191
 
1116
- $suspicionVector = $this->getSuspicionVector($context, $suspicionVector);
1117
- $finalScore = $this->calculateFinalScore($suspicionVector);
1192
+ $suspicionVector = $context->preCalculatedVector ?? $this->getSuspicionVector($context, $suspicionVector);
1193
+ $finalScore = $context->preCalculatedScore ?? $this->calculateFinalScore($suspicionVector);
1118
1194
  $this->log('Suspicion vector and final score calculated', [
1119
1195
  'finalScore' => round($finalScore, 2),
1120
1196
  'vector' => $suspicionVector
@@ -1354,7 +1430,8 @@
1354
1430
  $memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
1355
1431
 
1356
1432
  $originalFingerprint = RequestUtils::getCompositeDeviceHash($context);
1357
- $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint);
1433
+ $tlsSessionId = $context->tlsSessionId ?? '';
1434
+ $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint, $context->clientIp, $tlsSessionId);
1358
1435
 
1359
1436
  $challengeContext = [
1360
1437
  'clientSecret' => $clientSecret,
@@ -70,18 +70,22 @@ class OptimizationOperators
70
70
  */
71
71
  public static function createFullSecurityConfigEvaluator(array $context): callable
72
72
  {
73
- $trafficData = $context['trafficData'];
73
+ $trafficData = $context['trafficData'] ?? [];
74
+ $currentConfig = $context['currentConfig'] ?? null;
74
75
 
75
- return function (array $config) use ($trafficData): array {
76
- $falsePositives = 0;
77
- $falseNegatives = 0;
78
- $totalHumans = 0;
79
- $totalBots = 0;
76
+ return function (array $config) use ($trafficData, $currentConfig): array {
77
+ $falsePositives = 0.0;
78
+ $falseNegatives = 0.0;
79
+ $totalHumans = 0.0;
80
+ $totalBots = 0.0;
81
+
82
+ $maxHumanScore = 0.0;
83
+ $minBotScore = 100.0;
80
84
 
81
85
  $calculateScore = function (array $log) use ($config): float {
82
86
  $score = 0.0;
83
87
  foreach ($config['weights'] as $key => $weight) {
84
- $score += ($log['vector'][$key] ?? 0) * $weight;
88
+ $score += ($log['vector'][$key] ?? 0.0) * $weight;
85
89
  }
86
90
  return $score;
87
91
  };
@@ -94,30 +98,50 @@ class OptimizationOperators
94
98
  'trap_triggered' => 2.0,
95
99
  ];
96
100
 
101
+ // 1. Évaluation sur les logs de trafic réels
97
102
  foreach ($trafficData as $log) {
98
- $confidence = $confidenceWeights[$log['type']] ?? 1.0;
99
- $isLikelyBot = in_array($log['type'], ['challenge_issued', 'request_blocked', 'trap_triggered']);
100
- $isLikelyHuman = in_array($log['type'], ['request_passed', 'challenge_solved']);
103
+ $weight = (float)($log['weight'] ?? 1.0);
104
+ $confidence = ($confidenceWeights[$log['type'] ?? ''] ?? 1.0) * $weight;
105
+ $isLikelyBot = in_array($log['type'] ?? '', ['challenge_issued', 'request_blocked', 'trap_triggered']);
106
+ $isLikelyHuman = in_array($log['type'] ?? '', ['request_passed', 'challenge_solved']);
107
+
108
+ $score = $calculateScore($log);
101
109
 
102
110
  if ($isLikelyBot) {
103
111
  $totalBots += $confidence;
104
- $score = $calculateScore($log);
112
+ $minBotScore = min($minBotScore, $score);
105
113
  if ($score < $config['thresholds']['low']) {
106
114
  $falseNegatives += $confidence;
107
115
  }
108
116
  } elseif ($isLikelyHuman) {
109
117
  $totalHumans += $confidence;
110
- $score = $calculateScore($log);
118
+ $maxHumanScore = max($maxHumanScore, $score);
111
119
  if ($score >= $config['thresholds']['low']) {
112
120
  $falsePositives += $confidence;
113
121
  }
114
122
  }
115
123
  }
116
124
 
117
- $falsePositiveRate = $totalHumans > 0 ? $falsePositives / $totalHumans : 0;
118
- $falseNegativeRate = $totalBots > 0 ? $falseNegatives / $totalBots : 0;
125
+ $falsePositiveRate = $totalHumans > 0.0 ? $falsePositives / $totalHumans : 0.0;
126
+ $falseNegativeRate = $totalBots > 0.0 ? $falseNegatives / $totalBots : 0.0;
127
+
128
+ // 3. Pénalité de dérive d'échelle (L2 Regularization par rapport au profil d'origine)
129
+ $regularizationPenalty = 0.0;
130
+ if ($currentConfig && isset($currentConfig['weights'])) {
131
+ foreach ($config['weights'] as $key => $val) {
132
+ $originalVal = (float)($currentConfig['weights'][$key] ?? 0.0);
133
+ $regularizationPenalty += pow((float)$val - $originalVal, 2);
134
+ }
135
+ }
136
+
137
+ // 4. Maximisation de la marge
138
+ $marginOverlap = max(0.0, $maxHumanScore - $minBotScore);
139
+ $marginPenalty = $marginOverlap / 100.0;
140
+
141
+ $obj1 = $falsePositiveRate + ($regularizationPenalty * 0.05);
142
+ $obj2 = $falseNegativeRate + $marginPenalty;
119
143
 
120
- return [$falsePositiveRate, $falseNegativeRate];
144
+ return [$obj1, $obj2];
121
145
  };
122
146
  }
123
147
 
@@ -35,6 +35,10 @@ class RequestContext
35
35
  /** @var ?array<string, mixed> */
36
36
  public ?array $resolvedIdentity = null;
37
37
 
38
+ public ?float $preCalculatedScore = null;
39
+ /** @var ?array<string, float> */
40
+ public ?array $preCalculatedVector = null;
41
+
38
42
  // Propriétés spécifiques qui peuvent être fournies par un proxy inverse
39
43
  public ?string $ja3 = null;
40
44
  public ?string $ja4 = null;
@@ -39,4 +39,10 @@ interface IStore
39
39
  * @return void
40
40
  */
41
41
  public function delete(string $key): void;
42
+
43
+ /**
44
+ * Supprime toutes les clés du stockage.
45
+ * @return void
46
+ */
47
+ public function clear(): void;
42
48
  }
@@ -182,4 +182,16 @@ class MongoDbStore implements IStore
182
182
  $this->lastReconnectAttempt = microtime(true);
183
183
  }
184
184
  }
185
+
186
+ public function clear(): void
187
+ {
188
+ $this->fallbackStore->clear();
189
+ $this->checkConnection();
190
+ if ($this->isDown) return;
191
+ try {
192
+ $this->collection->deleteMany([]);
193
+ } catch (\Throwable $e) {
194
+ $this->isDown = true;
195
+ }
196
+ }
185
197
  }
@@ -131,4 +131,16 @@ class RedisStore implements IStore
131
131
  $this->lastReconnectAttempt = microtime(true);
132
132
  }
133
133
  }
134
+
135
+ public function clear(): void
136
+ {
137
+ $this->fallbackStore->clear();
138
+ $this->checkConnection();
139
+ if ($this->isDown) return;
140
+ try {
141
+ $this->redis->flushdb();
142
+ } catch (\Throwable $e) {
143
+ $this->isDown = true;
144
+ }
145
+ }
134
146
  }
@@ -228,6 +228,52 @@ class FingerprintEngineTest extends TestCase
228
228
  $this->assertContains($decision['action'], ['challenge', 'block']);
229
229
  }
230
230
 
231
+ public function testAllowsWhitelistedIpWithNumericFilterBelowThreshold(): void
232
+ {
233
+ $config = SecurityProfiles::createSecurityProfile('strict', [
234
+ 'whitelist' => [['type' => 'allowlist', 'entries' => ['10.0.0.1']]],
235
+ 'filterWhitelist' => 50.0,
236
+ 'weights' => ['headerAnomalyScore' => 1.0],
237
+ 'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95]
238
+ ]);
239
+ $engine = new FingerprintEngine($config);
240
+
241
+ $context = $this->createRequestContext([
242
+ 'clientIp' => '10.0.0.1',
243
+ 'headers' => [
244
+ 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
245
+ 'accept-language' => 'en-US,en;q=0.9'
246
+ ]
247
+ ]);
248
+ $decision = $engine->processRequest($context);
249
+
250
+ $this->assertEquals('next', $decision['action']);
251
+ $this->assertEquals(0, $decision['score']);
252
+ }
253
+
254
+ public function testAllowsWhitelistedIpWithNumericFilterExceedingThreshold(): void
255
+ {
256
+ $config = SecurityProfiles::createSecurityProfile('strict', [
257
+ 'whitelist' => [['type' => 'allowlist', 'entries' => ['10.0.0.1']]],
258
+ 'filterWhitelist' => 20.0,
259
+ 'weights' => ['headerAnomalyScore' => 1.0],
260
+ 'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95]
261
+ ]);
262
+ $engine = new FingerprintEngine($config);
263
+
264
+ $context = $this->createRequestContext([
265
+ 'clientIp' => '10.0.0.1',
266
+ 'headers' => [
267
+ 'user-agent' => 'Chrome',
268
+ 'accept-language' => 'en-US,en;q=0.9'
269
+ ]
270
+ ]);
271
+ $decision = $engine->processRequest($context);
272
+
273
+ $this->assertEquals('challenge', $decision['action']);
274
+ $this->assertEquals(60, $decision['score']);
275
+ }
276
+
231
277
  public function testAllowsWhitelistedIp(): void
232
278
  {
233
279
  $config = SecurityProfiles::createSecurityProfile('strict', [
@@ -25,6 +25,7 @@ class IpReputationTest extends TestCase
25
25
  public function set(string $key, $value, ?int $ttl = null): void { $this->data[$key] = $value; }
26
26
  public function has(string $key): bool { return isset($this->data[$key]); }
27
27
  public function delete(string $key): void { unset($this->data[$key]); }
28
+ public function clear(): void { $this->data = []; }
28
29
  };
29
30
 
30
31
  StoreManager::setStore($this->store);
@@ -1482,20 +1482,20 @@ class RequestUtils
1482
1482
  return [];
1483
1483
  }
1484
1484
 
1485
- $suspiciousLogs = [];
1486
- $passedLogs = [];
1485
+ $rawLogs = $trafficData;
1486
+ $totalCount = count($rawLogs);
1487
+
1488
+ $maxLogsPerDevice = max(3, (int)floor($totalCount * 0.02));
1489
+ $maxLogsPerIp = max(3, (int)floor($totalCount * 0.02));
1490
+ $maxLogsPerSubnet = max(5, (int)floor($totalCount * 0.05));
1491
+ $maxLogsPerHardwareCluster = max(3, (int)floor($totalCount * 0.02));
1492
+
1487
1493
  $deviceCounts = [];
1488
1494
  $ipCounts = [];
1489
1495
  $subnetCounts = [];
1490
1496
  $hardwareClusterCounts = [];
1491
1497
  $hwClusterCache = [];
1492
1498
 
1493
- $totalCount = count($trafficData);
1494
- $maxLogsPerDevice = max(3, (int)floor($totalCount * 0.02)); // Max 2% contribution per device
1495
- $maxLogsPerIp = max(3, (int)floor($totalCount * 0.02)); // Max 2% par adresse IP individuelle
1496
- $maxLogsPerSubnet = max(5, (int)floor($totalCount * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
1497
- $maxLogsPerHardwareCluster = max(3, (int)floor($totalCount * 0.02)); // Max 2% par cluster matériel stable
1498
-
1499
1499
  $getHardwareCluster = function (array $log) use (&$hwClusterCache): string {
1500
1500
  $fp = $log['deviceHash'] ?? $log['fingerprint'] ?? $log['deviceFingerprint'] ?? '';
1501
1501
  if (!empty($fp) && is_string($fp)) {
@@ -1522,7 +1522,40 @@ class RequestUtils
1522
1522
  return $log['deviceId'] ?? 'anonymous-cluster';
1523
1523
  };
1524
1524
 
1525
- foreach ($trafficData as $log) {
1525
+ // --- REVOLUTION : Compression de Cohorte par Densité Vectorielle (Anti-Sybil) ---
1526
+ $getVectorDistance = function (array $v1, array $v2): float {
1527
+ $sum = 0.0;
1528
+ $keys = array_unique(array_merge(array_keys($v1), array_keys($v2)));
1529
+ foreach ($keys as $key) {
1530
+ $sum += pow((float)($v1[$key] ?? 0.0) - (float)($v2[$key] ?? 0.0), 2);
1531
+ }
1532
+ return sqrt($sum);
1533
+ };
1534
+
1535
+ $clusteredLogs = [];
1536
+ foreach ($rawLogs as $log) {
1537
+ $matchedClusterKey = null;
1538
+ foreach ($clusteredLogs as $key => $cluster) {
1539
+ if (($log['type'] ?? '') === ($cluster['type'] ?? '') && $getVectorDistance($log['vector'] ?? [], $cluster['vector'] ?? []) < 5.0) {
1540
+ $matchedClusterKey = $key;
1541
+ break;
1542
+ }
1543
+ }
1544
+ if ($matchedClusterKey !== null) {
1545
+ $clusteredLogs[$matchedClusterKey]['instancesCount'] = ($clusteredLogs[$matchedClusterKey]['instancesCount'] ?? 1) + 1;
1546
+ $clusteredLogs[$matchedClusterKey]['weight'] = 1.0 + log((float)$clusteredLogs[$matchedClusterKey]['instancesCount']);
1547
+ } else {
1548
+ $logCopy = $log;
1549
+ $logCopy['instancesCount'] = 1;
1550
+ $logCopy['weight'] = 1.0;
1551
+ $clusteredLogs[] = $logCopy;
1552
+ }
1553
+ }
1554
+
1555
+ $suspiciousLogs = [];
1556
+ $passedLogs = [];
1557
+
1558
+ foreach ($clusteredLogs as $log) {
1526
1559
  $devId = $log['deviceId'] ?? 'anonymous';
1527
1560
  $ip = $log['clientIp'] ?? $log['ip'] ?? 'unknown';
1528
1561
  $subnet = self::getIpSubnet($ip) ?? 'unknown-subnet';