@anonympins/fingerprint 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/fingerprint.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { BlockList } from "node:net";
3
3
  import dns from "node:dns/promises";
4
- import { problemManager } from "./problem-manager.js";
4
+ import { getProblemManager, problemManager } from "./problem-manager.js";
5
5
  import { Optimization } from "./library.js";
6
6
  import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
7
7
  import { readFileSync } from "node:fs";
@@ -349,7 +349,7 @@ function getCompositeDeviceHash(context) {
349
349
  const tcpFingerprint = context.headers['x-tcp-fingerprint'];
350
350
  if (tcpFingerprint) srv.add("tcp", tcpFingerprint);
351
351
 
352
- // 3. SIGNAUX DE HAUT NIVEAU (Applicatif) - Moins fiables, mais utiles pour la corroboration
352
+ // 3. SIGNAUX DE HAUT NIVEAU (Applicatif) Moins fiables, mais utiles pour la corroboration
353
353
  const headersToCapture = {
354
354
  "ch_ua": "sec-ch-ua",
355
355
  "ch_platform": "sec-ch-ua-platform",
@@ -385,6 +385,46 @@ function getCompositeDeviceHash(context) {
385
385
  }
386
386
  export { getCompositeDeviceHash };
387
387
 
388
+ /**
389
+ * @private
390
+ * A knowledge base of known TLS (JA3) fingerprints for common browsers.
391
+ * This helps in detecting inconsistencies between the TLS layer and the HTTP User-Agent.
392
+ * The key is the JA3 hash, and the value is the browser family.
393
+ * This list is not exhaustive but covers many common cases.
394
+ */
395
+ const tlsFingerprintDb = {
396
+ // --- Chrome (Desktop) ---
397
+ 'e188a442b87f422c5a1e80b05399435b': 'Chrome', // Chrome 107, Windows 10
398
+ 'd8e35855049321c6042a4325c697858f': 'Chrome', // Chrome 114, Windows 11
399
+ 'a9f90958d44533748c139a5d1895b925': 'Chrome', // Chrome 116, macOS
400
+ '3b5379916d2b3882253c42885956a350': 'Chrome', // Chrome 124, Linux
401
+
402
+ // --- Chrome (Mobile) ---
403
+ '59822058c95c33d2d06e52f410855c8c': 'Chrome', // Chrome 120, Android 13
404
+
405
+ // --- Firefox (Desktop) ---
406
+ 'b386946a5a586163c7c533636b45c355': 'Firefox', // Firefox 102, Windows 10
407
+ '66236495a523c1785f8f3a105b248b11': 'Firefox', // Firefox 115, Windows 11
408
+ 'b73d470006575b5e35167a0b5a8540e2': 'Firefox', // Firefox 121, macOS
409
+ '8443d7562933834333943465d52363cf': 'Firefox', // Firefox 125, Linux
410
+
411
+ // --- Firefox (Mobile) ---
412
+ '02720628957d38c6111a18433abe833f': 'Firefox', // Firefox 125, Android 14
413
+
414
+ // --- Safari & iOS (Shared TLS Stack) ---
415
+ // On iOS, all browsers (Chrome, Firefox, etc.) must use WebKit, which uses Apple's TLS stack.
416
+ // Therefore, they all share the same JA3 fingerprint as Safari on that OS version.
417
+ 'b633f21d532d35967c8753c38536b4d3': 'Safari', // Safari 16, macOS
418
+ '4d7a28d5f55b359b69100a311013f03e': ['Safari', 'Chrome', 'Firefox'], // Safari 17, iOS 17 (and other browsers on iOS 17)
419
+ '8dd3d7532873575314df23c447543001': ['Safari', 'Chrome', 'Firefox'], // Safari 17.4, iOS 17.4
420
+
421
+ // --- Common Libraries & Bots (for spoofing detection) ---
422
+ '47344a349b75c4e82333475553b5f358': 'Python', // Python 3.10 `requests` library
423
+ 'b29587b8a143c42546133ad7704b3310': 'Go', // Go 1.19 `http` library
424
+ 'd435b5223b2884c5a832b842637e245f': 'Java', // Java 11 `HttpClient`
425
+ 'c72366b9551263d990b7fa574225332c': 'curl', // curl 7.81.0
426
+ };
427
+
388
428
  // Fonctions utilitaires
389
429
  function parseUserAgent(ua) {
390
430
  // Parser basique du User-Agent
@@ -520,7 +560,10 @@ export const verifyTspChallenge = (
520
560
  targetMaxDistance,
521
561
  cities,
522
562
  ) => {
523
- try {
563
+ // Input validation: ensure the solution is a non-empty string before trying to parse it.
564
+ if (typeof solutionPathJson !== 'string' || solutionPathJson.length === 0) return false;
565
+
566
+ try {
524
567
  const solutionPath = JSON.parse(solutionPathJson);
525
568
  if (!Array.isArray(solutionPath) || solutionPath.length !== numCities)
526
569
  return false;
@@ -701,6 +744,13 @@ export const verifyPoWAndGenerateTicket = (
701
744
  * The server performs the same calculation to validate.
702
745
  */
703
746
  export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
747
+ // Hard cap on memory difficulty to prevent DoS attacks from malicious clients
748
+ // submitting an arbitrarily large difficulty value.
749
+ const MAX_ALLOWED_MEM_DIFFICULTY = 128; // 128MB
750
+ if (difficulty > MAX_ALLOWED_MEM_DIFFICULTY) {
751
+ console.warn(`[Security] Memory PoW verification attempt with excessive difficulty: ${difficulty}MB. Denied.`);
752
+ return false;
753
+ }
704
754
  const size = difficulty * 1024 * 1024;
705
755
  const iterations = size / 16;
706
756
  const buffer = new Uint32Array(size / 4);
@@ -720,8 +770,9 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
720
770
  return finalHash === parseInt(solution, 10);
721
771
  };
722
772
  export const isTicketValid = (ip, ticket) => {
723
- if (!ticket) return false;
724
- const [expiry, sig] = ticket.split(":");
773
+ // Input validation: ensure the ticket is a non-empty string with the correct format.
774
+ if (typeof ticket !== 'string' || !ticket.includes(':')) return false;
775
+ const [expiry, sig] = ticket.split(':');
725
776
  if (!expiry || !sig || Date.now() > parseInt(expiry, 10)) return false;
726
777
  const expectedSig = crypto
727
778
  .createHmac("sha256", getPowSecret())
@@ -1023,36 +1074,46 @@ function getCrossLayerInconsistency(context) {
1023
1074
  * @returns {{tlsSpoofingScore: number}}
1024
1075
  */
1025
1076
  function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerprint) {
1026
- let score = 0;
1027
1077
  const { ja3, ja4 } = getTlsFingerprintFn(context) || { ja3: null, ja4: null }; // Defensive check
1028
1078
  const ua = context.headers["user-agent"] || '';
1029
1079
 
1030
- // Si un fingerprint TLS est présent, mais le User-Agent est générique ou manquant.
1080
+ // 1. Penalize if a TLS fingerprint is present but the User-Agent is generic or missing.
1081
+ // This is a strong indicator of a non-browser client trying to look legitimate.
1031
1082
  if ((ja3 || ja4) && (!ua || ua.length < 10 || ua.toLowerCase().includes('python') || ua.toLowerCase().includes('curl'))) {
1032
- score += 50; // Forte suspicion
1083
+ return { tlsSpoofingScore: 50 };
1033
1084
  }
1034
1085
 
1035
- // Plus complexe: Comparer le navigateur/OS déduit du JA3/JA4 avec le User-Agent.
1036
- // Ceci nécessiterait une base de données de JA3/JA4 connus ou une logique de parsing avancée.
1037
- // Pour l'instant, une implémentation simplifiée:
1038
- // Si JA3/JA4 est présent et le UA est un navigateur connu, mais ils ne correspondent pas.
1086
+ // 2. If no JA3 hash is available, we cannot perform the consistency check.
1039
1087
  if (ja3 && ua) {
1040
- // Exemple très simplifié: si JA3 est typique de Chrome, mais UA est Firefox.
1041
- // Ceci est une heuristique et peut générer des faux positifs sans une base de données robuste.
1042
- // JA3 de Chrome commence souvent par 'e' (TLS 1.3) ou 'd' (TLS 1.2)
1043
- const isJa3Chrome = ja3.startsWith('e') || ja3.startsWith('d');
1044
- const isUaChrome = ua.includes('Chrome') && !ua.includes('Edg');
1045
- // JA3 de Firefox commence souvent par 'c' (TLS 1.3) ou 'b' (TLS 1.2)
1046
- const isJa3Firefox = ja3.startsWith('c') || ja3.startsWith('b');
1047
- const isUaFirefox = ua.includes('Firefox');
1048
-
1049
- if ((isJa3Chrome && isUaFirefox) || (isJa3Firefox && isUaChrome)) {
1050
- score += 80; // Très forte incohérence
1088
+ // Look up the expected browser family (or families) from our database.
1089
+ let expectedBrowsers = tlsFingerprintDb[ja3];
1090
+
1091
+ if (expectedBrowsers) {
1092
+ // Ensure it's always an array for consistent logic.
1093
+ if (!Array.isArray(expectedBrowsers)) {
1094
+ expectedBrowsers = [expectedBrowsers];
1095
+ }
1096
+
1097
+ // Parse the User-Agent to get the claimed browser.
1098
+ const { browser: claimedBrowser } = parseUserAgent(ua);
1099
+
1100
+ // Check if the claimed browser is one of the legitimate possibilities for this JA3 hash.
1101
+ // We use `some` to see if the claimed browser starts with any of the expected browser names.
1102
+ // (e.g., "Chrome/116" starts with "Chrome").
1103
+ const isMatch = expectedBrowsers.some(expected => claimedBrowser?.startsWith(expected));
1104
+
1105
+ if (claimedBrowser && !isMatch) {
1106
+ return { tlsSpoofingScore: 80 }; // High score for a clear mismatch.
1107
+ }
1051
1108
  }
1052
1109
  }
1053
- // On pourrait ajouter des vérifications similaires pour JA4 si on avait une base de données de JA4.
1054
1110
 
1055
- return { tlsSpoofingScore: Math.min(100, score) };
1111
+ // If we reach here, either:
1112
+ // - No JA3 was available.
1113
+ // - The JA3 was not in our database (we can't make a decision).
1114
+ // - The JA3 and User-Agent were consistent.
1115
+ // In all these cases, the score is 0.
1116
+ return { tlsSpoofingScore: 0 };
1056
1117
  }
1057
1118
 
1058
1119
 
@@ -1733,15 +1794,74 @@ export function verifyCpuTargetPoWAndGenerateTicket(
1733
1794
  return null;
1734
1795
  }
1735
1796
 
1797
+ /**
1798
+ * @private
1799
+ * Parses a GraphQL query string to extract the operation type and name.
1800
+ * Uses a lightweight regex to avoid pulling in a heavy AST parser.
1801
+ * @param {object} body - The request body, which might contain the query.
1802
+ * @returns {{type: string, name: string}|null}
1803
+ */
1804
+ function parseGraphQLQuery(body) {
1805
+ const query = body?.query;
1806
+ if (typeof query !== 'string') {
1807
+ return null;
1808
+ }
1809
+ // Regex to capture operation type (query, mutation, subscription) and optional operation name.
1810
+ // Handles whitespace and potential comments.
1811
+ const match = query.match(/(?:^|\s)(query|mutation|subscription)\s+([_A-Za-z][_0-9A-Za-z]*)?/);
1812
+ if (match) {
1813
+ return {
1814
+ type: match[1],
1815
+ name: match[2] || 'Anonymous', // Default to 'Anonymous' if name is missing
1816
+ };
1817
+ }
1818
+ return null;
1819
+ }
1736
1820
  export class FingerprintEngine {
1737
1821
  constructor(securityConfig) {
1738
1822
  const isProduction = process.env.NODE_ENV === 'production';
1739
1823
  this.securityConfig = securityConfig;
1740
1824
  this.isProduction = isProduction;
1741
1825
  this._allowlist = this._buildAllowlist();
1826
+ this._validateConfig(securityConfig); // Validate the configuration
1742
1827
  this.verbose = securityConfig.verbose || false;
1743
1828
  }
1744
1829
 
1830
+ /**
1831
+ * Validates the security configuration object to detect potential typos or missing essential keys.
1832
+ * @private
1833
+ * @param {object} config - The security configuration object.
1834
+ */
1835
+ _validateConfig(config) {
1836
+ if (!config) {
1837
+ console.warn('[Fingerprint] Warning: No securityConfig provided. Using default behaviors, which may not be secure.');
1838
+ return;
1839
+ }
1840
+
1841
+ const knownKeys = new Set([
1842
+ 'weights', 'thresholds', 'cpu', 'ticketMaxAge', 'challengeTtl',
1843
+ 'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
1844
+ 'honeypot', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger',
1845
+ 'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist',
1846
+ 'similarityThreshold'
1847
+ ]);
1848
+
1849
+ // 1. Check for essential keys
1850
+ if (!config.weights) {
1851
+ console.warn('[Fingerprint] Warning: `securityConfig.weights` is not defined. Suspicion scores will be 0.');
1852
+ }
1853
+ if (!config.thresholds) {
1854
+ console.warn('[Fingerprint] Warning: `securityConfig.thresholds` is not defined. Challenges may not be issued correctly.');
1855
+ }
1856
+
1857
+ // 2. Check for unknown (potentially misspelled) keys
1858
+ for (const key in config) {
1859
+ if (!knownKeys.has(key)) {
1860
+ console.warn(`[Fingerprint] Warning: Unknown key '${key}' found in securityConfig. This might be a typo.`);
1861
+ }
1862
+ }
1863
+ }
1864
+
1745
1865
  _log(message, data = {}) {
1746
1866
  if (this.verbose) {
1747
1867
  console.log(`[FingerprintEngine] ${message}`, data);
@@ -1911,6 +2031,41 @@ export class FingerprintEngine {
1911
2031
 
1912
2032
  return false;
1913
2033
  }
2034
+ /**
2035
+ * Checks if the GraphQL operation matches an entry in the GraphQL operation allowlist.
2036
+ * Supports wildcards for operation names.
2037
+ * @private
2038
+ * @param {string} operationType - The type of the GraphQL operation (e.g., 'query', 'mutation').
2039
+ * @param {string} operationName - The name of the GraphQL operation.
2040
+ * @returns {boolean} True if the operation is in the allowlist.
2041
+ */
2042
+ _isGraphqlOperationInAllowlist(operationType, operationName) {
2043
+ const { whitelist = [] } = this.securityConfig;
2044
+ const graphqlRule = whitelist.find(rule => rule.type === 'graphql_operation_allowlist');
2045
+
2046
+ if (!graphqlRule || !graphqlRule.entries || !operationType || !operationName) {
2047
+ return false;
2048
+ }
2049
+
2050
+ for (const entry of graphqlRule.entries) {
2051
+ const [entryType, entryName] = entry.split(':');
2052
+ if (entryType !== operationType) {
2053
+ continue;
2054
+ }
2055
+
2056
+ // Check for exact name match or full wildcard
2057
+ if (entryName === operationName || entryName === '*') {
2058
+ return true;
2059
+ }
2060
+ // Check for partial wildcard (e.g., "Search*")
2061
+ if (entryName.endsWith('*') && operationName.startsWith(entryName.slice(0, -1))) {
2062
+ return true;
2063
+ }
2064
+ }
2065
+
2066
+ return false;
2067
+ }
2068
+
1914
2069
  /**
1915
2070
  * Verifies if a request comes from a legitimate, whitelisted bot (e.g., Googlebot)
1916
2071
  * using reverse and forward DNS lookups. The result is cached.
@@ -1977,7 +2132,7 @@ export class FingerprintEngine {
1977
2132
 
1978
2133
  async processRequest(requestContext) {
1979
2134
 
1980
- const { clientIp = "unknown", path, cookies, query, isStatic } = requestContext;
2135
+ const { clientIp = "unknown", path, cookies, query, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
1981
2136
  const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
1982
2137
 
1983
2138
  this._log('Processing request', { clientIp, path, isStatic });
@@ -2012,6 +2167,12 @@ export class FingerprintEngine {
2012
2167
  return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'path_allowlist' } };
2013
2168
  }
2014
2169
 
2170
+ // 5. Check GraphQL operation allowlist.
2171
+ if (graphqlOperationType && this._isGraphqlOperationInAllowlist(graphqlOperationType, graphqlOperationName)) {
2172
+ this._log('GraphQL operation in allowlist - allowing request', { operation: `${graphqlOperationType}:${graphqlOperationName}` });
2173
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'graphql_operation_allowlist' } };
2174
+ }
2175
+
2015
2176
  const { pow_nonce } = query;
2016
2177
 
2017
2178
  // Honeypot: Direct probing of challenge endpoints is highly suspicious.
@@ -2181,6 +2342,15 @@ export class FingerprintEngine {
2181
2342
  }
2182
2343
  } else {
2183
2344
  this._log('Challenge context not found or expired', { pow_nonce });
2345
+ // --- NOUVELLE MESURE DE SÉCURITÉ ---
2346
+ // Si un client soumet un nonce invalide ou expiré, c'est une tentative de probing.
2347
+ // On applique une pénalité maximale pour bloquer ou re-challenger lourdement.
2348
+ suspicionVector.honeypotScore = 100;
2349
+ finalScore = this.calculateFinalScore(suspicionVector);
2350
+ this._log('Invalid nonce submitted (probing attempt) - applying max penalty', { newFinalScore: finalScore });
2351
+ // La logique continue vers la section `if (isValid)` qui échouera,
2352
+ // puis le score élevé sera utilisé pour bloquer ou re-challenger.
2353
+ isValid = false; // On s'assure que la validation échoue.
2184
2354
  }
2185
2355
  if (isValid) {
2186
2356
  // La solution est valide. On supprime le secret et on redirige.
@@ -2278,7 +2448,7 @@ export class FingerprintEngine {
2278
2448
  if (challengeContext) {
2279
2449
  try {
2280
2450
  const workResult = JSON.parse(pow_solution_work_result);
2281
- problemManager.integrateSolution(pow_problem_id, workResult);
2451
+ getProblemManager(this.securityConfig.usefulWorkConfigPath).integrateSolution(pow_problem_id, workResult);
2282
2452
 
2283
2453
  await store.delete(`secret:${pow_nonce}`);
2284
2454
  // Accorder un ticket de passage comme pour un PoW normal
@@ -2358,10 +2528,13 @@ export class FingerprintEngine {
2358
2528
 
2359
2529
  // Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
2360
2530
  // Cela rend l'automatisation plus difficile pour un attaquant.
2361
- if (isSuspicious && this.securityConfig.enableUsefulWork && Math.random() > 0.5) {
2531
+ // Utilisation de crypto pour un choix plus sécurisé.
2532
+ const shouldUseUsefulWork = this.securityConfig.enableUsefulWork && crypto.randomBytes(1).readUInt8(0) / 255 > 0.5;
2533
+
2534
+ if (isSuspicious && shouldUseUsefulWork) {
2362
2535
  this._log('Issuing a useful work challenge', { finalScore });
2363
2536
 
2364
- const { problemId, task } = problemManager.dispatchWork(suspicionFactor);
2537
+ const { problemId, task } = getProblemManager(this.securityConfig.usefulWorkConfigPath).dispatchWork(suspicionFactor);
2365
2538
 
2366
2539
  await store.set(`secret:${nonce}`, { clientSecret, originalPath: path }, 300);
2367
2540
 
@@ -2664,6 +2837,7 @@ export const xss_analyzer = async (data) => {
2664
2837
  */
2665
2838
  export const modsecurity_analyzer = (rulesPath) => {
2666
2839
  let wafInstance = null; // Singleton instance for the WAF
2840
+ let isModSecurityAvailable = true; // Flag specific to this analyzer instance
2667
2841
 
2668
2842
  return async (data) => {
2669
2843
  if (!rulesPath) {
@@ -2671,8 +2845,12 @@ export const modsecurity_analyzer = (rulesPath) => {
2671
2845
  return false;
2672
2846
  }
2673
2847
 
2848
+ if (!isModSecurityAvailable) {
2849
+ return false; // Skip if the module is known to be unavailable
2850
+ }
2851
+
2674
2852
  try {
2675
- if (!wafInstance) {
2853
+ if (!wafInstance && isModSecurityAvailable) {
2676
2854
  // Dynamically import the library only when needed.
2677
2855
  const { ModSecurity } = await import('modsecurity-nodejs');
2678
2856
  wafInstance = new ModSecurity();
@@ -2687,7 +2865,8 @@ export const modsecurity_analyzer = (rulesPath) => {
2687
2865
  return result !== null; // A non-null result means a threat was detected.
2688
2866
  } catch (error) {
2689
2867
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
2690
- console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is disabled. Run "npm install modsecurity-nodejs" to enable it.');
2868
+ console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is now disabled. Run "npm install modsecurity-nodejs" to enable it.');
2869
+ isModSecurityAvailable = false; // Disable for future calls
2691
2870
  }
2692
2871
  return false; // Assume data is safe if any error occurs.
2693
2872
  }
@@ -2802,6 +2981,11 @@ export const default_whitelist = () => [
2802
2981
  export const powMiddleware = (securityConfig) => {
2803
2982
  const engine = new FingerprintEngine(securityConfig);
2804
2983
 
2984
+ // 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
2987
+ }
2988
+
2805
2989
  if (securityConfig.autotuning) {
2806
2990
  startThresholdAutoTuning({
2807
2991
  securityConfig: securityConfig,
@@ -2834,6 +3018,15 @@ export const powMiddleware = (securityConfig) => {
2834
3018
  httpVersion: req.httpVersion,
2835
3019
  };
2836
3020
 
3021
+ // New GraphQL parsing logic
3022
+ // It's common for GraphQL endpoints to be at '/graphql'
3023
+ if (req.path === '/graphql' && req.body) {
3024
+ const gqlInfo = parseGraphQLQuery(req.body);
3025
+ if (gqlInfo) {
3026
+ requestContext.graphqlOperationType = gqlInfo.type;
3027
+ requestContext.graphqlOperationName = gqlInfo.name;
3028
+ }
3029
+ }
2837
3030
  const decision = await engine.processRequest(requestContext);
2838
3031
 
2839
3032
  // Attach the fingerprinting result to the request object for downstream middlewares.
@@ -2894,6 +3087,7 @@ export const __internal = {
2894
3087
  getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
2895
3088
  generateCpuTargetChallengePage,
2896
3089
  generateCombinedPoWChallengePage,
3090
+ problemManager, // Re-export the problemManager promise
2897
3091
  };
2898
3092
 
2899
3093
  // --- THRESHOLD AUTO-TUNING SECTION ---
@@ -2909,8 +3103,16 @@ let autoTuningJobId = null;
2909
3103
  * @param {number} maxDataPoints - The maximum number of data points to keep after an optimization cycle.
2910
3104
  */
2911
3105
  function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
2912
- if (trafficData.length < minDataPoints) {
3106
+ const highConfidenceLogs = trafficData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
3107
+ const highConfidenceRatio = trafficData.length > 0 ? highConfidenceLogs / trafficData.length : 0;
3108
+ const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
3109
+
3110
+ if (trafficData.length < minDataPoints || highConfidenceRatio < MIN_CONFIDENCE_RATIO) {
3111
+ if (trafficData.length < minDataPoints) {
2913
3112
  console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
3113
+ } else {
3114
+ console.log(`[AutoTuning] Reporté : Ratio de confiance insuffisant (${(highConfidenceRatio * 100).toFixed(2)}% < ${(MIN_CONFIDENCE_RATIO * 100).toFixed(2)}%).`);
3115
+ }
2914
3116
  return;
2915
3117
  }
2916
3118
 
@@ -2941,23 +3143,43 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
2941
3143
  }
2942
3144
  }
2943
3145
 
2944
- // Appliquer la nouvelle configuration optimisée
3146
+ // --- NOUVEAU : Logique d'inertie pour l'application de la configuration ---
3147
+ // Au lieu d'appliquer directement la nouvelle configuration, on fait "glisser"
3148
+ // l'ancienne vers la nouvelle, avec une vélocité de changement maximale.
2945
3149
  const newConfig = bestSolution.solution;
3150
+ const MAX_CHANGE_VELOCITY = 0.15; // 15% de changement maximum par cycle
2946
3151
 
2947
- // S'assurer que les objets de configuration existent avant d'utiliser Object.assign
2948
- if (!securityConfig.thresholds) securityConfig.thresholds = {};
2949
- if (!securityConfig.weights) securityConfig.weights = {};
2950
- if (!securityConfig.patterns) securityConfig.patterns = {};
3152
+ /**
3153
+ * Met à jour un objet de configuration (ex: thresholds, weights) en douceur.
3154
+ * @param {object} currentConfig - La configuration actuelle à modifier.
3155
+ * @param {object} targetConfig - La configuration cible proposée par l'optimiseur.
3156
+ */
3157
+ 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
+ }
3171
+ }
3172
+ };
2951
3173
 
2952
- Object.assign(securityConfig.thresholds, newConfig.thresholds || {});
2953
- Object.assign(securityConfig.weights, newConfig.weights || {});
2954
- Object.assign(securityConfig.patterns, newConfig.patterns || {});
3174
+ applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds);
3175
+ applyInertialUpdate(securityConfig.weights, newConfig.weights);
3176
+ applyInertialUpdate(securityConfig.patterns, newConfig.patterns);
2955
3177
 
2956
3178
  console.log("[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.");
2957
3179
  console.log("[AutoTuning] Objectifs atteints :", { falsePositiveRate: bestSolution.objectives[0].toFixed(4), falseNegativeRate: bestSolution.objectives[1].toFixed(4) });
2958
- console.log("[AutoTuning] Seuils :", securityConfig.thresholds);
2959
- console.log("[AutoTuning] Poids :", securityConfig.weights);
2960
- console.log("[AutoTuning] Patterns :", securityConfig.patterns);
3180
+ console.log("[AutoTuning] Nouveaux seuils :", securityConfig.thresholds);
3181
+ console.log("[AutoTuning] Nouveaux poids :", securityConfig.weights);
3182
+ console.log("[AutoTuning] Nouveaux patterns :", securityConfig.patterns);
2961
3183
  }
2962
3184
 
2963
3185
  /**