@anonympins/fingerprint 0.6.3 → 0.6.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 +22 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +7014 -6802
- package/src/js/gpu_pow.solver.js +34 -3
- package/src/js/library.js +285 -124
- package/src/js/mongodb-store.js +13 -0
- package/src/js/redis-store.js +13 -0
- package/src/js/sql-store.js +80 -77
- package/src/js/tests/fingerprint.engine.test.js +2 -3
- package/src/js/tests/fingerprint.test.js +2 -2
- package/src/js/tests/gpu_pow.test.js +9 -7
- package/src/php/AutoTuner.php +41 -0
- package/src/php/Challenge/ChallengeUtils.php +40 -6
- package/src/php/Config/SecurityProfiles.php +5 -0
- package/src/php/FingerprintEngine.php +113 -25
- package/src/php/Optimization/OptimizationOperators.php +127 -35
- package/src/php/RequestContext.php +4 -0
- package/src/php/Store/IStore.php +6 -0
- package/src/php/Store/MongoDbStore.php +12 -0
- package/src/php/Store/RedisStore.php +12 -0
- package/src/php/Tests/ChallengeUtilsTest.php +6 -2
- package/src/php/Tests/FingerprintEngineTest.php +46 -0
- package/src/php/Tests/IpReputationTest.php +1 -0
- package/src/php/Utils/RequestUtils.php +42 -9
|
@@ -70,18 +70,55 @@ class OptimizationOperators
|
|
|
70
70
|
*/
|
|
71
71
|
public static function createFullSecurityConfigEvaluator(array $context): callable
|
|
72
72
|
{
|
|
73
|
-
$trafficData = $context['trafficData'];
|
|
73
|
+
$trafficData = $context['trafficData'] ?? [];
|
|
74
|
+
$currentConfig = $context['currentConfig'] ?? null;
|
|
74
75
|
|
|
75
|
-
return function (array $config) use ($trafficData): array {
|
|
76
|
-
$
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
return function (array $config) use ($trafficData, $currentConfig): array {
|
|
77
|
+
$threatProfiles = [
|
|
78
|
+
'account_takeover' => [
|
|
79
|
+
'importance' => 10.0,
|
|
80
|
+
'ux_vs_security_ratio' => 0.1,
|
|
81
|
+
'target_threshold' => 'block',
|
|
82
|
+
'indicators' => ['requestPatternScore', 'behaviorScore', 'timeInconsistencyScore', 'clickVarianceScore']
|
|
83
|
+
],
|
|
84
|
+
'active_exploitation' => [
|
|
85
|
+
'importance' => 8.0,
|
|
86
|
+
'ux_vs_security_ratio' => 0.2,
|
|
87
|
+
'target_threshold' => 'block',
|
|
88
|
+
'indicators' => ['honeypotScore', 'headerAnomalyScore']
|
|
89
|
+
],
|
|
90
|
+
'mass_scraping' => [
|
|
91
|
+
'importance' => 3.0,
|
|
92
|
+
'ux_vs_security_ratio' => 0.8,
|
|
93
|
+
'target_threshold' => 'low',
|
|
94
|
+
'indicators' => ['requestPatternScore', 'renderingAnomalyScore', 'clientHintsInconsistencyScore']
|
|
95
|
+
],
|
|
96
|
+
'distributed_botnets' => [
|
|
97
|
+
'importance' => 6.0,
|
|
98
|
+
'ux_vs_security_ratio' => 0.5,
|
|
99
|
+
'target_threshold' => 'high',
|
|
100
|
+
'indicators' => ['subnetScore', 'botnetClusterScore', 'ipReputationScore', 'tlsSpoofingScore']
|
|
101
|
+
],
|
|
102
|
+
'basic_automation' => [
|
|
103
|
+
'importance' => 5.0,
|
|
104
|
+
'ux_vs_security_ratio' => 0.4,
|
|
105
|
+
'target_threshold' => 'medium',
|
|
106
|
+
'indicators' => ['botScore', 'tlsSpoofingScore', 'tcpAnomalyScore']
|
|
107
|
+
]
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
$threatStats = [];
|
|
111
|
+
foreach ($threatProfiles as $name => $profile) {
|
|
112
|
+
$threatStats[$name] = ['fp' => 0.0, 'fn' => 0.0, 'totalHumans' => 0.0, 'totalBots' => 0.0];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$maxHumanScore = 0.0;
|
|
116
|
+
$minBotScore = 100.0;
|
|
80
117
|
|
|
81
118
|
$calculateScore = function (array $log) use ($config): float {
|
|
82
119
|
$score = 0.0;
|
|
83
120
|
foreach ($config['weights'] as $key => $weight) {
|
|
84
|
-
$score += ($log['vector'][$key] ?? 0) * $weight;
|
|
121
|
+
$score += ($log['vector'][$key] ?? 0.0) * $weight;
|
|
85
122
|
}
|
|
86
123
|
return $score;
|
|
87
124
|
};
|
|
@@ -94,30 +131,84 @@ class OptimizationOperators
|
|
|
94
131
|
'trap_triggered' => 2.0,
|
|
95
132
|
];
|
|
96
133
|
|
|
134
|
+
// 1. Évaluation sur les logs de trafic réels
|
|
97
135
|
foreach ($trafficData as $log) {
|
|
98
|
-
$
|
|
99
|
-
$
|
|
100
|
-
$
|
|
136
|
+
$weight = (float)($log['weight'] ?? 1.0);
|
|
137
|
+
$confidence = ($confidenceWeights[$log['type'] ?? ''] ?? 1.0) * $weight;
|
|
138
|
+
$isLikelyBot = in_array($log['type'] ?? '', ['challenge_issued', 'request_blocked', 'trap_triggered']);
|
|
139
|
+
$isLikelyHuman = in_array($log['type'] ?? '', ['request_passed', 'challenge_solved']);
|
|
140
|
+
|
|
141
|
+
$score = $calculateScore($log);
|
|
101
142
|
|
|
102
143
|
if ($isLikelyBot) {
|
|
103
|
-
$
|
|
104
|
-
$score = $calculateScore($log);
|
|
105
|
-
if ($score < $config['thresholds']['low']) {
|
|
106
|
-
$falseNegatives += $confidence;
|
|
107
|
-
}
|
|
144
|
+
$minBotScore = min($minBotScore, $score);
|
|
108
145
|
} elseif ($isLikelyHuman) {
|
|
109
|
-
$
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
146
|
+
$maxHumanScore = max($maxHumanScore, $score);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
foreach ($threatProfiles as $name => $profile) {
|
|
150
|
+
$threatActivity = 0.0;
|
|
151
|
+
foreach ($profile['indicators'] as $indicator) {
|
|
152
|
+
$threatActivity += (float)($log['vector'][$indicator] ?? 0.0);
|
|
153
|
+
}
|
|
154
|
+
if ($threatActivity <= 0.0) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
$effectiveConfidence = $confidence * ($threatActivity / 100.0);
|
|
158
|
+
$targetThreshold = (float)($config['thresholds'][$profile['target_threshold']] ?? 20.0);
|
|
159
|
+
|
|
160
|
+
if ($isLikelyBot) {
|
|
161
|
+
$threatStats[$name]['totalBots'] += $effectiveConfidence;
|
|
162
|
+
if ($score < $targetThreshold) {
|
|
163
|
+
$threatStats[$name]['fn'] += $effectiveConfidence;
|
|
164
|
+
}
|
|
165
|
+
} elseif ($isLikelyHuman) {
|
|
166
|
+
$threatStats[$name]['totalHumans'] += $effectiveConfidence;
|
|
167
|
+
if ($score >= $targetThreshold) {
|
|
168
|
+
$threatStats[$name]['fp'] += $effectiveConfidence;
|
|
169
|
+
}
|
|
113
170
|
}
|
|
114
171
|
}
|
|
115
172
|
}
|
|
116
173
|
|
|
117
|
-
$
|
|
118
|
-
$
|
|
174
|
+
$weightedFpr = 0.0;
|
|
175
|
+
$weightedFnr = 0.0;
|
|
176
|
+
$totalImportance = 0.0;
|
|
119
177
|
|
|
120
|
-
|
|
178
|
+
foreach ($threatProfiles as $name => $profile) {
|
|
179
|
+
$totalImportance += $profile['importance'];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
foreach ($threatProfiles as $name => $profile) {
|
|
183
|
+
$stats = $threatStats[$name];
|
|
184
|
+
$fpr = $stats['totalHumans'] > 0 ? $stats['fp'] / $stats['totalHumans'] : 0.0;
|
|
185
|
+
$fnr = $stats['totalBots'] > 0 ? $stats['fn'] / $stats['totalBots'] : 0.0;
|
|
186
|
+
|
|
187
|
+
$importanceWeight = $profile['importance'] / $totalImportance;
|
|
188
|
+
$uxRatio = $profile['ux_vs_security_ratio'];
|
|
189
|
+
$secRatio = 1.0 - $uxRatio;
|
|
190
|
+
|
|
191
|
+
$weightedFpr += $fpr * $importanceWeight * $uxRatio;
|
|
192
|
+
$weightedFnr += $fnr * $importanceWeight * $secRatio;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 3. Pénalité de dérive d'échelle (L2 Regularization par rapport au profil d'origine)
|
|
196
|
+
$regularizationPenalty = 0.0;
|
|
197
|
+
if ($currentConfig && isset($currentConfig['weights'])) {
|
|
198
|
+
foreach ($config['weights'] as $key => $val) {
|
|
199
|
+
$originalVal = (float)($currentConfig['weights'][$key] ?? 0.0);
|
|
200
|
+
$regularizationPenalty += pow((float)$val - $originalVal, 2);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 4. Maximisation de la marge
|
|
205
|
+
$marginOverlap = max(0.0, $maxHumanScore - $minBotScore);
|
|
206
|
+
$marginPenalty = $marginOverlap / 100.0;
|
|
207
|
+
|
|
208
|
+
$obj1 = $weightedFpr + ($regularizationPenalty * 0.05);
|
|
209
|
+
$obj2 = $weightedFnr + $marginPenalty;
|
|
210
|
+
|
|
211
|
+
return [$obj1, $obj2];
|
|
121
212
|
};
|
|
122
213
|
}
|
|
123
214
|
|
|
@@ -136,9 +227,7 @@ class OptimizationOperators
|
|
|
136
227
|
$child = $c1;
|
|
137
228
|
foreach (['thresholds', 'weights', 'patterns'] as $section) {
|
|
138
229
|
foreach ($child[$section] as $key => $value) {
|
|
139
|
-
|
|
140
|
-
$child[$section][$key] = ($c1[$section][$key] + $c2[$section][$key]) / 2;
|
|
141
|
-
}
|
|
230
|
+
$child[$section][$key] = ($c1[$section][$key] + $c2[$section][$key]) / 2;
|
|
142
231
|
}
|
|
143
232
|
}
|
|
144
233
|
return $child;
|
|
@@ -153,7 +242,7 @@ class OptimizationOperators
|
|
|
153
242
|
foreach (['thresholds', 'weights', 'patterns'] as $section) {
|
|
154
243
|
if (isset($config[$section]) && is_array($config[$section])) {
|
|
155
244
|
foreach ($config[$section] as $k => $v) {
|
|
156
|
-
if (is_numeric($v)
|
|
245
|
+
if (is_numeric($v)) {
|
|
157
246
|
$randomVariation = 1.0 + (self::secureRandom() - 0.5) * 0.5; // Variation de +/- 25%
|
|
158
247
|
$ind[$section][$k] = $v * $randomVariation;
|
|
159
248
|
} else {
|
|
@@ -173,9 +262,9 @@ class OptimizationOperators
|
|
|
173
262
|
$mutate = function (array $c, ?array $currentConfigRef = null) use ($currentConfig): array {
|
|
174
263
|
$newConfig = $c;
|
|
175
264
|
$sections = [
|
|
176
|
-
['name' => 'patterns', 'weight' => 0.
|
|
177
|
-
['name' => '
|
|
178
|
-
['name' => '
|
|
265
|
+
['name' => 'patterns', 'weight' => 0.50],
|
|
266
|
+
['name' => 'thresholds', 'weight' => 0.25],
|
|
267
|
+
['name' => 'weights', 'weight' => 0.25]
|
|
179
268
|
];
|
|
180
269
|
$rand = self::secureRandom();
|
|
181
270
|
$cumulativeWeight = 0;
|
|
@@ -191,13 +280,16 @@ class OptimizationOperators
|
|
|
191
280
|
$keys = array_keys($newConfig[$sectionToMutate]);
|
|
192
281
|
$keyToMutate = $keys[random_int(0, count($keys) - 1)];
|
|
193
282
|
|
|
194
|
-
if ($keyToMutate === 'honeypotScore') return $newConfig;
|
|
195
|
-
|
|
196
|
-
$mutationAmount = (self::secureRandom() - 0.5) * 0.4;
|
|
197
|
-
$newConfig[$sectionToMutate][$keyToMutate] *= (1 + $mutationAmount);
|
|
198
|
-
|
|
199
283
|
if ($sectionToMutate === 'weights') {
|
|
200
|
-
$newConfig[$sectionToMutate][$keyToMutate] = max(0, min(1.5, $newConfig[$sectionToMutate][$keyToMutate]));
|
|
284
|
+
$newConfig[$sectionToMutate][$keyToMutate] = max(0.05, min(1.5, $newConfig[$sectionToMutate][$keyToMutate] + (self::secureRandom() - 0.5) * 0.1));
|
|
285
|
+
} elseif ($sectionToMutate === 'thresholds') {
|
|
286
|
+
$newConfig[$sectionToMutate][$keyToMutate] = (int)round($newConfig[$sectionToMutate][$keyToMutate] + (self::secureRandom() - 0.5) * 5.0);
|
|
287
|
+
} else {
|
|
288
|
+
if ($keyToMutate === 'decayFactor') {
|
|
289
|
+
$newConfig['patterns']['decayFactor'] = max(0.8, min(0.999, $newConfig['patterns']['decayFactor'] + (self::secureRandom() - 0.5) * 0.05));
|
|
290
|
+
} elseif (str_contains($keyToMutate, 'Threshold') || str_contains($keyToMutate, 'Reset')) {
|
|
291
|
+
$newConfig['patterns'][$keyToMutate] = max(50.0, $newConfig['patterns'][$keyToMutate] + (self::secureRandom() - 0.5) * 50.0);
|
|
292
|
+
}
|
|
201
293
|
}
|
|
202
294
|
|
|
203
295
|
$refConfig = $currentConfigRef ?? $currentConfig;
|
|
@@ -35,6 +35,10 @@ class RequestContext
|
|
|
35
35
|
/** @var ?array<string, mixed> */
|
|
36
36
|
public ?array $resolvedIdentity = null;
|
|
37
37
|
|
|
38
|
+
public ?float $preCalculatedScore = null;
|
|
39
|
+
/** @var ?array<string, float> */
|
|
40
|
+
public ?array $preCalculatedVector = null;
|
|
41
|
+
|
|
38
42
|
// Propriétés spécifiques qui peuvent être fournies par un proxy inverse
|
|
39
43
|
public ?string $ja3 = null;
|
|
40
44
|
public ?string $ja4 = null;
|
package/src/php/Store/IStore.php
CHANGED
|
@@ -182,4 +182,16 @@ class MongoDbStore implements IStore
|
|
|
182
182
|
$this->lastReconnectAttempt = microtime(true);
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
+
|
|
186
|
+
public function clear(): void
|
|
187
|
+
{
|
|
188
|
+
$this->fallbackStore->clear();
|
|
189
|
+
$this->checkConnection();
|
|
190
|
+
if ($this->isDown) return;
|
|
191
|
+
try {
|
|
192
|
+
$this->collection->deleteMany([]);
|
|
193
|
+
} catch (\Throwable $e) {
|
|
194
|
+
$this->isDown = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
185
197
|
}
|
|
@@ -131,4 +131,16 @@ class RedisStore implements IStore
|
|
|
131
131
|
$this->lastReconnectAttempt = microtime(true);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
public function clear(): void
|
|
136
|
+
{
|
|
137
|
+
$this->fallbackStore->clear();
|
|
138
|
+
$this->checkConnection();
|
|
139
|
+
if ($this->isDown) return;
|
|
140
|
+
try {
|
|
141
|
+
$this->redis->flushdb();
|
|
142
|
+
} catch (\Throwable $e) {
|
|
143
|
+
$this->isDown = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
134
146
|
}
|
|
@@ -53,7 +53,9 @@ class ChallengeUtilsTest extends TestCase
|
|
|
53
53
|
|
|
54
54
|
// Tamper with value on verified sample index 12
|
|
55
55
|
$tampered = $solutions;
|
|
56
|
-
$
|
|
56
|
+
$sampleIndices = ChallengeUtils::deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
|
|
57
|
+
$tamperedIndex = $sampleIndices[0];
|
|
58
|
+
$tampered[$tamperedIndex] = (string)((float)$tampered[$tamperedIndex] + 0.0002); // Out of tolerance
|
|
57
59
|
$this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tampered)));
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -85,8 +87,10 @@ class ChallengeUtilsTest extends TestCase
|
|
|
85
87
|
}
|
|
86
88
|
$solutionString = implode(',', $solutions);
|
|
87
89
|
$this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $solutionString));
|
|
90
|
+
$sampleIndices = ChallengeUtils::deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
|
|
91
|
+
$tamperedIndex = $sampleIndices[0];
|
|
88
92
|
$tamperedSolutions = $solutions;
|
|
89
|
-
$tamperedSolutions[
|
|
93
|
+
$tamperedSolutions[$tamperedIndex] = (string)((float)$tamperedSolutions[$tamperedIndex] + 0.1);
|
|
90
94
|
$this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tamperedSolutions)));
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -228,6 +228,52 @@ class FingerprintEngineTest extends TestCase
|
|
|
228
228
|
$this->assertContains($decision['action'], ['challenge', 'block']);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
public function testAllowsWhitelistedIpWithNumericFilterBelowThreshold(): void
|
|
232
|
+
{
|
|
233
|
+
$config = SecurityProfiles::createSecurityProfile('strict', [
|
|
234
|
+
'whitelist' => [['type' => 'allowlist', 'entries' => ['10.0.0.1']]],
|
|
235
|
+
'filterWhitelist' => 50.0,
|
|
236
|
+
'weights' => ['headerAnomalyScore' => 1.0],
|
|
237
|
+
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95]
|
|
238
|
+
]);
|
|
239
|
+
$engine = new FingerprintEngine($config);
|
|
240
|
+
|
|
241
|
+
$context = $this->createRequestContext([
|
|
242
|
+
'clientIp' => '10.0.0.1',
|
|
243
|
+
'headers' => [
|
|
244
|
+
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
|
|
245
|
+
'accept-language' => 'en-US,en;q=0.9'
|
|
246
|
+
]
|
|
247
|
+
]);
|
|
248
|
+
$decision = $engine->processRequest($context);
|
|
249
|
+
|
|
250
|
+
$this->assertEquals('next', $decision['action']);
|
|
251
|
+
$this->assertEquals(0, $decision['score']);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public function testAllowsWhitelistedIpWithNumericFilterExceedingThreshold(): void
|
|
255
|
+
{
|
|
256
|
+
$config = SecurityProfiles::createSecurityProfile('strict', [
|
|
257
|
+
'whitelist' => [['type' => 'allowlist', 'entries' => ['10.0.0.1']]],
|
|
258
|
+
'filterWhitelist' => 20.0,
|
|
259
|
+
'weights' => ['headerAnomalyScore' => 1.0],
|
|
260
|
+
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95]
|
|
261
|
+
]);
|
|
262
|
+
$engine = new FingerprintEngine($config);
|
|
263
|
+
|
|
264
|
+
$context = $this->createRequestContext([
|
|
265
|
+
'clientIp' => '10.0.0.1',
|
|
266
|
+
'headers' => [
|
|
267
|
+
'user-agent' => 'Chrome',
|
|
268
|
+
'accept-language' => 'en-US,en;q=0.9'
|
|
269
|
+
]
|
|
270
|
+
]);
|
|
271
|
+
$decision = $engine->processRequest($context);
|
|
272
|
+
|
|
273
|
+
$this->assertEquals('challenge', $decision['action']);
|
|
274
|
+
$this->assertEquals(60, $decision['score']);
|
|
275
|
+
}
|
|
276
|
+
|
|
231
277
|
public function testAllowsWhitelistedIp(): void
|
|
232
278
|
{
|
|
233
279
|
$config = SecurityProfiles::createSecurityProfile('strict', [
|
|
@@ -25,6 +25,7 @@ class IpReputationTest extends TestCase
|
|
|
25
25
|
public function set(string $key, $value, ?int $ttl = null): void { $this->data[$key] = $value; }
|
|
26
26
|
public function has(string $key): bool { return isset($this->data[$key]); }
|
|
27
27
|
public function delete(string $key): void { unset($this->data[$key]); }
|
|
28
|
+
public function clear(): void { $this->data = []; }
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
StoreManager::setStore($this->store);
|
|
@@ -1482,20 +1482,20 @@ class RequestUtils
|
|
|
1482
1482
|
return [];
|
|
1483
1483
|
}
|
|
1484
1484
|
|
|
1485
|
-
$
|
|
1486
|
-
$
|
|
1485
|
+
$rawLogs = $trafficData;
|
|
1486
|
+
$totalCount = count($rawLogs);
|
|
1487
|
+
|
|
1488
|
+
$maxLogsPerDevice = max(3, (int)floor($totalCount * 0.02));
|
|
1489
|
+
$maxLogsPerIp = max(3, (int)floor($totalCount * 0.02));
|
|
1490
|
+
$maxLogsPerSubnet = max(5, (int)floor($totalCount * 0.05));
|
|
1491
|
+
$maxLogsPerHardwareCluster = max(3, (int)floor($totalCount * 0.02));
|
|
1492
|
+
|
|
1487
1493
|
$deviceCounts = [];
|
|
1488
1494
|
$ipCounts = [];
|
|
1489
1495
|
$subnetCounts = [];
|
|
1490
1496
|
$hardwareClusterCounts = [];
|
|
1491
1497
|
$hwClusterCache = [];
|
|
1492
1498
|
|
|
1493
|
-
$totalCount = count($trafficData);
|
|
1494
|
-
$maxLogsPerDevice = max(3, (int)floor($totalCount * 0.02)); // Max 2% contribution per device
|
|
1495
|
-
$maxLogsPerIp = max(3, (int)floor($totalCount * 0.02)); // Max 2% par adresse IP individuelle
|
|
1496
|
-
$maxLogsPerSubnet = max(5, (int)floor($totalCount * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
|
|
1497
|
-
$maxLogsPerHardwareCluster = max(3, (int)floor($totalCount * 0.02)); // Max 2% par cluster matériel stable
|
|
1498
|
-
|
|
1499
1499
|
$getHardwareCluster = function (array $log) use (&$hwClusterCache): string {
|
|
1500
1500
|
$fp = $log['deviceHash'] ?? $log['fingerprint'] ?? $log['deviceFingerprint'] ?? '';
|
|
1501
1501
|
if (!empty($fp) && is_string($fp)) {
|
|
@@ -1522,7 +1522,40 @@ class RequestUtils
|
|
|
1522
1522
|
return $log['deviceId'] ?? 'anonymous-cluster';
|
|
1523
1523
|
};
|
|
1524
1524
|
|
|
1525
|
-
|
|
1525
|
+
// --- REVOLUTION : Compression de Cohorte par Densité Vectorielle (Anti-Sybil) ---
|
|
1526
|
+
$getVectorDistance = function (array $v1, array $v2): float {
|
|
1527
|
+
$sum = 0.0;
|
|
1528
|
+
$keys = array_unique(array_merge(array_keys($v1), array_keys($v2)));
|
|
1529
|
+
foreach ($keys as $key) {
|
|
1530
|
+
$sum += pow((float)($v1[$key] ?? 0.0) - (float)($v2[$key] ?? 0.0), 2);
|
|
1531
|
+
}
|
|
1532
|
+
return sqrt($sum);
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
$clusteredLogs = [];
|
|
1536
|
+
foreach ($rawLogs as $log) {
|
|
1537
|
+
$matchedClusterKey = null;
|
|
1538
|
+
foreach ($clusteredLogs as $key => $cluster) {
|
|
1539
|
+
if (($log['type'] ?? '') === ($cluster['type'] ?? '') && $getVectorDistance($log['vector'] ?? [], $cluster['vector'] ?? []) < 5.0) {
|
|
1540
|
+
$matchedClusterKey = $key;
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if ($matchedClusterKey !== null) {
|
|
1545
|
+
$clusteredLogs[$matchedClusterKey]['instancesCount'] = ($clusteredLogs[$matchedClusterKey]['instancesCount'] ?? 1) + 1;
|
|
1546
|
+
$clusteredLogs[$matchedClusterKey]['weight'] = 1.0 + log((float)$clusteredLogs[$matchedClusterKey]['instancesCount']);
|
|
1547
|
+
} else {
|
|
1548
|
+
$logCopy = $log;
|
|
1549
|
+
$logCopy['instancesCount'] = 1;
|
|
1550
|
+
$logCopy['weight'] = 1.0;
|
|
1551
|
+
$clusteredLogs[] = $logCopy;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
$suspiciousLogs = [];
|
|
1556
|
+
$passedLogs = [];
|
|
1557
|
+
|
|
1558
|
+
foreach ($clusteredLogs as $log) {
|
|
1526
1559
|
$devId = $log['deviceId'] ?? 'anonymous';
|
|
1527
1560
|
$ip = $log['clientIp'] ?? $log['ip'] ?? 'unknown';
|
|
1528
1561
|
$subnet = self::getIpSubnet($ip) ?? 'unknown-subnet';
|