@anonympins/fingerprint 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +3 -7
- package/package.json +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.client.js +1463 -1447
- package/src/js/fingerprint.js +369 -95
- package/src/js/library.js +15 -8
- package/src/js/pow.solver.inline.js +56 -10
- package/src/js/pow.solver.js +56 -10
- package/src/js/pow.worker.js +35 -13
- package/src/js/tests/fingerprint.test.js +69 -35
- package/src/php/Challenge/ChallengeUtils.php +148 -22
- package/src/php/Config/SecurityProfiles.php +1 -0
- package/src/php/FingerprintClient.php +148 -148
- package/src/php/FingerprintEngine.php +1834 -1880
- package/src/php/Ja3AnomalyDetector.php +257 -227
- package/src/php/Optimization/OptimizationOperators.php +2 -2
- package/src/php/Tests/FingerprintEngineTest.php +2 -33
- package/src/php/Utils/RequestUtils.php +113 -18
- package/src/php/Utils/TLSClientHelloParser.php +158 -1
- package/src/js/fingerprint.client.obfuscated.js +0 -1
package/src/js/fingerprint.js
CHANGED
|
@@ -6,7 +6,7 @@ import {getProblemManager, problemManager} from "./problem-manager.js";
|
|
|
6
6
|
import {Optimization} from "./library.js";
|
|
7
7
|
import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
|
|
8
8
|
import {DynamicWasmGenerator} from "./dynamic-wasm.js";
|
|
9
|
-
import {writeFileSync, readFileSync, existsSync} from "node:fs";
|
|
9
|
+
import {writeFileSync, readFileSync, existsSync, promises as fsPromises} from "node:fs";
|
|
10
10
|
import {fileURLToPath} from "node:url";
|
|
11
11
|
import {dirname, join, resolve} from "node:path";
|
|
12
12
|
import {GpuPowSolver} from "./gpu_pow.solver.js";
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
30
|
const __dirname = dirname(__filename);
|
|
31
31
|
|
|
32
|
+
const secureRandomFloat = () => crypto.randomInt(0, 4294967296) / 4294967296;
|
|
33
|
+
|
|
32
34
|
let dnsCircuitBreaker = {
|
|
33
35
|
state: 'CLOSED', // 'CLOSED', 'OPEN', 'HALF-OPEN'
|
|
34
36
|
failureCount: 0,
|
|
@@ -277,9 +279,7 @@ async function ensureLatestMapping() {
|
|
|
277
279
|
mapping.timestamp = now;
|
|
278
280
|
|
|
279
281
|
activeMappings.unshift(mapping);
|
|
280
|
-
|
|
281
|
-
activeMappings.pop();
|
|
282
|
-
}
|
|
282
|
+
activeMappings = activeMappings.slice(0, 5);
|
|
283
283
|
lastMappingTime = now;
|
|
284
284
|
|
|
285
285
|
try {
|
|
@@ -402,16 +402,70 @@ function getProtocolAnomalyScore(context) {
|
|
|
402
402
|
if (kv.length === 2) params[kv[0]] = kv[1];
|
|
403
403
|
});
|
|
404
404
|
const priorityOrder = parts[2] || '';
|
|
405
|
+
const frameOrderRaw = parts[3] || context.headers?.['x-quic-frame-order'] || context.quicFrameOrder || '';
|
|
406
|
+
const frameOrder = frameOrderRaw.toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
|
|
405
407
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
408
|
+
const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
|
|
409
|
+
const isFirefox = browser.startsWith('Firefox');
|
|
410
|
+
const isSafari = browser.startsWith('Safari');
|
|
411
|
+
|
|
412
|
+
const maxData = parseInt(params['1'] || params['0x01'] || '0', 10);
|
|
413
|
+
const maxStreams = parseInt(params['4'] || params['8'] || params['0x08'] || '0', 10);
|
|
414
|
+
const bidiLocal = parseInt(params['5'] || params['0x05'] || '0', 10);
|
|
415
|
+
const bidiRemote = parseInt(params['6'] || params['0x06'] || '0', 10);
|
|
416
|
+
|
|
417
|
+
if (isChromium) {
|
|
418
|
+
// Contrôle de flux global et nombre de flux bidirectionnels
|
|
409
419
|
if (maxData > 0 && maxData < 1048576) quicAnomaly += 40.0;
|
|
410
420
|
if (maxStreams > 0 && maxStreams !== 100) quicAnomaly += 30.0;
|
|
411
421
|
if (priorityOrder && !priorityOrder.includes('u=')) quicAnomaly += 30.0;
|
|
412
|
-
|
|
422
|
+
|
|
423
|
+
// Contrôle de flux bidi (Chromium alloue 6MB = 6291456 ou au minimum 512 Ko)
|
|
424
|
+
// curl-impersonate / quiche alloue 256 Ko (262144) ou 128 Ko (131072)
|
|
425
|
+
if (bidiLocal > 0 && (bidiLocal < 524288 || bidiLocal === 262144)) quicAnomaly += 40.0;
|
|
426
|
+
if (bidiRemote > 0 && (bidiRemote < 524288 || bidiRemote === 262144)) quicAnomaly += 30.0;
|
|
427
|
+
|
|
428
|
+
// Ordre des trames de contrôle QUIC (SETTINGS, MAX_STREAMS, PRIORITY)
|
|
429
|
+
if (frameOrder.length >= 2) {
|
|
430
|
+
const sIdx = frameOrder.findIndex(f => f === 's' || f === 'settings' || f === '4');
|
|
431
|
+
const mIdx = frameOrder.findIndex(f => f === 'm' || f === 'max_streams' || f === '18');
|
|
432
|
+
const pIdx = frameOrder.findIndex(f => f === 'p' || f === 'priority' || f === 'priority_update' || f === '15');
|
|
433
|
+
|
|
434
|
+
if (sIdx !== 0 && sIdx !== -1) {
|
|
435
|
+
quicAnomaly += 50.0; // SETTINGS doit obligatoirement être la première trame
|
|
436
|
+
}
|
|
437
|
+
if (mIdx !== -1 && sIdx !== -1 && mIdx < sIdx) {
|
|
438
|
+
quicAnomaly += 60.0; // MAX_STREAMS envoyé avant SETTINGS (défaut curl/quiche)
|
|
439
|
+
}
|
|
440
|
+
if (pIdx !== -1 && sIdx !== -1 && pIdx < sIdx) {
|
|
441
|
+
quicAnomaly += 60.0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else if (isFirefox) {
|
|
413
445
|
const maxData = parseInt(params['1'] || '0', 10);
|
|
414
446
|
if (maxData > 0 && maxData > 5000000) quicAnomaly += 40.0;
|
|
447
|
+
// Necko (Firefox) n'utilise pas 100 flux bidi par défaut ni un buffer bidi de 6 Mo
|
|
448
|
+
if (maxStreams === 100) quicAnomaly += 50.0;
|
|
449
|
+
if (bidiLocal === 6291456) quicAnomaly += 50.0;
|
|
450
|
+
|
|
451
|
+
if (frameOrder.length >= 2) {
|
|
452
|
+
const sIdx = frameOrder.findIndex(f => f === 's' || f === 'settings' || f === '4');
|
|
453
|
+
if (sIdx !== 0 && sIdx !== -1) quicAnomaly += 50.0;
|
|
454
|
+
}
|
|
455
|
+
} else if (isSafari) {
|
|
456
|
+
// Safari Network.framework
|
|
457
|
+
if (maxStreams === 100 && maxData === 1572864 && priorityOrder.includes('u=2,i')) {
|
|
458
|
+
quicAnomaly += 60.0; // Usurpation par profil Cronet / curl-impersonate
|
|
459
|
+
}
|
|
460
|
+
if (bidiLocal === 6291456) quicAnomaly += 50.0;
|
|
461
|
+
|
|
462
|
+
if (frameOrder.length >= 2) {
|
|
463
|
+
const sIdx = frameOrder.findIndex(f => f === 's' || f === 'settings' || f === '4');
|
|
464
|
+
const mIdx = frameOrder.findIndex(f => f === 'm' || f === 'max_streams');
|
|
465
|
+
if (mIdx !== -1 && (mIdx === 0 || (sIdx !== -1 && mIdx < sIdx))) {
|
|
466
|
+
quicAnomaly += 50.0;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
415
469
|
}
|
|
416
470
|
}
|
|
417
471
|
}
|
|
@@ -422,7 +476,9 @@ function getProtocolAnomalyScore(context) {
|
|
|
422
476
|
0.0,
|
|
423
477
|
Math.min(100.0, http2Anomaly),
|
|
424
478
|
Math.min(100.0, quicAnomaly)
|
|
425
|
-
)
|
|
479
|
+
),
|
|
480
|
+
http2AnomalyScore: Math.min(100.0, http2Anomaly),
|
|
481
|
+
quicAnomalyScore: Math.min(100.0, quicAnomaly)
|
|
426
482
|
};
|
|
427
483
|
}
|
|
428
484
|
/**
|
|
@@ -612,6 +668,7 @@ const securityProfiles = {
|
|
|
612
668
|
protocolAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
|
|
613
669
|
renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
614
670
|
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
671
|
+
virtualizationScore: 0.8,
|
|
615
672
|
},
|
|
616
673
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
617
674
|
patterns: {
|
|
@@ -840,17 +897,28 @@ const getPowSecret = () => {
|
|
|
840
897
|
return secret || "fallback-dev-secret-32-chars-minimum";
|
|
841
898
|
};
|
|
842
899
|
|
|
843
|
-
let cachedPowSolverCode = null;
|
|
844
900
|
/**
|
|
845
901
|
* Loads the pow.solver.js content for inlining in HTML pages.
|
|
846
902
|
* @returns {string} The solver JavaScript code.
|
|
847
903
|
*/
|
|
904
|
+
let cachedPowSolverCode = "";
|
|
905
|
+
async function preloadPowSolverCode() {
|
|
906
|
+
if (!cachedPowSolverCode) {
|
|
907
|
+
try {
|
|
908
|
+
const solverPath = join(__dirname, 'pow.solver.inline.js');
|
|
909
|
+
cachedPowSolverCode = await fsPromises.readFile(solverPath, 'utf-8');
|
|
910
|
+
} catch (e) {
|
|
911
|
+
console.error('[Fingerprint] Failed to pre-load inline solver:', e.message);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
848
916
|
const getPowSolverCode = () => {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
917
|
+
if (!cachedPowSolverCode) {
|
|
918
|
+
const solverPath = join(__dirname, 'pow.solver.inline.js');
|
|
919
|
+
cachedPowSolverCode = readFileSync(solverPath, 'utf-8');
|
|
920
|
+
}
|
|
921
|
+
return cachedPowSolverCode;
|
|
854
922
|
};
|
|
855
923
|
/**
|
|
856
924
|
* Extracts the "stable" part of a fingerprint string.
|
|
@@ -1807,7 +1875,7 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
|
|
|
1807
1875
|
const queries = [];
|
|
1808
1876
|
const maxBlocks = sizeMb * 1024;
|
|
1809
1877
|
while (queries.length < numQueries) {
|
|
1810
|
-
const idx =
|
|
1878
|
+
const idx = crypto.randomInt(0, maxBlocks);
|
|
1811
1879
|
if (!queries.includes(idx)) {
|
|
1812
1880
|
queries.push(idx);
|
|
1813
1881
|
}
|
|
@@ -1824,7 +1892,7 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
|
|
|
1824
1892
|
const peer = await findPeerInSubnet(clientIp, nonce);
|
|
1825
1893
|
if (peer) {
|
|
1826
1894
|
challenge.peerId = peer.nodeId;
|
|
1827
|
-
challenge.peerBlockIdx =
|
|
1895
|
+
challenge.peerBlockIdx = crypto.randomInt(0, maxBlocks);
|
|
1828
1896
|
|
|
1829
1897
|
await store.set(`coop-assoc:${nonce}`, {
|
|
1830
1898
|
peerNodeId: peer.nodeId,
|
|
@@ -1861,7 +1929,12 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
|
|
|
1861
1929
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1862
1930
|
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
1863
1931
|
}
|
|
1864
|
-
|
|
1932
|
+
|
|
1933
|
+
async function sendWebRtcSignal(targetId, type, data) {
|
|
1934
|
+
const sig = await signCoop("webrtc_signal", nodeId, targetId + ":" + type + ":" + data);
|
|
1935
|
+
await fetch(window.location.pathname + "?coop_op=webrtc_signal&node_id=" + nodeId + "&target_peer_id=" + targetId + "&signal_type=" + type + "&signal_data=" + encodeURIComponent(data) + "&coop_sig=" + sig);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1865
1938
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
1866
1939
|
await new Promise(r => setTimeout(r, 10));
|
|
1867
1940
|
|
|
@@ -1874,47 +1947,110 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
|
|
|
1874
1947
|
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
|
|
1875
1948
|
}
|
|
1876
1949
|
|
|
1950
|
+
// Gestionnaire de connexions WebRTC P2P
|
|
1951
|
+
const peerConnections = {};
|
|
1952
|
+
|
|
1877
1953
|
// Écoute des requêtes entrantes de nos pairs
|
|
1878
1954
|
setInterval(async () => {
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1955
|
+
try {
|
|
1956
|
+
// 1. Récupération des signaux WebRTC P2P entrants
|
|
1957
|
+
const sigWebrtc = await signCoop("poll_signals", nodeId);
|
|
1958
|
+
const resWebrtc = await fetch(window.location.pathname + "?coop_op=poll_signals&node_id=" + nodeId + "&coop_sig=" + sigWebrtc);
|
|
1959
|
+
const dataWebrtc = await resWebrtc.json();
|
|
1960
|
+
if (dataWebrtc.signals && dataWebrtc.signals.length > 0) {
|
|
1961
|
+
for (const sig of dataWebrtc.signals) {
|
|
1962
|
+
const fromId = sig.from_peer_id;
|
|
1963
|
+
if (sig.signal_type === 'offer') {
|
|
1964
|
+
const pc = new RTCPeerConnection({ iceServers: [] });
|
|
1965
|
+
peerConnections[fromId] = pc;
|
|
1966
|
+
pc.onicecandidate = (e) => {
|
|
1967
|
+
if (e.candidate) sendWebRtcSignal(fromId, 'candidate', JSON.stringify(e.candidate));
|
|
1968
|
+
};
|
|
1969
|
+
pc.ondatachannel = (e) => {
|
|
1970
|
+
const dc = e.channel;
|
|
1971
|
+
dc.onmessage = async (evt) => {
|
|
1972
|
+
try {
|
|
1973
|
+
const req = JSON.parse(evt.data);
|
|
1974
|
+
if (req.type === 'get_block') {
|
|
1975
|
+
document.getElementById('loader').innerText = '📤 Transfert direct P2P (WebRTC) du bloc vers le pair...';
|
|
1976
|
+
const blockData = await window.readSpaceBlock(req.block_idx);
|
|
1977
|
+
dc.send(JSON.stringify({ type: 'block_data', block_data: blockData }));
|
|
1978
|
+
}
|
|
1979
|
+
} catch (err) {}
|
|
1980
|
+
};
|
|
1981
|
+
};
|
|
1982
|
+
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
|
|
1983
|
+
const answer = await pc.createAnswer();
|
|
1984
|
+
await pc.setLocalDescription(answer);
|
|
1985
|
+
await sendWebRtcSignal(fromId, 'answer', JSON.stringify(answer));
|
|
1986
|
+
} else if (sig.signal_type === 'answer' && peerConnections[fromId]) {
|
|
1987
|
+
await peerConnections[fromId].setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
|
|
1988
|
+
} else if (sig.signal_type === 'candidate' && peerConnections[fromId]) {
|
|
1989
|
+
await peerConnections[fromId].addIceCandidate(new RTCIceCandidate(JSON.parse(sig.signal_data)));
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
} catch (e) {}
|
|
1994
|
+
}, 800);
|
|
1895
1995
|
|
|
1896
1996
|
let peerBlock = "";
|
|
1897
1997
|
if (peerId && peerBlockIdx !== -1) {
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1998
|
+
document.getElementById('loader').innerText = '📥 Connexion WebRTC P2P directe au pair (' + peerId + ')...';
|
|
1999
|
+
|
|
2000
|
+
// 1. Tentative d'échange direct via WebRTC DataChannel (charge serveur = 0)
|
|
2001
|
+
const webrtcTransferPromise = new Promise(async (resolve) => {
|
|
2002
|
+
if (!window.RTCPeerConnection) return resolve(null);
|
|
2003
|
+
try {
|
|
2004
|
+
const pc = new RTCPeerConnection({ iceServers: [] });
|
|
2005
|
+
peerConnections[peerId] = pc;
|
|
2006
|
+
const dc = pc.createDataChannel("pospace-transfer");
|
|
2007
|
+
pc.onicecandidate = (e) => {
|
|
2008
|
+
if (e.candidate) sendWebRtcSignal(peerId, 'candidate', JSON.stringify(e.candidate));
|
|
2009
|
+
};
|
|
2010
|
+
dc.onopen = () => {
|
|
2011
|
+
dc.send(JSON.stringify({ type: 'get_block', block_idx: peerBlockIdx }));
|
|
2012
|
+
};
|
|
2013
|
+
dc.onmessage = (e) => {
|
|
2014
|
+
try {
|
|
2015
|
+
const msg = JSON.parse(e.data);
|
|
2016
|
+
if (msg.type === 'block_data' && msg.block_data) {
|
|
2017
|
+
resolve(msg.block_data);
|
|
2018
|
+
}
|
|
2019
|
+
} catch (err) {}
|
|
2020
|
+
};
|
|
2021
|
+
const offer = await pc.createOffer();
|
|
2022
|
+
await pc.setLocalDescription(offer);
|
|
2023
|
+
await sendWebRtcSignal(peerId, 'offer', JSON.stringify(offer));
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
resolve(null);
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
const webrtcTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 5000));
|
|
2030
|
+
peerBlock = await Promise.race([webrtcTransferPromise, webrtcTimeoutPromise]);
|
|
2031
|
+
|
|
2032
|
+
if (peerBlock) {
|
|
2033
|
+
document.getElementById('loader').innerText = '⚡ Bloc reçu en direct via WebRTC P2P sans transit serveur !';
|
|
2034
|
+
} else {
|
|
2035
|
+
// 2. Repli transparent vers la boîte aux lettres HTTP en cas de restriction réseau
|
|
2036
|
+
document.getElementById('loader').innerText = '⚠️ WebRTC P2P indisponible. Téléchargement via relais HTTP...';
|
|
2037
|
+
const reqId = Math.random().toString(36).substring(2);
|
|
2038
|
+
const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
|
|
2039
|
+
await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId + "&coop_sig=" + reqSig);
|
|
2040
|
+
|
|
2041
|
+
let attempts = 0;
|
|
2042
|
+
while (attempts < coopTimeout) {
|
|
2043
|
+
const pollSig = await signCoop("poll_response", nodeId, reqId);
|
|
2044
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
|
|
2045
|
+
const data = await res.json();
|
|
2046
|
+
if (data.status === 'ready') {
|
|
2047
|
+
peerBlock = data.block_data;
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
2051
|
+
attempts++;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
1918
2054
|
}
|
|
1919
2055
|
|
|
1920
2056
|
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
@@ -3019,6 +3155,25 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
3019
3155
|
|
|
3020
3156
|
await store.set(key, subnetData, 86400); // 24-hour TTL
|
|
3021
3157
|
}
|
|
3158
|
+
/**
|
|
3159
|
+
* Calcule un score d'incohérence analogique lisse plafonnant à une asymptote de 99.9.
|
|
3160
|
+
* @param {number} consistencyScore Similarité entre 0 et 1 issue du FingerprintBuilder.
|
|
3161
|
+
* @param {number} [inflectionPoint=0.72] Point d'inflexion où la suspicion accélère.
|
|
3162
|
+
* @param {number} [steepness=12] Raideur de la transition sigmoïdale.
|
|
3163
|
+
* @returns {number} Score de 0 à 99.9 sans saut de palier ni certitude absolue à 100.
|
|
3164
|
+
*/
|
|
3165
|
+
export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPoint = 0.72, steepness = 12) {
|
|
3166
|
+
const s = Math.max(0.0, Math.min(1.0, consistencyScore));
|
|
3167
|
+
if (s >= 0.98) return 0.0;
|
|
3168
|
+
|
|
3169
|
+
const asymptote = 99.9;
|
|
3170
|
+
const raw = 1.0 / (1.0 + Math.exp(steepness * (s - inflectionPoint)));
|
|
3171
|
+
const minVal = 1.0 / (1.0 + Math.exp(steepness * (1.0 - inflectionPoint)));
|
|
3172
|
+
const maxVal = 1.0 / (1.0 + Math.exp(steepness * (0.0 - inflectionPoint)));
|
|
3173
|
+
const normalized = ((raw - minVal) / (maxVal - minVal)) * asymptote;
|
|
3174
|
+
|
|
3175
|
+
return Math.min(asymptote, Math.round(normalized * 10.0) / 10.0);
|
|
3176
|
+
}
|
|
3022
3177
|
|
|
3023
3178
|
/**
|
|
3024
3179
|
* Calculates a suspicion score based on the historical activity of the IP subnet.
|
|
@@ -3054,28 +3209,29 @@ async function getSubnetScore(context) {
|
|
|
3054
3209
|
return { subnetScore: 0.0 };
|
|
3055
3210
|
}
|
|
3056
3211
|
|
|
3057
|
-
//
|
|
3058
|
-
|
|
3059
|
-
const
|
|
3212
|
+
// 1. Estimation Bayésienne de densité (évite les sur-réactions sur 1 ou 2 appareils)
|
|
3213
|
+
// Prior: alpha=0.5, beta=2.0 (a priori réseau sain)
|
|
3214
|
+
const bayesianDensity = (highScoreCount + 0.5) / (deviceCount + 2.5);
|
|
3060
3215
|
|
|
3061
|
-
// Ratio
|
|
3062
|
-
|
|
3063
|
-
|
|
3216
|
+
// 2. Ratio IP / Terminal (distingue un proxy distribué d'un gros NAT / CGNAT)
|
|
3217
|
+
// Sur un proxy distribué, chaque terminal utilise une IP différente (ratio >= 1.0)
|
|
3218
|
+
// Sur un CGNAT, des dizaines de terminaux partagent peu d'IPs (ratio << 1.0)
|
|
3219
|
+
const ipDispersion = Math.min(2.0, ipCount / deviceCount);
|
|
3220
|
+
const ipMultiplier = 0.6 + 0.4 * Math.tanh(ipDispersion);
|
|
3064
3221
|
|
|
3065
|
-
//
|
|
3066
|
-
const
|
|
3222
|
+
// 3. Volatilité des User-Agents (rotation de navigateurs sur matériel identique)
|
|
3223
|
+
const uaDispersion = Math.min(3.0, Math.max(1, uaCount) / deviceCount);
|
|
3224
|
+
const uaMultiplier = 0.7 + 0.3 * Math.tanh(uaDispersion - 1.0);
|
|
3067
3225
|
|
|
3068
|
-
//
|
|
3069
|
-
const
|
|
3070
|
-
const distributionMultiplier = 0.5 + (1.0 * ipDeviceRatio); // NAT (faible ratio IP/Device) vs Proxy distribué (fort ratio)
|
|
3226
|
+
// 4. Intensité brute continue de la menace (sans discontinuité)
|
|
3227
|
+
const rawThreatIntensity = highScoreCount * bayesianDensity * ipMultiplier * uaMultiplier;
|
|
3071
3228
|
|
|
3072
|
-
//
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3229
|
+
// 5. Saturation asymptotique continue (Asymptote à 99.9)
|
|
3230
|
+
// Même si rawThreatIntensity tend vers l'infini, tanh converge vers 1.0 sans jamais dépasser 85.0
|
|
3231
|
+
const ASYMPTOTE = 99.9;
|
|
3232
|
+
const scaleFactor = 4.0; // Sensibilité de la transition
|
|
3233
|
+
const finalScore = Math.round(ASYMPTOTE * Math.tanh(rawThreatIntensity / scaleFactor) * 10) / 10;
|
|
3077
3234
|
|
|
3078
|
-
const finalScore = Math.min(100, Math.round(baseScore * densityMultiplier * distributionMultiplier * uaMultiplier * dampening * 10) / 10);
|
|
3079
3235
|
return { subnetScore: finalScore };
|
|
3080
3236
|
}
|
|
3081
3237
|
|
|
@@ -3565,6 +3721,46 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
3565
3721
|
return { historyScore, rotationScore };
|
|
3566
3722
|
}
|
|
3567
3723
|
|
|
3724
|
+
function getVirtualizationAnomalyScore(context) {
|
|
3725
|
+
const clientFp = context.headers?.['x-device-fingerprint'];
|
|
3726
|
+
if (!clientFp) return 0.0;
|
|
3727
|
+
|
|
3728
|
+
const fpMap = {};
|
|
3729
|
+
clientFp.split('|').forEach(part => {
|
|
3730
|
+
const pair = part.split(':');
|
|
3731
|
+
if (pair.length === 2) {
|
|
3732
|
+
fpMap[pair[0]] = pair[1];
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
|
|
3736
|
+
let score = 0.0;
|
|
3737
|
+
const clientGpuHash = fpMap['gpu'];
|
|
3738
|
+
if (clientGpuHash) {
|
|
3739
|
+
const virtualGpus = [
|
|
3740
|
+
"Google SwiftShader", "SwiftShader",
|
|
3741
|
+
"Mesa llvmpipe", "llvmpipe", "Mesa Gallium",
|
|
3742
|
+
"Microsoft Basic Render Driver", "HeadlessChrome",
|
|
3743
|
+
"Intel(R) HD Graphics"
|
|
3744
|
+
];
|
|
3745
|
+
const virtualGpuHashes = new Set(virtualGpus.map(gpu => cyrb53(gpu).toString()));
|
|
3746
|
+
if (virtualGpuHashes.has(clientGpuHash)) {
|
|
3747
|
+
score += 75.0;
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
const clientScreenHash = fpMap['scr'];
|
|
3752
|
+
if (clientScreenHash) {
|
|
3753
|
+
const headlessResolutions = ["800x600_24", "1024x768_24"];
|
|
3754
|
+
const headlessHashes = new Set(headlessResolutions.map(res => cyrb53(res).toString()));
|
|
3755
|
+
if (headlessHashes.has(clientScreenHash)) {
|
|
3756
|
+
score += 25.0;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
return Math.min(100.0, score);
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
|
|
3568
3764
|
/**
|
|
3569
3765
|
* Returns a vector of raw (unweighted) suspicion scores.
|
|
3570
3766
|
* @param {object} context - The request context object.
|
|
@@ -3611,7 +3807,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3611
3807
|
_ // store.set result
|
|
3612
3808
|
] = await Promise.all([
|
|
3613
3809
|
getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
|
|
3614
|
-
getThreatIntelScore(zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3810
|
+
getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3615
3811
|
getTlsSpoofingScore(context),
|
|
3616
3812
|
getSubnetScore(context, deviceId),
|
|
3617
3813
|
getIpReputationScore(clientIp),
|
|
@@ -3621,12 +3817,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3621
3817
|
|
|
3622
3818
|
// Synchronous calculations
|
|
3623
3819
|
const { headerAnomalyScore } = getHeaderAnomalies(context);
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
// NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
|
|
3627
|
-
if (consistencyScore < 0.7) { // Seuil de rupture
|
|
3628
|
-
inconsistencyScore = 100;
|
|
3629
|
-
}
|
|
3820
|
+
const similarityThreshold = securityConfig?.similarityThreshold ?? 0.72;
|
|
3821
|
+
const inconsistencyScore = calculateAnalogInconsistencyScore(consistencyScore, similarityThreshold);
|
|
3630
3822
|
|
|
3631
3823
|
const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
|
|
3632
3824
|
|
|
@@ -3652,6 +3844,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3652
3844
|
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
3653
3845
|
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
3654
3846
|
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
3847
|
+
const virtualizationScore = getVirtualizationAnomalyScore(context);
|
|
3655
3848
|
|
|
3656
3849
|
// Save the updated device state to the store
|
|
3657
3850
|
// 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.
|
|
@@ -3663,7 +3856,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3663
3856
|
deviceData.ips = new Set(deviceData.ips);
|
|
3664
3857
|
}
|
|
3665
3858
|
// Le vecteur de suspicion est maintenant complet.
|
|
3666
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore };
|
|
3859
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
|
|
3667
3860
|
};
|
|
3668
3861
|
|
|
3669
3862
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -3879,6 +4072,8 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
3879
4072
|
const clientInitConfig = {
|
|
3880
4073
|
mouse: true,
|
|
3881
4074
|
keystrokes: true,
|
|
4075
|
+
wasmPath: securityConfig?.wasmPath || '/fp.wasm',
|
|
4076
|
+
workerPath: securityConfig?.workerPath || '/pow.worker.js',
|
|
3882
4077
|
trapUrls: trapUrls // On passe directement le tableau d'URL
|
|
3883
4078
|
};
|
|
3884
4079
|
|
|
@@ -4047,14 +4242,40 @@ function parseGraphQLQuery(body) {
|
|
|
4047
4242
|
* @param {string} zkpY - The 'y' component of the ZKP proof (public key).
|
|
4048
4243
|
* @returns {Promise<{threatIntelScore: number}>}
|
|
4049
4244
|
*/
|
|
4050
|
-
async function getThreatIntelScore(zkpY) {
|
|
4051
|
-
|
|
4245
|
+
async function getThreatIntelScore(context, zkpY) {
|
|
4246
|
+
let score = 0;
|
|
4247
|
+
if (zkpY) {
|
|
4248
|
+
const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
|
|
4249
|
+
if (isBanned) {
|
|
4250
|
+
score = 100;
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
const tcpRttHeader = context.headers?.['x-tcp-rtt'] || context.headers?.['x-real-rtt'];
|
|
4255
|
+
const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
|
|
4052
4256
|
|
|
4053
|
-
const
|
|
4054
|
-
if (
|
|
4055
|
-
|
|
4257
|
+
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
4258
|
+
if (behaviorHeader) {
|
|
4259
|
+
try {
|
|
4260
|
+
const metrics = JSON.parse(behaviorHeader);
|
|
4261
|
+
if (metrics && metrics.clientTimestamp && context.requestTimestamp) {
|
|
4262
|
+
const appLatency = context.requestTimestamp - metrics.clientTimestamp;
|
|
4263
|
+
if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0) {
|
|
4264
|
+
const clientToProxyDelta = appLatency - tcpRtt;
|
|
4265
|
+
if (tcpRtt < 35 && clientToProxyDelta > 150) {
|
|
4266
|
+
score = Math.max(score, 85);
|
|
4267
|
+
}
|
|
4268
|
+
} else {
|
|
4269
|
+
if (appLatency > 350) {
|
|
4270
|
+
score = Math.max(score, 40);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
} catch (e) {
|
|
4275
|
+
// Ignorer silencieusement
|
|
4276
|
+
}
|
|
4056
4277
|
}
|
|
4057
|
-
return { threatIntelScore:
|
|
4278
|
+
return { threatIntelScore: score };
|
|
4058
4279
|
}
|
|
4059
4280
|
|
|
4060
4281
|
export class FingerprintEngine {
|
|
@@ -4116,6 +4337,10 @@ export class FingerprintEngine {
|
|
|
4116
4337
|
this._validateConfig(finalConfig); // Validate the configuration
|
|
4117
4338
|
this.verbose = finalConfig.verbose || false;
|
|
4118
4339
|
this.dryRun = finalConfig.dryRun || false;
|
|
4340
|
+
|
|
4341
|
+
// Preload solver file asynchronously to liberate event loop during run
|
|
4342
|
+
preloadPowSolverCode().catch(() => {});
|
|
4343
|
+
|
|
4119
4344
|
if (finalConfig.reset) {
|
|
4120
4345
|
this.resetStore().catch(err => {
|
|
4121
4346
|
console.error('[FingerprintEngine] Failed to reset store on startup:', err.message);
|
|
@@ -5136,9 +5361,12 @@ export class FingerprintEngine {
|
|
|
5136
5361
|
// 2. La requête est très suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
|
|
5137
5362
|
const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
|
|
5138
5363
|
const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
|
|
5139
|
-
|
|
5364
|
+
// Correction : Pour éviter une boucle infinie de challenges (qui mène à l'erreur 429),
|
|
5365
|
+
// on fait confiance au ticket valide tant qu'il n'a pas expiré.
|
|
5366
|
+
const maxIndicatorsCount = Object.values(suspicionVector).filter(val => typeof val === 'number' && val >= 100).length;
|
|
5367
|
+
const mustReChallenge = (suspicionVector.honeypotScore >= (thresholds.medium ?? 45)) || (maxIndicatorsCount >= 1);
|
|
5140
5368
|
|
|
5141
|
-
if (isSuspicious &&
|
|
5369
|
+
if ((isSuspicious && !hasValidTicket) || mustReChallenge) {
|
|
5142
5370
|
if (mustReChallenge) {
|
|
5143
5371
|
this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
|
|
5144
5372
|
}
|
|
@@ -5185,7 +5413,7 @@ export class FingerprintEngine {
|
|
|
5185
5413
|
let usefulWorkDispatched = false;
|
|
5186
5414
|
let challengePayload = null;
|
|
5187
5415
|
|
|
5188
|
-
if (isSuspicious && shouldUseUsefulWork) {
|
|
5416
|
+
if ((isSuspicious || mustReChallenge) && shouldUseUsefulWork) {
|
|
5189
5417
|
this._log('Issuing a useful work challenge', { finalScore });
|
|
5190
5418
|
|
|
5191
5419
|
try {
|
|
@@ -5217,7 +5445,7 @@ export class FingerprintEngine {
|
|
|
5217
5445
|
}
|
|
5218
5446
|
}
|
|
5219
5447
|
|
|
5220
|
-
if (isSuspicious && usefulWorkDispatched) {
|
|
5448
|
+
if ((isSuspicious || mustReChallenge) && usefulWorkDispatched) {
|
|
5221
5449
|
if (isApi) {
|
|
5222
5450
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
5223
5451
|
} else {
|
|
@@ -5233,7 +5461,7 @@ export class FingerprintEngine {
|
|
|
5233
5461
|
</script></body></html>`;
|
|
5234
5462
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: html };
|
|
5235
5463
|
}
|
|
5236
|
-
} else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi / a échoué
|
|
5464
|
+
} else if (isSuspicious || mustReChallenge) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi / a échoué
|
|
5237
5465
|
const decision = { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404 };
|
|
5238
5466
|
if (this.dryRun) {
|
|
5239
5467
|
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
@@ -5489,7 +5717,7 @@ export async function findPeerInSubnet(clientIp, excludeNodeId) {
|
|
|
5489
5717
|
if (activePeers.length === 0) return null;
|
|
5490
5718
|
|
|
5491
5719
|
// Select a random peer
|
|
5492
|
-
const randomIndex =
|
|
5720
|
+
const randomIndex = crypto.randomInt(0, activePeers.length);
|
|
5493
5721
|
return activePeers[randomIndex];
|
|
5494
5722
|
}
|
|
5495
5723
|
|
|
@@ -5597,6 +5825,15 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
|
|
|
5597
5825
|
case 'register':
|
|
5598
5826
|
expectedMsg = `${clientSecret}:register:${nodeId}:${params.seed || ''}`;
|
|
5599
5827
|
break;
|
|
5828
|
+
case 'find_peer':
|
|
5829
|
+
expectedMsg = `${clientSecret}:find_peer:${nodeId}`;
|
|
5830
|
+
break;
|
|
5831
|
+
case 'webrtc_signal':
|
|
5832
|
+
expectedMsg = `${clientSecret}:webrtc_signal:${nodeId}:${params.target_peer_id || ''}:${params.signal_type || ''}:${params.signal_data || ''}`;
|
|
5833
|
+
break;
|
|
5834
|
+
case 'poll_signals':
|
|
5835
|
+
expectedMsg = `${clientSecret}:poll_signals:${nodeId}`;
|
|
5836
|
+
break;
|
|
5600
5837
|
case 'request_peer_block':
|
|
5601
5838
|
expectedMsg = `${clientSecret}:request_peer_block:${nodeId}:${params.peer_id || ''}:${params.block_idx || '0'}:${params.req_id || ''}`;
|
|
5602
5839
|
break;
|
|
@@ -5625,14 +5862,49 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
|
|
|
5625
5862
|
await registerCooperativeNode(clientIp, nodeId, seed);
|
|
5626
5863
|
return { status: 'registered' };
|
|
5627
5864
|
|
|
5865
|
+
case 'find_peer': {
|
|
5866
|
+
const peer = await findPeerInSubnet(clientIp, nodeId);
|
|
5867
|
+
if (!peer) {
|
|
5868
|
+
return { status: 'no_peers' };
|
|
5869
|
+
}
|
|
5870
|
+
return { status: 'peer_found', peer_id: peer.nodeId };
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
case 'webrtc_signal': {
|
|
5874
|
+
const targetPeerId = params.target_peer_id || '';
|
|
5875
|
+
const signalType = params.signal_type || '';
|
|
5876
|
+
const signalData = params.signal_data || '';
|
|
5877
|
+
if (!targetPeerId || !signalType) {
|
|
5878
|
+
return { error: 'Invalid parameters' };
|
|
5879
|
+
}
|
|
5880
|
+
const signalQueueKey = `coop-webrtc:signals:${targetPeerId}`;
|
|
5881
|
+
const signals = (await store.get(signalQueueKey)) || [];
|
|
5882
|
+
signals.push({
|
|
5883
|
+
from_peer_id: nodeId,
|
|
5884
|
+
signal_type: signalType,
|
|
5885
|
+
signal_data: signalData
|
|
5886
|
+
});
|
|
5887
|
+
await store.set(signalQueueKey, signals, 30);
|
|
5888
|
+
return { status: 'signal_queued' };
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
case 'poll_signals': {
|
|
5892
|
+
const pollSignalKey = `coop-webrtc:signals:${nodeId}`;
|
|
5893
|
+
const signals = (await store.get(pollSignalKey)) || [];
|
|
5894
|
+
if (signals.length > 0) {
|
|
5895
|
+
await store.delete(pollSignalKey);
|
|
5896
|
+
}
|
|
5897
|
+
return { status: 'ok', signals };
|
|
5898
|
+
}
|
|
5899
|
+
|
|
5628
5900
|
case 'request_peer_block':
|
|
5901
|
+
{
|
|
5629
5902
|
const peerId = params.peer_id || '';
|
|
5630
5903
|
const blockIdx = parseInt(params.block_idx || '0', 10);
|
|
5631
5904
|
const requestId = params.req_id || '';
|
|
5632
5905
|
if (!peerId || !requestId) {
|
|
5633
|
-
return {
|
|
5906
|
+
return {error: 'Invalid parameters'};
|
|
5634
5907
|
}
|
|
5635
|
-
|
|
5636
5908
|
const queueKey = `coop-mailbox:queue:${peerId}`;
|
|
5637
5909
|
const requests = (await store.get(queueKey)) || [];
|
|
5638
5910
|
requests.push({
|
|
@@ -5641,7 +5913,8 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
|
|
|
5641
5913
|
block_idx: blockIdx
|
|
5642
5914
|
});
|
|
5643
5915
|
await store.set(queueKey, requests, 30);
|
|
5644
|
-
return {
|
|
5916
|
+
return {status: 'queued'};
|
|
5917
|
+
}
|
|
5645
5918
|
|
|
5646
5919
|
case 'poll_requests':
|
|
5647
5920
|
const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
|
|
@@ -6481,6 +6754,7 @@ export const __internal = {
|
|
|
6481
6754
|
calculateTarget,
|
|
6482
6755
|
determineOptimalTicketTtl,
|
|
6483
6756
|
runBackgroundTtlOptimization,
|
|
6757
|
+
calculateAnalogInconsistencyScore,
|
|
6484
6758
|
getRequestPatternScore, // Expose for testing
|
|
6485
6759
|
getBehaviorScore, // Expose for testing
|
|
6486
6760
|
getCrossLayerInconsistency, // Expose for testing
|
|
@@ -6646,7 +6920,7 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
6646
6920
|
const minDataPoints = 200; // Seuil par défaut
|
|
6647
6921
|
const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
|
|
6648
6922
|
if (passedLogs.length > maxPassedAllowed) {
|
|
6649
|
-
const shuffledPassed = passedLogs.sort(() => 0.5 -
|
|
6923
|
+
const shuffledPassed = passedLogs.sort(() => 0.5 - secureRandomFloat());
|
|
6650
6924
|
return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
|
|
6651
6925
|
}
|
|
6652
6926
|
return [...suspiciousLogs, ...passedLogs];
|