@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.
@@ -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
- $falsePositives = 0;
77
- $falseNegatives = 0;
78
- $totalHumans = 0;
79
- $totalBots = 0;
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
- $confidence = $confidenceWeights[$log['type']] ?? 1.0;
99
- $isLikelyBot = in_array($log['type'], ['challenge_issued', 'request_blocked', 'trap_triggered']);
100
- $isLikelyHuman = in_array($log['type'], ['request_passed', 'challenge_solved']);
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
- $totalBots += $confidence;
104
- $score = $calculateScore($log);
105
- if ($score < $config['thresholds']['low']) {
106
- $falseNegatives += $confidence;
107
- }
144
+ $minBotScore = min($minBotScore, $score);
108
145
  } elseif ($isLikelyHuman) {
109
- $totalHumans += $confidence;
110
- $score = $calculateScore($log);
111
- if ($score >= $config['thresholds']['low']) {
112
- $falsePositives += $confidence;
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
- $falsePositiveRate = $totalHumans > 0 ? $falsePositives / $totalHumans : 0;
118
- $falseNegativeRate = $totalBots > 0 ? $falseNegatives / $totalBots : 0;
174
+ $weightedFpr = 0.0;
175
+ $weightedFnr = 0.0;
176
+ $totalImportance = 0.0;
119
177
 
120
- return [$falsePositiveRate, $falseNegativeRate];
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
- if ($key !== 'honeypotScore') {
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) && $k !== 'honeypotScore') {
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.5],
177
- ['name' => 'weights', 'weight' => 0.35],
178
- ['name' => 'thresholds', 'weight' => 0.15]
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;
@@ -39,4 +39,10 @@ interface IStore
39
39
  * @return void
40
40
  */
41
41
  public function delete(string $key): void;
42
+
43
+ /**
44
+ * Supprime toutes les clés du stockage.
45
+ * @return void
46
+ */
47
+ public function clear(): void;
42
48
  }
@@ -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
- $tampered[12] = (string)((float)$tampered[12] + 0.0002); // Out of tolerance
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[0] = (string)((float)$tamperedSolutions[0] + 0.1);
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
- $suspiciousLogs = [];
1486
- $passedLogs = [];
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
- foreach ($trafficData as $log) {
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';