@anonympins/fingerprint 0.5.2 → 0.5.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/package.json +1 -1
  3. package/src/js/build-client.js +67 -67
  4. package/src/js/fingerprint.client.js +280 -38
  5. package/src/js/fingerprint.client.obfuscated.js +1 -1
  6. package/src/js/fingerprint.js +474 -163
  7. package/src/js/fingerprint.utils.js +213 -213
  8. package/src/js/library.js +24 -8
  9. package/src/js/mongodb-store.js +165 -79
  10. package/src/js/obfuscation.worker.js +23 -0
  11. package/src/js/pow.solver.inline.js +509 -14
  12. package/src/js/pow.solver.js +350 -32
  13. package/src/js/problem-manager.js +1 -1
  14. package/src/js/redis-store.js +114 -42
  15. package/src/js/tests/cross_parity.test.js +175 -0
  16. package/src/js/tests/dns-circuit-breaker.test.js +55 -0
  17. package/src/js/tests/fingerprint.client.init.test.js +143 -143
  18. package/src/js/tests/fingerprint.client.test.js +128 -128
  19. package/src/js/tests/fingerprint.test.js +57 -2
  20. package/src/js/tests/ip-reputation.test.js +1 -1
  21. package/src/js/tests/obfuscation.test.js +48 -0
  22. package/src/js/tests/pow.solver.test.js +5 -2
  23. package/src/js/tests/usefulWorkSolver.test.js +162 -0
  24. package/src/php/AutoTuner.php +42 -0
  25. package/src/php/Challenge/ChallengeUtils.php +21 -8
  26. package/src/php/FingerprintClient.php +22 -5
  27. package/src/php/FingerprintEngine.php +233 -57
  28. package/src/php/Optimization/Optimization.php +23 -16
  29. package/src/php/Optimization/OptimizationOperators.php +27 -0
  30. package/src/php/ProblemManager.php +1 -1
  31. package/src/php/Store/MongoDbStore.php +184 -104
  32. package/src/php/Store/RedisStore.php +91 -11
  33. package/src/php/Tests/ChallengeUtilsTest.php +285 -285
  34. package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
  35. package/src/php/Tests/IpReputationTest.php +1 -1
  36. package/src/php/Tests/PowTest.php +1 -1
  37. package/src/php/Tests/RequestUtilsTest.php +29 -0
  38. package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
  39. package/src/php/Utils/BigInt.php +202 -202
  40. package/src/php/Utils/RequestUtils.php +74 -21
  41. package/src/php/Utils/TLSClientHelloParser.php +212 -116
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import {BlockList, isIPv4, isIPv6} from "node:net";
3
3
  import * as dns from "node:dns/promises";
4
+ import {Worker} from "node:worker_threads";
4
5
  import {getProblemManager, problemManager} from "./problem-manager.js";
5
6
  import {Optimization} from "./library.js";
6
7
  import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
