@anonympins/fingerprint 0.3.1 → 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 (51) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +298 -37
  3. package/composer.json +38 -0
  4. package/index.js +5 -0
  5. package/package.json +100 -94
  6. package/phpunit.xml +20 -0
  7. package/public/fp.js +2 -0
  8. package/public/fp.wasm +0 -0
  9. package/src/js/build-client.js +69 -0
  10. package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -171
  11. package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -483
  12. package/src/js/fingerprint.client.obfuscated.js +1 -0
  13. package/{fingerprint.js → src/js/fingerprint.js} +338 -120
  14. package/{library.js → src/js/library.js} +1729 -1727
  15. package/{problem-manager.js → src/js/problem-manager.js} +539 -522
  16. package/src/php/AutoTuner.php +155 -0
  17. package/src/php/Challenge/ChallengeUtils.php +306 -0
  18. package/src/php/Config/SecurityProfiles.php +257 -0
  19. package/src/php/DirectFingerprint.php +81 -0
  20. package/src/php/FingerprintBuilder.php +185 -0
  21. package/src/php/FingerprintClient.php +118 -0
  22. package/src/php/FingerprintEngine.php +850 -0
  23. package/src/php/Optimization/FunctionRegistry.php +63 -0
  24. package/src/php/Optimization/Optimization.php +256 -0
  25. package/src/php/Optimization/OptimizationOperators.php +305 -0
  26. package/src/php/Optimization/ProblemInitializers.php +53 -0
  27. package/src/php/ProblemManager.php +255 -0
  28. package/src/php/RequestContext.php +87 -0
  29. package/src/php/Store/IStore.php +42 -0
  30. package/src/php/Store/InMemoryStore.php +67 -0
  31. package/src/php/Store/StoreManager.php +26 -0
  32. package/src/php/Tests/ChallengeUtilsTest.php +82 -0
  33. package/src/php/Tests/FingerprintBuilderTest.php +58 -0
  34. package/src/php/Tests/FingerprintEngineTest.php +219 -0
  35. package/src/php/Tests/PowTest.php +40 -0
  36. package/src/php/Tests/ProblemManagerTest.php +295 -0
  37. package/src/php/Tests/RequestUtilsTest.php +81 -0
  38. package/src/php/Tests/problems.config.json +9 -0
  39. package/src/php/Utils/BigInt.php +102 -0
  40. package/src/php/Utils/BlockList.php +100 -0
  41. package/src/php/Utils/Logger.php +30 -0
  42. package/src/php/Utils/MaliciousPatterns.php +59 -0
  43. package/src/php/Utils/RequestUtils.php +673 -0
  44. package/fingerprint.client.obfuscated.js +0 -1
  45. /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
  46. /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
  47. /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
  48. /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
  49. /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
  50. /package/{redis-store.js → src/js/redis-store.js} +0 -0
  51. /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: {
@@ -235,6 +232,38 @@ const getPowSolverCode = () => {
235
232
  return readFileSync(solverPath, 'utf-8');
236
233
  };
237
234
 
235
+ /**
236
+ * @private
237
+ * A mapping of IANA cipher suite names (as used by Node.js) to their decimal IDs.
238
+ * This is essential for correct JA3 fingerprint calculation.
239
+ * The list is not exhaustive but covers the most common cipher suites.
240
+ */
241
+ const cipherSuiteMap = {
242
+ 'TLS_AES_128_GCM_SHA256': 4865,
243
+ 'TLS_AES_256_GCM_SHA384': 4866,
244
+ 'TLS_CHACHA20_POLY1305_SHA256': 4867,
245
+ 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256': 49195,
246
+ 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256': 49199,
247
+ 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384': 49196,
248
+ 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384': 49200,
249
+ 'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256': 52393,
250
+ 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256': 52392,
251
+ 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 49171,
252
+ 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 49172,
253
+ 'TLS_RSA_WITH_AES_128_GCM_SHA256': 156,
254
+ 'TLS_RSA_WITH_AES_256_GCM_SHA384': 157,
255
+ 'TLS_RSA_WITH_AES_128_CBC_SHA': 47,
256
+ 'TLS_RSA_WITH_AES_256_CBC_SHA': 53,
257
+ // Older/Less common suites
258
+ 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA': 49161,
259
+ 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA': 49162,
260
+ 'TLS_DHE_RSA_WITH_AES_128_GCM_SHA256': 158,
261
+ 'TLS_DHE_RSA_WITH_AES_256_GCM_SHA384': 159,
262
+ 'TLS_DHE_RSA_WITH_AES_128_CBC_SHA': 51,
263
+ 'TLS_DHE_RSA_WITH_AES_256_CBC_SHA': 57,
264
+ 'TLS_RSA_WITH_3DES_EDE_CBC_SHA': 10,
265
+ };
266
+
238
267
  /**
239
268
  * Extracts TLS fingerprints (JA3 and JA4) from request context.
240
269
  * Prioritizes headers from reverse proxies (x-ja4-hash) and falls back to JA3 calculation
@@ -265,15 +294,19 @@ function getTlsFingerprint(context) {
265
294
 
266
295
  // The official JA3 spec includes the TLS version.
267
296
  // Node.js provides it as a string like 'TLSv1.3', we need the corresponding decimal value.
268
- const tlsVersionMap = {
297
+ const tlsVersionMap = { // NOSONAR
269
298
  'TLSv1': 769, 'TLSv1.1': 770, 'TLSv1.2': 771, 'TLSv1.3': 772
270
299
  };
271
300
  const tlsVersionId = tlsVersionMap[version] || 0;
272
301
 
302
+ // Convert cipher suite names to their decimal IDs.
303
+ const cipherIds = Array.isArray(ciphers)
304
+ ? ciphers.map(c => cipherSuiteMap[c.name] || c).join('-') // Use the raw ID if name is not in map
305
+ : '';
306
+
273
307
  const ja3String = [
274
308
  tlsVersionId,
275
- // The ciphers array from clientHello is an array of objects, not just IDs.
276
- Array.isArray(ciphers) ? ciphers.join('-') : '',
309
+ cipherIds,
277
310
  extensions?.join('-') || '',
278
311
  ellipticCurves?.join('-') || '',
279
312
  ellipticCurvePointFormats?.join('-') || ''
@@ -621,47 +654,6 @@ export const verifyTspChallenge = (
621
654
  }
622
655
  };
623
656
 
624
- /**
625
- * Generates the HTML content for the CPU PoW challenge (SHA-256).
626
- */
627
- const generateCpuPoWChallenge = (
628
- clientIp,
629
- nonce,
630
- difficulty = 4,
631
- path = "",
632
- ) => {
633
- return `
634
- <html>
635
- <head><title>Security Check</title></head>
636
- <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
637
- <h1>One moment... (Level 1)</h1>
638
- <p>We are verifying that you are not a bot. This takes a few seconds.</p>
639
- <div id="loader" style="margin:20px;">⚙️ Performing CPU security calculation...</div>
640
- <script>
641
- async function solve() {
642
- const ip = "${clientIp}";
643
- const nonce = "${nonce}";
644
- const diff = ${difficulty};
645
- const target = "0".repeat(diff);
646
- let solution = 0;
647
-
648
- while (true) {
649
- const msg = "${ip}" + ":" + "${nonce}" + ":" + solution;
650
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
651
- const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
652
- if (hash.startsWith(target)) break;
653
- solution++;
654
- if (solution % 100000 === 0) await new Promise(resolve => setTimeout(resolve, 0)); // To avoid freezing the browser
655
- }
656
- window.location.href = "${path}" + "?pow_type=cpu&pow_nonce=" + nonce + "&pow_solution=" + solution;
657
- }
658
- solve();
659
- </script>
660
- </body>
661
- </html>
662
- `;
663
- };
664
-
665
657
  /**
666
658
  * Generates the HTML content for a memory-intensive PoW challenge.
667
659
  */
@@ -906,7 +898,7 @@ function getHoneypotScore(context, honeypotConfig = {}) {
906
898
  */
907
899
  const injectionPatterns = {
908
900
  // SQL/NoSQL injections, including time-based attacks
909
- sql: /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i,
901
+ sql: /(\$ne|\' *OR *\'1\'=\'1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|(?:SLEEP|BENCHMARK)\s*\(|WAITFOR DELAY)/i,
910
902
  // Log4Shell (JNDI injection)
911
903
  log4shell: /\$\{jndi:(ldap|rmi|dns):/i,
912
904
  // Server-Side Template Injection (SSTI) for engines like Jinja2, Twig, etc.
@@ -919,6 +911,68 @@ const injectionPatterns = {
919
911
  rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
920
912
  };
921
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
+
922
976
  /**
923
977
  * Calcule un score basé sur les métriques comportementales envoyées par le client.
924
978
  * @param {object} context - Le contexte de la requête, contenant les en-têtes.
@@ -939,8 +993,11 @@ function getBehaviorScore(context) {
939
993
  return { behaviorScore: 100 };
940
994
  }
941
995
 
942
- // 2. Pénalité pour absence totale d'interaction.
943
- 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) {
944
1001
  score += 40;
945
1002
  }
946
1003
 
@@ -955,35 +1012,28 @@ function getBehaviorScore(context) {
955
1012
  score -= 10; // Petit bonus pour une navigation de base.
956
1013
  }
957
1014
  }
958
- // 3. Vérification de la plausibilité et de la distribution des métriques.
959
- // Un bot pourrait envoyer des valeurs aléatoires, mais elles ne suivront probablement pas
960
- // des distributions naturelles (comme la loi de Benford pour les premiers chiffres).
961
1015
 
962
- // Plausibilité de l'entropie de la souris
963
- if (metrics.mouseEntropy > 0 && metrics.mouseEntropy < 0.1) score += 20; // Entropie très faible, suspect.
964
- 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
+ }
965
1023
 
966
1024
  // Plausibilité de la latence de frappe
967
1025
  if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
968
1026
  if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
969
1027
 
970
1028
  // 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
971
- // On utilise les décimales pour avoir plus de chiffres à analyser.
972
- const mouseEntropyStr = String(metrics.mouseEntropy).replace(".", "");
973
- const keystrokeLatencyStr = String(metrics.keystrokeLatency).replace(".", "");
974
-
975
- if (mouseEntropyStr.length > 2) {
976
- const mouseDeviation = Optimization.Operators.benfordTest(mouseEntropyStr);
977
- // Une déviation > 0.15 est fortement suspecte.
978
- if (mouseDeviation > 0.15) score += 40;
979
- }
980
-
981
- if (keystrokeLatencyStr.length > 2) {
982
- const keystrokeDeviation = Optimization.Operators.benfordTest(keystrokeLatencyStr);
983
- 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
+ }
984
1034
  }
985
1035
 
986
- return { behaviorScore: Math.max(0, Math.min(100, score)) }; // Assure que le score reste entre 0 et 100
1036
+ return { behaviorScore: Math.min(100, score) }; // Assure que le score ne dépasse pas 100, mais peut être négatif (bonus)
987
1037
  } catch (e) {
988
1038
  return { behaviorScore: 10 }; // En-tête malformé = légèrement suspect.
989
1039
  }
@@ -1116,6 +1166,64 @@ function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerprint) {
1116
1166
  return { tlsSpoofingScore: 0 };
1117
1167
  }
1118
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
+
1119
1227
 
1120
1228
  /**
1121
1229
  * Calcule un score basé sur la détection explicite de frameworks d'automatisation.
@@ -1475,10 +1583,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
1475
1583
  // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
1476
1584
  const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
1477
1585
 
1478
- // NOUVEAU: On calcule le score de spoofing TLS.
1479
1586
  const { tlsSpoofingScore } = getTlsSpoofingScore(context);
1480
1587
 
1481
- // NOUVEAU: On appelle getBotScore pour détecter les marqueurs d'automatisation.
1482
1588
  const { botScore } = getBotScore(context);
1483
1589
 
1484
1590
  // NOUVEAU: On calcule le score d'incohérence temporelle.
@@ -1487,6 +1593,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
1487
1593
  // NOUVEAU: On calcule le score d'incohérence entre les couches.
1488
1594
  const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
1489
1595
 
1596
+ // NOUVEAU: On calcule le score de variance des clics.
1597
+ const { clickVarianceScore } = getClickVarianceScore(context);
1598
+
1490
1599
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
1491
1600
 
1492
1601
  // Save the updated device state to the store
@@ -1499,7 +1608,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
1499
1608
  deviceData.ips = new Set(deviceData.ips);
1500
1609
  }
1501
1610
  // Le vecteur de suspicion est maintenant complet.
1502
- 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 };
1503
1612
  };
1504
1613
 
1505
1614
  // A residential user can change networks (home, 4G, public wifi).
@@ -1662,7 +1771,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1662
1771
  * @param {string} clientIp - The client's IP address.
1663
1772
  * @returns {string} HTML content.
1664
1773
  */
1665
- function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml, originalFingerprint) {
1774
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
1666
1775
  const { nonce, target, path } = cpuChallengeDetails;
1667
1776
  const solverCode = getPowSolverCode();
1668
1777
  // On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
@@ -1671,6 +1780,13 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1671
1780
  const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
1672
1781
  const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
1673
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
+
1674
1790
  const challengeScript = `
1675
1791
  async function solve() {
1676
1792
  const nonce = ${JSON.stringify(nonce)};
@@ -1702,6 +1818,12 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1702
1818
  const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
1703
1819
  window.location.href = finalUrl;
1704
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
+
1705
1827
  solve();
1706
1828
  `;
1707
1829
 
@@ -1717,13 +1839,12 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1717
1839
  }
1718
1840
 
1719
1841
  if (!htmlTemplate) {
1720
- 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
1721
1843
  }
1722
1844
 
1723
1845
  return htmlTemplate
1724
1846
  .replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
1725
- .replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript)
1726
- .replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
1847
+ .replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
1727
1848
  }
1728
1849
 
1729
1850
  /**
@@ -1825,6 +1946,7 @@ export class FingerprintEngine {
1825
1946
  this._allowlist = this._buildAllowlist();
1826
1947
  this._validateConfig(securityConfig); // Validate the configuration
1827
1948
  this.verbose = securityConfig.verbose || false;
1949
+ this.dryRun = securityConfig.dryRun || false;
1828
1950
  }
1829
1951
 
1830
1952
  /**
@@ -1842,7 +1964,7 @@ export class FingerprintEngine {
1842
1964
  'weights', 'thresholds', 'cpu', 'ticketMaxAge', 'challengeTtl',
1843
1965
  'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
1844
1966
  'honeypot', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger',
1845
- 'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist',
1967
+ 'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
1846
1968
  'similarityThreshold'
1847
1969
  ]);
1848
1970
 
@@ -1882,7 +2004,8 @@ export class FingerprintEngine {
1882
2004
  (suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
1883
2005
  (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
1884
2006
  (suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
1885
- (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0);
2007
+ (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
2008
+ (suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0);
1886
2009
 
1887
2010
  return Math.min(100, score);
1888
2011
  }
@@ -2203,7 +2326,15 @@ export class FingerprintEngine {
2203
2326
  if (onDeviceCompromised) {
2204
2327
  onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
2205
2328
  }
2206
- 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;
2207
2338
  }
2208
2339
 
2209
2340
  // The engine now works with the context directly, no more rawReq dependency here.
@@ -2404,7 +2535,15 @@ export class FingerprintEngine {
2404
2535
  const newBlockThreshold = thresholds.block ?? 95;
2405
2536
  if (finalScore >= newBlockThreshold) {
2406
2537
  this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
2407
- 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;
2408
2547
  }
2409
2548
  // If not blocked, the request will proceed to be re-challenged.
2410
2549
  }
@@ -2474,7 +2613,15 @@ export class FingerprintEngine {
2474
2613
  if (logger) {
2475
2614
  logger({ type: 'request_blocked', deviceId: deviceId, score: finalScore, vector: suspicionVector, timestamp: Date.now() });
2476
2615
  }
2477
- 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;
2478
2625
  }
2479
2626
 
2480
2627
  // Honeypot: Check if the request is for a trap URL generated in a previous challenge.
@@ -2490,7 +2637,15 @@ export class FingerprintEngine {
2490
2637
  logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now(), vector: { honeypotScore: 100 } });
2491
2638
  }
2492
2639
  await store.set(`device:${cookies.device_id}`, deviceData); // No TTL for condemned status
2493
- 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;
2494
2649
  }
2495
2650
 
2496
2651
  // --- NOUVELLE LOGIQUE DE RE-CHALLENGE ---
@@ -2519,7 +2674,15 @@ export class FingerprintEngine {
2519
2674
  suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
2520
2675
  // Recalculate the final score with the updated vector.
2521
2676
  const newFinalScore = this.calculateFinalScore(suspicionVector);
2522
- 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;
2523
2686
  }
2524
2687
 
2525
2688
  // --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
@@ -2547,11 +2710,18 @@ export class FingerprintEngine {
2547
2710
  }
2548
2711
  };
2549
2712
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
2550
- } 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
+ }
2551
2722
  // Generate some trap URLs to embed in the challenge page.
2552
2723
  // These links are visually hidden but present in the DOM to trap bots.
2553
- const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
2554
- 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
2555
2725
 
2556
2726
  // On passe la configuration pour que la difficulté soit calculée correctement.
2557
2727
  const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
@@ -2617,16 +2787,17 @@ export class FingerprintEngine {
2617
2787
  }
2618
2788
  };
2619
2789
  this._log('API challenge response generated', { challengePayload });
2620
- return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
2790
+ decision.body = challengePayload;
2621
2791
  } else {
2622
2792
  // For browsers, send the HTML page.
2623
- 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);
2624
- this._log('Browser challenge page generated', {
2625
- pageLength: page.length,
2626
- 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
2627
2797
  });
2628
- return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: page };
2798
+ decision.body = page;
2629
2799
  }
2800
+ return decision;
2630
2801
  }
2631
2802
  }
2632
2803
 
@@ -2637,7 +2808,7 @@ export class FingerprintEngine {
2637
2808
  logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
2638
2809
  }
2639
2810
 
2640
- return { action: 'next', score: finalScore, vector: suspicionVector };
2811
+ return { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
2641
2812
  }
2642
2813
 
2643
2814
  /**
@@ -2982,8 +3153,11 @@ export const powMiddleware = (securityConfig) => {
2982
3153
  const engine = new FingerprintEngine(securityConfig);
2983
3154
 
2984
3155
  // Initialize the problem manager with the configured path, if provided.
2985
- if (securityConfig.enableUsefulWork && securityConfig.usefulWorkConfigPath) {
2986
- 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);
2987
3161
  }
2988
3162
 
2989
3163
  if (securityConfig.autotuning) {
@@ -3033,6 +3207,7 @@ export const powMiddleware = (securityConfig) => {
3033
3207
  req.fingerprint = {
3034
3208
  score: decision.score,
3035
3209
  vector: decision.vector,
3210
+ intendedAction: decision.intendedAction, // Add intended action for logging
3036
3211
  };
3037
3212
 
3038
3213
  // After getSuspicionVector runs, it might have attached cookies to be set.
@@ -3083,6 +3258,7 @@ export const __internal = {
3083
3258
  getCrossLayerInconsistency, // Expose for testing
3084
3259
  // Expose page generators for security testing
3085
3260
  getTimeInconsistencyScore,
3261
+ getClickVarianceScore, // NOUVEAU: Expose pour les tests
3086
3262
  getTlsFingerprint, // NOUVEAU: Expose pour les tests
3087
3263
  getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
3088
3264
  generateCpuTargetChallengePage,
@@ -3093,6 +3269,7 @@ export const __internal = {
3093
3269
  // --- THRESHOLD AUTO-TUNING SECTION ---
3094
3270
 
3095
3271
  let autoTuningJobId = null;
3272
+ let lastBestSolution = null; // NOUVEAU: Stocke la meilleure solution trouvée
3096
3273
 
3097
3274
  /**
3098
3275
  * Executes a threshold optimization pass using collected traffic data.
@@ -3101,8 +3278,9 @@ let autoTuningJobId = null;
3101
3278
  * @param {Array<object>} trafficData - The array containing traffic logs.
3102
3279
  * @param {number} minDataPoints - The minimum number of data points required to start optimization.
3103
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.
3104
3282
  */
3105
- function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
3283
+ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
3106
3284
  const highConfidenceLogs = trafficData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
3107
3285
  const highConfidenceRatio = trafficData.length > 0 ? highConfidenceLogs / trafficData.length : 0;
3108
3286
  const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
@@ -3151,35 +3329,63 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
3151
3329
 
3152
3330
  /**
3153
3331
  * Met à jour un objet de configuration (ex: thresholds, weights) en douceur.
3332
+ * Cette nouvelle version préserve la proportionnalité des valeurs initiales.
3154
3333
  * @param {object} currentConfig - La configuration actuelle à modifier.
3155
3334
  * @param {object} targetConfig - La configuration cible proposée par l'optimiseur.
3156
3335
  */
3157
3336
  const applyInertialUpdate = (currentConfig, targetConfig) => {
3158
- if (!currentConfig || !targetConfig) return; // Vérifier aussi currentConfig
3159
- for (const key in targetConfig) {
3160
- if (Object.hasOwnProperty.call(currentConfig, key)) {
3161
- const currentValue = currentConfig[key];
3162
- const targetValue = targetConfig[key];
3163
- const delta = targetValue - currentValue;
3164
- const maxChange = Math.abs(currentValue * MAX_CHANGE_VELOCITY);
3165
-
3166
- // Limite le changement à la vélocité maximale
3167
- const change = Math.max(-maxChange, Math.min(maxChange, delta));
3168
-
3169
- currentConfig[key] += change;
3170
- }
3337
+ if (!currentConfig || !targetConfig) return; // Vérifier aussi currentConfig
3338
+
3339
+ // --- NOUVELLE LOGIQUE PROPORTIONNELLE ---
3340
+ let totalCurrentWeight = 0;
3341
+ let totalTargetWeight = 0;
3342
+
3343
+ // 1. Calculer la somme des poids actuels et cibles pour les clés communes.
3344
+ for (const key in currentConfig) {
3345
+ if (Object.hasOwnProperty.call(targetConfig, key)) {
3346
+ totalCurrentWeight += currentConfig[key];
3347
+ totalTargetWeight += targetConfig[key];
3171
3348
  }
3172
- };
3349
+ }
3350
+
3351
+ if (totalCurrentWeight === 0) return; // Éviter la division par zéro
3352
+
3353
+ // 2. Déterminer le ratio de changement global et le limiter par la vélocité.
3354
+ // Cela crée un "facteur d'ajustement" unique pour l'ensemble de la configuration.
3355
+ const globalChangeRatio = (totalTargetWeight - totalCurrentWeight) / totalCurrentWeight;
3356
+ const adjustmentFactor = Math.max(-MAX_CHANGE_VELOCITY, Math.min(MAX_CHANGE_VELOCITY, globalChangeRatio));
3173
3357
 
3358
+ // 3. Appliquer ce facteur à chaque valeur de la configuration actuelle.
3359
+ // Cela fait "glisser" l'ensemble de la configuration tout en préservant les proportions.
3360
+ for (const key in currentConfig) {
3361
+ if (Object.hasOwnProperty.call(targetConfig, key)) {
3362
+ currentConfig[key] *= (1 + adjustmentFactor);
3363
+ }
3364
+ }
3365
+ };
3174
3366
  applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds);
3175
3367
  applyInertialUpdate(securityConfig.weights, newConfig.weights);
3176
3368
  applyInertialUpdate(securityConfig.patterns, newConfig.patterns);
3177
3369
 
3370
+ // NOUVEAU: Stocker la meilleure solution pour une consultation externe
3371
+ lastBestSolution = bestSolution;
3372
+
3178
3373
  console.log("[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.");
3179
3374
  console.log("[AutoTuning] Objectifs atteints :", { falsePositiveRate: bestSolution.objectives[0].toFixed(4), falseNegativeRate: bestSolution.objectives[1].toFixed(4) });
3180
3375
  console.log("[AutoTuning] Nouveaux seuils :", securityConfig.thresholds);
3181
3376
  console.log("[AutoTuning] Nouveaux poids :", securityConfig.weights);
3182
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
+ }
3183
3389
  }
