@anonympins/fingerprint 0.3.2 → 0.3.3

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 (50) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +276 -34
  3. package/composer.json +38 -0
  4. package/index.js +5 -0
  5. package/package.json +23 -18
  6. package/phpunit.xml +20 -0
  7. package/public/fp.js +2 -0
  8. package/src/js/build-client.js +69 -0
  9. package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -175
  10. package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -538
  11. package/src/js/fingerprint.client.obfuscated.js +1 -0
  12. package/{fingerprint.js → src/js/fingerprint.js} +255 -101
  13. package/{library.js → src/js/library.js} +1729 -1729
  14. package/{problem-manager.js → src/js/problem-manager.js} +539 -522
  15. package/src/php/AutoTuner.php +155 -0
  16. package/src/php/Challenge/ChallengeUtils.php +306 -0
  17. package/src/php/Config/SecurityProfiles.php +257 -0
  18. package/src/php/DirectFingerprint.php +81 -0
  19. package/src/php/FingerprintBuilder.php +185 -0
  20. package/src/php/FingerprintClient.php +118 -0
  21. package/src/php/FingerprintEngine.php +850 -0
  22. package/src/php/Optimization/FunctionRegistry.php +63 -0
  23. package/src/php/Optimization/Optimization.php +256 -0
  24. package/src/php/Optimization/OptimizationOperators.php +305 -0
  25. package/src/php/Optimization/ProblemInitializers.php +53 -0
  26. package/src/php/ProblemManager.php +255 -0
  27. package/src/php/RequestContext.php +87 -0
  28. package/src/php/Store/IStore.php +42 -0
  29. package/src/php/Store/InMemoryStore.php +67 -0
  30. package/src/php/Store/StoreManager.php +26 -0
  31. package/src/php/Tests/ChallengeUtilsTest.php +82 -0
  32. package/src/php/Tests/FingerprintBuilderTest.php +58 -0
  33. package/src/php/Tests/FingerprintEngineTest.php +219 -0
  34. package/src/php/Tests/PowTest.php +40 -0
  35. package/src/php/Tests/ProblemManagerTest.php +295 -0
  36. package/src/php/Tests/RequestUtilsTest.php +81 -0
  37. package/src/php/Tests/problems.config.json +9 -0
  38. package/src/php/Utils/BigInt.php +102 -0
  39. package/src/php/Utils/BlockList.php +100 -0
  40. package/src/php/Utils/Logger.php +30 -0
  41. package/src/php/Utils/MaliciousPatterns.php +59 -0
  42. package/src/php/Utils/RequestUtils.php +673 -0
  43. package/fingerprint.client.obfuscated.js +0 -1
  44. /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
  45. /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
  46. /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
  47. /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
  48. /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
  49. /package/{redis-store.js → src/js/redis-store.js} +0 -0
  50. /package/{sql-store.js → src/js/sql-store.js} +0 -0
