@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.
- package/CHANGELOG.md +34 -0
- package/README.md +14 -1
- package/package.json +4 -1
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +266 -57
- package/src/js/gpu_pow.solver.js +3 -2
- package/src/js/pow.solver.inline.js +128 -2
- package/src/js/pow.solver.js +85 -0
- package/src/js/problem-manager.js +26 -4
- package/src/js/tests/cross_parity.test.js +2 -2
- package/src/js/tests/fingerprint.engine.test.js +25 -0
- package/src/js/tests/fingerprint.test.js +13 -4
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/quicFingerprint.test.js +21 -10
- package/src/js/upow-model-task.js +114 -0
- package/src/php/Challenge/ChallengeUtils.php +2 -2
- package/src/php/Config/SecurityProfiles.php +6 -6
- package/src/php/FingerprintEngine.php +1880 -1879
- package/src/php/Optimization/FunctionRegistry.php +1 -1
- package/src/php/Optimization/OptimizationOperators.php +13 -0
- package/src/php/ProblemManager.php +105 -0
- package/src/php/Tests/ChallengeUtilsTest.php +3 -2
- package/src/php/Tests/IpReputationTest.php +1 -1
- package/src/php/Tests/ProblemManagerTest.php +1 -1
- package/src/php/Tests/QuicFingerprintTest.php +9 -9
- package/src/php/Utils/RequestUtils.php +111 -46
|
@@ -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, '
|
|
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}",
|
|
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
|
|
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
|
|
13
|
+
public function testGetProtocolAnomalyScoreReturnsZeroIfNoFp(): void
|
|
14
14
|
{
|
|
15
15
|
$context = $this->createMock(RequestContext::class);
|
|
16
16
|
$context->method('getHeader')->willReturn(null);
|
|
17
|
-
$res = RequestUtils::
|
|
18
|
-
$this->assertEquals(0.0, $res['
|
|
17
|
+
$res = RequestUtils::getProtocolAnomalyScore($context);
|
|
18
|
+
$this->assertEquals(0.0, $res['protocolAnomalyScore']);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public function
|
|
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::
|
|
35
|
-
$this->assertEquals(100.0, $res['
|
|
34
|
+
$res = RequestUtils::getProtocolAnomalyScore($context);
|
|
35
|
+
$this->assertEquals(100.0, $res['protocolAnomalyScore']);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
public function
|
|
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::
|
|
52
|
-
$this->assertEquals(0.0, $res['
|
|
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 <
|
|
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
|
-
$
|
|
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
|
|
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{'
|
|
1867
|
+
* @return array{'protocolAnomalyScore': float}
|
|
1826
1868
|
*/
|
|
1827
|
-
public static function
|
|
1869
|
+
public static function getProtocolAnomalyScore(RequestContext $context): array
|
|
1828
1870
|
{
|
|
1829
|
-
$
|
|
1830
|
-
|
|
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
|
-
|
|
1853
|
-
|
|
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
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
$
|
|
1860
|
-
if ($
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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 [
|
|
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
|
/**
|