@anonympins/fingerprint 0.6.4 → 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.
@@ -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
@@ -74,10 +87,43 @@ class OptimizationOperators
74
87
  $currentConfig = $context['currentConfig'] ?? null;
75
88
 
76
89
  return function (array $config) use ($trafficData, $currentConfig): array {
77
- $falsePositives = 0.0;
78
- $falseNegatives = 0.0;
79
- $totalHumans = 0.0;
80
- $totalBots = 0.0;
90
+ $threatProfiles = [
91
+ 'account_takeover' => [
92
+ 'importance' => 10.0,
93
+ 'ux_vs_security_ratio' => 0.1,
94
+ 'target_threshold' => 'block',
95
+ 'indicators' => ['requestPatternScore', 'behaviorScore', 'timeInconsistencyScore', 'clickVarianceScore']
96
+ ],
97
+ 'active_exploitation' => [
98
+ 'importance' => 8.0,
99
+ 'ux_vs_security_ratio' => 0.2,
100
+ 'target_threshold' => 'block',
101
+ 'indicators' => ['honeypotScore', 'headerAnomalyScore']
102
+ ],
103
+ 'mass_scraping' => [
104
+ 'importance' => 3.0,
105
+ 'ux_vs_security_ratio' => 0.8,
106
+ 'target_threshold' => 'low',
107
+ 'indicators' => ['requestPatternScore', 'renderingAnomalyScore', 'clientHintsInconsistencyScore']
108
+ ],
109
+ 'distributed_botnets' => [
110
+ 'importance' => 6.0,
111
+ 'ux_vs_security_ratio' => 0.5,
112
+ 'target_threshold' => 'high',
113
+ 'indicators' => ['subnetScore', 'botnetClusterScore', 'ipReputationScore', 'tlsSpoofingScore']
114
+ ],
115
+ 'basic_automation' => [
116
+ 'importance' => 5.0,
117
+ 'ux_vs_security_ratio' => 0.4,
118
+ 'target_threshold' => 'medium',
119
+ 'indicators' => ['botScore', 'tlsSpoofingScore', 'tcpAnomalyScore']
120
+ ]
121
+ ];
122
+
123
+ $threatStats = [];
124
+ foreach ($threatProfiles as $name => $profile) {
125
+ $threatStats[$name] = ['fp' => 0.0, 'fn' => 0.0, 'totalHumans' => 0.0, 'totalBots' => 0.0];
126
+ }
81
127
 
82
128
  $maxHumanScore = 0.0;
83
129
  $minBotScore = 100.0;
@@ -108,22 +154,56 @@ class OptimizationOperators
108
154
  $score = $calculateScore($log);
109
155
 
110
156
  if ($isLikelyBot) {
111
- $totalBots += $confidence;
112
157
  $minBotScore = min($minBotScore, $score);
113
- if ($score < $config['thresholds']['low']) {
114
- $falseNegatives += $confidence;
115
- }
116
158
  } elseif ($isLikelyHuman) {
117
- $totalHumans += $confidence;
118
159
  $maxHumanScore = max($maxHumanScore, $score);
119
- if ($score >= $config['thresholds']['low']) {
120
- $falsePositives += $confidence;
160
+ }
161
+
162
+ foreach ($threatProfiles as $name => $profile) {
163
+ $threatActivity = 0.0;
164
+ foreach ($profile['indicators'] as $indicator) {
165
+ $threatActivity += (float)($log['vector'][$indicator] ?? 0.0);
166
+ }
167
+ if ($threatActivity <= 0.0) {
168
+ continue;
169
+ }
170
+ $effectiveConfidence = $confidence * ($threatActivity / 100.0);
171
+ $targetThreshold = (float)($config['thresholds'][$profile['target_threshold']] ?? 20.0);
172
+
173
+ if ($isLikelyBot) {
174
+ $threatStats[$name]['totalBots'] += $effectiveConfidence;
175
+ if ($score < $targetThreshold) {
176
+ $threatStats[$name]['fn'] += $effectiveConfidence;
177
+ }
178
+ } elseif ($isLikelyHuman) {
179
+ $threatStats[$name]['totalHumans'] += $effectiveConfidence;
180
+ if ($score >= $targetThreshold) {
181
+ $threatStats[$name]['fp'] += $effectiveConfidence;
182
+ }
121
183
  }
122
184
  }
123
185
  }
124
186
 
125
- $falsePositiveRate = $totalHumans > 0.0 ? $falsePositives / $totalHumans : 0.0;
126
- $falseNegativeRate = $totalBots > 0.0 ? $falseNegatives / $totalBots : 0.0;
187
+ $weightedFpr = 0.0;
188
+ $weightedFnr = 0.0;
189
+ $totalImportance = 0.0;
190
+
191
+ foreach ($threatProfiles as $name => $profile) {
192
+ $totalImportance += $profile['importance'];
193
+ }
194
+
195
+ foreach ($threatProfiles as $name => $profile) {
196
+ $stats = $threatStats[$name];
197
+ $fpr = $stats['totalHumans'] > 0 ? $stats['fp'] / $stats['totalHumans'] : 0.0;
198
+ $fnr = $stats['totalBots'] > 0 ? $stats['fn'] / $stats['totalBots'] : 0.0;
199
+
200
+ $importanceWeight = $profile['importance'] / $totalImportance;
201
+ $uxRatio = $profile['ux_vs_security_ratio'];
202
+ $secRatio = 1.0 - $uxRatio;
203
+
204
+ $weightedFpr += $fpr * $importanceWeight * $uxRatio;
205
+ $weightedFnr += $fnr * $importanceWeight * $secRatio;
206
+ }
127
207
 
128
208
  // 3. Pénalité de dérive d'échelle (L2 Regularization par rapport au profil d'origine)
129
209
  $regularizationPenalty = 0.0;
@@ -138,8 +218,8 @@ class OptimizationOperators
138
218
  $marginOverlap = max(0.0, $maxHumanScore - $minBotScore);
139
219
  $marginPenalty = $marginOverlap / 100.0;
140
220
 
141
- $obj1 = $falsePositiveRate + ($regularizationPenalty * 0.05);
142
- $obj2 = $falseNegativeRate + $marginPenalty;
221
+ $obj1 = $weightedFpr + ($regularizationPenalty * 0.05);
222
+ $obj2 = $weightedFnr + $marginPenalty;
143
223
 
144
224
  return [$obj1, $obj2];
145
225
  };