@@ -44,7 +44,7 @@ const securityProfiles = {
44
44
  headerAnomalyScore: 0.1,
45
45
  requestPatternScore: 0.6,
46
46
  inconsistencyScore: 0.8,
47
- behaviorScore: 0.7,
47
+ behaviorScore: 0.7, // Poids pour les métriques comportementales (souris, clavier)
48
48
  honeypotScore: 1.0,
49
49
  crossLayerInconsistencyScore: 0.4,
50
50
  timeInconsistencyScore: 0.9,
@@ -79,7 +79,7 @@ const securityProfiles = {
79
79
  honeypotScore: 1.0,
80
80
  crossLayerInconsistencyScore: 0.6,
81
81
  timeInconsistencyScore: 1.0,
82
- tlsSpoofingScore: 1.0 // NOUVEAU: Plus agressif pour le spoofing TLS
82
+ tlsSpoofingScore: 1.0 // Plus agressif pour le spoofing TLS
83
83
  },
84
84
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
85
85
  patterns: {
@@ -111,7 +111,7 @@ const securityProfiles = {
111
111
  honeypotScore: 1.0,
112
112
  crossLayerInconsistencyScore: 0.5,
113
113
  timeInconsistencyScore: 0.8,
114
- tlsSpoofingScore: 0.7 // NOUVEAU: Important pour les API
114
+ tlsSpoofingScore: 0.7 // Important pour les API
115
115
  },
116
116
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
117
117
  patterns: {
@@ -144,7 +144,7 @@ const securityProfiles = {
144
144
  honeypotScore: 1.0, // Crucial for comment spam
145
145
  crossLayerInconsistencyScore: 0.4,
146
146
  timeInconsistencyScore: 0.8,
147
- tlsSpoofingScore: 0.6 // NOUVEAU: Moins critique pour les blogs
147
+ tlsSpoofingScore: 0.6 // Moins critique pour les blogs
148
148
  },
149
149
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
150
150
  patterns: {
@@ -169,17 +169,14 @@ const securityProfiles = {
169
169
  historyScore: 0.4,
170
170
  rotationScore: 0.6,
171
171
  headerAnomalyScore: 0.2,
172
- // Scission du requestPatternScore pour un contrôle plus fin
173
- velocityScore: 0.8, // Pénalise la vitesse globale
174
- burstScore: 1.0, // Pénalise fortement les rafales sur la même ressource (scalping)
175
- scrapeScore: 0.9, // Pénalise le parcours de pages/produits
176
- regularityScore: 0.7, // Détecte les bots de type "cron"
172
+ // Utilisation d'un score de pattern unifié avec un poids très élevé
173
+ requestPatternScore: 0.9,
177
174
  inconsistencyScore: 1.0, // Crucial for preventing account takeover
178
175
  behaviorScore: 0.8, // Important for checkout/login forms
179
176
  honeypotScore: 1.0,
180
177
  crossLayerInconsistencyScore: 0.7,
181
178
  timeInconsistencyScore: 0.9,
182
- tlsSpoofingScore: 0.9 // NOUVEAU: Très important pour l'e-commerce
179
+ tlsSpoofingScore: 0.9 // Très important pour l'e-commerce
183
180
  },
184
181
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
185
182
  patterns: {
@@ -657,47 +654,6 @@ export const verifyTspChallenge = (
657
654
  }
658
655
  };
659
656
 
660
- /**
661
- * Generates the HTML content for the CPU PoW challenge (SHA-256).
662
- */
663
- const generateCpuPoWChallenge = (
664
- clientIp,
665
- nonce,
666
- difficulty = 4,
667
- path = "",
668
- ) => {
669
- return `
670
- <html>
671
- <head><title>Security Check</title></head>
672
- <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
673
- <h1>One moment... (Level 1)</h1>
674
- <p>We are verifying that you are not a bot. This takes a few seconds.</p>
675
- <div id="loader" style="margin:20px;">⚙️ Performing CPU security calculation...</div>
676
- <script>
677
- async function solve() {
678
- const ip = "${clientIp}";
679
- const nonce = "${nonce}";
680
- const diff = ${difficulty};
681
- const target = "0".repeat(diff);
682
- let solution = 0;
683
-
684
- while (true) {
685
- const msg = "${ip}" + ":" + "${nonce}" + ":" + solution;
686
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
687
- const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
688
- if (hash.startsWith(target)) break;
689
- solution++;
690
- if (solution % 100000 === 0) await new Promise(resolve => setTimeout(resolve, 0)); // To avoid freezing the browser
691
- }
692
- window.location.href = "${path}" + "?pow_type=cpu&pow_nonce=" + nonce + "&pow_solution=" + solution;
693
- }
694
- solve();
695
- </script>
696
- </body>
697
- </html>
698
- `;
699
- };
700
-
701
657
  /**
702
658
  * Generates the HTML content for a memory-intensive PoW challenge.
703
659
  */
@@ -955,6 +911,68 @@ const injectionPatterns = {
955
911
  rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
956
912
  };
957
913
 
914
+ /**
915
+ * @private
916
+ * Analyse une série de mouvements de souris pour en extraire des métriques comportementales.
917
+ * @param {Array<{x: number, y: number, t: number}>} history - L'historique des points de la souris.
918
+ * @returns {{avgSpeed: number, avgAcceleration: number, straightness: number, pauses: number, segments: Array<number>}}
919
+ */
920
+ function analyzeMouseMovements(history) {
921
+ if (!history || history.length < 3) {
922
+ return { avgSpeed: 0, avgAcceleration: 0, straightness: 1, pauses: 0, segments: [] };
923
+ }
924
+
925
+ const segments = [];
926
+ let totalDistance = 0;
927
+ let pauses = 0;
928
+
929
+ for (let i = 1; i < history.length; i++) {
930
+ const p1 = history[i - 1];
931
+ const p2 = history[i];
932
+ const dx = p2.x - p1.x;
933
+ const dy = p2.y - p1.y;
934
+ const dt = p2.t - p1.t;
935
+ const distance = Math.sqrt(dx * dx + dy * dy);
936
+
937
+ if (dt > 0) {
938
+ const speed = distance / dt;
939
+ segments.push({ distance, dt, speed });
940
+ totalDistance += distance;
941
+ }
942
+ // Une "micro-pause" est un intervalle de temps long sans mouvement significatif.
943
+ if (dt > 100 && distance < 5) {
944
+ pauses++;
945
+ }
946
+ }
947
+
948
+ if (segments.length < 2) {
949
+ return { avgSpeed: 0, avgAcceleration: 0, straightness: 1, pauses, segments: [] };
950
+ }
951
+
952
+ const totalTime = history[history.length - 1].t - history[0].t;
953
+ const avgSpeed = totalTime > 0 ? segments.reduce((sum, s) => sum + s.speed, 0) / segments.length : 0;
954
+
955
+ let totalAbsAcceleration = 0;
956
+ for (let i = 1; i < segments.length; i++) {
957
+ const s1 = segments[i - 1];
958
+ const s2 = segments[i];
959
+ if (s2.dt > 0) {
960
+ const acceleration = (s2.speed - s1.speed) / s2.dt;
961
+ totalAbsAcceleration += Math.abs(acceleration);
962
+ }
963
+ }
964
+ const avgAcceleration = totalAbsAcceleration / (segments.length - 1);
965
+
966
+ // Le score de rectitude compare la distance totale parcourue à la distance en ligne droite.
967
+ // Un score proche de 1 signifie un mouvement très droit (suspect).
968
+ const startPoint = history[0];
969
+ const endPoint = history[history.length - 1];
970
+ const straightDistance = Math.sqrt(Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2));
971
+ const straightness = totalDistance > 0 ? straightDistance / totalDistance : 1;
972
+
973
+ return { avgSpeed, avgAcceleration, straightness, pauses, segments: segments.map(s => s.distance) };
974
+ }
975
+
958
976
  /**
959
977
  * Calcule un score basé sur les métriques comportementales envoyées par le client.
960
978
  * @param {object} context - Le contexte de la requête, contenant les en-têtes.
@@ -975,8 +993,11 @@ function getBehaviorScore(context) {
975
993
  return { behaviorScore: 100 };
976
994
  }
977
995
 
978
- // 2. Pénalité pour absence totale d'interaction.
979
- if (metrics.mouseEntropy === 0 && metrics.keystrokeLatency === 0) {
996
+ // 2. Analyse des mouvements de la souris
997
+ const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
998
+
999
+ // Pénalité pour absence totale d'interaction (pas de mouvements, pas de frappes).
1000
+ if (avgSpeed === 0 && metrics.keystrokeLatency === 0) {
980
1001
  score += 40;
981
1002
  }
982
1003
 
@@ -991,32 +1012,25 @@ function getBehaviorScore(context) {
991
1012
  score -= 10; // Petit bonus pour une navigation de base.
992
1013
  }
993
1014
  }
994
- // 3. Vérification de la plausibilité et de la distribution des métriques.
995
- // Un bot pourrait envoyer des valeurs aléatoires, mais elles ne suivront probablement pas
996
- // des distributions naturelles (comme la loi de Benford pour les premiers chiffres).
997
1015
 
998
- // Plausibilité de l'entropie de la souris
999
- if (metrics.mouseEntropy > 0 && metrics.mouseEntropy < 0.1) score += 20; // Entropie très faible, suspect.
1000
- if (metrics.mouseEntropy > 500) score += 30; // Entropie irréalistement élevée.
1016
+ // 3. Analyse des métriques de la souris
1017
+ if (avgSpeed > 0) {
1018
+ if (avgSpeed > 3) score += 25; // Vitesse irréaliste (3 pixels/ms)
1019
+ if (avgAcceleration > 0.5) score += 20; // Accélération trop brutale
1020
+ if (straightness > 0.95) score += 30; // Mouvement trop droit
1021
+ if (pauses === 0 && segments.length > 20) score += 15; // Mouvement continu sans micro-pauses
1022
+ }
1001
1023
 
1002
1024
  // Plausibilité de la latence de frappe
1003
1025
  if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
1004
1026
  if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
1005
1027
 
1006
1028
  // 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
1007
- // On utilise les décimales pour avoir plus de chiffres à analyser.
1008
- const mouseEntropyStr = String(metrics.mouseEntropy).replace(".", "");
1009
- const keystrokeLatencyStr = String(metrics.keystrokeLatency).replace(".", "");
1010
-
1011
- if (mouseEntropyStr.length > 2) {
1012
- const mouseDeviation = Optimization.Operators.benfordTest(mouseEntropyStr);
1013
- // Une déviation > 0.15 est fortement suspecte.
1014
- if (mouseDeviation > 0.15) score += 40;
1015
- }
1016
-
1017
- if (keystrokeLatencyStr.length > 2) {
1018
- const keystrokeDeviation = Optimization.Operators.benfordTest(keystrokeLatencyStr);
1019
- if (keystrokeDeviation > 0.15) score += 40;
1029
+ if (segments.length > 10) {
1030
+ const benfordDeviation = Optimization.Operators.benfordTest(segments);
1031
+ if (benfordDeviation > 0.18) { // Seuil légèrement plus élevé pour cette métrique
1032
+ score += 35;
1033
+ }
1020
1034
  }
1021
1035
 
1022
1036
  return { behaviorScore: Math.min(100, score) }; // Assure que le score ne dépasse pas 100, mais peut être négatif (bonus)
@@ -1152,6 +1166,64 @@ function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerprint) {
1152
1166
  return { tlsSpoofingScore: 0 };
1153
1167
  }
1154
1168
 
1169
+ /**
1170
+ * @private
1171
+ * Analyzes click positions from client-side metrics to detect unnaturally low variance,
1172
+ * which can be a sign of automated clicking.
1173
+ * @param {Array<{x: number, y: number, targetId: string}>|null} history - The click history from the client.
1174
+ * @returns {number} A score from 0 to 100, where a higher score indicates lower variance (more bot-like).
1175
+ */
1176
+ function analyzeClickPositions(history) {
1177
+ if (!history || history.length < 3) {
1178
+ return 0;
1179
+ }
1180
+
1181
+ const clicksByTarget = {};
1182
+ for (const click of history) {
1183
+ if (!click.targetId) continue;
1184
+ if (!clicksByTarget[click.targetId]) {
1185
+ clicksByTarget[click.targetId] = [];
1186
+ }
1187
+ clicksByTarget[click.targetId].push(click);
1188
+ }
1189
+
1190
+ let maxScore = 0;
1191
+
1192
+ for (const targetId in clicksByTarget) {
1193
+ const clicks = clicksByTarget[targetId];
1194
+ if (clicks.length < 3) continue;
1195
+
1196
+ const n = clicks.length;
1197
+ const meanX = clicks.reduce((sum, c) => sum + c.x, 0) / n;
1198
+ const meanY = clicks.reduce((sum, c) => sum + c.y, 0) / n;
1199
+
1200
+ const variance = clicks.reduce((sum, c) => sum + Math.pow(c.x - meanX, 2) + Math.pow(c.y - meanY, 2), 0) / n;
1201
+
1202
+ // If variance is extremely low (e.g., less than 1 pixel), it's highly suspicious.
1203
+ // The score increases as variance approaches zero.
1204
+ if (variance < 1.0) {
1205
+ // A simple scoring model: score is 100 if variance is 0, and decreases.
1206
+ const score = (1 - Math.sqrt(variance) / 5) * 100;
1207
+ if (score > maxScore) {
1208
+ maxScore = score;
1209
+ }
1210
+ }
1211
+ }
1212
+
1213
+ return Math.min(100, maxScore);
1214
+ }
1215
+
1216
+ /**
1217
+ * Calculates a score based on click variance metrics sent by the client.
1218
+ * @param {object} context - The request context.
1219
+ * @returns {{clickVarianceScore: number}}
1220
+ */
1221
+ function getClickVarianceScore(context) {
1222
+ const metrics = JSON.parse(context.headers['x-behavior-metrics'] || '{}');
1223
+ const score = analyzeClickPositions(metrics.clicksHistory);
1224
+ return { clickVarianceScore: score };
1225
+ }
1226
+
1155
1227
 
1156
1228
  /**
1157
1229
  * Calcule un score basé sur la détection explicite de frameworks d'automatisation.
@@ -1511,10 +1583,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
1511
1583
  // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
1512
1584
  const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
1513
1585
 
1514
- // NOUVEAU: On calcule le score de spoofing TLS.
1515
1586
  const { tlsSpoofingScore } = getTlsSpoofingScore(context);
1516
1587
 
1517
- // NOUVEAU: On appelle getBotScore pour détecter les marqueurs d'automatisation.
1518
1588
  const { botScore } = getBotScore(context);
1519
1589
 
1520
1590
  // NOUVEAU: On calcule le score d'incohérence temporelle.
@@ -1523,6 +1593,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
1523
1593
  // NOUVEAU: On calcule le score d'incohérence entre les couches.
1524
1594
  const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
1525
1595
 
1596
+ // NOUVEAU: On calcule le score de variance des clics.
1597
+ const { clickVarianceScore } = getClickVarianceScore(context);
1598
+
1526
1599
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
1527
1600
 
1528
1601
  // Save the updated device state to the store
@@ -1535,7 +1608,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
1535
1608
  deviceData.ips = new Set(deviceData.ips);
1536
1609
  }
1537
1610
  // Le vecteur de suspicion est maintenant complet.
1538
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore };
1611
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore: clickVarianceScore };
1539
1612
  };
1540
1613
 
1541
1614
  // A residential user can change networks (home, 4G, public wifi).
@@ -1698,7 +1771,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1698
1771
  * @param {string} clientIp - The client's IP address.
1699
1772
  * @returns {string} HTML content.
1700
1773
  */
1701
- function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml, originalFingerprint) {
1774
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
1702
1775
  const { nonce, target, path } = cpuChallengeDetails;
1703
1776
  const solverCode = getPowSolverCode();
1704
1777
  // On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
@@ -1707,6 +1780,13 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1707
1780
  const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
1708
1781
  const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
1709
1782
 
1783
+ // Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
1784
+ const clientInitConfig = {
1785
+ mouse: true,
1786
+ keystrokes: true,
1787
+ trapUrls: trapUrls // On passe directement le tableau d'URL
1788
+ };
1789
+
1710
1790
  const challengeScript = `
1711
1791
  async function solve() {
1712
1792
  const nonce = ${JSON.stringify(nonce)};
@@ -1738,6 +1818,12 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1738
1818
  const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
1739
1819
  window.location.href = finalUrl;
1740
1820
  }
1821
+
1822
+ // Initialise la bibliothèque client avec les URL pièges
1823
+ // On crée un alias pour un appel plus propre, tout en s'assurant que la bibliothèque est chargée.
1824
+ const initializeClient = window.ClientLibrary?.initializeClient;
1825
+ if (initializeClient) initializeClient(${JSON.stringify(clientInitConfig)});
1826
+
1741
1827
  solve();
1742
1828
  `;
1743
1829
 
@@ -1753,13 +1839,12 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1753
1839
  }
1754
1840
 
1755
1841
  if (!htmlTemplate) {
1756
- htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>`;
1842
+ htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script></body></html>`; // eslint-disable-line max-len
1757
1843
  }
1758
1844
 
1759
1845
  return htmlTemplate
1760
1846
  .replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
1761
- .replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript)
1762
- .replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
1847
+ .replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
1763
1848
  }
