@anonympins/fingerprint 0.6.5 → 0.6.7

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.
@@ -20,7 +20,7 @@ class FunctionRegistry
20
20
  if (empty(self::$functions)) {
21
21
  // Fonctions de "Scoring"
22
22
  self::$functions['tsp.calculateEnergy'] = [OptimizationUtils::class, 'evaluatePathDistance'];
23
- self::$functions['portfolio.calculateMetrics'] = [OptimizationOperators::class, 'createPortfolioAllocator'];
23
+ self::$functions['portfolio.calculateMetrics'] = [OptimizationOperators::class, 'calculatePortfolioMetrics'];
24
24
  self::$functions['facility.calculateEnergy'] = [OptimizationOperators::class, 'evaluateFacilityLocation'];
25
25
 
26
26
  // Fonctions de "Résolution"
@@ -48,6 +48,19 @@ class OptimizationOperators
48
48
  }
49
49
  }
50
50
 
51
+ /**
52
+ * Évalue un portefeuille pour un payload donné.
53
+ *
54
+ * @param array $weights
55
+ * @param array $payload
56
+ * @return float
57
+ */
58
+ public static function calculatePortfolioMetrics(array $weights, array $payload): float
59
+ {
60
+ $fitnessFunction = self::createPortfolioAllocator($payload);
61
+ return $fitnessFunction($weights);
62
+ }
63
+
51
64
  /**
52
65
  * Crée un évaluateur pour l'optimisation de portefeuille.
53
66
  * @param array $config
@@ -238,6 +238,28 @@ class ProblemManager
238
238
  return;
239
239
  }
240
240
  }
241
+
242
+ // VÉRIFICATION PAR ÉCHANTILLONNAGE (Parité avec JS)
243
+ $fitnessFunction = FunctionRegistry::get('portfolio.calculateMetrics');
244
+ if ($fitnessFunction) {
245
+ $sampleSize = min(5, count($solutionData['population']));
246
+ $sampleKeys = (array)array_rand($solutionData['population'], $sampleSize);
247
+
248
+ foreach ($sampleKeys as $key) {
249
+ $individual = $solutionData['population'][$key];
250
+ if (isset($individual['chromosome'])) {
251
+ $recalculated = $fitnessFunction($individual['chromosome'], $problem['payload'] ?? []);
252
+ if (isset($individual['fitness']) && $individual['fitness'] !== -1) {
253
+ if (abs($individual['fitness'] - $recalculated) > 1e-4) {
254
+ error_log("[ProblemManager] Triche détectée pour {$problemId}! Fitness déclaré: {$individual['fitness']}, recalculé: {$recalculated}");
255
+ return; // Rejeter en cas d'incohérence
256
+ }
257
+ }
258
+ $solutionData['population'][$key]['fitness'] = $recalculated;
259
+ }
260
+ }
261
+ }
262
+
241
263
  $problem['state']['population'] = $solutionData['population'];
242
264
  $problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
243
265
  $stateChanged = true;
@@ -281,6 +303,89 @@ class ProblemManager
281
303
  return false;
282
304
  }
283
305
 
306
+ /**
307
+ * S'assure qu'un problème a une solution initiale. Si non, en génère une.
308
+ *
309
+ * @param array &$problem Le problème à vérifier (passé par référence).
310
+ */
311
+ private function ensureInitialSolution(array &$problem): void
312
+ {
313
+ if (!empty($problem['state']['bestSolution'])) {
314
+ return;
315
+ }
316
+
317
+ $scoreFunction = $problem['workUnit']['scoreFunction'] ?? null;
318
+ $initialSolutionSourceKey = $problem['workUnit']['initialSolutionSource'] ?? '';
319
+ $initialSolution = $problem['payload'][$initialSolutionSourceKey] ?? null;
320
+
321
+ if ($scoreFunction && is_array($initialSolution)) {
322
+ $score = $scoreFunction($initialSolution, $problem['payload'] ?? []);
323
+ $problem['state']['bestSolution'] = $initialSolution;
324
+ $problem['state']['bestEnergy'] = $score;
325
+ $problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
326
+ $this->store->set("problem-state:{$problem['id']}", $problem['state']);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Formate l'état d'un problème pour l'export externe.
332
+ */
333
+ private function formatSolution(array $problem): ?array
334
+ {
335
+ if (!isset($problem['state'])) {
336
+ return null;
337
+ }
338
+
339
+ if (($problem['workUnit']['type'] ?? '') === 'multi_objective_genetic_algorithm') {
340
+ return [
341
+ 'id' => $problem['id'],
342
+ 'solution' => $problem['state']['paretoFront'] ?? [],
343
+ 'score' => count($problem['state']['paretoFront'] ?? []),
344
+ 'lastUpdate' => $problem['state']['lastUpdate'] ?? null,
345
+ ];
346
+ }
347
+
348
+ return [
349
+ 'id' => $problem['id'],
350
+ 'solution' => $problem['state']['bestSolution'] ?? null,
351
+ 'score' => $problem['state']['bestEnergy'] ?? null,
352
+ 'lastUpdate' => $problem['state']['lastUpdate'] ?? null,
353
+ ];
354
+ }
355
+
356
+ /**
357
+ * Récupère la meilleure solution actuellement connue pour un ou plusieurs problèmes.
358
+ *
359
+ * @param string|null $problemId L'ID optionnel du problème à consulter.
360
+ * @return array|null Un tableau associatif ou une liste de tableaux associatifs.
361
+ */
362
+ public function getBestSolutions(?string $problemId = null): ?array
363
+ {
364
+ $problemsToProcess = [];
365
+ foreach ($this->problems as &$p) {
366
+ if ($problemId === null || $p['id'] === $problemId) {
367
+ if (($p['workUnit']['type'] ?? '') !== 'multi_objective_genetic_algorithm') {
368
+ $this->ensureInitialSolution($p);
369
+ }
370
+ $problemsToProcess[] = $p;
371
+ }
372
+ }
373
+ unset($p);
374
+
375
+ if ($problemId !== null) {
376
+ return !empty($problemsToProcess) ? $this->formatSolution($problemsToProcess[0]) : null;
377
+ }
378
+
379
+ $results = [];
380
+ foreach ($this->problems as $p) {
381
+ $formatted = $this->formatSolution($p);
382
+ if ($formatted && $formatted['solution'] !== null) {
383
+ $results[] = $formatted;
384
+ }
385
+ }
386
+ return $results;
387
+ }
388
+
284
389
  /**
285
390
  * Réinitialise l'instance singleton.
286
391
  * @internal Uniquement pour les tests.
@@ -40,7 +40,8 @@ class ChallengeUtilsTest extends TestCase
40
40
  };
41
41
 
42
42
  for ($idx = 0; $idx < 64; $idx++) {
43
- $x = $fround($numericSeed + $idx * 0.015);
43
+ $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
44
+ $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
44
45
  $rFloat = $fround($r);
45
46
  for ($i = 0; $i < $iterations; $i++) {
46
47
  $x = $fround($rFloat * $x * $fround(1.0 - $x));
@@ -78,7 +79,7 @@ class ChallengeUtilsTest extends TestCase
78
79
  return unpack('f', pack('f', $value))[1];
79
80
  };
80
81
  for ($idx = 0; $idx < 64; $idx++) {
81
- $x = $fround($numericSeed + $idx * 0.015);
82
+ $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
82
83
  $rFloat = $fround($r);
83
84
  for ($i = 0; $i < $iterations; $i++) {
84
85
  $x = $fround($rFloat * $x * $fround(1.0 - $x));
@@ -155,7 +155,7 @@ class IpReputationTest extends TestCase
155
155
  $this->assertEquals(0.0, $score['subnetScore']);
156
156
 
157
157
  for ($i = 1; $i <= 12; $i++) {
158
- RequestUtils::updateSubnetMetrics($context, "device-{$i}", 30.0);
158
+ RequestUtils::updateSubnetMetrics($context, "device-{$i}", 50.0);
159
159
  }
160
160
 
161
161
  $scoreWithHistory = RequestUtils::getSubnetScore($context, 'device-1');
@@ -372,6 +372,6 @@ class ProblemManagerTest extends TestCase
372
372
  );
373
373
 
374
374
  $manager = ProblemManager::getInstance($this->configPath, $storeMock);
375
- $manager->integrateSolution($problemId, ['population' => [['chromosome' => [1.0], 'fitness' => -0.15]]]);
375
+ $manager->integrateSolution($problemId, ['population' => [['chromosome' => [1.0], 'fitness' => -1.0]]]);
376
376
  }
377
377
  }
@@ -10,15 +10,15 @@ use PHPUnit\Framework\TestCase;
10
10
 
11
11
  class QuicFingerprintTest extends TestCase
12
12
  {
13
- public function testGetQuicAnomalyScoreReturnsZeroIfNoFp(): void
13
+ public function testGetProtocolAnomalyScoreReturnsZeroIfNoFp(): void
14
14
  {
15
15
  $context = $this->createMock(RequestContext::class);
16
16
  $context->method('getHeader')->willReturn(null);
17
- $res = RequestUtils::getQuicAnomalyScore($context);
18
- $this->assertEquals(0.0, $res['quicAnomalyScore']);
17
+ $res = RequestUtils::getProtocolAnomalyScore($context);
18
+ $this->assertEquals(0.0, $res['protocolAnomalyScore']);
19
19
  }
20
20
 
21
- public function testGetQuicAnomalyScoreDetectsSpoofedChrome(): void
21
+ public function testGetProtocolAnomalyScoreDetectsSpoofedChrome(): void
22
22
  {
23
23
  $context = $this->createMock(RequestContext::class);
24
24
  $context->method('getHeader')->willReturnCallback(function($name) {
@@ -31,11 +31,11 @@ class QuicFingerprintTest extends TestCase
31
31
  return null;
32
32
  });
33
33
 
34
- $res = RequestUtils::getQuicAnomalyScore($context);
35
- $this->assertEquals(100.0, $res['quicAnomalyScore']);
34
+ $res = RequestUtils::getProtocolAnomalyScore($context);
35
+ $this->assertEquals(100.0, $res['protocolAnomalyScore']);
36
36
  }
37
37
 
38
- public function testGetQuicAnomalyScoreAllowsLegitChrome(): void
38
+ public function testGetProtocolAnomalyScoreAllowsLegitChrome(): void
39
39
  {
40
40
  $context = $this->createMock(RequestContext::class);
41
41
  $context->method('getHeader')->willReturnCallback(function($name) {
@@ -48,7 +48,7 @@ class QuicFingerprintTest extends TestCase
48
48
  return null;
49
49
  });
50
50
 
51
- $res = RequestUtils::getQuicAnomalyScore($context);
52
- $this->assertEquals(0.0, $res['quicAnomalyScore']);
51
+ $res = RequestUtils::getProtocolAnomalyScore($context);
52
+ $this->assertEquals(0.0, $res['protocolAnomalyScore']);
53
53
  }
54
54
  }
@@ -1268,13 +1268,15 @@ class RequestUtils
1268
1268
  if (!isset($subnetData['uas'])) {
1269
1269
  $subnetData['uas'] = [];
1270
1270
  }
1271
+ $now = time();
1272
+ self::decaySubnetData($subnetData, $now);
1271
1273
 
1272
1274
  // Utilisation de la partie stable du fingerprint matériel plutôt que l'ID de cookie volatil
1273
1275
  $currentDeviceHash = self::getCompositeDeviceHash($context);
1274
1276
  $stableFpId = FingerprintBuilder::cyrb53(self::extractStablePart($currentDeviceHash));
1275
1277
 
1276
1278
  $currentDeviceContributions = $subnetData['highScoreDevices'][$stableFpId] ?? 0;
1277
- if ($currentDeviceContributions < 5 && $finalScore < 95) {
1279
+ if ($currentDeviceContributions < 1 && $finalScore < 95) {
1278
1280
  $subnetData['highScoreDevices'][$stableFpId] = $currentDeviceContributions + 1;
1279
1281
  $subnetData['highScoreCount']++;
1280
1282
  }
@@ -1291,8 +1293,7 @@ class RequestUtils
1291
1293
  if (!empty($userAgent) && !in_array($userAgent, $subnetData['uas'], true)) {
1292
1294
  $subnetData['uas'][] = $userAgent;
1293
1295
  }
1294
-
1295
- $subnetData['lastActivity'] = time();
1296
+ $subnetData['lastActivity'] = $now;
1296
1297
 
1297
1298
  // Limiter la taille du tableau des deviceIds pour éviter une consommation mémoire excessive.
1298
1299
  if (count($subnetData['deviceIds']) > 100) {
@@ -1340,19 +1341,16 @@ class RequestUtils
1340
1341
  $inactivitySec = $now - ($subnetData['lastActivity'] ?? $now);
1341
1342
  $halfLives = (int)floor($inactivitySec / 1800);
1342
1343
 
1344
+ if ($halfLives > 0) {
1345
+ self::decaySubnetData($subnetData, $now);
1346
+ $store->set($key, $subnetData, 86400);
1347
+ }
1348
+
1343
1349
  $highScoreCount = $subnetData['highScoreCount'] ?? 0;
1344
1350
  $deviceCount = isset($subnetData['deviceIds']) ? count($subnetData['deviceIds']) : 0;
1345
1351
  $ipCount = isset($subnetData['ips']) ? count($subnetData['ips']) : 1;
1346
1352
  $uaCount = isset($subnetData['uas']) ? count($subnetData['uas']) : 1;
1347
1353
 
1348
- if ($halfLives > 0) {
1349
- $decay = pow(2, $halfLives);
1350
- $highScoreCount = max(0, (int)floor($highScoreCount / $decay));
1351
- $deviceCount = max(0, (int)floor($deviceCount / $decay));
1352
- $ipCount = max(1, (int)floor($ipCount / $decay));
1353
- $uaCount = max(1, (int)floor($uaCount / $decay));
1354
- }
1355
-
1356
1354
  if ($deviceCount === 0) {
1357
1355
  return ['subnetScore' => 0.0];
1358
1356
  }
@@ -1360,15 +1358,59 @@ class RequestUtils
1360
1358
  $suspicionDensity = $highScoreCount / $deviceCount;
1361
1359
  $ipDeviceRatio = $ipCount / $deviceCount;
1362
1360
 
1361
+ // Ratio User-Agent / Device : détecte la rotation/spoofing de navigateurs sur une même empreinte matérielle
1362
+ $uaDeviceRatio = (double)max(1, $uaCount) / $deviceCount;
1363
+ $uaMultiplier = 0.6 + (0.4 * min(2.5, $uaDeviceRatio));
1364
+
1363
1365
  $baseScore = 100.0 * (1.0 - exp(-0.15 * $highScoreCount));
1364
1366
 
1365
1367
  $densityMultiplier = 0.4 + (1.6 * $suspicionDensity);
1366
1368
  $distributionMultiplier = 0.5 + (1.0 * $ipDeviceRatio);
1367
1369
 
1368
- $finalScore = min(100.0, round($baseScore * $densityMultiplier * $distributionMultiplier * 10.0) / 10.0);
1370
+ $dampening = 1.0;
1371
+ if( $highScoreCount < 3 ){
1372
+ $dampening = $highScoreCount / 3.0;
1373
+ }
1374
+ $finalScore = min(100.0, round($baseScore * $densityMultiplier * $distributionMultiplier * $uaMultiplier * $dampening * 10.0) / 10.0);
1369
1375
 
1370
1376
  return ['subnetScore' => $finalScore];
1371
1377
  }
1378
+ /**
1379
+ * Applies temporal decay (half-life of 30 minutes) to subnet metrics.
1380
+ *
1381
+ * @param array &$subnetData Subnet data passed by reference.
1382
+ * @param int $now Current timestamp.
1383
+ */
1384
+ private static function decaySubnetData(array &$subnetData, int $now): void
1385
+ {
1386
+ $inactivitySec = $now - ($subnetData['lastActivity'] ?? $now);
1387
+ $halfLives = (int)floor($inactivitySec / 1800);
1388
+
1389
+ if ($halfLives > 0) {
1390
+ $decay = pow(2, $halfLives);
1391
+ $subnetData['highScoreCount'] = max(0, (int)floor(($subnetData['highScoreCount'] ?? 0) / $decay));
1392
+
1393
+ if (isset($subnetData['highScoreDevices'])) {
1394
+ foreach ($subnetData['highScoreDevices'] as $fpId => $val) {
1395
+ $decayedVal = (int)floor($val / $decay);
1396
+ if ($decayedVal <= 0) {
1397
+ unset($subnetData['highScoreDevices'][$fpId]);
1398
+ } else {
1399
+ $subnetData['highScoreDevices'][$fpId] = $decayedVal;
1400
+ }
1401
+ }
1402
+ }
1403
+
1404
+ foreach (['deviceIds', 'ips', 'uas'] as $field) {
1405
+ if (isset($subnetData[$field])) {
1406
+ $newLen = max(0, (int)floor(count($subnetData[$field]) / $decay));
1407
+ $subnetData[$field] = array_slice($subnetData[$field], 0, $newLen);
1408
+ }
1409
+ }
1410
+
1411
+ $subnetData['lastActivity'] = $now - ($inactivitySec % 1800);
1412
+ }
1413
+ }
1372
1414
 
1373
1415
  /**
1374
1416
  * Calcule le score d'anomalie de similarité réseau (Botnet Clustering).
@@ -1820,52 +1862,75 @@ class RequestUtils
1820
1862
  }
1821
1863
 
1822
1864
  /**
1823
- * Détecte les anomalies de flux QUIC/HTTP3 par rapport au User-Agent.
1865
+ * Détecte les anomalies de protocole (HTTP/2 et QUIC/HTTP3) par rapport au User-Agent.
1824
1866
  * @param RequestContext $context
1825
- * @return array{'quicAnomalyScore': float}
1867
+ * @return array{'protocolAnomalyScore': float}
1826
1868
  */
1827
- public static function getQuicAnomalyScore(RequestContext $context): array
1869
+ public static function getProtocolAnomalyScore(RequestContext $context): array
1828
1870
  {
1829
- $quicFp = $context->getHeader('x-quic-fp') ?? $context->quicFingerprint ?? null;
1830
- if (empty($quicFp) || !is_string($quicFp)) {
1831
- return ['quicAnomalyScore' => 0.0];
1832
- }
1833
-
1834
- $parts = explode(';', $quicFp);
1835
- if (count($parts) < 2) {
1836
- return ['quicAnomalyScore' => 0.0];
1837
- }
1838
-
1839
- $params = [];
1840
- foreach (explode(',', $parts[1]) as $p) {
1841
- $kv = explode('=', $p, 2);
1842
- if (count($kv) === 2) {
1843
- $params[$kv[0]] = $kv[1];
1844
- }
1845
- }
1846
- $priorityOrder = $parts[2] ?? '';
1871
+ $http2Anomaly = 0.0;
1872
+ $quicAnomaly = 0.0;
1847
1873
 
1848
1874
  $ua = $context->getHeader('user-agent') ?? '';
1849
1875
  $uaParts = self::parseUserAgent($ua);
1850
1876
  $browser = $uaParts['browser'] ?? null;
1851
1877
 
1852
- if (empty($browser)) {
1853
- return ['quicAnomalyScore' => 0.0];
1878
+ // HTTP/2 Anomaly logic
1879
+ $h2Fp = $context->getHeader('x-http2-fingerprint') ?? $context->http2Fingerprint ?? null;
1880
+ if ($h2Fp && is_string($h2Fp) && $browser) {
1881
+ $parts = explode('|', $h2Fp);
1882
+ if (count($parts) >= 4) {
1883
+ $connWindow = (int)$parts[1];
1884
+ $headerOrder = $parts[3];
1885
+ $isChromium = str_starts_with($browser, 'Chrome') || str_starts_with($browser, 'Edge');
1886
+ $isFirefox = str_starts_with($browser, 'Firefox');
1887
+ $isSafari = str_starts_with($browser, 'Safari');
1888
+
1889
+ if ($isChromium) {
1890
+ if ($headerOrder && $headerOrder !== 'm,a,s,p') $http2Anomaly += 60.0;
1891
+ if ($connWindow === 65535 || $connWindow === 65536) $http2Anomaly += 40.0;
1892
+ } elseif ($isFirefox) {
1893
+ if ($headerOrder && $headerOrder !== 'm,s,p,a') $http2Anomaly += 60.0;
1894
+ } elseif ($isSafari) {
1895
+ if ($headerOrder && $headerOrder !== 'm,s,p,a') $http2Anomaly += 60.0;
1896
+ }
1897
+ }
1854
1898
  }
1855
1899
 
1856
- $anomaly = 0.0;
1857
- if (str_starts_with($browser, 'Chrome') || str_starts_with($browser, 'Edge')) {
1858
- $maxData = isset($params['1']) ? (int)$params['1'] : 0;
1859
- $maxStreams = isset($params['4']) ? (int)$params['4'] : 0;
1860
- if ($maxData > 0 && $maxData < 1048576) $anomaly += 40.0;
1861
- if ($maxStreams > 0 && $maxStreams !== 100) $anomaly += 30.0;
1862
- if (!empty($priorityOrder) && !str_contains($priorityOrder, 'u=')) $anomaly += 30.0;
1863
- } elseif (str_starts_with($browser, 'Firefox')) {
1864
- $maxData = isset($params['1']) ? (int)$params['1'] : 0;
1865
- if ($maxData > 0 && $maxData > 5000000) $anomaly += 40.0;
1900
+ // QUIC Anomaly logic
1901
+ $quicFp = $context->getHeader('x-quic-fp') ?? $context->quicFingerprint ?? null;
1902
+ if ($quicFp && is_string($quicFp) && $browser) {
1903
+ $parts = explode(';', $quicFp);
1904
+ if (count($parts) >= 2) {
1905
+ $params = [];
1906
+ foreach (explode(',', $parts[1]) as $p) {
1907
+ $kv = explode('=', $p, 2);
1908
+ if (count($kv) === 2) {
1909
+ $params[$kv[0]] = $kv[1];
1910
+ }
1911
+ }
1912
+ $priorityOrder = $parts[2] ?? '';
1913
+
1914
+ if (str_starts_with($browser, 'Chrome') || str_starts_with($browser, 'Edge')) {
1915
+ $maxData = isset($params['1']) ? (int)$params['1'] : 0;
1916
+ $maxStreams = isset($params['4']) ? (int)$params['4'] : 0;
1917
+ if ($maxData > 0 && $maxData < 1048576) $quicAnomaly += 40.0;
1918
+ if ($maxStreams > 0 && $maxStreams !== 100) $quicAnomaly += 30.0;
1919
+ if (!empty($priorityOrder) && !str_contains($priorityOrder, 'u=')) $quicAnomaly += 30.0;
1920
+ } elseif (str_starts_with($browser, 'Firefox')) {
1921
+ $maxData = isset($params['1']) ? (int)$params['1'] : 0;
1922
+ if ($maxData > 0 && $maxData > 5000000) $quicAnomaly += 40.0;
1923
+ }
1924
+ }
1866
1925
  }
1867
1926
 
1868
- return ['quicAnomalyScore' => max(0.0, min(100.0, $anomaly))];
1927
+ return [
1928
+ 'protocolAnomalyScore' => max(
1929
+ 0.0,
1930
+ min(100.0, $http2Anomaly),
1931
+ min(100.0, $quicAnomaly)
1932
+ )
1933
+ ];
1869
1934
  }
1870
1935
 
1871
1936
  /**