@anonympins/fingerprint 0.7.2 → 0.7.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +36 -9
  2. package/README.md +1 -1
  3. package/package.json +109 -107
  4. package/public/fingerprint-anti-bot.zip +0 -0
  5. package/src/js/fingerprint.client.js +252 -45
  6. package/src/js/fingerprint.client.obfuscated.js +1 -1
  7. package/src/js/fingerprint.js +265 -185
  8. package/src/js/fingerprint.utils.js +32 -5
  9. package/src/js/fingerprint.worker.js +81 -0
  10. package/src/js/library.js +6 -2
  11. package/src/js/pow.solver.js +2 -3
  12. package/src/js/problem-manager.js +113 -113
  13. package/src/js/tests/fingerprint.client.init.test.js +30 -0
  14. package/src/js/tests/fingerprint.test.js +136 -4
  15. package/src/js/tests/threatIntel.test.js +76 -0
  16. package/src/php/AutoTuner.php +36 -18
  17. package/src/php/Challenge/ChallengeUtils.php +121 -240
  18. package/src/php/DirectFingerprint.php +16 -4
  19. package/src/php/FingerprintClient.php +17 -19
  20. package/src/php/FingerprintEngine.php +77 -39
  21. package/src/php/Ja3AnomalyDetector.php +20 -12
  22. package/src/php/Optimization/Optimization.php +5 -1
  23. package/src/php/Optimization/OptimizationOperators.php +5 -1
  24. package/src/php/Optimization/ProblemInitializers.php +16 -4
  25. package/src/php/ProblemManager.php +20 -14
  26. package/src/php/Tests/ChallengeUtilsTest.php +469 -453
  27. package/src/php/Tests/FingerprintEngineTest.php +6 -1
  28. package/src/php/Tests/RequestUtilsTest.php +1 -1
  29. package/src/php/Tests/ThreatIntelTest.php +64 -0
  30. package/src/php/Utils/Env.php +51 -49
  31. package/src/php/Utils/MaliciousPatterns.php +74 -74
  32. package/src/php/Utils/RequestUtils.php +231 -132
  33. package/src/php/WordPress/WpDbStore.php +4 -0
  34. package/src/php/WordPress/{fingerprint-wordpress.php → fingerprint-anti-bot.php} +131 -126
  35. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  36. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
  37. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  38. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
  39. package/src/php/WordPress/package.php +6 -3
  40. package/src/php/WordPress/readme.txt +36 -0
  41. package/public/fingerprint-wordpress.zip +0 -0
