@anonympins/fingerprint 0.4.3 → 0.4.5
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.
- package/CHANGELOG.md +43 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +158 -0
- package/src/js/fingerprint.client.js +852 -635
- package/src/js/fingerprint.js +582 -48
- package/src/js/tests/dynamic-wasm.test.js +78 -0
- package/src/js/tests/fingerprint.client.init.test.js +140 -119
- package/src/js/tests/fingerprint.client.test.js +128 -104
- package/src/js/tests/fingerprint.test.js +130 -11
- package/src/js/tests/ip-reputation.test.js +141 -131
- package/src/js/tests/tcpFingerprint.test.js +78 -0
- package/src/php/Challenge/ChallengeUtils.php +480 -361
- package/src/php/Config/SecurityProfiles.php +286 -271
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +30 -0
- package/src/php/RequestContext.php +90 -90
- package/src/php/Store/IStore.php +41 -41
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/FingerprintEngineTest.php +27 -1
- package/src/php/Tests/IpReputationTest.php +175 -156
- package/src/php/Tests/RequestUtilsTest.php +130 -0
- package/src/php/Utils/RequestUtils.php +474 -21
|
@@ -90,6 +90,7 @@ class RequestUtils
|
|
|
90
90
|
"ch_model" => "sec-ch-ua-model",
|
|
91
91
|
"ch_arch" => "sec-ch-ua-arch",
|
|
92
92
|
"ch_bitness" => "sec-ch-ua-bitness",
|
|
93
|
+
"ch_full_version_list" => "sec-ch-ua-full-version-list",
|
|
93
94
|
"upgrade_req" => "upgrade-insecure-requests",
|
|
94
95
|
"accept_lang" => "accept-language",
|
|
95
96
|
"accept_enc" => "accept-encoding",
|
|
@@ -289,7 +290,105 @@ class RequestUtils
|
|
|
289
290
|
|
|
290
291
|
return ['avgSpeed' => $avgSpeed, 'avgAcceleration' => $avgAcceleration, 'straightness' => $straightness, 'pauses' => $pauses, 'segments' => array_column($segments, 'distance')];
|
|
291
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Analyse une série d'événements tactiles mobiles pour en extraire des indicateurs comportementaux.
|
|
295
|
+
* @param array|null $history
|
|
296
|
+
* @return array
|
|
297
|
+
*/
|
|
298
|
+
private static function analyzeTouchMovements(?array $history): array
|
|
299
|
+
{
|
|
300
|
+
if (empty($history) || count($history) < 3) {
|
|
301
|
+
return [
|
|
302
|
+
'avgSpeed' => 0.0, 'avgAcceleration' => 0.0, 'straightness' => 1.0, 'pauses' => 0, 'segments' => [],
|
|
303
|
+
'avgPressure' => 0.0, 'avgRadius' => 0.0, 'pressureVariance' => 0.0, 'radiusVariance' => 0.0, 'maxTouches' => 1
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
$segments = [];
|
|
308
|
+
$totalDistance = 0.0;
|
|
309
|
+
$pauses = 0;
|
|
310
|
+
$totalPressure = 0.0;
|
|
311
|
+
$totalRadius = 0.0;
|
|
312
|
+
$maxTouches = 1;
|
|
313
|
+
|
|
314
|
+
for ($i = 1; $i < count($history); $i++) {
|
|
315
|
+
$p1 = $history[$i - 1];
|
|
316
|
+
$p2 = $history[$i];
|
|
317
|
+
$dx = $p2['x'] - $p1['x'];
|
|
318
|
+
$dy = $p2['y'] - $p1['y'];
|
|
319
|
+
$dt = $p2['t'] - $p1['t'];
|
|
320
|
+
$distance = sqrt($dx * $dx + $dy * $dy);
|
|
321
|
+
|
|
322
|
+
$totalPressure += (float)($p2['p'] ?? 0.0);
|
|
323
|
+
$totalRadius += (float)($p2['r'] ?? 0.0);
|
|
324
|
+
if (($p2['num'] ?? 1) > $maxTouches) {
|
|
325
|
+
$maxTouches = (int)$p2['num'];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if ($dt > 0) {
|
|
329
|
+
$speed = $distance / $dt;
|
|
330
|
+
$segments[] = ['distance' => $distance, 'dt' => $dt, 'speed' => $speed];
|
|
331
|
+
$totalDistance += $distance;
|
|
332
|
+
}
|
|
333
|
+
if ($dt > 100 && $distance < 5) {
|
|
334
|
+
$pauses++;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
$totalPressure += (float)($history[0]['p'] ?? 0.0);
|
|
339
|
+
$totalRadius += (float)($history[0]['r'] ?? 0.0);
|
|
340
|
+
|
|
341
|
+
$avgPressure = $totalPressure / count($history);
|
|
342
|
+
$avgRadius = $totalRadius / count($history);
|
|
343
|
+
|
|
344
|
+
$sqDiffPressureSum = 0.0;
|
|
345
|
+
$sqDiffRadiusSum = 0.0;
|
|
346
|
+
foreach ($history as $pt) {
|
|
347
|
+
$sqDiffPressureSum += pow((float)($pt['p'] ?? 0.0) - $avgPressure, 2);
|
|
348
|
+
$sqDiffRadiusSum += pow((float)($pt['r'] ?? 0.0) - $avgRadius, 2);
|
|
349
|
+
}
|
|
350
|
+
$pressureVariance = $sqDiffPressureSum / count($history);
|
|
351
|
+
$radiusVariance = $sqDiffRadiusSum / count($history);
|
|
352
|
+
|
|
353
|
+
if (count($segments) < 2) {
|
|
354
|
+
return [
|
|
355
|
+
'avgSpeed' => 0.0, 'avgAcceleration' => 0.0, 'straightness' => 1.0, 'pauses' => $pauses, 'segments' => [],
|
|
356
|
+
'avgPressure' => $avgPressure, 'avgRadius' => $avgRadius, 'pressureVariance' => $pressureVariance, 'radiusVariance' => $radiusVariance, 'maxTouches' => $maxTouches
|
|
357
|
+
];
|
|
358
|
+
}
|
|
292
359
|
|
|
360
|
+
$totalTime = $history[count($history) - 1]['t'] - $history[0]['t'];
|
|
361
|
+
$avgSpeed = $totalTime > 0 ? array_sum(array_column($segments, 'speed')) / count($segments) : 0.0;
|
|
362
|
+
|
|
363
|
+
$totalAbsAcceleration = 0.0;
|
|
364
|
+
for ($i = 1; $i < count($segments); $i++) {
|
|
365
|
+
$s1 = $segments[$i - 1];
|
|
366
|
+
$s2 = $segments[$i];
|
|
367
|
+
if ($s2['dt'] > 0) {
|
|
368
|
+
$acceleration = ($s2['speed'] - $s1['speed']) / $s2['dt'];
|
|
369
|
+
$totalAbsAcceleration += abs($acceleration);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
$avgAcceleration = $totalAbsAcceleration / (count($segments) - 1);
|
|
373
|
+
|
|
374
|
+
$startPoint = $history[0];
|
|
375
|
+
$endPoint = $history[count($history) - 1];
|
|
376
|
+
$straightDistance = sqrt(pow($endPoint['x'] - $startPoint['x'], 2) + pow($endPoint['y'] - $startPoint['y'], 2));
|
|
377
|
+
$straightness = $totalDistance > 0 ? $straightDistance / $totalDistance : 1.0;
|
|
378
|
+
|
|
379
|
+
return [
|
|
380
|
+
'avgSpeed' => $avgSpeed,
|
|
381
|
+
'avgAcceleration' => $avgAcceleration,
|
|
382
|
+
'straightness' => $straightness,
|
|
383
|
+
'pauses' => $pauses,
|
|
384
|
+
'segments' => array_column($segments, 'distance'),
|
|
385
|
+
'avgPressure' => $avgPressure,
|
|
386
|
+
'avgRadius' => $avgRadius,
|
|
387
|
+
'pressureVariance' => $pressureVariance,
|
|
388
|
+
'radiusVariance' => $radiusVariance,
|
|
389
|
+
'maxTouches' => $maxTouches
|
|
390
|
+
];
|
|
391
|
+
}
|
|
293
392
|
|
|
294
393
|
/**
|
|
295
394
|
* Calcule un score basé sur les métriques comportementales envoyées par le client.
|
|
@@ -314,6 +413,7 @@ class RequestUtils
|
|
|
314
413
|
$score = 0.0;
|
|
315
414
|
|
|
316
415
|
$mouseAnalysis = self::analyzeMouseMovements($metrics['mouseMovementsHistory'] ?? null);
|
|
416
|
+
$touch = self::analyzeTouchMovements($metrics['touchMovementsHistory'] ?? null);
|
|
317
417
|
|
|
318
418
|
if (isset($metrics['historyLength'])) {
|
|
319
419
|
if ($metrics['historyLength'] === 1) $score += 15;
|
|
@@ -321,7 +421,7 @@ class RequestUtils
|
|
|
321
421
|
elseif ($metrics['historyLength'] >= 2) $score -= 10;
|
|
322
422
|
} else {
|
|
323
423
|
// Pénalité pour absence totale d'interaction si l'historique n'est pas dispo
|
|
324
|
-
if ($mouseAnalysis['avgSpeed'] == 0 && ($metrics['keystrokeLatency'] ?? 0) == 0) {
|
|
424
|
+
if ($mouseAnalysis['avgSpeed'] == 0 && $touch['avgSpeed'] == 0 && ($metrics['keystrokeLatency'] ?? 0) == 0) {
|
|
325
425
|
$score += 40;
|
|
326
426
|
}
|
|
327
427
|
}
|
|
@@ -343,6 +443,30 @@ class RequestUtils
|
|
|
343
443
|
$score += 35;
|
|
344
444
|
}
|
|
345
445
|
}
|
|
446
|
+
// Analyse comportementale des événements tactiles (Touch Move)
|
|
447
|
+
$touchHistory = $metrics['touchMovementsHistory'] ?? null;
|
|
448
|
+
if (!empty($touchHistory)) {
|
|
449
|
+
if ($touch['avgSpeed'] > 0) {
|
|
450
|
+
if ($touch['avgSpeed'] > 5) $score += 30;
|
|
451
|
+
if ($touch['avgAcceleration'] > 0.8) $score += 20;
|
|
452
|
+
if ($touch['straightness'] > 0.98) $score += 35;
|
|
453
|
+
if ($touch['pauses'] === 0 && count($touch['segments']) > 25) $score += 15;
|
|
454
|
+
|
|
455
|
+
// Détection de l'émulation (pression et rayon de contact constants)
|
|
456
|
+
if ($touch['avgPressure'] > 0 && $touch['pressureVariance'] == 0) {
|
|
457
|
+
$score += 30;
|
|
458
|
+
}
|
|
459
|
+
if ($touch['avgRadius'] > 0 && $touch['radiusVariance'] == 0) {
|
|
460
|
+
$score += 30;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (count($touch['segments']) > 10) {
|
|
464
|
+
$benfordDev = Optimization::benfordTest($touch['segments']);
|
|
465
|
+
if ($benfordDev > 0.18) {
|
|
466
|
+
$score += 35;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
346
470
|
|
|
347
471
|
return ['behaviorScore' => min(100.0, $score)];
|
|
348
472
|
}
|
|
@@ -444,6 +568,50 @@ class RequestUtils
|
|
|
444
568
|
return ['clientHintsInconsistencyScore' => 0.0];
|
|
445
569
|
}
|
|
446
570
|
|
|
571
|
+
$fullVersionList = $context->getHeader('sec-ch-ua-full-version-list');
|
|
572
|
+
if (!empty($fullVersionList)) {
|
|
573
|
+
$chFullVersion = null;
|
|
574
|
+
$chFullBrowser = null;
|
|
575
|
+
if (preg_match_all('/"([^"]+)";v="([^"]+)"/', $fullVersionList, $matches, PREG_SET_ORDER)) {
|
|
576
|
+
foreach ($matches as $match) {
|
|
577
|
+
$brand = $match[1];
|
|
578
|
+
$version = $match[2];
|
|
579
|
+
if ($brand === 'Google Chrome' || $brand === 'Chromium' || $brand === 'Microsoft Edge') {
|
|
580
|
+
$chFullVersion = $version;
|
|
581
|
+
$chFullBrowser = $brand === 'Microsoft Edge' ? 'Edge' : 'Chrome';
|
|
582
|
+
if ($brand === 'Google Chrome' || $brand === 'Microsoft Edge') {
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if ($chFullVersion && $chFullBrowser) {
|
|
589
|
+
if (preg_match('/(Chrome|Edg)\/([\d\.]+)/', $ua, $uaFullMatches)) {
|
|
590
|
+
$uaBrowserMapped = $uaFullMatches[1] === 'Edg' ? 'Edge' : 'Chrome';
|
|
591
|
+
$uaFullVersion = $uaFullMatches[2];
|
|
592
|
+
if ($uaBrowserMapped === $chFullBrowser && $uaFullVersion !== $chFullVersion) {
|
|
593
|
+
$parts1 = array_map('intval', explode('.', $uaFullVersion));
|
|
594
|
+
$parts2 = array_map('intval', explode('.', $chFullVersion));
|
|
595
|
+
$diffIndex = -1;
|
|
596
|
+
$maxLen = max(count($parts1), count($parts2));
|
|
597
|
+
for ($i = 0; $i < $maxLen; $i++) {
|
|
598
|
+
$p1 = $parts1[$i] ?? 0;
|
|
599
|
+
$p2 = $parts2[$i] ?? 0;
|
|
600
|
+
if ($p1 !== $p2) {
|
|
601
|
+
$diffIndex = $i;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
$baseScores = [95.0, 90.0, 85.0, 80.0];
|
|
606
|
+
$baseScore = $baseScores[$diffIndex] ?? 80.0;
|
|
607
|
+
$delta = abs(($parts1[$diffIndex] ?? 0) - ($parts2[$diffIndex] ?? 0));
|
|
608
|
+
$finalFullScore = min(100.0, $baseScore + min(5.0, $delta * 5.0));
|
|
609
|
+
return ['clientHintsInconsistencyScore' => $finalFullScore];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
447
615
|
// 1. Extraire la version du navigateur depuis le User-Agent
|
|
448
616
|
$uaVersion = null;
|
|
449
617
|
if (preg_match('/(Chrome|Firefox|Edg|Safari)\/([\d\.]+)/', $ua, $uaMatches)) {
|
|
@@ -479,13 +647,19 @@ class RequestUtils
|
|
|
479
647
|
return ['clientHintsInconsistencyScore' => 90.0];
|
|
480
648
|
}
|
|
481
649
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
650
|
+
$clientHintsInconsistencyScore = 0.0;
|
|
651
|
+
if ($versionDifference > 0) {
|
|
652
|
+
if ($versionDifference <= 2) {
|
|
653
|
+
$clientHintsInconsistencyScore = $versionDifference * 20.0;
|
|
654
|
+
} elseif ($versionDifference <= 7) {
|
|
655
|
+
$clientHintsInconsistencyScore = 40.0 + ($versionDifference - 2) * 8.0;
|
|
656
|
+
} else {
|
|
657
|
+
$clientHintsInconsistencyScore = min(100.0, 80.0 + ($versionDifference - 7) * 3.33);
|
|
658
|
+
}
|
|
659
|
+
$clientHintsInconsistencyScore = round($clientHintsInconsistencyScore, 1);
|
|
486
660
|
}
|
|
487
661
|
|
|
488
|
-
return ['clientHintsInconsistencyScore' =>
|
|
662
|
+
return ['clientHintsInconsistencyScore' => $clientHintsInconsistencyScore];
|
|
489
663
|
}
|
|
490
664
|
/**
|
|
491
665
|
* Calcule les indicateurs comportementaux liés à l'historique de l'appareil.
|
|
@@ -553,7 +727,7 @@ class RequestUtils
|
|
|
553
727
|
* @param string $fpString La chaîne d'empreinte complète.
|
|
554
728
|
* @return string La sous-chaîne de l'empreinte contenant uniquement les parties stables.
|
|
555
729
|
*/
|
|
556
|
-
|
|
730
|
+
public static function extractStablePart(string $fpString): string
|
|
557
731
|
{
|
|
558
732
|
$stableKeys = ['ua', 'ja3', 'ja4', 'h2', 'tcp'];
|
|
559
733
|
$parts = explode('|', $fpString);
|
|
@@ -584,6 +758,9 @@ class RequestUtils
|
|
|
584
758
|
$patternWeight = $patternConfig['patternWeight'] ?? 80;
|
|
585
759
|
$decayFactor = $patternConfig['decayFactor'] ?? 0.95;
|
|
586
760
|
$inactivityReset = $patternConfig['inactivityReset'] ?? 180000;
|
|
761
|
+
$regularityRatio = $patternConfig['regularityRatio'] ?? 0.4;
|
|
762
|
+
$benfordRatio = $patternConfig['benfordRatio'] ?? 0.3;
|
|
763
|
+
$enumerationRatio = $patternConfig['enumerationRatio'] ?? 0.3;
|
|
587
764
|
|
|
588
765
|
$now = time() * 1000;
|
|
589
766
|
$history = $deviceData['requestHistory'] ?? [];
|
|
@@ -606,7 +783,8 @@ class RequestUtils
|
|
|
606
783
|
}
|
|
607
784
|
$deviceData['requestHistory'] = $history;
|
|
608
785
|
|
|
609
|
-
$
|
|
786
|
+
$regularityScore = 0.0;
|
|
787
|
+
$benfordScore = 0.0;
|
|
610
788
|
$timings = $deviceData['timingHistory'];
|
|
611
789
|
|
|
612
790
|
// Analyse statistique si nous avons assez de données
|
|
@@ -621,18 +799,16 @@ class RequestUtils
|
|
|
621
799
|
$stdDev = sqrt($variance);
|
|
622
800
|
$benfordDeviation = Optimization::benfordTest($timings);
|
|
623
801
|
|
|
624
|
-
// Détection de régularité (bots de type "cron")
|
|
625
802
|
if ($stdDev < $regularityThreshold) {
|
|
626
|
-
$
|
|
803
|
+
$regularityScore = 1.0 - ($stdDev / $regularityThreshold);
|
|
627
804
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
$instantScore = $patternWeight;
|
|
805
|
+
if ($benfordDeviation > $benfordThreshold) {
|
|
806
|
+
$benfordScore = min(1.0, ($benfordDeviation - $benfordThreshold) / (0.5 - $benfordThreshold));
|
|
631
807
|
}
|
|
632
808
|
}
|
|
633
809
|
|
|
634
|
-
//
|
|
635
|
-
$enumerationScore = 0;
|
|
810
|
+
// Path enumeration progressif
|
|
811
|
+
$enumerationScore = 0.0;
|
|
636
812
|
if (count($history) >= 3) {
|
|
637
813
|
$templates = array_map(function($h) {
|
|
638
814
|
return preg_replace('/\d+/', '{num}', $h['path']);
|
|
@@ -646,10 +822,15 @@ class RequestUtils
|
|
|
646
822
|
$maxTemplateRepetition = !empty($templateCounts) ? max($templateCounts) : 0;
|
|
647
823
|
|
|
648
824
|
if ($maxTemplateRepetition >= 3 && count($uniquePaths) === count($history)) {
|
|
649
|
-
$enumerationScore = $
|
|
825
|
+
$enumerationScore = min(1.0, ($maxTemplateRepetition - 2) / 5.0);
|
|
650
826
|
}
|
|
651
827
|
}
|
|
652
828
|
|
|
829
|
+
$weightedScore = ($regularityScore * $regularityRatio) +
|
|
830
|
+
($benfordScore * $benfordRatio) +
|
|
831
|
+
($enumerationScore * $enumerationRatio);
|
|
832
|
+
$instantScore = $weightedScore * $patternWeight;
|
|
833
|
+
|
|
653
834
|
// Logique de décroissance et de score final
|
|
654
835
|
$newPatternScore = $deviceData['lastPatternScore'] ?? 0;
|
|
655
836
|
|
|
@@ -660,7 +841,7 @@ class RequestUtils
|
|
|
660
841
|
}
|
|
661
842
|
$newPatternScore = max(0, $newPatternScore);
|
|
662
843
|
|
|
663
|
-
$deviceData['lastPatternScore'] = $
|
|
844
|
+
$deviceData['lastPatternScore'] = max((float)$instantScore, (float)$newPatternScore);
|
|
664
845
|
|
|
665
846
|
return ['requestPatternScore' => min(100.0, $deviceData['lastPatternScore'])];
|
|
666
847
|
}
|
|
@@ -914,14 +1095,18 @@ class RequestUtils
|
|
|
914
1095
|
$subnetData['highScoreDevices'] = [];
|
|
915
1096
|
}
|
|
916
1097
|
|
|
917
|
-
|
|
1098
|
+
// Utilisation de la partie stable du fingerprint matériel plutôt que l'ID de cookie volatil
|
|
1099
|
+
$currentDeviceHash = self::getCompositeDeviceHash($context);
|
|
1100
|
+
$stableFpId = FingerprintBuilder::cyrb53(self::extractStablePart($currentDeviceHash));
|
|
1101
|
+
|
|
1102
|
+
$currentDeviceContributions = $subnetData['highScoreDevices'][$stableFpId] ?? 0;
|
|
918
1103
|
if ($currentDeviceContributions < 5 && $finalScore < 95) {
|
|
919
|
-
$subnetData['highScoreDevices'][$
|
|
1104
|
+
$subnetData['highScoreDevices'][$stableFpId] = $currentDeviceContributions + 1;
|
|
920
1105
|
$subnetData['highScoreCount']++;
|
|
921
1106
|
}
|
|
922
1107
|
|
|
923
|
-
if (!in_array($
|
|
924
|
-
$subnetData['deviceIds'][] = $
|
|
1108
|
+
if (!in_array($stableFpId, $subnetData['deviceIds'], true)) {
|
|
1109
|
+
$subnetData['deviceIds'][] = $stableFpId;
|
|
925
1110
|
}
|
|
926
1111
|
$subnetData['lastActivity'] = time();
|
|
927
1112
|
|
|
@@ -986,6 +1171,56 @@ class RequestUtils
|
|
|
986
1171
|
return ['subnetScore' => min(100.0, $score)];
|
|
987
1172
|
}
|
|
988
1173
|
|
|
1174
|
+
/**
|
|
1175
|
+
* Calcule le score d'anomalie de similarité réseau (Botnet Clustering).
|
|
1176
|
+
* @param RequestContext $context
|
|
1177
|
+
* @param string $stableFpHash
|
|
1178
|
+
* @return array{'botnetClusterScore': float}
|
|
1179
|
+
*/
|
|
1180
|
+
public static function getBotnetClusterScore(RequestContext $context, string $stableFpHash): array
|
|
1181
|
+
{
|
|
1182
|
+
if (empty($stableFpHash)) {
|
|
1183
|
+
return ['botnetClusterScore' => 0.0];
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
$store = StoreManager::getStore();
|
|
1187
|
+
$key = "botnet-cluster:{$stableFpHash}";
|
|
1188
|
+
$now = time();
|
|
1189
|
+
$tenMinutesAgo = $now - 600;
|
|
1190
|
+
|
|
1191
|
+
$clusterData = $store->get($key) ?? [];
|
|
1192
|
+
if (!is_array($clusterData)) {
|
|
1193
|
+
$clusterData = [];
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
$clusterData = array_filter($clusterData, function ($entry) use ($tenMinutesAgo) {
|
|
1197
|
+
return isset($entry['timestamp']) && $entry['timestamp'] > $tenMinutesAgo;
|
|
1198
|
+
});
|
|
1199
|
+
$clusterData = array_values($clusterData);
|
|
1200
|
+
|
|
1201
|
+
$found = false;
|
|
1202
|
+
foreach ($clusterData as &$entry) {
|
|
1203
|
+
if (isset($entry['ip']) && $entry['ip'] === $context->clientIp) {
|
|
1204
|
+
$entry['timestamp'] = $now;
|
|
1205
|
+
$found = true;
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
unset($entry);
|
|
1210
|
+
|
|
1211
|
+
if (!$found) {
|
|
1212
|
+
$clusterData[] = ['ip' => $context->clientIp, 'timestamp' => $now];
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
$store->set($key, $clusterData, 600);
|
|
1216
|
+
$uniqueIpsCount = count($clusterData);
|
|
1217
|
+
$botnetClusterScore = 0.0;
|
|
1218
|
+
if ($uniqueIpsCount >= 2) {
|
|
1219
|
+
$botnetClusterScore = min(100.0, round(100.0 * (1.0 - exp(-0.35 * ($uniqueIpsCount - 1))), 1));
|
|
1220
|
+
}
|
|
1221
|
+
return ['botnetClusterScore' => $botnetClusterScore];
|
|
1222
|
+
}
|
|
1223
|
+
|
|
989
1224
|
/**
|
|
990
1225
|
* Calcule le score de réputation d'une IP en appliquant la décroissance temporelle.
|
|
991
1226
|
*/
|
|
@@ -1130,6 +1365,183 @@ class RequestUtils
|
|
|
1130
1365
|
return hash_equals($expectedSignature, $storedSignature);
|
|
1131
1366
|
}
|
|
1132
1367
|
|
|
1368
|
+
/**
|
|
1369
|
+
* Parse une trame TCP SYN brute (IPv4 ou IPv6).
|
|
1370
|
+
*/
|
|
1371
|
+
public static function parseTcpSyn(?string $binary): ?array
|
|
1372
|
+
{
|
|
1373
|
+
if (!$binary || strlen($binary) < 40) return null;
|
|
1374
|
+
$ttl = 64;
|
|
1375
|
+
$tcpOffset = 20;
|
|
1376
|
+
$version = ord($binary[0]) >> 4;
|
|
1377
|
+
|
|
1378
|
+
if ($version === 4) {
|
|
1379
|
+
$ttl = ord($binary[8]);
|
|
1380
|
+
$ihl = ord($binary[0]) & 0x0f;
|
|
1381
|
+
$tcpOffset = $ihl * 4;
|
|
1382
|
+
} elseif ($version === 6) {
|
|
1383
|
+
$ttl = ord($binary[7]); // Hop Limit
|
|
1384
|
+
$tcpOffset = 40;
|
|
1385
|
+
} else {
|
|
1386
|
+
$tcpOffset = 0;
|
|
1387
|
+
$ttl = 64;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (strlen($binary) < $tcpOffset + 20) return null;
|
|
1391
|
+
|
|
1392
|
+
$windowSize = (ord($binary[$tcpOffset + 14]) << 8) | ord($binary[$tcpOffset + 15]);
|
|
1393
|
+
$dataOffset = (ord($binary[$tcpOffset + 12]) >> 4) * 4;
|
|
1394
|
+
$optionsEnd = $tcpOffset + $dataOffset;
|
|
1395
|
+
|
|
1396
|
+
$mss = null;
|
|
1397
|
+
$ws = null;
|
|
1398
|
+
$sack = false;
|
|
1399
|
+
|
|
1400
|
+
$i = $tcpOffset + 20;
|
|
1401
|
+
while ($i < $optionsEnd && $i < strlen($binary)) {
|
|
1402
|
+
$optType = ord($binary[$i]);
|
|
1403
|
+
if ($optType === 0) break;
|
|
1404
|
+
if ($optType === 1) {
|
|
1405
|
+
$i++;
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if ($i + 1 >= strlen($binary)) break;
|
|
1409
|
+
$optLen = ord($binary[$i + 1]);
|
|
1410
|
+
if ($optLen < 2 || $i + $optLen > strlen($binary)) break;
|
|
1411
|
+
|
|
1412
|
+
if ($optType === 2 && $optLen === 4) {
|
|
1413
|
+
$mss = (ord($binary[$i + 2]) << 8) | ord($binary[$i + 3]);
|
|
1414
|
+
} elseif ($optType === 3 && $optLen === 3) {
|
|
1415
|
+
$ws = ord($binary[$i + 2]);
|
|
1416
|
+
} elseif ($optType === 4 && $optLen === 2) {
|
|
1417
|
+
$sack = true;
|
|
1418
|
+
}
|
|
1419
|
+
$i += $optLen;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return ['ttl' => $ttl, 'windowSize' => $windowSize, 'mss' => $mss, 'ws' => $ws, 'sack' => $sack];
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Classifie l'OS à partir du fingerprint de la pile TCP/IP.
|
|
1427
|
+
*/
|
|
1428
|
+
public static function classifyTcpOs(?array $fingerprint): string
|
|
1429
|
+
{
|
|
1430
|
+
if (!$fingerprint) return 'unknown';
|
|
1431
|
+
$ttl = $fingerprint['ttl'] ?? 64;
|
|
1432
|
+
$windowSize = $fingerprint['windowSize'] ?? 0;
|
|
1433
|
+
$ws = $fingerprint['ws'] ?? null;
|
|
1434
|
+
|
|
1435
|
+
if ($ttl > 64 && $ttl <= 128) {
|
|
1436
|
+
return 'Windows';
|
|
1437
|
+
}
|
|
1438
|
+
if ($ttl > 32 && $ttl <= 64) {
|
|
1439
|
+
if ($windowSize === 29200 || $windowSize === 14600 || $windowSize === 5840) {
|
|
1440
|
+
return 'Linux';
|
|
1441
|
+
}
|
|
1442
|
+
return 'Linux';
|
|
1443
|
+
}
|
|
1444
|
+
if ($ttl <= 64) {
|
|
1445
|
+
if ($windowSize === 65535 && ($ws === 6 || $ws === 8 || $ws === 5)) {
|
|
1446
|
+
return 'macOS/iOS';
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if ($ttl > 64) return 'Windows';
|
|
1450
|
+
if ($ttl > 0) return 'Linux';
|
|
1451
|
+
return 'unknown';
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Détecte les anomalies de pile réseau par rapport au User-Agent.
|
|
1456
|
+
*/
|
|
1457
|
+
public static function getTcpAnomalyScore(RequestContext $context): array
|
|
1458
|
+
{
|
|
1459
|
+
$fp = null;
|
|
1460
|
+
$rawTcpBinary = $context->getHeader('x-raw-tcp-binary') ?? $context->headers['x-raw-tcp-binary'] ?? null;
|
|
1461
|
+
if ($rawTcpBinary) {
|
|
1462
|
+
$binary = @hex2bin($rawTcpBinary) ?: $rawTcpBinary;
|
|
1463
|
+
$fp = self::parseTcpSyn($binary);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (!$fp) {
|
|
1467
|
+
$tcpHeader = $context->getHeader('x-tcp-fingerprint') ?? $context->tcpFingerprint ?? null;
|
|
1468
|
+
if ($tcpHeader && is_string($tcpHeader)) {
|
|
1469
|
+
$parts = explode(':', $tcpHeader);
|
|
1470
|
+
if (count($parts) >= 2) {
|
|
1471
|
+
$fp = [
|
|
1472
|
+
'ttl' => (int)$parts[0],
|
|
1473
|
+
'windowSize' => (int)$parts[1],
|
|
1474
|
+
'mss' => isset($parts[2]) ? (int)$parts[2] : null,
|
|
1475
|
+
'ws' => isset($parts[3]) ? (int)$parts[3] : null,
|
|
1476
|
+
'sack' => isset($parts[4]) && ($parts[4] === '1' || $parts[4] === 'true')
|
|
1477
|
+
];
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (!$fp) {
|
|
1483
|
+
return ['tcpAnomalyScore' => 0.0];
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
$tcpOs = self::classifyTcpOs($fp);
|
|
1487
|
+
$ua = $context->getHeader('user-agent') ?? '';
|
|
1488
|
+
$uaParts = self::parseUserAgent($ua);
|
|
1489
|
+
$uaOs = $uaParts['os'] ?? null;
|
|
1490
|
+
|
|
1491
|
+
if (!$uaOs || $tcpOs === 'unknown') {
|
|
1492
|
+
return ['tcpAnomalyScore' => 0.0];
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
$mappedOs = null;
|
|
1496
|
+
if (str_starts_with($uaOs, 'Windows')) {
|
|
1497
|
+
$mappedOs = 'Windows';
|
|
1498
|
+
} elseif (str_starts_with($uaOs, 'Mac') || str_starts_with($uaOs, 'macOS')) {
|
|
1499
|
+
$mappedOs = 'macOS';
|
|
1500
|
+
} elseif (str_starts_with($uaOs, 'iOS')) {
|
|
1501
|
+
$mappedOs = 'iOS';
|
|
1502
|
+
} elseif (str_starts_with($uaOs, 'Linux')) {
|
|
1503
|
+
$mappedOs = 'Linux';
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if ($mappedOs === null) {
|
|
1507
|
+
return ['tcpAnomalyScore' => 0.0];
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
$osExpectedTcp = [
|
|
1511
|
+
'Windows' => ['ttl' => 128, 'windowSize' => 64240, 'ws' => 8, 'mss' => 1460, 'sack' => true],
|
|
1512
|
+
'Linux' => ['ttl' => 64, 'windowSize' => 29200, 'ws' => 7, 'mss' => 1460, 'sack' => true],
|
|
1513
|
+
'macOS' => ['ttl' => 64, 'windowSize' => 65535, 'ws' => 6, 'mss' => 1460, 'sack' => true],
|
|
1514
|
+
'iOS' => ['ttl' => 64, 'windowSize' => 65535, 'ws' => 6, 'mss' => 1460, 'sack' => true]
|
|
1515
|
+
];
|
|
1516
|
+
|
|
1517
|
+
$expected = $osExpectedTcp[$mappedOs];
|
|
1518
|
+
$ttlDiff = abs($fp['ttl'] - $expected['ttl']) / $expected['ttl'];
|
|
1519
|
+
$winDiff = abs($fp['windowSize'] - $expected['windowSize']) / $expected['windowSize'];
|
|
1520
|
+
$wsDiff = $expected['ws'] !== null && ($fp['ws'] ?? null) !== null ? abs($fp['ws'] - $expected['ws']) / $expected['ws'] : 0.0;
|
|
1521
|
+
$mssDiff = $expected['mss'] !== null && ($fp['mss'] ?? null) !== null ? abs($fp['mss'] - $expected['mss']) / $expected['mss'] : 0.0;
|
|
1522
|
+
$sackDiff = ($fp['sack'] ?? true) === ($expected['sack'] ?? true) ? 0.0 : 1.0;
|
|
1523
|
+
|
|
1524
|
+
$deviation = (
|
|
1525
|
+
min(1.0, $ttlDiff) * 0.50 +
|
|
1526
|
+
min(1.0, $winDiff) * 0.25 +
|
|
1527
|
+
min(1.0, $wsDiff) * 0.15 +
|
|
1528
|
+
min(1.0, $mssDiff) * 0.05 +
|
|
1529
|
+
$sackDiff * 0.05
|
|
1530
|
+
);
|
|
1531
|
+
|
|
1532
|
+
$tcpAnomalyScore = 0.0;
|
|
1533
|
+
if ($tcpOs !== $mappedOs && $tcpOs !== 'unknown') {
|
|
1534
|
+
$baseAnomaly = $mappedOs === 'Windows' ? 80.0 :
|
|
1535
|
+
(($mappedOs === 'macOS' || $mappedOs === 'iOS') ? 85.0 : 75.0);
|
|
1536
|
+
$tcpAnomalyScore = $baseAnomaly + ($deviation - 0.4) * 10.0;
|
|
1537
|
+
} else {
|
|
1538
|
+
$tcpAnomalyScore = $deviation * 40.0;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
$tcpAnomalyScore = max(0.0, min(100.0, round($tcpAnomalyScore, 1)));
|
|
1542
|
+
return ['tcpAnomalyScore' => $tcpAnomalyScore];
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1133
1545
|
/**
|
|
1134
1546
|
* Vérifie si un ticket de clearance (PoW) est valide, en supportant la tolérance au roaming.
|
|
1135
1547
|
*
|
|
@@ -1146,6 +1558,47 @@ class RequestUtils
|
|
|
1146
1558
|
return false;
|
|
1147
1559
|
}
|
|
1148
1560
|
|
|
1561
|
+
if (str_contains($ticket, '.')) {
|
|
1562
|
+
$parts = explode('.', $ticket);
|
|
1563
|
+
if (count($parts) === 3) {
|
|
1564
|
+
$base64UrlDecode = function ($input) {
|
|
1565
|
+
return base64_decode(strtr($input, '-_', '+/'));
|
|
1566
|
+
};
|
|
1567
|
+
$iv = $base64UrlDecode($parts[0]);
|
|
1568
|
+
$encrypted = $base64UrlDecode($parts[1]);
|
|
1569
|
+
$signature = $base64UrlDecode($parts[2]);
|
|
1570
|
+
if ($iv && $encrypted && $signature && strlen($iv) === 16) {
|
|
1571
|
+
$key = hash('sha256', $secret ?: "fallback-dev-secret-32-chars-minimum", true);
|
|
1572
|
+
$expectedSignature = hash_hmac('sha256', $iv . $encrypted, $key, true);
|
|
1573
|
+
if (hash_equals($expectedSignature, $signature)) {
|
|
1574
|
+
$decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
|
1575
|
+
if ($decrypted !== false) {
|
|
1576
|
+
$ticketData = json_decode($decrypted, true);
|
|
1577
|
+
if ($ticketData) {
|
|
1578
|
+
$expiry = $ticketData['expiry'] ?? null;
|
|
1579
|
+
$originalIp = $ticketData['originalIp'] ?? null;
|
|
1580
|
+
$storedDeviceId = $ticketData['deviceId'] ?? '';
|
|
1581
|
+
$storedDeviceHash = $ticketData['deviceHash'] ?? '';
|
|
1582
|
+
if (!$expiry || (time() * 1000) > (int)$expiry) {
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
if ($ip === $originalIp) {
|
|
1586
|
+
return true;
|
|
1587
|
+
}
|
|
1588
|
+
$currentSubnet = self::getIpSubnet($ip);
|
|
1589
|
+
$originalSubnet = self::getIpSubnet($originalIp);
|
|
1590
|
+
if ($currentSubnet !== null && $originalSubnet !== null && $currentSubnet === $originalSubnet) {
|
|
1591
|
+
return true;
|
|
1592
|
+
}
|
|
1593
|
+
return !empty($deviceId) && $deviceId === $storedDeviceId && !empty($deviceHash) && $deviceHash === $storedDeviceHash;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1149
1602
|
if (str_contains($ticket, '|')) {
|
|
1150
1603
|
$parts = explode('|', $ticket);
|
|
1151
1604
|
if (count($parts) < 3) return false;
|