@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.
- package/CHANGELOG.md +29 -0
- package/README.md +2 -2
- package/package.json +4 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +315 -54
- package/src/js/gpu_pow.solver.js +37 -5
- package/src/js/library.js +188 -97
- package/src/js/pow.solver.inline.js +128 -2
- package/src/js/pow.solver.js +85 -0
- package/src/js/problem-manager.js +16 -1
- package/src/js/tests/cross_parity.test.js +2 -2
- package/src/js/tests/fingerprint.test.js +12 -3
- package/src/js/tests/gpu_pow.test.js +9 -7
- 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/AutoTuner.php +41 -0
- package/src/php/Challenge/ChallengeUtils.php +39 -5
- package/src/php/Config/SecurityProfiles.php +6 -6
- package/src/php/FingerprintEngine.php +17 -6
- package/src/php/Optimization/FunctionRegistry.php +1 -1
- package/src/php/Optimization/OptimizationOperators.php +109 -28
- package/src/php/ProblemManager.php +22 -0
- package/src/php/Tests/ChallengeUtilsTest.php +9 -4
- 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 +102 -45
|
@@ -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
|
-
$
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
$
|
|
126
|
-
$
|
|
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 = $
|
|
142
|
-
$obj2 = $
|
|
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
|
-
|
|
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)
|
|
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.
|
|
201
|
-
['name' => '
|
|
202
|
-
['name' => '
|
|
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
|
-
$
|
|
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[
|
|
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}",
|
|
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
|
}
|
|
@@ -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
|
|
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{'
|
|
1859
|
+
* @return array{'protocolAnomalyScore': float}
|
|
1826
1860
|
*/
|
|
1827
|
-
public static function
|
|
1861
|
+
public static function getProtocolAnomalyScore(RequestContext $context): array
|
|
1828
1862
|
{
|
|
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] ?? '';
|
|
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
|
-
|
|
1853
|
-
|
|
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
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
$
|
|
1860
|
-
if ($
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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 [
|
|
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
|
/**
|