1764
1849
 
1765
1850
  /**
@@ -1861,6 +1946,7 @@ export class FingerprintEngine {
1861
1946
  this._allowlist = this._buildAllowlist();
1862
1947
  this._validateConfig(securityConfig); // Validate the configuration
1863
1948
  this.verbose = securityConfig.verbose || false;
1949
+ this.dryRun = securityConfig.dryRun || false;
1864
1950
  }
1865
1951
 
1866
1952
  /**
@@ -1878,7 +1964,7 @@ export class FingerprintEngine {
1878
1964
  'weights', 'thresholds', 'cpu', 'ticketMaxAge', 'challengeTtl',
1879
1965
  'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
1880
1966
  'honeypot', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger',
1881
- 'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist',
1967
+ 'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
1882
1968
  'similarityThreshold'
1883
1969
  ]);
1884
1970
 
@@ -1918,7 +2004,8 @@ export class FingerprintEngine {
1918
2004
  (suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
1919
2005
  (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
1920
2006
  (suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
1921
- (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0);
2007
+ (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
2008
+ (suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0);
1922
2009
 
1923
2010
  return Math.min(100, score);
1924
2011
  }
@@ -2239,7 +2326,15 @@ export class FingerprintEngine {
2239
2326
  if (onDeviceCompromised) {
2240
2327
  onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
2241
2328
  }
2242
- return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
2329
+ const decision = { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
2330
+ if (this.dryRun) {
2331
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
2332
+ decision.intendedAction = decision.action;
2333
+ decision.action = 'next';
2334
+ delete decision.status;
2335
+ delete decision.body;
2336
+ }
2337
+ return decision;
2243
2338
  }
2244
2339
 
2245
2340
  // The engine now works with the context directly, no more rawReq dependency here.
@@ -2440,7 +2535,15 @@ export class FingerprintEngine {
2440
2535
  const newBlockThreshold = thresholds.block ?? 95;
2441
2536
  if (finalScore >= newBlockThreshold) {
2442
2537
  this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
2443
- return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
2538
+ const decision = { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
2539
+ if (this.dryRun) {
2540
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
2541
+ decision.intendedAction = decision.action;
2542
+ decision.action = 'next';
2543
+ delete decision.status;
2544
+ delete decision.body;
2545
+ }
2546
+ return decision;
2444
2547
  }
2445
2548
  // If not blocked, the request will proceed to be re-challenged.
2446
2549
  }
@@ -2510,7 +2613,15 @@ export class FingerprintEngine {
2510
2613
  if (logger) {
2511
2614
  logger({ type: 'request_blocked', deviceId: deviceId, score: finalScore, vector: suspicionVector, timestamp: Date.now() });
2512
2615
  }
2513
- return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
2616
+ const decision = { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
2617
+ if (this.dryRun) {
2618
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
2619
+ decision.intendedAction = decision.action;
2620
+ decision.action = 'next';
2621
+ delete decision.status;
2622
+ delete decision.body;
2623
+ }
2624
+ return decision;
2514
2625
  }
2515
2626
 
2516
2627
  // Honeypot: Check if the request is for a trap URL generated in a previous challenge.
@@ -2526,7 +2637,15 @@ export class FingerprintEngine {
2526
2637
  logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now(), vector: { honeypotScore: 100 } });
2527
2638
  }
2528
2639
  await store.set(`device:${cookies.device_id}`, deviceData); // No TTL for condemned status
2529
- return { action: 'block', status: 404, score: 100, vector: { honeypotScore: 100 } };
2640
+ const decision = { action: 'block', status: 404, score: 100, vector: { honeypotScore: 100 } };
2641
+ if (this.dryRun) {
2642
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
2643
+ decision.intendedAction = decision.action;
2644
+ decision.action = 'next';
2645
+ delete decision.status;
2646
+ delete decision.body;
2647
+ }
2648
+ return decision;
2530
2649
  }
2531
2650
 
2532
2651
  // --- NOUVELLE LOGIQUE DE RE-CHALLENGE ---
@@ -2555,7 +2674,15 @@ export class FingerprintEngine {
2555
2674
  suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
2556
2675
  // Recalculate the final score with the updated vector.
2557
2676
  const newFinalScore = this.calculateFinalScore(suspicionVector);
2558
- return { action: 'block', status: 404, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
2677
+ const decision = { action: 'block', status: 404, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
2678
+ if (this.dryRun) {
2679
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
2680
+ decision.intendedAction = decision.action;
2681
+ decision.action = 'next';
2682
+ delete decision.status;
2683
+ delete decision.body;
2684
+ }
2685
+ return decision;
2559
2686
  }
2560
2687
 
2561
2688
  // --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
@@ -2583,11 +2710,18 @@ export class FingerprintEngine {
2583
2710
  }
2584
2711
  };
2585
2712
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
2586
- } else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi
2713
+ } else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi
2714
+ const decision = { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404 };
2715
+ if (this.dryRun) {
2716
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
2717
+ decision.intendedAction = decision.action;
2718
+ decision.action = 'next';
2719
+ delete decision.status;
2720
+ return decision;
2721
+ }
2587
2722
  // Generate some trap URLs to embed in the challenge page.
2588
2723
  // These links are visually hidden but present in the DOM to trap bots.
2589
- const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
2590
- const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
2724
+ const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce)); // Génère les URL
2591
2725
 
2592
2726
  // On passe la configuration pour que la difficulté soit calculée correctement.
2593
2727
  const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
@@ -2653,16 +2787,17 @@ export class FingerprintEngine {
2653
2787
  }
2654
2788
  };
2655
2789
  this._log('API challenge response generated', { challengePayload });
2656
- return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
2790
+ decision.body = challengePayload;
2657
2791
  } else {
2658
2792
  // For browsers, send the HTML page.
2659
- const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer, originalFingerprint);
2660
- this._log('Browser challenge page generated', {
2661
- pageLength: page.length,
2662
- hasTrapContainer: true
2793
+ const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapUrls, originalFingerprint);
2794
+ this._log('Browser challenge page generated', {
2795
+ pageLength: page.length,
2796
+ trapUrlsInjected: trapUrls.length
2663
2797
  });
2664
- return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: page };
2798
+ decision.body = page;
2665
2799
  }
2800
+ return decision;
2666
2801
  }
2667
2802
  }
2668
2803
 
@@ -2673,7 +2808,7 @@ export class FingerprintEngine {
2673
2808
  logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
2674
2809
  }
2675
2810
 
2676
- return { action: 'next', score: finalScore, vector: suspicionVector };
2811
+ return { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
2677
2812
  }
2678
2813
 
2679
2814
  /**
@@ -3018,8 +3153,11 @@ export const powMiddleware = (securityConfig) => {
3018
3153
  const engine = new FingerprintEngine(securityConfig);
3019
3154
 
3020
3155
  // Initialize the problem manager with the configured path, if provided.
3021
- if (securityConfig.enableUsefulWork && securityConfig.usefulWorkConfigPath) {
3022
- getProblemManager(securityConfig.usefulWorkConfigPath, store); // This correctly initializes the singleton
3156
+ if (securityConfig.enableUsefulWork) {
3157
+ getProblemManager({
3158
+ configPath: securityConfig.usefulWorkConfigPath,
3159
+ config: securityConfig.usefulWorkConfig
3160
+ }, store);
3023
3161
  }
3024
3162
 
3025
3163
  if (securityConfig.autotuning) {
@@ -3069,6 +3207,7 @@ export const powMiddleware = (securityConfig) => {
3069
3207
  req.fingerprint = {
3070
3208
  score: decision.score,
3071
3209
  vector: decision.vector,
3210
+ intendedAction: decision.intendedAction, // Add intended action for logging
3072
3211
  };
3073
3212
 
3074
3213
  // After getSuspicionVector runs, it might have attached cookies to be set.
@@ -3119,6 +3258,7 @@ export const __internal = {
3119
3258
  getCrossLayerInconsistency, // Expose for testing
3120
3259
  // Expose page generators for security testing
3121
3260
  getTimeInconsistencyScore,
3261
+ getClickVarianceScore, // NOUVEAU: Expose pour les tests
3122
3262
  getTlsFingerprint, // NOUVEAU: Expose pour les tests
3123
3263
  getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
3124
3264
  generateCpuTargetChallengePage,
@@ -3138,8 +3278,9 @@ let lastBestSolution = null; // NOUVEAU: Stocke la meilleure solution trouvée
3138
3278
  * @param {Array<object>} trafficData - The array containing traffic logs.
3139
3279
  * @param {number} minDataPoints - The minimum number of data points required to start optimization.
3140
3280
  * @param {number} maxDataPoints - The maximum number of data points to keep after an optimization cycle.
3281
+ * @param {string} [savePath] - Optional path to save the best configuration to a file.
3141
3282
  */
