@anonympins/fingerprint 0.4.6 → 0.5.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.
@@ -8,6 +8,8 @@ import {DynamicWasmGenerator} from "./dynamic-wasm.js";
8
8
  import {readFileSync, existsSync} from "node:fs";
9
9
  import {fileURLToPath} from "node:url";
10
10
  import {dirname, join, resolve} from "node:path";
11
+ import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "./fingerprint.utils.js";
12
+
11
13
 
12
14
  const __filename = fileURLToPath(import.meta.url);
13
15
  const __dirname = dirname(__filename);
@@ -140,23 +142,7 @@ function getActiveMappingForRequest(headers) {
140
142
  return null;
141
143
  }
142
144
 
143
- function decodePolymorphicFingerprint(fpString, mapping) {
144
- if (!fpString || !mapping || !mapping.keys) return fpString;
145
- const reverseKeys = {};
146
- for (const [orig, rand] of Object.entries(mapping.keys)) {
147
- reverseKeys[rand] = orig;
148
- }
149
- const parts = fpString.split('|');
150
- const mappedParts = parts.map(part => {
151
- const pair = part.split(':');
152
- if (pair.length === 2) {
153
- const origKey = reverseKeys[pair[0]] || pair[0];
154
- return `${origKey}:${pair[1]}`;
155
- }
156
- return part;
157
- });
158
- return mappedParts.join('|');
159
- }
145
+
160
146
 
161
147
  const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
