@anonympins/fingerprint 0.6.3 → 0.6.4
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 +11 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +6944 -6802
- package/src/js/library.js +113 -43
- 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/php/Challenge/ChallengeUtils.php +2 -2
- package/src/php/Config/SecurityProfiles.php +5 -0
- package/src/php/FingerprintEngine.php +100 -23
- package/src/php/Optimization/OptimizationOperators.php +39 -15
- 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/FingerprintEngineTest.php +46 -0
- package/src/php/Tests/IpReputationTest.php +1 -0
- package/src/php/Utils/RequestUtils.php +42 -9
|
@@ -237,6 +237,9 @@
|
|
|
237
237
|
$this->logger = isset($securityConfig['logger']) && is_callable($securityConfig['logger']) ? new Logger($securityConfig['logger']) : null;
|
|
238
238
|
$this->dryRun = $securityConfig['dryRun'] ?? false;
|
|
239
239
|
$this->validateConfig($securityConfig);
|
|
240
|
+
if ($this->securityConfig['reset'] ?? false) {
|
|
241
|
+
$this->resetStore();
|
|
242
|
+
}
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
/**
|
|
@@ -252,6 +255,18 @@
|
|
|
252
255
|
$this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
|
|
253
256
|
}
|
|
254
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Réinitialise le store de persistance actif.
|
|
260
|
+
*/
|
|
261
|
+
public function resetStore(): void
|
|
262
|
+
{
|
|
263
|
+
$store = StoreManager::getStore();
|
|
264
|
+
if (method_exists($store, 'clear')) {
|
|
265
|
+
$store->clear();
|
|
266
|
+
}
|
|
267
|
+
$this->log('Store has been reset/cleared.');
|
|
268
|
+
}
|
|
269
|
+
|
|
255
270
|
private function validateConfig(array $config): void
|
|
256
271
|
{
|
|
257
272
|
if (empty($config)) {
|
|
@@ -266,7 +281,7 @@
|
|
|
266
281
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
267
282
|
'similarityThreshold', 'summary', 'description',
|
|
268
283
|
'ed25519_private_key', 'ed25519_public_key',
|
|
269
|
-
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret'
|
|
284
|
+
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset'
|
|
270
285
|
];
|
|
271
286
|
|
|
272
287
|
if (empty($config['weights'])) {
|
|
@@ -497,28 +512,50 @@
|
|
|
497
512
|
*/
|
|
498
513
|
private function checkAllowlists(RequestContext $context): bool
|
|
499
514
|
{
|
|
515
|
+
$whitelisted = false;
|
|
516
|
+
$type = '';
|
|
517
|
+
|
|
500
518
|
if ($this->isIpInAllowlist($context->clientIp)) {
|
|
501
|
-
$
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
$
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
$
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if ($
|
|
518
|
-
$this->
|
|
519
|
+
$whitelisted = true;
|
|
520
|
+
$type = 'allowlist';
|
|
521
|
+
} elseif ($this->isPathInAllowlist($context->path)) {
|
|
522
|
+
$whitelisted = true;
|
|
523
|
+
$type = 'path_allowlist';
|
|
524
|
+
} elseif ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
|
|
525
|
+
$whitelisted = true;
|
|
526
|
+
$type = 'host_path_allowlist';
|
|
527
|
+
} elseif ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
|
|
528
|
+
$whitelisted = true;
|
|
529
|
+
$type = 'graphql_operation_allowlist';
|
|
530
|
+
} elseif ($this->verifyWhitelistedBot($context)) {
|
|
531
|
+
$whitelisted = true;
|
|
532
|
+
$type = 'bot';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if ($whitelisted) {
|
|
536
|
+
$filterWhitelist = $this->securityConfig['filterWhitelist'] ?? false;
|
|
537
|
+
$bypassWhitelist = false;
|
|
538
|
+
if ($filterWhitelist === true) {
|
|
539
|
+
$bypassWhitelist = $this->hasCertainAttack($context);
|
|
540
|
+
} elseif (is_numeric($filterWhitelist)) {
|
|
541
|
+
if ($context->preCalculatedScore === null) {
|
|
542
|
+
$suspicionVector = [];
|
|
543
|
+
$context->preCalculatedVector = $this->getSuspicionVector($context, $suspicionVector);
|
|
544
|
+
$context->preCalculatedScore = $this->calculateFinalScore($context->preCalculatedVector);
|
|
545
|
+
}
|
|
546
|
+
if ($context->preCalculatedScore > $filterWhitelist) {
|
|
547
|
+
$bypassWhitelist = true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if ($bypassWhitelist) {
|
|
552
|
+
$this->log('Whitelisted request exceeds filter threshold - bypassing whitelist bypass', ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
$this->log("IP/Path in allowlist ({$type}) - allowing request", ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
519
556
|
return true;
|
|
520
557
|
}
|
|
521
|
-
|
|
558
|
+
|
|
522
559
|
return false;
|
|
523
560
|
}
|
|
524
561
|
|
|
@@ -908,6 +945,23 @@
|
|
|
908
945
|
}
|
|
909
946
|
}
|
|
910
947
|
|
|
948
|
+
/**
|
|
949
|
+
* Évalue si la requête présente des caractéristiques d'attaque flagrantes ou a déclenché un honeypot.
|
|
950
|
+
*/
|
|
951
|
+
private function hasCertainAttack(RequestContext $context): bool
|
|
952
|
+
{
|
|
953
|
+
$honeypotConfig = $this->securityConfig['honeypot'] ?? [];
|
|
954
|
+
$honeypot = RequestUtils::getHoneypotScore($context, $honeypotConfig);
|
|
955
|
+
if (($honeypot['honeypotScore'] ?? 0.0) >= 100.0) {
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
$bot = RequestUtils::getBotScore($context);
|
|
959
|
+
if (($bot['botScore'] ?? 0.0) >= 100.0) {
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
911
965
|
/**
|
|
912
966
|
* Traite une requête entrante et retourne une décision.
|
|
913
967
|
* @param RequestContext $context Le contexte de la requête.
|
|
@@ -1032,6 +1086,28 @@
|
|
|
1032
1086
|
$problemManager = \Anonympins\Fingerprint\ProblemManager::getInstance($configPath, $store);
|
|
1033
1087
|
// FIX: La solution est directement le $workResult, pas une sous-propriété.
|
|
1034
1088
|
$problemManager->integrateSolution($problemId, $workResult);
|
|
1089
|
+
|
|
1090
|
+
// Si le problème résolu est l'auto-tuning de sécurité et que l'auto-tuning est activé,
|
|
1091
|
+
// on applique directement la meilleure solution calculée au moteur en direct.
|
|
1092
|
+
if ($problemId === 'security_auto_tuning' && ($this->securityConfig['autotuning']['enabled'] ?? false)) {
|
|
1093
|
+
$paretoFront = $workResult['paretoFront'] ?? null;
|
|
1094
|
+
if (is_array($paretoFront) && !empty($paretoFront)) {
|
|
1095
|
+
$bestSolution = $paretoFront[0];
|
|
1096
|
+
$minDistance = sqrt(pow((float)$bestSolution['objectives'][0], 2) + pow((float)$bestSolution['objectives'][1], 2));
|
|
1097
|
+
for ($i = 1; $i < count($paretoFront); $i++) {
|
|
1098
|
+
$distance = sqrt(pow((float)$paretoFront[$i]['objectives'][0], 2) + pow((float)$paretoFront[$i]['objectives'][1], 2));
|
|
1099
|
+
if ($distance < $minDistance) {
|
|
1100
|
+
$minDistance = $distance;
|
|
1101
|
+
$bestSolution = $paretoFront[$i];
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (isset($bestSolution['solution'])) {
|
|
1105
|
+
$this->updateConfig($bestSolution['solution']);
|
|
1106
|
+
$this->log('Useful Work auto-tuning applied successfully to live config.');
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1035
1111
|
$isValid = true;
|
|
1036
1112
|
// FIX: Générer un vrai ticket pour uPoW, comme pour un PoW normal.
|
|
1037
1113
|
$ticketTtl = $this->securityConfig['ticketMaxAge'] ?? 3600000; // 1 heure par défaut
|
|
@@ -1113,8 +1189,8 @@
|
|
|
1113
1189
|
return $decision;
|
|
1114
1190
|
}
|
|
1115
1191
|
|
|
1116
|
-
$suspicionVector = $this->getSuspicionVector($context, $suspicionVector);
|
|
1117
|
-
$finalScore = $this->calculateFinalScore($suspicionVector);
|
|
1192
|
+
$suspicionVector = $context->preCalculatedVector ?? $this->getSuspicionVector($context, $suspicionVector);
|
|
1193
|
+
$finalScore = $context->preCalculatedScore ?? $this->calculateFinalScore($suspicionVector);
|
|
1118
1194
|
$this->log('Suspicion vector and final score calculated', [
|
|
1119
1195
|
'finalScore' => round($finalScore, 2),
|
|
1120
1196
|
'vector' => $suspicionVector
|
|
@@ -1354,7 +1430,8 @@
|
|
|
1354
1430
|
$memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
|
|
1355
1431
|
|
|
1356
1432
|
$originalFingerprint = RequestUtils::getCompositeDeviceHash($context);
|
|
1357
|
-
$
|
|
1433
|
+
$tlsSessionId = $context->tlsSessionId ?? '';
|
|
1434
|
+
$baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint, $context->clientIp, $tlsSessionId);
|
|
1358
1435
|
|
|
1359
1436
|
$challengeContext = [
|
|
1360
1437
|
'clientSecret' => $clientSecret,
|
|
@@ -70,18 +70,22 @@ 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
|
-
$falsePositives = 0;
|
|
77
|
-
$falseNegatives = 0;
|
|
78
|
-
$totalHumans = 0;
|
|
79
|
-
$totalBots = 0;
|
|
76
|
+
return function (array $config) use ($trafficData, $currentConfig): array {
|
|
77
|
+
$falsePositives = 0.0;
|
|
78
|
+
$falseNegatives = 0.0;
|
|
79
|
+
$totalHumans = 0.0;
|
|
80
|
+
$totalBots = 0.0;
|
|
81
|
+
|
|
82
|
+
$maxHumanScore = 0.0;
|
|
83
|
+
$minBotScore = 100.0;
|
|
80
84
|
|
|
81
85
|
$calculateScore = function (array $log) use ($config): float {
|
|
82
86
|
$score = 0.0;
|
|
83
87
|
foreach ($config['weights'] as $key => $weight) {
|
|
84
|
-
$score += ($log['vector'][$key] ?? 0) * $weight;
|
|
88
|
+
$score += ($log['vector'][$key] ?? 0.0) * $weight;
|
|
85
89
|
}
|
|
86
90
|
return $score;
|
|
87
91
|
};
|
|
@@ -94,30 +98,50 @@ class OptimizationOperators
|
|
|
94
98
|
'trap_triggered' => 2.0,
|
|
95
99
|
];
|
|
96
100
|
|
|
101
|
+
// 1. Évaluation sur les logs de trafic réels
|
|
97
102
|
foreach ($trafficData as $log) {
|
|
98
|
-
$
|
|
99
|
-
$
|
|
100
|
-
$
|
|
103
|
+
$weight = (float)($log['weight'] ?? 1.0);
|
|
104
|
+
$confidence = ($confidenceWeights[$log['type'] ?? ''] ?? 1.0) * $weight;
|
|
105
|
+
$isLikelyBot = in_array($log['type'] ?? '', ['challenge_issued', 'request_blocked', 'trap_triggered']);
|
|
106
|
+
$isLikelyHuman = in_array($log['type'] ?? '', ['request_passed', 'challenge_solved']);
|
|
107
|
+
|
|
108
|
+
$score = $calculateScore($log);
|
|
101
109
|
|
|
102
110
|
if ($isLikelyBot) {
|
|
103
111
|
$totalBots += $confidence;
|
|
104
|
-
$
|
|
112
|
+
$minBotScore = min($minBotScore, $score);
|
|
105
113
|
if ($score < $config['thresholds']['low']) {
|
|
106
114
|
$falseNegatives += $confidence;
|
|
107
115
|
}
|
|
108
116
|
} elseif ($isLikelyHuman) {
|
|
109
117
|
$totalHumans += $confidence;
|
|
110
|
-
$
|
|
118
|
+
$maxHumanScore = max($maxHumanScore, $score);
|
|
111
119
|
if ($score >= $config['thresholds']['low']) {
|
|
112
120
|
$falsePositives += $confidence;
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
$falsePositiveRate = $totalHumans > 0 ? $falsePositives / $totalHumans : 0;
|
|
118
|
-
$falseNegativeRate = $totalBots > 0 ? $falseNegatives / $totalBots : 0;
|
|
125
|
+
$falsePositiveRate = $totalHumans > 0.0 ? $falsePositives / $totalHumans : 0.0;
|
|
126
|
+
$falseNegativeRate = $totalBots > 0.0 ? $falseNegatives / $totalBots : 0.0;
|
|
127
|
+
|
|
128
|
+
// 3. Pénalité de dérive d'échelle (L2 Regularization par rapport au profil d'origine)
|
|
129
|
+
$regularizationPenalty = 0.0;
|
|
130
|
+
if ($currentConfig && isset($currentConfig['weights'])) {
|
|
131
|
+
foreach ($config['weights'] as $key => $val) {
|
|
132
|
+
$originalVal = (float)($currentConfig['weights'][$key] ?? 0.0);
|
|
133
|
+
$regularizationPenalty += pow((float)$val - $originalVal, 2);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 4. Maximisation de la marge
|
|
138
|
+
$marginOverlap = max(0.0, $maxHumanScore - $minBotScore);
|
|
139
|
+
$marginPenalty = $marginOverlap / 100.0;
|
|
140
|
+
|
|
141
|
+
$obj1 = $falsePositiveRate + ($regularizationPenalty * 0.05);
|
|
142
|
+
$obj2 = $falseNegativeRate + $marginPenalty;
|
|
119
143
|
|
|
120
|
-
return [$
|
|
144
|
+
return [$obj1, $obj2];
|
|
121
145
|
};
|
|
122
146
|
}
|
|
123
147
|
|
|
@@ -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
|
}
|
|
@@ -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';
|