@anonympins/fingerprint 0.4.3 → 0.4.5

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.
@@ -4,6 +4,7 @@ import * as dns from "node:dns/promises";
4
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
+ import {DynamicWasmGenerator} from "./dynamic-wasm.js";
7
8
  import {readFileSync, existsSync} from "node:fs";
8
9
  import {fileURLToPath} from "node:url";
9
10
  import {dirname, join, resolve} from "node:path";
@@ -15,6 +16,87 @@ export { createRedisStore } from "./redis-store.js";
15
16
  export { createMongoDbStore } from "./mongodb-store.js";
16
17
 
17
18
 
19
+ const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
20
+ const base64UrlDecode = (str) => {
21
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
22
+ while (base64.length % 4) {
23
+ base64 += '=';
24
+ }
25
+ return Buffer.from(base64, 'base64');
26
+ };
27
+
28
+ export function generateStatelessTicket(payload) {
29
+ const secret = getPowSecret();
30
+ const key = crypto.createHash('sha256').update(secret).digest();
31
+ const iv = crypto.randomBytes(16);
32
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
33
+ let encrypted = cipher.update(JSON.stringify(payload));
34
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
35
+
36
+ const signature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
37
+ return `${base64UrlEncode(iv)}.${base64UrlEncode(encrypted)}.${base64UrlEncode(signature)}`;
38
+ }
39
+
40
+ export function parseStatelessTicket(ticket) {
41
+ try {
42
+ const parts = ticket.split('.');
43
+ if (parts.length !== 3) return null;
44
+
45
+ const iv = base64UrlDecode(parts[0]);
46
+ const encrypted = base64UrlDecode(parts[1]);
47
+ const signature = base64UrlDecode(parts[2]);
48
+
49
+ if (iv.length !== 16) return null;
50
+
51
+ const secret = getPowSecret();
52
+ const key = crypto.createHash('sha256').update(secret).digest();
53
+
54
+ const expectedSignature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
55
+ if (signature.length !== expectedSignature.length || !crypto.timingSafeEqual(signature, expectedSignature)) {
56
+ return null;
57
+ }
58
+
59
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
60
+ let decrypted = decipher.update(encrypted);
61
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
62
+
63
+ return JSON.parse(decrypted.toString('utf8'));
64
+ } catch (e) {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
71
+ * @param {string} clientIp - L'adresse IP du client.
72
+ * @returns {Promise<boolean>} True si la requête est autorisée, false si elle est limitée.
73
+ */
74
+ async function checkChallengeRateLimit(clientIp) {
75
+ const subnet = getIpSubnet(clientIp);
76
+ if (!subnet) return false;
77
+
78
+ const key = `rate-limit:${subnet}`;
79
+ const rateLimitData = (await store.get(key)) || {
80
+ tokens: 5.0,
81
+ lastRefill: Date.now() / 1000
82
+ };
83
+
84
+ const capacity = 5.0;
85
+ const refillRate = 0.1; // 1 token toutes les 10 secondes
86
+ const now = Date.now() / 1000;
87
+
88
+ const elapsed = now - rateLimitData.lastRefill;
89
+ const tokens = Math.min(capacity, rateLimitData.tokens + elapsed * refillRate);
90
+
91
+ if (tokens < 1.0) {
92
+ await store.set(key, { tokens, lastRefill: now }, 60);
93
+ return false;
94
+ }
95
+
96
+ await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, 60);
97
+ return true;
98
+ }
99
+
18
100
  /**
19
101
  * @private
20
102
  * Deep merges two objects. The `source` object's properties overwrite the `target`'s.
@@ -54,7 +136,8 @@ const securityProfiles = {
54
136
  timeInconsistencyScore: 0.9,
55
137
  tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
56
138
  subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
57
- ipReputationScore: 0.5 // NOUVEAU: Poids pour la réputation IP
139
+ ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
140
+ botnetClusterScore: 0.6 // NOUVEAU: Poids pour le clustering botnet
58
141
  },
59
142
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
60
143
  patterns: {
@@ -89,7 +172,8 @@ const securityProfiles = {
89
172
  timeInconsistencyScore: 1.0,
90
173
  tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
91
174
  subnetScore: 0.5,
92
- ipReputationScore: 0.6 // NOUVEAU: Poids pour la réputation IP
175
+ ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
176
+ botnetClusterScore: 0.8 // NOUVEAU: Poids pour le clustering botnet
93
177
  },
94
178
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
95
179
  patterns: {
@@ -125,7 +209,8 @@ const securityProfiles = {
125
209
  timeInconsistencyScore: 0.8,
126
210
  tlsSpoofingScore: 0.7, // Important pour les API
127
211
  subnetScore: 0.4,
128
- ipReputationScore: 0.5 // NOUVEAU: Poids pour la réputation IP
212
+ ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
213
+ botnetClusterScore: 0.7 // NOUVEAU: Poids pour le clustering botnet
129
214
  },
130
215
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
131
216
  patterns: {
@@ -162,7 +247,8 @@ const securityProfiles = {
162
247
  timeInconsistencyScore: 0.8,
163
248
  tlsSpoofingScore: 0.6, // Moins critique pour les blogs
164
249
  subnetScore: 0.2,
165
- ipReputationScore: 0.3 // NOUVEAU: Poids pour la réputation IP
250
+ ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
251
+ botnetClusterScore: 0.5 // NOUVEAU: Poids pour le clustering botnet
166
252
  },
167
253
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
168
254
  patterns: {
@@ -198,7 +284,8 @@ const securityProfiles = {
198
284
  timeInconsistencyScore: 0.9,
199
285
  tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
200
286
  subnetScore: 0.5,
201
- ipReputationScore: 0.6 // NOUVEAU: Poids pour la réputation IP
287
+ ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
288
+ botnetClusterScore: 0.9 // NOUVEAU: Poids pour le clustering botnet
202
289
  },
203
290
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
204
291
  patterns: {
@@ -319,12 +406,12 @@ function getTlsFingerprint(context) {
319
406
  let ja4 = null;
320
407
 
321
408
  // 1. Prefer JA4 hash from a trusted reverse proxy header.
322
- const ja4FromHeader = context.headers['x-ja4-hash'];
409
+ const ja4FromHeader = context.headers ? context.headers['x-ja4-hash'] : null;
323
410
  if (ja4FromHeader) {
324
411
  ja4 = ja4FromHeader;
325
412
  }
326
413
  // 2. Prefer JA3 hash from a trusted reverse proxy header.
327
- const ja3FromHeader = context.headers['x-ja3-hash']; // Assuming a proxy might provide JA3 too
414
+ const ja3FromHeader = context.headers ? context.headers['x-ja3-hash'] : null; // Assuming a proxy might provide JA3 too
328
415
  if (ja3FromHeader) {
329
416
  ja3 = ja3FromHeader;
330
417
  }
@@ -425,7 +512,7 @@ function getCompositeDeviceHash(context) {
425
512
  // notre propre fingerprint serveur pour le comparer.
426
513
  // Un attaquant qui forge un `clientFp` mais oublie de forger les en-têtes
427
514
  // correspondants sera détecté par l'incohérence.
428
- const clientFp = context.headers['x-device-fingerprint'];
515
+ const clientFp = context.headers ? context.headers['x-device-fingerprint'] : null;
429
516
  if (clientFp && typeof clientFp === 'string' && clientFp.includes('cvs:')) {
430
517
  // On ajoute le hash du fingerprint client comme un composant du fingerprint serveur.
431
518
  // Si le clientFp change, le hash serveur changera aussi.
@@ -433,7 +520,7 @@ function getCompositeDeviceHash(context) {
433
520
  }
434
521
 
435
522
  // 1. SIGNAL FORT: User Agent (poids élevé)
436
- const ua = context.headers["user-agent"];
523
+ const ua = context.headers ? context.headers["user-agent"] : null;
437
524
  if (ua) {
438
525
  srv.add("ua", ua); // User-Agent
439
526
  }
@@ -443,10 +530,10 @@ function getCompositeDeviceHash(context) {
443
530
  if (ja3) srv.add("ja3", ja3);
444
531
  if (ja4) srv.add("ja4", ja4);
445
532
 
446
- const h2Fingerprint = context.headers['x-http2-fingerprint'];
533
+ const h2Fingerprint = context.headers ? context.headers['x-http2-fingerprint'] : null;
447
534
  if (h2Fingerprint) srv.add("h2", h2Fingerprint);
448
535
 
449
- const tcpFingerprint = context.headers['x-tcp-fingerprint'];
536
+ const tcpFingerprint = context.headers ? context.headers['x-tcp-fingerprint'] : null;
450
537
  if (tcpFingerprint) srv.add("tcp", tcpFingerprint);
451
538
 
452
539
  // 3. SIGNAUX DE HAUT NIVEAU (Applicatif) Moins fiables, mais utiles pour la corroboration
@@ -457,6 +544,7 @@ function getCompositeDeviceHash(context) {
457
544
  "ch_model": "sec-ch-ua-model",
458
545
  "ch_arch": "sec-ch-ua-arch",
459
546
  "ch_bitness": "sec-ch-ua-bitness",
547
+ "ch_full_version_list": "sec-ch-ua-full-version-list",
460
548
  "upgrade_req": "upgrade-insecure-requests",
461
549
  "accept_lang": "accept-language",
462
550
  "accept_enc": "accept-encoding",
@@ -464,7 +552,7 @@ function getCompositeDeviceHash(context) {
464
552
  };
465
553
 
466
554
  for (const [key, headerName] of Object.entries(headersToCapture)) {
467
- const headerValue = context.headers[headerName];
555
+ const headerValue = context.headers ? context.headers[headerName] : null;
468
556
  if (headerValue) {
469
557
  srv.add(key, headerValue);
470
558
  }
@@ -799,18 +887,16 @@ export const verifyPoWAndGenerateTicket = async (
799
887
  return null;
800
888
  }
801
889
 
802
- // 2. Generate an opaque ticket ID and store session metadata securely on the server
803
- const ticketId = crypto.randomUUID();
890
+ // 2. Generate a signed and encrypted stateless ticket
804
891
  const expiry = Date.now() + 3600000; // 1 hour
805
-
806
- await store.set(`ticket:${ticketId}`, {
892
+ const payload = {
807
893
  expiry,
808
894
  originalIp: ip,
809
895
  deviceId,
810
896
  deviceHash
811
- }, 3600); // 1 hour TTL
897
+ };
812
898
 
813
- return ticketId;
899
+ return generateStatelessTicket(payload);
814
900
  };
815
901
 
816
902
 
@@ -866,7 +952,26 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
866
952
  // Input validation: ensure the ticket is a non-empty string with the correct format.
867
953
  if (typeof ticket !== 'string' || ticket.length === 0) return false;
868
954
 
869
- // 1. Resolve opaque ticket session from server-side store
955
+ // 1. Resolve stateless ticket first (zero database I/O cost)
956
+ const statelessData = parseStatelessTicket(ticket);
957
+ if (statelessData) {
958
+ const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
959
+
960
+ if (!expiry || Date.now() > expiry) {
961
+ return false;
962
+ }
963
+
964
+ if (ip === originalIp) return true;
965
+ const currentSubnet = getIpSubnet(ip);
966
+ const originalSubnet = getIpSubnet(originalIp);
967
+ if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
968
+
969
+ if (!allowCrossNetworkRoaming) return false;
970
+
971
+ return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
972
+ }
973
+
974
+ // 2. Resolve opaque ticket session from server-side store
870
975
  const ticketData = await store.get(`ticket:${ticket}`);
871
976
  if (ticketData) {
872
977
  const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = ticketData;
@@ -886,7 +991,7 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
886
991
  return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
887
992
  }
888
993
 
889
- // 2. Legacy fallback verification (backward compatibility for old client tokens)
994
+ // 3. Legacy fallback verification (backward compatibility for old client tokens)
890
995
  let expiry, originalIp, sig;
891
996
  if (ticket.includes('|')) {
892
997
  const parts = ticket.split('|');
@@ -1149,6 +1254,79 @@ function analyzeMouseMovements(history) {
1149
1254
  return { avgSpeed, avgAcceleration, straightness, pauses, segments: segments.map(s => s.distance) };
1150
1255
  }
1151
1256
 
1257
+ /**
1258
+ * @private
1259
+ * Analyse une série d'événements tactiles mobiles pour en extraire des indicateurs comportementaux robustes.
1260
+ * @param {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} history
1261
+ * @returns {{avgSpeed: number, avgAcceleration: number, straightness: number, pauses: number, segments: Array<number>, avgPressure: number, avgRadius: number, pressureVariance: number, radiusVariance: number, maxTouches: number}}
1262
+ */
1263
+ function analyzeTouchMovements(history) {
1264
+ if (!history || history.length < 3) {
1265
+ return { avgSpeed: 0, avgAcceleration: 0, straightness: 1, pauses: 0, segments: [], avgPressure: 0, avgRadius: 0, pressureVariance: 0, radiusVariance: 0, maxTouches: 1 };
1266
+ }
1267
+
1268
+ const segments = [];
1269
+ let totalDistance = 0;
1270
+ let pauses = 0;
1271
+ let totalPressure = 0;
1272
+ let totalRadius = 0;
1273
+ let maxTouches = 1;
1274
+
1275
+ for (let i = 1; i < history.length; i++) {
1276
+ const p1 = history[i - 1];
1277
+ const p2 = history[i];
1278
+ const dx = p2.x - p1.x;
1279
+ const dy = p2.y - p1.y;
1280
+ const dt = p2.t - p1.t;
1281
+ const distance = Math.sqrt(dx * dx + dy * dy);
1282
+
1283
+ totalPressure += p2.p || 0;
1284
+ totalRadius += p2.r || 0;
1285
+ if (p2.num > maxTouches) {
1286
+ maxTouches = p2.num;
1287
+ }
1288
+
1289
+ if (dt > 0) {
1290
+ const speed = distance / dt;
1291
+ segments.push({ distance, dt, speed });
1292
+ totalDistance += distance;
1293
+ }
1294
+ if (dt > 100 && distance < 5) {
1295
+ pauses++;
1296
+ }
1297
+ }
1298
+
1299
+ totalPressure += history[0].p || 0;
1300
+ totalRadius += history[0].r || 0;
1301
+
1302
+ const avgPressure = totalPressure / history.length;
1303
+ const avgRadius = totalRadius / history.length;
1304
+
1305
+ let sqDiffPressureSum = 0;
1306
+ let sqDiffRadiusSum = 0;
1307
+ for (const pt of history) {
1308
+ sqDiffPressureSum += Math.pow((pt.p || 0) - avgPressure, 2);
1309
+ sqDiffRadiusSum += Math.pow((pt.r || 0) - avgRadius, 2);
1310
+ }
1311
+ const pressureVariance = sqDiffPressureSum / history.length;
1312
+ const radiusVariance = sqDiffRadiusSum / history.length;
1313
+
1314
+ if (segments.length < 2) {
1315
+ return { avgSpeed: 0, avgAcceleration: 0, straightness: 1, pauses, segments: [], avgPressure, avgRadius, pressureVariance, radiusVariance, maxTouches };
1316
+ }
1317
+
1318
+ const totalTime = history[history.length - 1].t - history[0].t;
1319
+ const avgSpeed = totalTime > 0 ? segments.reduce((sum, s) => sum + s.speed, 0) / segments.length : 0;
1320
+ const avgAcceleration = segments.reduce((sum, s) => sum + (s.speed / s.dt), 0) / segments.length;
1321
+
1322
+ const startPoint = history[0];
1323
+ const endPoint = history[history.length - 1];
1324
+ const straightDistance = Math.sqrt(Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2));
1325
+ const straightness = totalDistance > 0 ? straightDistance / totalDistance : 1;
1326
+
1327
+ return { avgSpeed, avgAcceleration, straightness, pauses, segments: segments.map(s => s.distance), avgPressure, avgRadius, pressureVariance, radiusVariance, maxTouches };
1328
+ }
1329
+
1152
1330
  /**
1153
1331
  * Calcule un score basé sur les métriques comportementales envoyées par le client.
1154
1332
  * @param {object} context - Le contexte de la requête, contenant les en-têtes.
@@ -1171,9 +1349,10 @@ function getBehaviorScore(context) {
1171
1349
 
1172
1350
  // 2. Analyse des mouvements de la souris
1173
1351
  const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
1352
+ const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
1174
1353
 
1175
1354
  // Pénalité pour absence totale d'interaction (pas de mouvements, pas de frappes).
1176
- if (avgSpeed === 0 && metrics.keystrokeLatency === 0) {
1355
+ if (avgSpeed === 0 && touchAnalysis.avgSpeed === 0 && metrics.keystrokeLatency === 0) {
1177
1356
  score += 40;
1178
1357
  }
1179
1358
 
@@ -1197,6 +1376,30 @@ function getBehaviorScore(context) {
1197
1376
  if (pauses === 0 && segments.length > 20) score += 15; // Mouvement continu sans micro-pauses
1198
1377
  }
1199
1378
 
1379
+ // 4. Analyse comportementale des événements tactiles (Touch Move)
1380
+ const touchHistory = metrics.touchMovementsHistory;
1381
+ if (touchHistory && touchHistory.length > 0) {
1382
+ const touch = analyzeTouchMovements(touchHistory);
1383
+ if (touch.avgSpeed > 0) {
1384
+ if (touch.avgSpeed > 5) score += 30; // Touch d'une vitesse anormale/robotique
1385
+ if (touch.avgAcceleration > 0.8) score += 20;
1386
+ if (touch.straightness > 0.98) score += 35; // Un tracé de doigt humain n'est jamais parfaitement rectiligne
1387
+ if (touch.pauses === 0 && touch.segments.length > 25) score += 15;
1388
+
1389
+ // Détection de l'émulation (pression et rayon de contact constants)
1390
+ if (touch.avgPressure > 0 && touch.pressureVariance === 0) {
1391
+ score += 30; // Spoofed force/pressure
1392
+ }
1393
+ if (touch.avgRadius > 0 && touch.radiusVariance === 0) {
1394
+ score += 30; // Spoofed pointer area size
1395
+ }
1396
+ }
1397
+ if (touch.segments.length > 10) {
1398
+ const benfordDev = Optimization.Operators.benfordTest(touch.segments);
1399
+ if (benfordDev > 0.18) score += 35;
1400
+ }
1401
+ }
1402
+
1200
1403
  // Plausibilité de la latence de frappe
1201
1404
  if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
1202
1405
  if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
@@ -1513,6 +1716,48 @@ function getClientHintsInconsistencyScore(context) {
1513
1716
  return { clientHintsInconsistencyScore: 0 };
1514
1717
  }
1515
1718
 
1719
+ const fullVersionList = context.headers['sec-ch-ua-full-version-list'];
1720
+ if (fullVersionList) {
1721
+ let chFullVersion = null;
1722
+ let chFullBrowser = null;
1723
+ const matches = [...fullVersionList.matchAll(/"([^"]+)";v="([^"]+)"/g)];
1724
+ for (const match of matches) {
1725
+ const brand = match[1];
1726
+ const version = match[2];
1727
+ if (brand === 'Google Chrome' || brand === 'Chromium' || brand === 'Microsoft Edge') {
1728
+ chFullVersion = version;
1729
+ chFullBrowser = brand === 'Microsoft Edge' ? 'Edge' : 'Chrome';
1730
+ if (brand === 'Google Chrome' || brand === 'Microsoft Edge') {
1731
+ break;
1732
+ }
1733
+ }
1734
+ }
1735
+ if (chFullVersion && chFullBrowser) {
1736
+ let uaFullVersion = null;
1737
+ const uaFullMatch = ua.match(/(Chrome|Edg)\/([\d\.]+)/);
1738
+ if (uaFullMatch) {
1739
+ const uaBrowserMapped = uaFullMatch[1] === 'Edg' ? 'Edge' : 'Chrome';
1740
+ uaFullVersion = uaFullMatch[2];
1741
+ if (uaBrowserMapped === chFullBrowser && uaFullVersion !== chFullVersion) {
1742
+ const parts1 = uaFullVersion.split('.').map(Number);
1743
+ const parts2 = chFullVersion.split('.').map(Number);
1744
+ let diffIndex = -1;
1745
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
1746
+ if ((parts1[i] || 0) !== (parts2[i] || 0)) {
1747
+ diffIndex = i;
1748
+ break;
1749
+ }
1750
+ }
1751
+ const baseScores = [95, 90, 85, 80];
1752
+ const baseScore = baseScores[diffIndex] || 80;
1753
+ const delta = Math.abs((parts1[diffIndex] || 0) - (parts2[diffIndex] || 0));
1754
+ const finalFullScore = Math.min(100, baseScore + Math.min(5, delta * 5));
1755
+ return { clientHintsInconsistencyScore: finalFullScore };
1756
+ }
1757
+ }
1758
+ }
1759
+ }
1760
+
1516
1761
  // 1. Extract browser and version from User-Agent
1517
1762
  let uaVersion = null;
1518
1763
  let uaBrowser = null;
@@ -1547,10 +1792,19 @@ function getClientHintsInconsistencyScore(context) {
1547
1792
 
1548
1793
  const versionDifference = Math.abs(parseInt(uaVersion, 10) - parseInt(chVersion, 10));
1549
1794
 
1550
- if (versionDifference > 5) return { clientHintsInconsistencyScore: 80 };
1551
- if (versionDifference > 1) return { clientHintsInconsistencyScore: 40 };
1795
+ let clientHintsInconsistencyScore = 0;
1796
+ if (versionDifference > 0) {
1797
+ if (versionDifference <= 2) {
1798
+ clientHintsInconsistencyScore = versionDifference * 20;
1799
+ } else if (versionDifference <= 7) {
1800
+ clientHintsInconsistencyScore = 40 + (versionDifference - 2) * 8;
1801
+ } else {
1802
+ clientHintsInconsistencyScore = Math.min(100, 80 + (versionDifference - 7) * 3.33);
1803
+ }
1804
+ clientHintsInconsistencyScore = Math.round(clientHintsInconsistencyScore);
1805
+ }
1552
1806
 
1553
- return { clientHintsInconsistencyScore: 0 };
1807
+ return { clientHintsInconsistencyScore };
1554
1808
  }
1555
1809
 
1556
1810
  /**
@@ -1738,14 +1992,18 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
1738
1992
  subnetData.highScoreDevices = {};
1739
1993
  }
1740
1994
 
1741
- const currentDeviceContributions = subnetData.highScoreDevices[deviceId] || 0;
1995
+ // Utilisation d'un identifiant d'appareil stable (fingerprint matériel) plutôt que l'ID de cookie volatil
1996
+ const currentDeviceHash = getCompositeDeviceHash(context);
1997
+ const stableFpId = cyrb53(extractStablePart(currentDeviceHash)).toString();
1998
+
1999
+ const currentDeviceContributions = subnetData.highScoreDevices[stableFpId] || 0;
1742
2000
  if (currentDeviceContributions < 5) {
1743
- subnetData.highScoreDevices[deviceId] = currentDeviceContributions + 1;
2001
+ subnetData.highScoreDevices[stableFpId] = currentDeviceContributions + 1;
1744
2002
  subnetData.highScoreCount++;
1745
2003
  }
1746
2004
 
1747
- if (!subnetData.deviceIds.includes(deviceId)) {
1748
- subnetData.deviceIds.push(deviceId);
2005
+ if (!subnetData.deviceIds.includes(stableFpId)) {
2006
+ subnetData.deviceIds.push(stableFpId);
1749
2007
  }
1750
2008
  subnetData.lastActivity = Date.now();
1751
2009
 
@@ -1792,6 +2050,40 @@ async function getSubnetScore(context) {
1792
2050
  return { subnetScore: Math.min(100, deviceCountPenalty + highScorePenalty) };
1793
2051
  }
1794
2052
 
2053
+ /**
2054
+ * Calcule le score d'anomalie de similarité réseau (Botnet Clustering).
2055
+ * @param {object} context - Le contexte de la requête.
2056
+ * @param {string} stableFpHash - Le hash de la partie stable de l'empreinte.
2057
+ * @returns {Promise<{botnetClusterScore: number}>}
2058
+ */
2059
+ async function getBotnetClusterScore(context, stableFpHash) {
2060
+ if (!stableFpHash) return { botnetClusterScore: 0 };
2061
+ const key = `botnet-cluster:${stableFpHash}`;
2062
+ const now = Date.now();
2063
+ const tenMinutesAgo = now - 600 * 1000;
2064
+
2065
+ let clusterData = (await store.get(key)) || [];
2066
+ if (!Array.isArray(clusterData)) {
2067
+ clusterData = [];
2068
+ }
2069
+
2070
+ clusterData = clusterData.filter(entry => entry.timestamp > tenMinutesAgo);
2071
+ const existingIndex = clusterData.findIndex(entry => entry.ip === context.clientIp);
2072
+ if (existingIndex !== -1) {
2073
+ clusterData[existingIndex].timestamp = now;
2074
+ } else {
2075
+ clusterData.push({ ip: context.clientIp, timestamp: now });
2076
+ }
2077
+
2078
+ await store.set(key, clusterData, 600);
2079
+ const uniqueIpsCount = clusterData.length;
2080
+ let botnetClusterScore = 0;
2081
+ if (uniqueIpsCount >= 2) {
2082
+ botnetClusterScore = Math.min(100, Math.round(1000 * (1 - Math.exp(-0.35 * (uniqueIpsCount - 1)))) / 10);
2083
+ }
2084
+ return { botnetClusterScore };
2085
+ }
2086
+
1795
2087
  /**
1796
2088
  * Retrieves the current local IP reputation score, applying time-based decay.
1797
2089
  * @param {string} ip - The client's IP address.
@@ -1858,7 +2150,10 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
1858
2150
  benfordThreshold = 0.15, // Seuil de déviation de Benford au-dessus duquel la distribution est "non naturelle".
1859
2151
  patternWeight = 80, // Pénalité FORTE et unique si un pattern est détecté.
1860
2152
  decayFactor = 0.95, // Décroissance du score dans le temps.
1861
- inactivityReset = 180000 // Réinitialisation du score après 3 minutes d'inactivité.
2153
+ inactivityReset = 180000, // Réinitialisation du score après 3 minutes d'inactivité.
2154
+ regularityRatio = 0.4, // (NOUVEAU) Poids relatif de l'écart-type
2155
+ benfordRatio = 0.3, // (NOUVEAU) Poids relatif de Benford
2156
+ enumerationRatio = 0.3 // (NOUVEAU) Poids relatif de l'énumération de chemins
1862
2157
  } = patternConfig;
1863
2158
 
1864
2159
  const now = Date.now();
@@ -1888,24 +2183,24 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
1888
2183
  deviceData.timingHistory.push(timeSinceLast);
1889
2184
  }
1890
2185
 
1891
- let instantScore = 0;
2186
+ let regularityScore = 0;
2187
+ let benfordScore = 0;
1892
2188
  const timings = deviceData.timingHistory;
1893
2189
 
1894
2190
  // Analyse statistique unifiée si nous avons assez de données
1895
2191
  if (timings.length >= minSamples) {
1896
- const timings = deviceData.timingHistory;
1897
2192
  const mean = timings.reduce((a, b) => a + b, 0) / timings.length;
1898
2193
  const variance = timings.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / timings.length;
1899
2194
  const stdDev = Math.sqrt(variance);
1900
2195
  const benfordDeviation = Optimization.Operators.benfordTest(timings);
1901
2196
 
1902
- // Détection de régularité (bots de type "cron")
2197
+ // Calcul progressif de la régularité (stdDev proche de 0 = score max)
1903
2198
  if (stdDev < regularityThreshold) {
1904
- instantScore = patternWeight;
2199
+ regularityScore = 1 - (stdDev / regularityThreshold);
1905
2200
  }
1906
- // Détection de distribution non-naturelle (bots "faussement aléatoires")
1907
- else if (benfordDeviation > benfordThreshold) {
1908
- instantScore = patternWeight;
2201
+ // Calcul progressif de Benford (excès par rapport au seuil)
2202
+ if (benfordDeviation > benfordThreshold) {
2203
+ benfordScore = Math.min(1, (benfordDeviation - benfordThreshold) / (0.5 - benfordThreshold));
1909
2204
  }
1910
2205
  }
1911
2206
 
@@ -1919,12 +2214,18 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
1919
2214
  templates.forEach(t => templateCounts[t] = (templateCounts[t] || 0) + 1);
1920
2215
 
1921
2216
  const maxTemplateRepetition = Math.max(...Object.values(templateCounts), 0);
1922
- // Si une même structure de route est répétée mais sur des URLs réelles différentes
1923
2217
  if (maxTemplateRepetition >= 3 && uniquePaths.size === history.length) {
1924
- enumerationScore = patternWeight * 0.8; // Appliquer une forte pénalité
2218
+ enumerationScore = Math.min(1, (maxTemplateRepetition - 2) / 5);
1925
2219
  }
1926
2220
  }
1927
2221
 
2222
+ // Score instantané combiné linéaire pondéré
2223
+ const weightedScore = (regularityScore * regularityRatio) +
2224
+ (benfordScore * benfordRatio) +
2225
+ (enumerationScore * enumerationRatio);
2226
+
2227
+ const instantScore = weightedScore * patternWeight;
2228
+
1928
2229
  // Garder l'historique à une taille raisonnable
1929
2230
  if (history.length > historySize) {
1930
2231
  history.shift();
@@ -1943,7 +2244,7 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
1943
2244
  }
1944
2245
  newPatternScore = Math.max(0, newPatternScore);
1945
2246
 
1946
- deviceData.lastPatternScore = newPatternScore + instantScore + enumerationScore;
2247
+ deviceData.lastPatternScore = Math.max(instantScore, newPatternScore);
1947
2248
 
1948
2249
  return { requestPatternScore: Math.min(100, deviceData.lastPatternScore) };
1949
2250
  }
@@ -2175,6 +2476,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
2175
2476
  const { deviceId, deviceData, consistencyScore, newCookie } = await resolveRequestIdentity(context, securityConfig);
2176
2477
 
2177
2478
  const clientIp = context.clientIp;
2479
+ const currentDeviceHash = getCompositeDeviceHash(context);
2178
2480
 
2179
2481
  // If a new cookie needs to be set, attach it to the request object
2180
2482
  // so the middleware can handle it. This is a temporary state holder.
@@ -2230,6 +2532,12 @@ export const getSuspicionVector = async (context, securityConfig) => {
2230
2532
 
2231
2533
  const ipReputationScore = await getIpReputationScore(clientIp);
2232
2534
 
2535
+ const stableFp = extractStablePart(currentDeviceHash);
2536
+ const stableFpHash = cyrb53(stableFp).toString();
2537
+ const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
2538
+
2539
+ const { tcpAnomalyScore } = getTcpAnomalyScore(context);
2540
+
2233
2541
  // Save the updated device state to the store
2234
2542
  // 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.
2235
2543
  await store.set(`device:${deviceId}`, deviceData);
@@ -2240,7 +2548,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
2240
2548
  deviceData.ips = new Set(deviceData.ips);
2241
2549
  }
2242
2550
  // Le vecteur de suspicion est maintenant complet.
2243
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore };
2551
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore };
2244
2552
  };
2245
2553
 
2246
2554
  // A residential user can change networks (home, 4G, public wifi).
@@ -2536,19 +2844,15 @@ export async function verifyCpuTargetPoWAndGenerateTicket(
2536
2844
  if (isValid) {
2537
2845
  console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
2538
2846
  });
2539
- // Generate an opaque ticket ID and store session metadata securely on the server
2540
- const ticketId = crypto.randomUUID();
2541
2847
  const ttl = ticketTtl || 3600000; // Calculates expiration from TTL
2542
2848
  const expiry = Date.now() + ttl;
2543
2849
 
2544
- await store.set(`ticket:${ticketId}`, {
2850
+ return generateStatelessTicket({
2545
2851
  expiry,
2546
2852
  originalIp: clientIp,
2547
2853
  deviceId,
2548
2854
  deviceHash
2549
2855
  }, Math.ceil(ttl / 1000));
2550
-
2551
- return ticketId;
2552
2856
  }
2553
2857
 
2554
2858
  return null;
@@ -2644,12 +2948,14 @@ export class FingerprintEngine {
2644
2948
  (suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
2645
2949
  (suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
2646
2950
  (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
2951
+ (suspicionVector.botnetClusterScore || 0) * (weights.botnetClusterScore || 0) +
2647
2952
  (suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
2648
2953
  (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
2649
2954
  (suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0) +
2650
2955
  (suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
2651
2956
  (suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
2652
- (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0);
2957
+ (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
2958
+ (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0);
2653
2959
 
2654
2960
  return Math.min(100, score);
2655
2961
  }
@@ -3374,6 +3680,28 @@ export class FingerprintEngine {
3374
3680
  if (mustReChallenge) {
3375
3681
  this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
3376
3682
  }
3683
+
3684
+ // --- AJOUT : Limiteur de débit (Token Bucket) ---
3685
+ const rateLimitPassed = await checkChallengeRateLimit(clientIp);
3686
+ if (!rateLimitPassed) {
3687
+ this._log('Challenge rate limit exceeded - blocking with 429', { clientIp });
3688
+ const decision = {
3689
+ action: 'block',
3690
+ status: 429,
3691
+ body: 'Too Many Requests',
3692
+ score: finalScore,
3693
+ vector: suspicionVector
3694
+ };
3695
+ if (this.dryRun) {
3696
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
3697
+ decision.intendedAction = decision.action;
3698
+ decision.action = 'next';
3699
+ delete decision.status;
3700
+ delete decision.body;
3701
+ }
3702
+ return decision;
3703
+ }
3704
+
3377
3705
  this._log('Suspicious request without valid ticket - issuing challenge', { finalScore, hasPowCookie: !!powCookie });
3378
3706
 
3379
3707
  // --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
@@ -3773,6 +4101,185 @@ function determineOptimalTicketTtl(suspicionScore) {
3773
4101
  return ttl;
3774
4102
  }
3775
4103
 
4104
+
4105
+ /**
4106
+ * Parse une trame TCP SYN brute (IPv4 ou IPv6).
4107
+ * @private
4108
+ * @param {Buffer|Uint8Array} binary - Le paquet binaire.
4109
+ * @returns {object|null}
4110
+ */
4111
+ function parseTcpSyn(binary) {
4112
+ if (!binary || binary.length < 40) return null;
4113
+ let ttl = 64;
4114
+ let tcpOffset = 20;
4115
+ const version = binary[0] >> 4;
4116
+
4117
+ if (version === 4) {
4118
+ ttl = binary[8];
4119
+ const ihl = binary[0] & 0x0f;
4120
+ tcpOffset = ihl * 4;
4121
+ } else if (version === 6) {
4122
+ ttl = binary[7]; // Hop Limit
4123
+ tcpOffset = 40;
4124
+ } else {
4125
+ tcpOffset = 0;
4126
+ ttl = 64;
4127
+ }
4128
+
4129
+ if (binary.length < tcpOffset + 20) return null;
4130
+
4131
+ const windowSize = (binary[tcpOffset + 14] << 8) | binary[tcpOffset + 15];
4132
+ const dataOffset = (binary[tcpOffset + 12] >> 4) * 4;
4133
+ const optionsEnd = tcpOffset + dataOffset;
4134
+
4135
+ let mss = null;
4136
+ let ws = null;
4137
+ let sack = false;
4138
+
4139
+ let i = tcpOffset + 20;
4140
+ while (i < optionsEnd && i < binary.length) {
4141
+ const optType = binary[i];
4142
+ if (optType === 0) break;
4143
+ if (optType === 1) {
4144
+ i++;
4145
+ continue;
4146
+ }
4147
+ if (i + 1 >= binary.length) break;
4148
+ const optLen = binary[i + 1];
4149
+ if (optLen < 2 || i + optLen > binary.length) break;
4150
+
4151
+ if (optType === 2 && optLen === 4) {
4152
+ mss = (binary[i + 2] << 8) | binary[i + 3];
4153
+ } else if (optType === 3 && optLen === 3) {
4154
+ ws = binary[i + 2];
4155
+ } else if (optType === 4 && optLen === 2) {
4156
+ sack = true;
4157
+ }
4158
+ i += optLen;
4159
+ }
4160
+
4161
+ return { ttl, windowSize, mss, ws, sack };
4162
+ }
4163
+
4164
+ /**
4165
+ * Classifie l'OS à partir du fingerprint de la pile TCP/IP.
4166
+ * @private
4167
+ * @param {object|null} fingerprint
4168
+ * @returns {string}
4169
+ */
4170
+ function classifyTcpOs(fingerprint) {
4171
+ if (!fingerprint) return 'unknown';
4172
+ const ttl = fingerprint.ttl ?? 64;
4173
+ const windowSize = fingerprint.windowSize ?? 0;
4174
+ const ws = fingerprint.ws ?? null;
4175
+
4176
+ if (ttl > 64 && ttl <= 128) {
4177
+ return 'Windows';
4178
+ }
4179
+ if (ttl > 32 && ttl <= 64) {
4180
+ if (windowSize === 29200 || windowSize === 14600 || windowSize === 5840) {
4181
+ return 'Linux';
4182
+ }
4183
+ return 'Linux';
4184
+ }
4185
+ if (ttl <= 64) {
4186
+ if (windowSize === 65535 && (ws === 6 || ws === 8 || ws === 5)) {
4187
+ return 'macOS/iOS';
4188
+ }
4189
+ }
4190
+ if (ttl > 64) return 'Windows';
4191
+ if (ttl > 0) return 'Linux';
4192
+ return 'unknown';
4193
+ }
4194
+
4195
+ /**
4196
+ * Détecte les anomalies de pile réseau par rapport au User-Agent.
4197
+ * @private
4198
+ * @param {object} context - Le contexte de la requête.
4199
+ * @returns {{tcpAnomalyScore: number}}
4200
+ */
4201
+ function getTcpAnomalyScore(context) {
4202
+ let fp = null;
4203
+ const rawTcpBinary = context.headers?.['x-raw-tcp-binary'] || context.rawTcpBinary || null;
4204
+ if (rawTcpBinary) {
4205
+ const binary = Buffer.isBuffer(rawTcpBinary) ? rawTcpBinary : (typeof rawTcpBinary === 'string' ? Buffer.from(rawTcpBinary, 'hex') : rawTcpBinary);
4206
+ fp = parseTcpSyn(binary);
4207
+ }
4208
+
4209
+ if (!fp) {
4210
+ const tcpHeader = context.headers?.['x-tcp-fingerprint'] || context.tcpFingerprint || null;
4211
+ if (tcpHeader && typeof tcpHeader === 'string') {
4212
+ const parts = tcpHeader.split(':');
4213
+ if (parts.length >= 2) {
4214
+ fp = {
4215
+ ttl: parseInt(parts[0], 10),
4216
+ windowSize: parseInt(parts[1], 10),
4217
+ mss: parts[2] ? parseInt(parts[2], 10) : null,
4218
+ ws: parts[3] ? parseInt(parts[3], 10) : null,
4219
+ sack: parts[4] === '1' || parts[4] === 'true'
4220
+ };
4221
+ }
4222
+ }
4223
+ }
4224
+
4225
+ if (!fp) {
4226
+ return { tcpAnomalyScore: 0.0 };
4227
+ }
4228
+
4229
+ const tcpOs = classifyTcpOs(fp);
4230
+ const ua = context.headers?.['user-agent'] || '';
4231
+ const uaParts = parseUserAgent(ua);
4232
+ const uaOs = uaParts.os;
4233
+
4234
+ if (!uaOs || tcpOs === 'unknown') {
4235
+ return { tcpAnomalyScore: 0.0 };
4236
+ }
4237
+
4238
+ let mappedOs = null;
4239
+ if (uaOs.startsWith('Windows')) mappedOs = 'Windows';
4240
+ else if (uaOs.startsWith('Mac') || uaOs.startsWith('macOS')) mappedOs = 'macOS';
4241
+ else if (uaOs.startsWith('iOS')) mappedOs = 'iOS';
4242
+ else if (uaOs.startsWith('Linux')) mappedOs = 'Linux';
4243
+
4244
+ if (!mappedOs) {
4245
+ return { tcpAnomalyScore: 0.0 };
4246
+ }
4247
+
4248
+ const OS_EXPECTED_TCP = {
4249
+ 'Windows': { ttl: 128, windowSize: 64240, ws: 8, mss: 1460, sack: true },
4250
+ 'Linux': { ttl: 64, windowSize: 29200, ws: 7, mss: 1460, sack: true },
4251
+ 'macOS': { ttl: 64, windowSize: 65535, ws: 6, mss: 1460, sack: true },
4252
+ 'iOS': { ttl: 64, windowSize: 65535, ws: 6, mss: 1460, sack: true }
4253
+ };
4254
+
4255
+ const expected = OS_EXPECTED_TCP[mappedOs];
4256
+ const ttlDiff = Math.abs(fp.ttl - expected.ttl) / expected.ttl;
4257
+ const winDiff = Math.abs(fp.windowSize - expected.windowSize) / expected.windowSize;
4258
+ const wsDiff = expected.ws !== null && fp.ws !== null ? Math.abs(fp.ws - expected.ws) / expected.ws : 0.0;
4259
+ const mssDiff = expected.mss !== null && fp.mss !== null ? Math.abs(fp.mss - expected.mss) / expected.mss : 0.0;
4260
+ const sackDiff = (fp.sack ?? true) === (expected.sack ?? true) ? 0.0 : 1.0;
4261
+
4262
+ const deviation = (
4263
+ Math.min(1.0, ttlDiff) * 0.50 +
4264
+ Math.min(1.0, winDiff) * 0.25 +
4265
+ Math.min(1.0, wsDiff) * 0.15 +
4266
+ Math.min(1.0, mssDiff) * 0.05 +
4267
+ sackDiff * 0.05
4268
+ );
4269
+
4270
+ let tcpAnomalyScore = 0.0;
4271
+ if (tcpOs !== mappedOs && tcpOs !== 'unknown') {
4272
+ const baseAnomaly = mappedOs === 'Windows' ? 80.0 :
4273
+ (mappedOs === 'macOS' || mappedOs === 'iOS' ? 85.0 : 75.0);
4274
+ tcpAnomalyScore = baseAnomaly + (deviation - 0.4) * 10.0;
4275
+ } else {
4276
+ tcpAnomalyScore = deviation * 40.0;
4277
+ }
4278
+
4279
+ tcpAnomalyScore = Math.max(0.0, Math.min(100.0, Math.round(tcpAnomalyScore * 10) / 10));
4280
+ return { tcpAnomalyScore };
4281
+ }
4282
+
3776
4283
  /**
3777
4284
  * Vérifie si une chaîne de caractères contient des patterns d'injection connus.
3778
4285
  * @private
@@ -4037,8 +4544,19 @@ export const powMiddleware = (securityConfig) => {
4037
4544
 
4038
4545
  if (jsFile && req.path === jsPath) {
4039
4546
  try {
4040
- const fileContent = readFileSync(jsFile);
4041
- res.setHeader('Content-Type', 'application/javascript');
4547
+ if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
4548
+ console.log('[Fingerprint] Generating dynamic polymorphic WASM module...');
4549
+ // Génère des constantes aléatoires uniques pour cette session / requête
4550
+ const seed = crypto.randomBytes(4).readInt32LE(0);
4551
+ const multiplier = crypto.randomBytes(4).readInt32LE(0) | 1; // Doit être impair pour un LCG optimal
4552
+ const adder = crypto.randomBytes(4).readInt32LE(0);
4553
+
4554
+ const wasmBuffer = DynamicWasmGenerator.generate({ seed, multiplier, adder });
4555
+ res.setHeader('Content-Type', 'application/wasm');
4556
+ return res.send(wasmBuffer);
4557
+ }
4558
+ const fileContent = readFileSync(wasmFile);
4559
+ res.setHeader('Content-Type', 'application/wasm');
4042
4560
  return res.send(fileContent);
4043
4561
  } catch (e) {
4044
4562
  // Fallback
@@ -4046,6 +4564,16 @@ export const powMiddleware = (securityConfig) => {
4046
4564
  }
4047
4565
  if (wasmFile && req.path === wasmPath) {
4048
4566
  try {
4567
+ if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
4568
+ // Génère des constantes aléatoires uniques pour cette session / requête
4569
+ const seed = crypto.randomBytes(4).readInt32LE(0);
4570
+ const multiplier = crypto.randomBytes(4).readInt32LE(0) | 1; // Doit être impair pour un LCG optimal
4571
+ const adder = crypto.randomBytes(4).readInt32LE(0);
4572
+
4573
+ const wasmBuffer = DynamicWasmGenerator.generate({ seed, multiplier, adder });
4574
+ res.setHeader('Content-Type', 'application/wasm');
4575
+ return res.send(wasmBuffer);
4576
+ }
4049
4577
  const fileContent = readFileSync(wasmFile);
4050
4578
  res.setHeader('Content-Type', 'application/wasm');
4051
4579
  return res.send(fileContent);
@@ -4143,7 +4671,10 @@ export const __internal = {
4143
4671
  getTlsFingerprint, // NOUVEAU: Expose pour les tests
4144
4672
  sanitizeTrafficData, // NOUVEAU: Expose pour l'auto-tuner/tests
4145
4673
  getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
4674
+ generateStatelessTicket,
4675
+ parseStatelessTicket,
4146
4676
  parseJa3,
4677
+ getBotnetClusterScore, // NOUVEAU: Expose pour les tests
4147
4678
  generateCpuTargetChallengePage,
4148
4679
  getClientHintsInconsistencyScore, // Expose for testing
4149
4680
  generateCombinedPoWChallengePage,
@@ -4154,6 +4685,9 @@ export const __internal = {
4154
4685
  getIpReputationScore, // Expose for testing
4155
4686
  updateIpReputationScore, // Expose for testing
4156
4687
  setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
4688
+ parseTcpSyn, // Expose for testing
4689
+ classifyTcpOs, // Expose for testing
4690
+ getTcpAnomalyScore // Expose for testing
4157
4691
  };
4158
4692
 
4159
4693
  // --- THRESHOLD AUTO-TUNING SECTION ---