162
148
  const base64UrlDecode = (str) => {
@@ -168,6 +154,31 @@ const base64UrlDecode = (str) => {
168
154
  };
169
155
 
170
156
  export function generateStatelessTicket(payload) {
157
+ let ed25519Key = process.env.ED25519_PRIVATE_KEY;
158
+ if (ed25519Key) {
159
+ try {
160
+ ed25519Key = ed25519Key.replace(/\\n/g, '\n');
161
+ const serialized = JSON.stringify(payload);
162
+ let signature;
163
+ try {
164
+ signature = crypto.sign(undefined, Buffer.from(serialized), {
165
+ key: ed25519Key,
166
+ format: 'pem',
167
+ type: 'pkcs8'
168
+ });
169
+ } catch (signErr) {
170
+ signature = crypto.sign(null, Buffer.from(serialized), {
171
+ key: ed25519Key,
172
+ format: 'pem',
173
+ type: 'pkcs8'
174
+ });
175
+ }
176
+ return `ed25519.${base64UrlEncode(Buffer.from(serialized))}.${base64UrlEncode(signature)}`;
177
+ } catch (e) {
178
+ console.error('[Fingerprint] Ed25519 signing failed, falling back to symmetric:', e.message);
179
+ }
180
+ }
181
+
171
182
  const secret = getPowSecret();
172
183
  const key = crypto.createHash('sha256').update(secret).digest();
173
184
  const iv = crypto.randomBytes(16);
@@ -178,9 +189,118 @@ export function generateStatelessTicket(payload) {
178
189
  const signature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
179
190
  return `${base64UrlEncode(iv)}.${base64UrlEncode(encrypted)}.${base64UrlEncode(signature)}`;
180
191
  }
192
+ /**
193
+ * Détecte les anomalies de flux QUIC/HTTP3 par rapport au User-Agent.
194
+ * @private
195
+ * @param {object} context - Le contexte de la requête.
196
+ * @returns {{quicAnomalyScore: number}}
197
+ */
198
+ function getQuicAnomalyScore(context) {
199
+ const quicFp = context.headers?.['x-quic-fp'] || context.quicFingerprint || null;
200
+ if (!quicFp || typeof quicFp !== 'string') {
201
+ return { quicAnomalyScore: 0.0 };
202
+ }
203
+
204
+ const parts = quicFp.split(';');
205
+ if (parts.length < 2) return { quicAnomalyScore: 0.0 };
181
206
 
207
+ const params = {};
208
+ parts[1].split(',').forEach(p => {
209
+ const kv = p.split('=');
210
+ if (kv.length === 2) params[kv[0]] = kv[1];
211
+ });
212
+ const priorityOrder = parts[2] || '';
213
+
214
+ const ua = context.headers?.['user-agent'] || '';
215
+ const uaParts = parseUserAgent(ua);
216
+ const browser = uaParts.browser;
217
+
218
+ if (!browser) return { quicAnomalyScore: 0.0 };
219
+
220
+ let anomaly = 0.0;
221
+ if (browser.startsWith('Chrome') || browser.startsWith('Edge')) {
222
+ const maxData = parseInt(params['1'] || '0', 10);
223
+ const maxStreams = parseInt(params['4'] || '0', 10);
224
+ if (maxData > 0 && maxData < 1048576) anomaly += 40.0;
225
+ if (maxStreams > 0 && maxStreams !== 100) anomaly += 30.0;
226
+ if (priorityOrder && !priorityOrder.includes('u=')) anomaly += 30.0;
227
+ } else if (browser.startsWith('Firefox')) {
228
+ const maxData = parseInt(params['1'] || '0', 10);
229
+ if (maxData > 0 && maxData > 5000000) anomaly += 40.0;
230
+ }
231
+
232
+ return { quicAnomalyScore: Math.max(0.0, Math.min(100.0, anomaly)) };
233
+ }
234
+ /**
235
+ * Détecte les anomalies de rendu (V-Sync, FPS, gigue) à partir des métriques d'affichage.
236
+ * @private
237
+ * @param {object} context - Le contexte de la requête.
238
+ * @returns {{renderingAnomalyScore: number}}
239
+ */
240
+ function getRenderingAnomalyScore(context) {
241
+ const behaviorHeader = context.headers?.['x-behavior-metrics'];
242
+ if (!behaviorHeader) {
243
+ return { renderingAnomalyScore: 0.0 };
244
+ }
245
+ try {
246
+ const metrics = JSON.parse(behaviorHeader);
247
+ if (!metrics || !metrics.rendering) {
248
+ return { renderingAnomalyScore: 0.0 };
249
+ }
250
+ const rendering = metrics.rendering;
251
+ let score = 0.0;
252
+ if (rendering.offscreenAnom) {
253
+ score += 100.0;
254
+ }
255
+ const fps = parseFloat(rendering.fps || 0.0);
256
+ const jitter = parseFloat(rendering.jitter || 0.0);
257
+ if (fps > 250.0 || (fps > 0.0 && fps < 15.0)) {
258
+ score += 50.0;
259
+ }
260
+ if (jitter > 6.0) {
261
+ score += Math.min(80.0, (jitter - 6.0) * 10.0);
262
+ }
263
+ return { renderingAnomalyScore: Math.min(100.0, score) };
264
+ } catch (e) {
265
+ return { renderingAnomalyScore: 0.0 };
266
+ }
267
+ }
182
268
  export function parseStatelessTicket(ticket) {
183
269
  try {
270
+ if (ticket.startsWith('ed25519.')) {
271
+ const parts = ticket.split('.');
272
+ if (parts.length !== 3) return null;
273
+ const payloadBuffer = base64UrlDecode(parts[1]);
274
+ const signatureBuffer = base64UrlDecode(parts[2]);
275
+ let publicKey = process.env.ED25519_PUBLIC_KEY;
276
+ if (!publicKey) {
277
+ console.error('[Fingerprint] ED25519_PUBLIC_KEY is not defined in environment.');
278
+ return null;
279
+ }
280
+ publicKey = publicKey.replace(/\\n/g, '\n');
281
+
282
+ let isVerified = false;
283
+ try {
284
+ isVerified = crypto.verify(undefined, payloadBuffer, {
285
+ key: publicKey,
286
+ format: 'pem',
287
+ type: 'spki'
288
+ }, signatureBuffer);
289
+ } catch (verifyErr) {
290
+ try {
291
+ isVerified = crypto.verify(null, payloadBuffer, {
292
+ key: publicKey,
293
+ format: 'pem',
294
+ type: 'spki'
295
+ }, signatureBuffer);
296
+ } catch (verifyErr2) {
297
+ isVerified = false;
298
+ }
299
+ }
300
+ if (!isVerified) return null;
301
+ return JSON.parse(payloadBuffer.toString('utf8'));
302
+ }
303
+
184
304
  const parts = ticket.split('.');
185
305
  if (parts.length !== 3) return null;
186
306
 
@@ -239,26 +359,6 @@ async function checkChallengeRateLimit(clientIp) {
239
359
  return true;
240
360
  }
241
361
 
242
- /**
243
- * @private
244
- * Deep merges two objects. The `source` object's properties overwrite the `target`'s.
245
- * @param {object} target - The target object.
246
- * @param {object} source - The source object.
247
- * @returns {object} The merged object.
248
- */
249
- function deepMerge(target, source) {
250
- const output = { ...target };
251
- if (target && typeof target === 'object' && source && typeof source === 'object') {
252
- Object.keys(source).forEach(key => {
253
- if (source[key] && typeof source[key] === 'object' && key in target) {
254
- output[key] = deepMerge(target[key], source[key]);
255
- } else {
256
- output[key] = source[key];
257
- }
258
- });
259
- }
260
- return output;
261
- }
262
362
 
263
363
  const securityProfiles = {
264
364
  /**
@@ -279,7 +379,10 @@ const securityProfiles = {
279
379
  tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
280
380
  subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
281
381
  ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
282
- botnetClusterScore: 0.6 // NOUVEAU: Poids pour le clustering botnet
382
+ botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
383
+ tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
384
+ quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
385
+ renderingAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie de rendu
283
386
  },
284
387
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
285
388
  patterns: {
@@ -315,7 +418,8 @@ const securityProfiles = {
315
418
  tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
316
419
  subnetScore: 0.5,
317
420
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
318
- botnetClusterScore: 0.8 // NOUVEAU: Poids pour le clustering botnet
421
+ botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
422
+ renderingAnomalyScore: 1.0 // NOUVEAU: Poids pour l'anomalie de rendu
319
423
  },
320
424
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
321
425
  patterns: {
@@ -352,7 +456,9 @@ const securityProfiles = {
352
456
  tlsSpoofingScore: 0.7, // Important pour les API
353
457
  subnetScore: 0.4,
354
458
  ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
355
- botnetClusterScore: 0.7 // NOUVEAU: Poids pour le clustering botnet
459
+ botnetClusterScore: 0.7, // NOUVEAU: Poids pour le clustering botnet
460
+ tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
461
+ quicAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie QUIC
356
462
  },
357
463
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
358
464
  patterns: {
@@ -390,7 +496,10 @@ const securityProfiles = {
390
496
  tlsSpoofingScore: 0.6, // Moins critique pour les blogs
391
497
  subnetScore: 0.2,
392
498
  ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
393
- botnetClusterScore: 0.5 // NOUVEAU: Poids pour le clustering botnet
499
+ botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
500
+ tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
501
+ quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
502
+ renderingAnomalyScore: 0.5 // NOUVEAU: Poids pour l'anomalie de rendu
394
503
  },
395
504
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
396
505
  patterns: {
@@ -427,7 +536,10 @@ const securityProfiles = {
427
536
  tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
428
537
  subnetScore: 0.5,
429
538
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
430
- botnetClusterScore: 0.9 // NOUVEAU: Poids pour le clustering botnet
539
+ botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
540
+ tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
541
+ quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
542
+ renderingAnomalyScore: 0.9 // NOUVEAU: Poids pour l'anomalie de rendu
431
543
  },
432
544
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
433
545
  patterns: {
@@ -593,44 +705,6 @@ function getTlsFingerprint(context) {
593
705
  return { ja3, ja4 };
594
706
  }
595
707
 
596
- /**
597
- * Analyses a raw JA3 string.
598
- * Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
599
- * @param {string} ja3String
600
- * @returns {object|null}
601
- */
602
- export function parseJa3(ja3String) {
603
- if (!ja3String || typeof ja3String !== 'string') {
604
- return null;
605
- }
606
- const parts = ja3String.split(',');
607
- if (parts.length !== 5) {
608
- return null;
609
- }
610
- return {
611
- tlsVersion: parseInt(parts[0], 10),
612
- ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
613
- extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
614
- curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
615
- points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
616
- };
617
- }
618
-
619
- /**
620
- * Creates a stable hash based on device characteristics, independent of the IP.
621
- * This is our "level 2 fingerprint".
622
- * @param {object} context - The request context.
623
- * @returns {string} A hash representing the device.
624
- */
625
- function getHeaderSignature(context) {
626
- if (!context.rawHeaders) return '';
627
- const headerKeys = [];
628
- for (let i = 0; i < context.rawHeaders.length; i += 2) {
629
- headerKeys.push(context.rawHeaders[i]);
630
- }
631
- return cyrb53(headerKeys.sort().join(','));
632
- }
633
-
634
708
  /**
635
709
  * Returns the client-side fingerprint if available, otherwise computes a server-side hash.
636
710
  * This aligns with the test's expectation for prioritization.
@@ -766,79 +840,6 @@ function hasGrease(values) {
766
840
  return values.some(val => GREASE_VALUES.includes(val));
767
841
  }
768
842
 
769
- // Fonctions utilitaires
770
- function parseUserAgent(ua) {
771
- // Parser basique du User-Agent
772
- const result = {};
773
-
774
- // Détection du navigateur
775
- if (ua.includes('Chrome') && !ua.includes('Edg')) {
776
- result.browser = 'Chrome';
777
- const match = ua.match(/Chrome\/(\d+)/);
778
- if (match) result.browser += `/${match[1]}`;
779
- } else if (ua.includes('Firefox')) {
780
- result.browser = 'Firefox';
781
- const match = ua.match(/Firefox\/(\d+)/);
782
- if (match) result.browser += `/${match[1]}`;
783
- } else if (ua.includes('Safari') && !ua.includes('Chrome')) {
784
- result.browser = 'Safari';
785
- const match = ua.match(/Version\/(\d+)/);
786
- if (match) result.browser += `/${match[1]}`;
787
- } else if (ua.includes('Edg')) {
788
- result.browser = 'Edge';
789
- const match = ua.match(/Edg\/(\d+)/);
790
- if (match) result.browser += `/${match[1]}`;
791
- }
792
-
793
- // Détection de l'OS
794
- if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
795
- else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
796
- else if (ua.includes('Mac OS X')) result.os = 'macOS';
797
- else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
798
- else if (ua.includes('Android')) result.os = 'Android';
799
- else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
800
-
801
- // Détection du type d'appareil
802
- if (ua.includes('Mobile')) result.device = 'mobile';
803
- else if (ua.includes('Tablet')) result.device = 'tablet';
804
- else result.device = 'desktop';
805
-
806
- return result;
807
- }
808
-
809
- function normalizeReferer(referer) {
810
- try {
811
- const url = new URL(referer);
812
- return `${url.protocol}//${url.hostname}`;
813
- } catch {
814
- return referer;
815
- }
816
- }
817
-
818
- function isPrivateIp(ip) {
819
- // Vérifier si l'IP est privée
820
- const parts = ip.split('.');
821
- if (parts.length !== 4) return false;
822
- const first = parseInt(parts[0]);
823
- return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
824
- }
825
-
826
- function hashNetwork(ip, prefix = 24) {
827
- // Hash du réseau (masque /24 ou /16)
828
- const parts = ip.split('.');
829
- if (parts.length !== 4) return null;
830
- const maskBytes = prefix / 8;
831
- const network = parts.slice(0, maskBytes).join('.');
832
- // Hash simple
833
- let hash = 0;
834
- for (let i = 0; i < network.length; i++) {
835
- const char = network.charCodeAt(i);
836
- hash = ((hash << 5) - hash) + char;
837
- hash = hash & hash;
838
- }
839
- return hash.toString(16);
840
- }
841
-
842
843
  /**
843
844
  * Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
844
845
  * @param {string} nonce - Unique nonce for the challenge.
@@ -1091,19 +1092,30 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
1091
1092
  return finalHash === parseInt(solution, 10);
1092
1093
  };
1093
1094
 
1094
- export function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
1095
- const combined = new Uint8Array(queries.length * 1024);
1096
- for (let i = 0; i < queries.length; i++) {
1097
- const idx = queries[i];
1098
- const block = generateBlock(seed, idx);
1099
- combined.set(block, i * 1024);
1100
- }
1101
-
1102
- const nonceBytes = Buffer.from(nonce + ":" + clientSecret, "utf8");
1103
- const finalBlock = Buffer.concat([Buffer.from(combined), nonceBytes]);
1104
-
1105
- const hash = crypto.createHash("sha256").update(finalBlock).digest("hex");
1106
- return hash === solution;
1095
+ export async function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
1096
+ let combined = [];
1097
+ for (let i = 0; i < queries.length; i++) {
1098
+ const idx = queries[i];
1099
+ const block = generateBlock(seed, idx);
1100
+ combined.push(...block);
1101
+ }
1102
+
1103
+ const assoc = await store.get(`coop-assoc:${nonce}`);
1104
+ if (assoc) {
1105
+ const peerSeed = assoc.peerSeed;
1106
+ const peerBlockIdx = assoc.peerBlockIdx;
1107
+ if (peerSeed !== undefined && peerBlockIdx !== undefined) {
1108
+ const peerBlock = generateBlock(peerSeed, peerBlockIdx);
1109
+ combined.push(...peerBlock);
1110
+ }
1111
+ await store.delete(`coop-assoc:${nonce}`);
1112
+ }
1113
+
1114
+ const nonceBytes = Buffer.from(nonce + ":" + clientSecret, "utf8");
1115
+ const finalBlock = Buffer.concat([Buffer.from(combined), nonceBytes]);
1116
+
1117
+ const hash = crypto.createHash("sha256").update(finalBlock).digest("hex");
1118
+ return hash === solution;
1107
1119
  }
1108
1120
 
1109
1121
  function generateBlock(seed, blockIndex, blockSize = 1024) {
@@ -1116,26 +1128,31 @@ function generateBlock(seed, blockIndex, blockSize = 1024) {
1116
1128
  return block;
1117
1129
  }
1118
1130
 
1119
- export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false) => {
1131
+ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false, zkpProof = '') => {
1120
1132
  // Input validation: ensure the ticket is a non-empty string with the correct format.
1121
- if (typeof ticket !== 'string' || ticket.length === 0) return false;
1122
-
1133
+ if (typeof ticket !== 'string' || ticket.length === 0) return false;
1123
1134
  // 1. Resolve stateless ticket first (zero database I/O cost)
1124
1135
  const statelessData = parseStatelessTicket(ticket);
1125
1136
  if (statelessData) {
1126
1137
  const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
1127
-
1128
1138
  if (!expiry || Date.now() > expiry) {
1129
1139
  return false;
1130
1140
  }
1131
-
1141
+ if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1142
+ const expectedY = storedDeviceHash.split(':')[1];
1143
+ if (zkpProof) {
1144
+ const [y, t, s] = zkpProof.split(':');
1145
+ if (y === expectedY && verifyZkpProof(y, t, s)) {
1146
+ return true;
1147
+ }
1148
+ }
1149
+ return false;
1150
+ }
1132
1151
  if (ip === originalIp) return true;
1133
1152
  const currentSubnet = getIpSubnet(ip);
1134
1153
  const originalSubnet = getIpSubnet(originalIp);
1135
1154
  if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
1136
-
1137
1155
  if (!allowCrossNetworkRoaming) return false;
1138
-
1139
1156
  return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
1140
1157
  }
1141
1158
 
@@ -1148,6 +1165,16 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
1148
1165
  await store.delete(`ticket:${ticket}`);
1149
1166
  return false;
1150
1167
  }
1168
+ if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1169
+ const expectedY = storedDeviceHash.split(':')[1];
1170
+ if (zkpProof) {
1171
+ const [y, t, s] = zkpProof.split(':');
1172
+ if (y === expectedY && verifyZkpProof(y, t, s)) {
1173
+ return true;
1174
+ }
1175
+ }
1176
+ return false;
1177
+ }
1151
1178
 
1152
1179
  if (ip === originalIp) return true;
1153
1180
  const currentSubnet = getIpSubnet(ip);
@@ -1255,7 +1282,7 @@ function getHeaderAnomalies(context) {
1255
1282
  };
1256
1283
  }
1257
1284
 
1258
- export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
1285
+ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
1259
1286
  const sizeMb = securityConfig?.pospace?.sizeMb || 100;
1260
1287
  const numQueries = securityConfig?.pospace?.numQueries || 10;
1261
1288
 
@@ -1268,13 +1295,27 @@ export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, origina
1268
1295
  }
1269
1296
  }
1270
1297
 
1271
- return {
1298
+ const challenge = {
1272
1299
  type: "pospace",
1273
1300
  nonce: nonce,
1274
1301
  sizeMb,
1275
1302
  queries,
1276
1303
  path: originalUrl
1277
1304
  };
1305
+
1306
+ const peer = await findPeerInSubnet(clientIp, nonce);
1307
+ if (peer) {
1308
+ challenge.peerId = peer.nodeId;
1309
+ challenge.peerBlockIdx = Math.floor(Math.random() * maxBlocks);
1310
+
1311
+ await store.set(`coop-assoc:${nonce}`, {
1312
+ peerNodeId: peer.nodeId,
1313
+ peerSeed: peer.seed,
1314
+ peerBlockIdx: challenge.peerBlockIdx
1315
+ }, 120);
1316
+ }
1317
+
1318
+ return challenge;
1278
1319
  }
1279
1320
 
1280
1321
  function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
@@ -1418,6 +1459,20 @@ const injectionPatterns = {
1418
1459
  traversal: /(\.\.\/|\.\.\\)/,
1419
1460
  // Remote Command Execution (RCE)
1420
1461
  rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
1462
+ // Server-Side Request Forgery (SSRF) - Detects local/private IPs and hosts
1463
+ ssrf: /((?:https?:\/\/)?(?:127\.\d+\.\d+\.\d+\b|169\.254\.169\.254\b|10\.\d+\.\d+\.\d+\b|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+\b|192\.168\.\d+\.\d+\b|localhost\b|0\.0\.0\.0\b|\[[0:]+1\](?=\W|$)))/i,
1464
+ // Carriage Return Line Feed (CRLF) Injection / HTTP Response Splitting
1465
+ crlf: /[\r\n]|%0[ad]/i,
1466
+ // Cross-Site Scripting (XSS) - Fast native regex fallback
1467
+ xss: /(<script|javascript:|on\w+\s*=|alert\s*\(|confirm\s*\(|prompt\s*\(|<img\s+src[^>]+onerror|<iframe)/i,
1468
+ // Open Redirect - Basic detection of external protocol/URLs
1469
+ openRedirect: /^(https?:)?\/\/(?![^\/]*?(localhost|127\.0\.0\.1))[^\s\/]+/i,
1470
+ // Local/Remote File Inclusion (LFI/RFI)
1471
+ lfi: /(?:etc\/passwd|win\.ini|boot\.ini|php:\/\/filter|data:\/\/|zip:\/\/)/i,
1472
+ // Shellshock (CVE-2014-6271)
1473
+ shellshock: /\(\)\s*\{\s*:\s*;\s*\}\s*/i,
1474
+ // NoSQL Injection (MongoDB query operators)
1475
+ nosql: /\$(?:eq|ne|gt|gte|lt|lte|in|nin|and|or|nor|not|expr|jsonSchema|mod|regex|text|where|elemMatch)/i
1421
1476
  };
1422
1477
 
1423
1478
  /**
@@ -1632,6 +1687,41 @@ function getBehaviorScore(context) {
1632
1687
  if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
1633
1688
  if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
1634
1689
 
1690
+ // NOUVEAU: Analyse de digraphie/trigraphie (dwell & flight times)
1691
+ const dwellTimes = metrics.keystrokeDwellTimes || [];
1692
+ const flightTimes = metrics.keystrokeFlightTimes || [];
1693
+
1694
+ if (dwellTimes.length >= 5) {
1695
+ const meanDwell = dwellTimes.reduce((a, b) => a + b, 0) / dwellTimes.length;
1696
+ const varDwell = dwellTimes.reduce((a, b) => a + Math.pow(b - meanDwell, 2), 0) / dwellTimes.length;
1697
+ const stdDevDwell = Math.sqrt(varDwell);
1698
+
1699
+ if (stdDevDwell < 2.0) {
1700
+ score += 35; // Suspicion d'automatisation (pas de variation humaine de pression)
1701
+ }
1702
+ if (meanDwell < 15.0) {
1703
+ score += 25; // Dwell time irréaliste
1704
+ }
1705
+ }
1706
+
1707
+ if (flightTimes.length >= 5) {
1708
+ const times = flightTimes.map(f => f.time);
1709
+ const meanFlight = times.reduce((a, b) => a + b, 0) / times.length;
1710
+ const varFlight = times.reduce((a, b) => a + Math.pow(b - meanFlight, 2), 0) / times.length;
1711
+ const stdDevFlight = Math.sqrt(varFlight);
1712
+
1713
+ if (stdDevFlight < 3.0) {
1714
+ score += 35; // Pas de variation de transition (flight time robotique)
1715
+ }
1716
+ if (meanFlight < 25.0) {
1717
+ score += 25; // Transitions trop rapides
1718
+ }
1719
+ const benfordDev = Optimization.Operators.benfordTest(times);
1720
+ if (benfordDev > 0.18) {
1721
+ score += 30; // Les intervalles ne suivent pas la loi de Benford
1722
+ }
1723
+ }
1724
+
1635
1725
  // 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
1636
1726
  if (segments.length > 10) {
1637
1727
  const benfordDeviation = Optimization.Operators.benfordTest(segments);
@@ -2798,8 +2888,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
2798
2888
  const stableFp = extractStablePart(currentDeviceHash);
2799
2889
  const stableFpHash = cyrb53(stableFp).toString();
2800
2890
  const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
2801
-
2802
- const { tcpAnomalyScore } = getTcpAnomalyScore(context);
2891
+ const { tcpAnomalyScore } = getTcpAnomalyScore(context);
2892
+ const { quicAnomalyScore } = getQuicAnomalyScore(context);
2893
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
2803
2894
 
2804
2895
  // Save the updated device state to the store
2805
2896
  // Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
@@ -2811,7 +2902,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
2811
2902
  deviceData.ips = new Set(deviceData.ips);
2812
2903
  }
2813
2904
  // Le vecteur de suspicion est maintenant complet.
2814
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore };
2905
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore };
2815
2906
  };
2816
2907
 
2817
2908
  // A residential user can change networks (home, 4G, public wifi).
@@ -3147,12 +3238,45 @@ function parseGraphQLQuery(body) {
3147
3238
  export class FingerprintEngine {
3148
3239
  constructor(securityConfig) {
3149
3240
  const isProduction = process.env.NODE_ENV === 'production';
3150
- this.securityConfig = securityConfig;
3241
+
3242
+ // Dynamically bind Ed25519 keys if passed via config
3243
+ if (securityConfig && securityConfig.ed25519_private_key) {
3244
+ process.env.ED25519_PRIVATE_KEY = securityConfig.ed25519_private_key;
3245
+ }
3246
+ if (securityConfig && securityConfig.ed25519_public_key) {
3247
+ process.env.ED25519_PUBLIC_KEY = securityConfig.ed25519_public_key;
3248
+ }
3249
+
3250
+ let finalConfig = securityConfig;
3251
+ if (securityConfig && securityConfig.autotuning && securityConfig.autotuning.savePath) {
3252
+ const sPath = securityConfig.autotuning.savePath;
3253
+ if (existsSync(sPath)) {
3254
+ try {
3255
+ const savedConfig = JSON.parse(readFileSync(sPath, 'utf-8'));
3256
+ finalConfig = deepMerge(securityConfig, savedConfig);
3257
+ } catch (e) {
3258
+ console.warn(`[Fingerprint] Failed to auto-load optimized config from ${sPath}:`, e.message);
3259
+ }
3260
+ }
3261
+ }
3262
+ this.securityConfig = finalConfig;
3151
3263
  this.isProduction = isProduction;
3152
3264
  this._allowlist = this._buildAllowlist();
3153
- this._validateConfig(securityConfig); // Validate the configuration
3154
- this.verbose = securityConfig.verbose || false;
3155
- this.dryRun = securityConfig.dryRun || false;
3265
+ this._validateConfig(finalConfig); // Validate the configuration
3266
+ this.verbose = finalConfig.verbose || false;
3267
+ this.dryRun = finalConfig.dryRun || false;
3268
+ }
3269
+
3270
+ /**
3271
+ * Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
3272
+ * sans nécessiter de redémarrage.
3273
+ * @param {object} newConfig - La nouvelle configuration partielle ou complète.
3274
+ */
3275
+ updateConfig(newConfig) {
3276
+ this._validateConfig(newConfig);
3277
+ this.securityConfig = deepMerge(this.securityConfig, newConfig);
3278
+ this.dryRun = this.securityConfig.dryRun || false;
3279
+ this._log('Configuration mise à jour à chaud (Hot-Reloaded)', this.securityConfig);
3156
3280
  }
3157
3281
 
3158
3282
  /**
@@ -3173,7 +3297,8 @@ export class FingerprintEngine {
3173
3297
  'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
3174
3298
  'trustedProxies',
3175
3299
  'wasm',
3176
- 'similarityThreshold'
3300
+ 'similarityThreshold',
3301
+ 'ed25519_private_key', 'ed25519_public_key'
3177
3302
  ]);
3178
3303
 
3179
3304
  // 1. Check for essential keys
@@ -3218,7 +3343,9 @@ export class FingerprintEngine {
3218
3343
  (suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
3219
3344
  (suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
3220
3345
  (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
3221
- (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0);
3346
+ (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
3347
+ (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
3348
+ (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
3222
3349
 
3223
3350
  return Math.min(100, score);
3224
3351
  }
@@ -3477,7 +3604,17 @@ export class FingerprintEngine {
3477
3604
  sanitizeProxyHeaders(requestContext, this.securityConfig);
3478
3605
 
3479
3606
  const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
3480
- const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
3607
+
3608
+ if (query.coop_op) {
3609
+ const result = await handleCooperativeRequest(query, clientIp);
3610
+ return {
3611
+ action: 'challenge',
3612
+ status: 200,
3613
+ body: result
3614
+ };
3615
+ }
3616
+
3617
+ const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
3481
3618
 
3482
3619
  this._log('Processing request', { clientIp, path, isStatic });
3483
3620
 
@@ -3699,7 +3836,7 @@ export class FingerprintEngine {
3699
3836
  isValid
3700
3837
  });
3701
3838
  } else if (pow_type === "pospace" && pow_solution_space) {
3702
- const isSpaceValid = verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
3839
+ const isSpaceValid = await verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
3703
3840
  isValid = isSpaceValid;
3704
3841
  if (isValid) {
3705
3842
  const ttl = finalTtl || 3600000;
@@ -3707,7 +3844,7 @@ export class FingerprintEngine {
3707
3844
  expiry: Date.now() + ttl,
3708
3845
  originalIp: clientIp,
3709
3846
  deviceId,
3710
- deviceHash
3847
+ deviceHash: currentDeviceHash
3711
3848
  });
3712
3849
  }
3713
3850
  }
@@ -3980,7 +4117,8 @@ export class FingerprintEngine {
3980
4117
  // 1. La requête est suspecte ET il n'y a pas de ticket valide.
3981
4118
  // OU
3982
4119
  // 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
3983
- const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming);
4120
+ const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
4121
+ const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
3984
4122
  const mustReChallenge = isSuspiciousHigh && hasValidTicket;
3985
4123
 
3986
4124
  if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
@@ -4081,7 +4219,7 @@ export class FingerprintEngine {
4081
4219
  return decision;
4082
4220
  }
4083
4221
  if (this.securityConfig.enableProofOfSpace) {
4084
- const spaceChallenge = generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
4222
+ const spaceChallenge = await generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
4085
4223
  const clientSecret = crypto.randomBytes(16).toString("hex");
4086
4224
  await store.set(`secret:${nonce}`, {
4087
4225
  clientSecret,
@@ -4262,6 +4400,117 @@ const staticExtensions = new RegExp(
4262
4400
  );
4263
4401
  const isStaticResource = (path) => staticExtensions.test(path);
4264
4402
 
4403
+ export async function registerCooperativeNode(clientIp, nodeId, seed) {
4404
+ const subnet = getIpSubnet(clientIp);
4405
+ if (!subnet) return;
4406
+
4407
+ const key = `coop-pospace:subnet:${subnet}`;
4408
+ const nodes = (await store.get(key)) || {};
4409
+ const now = Math.floor(Date.now() / 1000);
4410
+
4411
+ // Clean up expired nodes (older than 120 seconds)
4412
+ const cleanedNodes = {};
4413
+ for (const [id, node] of Object.entries(nodes)) {
4414
+ if (now - node.timestamp < 120) {
4415
+ cleanedNodes[id] = node;
4416
+ }
4417
+ }
4418
+
4419
+ cleanedNodes[nodeId] = {
4420
+ nodeId,
4421
+ seed,
4422
+ timestamp: now
4423
+ };
4424
+
4425
+ await store.set(key, cleanedNodes, 120);
4426
+ }
4427
+
4428
+ export async function findPeerInSubnet(clientIp, excludeNodeId) {
4429
+ const subnet = getIpSubnet(clientIp);
4430
+ if (!subnet) return null;
4431
+
4432
+ const key = `coop-pospace:subnet:${subnet}`;
4433
+ const nodes = (await store.get(key)) || {};
4434
+ const now = Math.floor(Date.now() / 1000);
4435
+
4436
+ const activePeers = [];
4437
+ for (const [id, node] of Object.entries(nodes)) {
4438
+ if (id !== excludeNodeId && now - node.timestamp < 120) {
4439
+ activePeers.push(node);
4440
+ }
4441
+ }
4442
+
4443
+ if (activePeers.length === 0) return null;
4444
+
4445
+ // Select a random peer
4446
+ const randomIndex = Math.floor(Math.random() * activePeers.length);
4447
+ return activePeers[randomIndex];
4448
+ }
4449
+
4450
+ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
4451
+ const op = params.coop_op;
4452
+ if (!op) return null;
4453
+
4454
+ const nodeId = params.node_id || '';
4455
+ if (!nodeId) {
4456
+ return { error: 'Missing node_id' };
4457
+ }
4458
+
4459
+ switch (op) {
4460
+ case 'register':
4461
+ const seed = params.seed || '';
4462
+ await registerCooperativeNode(clientIp, nodeId, seed);
4463
+ return { status: 'registered' };
4464
+
4465
+ case 'request_peer_block':
4466
+ const peerId = params.peer_id || '';
4467
+ const blockIdx = parseInt(params.block_idx || '0', 10);
4468
+ const requestId = params.req_id || '';
4469
+ if (!peerId || !requestId) {
4470
+ return { error: 'Invalid parameters' };
4471
+ }
4472
+
4473
+ const queueKey = `coop-mailbox:queue:${peerId}`;
4474
+ const requests = (await store.get(queueKey)) || [];
4475
+ requests.push({
4476
+ req_id: requestId,
4477
+ requester_id: nodeId,
4478
+ block_idx: blockIdx
4479
+ });
4480
+ await store.set(queueKey, requests, 30);
4481
+ return { status: 'queued' };
4482
+
4483
+ case 'poll_requests':
4484
+ const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
4485
+ const polledRequests = (await store.get(pollQueueKey)) || [];
4486
+ await store.delete(pollQueueKey);
4487
+ return { requests: polledRequests };
4488
+
4489
+ case 'respond_block':
4490
+ const requesterId = params.requester_id || '';
4491
+ const respondRequestId = params.req_id || '';
4492
+ const blockData = params.block_data || '';
4493
+ if (!requesterId || !respondRequestId) {
4494
+ return { error: 'Invalid parameters' };
4495
+ }
4496
+
4497
+ const responseKey = `coop-mailbox:res:${requesterId}:${respondRequestId}`;
4498
+ await store.set(responseKey, { block_data: blockData }, 30);
4499
+ return { status: 'delivered' };
4500
+
4501
+ case 'poll_response':
4502
+ const pollResponseRequestId = params.req_id || '';
4503
+ const pollResponseKey = `coop-mailbox:res:${nodeId}:${pollResponseRequestId}`;
4504
+ const data = await store.get(pollResponseKey);
4505
+ if (data) {
4506
+ await store.delete(pollResponseKey);
4507
+ return { status: 'ready', block_data: data.block_data };
4508
+ }
4509
+ return { status: 'pending' };
4510
+ }
4511
+ return null;
4512
+ }
4513
+
4265
4514
 
4266
4515
  /** @type {Map<number, number>} Cache des TTL optimisés par score de suspicion (clés de 0 à 100 par pas de 10) */
4267
4516
  let optimizedTtlCache = new Map();
@@ -4630,7 +4879,7 @@ function getTcpAnomalyScore(context) {
4630
4879
  * @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
4631
4880
  * @returns {boolean} - True si un pattern malveillant est détecté.
4632
4881
  */
4633
- function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
4882
+ function isMalicious(str, typesToDetect = Object.keys(injectionPatterns).filter(k => k !== 'openRedirect')) {
4634
4883
  if (typeof str !== 'string') return false;
4635
4884
 
4636
4885
  for (const type of typesToDetect) {
@@ -5040,6 +5289,7 @@ export const __internal = {
5040
5289
  getCompositeDeviceHash,
5041
5290
  getSuspicionVector,
5042
5291
  getTlsSessionId,
5292
+ pruneTrafficData,
5043
5293
  cyrb53, // Export for testing
5044
5294
  FingerprintBuilder, // Export for testing
5045
5295
  calculateTarget,
@@ -5068,9 +5318,16 @@ export const __internal = {
5068
5318
  getIpReputationScore, // Expose for testing
5069
5319
  updateIpReputationScore, // Expose for testing
5070
5320
  setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
5321
+ verifyZkpProof,
5322
+ modPow,
5071
5323
  parseTcpSyn, // Expose for testing
5072
5324
  classifyTcpOs, // Expose for testing
5073
- getTcpAnomalyScore // Expose for testing
5325
+ getTcpAnomalyScore, // Expose for testing,
5326
+ getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
5327
+ getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
5328
+ registerCooperativeNode,
5329
+ findPeerInSubnet,
5330
+ handleCooperativeRequest
5074
5331
  };
5075
5332
 
5076
5333
  // --- THRESHOLD AUTO-TUNING SECTION ---
@@ -5132,13 +5389,55 @@ export function sanitizeTrafficData(trafficData) {
5132
5389
 
5133
5390
  return [...suspiciousLogs, ...selectedPassed];
5134
5391
  }
5392
+ /**
5393
+ * Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
5394
+ * @private
5395
+ */
5396
+ function pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup) {
5397
+ if (!Array.isArray(trafficData)) return;
5398
+ const now = Date.now();
5399
+ const removed = [];
5400
+
5401
+ // 1. Politique temporelle d'expiration
5402
+ if (maxAgeMs && maxAgeMs > 0) {
5403
+ const threshold = now - maxAgeMs;
5404
+ let i = 0;
5405
+ while (i < trafficData.length) {
5406
+ const log = trafficData[i];
5407
+ const logTs = log.timestamp || log.requestTimestamp || now;
5408
+ if (logTs < threshold) {
5409
+ removed.push(trafficData.splice(i, 1)[0]);
5410
+ } else {
5411
+ i++;
5412
+ }
5413
+ }
5414
+ }
5415
+
5416
+ // 2. Politique de taille maximale (conserver les plus récents)
5417
+ if (maxDataPoints && maxDataPoints > 0 && trafficData.length > maxDataPoints) {
5418
+ const overflowCount = trafficData.length - maxDataPoints;
5419
+ const spliced = trafficData.splice(0, overflowCount);
5420
+ removed.push(...spliced);
5421
+ }
5135
5422
 
5423
+ // 3. Callback de nettoyage
5424
+ if (onCleanup && typeof onCleanup === 'function' && removed.length > 0) {
5425
+ try {
5426
+ onCleanup(removed);
5427
+ } catch (e) {
5428
+ console.error('[AutoTuning] Error in onCleanup callback:', e);
5429
+ }
5430
+ }
5431
+ }
5136
5432
  /**
5137
5433
  * Executes a threshold optimization pass using collected traffic data.
5138
5434
  * @private
5139
5435
  */
5140
- function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
5141
- const sanitizedData = sanitizeTrafficData(trafficData);
5436
+ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, tuningOptions = {}) {
5437
+ const { maxAgeMs, clearAfterTuning = false, onCleanup } = tuningOptions;
5438
+
5439
+ pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup);
5440
+ const sanitizedData = sanitizeTrafficData(trafficData);
5142
5441
 
5143
5442
  const highConfidenceLogs = sanitizedData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
5144
5443
  const highConfidenceRatio = sanitizedData.length > 0 ? highConfidenceLogs / sanitizedData.length : 0;
@@ -5155,12 +5454,6 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
5155
5454
  }
5156
5455
  return;
5157
5456
  }
5158
-
5159
- if (trafficData.length > maxDataPoints) {
5160
- console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
5161
- trafficData.splice(0, trafficData.length - maxDataPoints);
5162
- }
5163
-
5164
5457
  console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${sanitizedData.length} points de données assainis.`);
5165
5458
 
5166
5459
  const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
@@ -5295,6 +5588,18 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
5295
5588
  console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
5296
5589
  }
5297
5590
  }
5591
+
5592
+ if (clearAfterTuning) {
5593
+ const cleared = trafficData.splice(0, trafficData.length);
5594
+ if (onCleanup && typeof onCleanup === 'function' && cleared.length > 0) {
5595
+ try {
5596
+ onCleanup(cleared);
5597
+ } catch (e) {
5598
+ console.error('[AutoTuning] Error in onCleanup callback after clearing:', e);
5599
+ }
5600
+ }
5601
+ console.log(`[AutoTuning] Explicitly cleared ${cleared.length} processed traffic data points.`);
5602
+ }
5298
5603
  }
5299
5604
 
5300
5605
  /**
@@ -5321,6 +5626,9 @@ export function startThresholdAutoTuning(options) {
5321
5626
  minDataPoints = 200,
5322
5627
  maxDataPoints = 10000, // Limite par défaut à 10 000 entrées
5323
5628
  savePath, // NOUVEAU: Chemin de sauvegarde optionnel
5629
+ maxAgeMs,
5630
+ clearAfterTuning = false,
5631
+ onCleanup,
5324
5632
  } = options;
5325
5633
 
5326
5634
  if (!securityConfig || !trafficData) {
@@ -5330,7 +5638,7 @@ export function startThresholdAutoTuning(options) {
5330
5638
  console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
5331
5639
 
5332
5640
  autoTuningJobId = setInterval(() => {
5333
- runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
5641
+ runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, { maxAgeMs, clearAfterTuning, onCleanup });
5334
5642
  }, interval);
5335
5643
  }
5336
5644