@@ -27,6 +28,57 @@ import {
27
28
  const __filename = fileURLToPath(import.meta.url);
28
29
  const __dirname = dirname(__filename);
29
30
 
31
+ let dnsCircuitBreaker = {
32
+ state: 'CLOSED', // 'CLOSED', 'OPEN', 'HALF-OPEN'
33
+ failureCount: 0,
34
+ lastStateChange: 0,
35
+ threshold: 5,
36
+ cooldownMs: 30000, // 30 seconds
37
+ };
38
+
39
+ function recordDnsSuccess() {
40
+ dnsCircuitBreaker.failureCount = 0;
41
+ dnsCircuitBreaker.state = 'CLOSED';
42
+ }
43
+
44
+ function recordDnsFailure() {
45
+ dnsCircuitBreaker.failureCount++;
46
+ if (dnsCircuitBreaker.failureCount >= dnsCircuitBreaker.threshold) {
47
+ dnsCircuitBreaker.state = 'OPEN';
48
+ dnsCircuitBreaker.lastStateChange = Date.now();
49
+ }
50
+ }
51
+
52
+ function canAttemptDns() {
53
+ if (dnsCircuitBreaker.state === 'CLOSED') {
54
+ return true;
55
+ }
56
+ if (dnsCircuitBreaker.state === 'OPEN') {
57
+ if (Date.now() - dnsCircuitBreaker.lastStateChange > dnsCircuitBreaker.cooldownMs) {
58
+ dnsCircuitBreaker.state = 'HALF-OPEN';
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ return true; // HALF-OPEN
64
+ }
65
+
66
+ const withTimeout = (promise, ms) => {
67
+ let timeoutId;
68
+ const timeoutPromise = new Promise((_, reject) => {
69
+ timeoutId = setTimeout(() => {
70
+ reject(new Error('DNS_TIMEOUT'));
71
+ }, ms);
72
+ });
73
+ return Promise.race([
74
+ promise.then((res) => {
75
+ clearTimeout(timeoutId);
76
+ return res;
77
+ }),
78
+ timeoutPromise
79
+ ]);
80
+ };
81
+
30
82
  export { createRedisStore } from "./redis-store.js";
31
83
  export { createMongoDbStore } from "./mongodb-store.js";
32
84
 
@@ -72,7 +124,7 @@ const yandexEntries = loadBotWhitelist('yandex.json', [
72
124
  ]);
73
125
 
74
126
  function generateSessionMapping() {
75
- const randomStr = (len = 6) => crypto.randomBytes(len).toString('hex').replace(/[0-9]/g, 'g').substring(0, len);
127
+ const randomStr = (len = 6) => Array.from({ length: len }, () => String.fromCharCode(crypto.randomInt(97, 123))).join('');
76
128
  const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
77
129
 
78
130
  return {
@@ -133,18 +185,29 @@ async function compilePolymorphicJs(mapping) {
133
185
  jsCode = jsCode.replace(regex2, `addRaw("${randKey}"`);
134
186
  }
135
187
 
136
- const obfuscationResult = JavaScriptObfuscator.obfuscate(jsCode, {
137
- compact: true,
138
- controlFlowFlattening: true,
139
- deadCodeInjection: true,
140
- stringArray: true,
141
- stringArrayRotate: true,
142
- stringArrayShuffle: true,
143
- seed: Math.abs(mapping.wasmConstants.seed),
144
- selfDefending: true,
188
+ return new Promise((resolve, reject) => {
189
+ const worker = new Worker(new URL('./obfuscation.worker.js', import.meta.url));
190
+ worker.on('message', (message) => {
191
+ if (message.error) {
192
+ console.error('[Fingerprint] Obfuscation worker error:', message.error, message.stack);
193
+ reject(new Error('Obfuscation failed in worker.'));
194
+ } else {
195
+ resolve(message.obfuscatedCode);
196
+ }
197
+ worker.terminate();
198
+ });
199
+ worker.on('error', (error) => {
200
+ console.error('[Fingerprint] Obfuscation worker crashed:', error);
201
+ reject(error);
202
+ });
203
+ worker.on('exit', (code) => {
204
+ if (code !== 0) {
205
+ console.error(`[Fingerprint] Obfuscation worker stopped with exit code ${code}`);
206
+ reject(new Error(`Obfuscation worker stopped with exit code ${code}`));
207
+ }
208
+ });
209
+ worker.postMessage({ jsCode, seed: mapping.wasmConstants.seed });
145
210
  });
146
-
147
- return obfuscationResult.getObfuscatedCode();
148
211
  }
149
212
 
150
213
  async function ensureLatestMapping() {
@@ -699,6 +762,54 @@ const cipherSuiteMap = {
699
762
  'TLS_RSA_WITH_3DES_EDE_CBC_SHA': 10,
700
763
  };
701
764
 
765
+ /**
766
+ * Valide cryptographiquement l'attestation/assertion WebAuthn émise par l'enclave sécurisée.
767
+ * @private
768
+ * @param {object} anchor Les données d'attestation WebAuthn reçues du client
769
+ * @param {object} deviceData Les données persistantes de l'appareil dans notre store
770
+ * @returns {boolean} True si la signature matérielle est valide
771
+ */
772
+ function verifyWebAuthnHardwareAnchor(anchor, deviceData) {
773
+ if (!anchor || !anchor.type) return false;
774
+
775
+ try {
776
+ const clientDataHash = crypto.createHash('sha256')
777
+ .update(Buffer.from(anchor.clientDataJSON, 'base64'))
778
+ .digest();
779
+
780
+ if (anchor.type === 'registration') {
781
+ if (!anchor.publicKey || !anchor.credentialId) return false;
782
+
783
+ // Enregistrement initial : on stocke la clé publique matérielle SPKI
784
+ deviceData.webauthnPublicKey = anchor.publicKey;
785
+ deviceData.webauthnCredentialId = anchor.credentialId;
786
+ return true;
787
+ } else if (anchor.type === 'assertion') {
788
+ const storedPublicKeyPem = deviceData.webauthnPublicKey;
789
+ if (!storedPublicKeyPem || deviceData.webauthnCredentialId !== anchor.credentialId) {
790
+ return false;
791
+ }
792
+
793
+ // Reconstitution du message signé (authenticatorData + clientDataHash)
794
+ const verifyBuffer = Buffer.concat([
795
+ Buffer.from(anchor.authenticatorData, 'base64'),
796
+ clientDataHash
797
+ ]);
798
+
799
+ const publicKey = crypto.createPublicKey(Buffer.from(storedPublicKeyPem, 'base64'));
800
+ return crypto.verify(
801
+ 'sha256',
802
+ verifyBuffer,
803
+ publicKey,
804
+ Buffer.from(anchor.signature, 'base64')
805
+ );
806
+ }
807
+ } catch (e) {
808
+ console.error('[WebAuthn-Server] Verification failed:', e.message);
809
+ }
810
+ return false;
811
+ }
812
+
702
813
  /**
703
814
  * Extracts TLS fingerprints (JA3 and JA4) from request context.
704
815
  * Prioritizes headers from reverse proxies (x-ja4-hash) and falls back to JA3 calculation
@@ -1455,9 +1566,10 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
1455
1566
  }
1456
1567
 
1457
1568
  function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
1458
- const { nonce, sizeMb, queries, path } = challengeDetails;
1569
+ const { nonce, sizeMb, queries, path, peerId, peerBlockIdx } = challengeDetails;
1459
1570
  const solverCode = getPowSolverCode();
1460
1571
  const safePath = sanitizeRedirectPath(path);
1572
+ const coopTimeout = securityConfig?.pospace?.coopTimeout ?? 15;
1461
1573
 
1462
1574
  const challengeScript = `
1463
1575
  async function solve() {
@@ -1466,16 +1578,49 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1466
1578
  const clientSecret = ${safeJsonStringify(clientSecret)};
1467
1579
  const queries = ${JSON.stringify(queries)};
1468
1580
  const sizeMb = ${sizeMb};
1581
+ const nodeId = nonce;
1582
+ const peerId = ${safeJsonStringify(peerId || '')};
1583
+ const peerBlockIdx = ${peerBlockIdx ?? -1};
1584
+ const coopTimeout = ${coopTimeout};
1469
1585
 
1470
1586
  document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
1471
1587
  await new Promise(r => setTimeout(r, 10));
1472
1588
 
1473
1589
  try {
1474
1590
  await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
1591
+
1592
+ if (peerId && peerBlockIdx !== -1) {
1593
+ // Enregistrement coopératif
1594
+ await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
1595
+ }
1596
+
1475
1597
  document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
1476
- const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
1598
+
1599
+ let peerBlock = "";
1600
+ if (peerId && peerBlockIdx !== -1) {
1601
+ document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
1602
+ const reqId = Math.random().toString(36).substring(2);
1603
+ await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
1604
+
1605
+ let attempts = 0;
1606
+ while (attempts < coopTimeout) {
1607
+ const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
1608
+ const data = await res.json();
1609
+ if (data.status === 'ready') {
1610
+ peerBlock = data.block_data;
1611
+ break;
1612
+ }
1613
+ await new Promise(r => setTimeout(r, 1000));
1614
+ attempts++;
1615
+ }
1616
+ if (!peerBlock) {
1617
+ document.getElementById('loader').innerText = '⚠️ Peer de sous-réseau injoignable. Validation solo...';
1618
+ }
1619
+ }
1620
+
1621
+ const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
1477
1622
 
1478
- window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
1623
+ window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
1479
1624
  } catch(e) {
1480
1625
  document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
1481
1626
  }
@@ -1879,10 +2024,29 @@ function getBehaviorScore(context) {
1879
2024
  * @param {object} metrics - Les métriques comportementales parsées depuis le client.
1880
2025
  * @returns {{timeInconsistencyScore: number}}
1881
2026
  */
1882
- function getTimeInconsistencyScore(context, metrics) {
2027
+ function getTimeInconsistencyScore(context, metrics, deviceData = null) {
1883
2028
  const REPLAY_THRESHOLD_MS = 5000; // 5 secondes
1884
2029
  let score = 0;
1885
2030
 
2031
+ if (deviceData && deviceData.sessionHmacKey && metrics.signature) {
2032
+ try {
2033
+ const copy = JSON.parse(JSON.stringify(metrics));
2034
+ const clientSig = copy.signature;
2035
+ delete copy.signature;
2036
+ const dataToSign = JSON.stringify(copy);
2037
+ const expectedSig = crypto.createHmac('sha256', Buffer.from(deviceData.sessionHmacKey, 'hex'))
2038
+ .update(dataToSign)
2039
+ .digest('hex');
2040
+ if (!crypto.timingSafeEqual(Buffer.from(clientSig, 'hex'), Buffer.from(expectedSig, 'hex'))) {
2041
+ return { timeInconsistencyScore: 100 }; // Replay/tampering detected
2042
+ }
2043
+ } catch (e) {
2044
+ return { timeInconsistencyScore: 100 };
2045
+ }
2046
+ } else if (deviceData && deviceData.sessionHmacKey && !metrics.signature) {
2047
+ return { timeInconsistencyScore: 100 }; // Missing mandatory signature
2048
+ }
2049
+
1886
2050
  if (metrics.clientTimestamp && context.requestTimestamp) {
1887
2051
  const timeDelta = context.requestTimestamp - metrics.clientTimestamp;
1888
2052
 
@@ -2351,80 +2515,6 @@ function getClickVarianceScore(context) {
2351
2515
  * @returns {string|null} The subnet CIDR or null if the IP is invalid.
2352
2516
  */
2353
2517
  function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-line no-unused-vars
2354
- /**
2355
- * @private
2356
- * Parses an IPv6 string, expanding '::' into a 16-byte Buffer.
2357
- * @param {string} ipStr The IPv6 address string.
2358
- * @returns {Buffer|null}
2359
- */
2360
- const parseIPv6 = (ipStr) => {
2361
- const parts = ipStr.split('::');
2362
- if (parts.length > 2) return null;
2363
-
2364
- let hextets = [];
2365
- if (parts[0]) hextets.push(...parts[0].split(':'));
2366
-
2367
- if (parts.length === 2) {
2368
- const hextetsInPart2 = parts[1] ? parts[1].split(':').length : 0;
2369
- const zerosToInsert = 8 - hextets.length - hextetsInPart2;
2370
- for (let i = 0; i < zerosToInsert; i++) {
2371
- hextets.push('0');
2372
- }
2373
- if (parts[1]) hextets.push(...parts[1].split(':'));
2374
- }
2375
-
2376
- if (hextets.length !== 8) return null;
2377
-
2378
- const buffer = Buffer.alloc(16);
2379
- for (let i = 0; i < 8; i++) {
2380
- const val = parseInt(hextets[i] || '0', 16);
2381
- if (isNaN(val)) return null;
2382
- buffer.writeUInt16BE(val, i * 2);
2383
- }
2384
- return buffer;
2385
- };
2386
-
2387
- /**
2388
- * @private
2389
- * Formats a 16-byte IPv6 buffer into a compressed string representation.
2390
- * @param {Buffer} buffer The 16-byte buffer.
2391
- * @returns {string}
2392
- */
2393
- const formatIPv6 = (buffer) => {
2394
- const hextets = [];
2395
- for (let i = 0; i < 16; i += 2) {
2396
- hextets.push(buffer.readUInt16BE(i).toString(16));
2397
- }
2398
-
2399
- let bestStart = -1, bestLength = 0, currentStart = -1, currentLength = 0;
2400
- for (let i = 0; i < hextets.length; i++) {
2401
- if (hextets[i] === '0') {
2402
- if (currentStart === -1) currentStart = i;
2403
- currentLength++;
2404
- } else {
2405
- if (currentLength > bestLength) {
2406
- bestStart = currentStart;
2407
- bestLength = currentLength;
2408
- }
2409
- currentStart = -1;
2410
- currentLength = 0;
2411
- }
2412
- }
2413
- if (currentLength > bestLength) {
2414
- bestStart = currentStart;
2415
- bestLength = currentLength;
2416
- }
2417
-
2418
- // For subnet calculations, an uncompressed view is often clearer.
2419
- // We will avoid compression to match test expectations.
2420
- // if (bestLength > 1) {
2421
- // const part1 = hextets.slice(0, bestStart).join(':');
2422
- // const part2 = hextets.slice(bestStart + bestLength).join(':');
2423
- // return `${part1}::${part2}`;
2424
- // }
2425
- return hextets.join(':');
2426
- };
2427
-
2428
2518
  try {
2429
2519
  if (isIPv4(ip)) {
2430
2520
  const ipBuffer = Buffer.from(ip.split('.').map(Number));
@@ -2433,14 +2523,37 @@ function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-l
2433
2523
  for (let i = 0; i < 4; i++) ipBuffer[i] &= mask[i];
2434
2524
  return `${Array.from(ipBuffer).join('.')}/${ipv4Prefix}`;
2435
2525
  } else if (isIPv6(ip)) {
2436
- const ipBuffer = parseIPv6(ip);
2437
- if (!ipBuffer) return null;
2438
-
2439
- const mask = Buffer.alloc(16, 0);
2440
- for (let i = 0; i < ipv6Prefix; i++) mask[Math.floor(i / 8)] |= 1 << (7 - (i % 8));
2441
- for (let i = 0; i < 16; i++) ipBuffer[i] &= mask[i];
2442
-
2443
- return `${formatIPv6(ipBuffer)}/${ipv6Prefix}`;
2526
+ let normalized = ip.trim().toLowerCase();
2527
+ if (normalized.includes("::")) {
2528
+ const parts = normalized.split("::");
2529
+ if (parts.length > 2) return null;
2530
+ const left = parts[0] ? parts[0].split(":") : [];
2531
+ const right = parts[1] ? parts[1].split(":") : [];
2532
+ const missing = 8 - (left.length + right.length);
2533
+ const middle = Array(missing).fill("0000");
2534
+ normalized = [...left, ...middle, ...right].join(":");
2535
+ } else {
2536
+ const parts = normalized.split(":");
2537
+ if (parts.length !== 8) return null;
2538
+ }
2539
+ const groups = normalized.split(":").map(g => {
2540
+ const val = parseInt(g, 16);
2541
+ return isNaN(val) ? "0000" : val.toString(16).padStart(4, "0");
2542
+ });
2543
+ for (let i = 0; i < 8; i++) {
2544
+ const startBit = i * 16;
2545
+ if (ipv6Prefix >= (i + 1) * 16) {
2546
+ continue;
2547
+ } else if (ipv6Prefix <= startBit) {
2548
+ groups[i] = "0000";
2549
+ } else {
2550
+ const bitsToKeep = ipv6Prefix - startBit;
2551
+ const val = parseInt(groups[i], 16);
2552
+ const mask = (0xffff << (16 - bitsToKeep)) & 0xffff;
2553
+ groups[i] = (val & mask).toString(16).padStart(4, "0");
2554
+ }
2555
+ }
2556
+ return `${groups.join(":")}/${ipv6Prefix}`;
2444
2557
  }
2445
2558
  } catch (e) {
2446
2559
  // Catch any unexpected errors during parsing or manipulation
@@ -2853,12 +2966,19 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
2853
2966
  // Case 2: New user or lost/invalid cookie.
2854
2967
  deviceId = crypto.randomUUID(); // Generate a new "passport".
2855
2968
 
2969
+ const isHttps = context.headers?.['x-forwarded-proto'] === 'https' ||
2970
+ context.rawReq?.secure ||
2971
+ context.rawReq?.protocol === 'https' ||
2972
+ context.rawReq?.connection?.encrypted;
2973
+ const secureOption = isHttps || process.env.NODE_ENV === "production";
2974
+
2856
2975
  // Return the intention to set a cookie.
2857
2976
  newCookie = {
2858
2977
  name: "device_id",
2859
2978
  value: deviceId,
2860
2979
  options: {
2861
- httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict",
2980
+ httpOnly: true, secure: secureOption, sameSite: "strict",
2981
+ ...(secureOption && { partitioned: true }),
2862
2982
  // Le maxAge est maintenant configurable. Par défaut, c'est un cookie de session.
2863
2983
  ...(securityConfig.deviceIdCookieMaxAge && { maxAge: securityConfig.deviceIdCookieMaxAge }),
2864
2984
  }
@@ -2931,6 +3051,23 @@ async function getBehavioralIndicators(context, deviceData) {
2931
3051
  deviceData.lastFpHash = currentFpHash;
2932
3052
  deviceData.ips.add(clientIp); // Record the IP used by this device
2933
3053
 
3054
+ // --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
3055
+ const behaviorHeader = context.headers?.['x-behavior-metrics'];
3056
+ let webauthnVerified = false;
3057
+ if (behaviorHeader) {
3058
+ try {
3059
+ const metrics = JSON.parse(behaviorHeader);
3060
+ if (metrics && metrics.webauthnAnchor) {
3061
+ if (verifyWebAuthnHardwareAnchor(metrics.webauthnAnchor, deviceData)) {
3062
+ webauthnVerified = true;
3063
+ deviceData.webauthnVerified = true;
3064
+ }
3065
+ }
3066
+ } catch (e) {
3067
+ // Ignorer les erreurs de parsing
3068
+ }
3069
+ }
3070
+
2934
3071
  // NOUVELLE LOGIQUE : Le score d'historique est basé sur le nombre d'IPs utilisées par l'appareil.
2935
3072
  // Très efficace contre la rotation de proxy.
2936
3073
  const maxIpsForDevice = isSharedIp
@@ -2974,7 +3111,6 @@ export const getSuspicionVector = async (context, securityConfig) => {
2974
3111
  context._newCookies = context._newCookies || [];
2975
3112
  context._newCookies.push(newCookie);
2976
3113
  }
2977
- await store.set(`ip-device:${clientIp}`, deviceId, 600); // Link the IP to the device for 10 minutes
2978
3114
 
2979
3115
  // Periodically clean up device data
2980
3116
  if (Date.now() - deviceData.lastUpdate > 10 * 60 * 1000) { // 10 minutes
@@ -2983,48 +3119,56 @@ export const getSuspicionVector = async (context, securityConfig) => {
2983
3119
  }
2984
3120
  deviceData.lastUpdate = Date.now();
2985
3121
 
2986
- const behavioral = await getBehavioralIndicators(context, deviceData);
2987
- const { headerAnomalyScore } = getHeaderAnomalies(context);
2988
- // Calculate the inconsistency score here, separately.
2989
- let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
2990
-
2991
- // NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
2992
- if (consistencyScore < 0.7) { // Seuil de rupture
2993
- inconsistencyScore = 100;
2994
- }
2995
-
2996
- const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
2997
-
2998
- // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
2999
- const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
3122
+ const stableFp = extractStablePart(currentDeviceHash);
3123
+ const stableFpHash = cyrb53(stableFp).toString();
3000
3124
 
3001
- const { tlsSpoofingScore } = await getTlsSpoofingScore(context);
3125
+ // Execute non-interdependent asynchronous operations in parallel
3126
+ const [
3127
+ behavioral,
3128
+ { tlsSpoofingScore },
3129
+ { subnetScore },
3130
+ ipReputationScore,
3131
+ { botnetClusterScore },
3132
+ _ // store.set result
3133
+ ] = await Promise.all([
3134
+ getBehavioralIndicators(context, deviceData),
3135
+ getTlsSpoofingScore(context),
3136
+ getSubnetScore(context, deviceId),
3137
+ getIpReputationScore(clientIp),
3138
+ getBotnetClusterScore(context, stableFpHash),
3139
+ store.set(`ip-device:${clientIp}`, deviceId, 600) // Link the IP to the device for 10 minutes
3140
+ ]);
3141
+
3142
+ // Synchronous calculations
3143
+ const { headerAnomalyScore } = getHeaderAnomalies(context);
3144
+ let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
3145
+
3146
+ // NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
3147
+ if (consistencyScore < 0.7) { // Seuil de rupture
3148
+ inconsistencyScore = 100;
3149
+ }
3002
3150
 
3003
- const { botScore } = getBotScore(context);
3151
+ const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
3004
3152
 
3005
- // NOUVEAU: On calcule le score d'incohérence temporelle.
3006
- const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'));
3153
+ // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
3154
+ const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
3007
3155
 
3008
- // NOUVEAU: On calcule le score d'incohérence entre les couches.
3009
- const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
3156
+ const { botScore } = getBotScore(context);
3010
3157
 
3011
- // NOUVEAU: On calcule le score de variance des clics.
3012
- const { clickVarianceScore } = getClickVarianceScore(context);
3158
+ // NOUVEAU: On calcule le score d'incohérence temporelle.
3159
+ const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'), deviceData);
3013
3160
 
3014
- // NOUVEAU: On calcule le score d'incohérence des Client-Hints.
3015
- const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
3161
+ // NOUVEAU: On calcule le score d'incohérence entre les couches.
3162
+ const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
3016
3163
 
3017
- // NOUVEAU: On calcule le score de réputation du sous-réseau.
3018
- // FIX: Pass the deviceId to getSubnetScore
3019
- const { subnetScore } = await getSubnetScore(context, deviceId);
3164
+ // NOUVEAU: On calcule le score de variance des clics.
3165
+ const { clickVarianceScore } = getClickVarianceScore(context);
3020
3166
 
3021
- const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
3167
+ // NOUVEAU: On calcule le score d'incohérence des Client-Hints.
3168
+ const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
3022
3169
 
3023
- const ipReputationScore = await getIpReputationScore(clientIp);
3170
+ const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
3024
3171
 
3025
- const stableFp = extractStablePart(currentDeviceHash);
3026
- const stableFpHash = cyrb53(stableFp).toString();
3027
- const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
3028
3172
  const { tcpAnomalyScore } = getTcpAnomalyScore(context);
3029
3173
  const { quicAnomalyScore } = getQuicAnomalyScore(context);
3030
3174
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
@@ -3163,7 +3307,40 @@ export function generateCpuTargetChallenge(
3163
3307
  };
3164
3308
  }
3165
3309
 
3166
- const htmlTemplateCache = new Map();
3310
+ class SimpleLRUCache {
3311
+ constructor(maxSize = 100) {
3312
+ this.maxSize = maxSize;
3313
+ this.cache = new Map();
3314
+ }
3315
+
3316
+ get(key) {
3317
+ if (!this.cache.has(key)) return undefined;
3318
+ const value = this.cache.get(key);
3319
+ this.cache.delete(key);
3320
+ this.cache.set(key, value);
3321
+ return value;
3322
+ }
3323
+
3324
+ set(key, value) {
3325
+ if (this.cache.has(key)) {
3326
+ this.cache.delete(key);
3327
+ } else if (this.cache.size >= this.maxSize) {
3328
+ const lruKey = this.cache.keys().next().value;
3329
+ this.cache.delete(lruKey);
3330
+ }
3331
+ this.cache.set(key, value);
3332
+ }
3333
+
3334
+ has(key) {
3335
+ return this.cache.has(key);
3336
+ }
3337
+
3338
+ clear() {
3339
+ this.cache.clear();
3340
+ }
3341
+ }
3342
+
3343
+ const htmlTemplateCache = new SimpleLRUCache(100);
3167
3344
 
3168
3345
  /**
3169
3346
  * Generates the HTML page for the CPU target challenge.
@@ -3250,7 +3427,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
3250
3427
  }
3251
3428
 
3252
3429
  // Redirect with both solutions and the fingerprint used to solve.
3253
- const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
3430
+ const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + encodeURIComponent(JSON.stringify(memSolution));
3254
3431
  window.location.href = finalUrl;
3255
3432
  }
3256
3433
 
@@ -3705,6 +3882,10 @@ export class FingerprintEngine {
3705
3882
  return false;
3706
3883
  }
3707
3884
 
3885
+ if (!canAttemptDns()) {
3886
+ return false;
3887
+ }
3888
+
3708
3889
  const cacheKey = `ip-whitelist:${clientIp}`;
3709
3890
  const cachedStatus = await store.get(cacheKey);
3710
3891
 
@@ -3717,32 +3898,35 @@ export class FingerprintEngine {
3717
3898
 
3718
3899
  try {
3719
3900
  // 1. Reverse DNS lookup
3720
- const hostnames = await dns.reverse(clientIp);
3901
+ const hostnames = await withTimeout(dns.reverse(clientIp), 500);
3721
3902
  const validHostname = hostnames.find(h => h.endsWith(matchedRule.hostnameSuffix));
3722
3903
 
3723
3904
  if (!validHostname) {
3724
- await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h (TTL in seconds)
3905
+ await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
3725
3906
  return false;
3726
3907
  }
3727
3908
 
3728
3909
  // 2. Forward DNS lookup
3729
3910
  let addresses = [];
3730
3911
  try {
3731
- addresses = await dns.resolve(validHostname);
3912
+ addresses = await withTimeout(dns.resolve(validHostname), 500);
3732
3913
  } catch (e) {}
3733
3914
  try {
3734
- const ipv6 = await dns.resolve(validHostname, 'AAAA');
3915
+ const ipv6 = await withTimeout(dns.resolve(validHostname, 'AAAA'), 500);
3735
3916
  addresses = addresses.concat(ipv6);
3736
3917
  } catch (e) {}
3737
3918
  if (addresses.includes(clientIp)) {
3919
+ recordDnsSuccess();
3738
3920
  await store.set(cacheKey, 'verified', 86400); // Cache success for 24h (TTL in seconds)
3739
3921
  return true;
3740
3922
  }
3741
3923
  } catch (error) {
3742
- // DNS errors are common (e.g., for IPs with no rDNS record), treat as failure.
3924
+ recordDnsFailure();
3925
+ await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
3926
+ return false;
3743
3927
  }
3744
3928
 
3745
- await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h (TTL in seconds)
3929
+ await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
3746
3930
  return false;
3747
3931
  }
3748
3932
 
@@ -3764,13 +3948,19 @@ export class FingerprintEngine {
3764
3948
 
3765
3949
  this._log('Processing request', { clientIp, path, isStatic });
3766
3950
 
3951
+ // Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
3952
+ const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
3953
+ if (deviceData && deviceData.webauthnVerified) {
3954
+ this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
3955
+ return { action: 'next', score: 0, vector: { webauthn_verified: 100 } };
3956
+ }
3957
+
3767
3958
  if (isStatic) {
3768
3959
  this._log('Static resource - skipping checks');
3769
3960
  return { action: 'next', score: 0, vector: {} };
3770
3961
  }
3771
3962
 
3772
3963
  // Resolve identity and check for persisted "condemned" status early.
3773
- const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
3774
3964
  const currentDeviceHash = getCompositeDeviceHash(requestContext);
3775
3965
  const isNewDevice = !!newCookie;
3776
3966
  const allowRoaming = this.securityConfig?.allowCrossNetworkRoaming ?? false;
@@ -4040,6 +4230,11 @@ export class FingerprintEngine {
4040
4230
  const finalQueryString = finalSearchParams.toString();
4041
4231
  const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
4042
4232
  this._log('Redirecting to clean path', { finalRedirectPath, cookieMaxAge: finalTtl });
4233
+ const isHttps = requestContext.headers?.['x-forwarded-proto'] === 'https' ||
4234
+ requestContext.rawReq?.secure ||
4235
+ requestContext.rawReq?.protocol === 'https' ||
4236
+ requestContext.rawReq?.connection?.encrypted;
4237
+ const secureOption = isHttps || this.isProduction;
4043
4238
  return {
4044
4239
  action: 'redirect',
4045
4240
  path: finalRedirectPath,
@@ -4048,7 +4243,13 @@ export class FingerprintEngine {
4048
4243
  cookie: {
4049
4244
  name: 'pow_clearance',
4050
4245
  value: ticket, // The ticket itself
4051
- options: { httpOnly: true, secure: this.isProduction, maxAge: finalTtl } // Options for setting the cookie
4246
+ options: {
4247
+ httpOnly: true,
4248
+ secure: secureOption,
4249
+ sameSite: 'strict',
4250
+ ...(secureOption && { partitioned: true }),
4251
+ maxAge: finalTtl
4252
+ } // Options for setting the cookie
4052
4253
  }
4053
4254
  };
4054
4255
  } else {
@@ -4123,7 +4324,7 @@ export class FingerprintEngine {
4123
4324
  if (challengeContext) {
4124
4325
  try {
4125
4326
  const workResult = JSON.parse(pow_solution_work_result);
4126
- const defaultPath = resolve(__dirname, '..', '..', 'problems.config.json');
4327
+ const defaultPath = resolve(__dirname, '..', '..', 'config', 'problems.config.json');
4127
4328
  const configPath = this.securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
4128
4329
  const manager = await getProblemManager({
4129
4330
  configPath,
@@ -4178,17 +4379,22 @@ export class FingerprintEngine {
4178
4379
 
4179
4380
  const isBlocked = finalScore >= blockThreshold;
4180
4381
 
4181
- const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
4382
+ const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked && finalScore > 0;
4182
4383
  const isSuspiciousMedium = finalScore >= thresholds.medium;
4183
4384
  const isSuspicious = finalScore >= thresholds.low;
4184
4385
  const isVerySuspicious = finalScore >= thresholds.medium; // Seuil pour le challenge d'optimisation
4185
4386
 
4186
4387
  // Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
4187
- const suspicionFactor = isSuspicious
4188
- ? Math.min(
4189
- 1.5, // On autorise un dépassement pour rendre les challenges très difficiles si le score est très élevé
4190
- (finalScore - thresholds.low) / (thresholds.high - thresholds.low),
4191
- )
4388
+ const suspicionFactor = isSuspicious // eslint-disable-line no-nested-ternary
4389
+ ? (() => {
4390
+ const denominator = thresholds.high - thresholds.low;
4391
+ if (denominator === 0) {
4392
+ // If the range is zero, and finalScore is at or above low threshold,
4393
+ // return 0 to avoid NaN.
4394
+ return 0;
4395
+ }
4396
+ return Math.min(1.5, (finalScore - thresholds.low) / denominator);
4397
+ })()
4192
4398
  : 0;
4193
4399
 
4194
4400
  this._log('Suspicion levels evaluated', {
@@ -4319,7 +4525,7 @@ export class FingerprintEngine {
4319
4525
  this._log('Issuing a useful work challenge', { finalScore });
4320
4526
 
4321
4527
  try {
4322
- const defaultPath = resolve(process.cwd(), 'problems.config.json');
4528
+ const defaultPath = resolve(process.cwd(), 'config', 'problems.config.json');
4323
4529
  const configPath = this.securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
4324
4530
  const manager = await getProblemManager({
4325
4531
  configPath,
@@ -5278,14 +5484,14 @@ function getTlsSessionId(context) {
5278
5484
  }
5279
5485
 
5280
5486
  // --- Proof-of-Work Middleware (The Tollbooth) ---
5281
- const staticFileCache = new Map();
5487
+ const staticFileCache = new SimpleLRUCache(50);
5282
5488
 
5283
5489
  export const powMiddleware = (securityConfig) => {
5284
5490
  const engine = new FingerprintEngine(securityConfig);
5285
5491
 
5286
5492
  // Initialize the problem manager with the configured path, if provided.
5287
5493
  if (securityConfig.enableUsefulWork) {
5288
- const defaultPath = resolve(__dirname, '..', '..', 'problems.config.json');
5494
+ const defaultPath = resolve(__dirname, '..', '..', 'config', 'problems.config.json');
5289
5495
  const configPath = securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
5290
5496
  getProblemManager({
5291
5497
  configPath,
@@ -5310,6 +5516,36 @@ export const powMiddleware = (securityConfig) => {
5310
5516
  }
5311
5517
 
5312
5518
  return async (req, res, next) => {
5519
+ if (req.query && req.query.fp_handshake) {
5520
+ const clientKeyHex = req.headers['x-client-ephemeral-key'];
5521
+ if (clientKeyHex) {
5522
+ try {
5523
+ const serverECDH = crypto.createECDH('prime256v1');
5524
+ serverECDH.generateKeys();
5525
+ const serverPubKeyHex = serverECDH.getKeys('hex');
5526
+ const sharedSecret = serverECDH.computeSecret(clientKeyHex, 'hex');
5527
+ const hmacKey = crypto.createHash('sha256').update(sharedSecret).digest('hex');
5528
+
5529
+ const requestContext = {
5530
+ clientIp: req.ip || req.socket?.remoteAddress || "unknown",
5531
+ headers: req.headers,
5532
+ cookies: req.cookies
5533
+ };
5534
+ const { deviceId, deviceData } = await resolveRequestIdentity(requestContext, securityConfig);
5535
+ if (deviceData) {
5536
+ deviceData.sessionHmacKey = hmacKey;
5537
+ await store.set(`device:${deviceId}`, deviceData);
5538
+ }
5539
+
5540
+ res.setHeader('x-server-ephemeral-key', serverPubKeyHex);
5541
+ return res.status(200).json({ status: 'success' });
5542
+ } catch (e) {
5543
+ return res.status(400).json({ error: 'Handshake failed' });
5544
+ }
5545
+ }
5546
+ return res.status(400).json({ error: 'Missing client key' });
5547
+ }
5548
+
5313
5549
  if (!req.headers_translated) {
5314
5550
  req.headers_translated = true;
5315
5551
  const matchedMapping = getActiveMappingForRequest(req.headers);
@@ -5484,6 +5720,7 @@ export const __internal = {
5484
5720
  getTlsFingerprint, // NOUVEAU: Expose pour les tests
5485
5721
  sanitizeTrafficData, // NOUVEAU: Expose pour l'auto-tuner/tests
5486
5722
  getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
5723
+ verifyWebAuthnHardwareAnchor,
5487
5724
  generateStatelessTicket,
5488
5725
  parseStatelessTicket,
5489
5726
  parseJa3,
@@ -5500,11 +5737,17 @@ export const __internal = {
5500
5737
  setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
5501
5738
  verifyZkpProof,
5502
5739
  modPow,
5740
+ generateSessionMapping,
5741
+ compilePolymorphicJs,
5503
5742
  parseTcpSyn, // Expose for testing
5504
5743
  classifyTcpOs, // Expose for testing
5505
5744
  getTcpAnomalyScore, // Expose for testing,
5506
5745
  getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
5507
5746
  getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
5747
+ dnsCircuitBreaker,
5748
+ recordDnsSuccess,
5749
+ recordDnsFailure,
5750
+ canAttemptDns,
5508
5751
  registerCooperativeNode,
5509
5752
  findPeerInSubnet,
5510
5753
  handleCooperativeRequest
@@ -5526,48 +5769,79 @@ export function sanitizeTrafficData(trafficData) {
5526
5769
  if (!trafficData || trafficData.length === 0) {
5527
5770
  return [];
5528
5771
  }
5529
- const tempSanitized = [];
5772
+ const suspiciousLogs = [];
5773
+ const passedLogs = [];
5530
5774
  const deviceCounts = new Map();
5531
5775
  const ipCounts = new Map();
5532
5776
  const subnetCounts = new Map();
5777
+ const hardwareClusterCounts = new Map();
5778
+ const hwClusterCache = new Map();
5533
5779
 
5534
5780
  // Calcul des quotas maximums pour éviter l'influence démesurée d'une entité
5535
5781
  const maxLogsPerDevice = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% contribution per device
5536
5782
  const maxLogsPerIp = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par adresse IP individuelle
5537
5783
  const maxLogsPerSubnet = Math.max(5, Math.floor(trafficData.length * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
5784
+ const maxLogsPerHardwareCluster = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par cluster matériel stable
5785
+
5786
+ const getHardwareCluster = (log) => {
5787
+ const fp = log.deviceHash || log.fingerprint || log.deviceFingerprint || '';
5788
+ if (!fp || typeof fp !== 'string') {
5789
+ return log.deviceId || 'anonymous-cluster';
5790
+ }
5791
+ if (hwClusterCache.has(fp)) {
5792
+ return hwClusterCache.get(fp);
5793
+ }
5794
+ const parts = fp.split('|');
5795
+ const hwComponents = [];
5796
+ for (const part of parts) {
5797
+ const pair = part.split(':');
5798
+ if (pair.length === 2 && (pair[0] === 'gpu' || pair[0] === 'cvs' || pair[0] === 'hw')) {
5799
+ hwComponents.push(part);
5800
+ }
5801
+ }
5802
+ const result = hwComponents.length > 0 ? hwComponents.sort().join('|') : (log.deviceId || 'anonymous-cluster');
5803
+ hwClusterCache.set(fp, result);
5804
+ return result;
5805
+ };
5538
5806
 
5539
5807
  for (const log of trafficData) {
5540
5808
  const devId = log.deviceId || 'anonymous';
5541
5809
  const ip = log.clientIp || log.ip || 'unknown';
5542
5810
  const subnet = getIpSubnet(ip) || 'unknown-subnet';
5811
+ const hwCluster = getHardwareCluster(log);
5543
5812
 
5544
5813
  const currentDeviceCount = deviceCounts.get(devId) || 0;
5545
5814
  const currentIpCount = ipCounts.get(ip) || 0;
5546
5815
  const currentSubnetCount = subnetCounts.get(subnet) || 0;
5816
+ const currentHwClusterCount = hardwareClusterCounts.get(hwCluster) || 0;
5547
5817
 
5548
- // Filtrage anti-poisoning strict sur 3 axes cumulatifs
5818
+ // Filtrage anti-poisoning strict sur 4 axes cumulatifs (incluant le clustering matériel stable)
5549
5819
  if (
5550
5820
  currentDeviceCount < maxLogsPerDevice &&
5551
5821
  (ip === 'unknown' || currentIpCount < maxLogsPerIp) &&
5552
- (subnet === 'unknown-subnet' || currentSubnetCount < maxLogsPerSubnet)
5822
+ (subnet === 'unknown-subnet' || currentSubnetCount < maxLogsPerSubnet) &&
5823
+ currentHwClusterCount < maxLogsPerHardwareCluster
5553
5824
  ) {
5554
5825
  deviceCounts.set(devId, currentDeviceCount + 1);
5555
5826
  if (ip !== 'unknown') ipCounts.set(ip, currentIpCount + 1);
5556
5827
  if (subnet !== 'unknown-subnet') subnetCounts.set(subnet, currentSubnetCount + 1);
5828
+ hardwareClusterCounts.set(hwCluster, currentHwClusterCount + 1);
5557
5829
 
5558
- tempSanitized.push(log);
5830
+ if (log.type === 'request_passed') {
5831
+ passedLogs.push(log);
5832
+ } else {
5833
+ suspiciousLogs.push(log);
5834
+ }
5559
5835
  }
5560
5836
  }
5561
5837
 
5562
- const passedLogs = tempSanitized.filter(log => log.type === 'request_passed');
5563
- const suspiciousLogs = tempSanitized.filter(log => log.type !== 'request_passed');
5564
-
5565
5838
  const minDataPoints = 200; // Seuil par défaut
5566
5839
  const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
5567
- const shuffledPassed = passedLogs.sort(() => 0.5 - Math.random());
5568
- const selectedPassed = shuffledPassed.slice(0, maxPassedAllowed);
5569
-
5570
- return [...suspiciousLogs, ...selectedPassed];
5840
+ if (passedLogs.length > maxPassedAllowed) {
5841
+ const shuffledPassed = passedLogs.sort(() => 0.5 - Math.random());
5842
+ return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
5843
+ }
5844
+ return [...suspiciousLogs, ...passedLogs];
5571
5845
  }
5572
5846
  /**
5573
5847
  * Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
@@ -5745,6 +6019,43 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
5745
6019
  // Plus le ratio est équilibré et le volume important, plus nous faisons confiance au Front de Pareto.
5746
6020
  const trafficConfidence = Math.min(1.5, Math.max(0.3, highConfidenceRatio * 4));
5747
6021
 
6022
+ // --- VALIDATION POST-CALCUL (Anti-empoisonnement & Validation Croisée) ---
6023
+ const tempConfig = {
6024
+ thresholds: { ...securityConfig.thresholds },
6025
+ weights: { ...securityConfig.weights },
6026
+ patterns: { ...securityConfig.patterns }
6027
+ };
6028
+
6029
+ applyInertialUpdate(tempConfig.thresholds, newConfig.thresholds, 'thresholds', trafficConfidence);
6030
+ applyInertialUpdate(tempConfig.weights, newConfig.weights, 'weights', trafficConfidence);
6031
+ applyInertialUpdate(tempConfig.patterns, newConfig.patterns, 'patterns', trafficConfidence);
6032
+
6033
+ const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator({ trafficData: sanitizedData });
6034
+ const currentObjectives = fitnessFunction(securityConfig);
6035
+ const proposedObjectives = fitnessFunction(tempConfig);
6036
+
6037
+ const currentFPR = currentObjectives[0];
6038
+ const currentFNR = currentObjectives[1];
6039
+ const proposedFPR = proposedObjectives[0];
6040
+ const proposedFNR = proposedObjectives[1];
6041
+
6042
+ const validationTolerance = securityConfig?.autotuning?.validationTolerance ?? tuningOptions?.validationTolerance ?? 0.15;
6043
+
6044
+ if (proposedFPR > currentFPR + validationTolerance || proposedFNR > currentFNR + validationTolerance) {
6045
+ console.error(`[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: ${proposedFPR.toFixed(4)} (Current: ${currentFPR.toFixed(4)}), Proposed FNR: ${proposedFNR.toFixed(4)} (Current: ${currentFNR.toFixed(4)})`);
6046
+ if (securityConfig.logger && typeof securityConfig.logger === 'function') {
6047
+ securityConfig.logger({
6048
+ type: 'autotuning_instability_alert',
6049
+ proposedFPR,
6050
+ currentFPR,
6051
+ proposedFNR,
6052
+ currentFNR,
6053
+ timestamp: Date.now()
6054
+ });
6055
+ }
6056
+ return; // Rollback automatique : On arrête l'application
6057
+ }
6058
+
5748
6059
  applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds, 'thresholds', trafficConfidence);
5749
6060
  applyInertialUpdate(securityConfig.weights, newConfig.weights, 'weights', trafficConfidence);
5750
6061
  applyInertialUpdate(securityConfig.patterns, newConfig.patterns, 'patterns', trafficConfidence);