@@ -160,9 +240,7 @@ class OptimizationOperators
160
240
  $child = $c1;
161
241
  foreach (['thresholds', 'weights', 'patterns'] as $section) {
162
242
  foreach ($child[$section] as $key => $value) {
163
- if ($key !== 'honeypotScore') {
164
- $child[$section][$key] = ($c1[$section][$key] + $c2[$section][$key]) / 2;
165
- }
243
+ $child[$section][$key] = ($c1[$section][$key] + $c2[$section][$key]) / 2;
166
244
  }
167
245
  }
168
246
  return $child;
@@ -177,7 +255,7 @@ class OptimizationOperators
177
255
  foreach (['thresholds', 'weights', 'patterns'] as $section) {
178
256
  if (isset($config[$section]) && is_array($config[$section])) {
179
257
  foreach ($config[$section] as $k => $v) {
180
- if (is_numeric($v) && $k !== 'honeypotScore') {
258
+ if (is_numeric($v)) {
181
259
  $randomVariation = 1.0 + (self::secureRandom() - 0.5) * 0.5; // Variation de +/- 25%
182
260
  $ind[$section][$k] = $v * $randomVariation;
183
261
  } else {
@@ -197,9 +275,9 @@ class OptimizationOperators
197
275
  $mutate = function (array $c, ?array $currentConfigRef = null) use ($currentConfig): array {
198
276
  $newConfig = $c;
199
277
  $sections = [
200
- ['name' => 'patterns', 'weight' => 0.5],
201
- ['name' => 'weights', 'weight' => 0.35],
202
- ['name' => 'thresholds', 'weight' => 0.15]
278
+ ['name' => 'patterns', 'weight' => 0.50],
279
+ ['name' => 'thresholds', 'weight' => 0.25],
280
+ ['name' => 'weights', 'weight' => 0.25]
203
281
  ];
204
282
  $rand = self::secureRandom();
205
283
  $cumulativeWeight = 0;
@@ -215,13 +293,16 @@ class OptimizationOperators
215
293
  $keys = array_keys($newConfig[$sectionToMutate]);
216
294
  $keyToMutate = $keys[random_int(0, count($keys) - 1)];
217
295
 
218
- if ($keyToMutate === 'honeypotScore') return $newConfig;
219
-
220
- $mutationAmount = (self::secureRandom() - 0.5) * 0.4;
221
- $newConfig[$sectionToMutate][$keyToMutate] *= (1 + $mutationAmount);
222
-
223
296
  if ($sectionToMutate === 'weights') {
224
- $newConfig[$sectionToMutate][$keyToMutate] = max(0, min(1.5, $newConfig[$sectionToMutate][$keyToMutate]));
297
+ $newConfig[$sectionToMutate][$keyToMutate] = max(0.05, min(1.5, $newConfig[$sectionToMutate][$keyToMutate] + (self::secureRandom() - 0.5) * 0.1));
298
+ } elseif ($sectionToMutate === 'thresholds') {
299
+ $newConfig[$sectionToMutate][$keyToMutate] = (int)round($newConfig[$sectionToMutate][$keyToMutate] + (self::secureRandom() - 0.5) * 5.0);
300
+ } else {
301
+ if ($keyToMutate === 'decayFactor') {
302
+ $newConfig['patterns']['decayFactor'] = max(0.8, min(0.999, $newConfig['patterns']['decayFactor'] + (self::secureRandom() - 0.5) * 0.05));
303
+ } elseif (str_contains($keyToMutate, 'Threshold') || str_contains($keyToMutate, 'Reset')) {
304
+ $newConfig['patterns'][$keyToMutate] = max(50.0, $newConfig['patterns'][$keyToMutate] + (self::secureRandom() - 0.5) * 50.0);
305
+ }
225
306
  }
226
307
 
227
308
  $refConfig = $currentConfigRef ?? $currentConfig;
@@ -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;
@@ -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));
@@ -53,7 +54,9 @@ class ChallengeUtilsTest extends TestCase
53
54
 
54
55
  // Tamper with value on verified sample index 12
55
56
  $tampered = $solutions;
56
- $tampered[12] = (string)((float)$tampered[12] + 0.0002); // Out of tolerance
57
+ $sampleIndices = ChallengeUtils::deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
58
+ $tamperedIndex = $sampleIndices[0];
59
+ $tampered[$tamperedIndex] = (string)((float)$tampered[$tamperedIndex] + 0.0002); // Out of tolerance
57
60
  $this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tampered)));
58
61
  }
59
62
 
@@ -76,7 +79,7 @@ class ChallengeUtilsTest extends TestCase
76
79
  return unpack('f', pack('f', $value))[1];
77
80
  };
78
81
  for ($idx = 0; $idx < 64; $idx++) {
79
- $x = $fround($numericSeed + $idx * 0.015);
82
+ $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
80
83
  $rFloat = $fround($r);
81
84
  for ($i = 0; $i < $iterations; $i++) {
82
85
  $x = $fround($rFloat * $x * $fround(1.0 - $x));
@@ -85,8 +88,10 @@ class ChallengeUtilsTest extends TestCase
85
88
  }
86
89
  $solutionString = implode(',', $solutions);
87
90
  $this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $solutionString));
91
+ $sampleIndices = ChallengeUtils::deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
92
+ $tamperedIndex = $sampleIndices[0];
88
93
  $tamperedSolutions = $solutions;
89
- $tamperedSolutions[0] = (string)((float)$tamperedSolutions[0] + 0.1);
94
+ $tamperedSolutions[$tamperedIndex] = (string)((float)$tamperedSolutions[$tamperedIndex] + 0.1);
90
95
  $this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tamperedSolutions)));
91
96
  }
92
97
 
@@ -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
  }
@@ -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
  /**