@@ -20,6 +20,7 @@ import {
20
20
  modPow,
21
21
  hashNetwork,
22
22
  normalizeReferer,
23
+ isLoopbackIp,
23
24
  isPrivateIp,
24
25
  parseUserAgent,
25
26
  safeJsonStringify
@@ -157,25 +158,12 @@ const loadBotWhitelist = (filename, fallbackEntries) => {
157
158
  };
158
159
 
159
160
  const googlebotEntries = loadBotWhitelist('googlebot.json', [
160
- "2001:4860:4801:10::/64",
161
- "2001:4860:4801:11::/64",
162
- "2001:4860:4801:12::/64",
163
- // ... [Keep fallback inline values for safety]
164
- "66.249.79.64"
165
161
  ]);
166
162
 
167
163
  const bingbotEntries = loadBotWhitelist('bingbot.json', [
168
- "157.55.39.0/24",
169
- "207.46.13.0/24",
170
- // ... [Keep fallback inline values for safety]
171
- "40.77.178.0/23"
172
164
  ]);
173
165
 
174
166
  const yandexEntries = loadBotWhitelist('yandex.json', [
175
- "2a02:6b8::/29",
176
- "5.45.192.0/18",
177
- // ... [Keep fallback inline values for safety]
178
- "213.180.192.0/19"
179
167
  ]);
180
168
 
181
169
  function generateSessionMapping() {
@@ -360,7 +348,7 @@ export function generateStatelessTicket(payload) {
360
348
  * @param {object} context - Le contexte de la requête.
361
349
  * @returns {{protocolAnomalyScore: number}}
362
350
  */
363
- function getProtocolAnomalyScore(context) {
351
+ export function getProtocolAnomalyScore(context) {
364
352
  let http2Anomaly = 0.0;
365
353
  let quicAnomaly = 0.0;
366
354
 
@@ -373,9 +361,10 @@ function getProtocolAnomalyScore(context) {
373
361
  const h2Fp = context.headers?.['x-http2-fingerprint'] || context.http2Fingerprint || null;
374
362
  if (h2Fp && typeof h2Fp === 'string') {
375
363
  const parts = h2Fp.split('|');
376
- if (parts.length >= 4) {
364
+ if (parts.length >= 3) {
377
365
  const connWindow = parseInt(parts[1], 10);
378
- const headerOrder = parts[3];
366
+ const streamPriority = parts[2] || '';
367
+ const headerOrder = parts.length > 3 ? parts[3] : '';
379
368
  const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
380
369
  const isFirefox = browser.startsWith('Firefox');
381
370
  const isSafari = browser.startsWith('Safari');
@@ -383,11 +372,43 @@ function getProtocolAnomalyScore(context) {
383
372
  if (isChromium) {
384
373
  if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
385
374
  if (connWindow === 65535 || connWindow === 65536) http2Anomaly += 40.0;
375
+ if (streamPriority === '0' || streamPriority === '') http2Anomaly += 50.0;
386
376
  } else if (isFirefox) {
387
377
  if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
388
378
  } else if (isSafari) {
389
379
  if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
390
380
  }
381
+
382
+ // Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
383
+ if (parts.length >= 5) {
384
+ const frameCountsStr = parts[4];
385
+ const frameCounts = {};
386
+ frameCountsStr.split(',').forEach(item => {
387
+ const kv = item.split(':');
388
+ if (kv.length === 2) {
389
+ frameCounts[kv[0]] = parseInt(kv[1], 10);
390
+ }
391
+ });
392
+
393
+ const priorityCount = frameCounts.p || 0;
394
+ const windowUpdateCount = frameCounts.w || 0;
395
+ const continuationCount = frameCounts.c || 0;
396
+
397
+ if (isChromium) {
398
+ // Chrome envoie des trames PRIORITY pour l'arbre de dépendances
399
+ if (priorityCount === 0) http2Anomaly += 25.0;
400
+ // Chrome est agressif avec les WINDOW_UPDATE
401
+ if (windowUpdateCount < 2) http2Anomaly += 20.0;
402
+ } else if (isFirefox) {
403
+ // Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
404
+ if (priorityCount > 1) http2Anomaly += 20.0;
405
+ }
406
+
407
+ // Les bots génériques n'envoient souvent pas de trames CONTINUATION pour les en-têtes longs
408
+ if (continuationCount === 0 && (headerOrder.match(/,/g) || []).length > 3) {
409
+ http2Anomaly += 30.0;
410
+ }
411
+ }
391
412
  }
392
413
  }
393
414
 
@@ -489,7 +510,7 @@ function getProtocolAnomalyScore(context) {
489
510
  */
490
511
  function getRenderingAnomalyScore(context) {
491
512
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
492
- if (!behaviorHeader) {
513
+ if (!behaviorHeader || !isNaN(Number(behaviorHeader))) {
493
514
  return { renderingAnomalyScore: 0.0 };
494
515
  }
495
516
  try {
@@ -579,33 +600,45 @@ export function parseStatelessTicket(ticket) {
579
600
  }
580
601
 
581
602
  /**
582
- * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
603
+ * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau par domaine.
583
604
  * @param {string} clientIp - L'adresse IP du client.
605
+ * @param {string} [domain='default'] - Le domaine/hôte ciblé (en-tête Host).
606
+ * @param {object} [rateLimitConfig] - Configuration optionnelle du limiteur.
584
607
  * @returns {Promise<boolean>} True si la requête est autorisée, false si elle est limitée.
585
608
  */
586
- async function checkChallengeRateLimit(clientIp) {
587
- const subnet = getIpSubnet(clientIp);
609
+ async function checkChallengeRateLimit(clientIp, domain = 'default', rateLimitConfig = {}) {
610
+ if (rateLimitConfig && rateLimitConfig.enabled === false) {
611
+ return true;
612
+ }
613
+ if (!clientIp || isLoopbackIp(clientIp)) {
614
+ return true; // Bypass local/dev
615
+ }
616
+
617
+ const subnet = getIpSubnet(clientIp) || clientIp;
588
618
  if (!subnet) return false;
589
619
 
590
- const key = `rate-limit:${subnet}`;
591
- const rateLimitData = (await store.get(key)) || {
592
- tokens: 5.0,
593
- lastRefill: Date.now() / 1000
594
- };
620
+ const host = (domain || 'default').toLowerCase().split(':')[0];
621
+ const key = `rate-limit:${host}:${subnet}`;
595
622
 
596
- const capacity = 5.0;
597
- const refillRate = 0.1; // 1 token toutes les 10 secondes
623
+ const capacity = Number(rateLimitConfig?.capacity ?? 30.0);
624
+ const refillRate = Number(rateLimitConfig?.refillRate ?? 1.0); // 1 token par seconde (au lieu de 0.1)
598
625
  const now = Date.now() / 1000;
599
626
 
600
- const elapsed = now - rateLimitData.lastRefill;
627
+ const rateLimitData = (await store.get(key)) || {
628
+ tokens: capacity,
629
+ lastRefill: now
630
+ };
631
+
632
+ const elapsed = Math.max(0, now - rateLimitData.lastRefill);
601
633
  const tokens = Math.min(capacity, rateLimitData.tokens + elapsed * refillRate);
634
+ const ttl = Math.max(60, Math.ceil(capacity / Math.max(0.1, refillRate)));
602
635
 
603
636
  if (tokens < 1.0) {
604
- await store.set(key, { tokens, lastRefill: now }, 60);
637
+ await store.set(key, { tokens, lastRefill: now }, ttl);
605
638
  return false;
606
639
  }
607
640
 
608
- await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, 60);
641
+ await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, ttl);
609
642
  return true;
610
643
  }
611
644
 
@@ -655,18 +688,22 @@ const securityProfiles = {
655
688
  headerAnomalyScore: 0.1,
656
689
  requestPatternScore: 0.6,
657
690
  inconsistencyScore: 0.8,
658
- behaviorScore: 0.7, // Poids pour les métriques comportementales (souris, clavier)
691
+ behaviorScore: 0.7,
659
692
  honeypotScore: 1.0,
693
+ botScore: 1.0,
694
+ cookieDroppingScore: 0.9,
660
695
  crossLayerInconsistencyScore: 0.4,
661
696
  timeInconsistencyScore: 0.9,
662
- tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
663
- subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
664
- ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
665
- botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
666
- tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
697
+ tlsSpoofingScore: 0.8,
698
+ clientHintsInconsistencyScore: 0.7,
699
+ clickVarianceScore: 0.6,
700
+ subnetScore: 0.4,
701
+ ipReputationScore: 0.5,
702
+ botnetClusterScore: 0.6,
703
+ tcpAnomalyScore: 0.8,
667
704
  quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
668
- protocolAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
669
705
  renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
706
+ http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
670
707
  threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
671
708
  virtualizationScore: 0.8,
672
709
  },
@@ -702,15 +739,22 @@ const securityProfiles = {
702
739
  inconsistencyScore: 1.0,
703
740
  behaviorScore: 0.8,
704
741
  honeypotScore: 1.0,
742
+ botScore: 1.0,
743
+ cookieDroppingScore: 1.0,
705
744
  crossLayerInconsistencyScore: 0.6,
706
745
  timeInconsistencyScore: 1.0,
707
- tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
746
+ tlsSpoofingScore: 1.0,
747
+ clientHintsInconsistencyScore: 0.9,
748
+ clickVarianceScore: 0.7,
708
749
  subnetScore: 0.5,
709
- ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
710
- botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
711
- renderingAnomalyScore: 1.0, // NOUVEAU: Poids pour l'anomalie de rendu
712
- protocolAnomalyScore: 1.0, // NOUVEAU
713
- threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
750
+ ipReputationScore: 0.6,
751
+ botnetClusterScore: 0.8,
752
+ tcpAnomalyScore: 1.0,
753
+ quicAnomalyScore: 1.0,
754
+ renderingAnomalyScore: 1.0,
755
+ http2AnomalyScore: 1.0,
756
+ threatIntelScore: 1.0,
757
+ virtualizationScore: 1.0,
714
758
  },
715
759
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
716
760
  patterns: {
@@ -745,15 +789,22 @@ const securityProfiles = {
745
789
  inconsistencyScore: 0.7,
746
790
  behaviorScore: 0.2, // Lower weight, as browser behavior is not applicable
747
791
  honeypotScore: 1.0,
792
+ botScore: 0.8,
793
+ cookieDroppingScore: 0.8,
748
794
  crossLayerInconsistencyScore: 0.5,
749
795
  timeInconsistencyScore: 0.8,
750
- tlsSpoofingScore: 0.7, // Important pour les API
796
+ tlsSpoofingScore: 0.7,
797
+ clientHintsInconsistencyScore: 0.6,
798
+ clickVarianceScore: 0.3,
751
799
  subnetScore: 0.4,
752
- ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
753
- botnetClusterScore: 0.7, // NOUVEAU: Poids pour le clustering botnet
754
- tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
755
- protocolAnomalyScore: 0.8,
756
- quicAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie QUIC
800
+ ipReputationScore: 0.5,
801
+ botnetClusterScore: 0.7,
802
+ tcpAnomalyScore: 0.8,
803
+ quicAnomalyScore: 0.8,
804
+ http2AnomalyScore: 0.8,
805
+ renderingAnomalyScore: 0.2,
806
+ threatIntelScore: 0.6,
807
+ virtualizationScore: 0.8,
757
808
  },
758
809
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
759
810
  patterns: {
@@ -800,9 +851,10 @@ const securityProfiles = {
800
851
  ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
801
852
  botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
802
853
  tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
803
- protocolAnomalyScore: 0.5,
804
854
  quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
855
+ http2AnomalyScore: 0.5,
805
856
  renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
857
+ virtualizationScore: 0.8,
806
858
  },
807
859
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
808
860
  patterns: {
@@ -831,7 +883,6 @@ const securityProfiles = {
831
883
  historyScore: 0.4,
832
884
  rotationScore: 0.6,
833
885
  headerAnomalyScore: 0.2,
834
- // Utilisation d'un score de pattern unifié avec un poids très élevé
835
886
  requestPatternScore: 0.9,
836
887
  inconsistencyScore: 1.0, // Crucial for preventing account takeover
837
888
  behaviorScore: 0.8, // Important for checkout/login forms
@@ -848,9 +899,10 @@ const securityProfiles = {
848
899
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
849
900
  botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
850
901
  tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
851
- protocolAnomalyScore: 0.9,
852
902
  quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
853
- renderingAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie de rendu,threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
903
+ http2AnomalyScore: 0.9,
904
+ renderingAnomalyScore: 0.9,
905
+ virtualizationScore: 0.8,
854
906
  },
855
907
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
856
908
  patterns: {
@@ -1465,88 +1517,6 @@ export const verifyTspChallenge = (
1465
1517
  }
1466
1518
  };
1467
1519
 
1468
- /**
1469
- * Generates the HTML content for a memory-intensive PoW challenge.
1470
- */
1471
- const generateMemoryPoWChallenge = (
1472
- clientIp,
1473
- nonce,
1474
- difficulty = 16,
1475
- path = "",
1476
- ) => {
1477
- const safePath = sanitizeRedirectPath(path);
1478
- // difficulty here is the buffer size in MB.
1479
- return `
1480
- <html>
1481
- <head><title>Advanced Security Check</title></head>
1482
- <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
1483
- <h1>Enhanced Verification... (Level 2)</h1>
1484
- <p>Your activity requires an additional security check.</p>
1485
- <div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
1486
- <script>
1487
- async function solve() {
1488
- const nonce = ${safeJsonStringify(nonce)};
1489
- const size = ${difficulty} * 1024 * 1024; // en octets
1490
- const iterations = size / 16;
1491
-
1492
- try {
1493
- const buffer = new Uint32Array(size / 4);
1494
- let h = new TextEncoder().encode(nonce).reduce((acc, v) => acc + v, 0);
1495
- for (let i = 0; i < buffer.length; i++) {
1496
- buffer[i] = (h = Math.imul(h ^ i, 1597334677));
1497
- }
1498
-
1499
- let finalHash = 0;
1500
- for(let i = 0; i < iterations; i++) {
1501
- const addr = buffer[i % buffer.length] % buffer.length;
1502
- finalHash ^= buffer[addr];
1503
- }
1504
- window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
1505
- } catch(e) {
1506
- window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
1507
- }
1508
- }
1509
- solve();
1510
- </script>
1511
- </body>
1512
- </html>`;
1513
- };
1514
-
1515
- /**
1516
- * Verifies if a PoW solution is valid and generates a clearance ticket.
1517
- */
1518
- export const verifyPoWAndGenerateTicket = async (
1519
- ip,
1520
- nonce,
1521
- solution,
1522
- difficulty = 4,
1523
- deviceId = '',
1524
- deviceHash = ''
1525
- ) => {
1526
- // 1. Verify the solution: hash(ip + nonce + solution) must start with N zeros
1527
- const hash = crypto
1528
- .createHash("sha256")
1529
- .update(`${ip}:${nonce}:${solution}`)
1530
- .digest("hex");
1531
-
1532
- if (!hash.startsWith("0".repeat(difficulty))) {
1533
- return null;
1534
- }
1535
-
1536
- // 2. Generate a signed and encrypted stateless ticket
1537
- const expiry = Date.now() + 3600000; // 1 hour
1538
- const payload = {
1539
- expiry,
1540
- originalIp: ip,
1541
- deviceId,
1542
- deviceHash
1543
- };
1544
-
1545
- return generateStatelessTicket(payload);
1546
- };
1547
-
1548
-
1549
-
1550
1520
  /**
1551
1521
  * Verifies a memory PoW solution.
1552
1522
  *
@@ -2334,13 +2304,37 @@ function analyzeTouchMovements(history) {
2334
2304
  * @returns {{behaviorScore: number}}
2335
2305
  */
2336
2306
  function getBehaviorScore(context) {
2337
- const behaviorHeader = context.headers["x-behavior-metrics"];
2338
- if (!behaviorHeader) {
2307
+ const behaviorHeader = context.headers ? (context.headers["x-behavior-metrics"] ?? context.headers["X-Behavior-Metrics"]) : undefined;
2308
+ if (behaviorHeader === undefined || behaviorHeader === null || behaviorHeader === '') {
2339
2309
  return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
2340
2310
  }
2341
2311
 
2312
+ // Traitement direct si un flag binaire client-side (1 = humain, 0 = bot) est envoyé
2313
+ if (behaviorHeader === 1 || behaviorHeader === '1') {
2314
+ return { behaviorScore: 0 };
2315
+ }
2316
+ if (behaviorHeader === 0 || behaviorHeader === '0') {
2317
+ return { behaviorScore: 100 };
2318
+ }
2319
+
2320
+ // Traitement direct si un score numérique est envoyé côté client
2321
+ if (typeof behaviorHeader === 'number') {
2322
+ return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
2323
+ }
2324
+ if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
2325
+ const trimmed = behaviorHeader.trim();
2326
+ if (trimmed === '1') return { behaviorScore: 0 };
2327
+ if (trimmed === '0') return { behaviorScore: 100 };
2328
+ return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
2329
+ }
2330
+
2342
2331
  try {
2343
2332
  const metrics = JSON.parse(behaviorHeader);
2333
+ if (metrics === 1) return { behaviorScore: 0 };
2334
+ if (metrics === 0) return { behaviorScore: 100 };
2335
+ if (typeof metrics === 'number') {
2336
+ return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
2337
+ }
2344
2338
  let score = 0;
2345
2339
 
2346
2340
  // 1. Pénalité maximale si un honeypot client a été déclenché.
@@ -2354,9 +2348,18 @@ function getBehaviorScore(context) {
2354
2348
  const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
2355
2349
  const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
2356
2350
 
2357
- // Pénalité pour absence totale d'interaction (pas de mouvements, pas de frappes).
2351
+ // Pénalité pour absence totale d'interaction. Un utilisateur légitime peut simplement lire la page.
2352
+ // On applique donc une pénalité de base faible, qui est amplifiée uniquement si d'autres
2353
+ // signaux passifs de bot (ex: rendu offscreen) sont présents.
2358
2354
  if (avgSpeed === 0 && touchAnalysis.avgSpeed === 0 && metrics.keystrokeLatency === 0) {
2359
- score += 40;
2355
+ let noInteractionPenalty = 5; // Pénalité de base très faible.
2356
+
2357
+ // Amplification si d'autres signaux passifs de bot sont présents.
2358
+ if (metrics.rendering?.offscreenAnom) {
2359
+ noInteractionPenalty += 40;
2360
+ }
2361
+
2362
+ score += noInteractionPenalty;
2360
2363
  }
2361
2364
 
2362
2365
  // 3. (NOUVEAU) Analyse de la longueur de l'historique de navigation.
@@ -2396,21 +2399,19 @@ function getBehaviorScore(context) {
2396
2399
  if (touch.avgRadius > 0 && touch.radiusVariance === 0) {
2397
2400
  score += 30; // Spoofed pointer area size
2398
2401
  }
2399
- }
2400
- if (touch.segments.length > 10) {
2401
- const benfordDev = Optimization.Operators.benfordTest(touch.segments);
2402
- if (benfordDev > 0.18) score += 35;
2403
- }
2404
-
2405
- // Détection de ferme mobile : Touch actif sur mobile sans aucune vibration physique (châssis/rack ADB)
2406
- const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
2407
- if (isMobileDevice && touchHistory.length >= 5 && typeof metrics.motionVariance === 'number') {
2408
- if (metrics.motionVariance === 0) {
2409
- score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
2402
+ if (touch.segments.length > 10) {
2403
+ const benfordDev = Optimization.Operators.benfordTest(touch.segments);
2404
+ if (benfordDev > 0.18) score += 35;
2410
2405
  }
2411
2406
  }
2412
2407
  }
2413
2408
 
2409
+ // Détection de ferme mobile : un appareil mobile parfaitement immobile est suspect, indépendamment des interactions tactiles.
2410
+ const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
2411
+ if (isMobileDevice && typeof metrics.motionVariance === 'number' && metrics.motionVariance === 0) {
2412
+ score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
2413
+ }
2414
+
2414
2415
  // Plausibilité de la latence de frappe
2415
2416
  if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
2416
2417
  if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
@@ -2666,9 +2667,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2666
2667
 
2667
2668
  // Check for known spoofed/suspicious JA4 fingerprints
2668
2669
  const spoofedJa4s = [
2669
- 't13d1516h2_8daaf6152771_390237aa04be', // Chrome classique (curl-impersonate / tls-client)
2670
- 't13d1413h2_bc66258908f0_bc2531da1615', // Firefox statique (curl-impersonate-ff / curl_cffi)
2671
- 't13d1515h2_8daaf6152771_a729e2f67de4', // Safari statique (curl-impersonate-safari / tls-client)
2672
2670
  't13d1516h2_8daaf6152771_4be0df930c2c', // Alternatif Chrome (tls-client Go)
2673
2671
  't12d1516h2_8daaf6152771_390237aa04be', // Chrome usurpé dégradé en TLS 1.2
2674
2672
  't13d1516h2_e822d36d892d_93ec3f0b2f5b' // Scraping bot OpenSSL customisé
@@ -2983,6 +2981,10 @@ function analyzeClickPositions(history) {
2983
2981
  * @returns {{clickVarianceScore: number}}
2984
2982
  */
2985
2983
  function getClickVarianceScore(context) {
2984
+ const rawMetrics = context.headers?.['x-behavior-metrics'];
2985
+ if (!rawMetrics || !isNaN(Number(rawMetrics))) {
2986
+ return { clickVarianceScore: 0 };
2987
+ }
2986
2988
  const metrics = JSON.parse(context.headers['x-behavior-metrics'] || '{}');
2987
2989
  const score = analyzeClickPositions(metrics.clicksHistory);
2988
2990
  return { clickVarianceScore: score };
@@ -3162,6 +3164,9 @@ function decaySubnetData(subnetData, now) {
3162
3164
  * @param {number} finalScore The final suspicion score.
3163
3165
  */
3164
3166
  async function updateSubnetMetrics(context, deviceId, finalScore) {
3167
+ if (!context.clientIp || isLoopbackIp(context.clientIp)) {
3168
+ return;
3169
+ }
3165
3170
  const subnet = getIpSubnet(context.clientIp);
3166
3171
  if (!subnet) return;
3167
3172
 
@@ -3194,7 +3199,8 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3194
3199
 
3195
3200
  // Utilisation d'un identifiant d'appareil stable (fingerprint matériel) plutôt que l'ID de cookie volatil
3196
3201
  const currentDeviceHash = getCompositeDeviceHash(context);
3197
- const stableFpId = cyrb53(extractStablePart(currentDeviceHash)).toString();
3202
+ const stablePart = extractStablePart(currentDeviceHash);
3203
+ const stableFpId = stablePart ? cyrb53(stablePart).toString() : (deviceId || cyrb53(currentDeviceHash).toString());
3198
3204
 
3199
3205
  const currentDeviceContributions = subnetData.highScoreDevices[stableFpId] || 0;
3200
3206
  if (currentDeviceContributions < 1) {
@@ -3212,7 +3218,9 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3212
3218
 
3213
3219
  const userAgent = context.headers?.['user-agent'] || '';
3214
3220
 
3215
- if (finalScore >= 70) {
3221
+ // Ne pas enregistrer une IP en attaquant si le score élevé provient uniquement d'un seul appareil isolé
3222
+ const isConfirmedClusterAttack = (subnetData.deviceIds.length > 1 && finalScore >= 70) || finalScore >= 95;
3223
+ if (isConfirmedClusterAttack) {
3216
3224
  const existingAttacker = subnetData.attackerIps.find(a => a.ip === context.clientIp);
3217
3225
  if (existingAttacker) {
3218
3226
  existingAttacker.lastSeen = now;
@@ -3272,6 +3280,9 @@ export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPo
3272
3280
  * @returns {Promise<{subnetScore: number}>}
3273
3281
  */
3274
3282
  async function getSubnetScore(context, deviceId = '', securityConfig = null) {
3283
+ if (!context.clientIp || isLoopbackIp(context.clientIp)) {
3284
+ return { subnetScore: 0.0 };
3285
+ }
3275
3286
  if (typeof deviceId === 'object' && deviceId !== null && !securityConfig) {
3276
3287
  securityConfig = deviceId;
3277
3288
  }
@@ -3299,7 +3310,7 @@ async function getSubnetScore(context, deviceId = '', securityConfig = null) {
3299
3310
  }
3300
3311
  let uaCount = subnetData.uas ? subnetData.uas.length : 1;
3301
3312
 
3302
- if (deviceCount === 0) {
3313
+ if (deviceCount <= 1 && highScoreCount <= 1) {
3303
3314
  return { subnetScore: 0.0 };
3304
3315
  }
3305
3316
 
@@ -3825,7 +3836,7 @@ async function getBehavioralIndicators(context, deviceData) {
3825
3836
  // --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
3826
3837
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
3827
3838
  let webauthnVerified = false;
3828
- if (behaviorHeader) {
3839
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
3829
3840
  try {
3830
3841
  const metrics = JSON.parse(behaviorHeader);
3831
3842
  if (metrics && metrics.webauthnAnchor) {
@@ -3948,7 +3959,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3948
3959
  _ // store.set result
3949
3960
  ] = await Promise.all([
3950
3961
  getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
3951
- getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
3962
+ getThreatIntelScore(context, zkpY, securityConfig), // NOUVEAU: Score de Threat Intelligence Fédéré
3952
3963
  getTlsSpoofingScore(context),
3953
3964
  getSubnetScore(context, deviceId, securityConfig),
3954
3965
  getIpReputationScore(clientIp),
@@ -3969,7 +3980,12 @@ export const getSuspicionVector = async (context, securityConfig) => {
3969
3980
  const { botScore } = getBotScore(context);
3970
3981
 
3971
3982
  // NOUVEAU: On calcule le score d'incohérence temporelle.
3972
- const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'), deviceData);
3983
+ let parsedBehaviorMetrics = {};
3984
+ const rawBehaviorHeader = context.headers?.['x-behavior-metrics'];
3985
+ if (rawBehaviorHeader && typeof rawBehaviorHeader === 'string' && rawBehaviorHeader.startsWith('{')) {
3986
+ try { parsedBehaviorMetrics = JSON.parse(rawBehaviorHeader); } catch (e) {}
3987
+ }
3988
+ const { timeInconsistencyScore } = getTimeInconsistencyScore(context, parsedBehaviorMetrics, deviceData);
3973
3989
 
3974
3990
  // NOUVEAU: On calcule le score d'incohérence entre les couches.
3975
3991
  const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
@@ -3983,7 +3999,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3983
3999
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
3984
4000
 
3985
4001
  const { tcpAnomalyScore } = getTcpAnomalyScore(context);
3986
- const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
4002
+ const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
3987
4003
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
3988
4004
  const virtualizationScore = getVirtualizationAnomalyScore(context);
3989
4005
 
@@ -3996,8 +4012,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
3996
4012
  if (Array.isArray(deviceData.ips)) {
3997
4013
  deviceData.ips = new Set(deviceData.ips);
3998
4014
  }
3999
- // Le vecteur de suspicion est maintenant complet.
4000
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
4015
+ // Le vecteur de suspicion est maintenant complet.
4016
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
4001
4017
  };
4002
4018
 
4003
4019
  // A residential user can change networks (home, 4G, public wifi).
@@ -4020,7 +4036,31 @@ export const identifyRequest = (securityConfig) => async (req, res) => {
4020
4036
  // This function now acts as a lightweight wrapper around the engine's identifyRequest method.
4021
4037
  // It requires a default configuration to work.
4022
4038
  const config = securityConfig || {
4023
- weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8, honeypotScore: 1.0 },
4039
+ weights: {
4040
+ historyScore: 0.3,
4041
+ rotationScore: 0.5,
4042
+ headerAnomalyScore: 0.2,
4043
+ requestPatternScore: 0.6,
4044
+ inconsistencyScore: 0.8,
4045
+ behaviorScore: 0.7,
4046
+ honeypotScore: 1.0,
4047
+ botScore: 1.0,
4048
+ cookieDroppingScore: 0.9,
4049
+ crossLayerInconsistencyScore: 0.4,
4050
+ timeInconsistencyScore: 0.9,
4051
+ tlsSpoofingScore: 0.8,
4052
+ clientHintsInconsistencyScore: 0.7,
4053
+ clickVarianceScore: 0.6,
4054
+ subnetScore: 0.4,
4055
+ ipReputationScore: 0.5,
4056
+ botnetClusterScore: 0.7,
4057
+ tcpAnomalyScore: 0.8,
4058
+ quicAnomalyScore: 0.8,
4059
+ renderingAnomalyScore: 0.8,
4060
+ http2AnomalyScore: 0.8,
4061
+ threatIntelScore: 1.0,
4062
+ virtualizationScore: 0.8
4063
+ },
4024
4064
  thresholds: { low: 20, medium: 40, high: 75 },
4025
4065
  honeypot: { fields: [] } // Ensure honeypot config exists to prevent errors
4026
4066
  };
@@ -4383,40 +4423,68 @@ function parseGraphQLQuery(body) {
4383
4423
  * @param {string} zkpY - The 'y' component of the ZKP proof (public key).
4384
4424
  * @returns {Promise<{threatIntelScore: number}>}
4385
4425
  */
4386
- async function getThreatIntelScore(context, zkpY) {
4387
- let score = 0;
4426
+ async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
4427
+ let score = 0.0;
4428
+ const signals = [];
4429
+
4430
+ // 1. Détection déterministe : Clé publique ZKP bannie par consensus fédéré
4388
4431
  if (zkpY) {
4389
4432
  const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
4390
4433
  if (isBanned) {
4391
- score = 100;
4434
+ score = 100.0;
4435
+ signals.push({
4436
+ ruleId: 'FEDERATED_ZKP_BANNED',
4437
+ confidence: 1.0,
4438
+ score: 100.0,
4439
+ rationale: 'Cryptographic identity matched banned list consensus'
4440
+ });
4392
4441
  }
4393
4442
  }
4394
4443
 
4444
+ // 2. Détection physique : Modélisation continue du différentiel de transport (RTT TCP Edge vs Transit Applicatif)
4445
+ // Rationale : Un RTT TCP de socket très faible (datacenter/edge CDN) couplé à un délai applicatif WAN élevé
4446
+ // prouve l'interposition d'un relais/tunnel/proxy résidentiel entre le client réel et le point d'entrée.
4395
4447
  const tcpRttHeader = context.headers?.['x-tcp-rtt'] || context.headers?.['x-real-rtt'];
4396
4448
  const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
4397
4449
 
4398
4450
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
4399
- if (behaviorHeader) {
4451
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
4400
4452
  try {
4401
4453
  const metrics = JSON.parse(behaviorHeader);
4402
- if (metrics && metrics.clientTimestamp && context.requestTimestamp) {
4454
+ if (metrics && typeof metrics.clientTimestamp === 'number' && typeof context.requestTimestamp === 'number') {
4403
4455
  const appLatency = context.requestTimestamp - metrics.clientTimestamp;
4404
- if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0) {
4405
- const clientToProxyDelta = appLatency - tcpRtt;
4406
- if (tcpRtt < 35 && clientToProxyDelta > 150) {
4407
- score = Math.max(score, 85);
4408
- }
4409
- } else {
4410
- if (appLatency > 350) {
4411
- score = Math.max(score, 40);
4456
+
4457
+ // Évaluation uniquement si la latence est mesurable et positive avec une mesure socket RTT valide
4458
+ if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0 && appLatency > 0) {
4459
+ // Marge de tolérance contre le jitter réseau et le scheduling JS (Garbage Collector, event-loop)
4460
+ const jitterAllowance = 60.0;
4461
+ const effectiveRtt = Math.max(tcpRtt, 5.0);
4462
+ const tunnelDelta = appLatency - (tcpRtt + jitterAllowance);
4463
+ const divergenceRatio = appLatency / effectiveRtt;
4464
+
4465
+ // Condition de disjonction : liaison edge ultra-proche (< 40ms) + transit applicatif au moins 3x supérieur
4466
+ if (tcpRtt <= 40 && divergenceRatio >= 3.0 && tunnelDelta > 0) {
4467
+ // Progression sigmoïdale continue calibrée sur la propagation optique intercontinentale (120ms)
4468
+ const scaling = 120.0;
4469
+ const ratioWeight = Math.min(1.0, (divergenceRatio - 3.0) / 5.0);
4470
+ const proxyScore = Math.min(95.0, 40.0 + 55.0 * Math.tanh(tunnelDelta / scaling) * ratioWeight);
4471
+ const finalProxyScore = Math.round(proxyScore * 10) / 10;
4472
+
4473
+ score = Math.max(score, finalProxyScore);
4474
+ signals.push({
4475
+ ruleId: 'RESIDENTIAL_PROXY_RTT_DISCREPANCY',
4476
+ confidence: Math.round(ratioWeight * 100) / 100,
4477
+ score: finalProxyScore,
4478
+ rationale: `TCP RTT (${tcpRtt}ms) diverges from application transit (${appLatency}ms) with ratio ${divergenceRatio.toFixed(1)}:1`
4479
+ });
4412
4480
  }
4413
4481
  }
4414
4482
  }
4415
4483
  } catch (e) {
4416
- // Ignorer silencieusement
4484
+ // Ignorer silencieusement les erreurs de parsing des métriques
4417
4485
  }
4418
4486
  }
4419
- return { threatIntelScore: score };
4487
+ return { threatIntelScore: score, threatIntelSignals: signals };
4420
4488
  }
4421
4489
 
4422
4490
  export class FingerprintEngine {
@@ -4542,7 +4610,8 @@ export class FingerprintEngine {
4542
4610
  'wasm',
4543
4611
  'similarityThreshold', 'reset',
4544
4612
  'ed25519_private_key', 'ed25519_public_key', 'upowModel',
4545
- 'federatedPeers', 'federationSecret', 'filterWhitelist'
4613
+ 'federatedPeers', 'federationSecret', 'filterWhitelist',
4614
+ 'challengeRateLimit'
4546
4615
  ]);
4547
4616
 
4548
4617
  // 1. Check for essential keys
@@ -4602,7 +4671,9 @@ export class FingerprintEngine {
4602
4671
  (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
4603
4672
  (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
4604
4673
  (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
4605
- (suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) + // NOUVEAU: HTTP/2 Anomaly
4674
+ (suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) +
4675
+ (suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
4676
+ (suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
4606
4677
  (suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
4607
4678
  (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
4608
4679
 
@@ -4802,7 +4873,7 @@ export class FingerprintEngine {
4802
4873
  const { whitelist = [] } = this.securityConfig;
4803
4874
  const botRules = whitelist.filter(rule => rule.hostnameSuffix);
4804
4875
  if (botRules.length === 0) {
4805
- return false;
4876
+ return null;
4806
4877
  }
4807
4878
 
4808
4879
  const { clientIp, headers } = requestContext;
@@ -4818,7 +4889,7 @@ export class FingerprintEngine {
4818
4889
  }
4819
4890
  });
4820
4891
  if (!matchedRule) {
4821
- return false;
4892
+ return null;
4822
4893
  }
4823
4894
 
4824
4895
  const botName = matchedRule.userAgent; // e.g., 'Googlebot'
@@ -4885,6 +4956,13 @@ export class FingerprintEngine {
4885
4956
  }
4886
4957
 
4887
4958
  async processRequest(requestContext) {
4959
+ if (this.securityConfig?.reset) {
4960
+ const subnet = getIpSubnet(requestContext.clientIp);
4961
+ if (subnet) {
4962
+ await store.delete(`subnet:${subnet}`);
4963
+ }
4964
+ await store.delete(`ip-reputation:${requestContext.clientIp}`);
4965
+ }
4888
4966
  sanitizeProxyHeaders(requestContext, this.securityConfig);
4889
4967
 
4890
4968
  const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
@@ -5544,10 +5622,11 @@ export class FingerprintEngine {
5544
5622
  this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
5545
5623
  }
5546
5624
 
5547
- // --- AJOUT : Limiteur de débit (Token Bucket) ---
5548
- const rateLimitPassed = await checkChallengeRateLimit(clientIp);
5625
+ // --- Limiteur de débit par domaine et sous-réseau (Token Bucket) ---
5626
+ const domain = requestContext.headers?.host || 'default';
5627
+ const rateLimitPassed = await checkChallengeRateLimit(clientIp, domain, this.securityConfig?.challengeRateLimit);
5549
5628
  if (!rateLimitPassed) {
5550
- this._log('Challenge rate limit exceeded - blocking with 429', { clientIp });
5629
+ this._log('Challenge rate limit exceeded - blocking with 429', { clientIp, domain });
5551
5630
  const decision = {
5552
5631
  action: 'block',
5553
5632
  status: 429,
@@ -6917,6 +6996,7 @@ export const powMiddleware = (securityConfig) => {
6917
6996
  */
6918
6997
  export const __internal = {
6919
6998
  get store() { return store; }, // Export the store for testing
6999
+ checkChallengeRateLimit,
6920
7000
  getDeviceHash,
6921
7001
  getCompositeDeviceHash,
6922
7002
  getSuspicionVector,