3184
3390
 
3185
3391
  /**
@@ -3191,6 +3397,7 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
3191
3397
  * @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
3192
3398
  * @param {number} [options.minDataPoints=200] - The minimum number of requests to have before starting a cycle (default: 200).
3193
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.
3194
3401
  */
3195
3402
  export function startThresholdAutoTuning(options) {
3196
3403
  if (autoTuningJobId) {
@@ -3203,7 +3410,8 @@ export function startThresholdAutoTuning(options) {
3203
3410
  trafficData,
3204
3411
  interval = 1800000, // 30 minutes
3205
3412
  minDataPoints = 200,
3206
- 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
3207
3415
  } = options;
3208
3416
 
3209
3417
  if (!securityConfig || !trafficData) {
@@ -3213,7 +3421,7 @@ export function startThresholdAutoTuning(options) {
3213
3421
  console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
3214
3422
 
3215
3423
  autoTuningJobId = setInterval(() => {
3216
- runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints);
3424
+ runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
3217
3425
  }, interval);
3218
3426
  }
3219
3427
 
@@ -3228,3 +3436,13 @@ export function stopThresholdAutoTuning() {
3228
3436
  console.log("[AutoTuning] Job d'optimisation des seuils arrêté.");
3229
3437
  }
3230
3438
  }
3439
+
3440
+ /**
3441
+ * Returns the last best solution found by the auto-tuner.
3442
+ * This is useful for logging or creating a "finops" security configuration.
3443
+ * @export
3444
+ * @returns {object|null} The best solution object { solution, objectives } or null if no tuning has run.
3445
+ */
3446
+ export function getBestTuningSolution() {
3447
+ return lastBestSolution;
3448
+ }