@anonympins/fingerprint 0.6.5 → 0.6.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.
@@ -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
  }
@@ -1369,6 +1367,42 @@ class RequestUtils
1369
1367
 
1370
1368
  return ['subnetScore' => $finalScore];
1371
1369
  }
1370
+ /**
1371
+ * Applies temporal decay (half-life of 30 minutes) to subnet metrics.
1372
+ *
1373
+ * @param array &$subnetData Subnet data passed by reference.
1374
+ * @param int $now Current timestamp.
1375
+ */
1376
+ private static function decaySubnetData(array &$subnetData, int $now): void
1377
+ {
1378
+ $inactivitySec = $now - ($subnetData['lastActivity'] ?? $now);
1379
+ $halfLives = (int)floor($inactivitySec / 1800);
1380
+
1381
+ if ($halfLives > 0) {
1382
+ $decay = pow(2, $halfLives);
1383
+ $subnetData['highScoreCount'] = max(0, (int)floor(($subnetData['highScoreCount'] ?? 0) / $decay));
1384
+
1385
+ if (isset($subnetData['highScoreDevices'])) {
1386
+ foreach ($subnetData['highScoreDevices'] as $fpId => $val) {
1387
+ $decayedVal = (int)floor($val / $decay);
1388
+ if ($decayedVal <= 0) {
1389
+ unset($subnetData['highScoreDevices'][$fpId]);
1390
+ } else {
1391
+ $subnetData['highScoreDevices'][$fpId] = $decayedVal;
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ foreach (['deviceIds', 'ips', 'uas'] as $field) {
1397
+ if (isset($subnetData[$field])) {
1398
+ $newLen = max(0, (int)floor(count($subnetData[$field]) / $decay));
1399
+ $subnetData[$field] = array_slice($subnetData[$field], 0, $newLen);
1400
+ }
1401
+ }
1402
+
1403
+ $subnetData['lastActivity'] = $now - ($inactivitySec % 1800);
1404
+ }
1405
+ }
1372
1406
 
1373
1407
  /**
1374
1408
  * Calcule le score d'anomalie de similarité réseau (Botnet Clustering).
@@ -1820,52 +1854,75 @@ class RequestUtils
1820
1854
  }
1821
1855
 
1822
1856
  /**
1823
- * Détecte les anomalies de flux QUIC/HTTP3 par rapport au User-Agent.
1857
+ * Détecte les anomalies de protocole (HTTP/2 et QUIC/HTTP3) par rapport au User-Agent.
1824
1858
  * @param RequestContext $context
1825
- * @return array{'quicAnomalyScore': float}
1859
+ * @return array{'protocolAnomalyScore': float}
1826
1860
  */
1827
- public static function getQuicAnomalyScore(RequestContext $context): array
1861
+ public static function getProtocolAnomalyScore(RequestContext $context): array
1828
1862
  {
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] ?? '';
1863
+ $http2Anomaly = 0.0;
1864
+ $quicAnomaly = 0.0;
1847
1865
 
1848
1866
  $ua = $context->getHeader('user-agent') ?? '';
1849
1867
  $uaParts = self::parseUserAgent($ua);
1850
1868
  $browser = $uaParts['browser'] ?? null;
1851
1869
 
1852
- if (empty($browser)) {
1853
- return ['quicAnomalyScore' => 0.0];
1870
+ // HTTP/2 Anomaly logic
1871
+ $h2Fp = $context->getHeader('x-http2-fingerprint') ?? $context->http2Fingerprint ?? null;
1872
+ if ($h2Fp && is_string($h2Fp) && $browser) {
1873
+ $parts = explode('|', $h2Fp);
1874
+ if (count($parts) >= 4) {
1875
+ $connWindow = (int)$parts[1];
1876
+ $headerOrder = $parts[3];
1877
+ $isChromium = str_starts_with($browser, 'Chrome') || str_starts_with($browser, 'Edge');
1878
+ $isFirefox = str_starts_with($browser, 'Firefox');
1879
+ $isSafari = str_starts_with($browser, 'Safari');
1880
+
1881
+ if ($isChromium) {
1882
+ if ($headerOrder && $headerOrder !== 'm,a,s,p') $http2Anomaly += 60.0;
1883
+ if ($connWindow === 65535 || $connWindow === 65536) $http2Anomaly += 40.0;
1884
+ } elseif ($isFirefox) {
1885
+ if ($headerOrder && $headerOrder !== 'm,s,p,a') $http2Anomaly += 60.0;
1886
+ } elseif ($isSafari) {
1887
+ if ($headerOrder && $headerOrder !== 'm,s,p,a') $http2Anomaly += 60.0;
1888
+ }
1889
+ }
1854
1890
  }
1855
1891
 
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;
1892
+ // QUIC Anomaly logic
1893
+ $quicFp = $context->getHeader('x-quic-fp') ?? $context->quicFingerprint ?? null;
1894
+ if ($quicFp && is_string($quicFp) && $browser) {
1895
+ $parts = explode(';', $quicFp);
1896
+ if (count($parts) >= 2) {
1897
+ $params = [];
1898
+ foreach (explode(',', $parts[1]) as $p) {
1899
+ $kv = explode('=', $p, 2);
1900
+ if (count($kv) === 2) {
1901
+ $params[$kv[0]] = $kv[1];
1902
+ }
1903
+ }
1904
+ $priorityOrder = $parts[2] ?? '';
1905
+
1906
+ if (str_starts_with($browser, 'Chrome') || str_starts_with($browser, 'Edge')) {
1907
+ $maxData = isset($params['1']) ? (int)$params['1'] : 0;
1908
+ $maxStreams = isset($params['4']) ? (int)$params['4'] : 0;
1909
+ if ($maxData > 0 && $maxData < 1048576) $quicAnomaly += 40.0;
1910
+ if ($maxStreams > 0 && $maxStreams !== 100) $quicAnomaly += 30.0;
1911
+ if (!empty($priorityOrder) && !str_contains($priorityOrder, 'u=')) $quicAnomaly += 30.0;
1912
+ } elseif (str_starts_with($browser, 'Firefox')) {
1913
+ $maxData = isset($params['1']) ? (int)$params['1'] : 0;
1914
+ if ($maxData > 0 && $maxData > 5000000) $quicAnomaly += 40.0;
1915
+ }
1916
+ }
1866
1917
  }
1867
1918
 
1868
- return ['quicAnomalyScore' => max(0.0, min(100.0, $anomaly))];
1919
+ return [
1920
+ 'protocolAnomalyScore' => max(
1921
+ 0.0,
1922
+ min(100.0, $http2Anomaly),
1923
+ min(100.0, $quicAnomaly)
1924
+ )
1925
+ ];
1869
1926
  }
1870
1927
 
1871
1928
  /**