3142
- function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
3283
+ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
3143
3284
  const highConfidenceLogs = trafficData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
3144
3285
  const highConfidenceRatio = trafficData.length > 0 ? highConfidenceLogs / trafficData.length : 0;
3145
3286
  const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
@@ -3234,6 +3375,17 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
3234
3375
  console.log("[AutoTuning] Nouveaux seuils :", securityConfig.thresholds);
3235
3376
  console.log("[AutoTuning] Nouveaux poids :", securityConfig.weights);
3236
3377
  console.log("[AutoTuning] Nouveaux patterns :", securityConfig.patterns);
3378
+
3379
+ // NOUVEAU: Sauvegarder la meilleure configuration si un chemin est fourni.
3380
+ if (savePath) {
3381
+ try {
3382
+ const configToSave = JSON.stringify(bestSolution.solution, null, 2);
3383
+ fs.writeFileSync(savePath, configToSave, 'utf-8');
3384
+ console.log(`[AutoTuning] Meilleure configuration sauvegardée dans : ${savePath}`);
3385
+ } catch (error) {
3386
+ console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
3387
+ }
3388
+ }
3237
3389
  }
3238
3390
 
3239
3391
  /**
@@ -3245,6 +3397,7 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
3245
3397
  * @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
3246
3398
  * @param {number} [options.minDataPoints=200] - The minimum number of requests to have before starting a cycle (default: 200).
3247
3399
  * @param {number} [options.maxDataPoints=10000] - The maximum number of log entries to keep in memory (default: 10,000).
3400
+ * @param {string} [options.savePath] - Optional. If provided, the best configuration found will be saved to this file path.
3248
3401
  */
3249
3402
  export function startThresholdAutoTuning(options) {
3250
3403
  if (autoTuningJobId) {
@@ -3257,7 +3410,8 @@ export function startThresholdAutoTuning(options) {
3257
3410
  trafficData,
3258
3411
  interval = 1800000, // 30 minutes
3259
3412
  minDataPoints = 200,
3260
- maxDataPoints = 10000 // Limite par défaut à 10 000 entrées
3413
+ maxDataPoints = 10000, // Limite par défaut à 10 000 entrées
3414
+ savePath, // NOUVEAU: Chemin de sauvegarde optionnel
3261
3415
  } = options;
3262
3416
 
3263
3417
  if (!securityConfig || !trafficData) {
@@ -3267,7 +3421,7 @@ export function startThresholdAutoTuning(options) {
3267
3421
  console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
3268
3422
 
3269
3423
  autoTuningJobId = setInterval(() => {
3270
- runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints);
3424
+ runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
3271
3425
  }, interval);
3272
3426
  }
3273
3427