@anonympins/fingerprint 0.5.4 → 0.6.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 +7 -6
- package/package.json +1 -1
- package/src/js/build-client.js +67 -67
- package/src/js/fingerprint.client.js +1445 -1271
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +499 -116
- package/src/js/obfuscation.worker.js +23 -0
- package/src/js/pow.solver.inline.js +37 -28
- package/src/js/pow.solver.js +37 -28
- package/src/js/problem-manager.js +22 -0
- package/src/js/tests/cross_parity.test.js +175 -0
- package/src/js/tests/fingerprint.client.test.js +130 -128
- package/src/js/tests/fingerprint.test.js +42 -2
- package/src/js/tests/obfuscation.test.js +48 -0
- package/src/js/ttl-optimization.worker.js +51 -0
- package/src/php/AutoTuner.php +42 -0
- package/src/php/Challenge/ChallengeUtils.php +57 -6
- package/src/php/FingerprintBuilder.php +193 -185
- package/src/php/FingerprintEngine.php +147 -1
- package/src/php/ProblemManager.php +20 -0
- package/src/php/Tests/ChallengeUtilsTest.php +446 -285
- package/src/php/Utils/RequestUtils.php +19 -0
package/src/js/fingerprint.js
CHANGED
|
@@ -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";
|
|
@@ -123,7 +124,7 @@ const yandexEntries = loadBotWhitelist('yandex.json', [
|
|
|
123
124
|
]);
|
|
124
125
|
|
|
125
126
|
function generateSessionMapping() {
|
|
126
|
-
const randomStr = (len = 6) =>
|
|
127
|
+
const randomStr = (len = 6) => Array.from({ length: len }, () => String.fromCharCode(crypto.randomInt(97, 123))).join('');
|
|
127
128
|
const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
|
|
128
129
|
|
|
129
130
|
return {
|
|
@@ -184,18 +185,29 @@ async function compilePolymorphicJs(mapping) {
|
|
|
184
185
|
jsCode = jsCode.replace(regex2, `addRaw("${randKey}"`);
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 });
|
|
196
210
|
});
|
|
197
|
-
|
|
198
|
-
return obfuscationResult.getObfuscatedCode();
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
async function ensureLatestMapping() {
|
|
@@ -750,6 +762,181 @@ const cipherSuiteMap = {
|
|
|
750
762
|
'TLS_RSA_WITH_3DES_EDE_CBC_SHA': 10,
|
|
751
763
|
};
|
|
752
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
|
+
const TRUSTED_HARDWARE_ROOTS = [
|
|
773
|
+
"-----BEGIN CERTIFICATE-----\n" +
|
|
774
|
+
"MIIDHzCCAfegAwIBAgIJANCvWjvF+2O6MA0GCSqGSIb3DQEBCwUAMC0xKzApBgNV\n" +
|
|
775
|
+
"BAMTIll1YmljbyBBdHRlc3RhdGlvbiBSb290IENBMB4XDTE0MDgwNDAwMDAwMFox\n" +
|
|
776
|
+
"TSUxSDBGBgNVBAMMT1l1YmljbyBBdHRlc3RhdGlvbiBSb290IENBMSowKAYDVQQK\n" +
|
|
777
|
+
"EyFZdWJpY28gQUIxDzANBgNVBAcTBVN0b2NraG9sbTELMAkGA1UEBhMCU0UwggEi\n" +
|
|
778
|
+
"MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6XW0d87g+N6kGgSgC/H9UfA2p\n" +
|
|
779
|
+
"-----END CERTIFICATE-----",
|
|
780
|
+
"-----BEGIN CERTIFICATE-----\n" +
|
|
781
|
+
"MIIB1DCCAXWgAwIBAgIEUI70WjAKBggqhkjOPQQDAjArMSkwJwYDVQQDEyBGSURP\n" +
|
|
782
|
+
"IEFsbGlhbmNlIFJvb3QgQ0EgKFRlc3QpMB4XDTE0MDgxODA4MzA0NVoXDTM5MDgx\n" +
|
|
783
|
+
"ODA4MzA0NVowKzEpMCcGA1UEAxMgRklETyBBbGxpYW5jZSBSb290IENBIChUZXN0\n" +
|
|
784
|
+
"KTB2MBAGByqGSM49AgEGBSuBBAAiA2IABFv81Jm9M7AehfOIdpCH567gP0yqS40m\n" +
|
|
785
|
+
"aN0j1a8n152G7n/nUf7J0j9F4pL9J2w1X8hN1N8f9Y3G9w8L29/m7/zX3O3n2e7/\n" +
|
|
786
|
+
"g==\n" +
|
|
787
|
+
"-----END CERTIFICATE-----"
|
|
788
|
+
];
|
|
789
|
+
|
|
790
|
+
function decodeCBOR(buffer) {
|
|
791
|
+
let offset = 0;
|
|
792
|
+
function readByte() {
|
|
793
|
+
if (offset >= buffer.length) throw new Error("Unexpected end of CBOR data");
|
|
794
|
+
return buffer[offset++];
|
|
795
|
+
}
|
|
796
|
+
function readBytes(len) {
|
|
797
|
+
if (offset + len > buffer.length) throw new Error("Unexpected end of CBOR bytes");
|
|
798
|
+
const res = buffer.slice(offset, offset + len);
|
|
799
|
+
offset += len;
|
|
800
|
+
return res;
|
|
801
|
+
}
|
|
802
|
+
function readInt(val) {
|
|
803
|
+
if (val < 24) return val;
|
|
804
|
+
if (val === 24) return readByte();
|
|
805
|
+
if (val === 25) {
|
|
806
|
+
const b1 = readByte(); const b2 = readByte();
|
|
807
|
+
return (b1 << 8) | b2;
|
|
808
|
+
}
|
|
809
|
+
if (val === 26) {
|
|
810
|
+
const b1 = readByte(); const b2 = readByte();
|
|
811
|
+
const b3 = readByte(); const b4 = readByte();
|
|
812
|
+
return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
|
|
813
|
+
}
|
|
814
|
+
throw new Error("Unsupported integer size: " + val);
|
|
815
|
+
}
|
|
816
|
+
function decodeType() {
|
|
817
|
+
const initial = readByte();
|
|
818
|
+
const major = initial >> 5;
|
|
819
|
+
const val = initial & 0x1f;
|
|
820
|
+
if (major === 0) {
|
|
821
|
+
return readInt(val);
|
|
822
|
+
} else if (major === 1) {
|
|
823
|
+
return -1 - readInt(val);
|
|
824
|
+
} else if (major === 2) {
|
|
825
|
+
const len = readInt(val);
|
|
826
|
+
return readBytes(len);
|
|
827
|
+
} else if (major === 3) {
|
|
828
|
+
const len = readInt(val);
|
|
829
|
+
return readBytes(len).toString('utf8');
|
|
830
|
+
} else if (major === 4) {
|
|
831
|
+
const len = readInt(val);
|
|
832
|
+
const arr = [];
|
|
833
|
+
for (let i = 0; i < len; i++) arr.push(decodeType());
|
|
834
|
+
return arr;
|
|
835
|
+
} else if (major === 5) {
|
|
836
|
+
const len = readInt(val);
|
|
837
|
+
const obj = {};
|
|
838
|
+
for (let i = 0; i < len; i++) {
|
|
839
|
+
const k = decodeType();
|
|
840
|
+
const v = decodeType();
|
|
841
|
+
obj[k] = v;
|
|
842
|
+
}
|
|
843
|
+
return obj;
|
|
844
|
+
}
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
return decodeType();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function verifyCertificateChain(x5c) {
|
|
851
|
+
if (!x5c || x5c.length === 0) return false;
|
|
852
|
+
try {
|
|
853
|
+
const pems = x5c.map(der => {
|
|
854
|
+
const base64 = der.toString('base64');
|
|
855
|
+
return `-----BEGIN CERTIFICATE-----\n${base64.match(/.{1,64}/g).join('\n')}\n-----END CERTIFICATE-----`;
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
for (let i = 0; i < pems.length - 1; i++) {
|
|
859
|
+
const child = new crypto.X509Certificate(pems[i]);
|
|
860
|
+
const parent = new crypto.X509Certificate(pems[i + 1]);
|
|
861
|
+
if (!child.verify(parent.publicKey)) {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const rootCert = new crypto.X509Certificate(pems[pems.length - 1]);
|
|
867
|
+
let trusted = false;
|
|
868
|
+
for (const trustedRootPem of TRUSTED_HARDWARE_ROOTS) {
|
|
869
|
+
const trustedRoot = new crypto.X509Certificate(trustedRootPem);
|
|
870
|
+
if (rootCert.subject === trustedRoot.subject) {
|
|
871
|
+
trusted = true;
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
if (rootCert.verify(trustedRoot.publicKey)) {
|
|
875
|
+
trusted = true;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return trusted;
|
|
880
|
+
} catch (e) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function verifyWebAuthnHardwareAnchor(anchor, deviceData) {
|
|
886
|
+
if (!anchor || !anchor.type) return false;
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
const clientDataHash = crypto.createHash('sha256')
|
|
890
|
+
.update(Buffer.from(anchor.clientDataJSON, 'base64'))
|
|
891
|
+
.digest();
|
|
892
|
+
|
|
893
|
+
if (anchor.type === 'registration') {
|
|
894
|
+
if (!anchor.publicKey || !anchor.credentialId || !anchor.attestationObject) return false;
|
|
895
|
+
|
|
896
|
+
const attestationBytes = Buffer.from(anchor.attestationObject, 'base64');
|
|
897
|
+
const decoded = decodeCBOR(attestationBytes);
|
|
898
|
+
|
|
899
|
+
if (!decoded || !decoded.fmt || !decoded.attStmt) return false;
|
|
900
|
+
|
|
901
|
+
if (decoded.fmt !== 'none') {
|
|
902
|
+
const attStmt = decoded.attStmt;
|
|
903
|
+
if (!attStmt.x5c || !Array.isArray(attStmt.x5c)) return false;
|
|
904
|
+
|
|
905
|
+
if (!verifyCertificateChain(attStmt.x5c)) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Enregistrement initial : on stocke la clé publique matérielle SPKI
|
|
911
|
+
deviceData.webauthnPublicKey = anchor.publicKey;
|
|
912
|
+
deviceData.webauthnCredentialId = anchor.credentialId;
|
|
913
|
+
return true;
|
|
914
|
+
} else if (anchor.type === 'assertion') {
|
|
915
|
+
const storedPublicKeyPem = deviceData.webauthnPublicKey;
|
|
916
|
+
if (!storedPublicKeyPem || deviceData.webauthnCredentialId !== anchor.credentialId) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Reconstitution du message signé (authenticatorData + clientDataHash)
|
|
921
|
+
const verifyBuffer = Buffer.concat([
|
|
922
|
+
Buffer.from(anchor.authenticatorData, 'base64'),
|
|
923
|
+
clientDataHash
|
|
924
|
+
]);
|
|
925
|
+
|
|
926
|
+
const publicKey = crypto.createPublicKey(Buffer.from(storedPublicKeyPem, 'base64'));
|
|
927
|
+
return crypto.verify(
|
|
928
|
+
'sha256',
|
|
929
|
+
verifyBuffer,
|
|
930
|
+
publicKey,
|
|
931
|
+
Buffer.from(anchor.signature, 'base64')
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
} catch (e) {
|
|
935
|
+
console.error('[WebAuthn-Server] Verification failed:', e.message);
|
|
936
|
+
}
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
|
|
753
940
|
/**
|
|
754
941
|
* Extracts TLS fingerprints (JA3 and JA4) from request context.
|
|
755
942
|
* Prioritizes headers from reverse proxies (x-ja4-hash) and falls back to JA3 calculation
|
|
@@ -1523,6 +1710,14 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
|
|
|
1523
1710
|
const peerBlockIdx = ${peerBlockIdx ?? -1};
|
|
1524
1711
|
const coopTimeout = ${coopTimeout};
|
|
1525
1712
|
|
|
1713
|
+
async function signCoop(op, nid, extra = "") {
|
|
1714
|
+
const msg = clientSecret + ":" + op + ":" + nid + (extra ? ":" + extra : "");
|
|
1715
|
+
const encoder = new TextEncoder();
|
|
1716
|
+
const data = encoder.encode(msg);
|
|
1717
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1718
|
+
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1526
1721
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
1527
1722
|
await new Promise(r => setTimeout(r, 10));
|
|
1528
1723
|
|
|
@@ -1531,20 +1726,40 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
|
|
|
1531
1726
|
|
|
1532
1727
|
if (peerId && peerBlockIdx !== -1) {
|
|
1533
1728
|
// Enregistrement coopératif
|
|
1534
|
-
await
|
|
1729
|
+
const sig = await signCoop("register", nodeId, nonce + ":" + clientSecret);
|
|
1730
|
+
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
|
|
1535
1731
|
}
|
|
1536
1732
|
|
|
1537
|
-
|
|
1733
|
+
// Écoute des requêtes entrantes de nos pairs
|
|
1734
|
+
setInterval(async () => {
|
|
1735
|
+
try {
|
|
1736
|
+
const sig = await signCoop("poll_requests", nodeId);
|
|
1737
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
|
|
1738
|
+
const data = await res.json();
|
|
1739
|
+
if (data.requests && data.requests.length > 0) {
|
|
1740
|
+
for (const req of data.requests) {
|
|
1741
|
+
document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
|
|
1742
|
+
const blockData = await window.readSpaceBlock(req.block_idx);
|
|
1743
|
+
const respSig = await signCoop("respond_block", nodeId, req.requester_id + ":" + req.req_id + ":" + blockData);
|
|
1744
|
+
await fetch(window.location.pathname + "?coop_op=respond_block&node_id=" + nodeId + "&requester_id=" + req.requester_id + "&req_id=" + req.req_id + "&block_data=" + encodeURIComponent(blockData) + "&coop_sig=" + respSig);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
} catch (e) {
|
|
1748
|
+
console.error("Cooperative polling error", e);
|
|
1749
|
+
}
|
|
1750
|
+
}, 1000);
|
|
1538
1751
|
|
|
1539
1752
|
let peerBlock = "";
|
|
1540
1753
|
if (peerId && peerBlockIdx !== -1) {
|
|
1541
1754
|
document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
|
|
1542
1755
|
const reqId = Math.random().toString(36).substring(2);
|
|
1543
|
-
await
|
|
1756
|
+
const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
|
|
1757
|
+
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);
|
|
1544
1758
|
|
|
1545
1759
|
let attempts = 0;
|
|
1546
1760
|
while (attempts < coopTimeout) {
|
|
1547
|
-
const
|
|
1761
|
+
const pollSig = await signCoop("poll_response", nodeId, reqId);
|
|
1762
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
|
|
1548
1763
|
const data = await res.json();
|
|
1549
1764
|
if (data.status === 'ready') {
|
|
1550
1765
|
peerBlock = data.block_data;
|
|
@@ -1558,6 +1773,7 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
|
|
|
1558
1773
|
}
|
|
1559
1774
|
}
|
|
1560
1775
|
|
|
1776
|
+
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
1561
1777
|
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
|
|
1562
1778
|
|
|
1563
1779
|
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
|
|
@@ -1964,10 +2180,29 @@ function getBehaviorScore(context) {
|
|
|
1964
2180
|
* @param {object} metrics - Les métriques comportementales parsées depuis le client.
|
|
1965
2181
|
* @returns {{timeInconsistencyScore: number}}
|
|
1966
2182
|
*/
|
|
1967
|
-
function getTimeInconsistencyScore(context, metrics) {
|
|
2183
|
+
function getTimeInconsistencyScore(context, metrics, deviceData = null) {
|
|
1968
2184
|
const REPLAY_THRESHOLD_MS = 5000; // 5 secondes
|
|
1969
2185
|
let score = 0;
|
|
1970
2186
|
|
|
2187
|
+
if (deviceData && deviceData.sessionHmacKey && metrics.signature) {
|
|
2188
|
+
try {
|
|
2189
|
+
const copy = JSON.parse(JSON.stringify(metrics));
|
|
2190
|
+
const clientSig = copy.signature;
|
|
2191
|
+
delete copy.signature;
|
|
2192
|
+
const dataToSign = JSON.stringify(copy);
|
|
2193
|
+
const expectedSig = crypto.createHmac('sha256', Buffer.from(deviceData.sessionHmacKey, 'hex'))
|
|
2194
|
+
.update(dataToSign)
|
|
2195
|
+
.digest('hex');
|
|
2196
|
+
if (!crypto.timingSafeEqual(Buffer.from(clientSig, 'hex'), Buffer.from(expectedSig, 'hex'))) {
|
|
2197
|
+
return { timeInconsistencyScore: 100 }; // Replay/tampering detected
|
|
2198
|
+
}
|
|
2199
|
+
} catch (e) {
|
|
2200
|
+
return { timeInconsistencyScore: 100 };
|
|
2201
|
+
}
|
|
2202
|
+
} else if (deviceData && deviceData.sessionHmacKey && !metrics.signature) {
|
|
2203
|
+
return { timeInconsistencyScore: 100 }; // Missing mandatory signature
|
|
2204
|
+
}
|
|
2205
|
+
|
|
1971
2206
|
if (metrics.clientTimestamp && context.requestTimestamp) {
|
|
1972
2207
|
const timeDelta = context.requestTimestamp - metrics.clientTimestamp;
|
|
1973
2208
|
|
|
@@ -2972,6 +3207,37 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
2972
3207
|
deviceData.lastFpHash = currentFpHash;
|
|
2973
3208
|
deviceData.ips.add(clientIp); // Record the IP used by this device
|
|
2974
3209
|
|
|
3210
|
+
// Nettoyage par fenêtre glissante pour éviter l'accumulation sur les sessions longues
|
|
3211
|
+
if (!deviceData.ipTimes) {
|
|
3212
|
+
deviceData.ipTimes = {};
|
|
3213
|
+
}
|
|
3214
|
+
deviceData.ipTimes[clientIp] = now;
|
|
3215
|
+
|
|
3216
|
+
const slidingWindow = 2 * 60 * 60 * 1000; // 2 heures
|
|
3217
|
+
const cutOff = now - slidingWindow;
|
|
3218
|
+
for (const [ip, lastSeen] of Object.entries(deviceData.ipTimes)) {
|
|
3219
|
+
if (lastSeen < cutOff) {
|
|
3220
|
+
deviceData.ips.delete(ip);
|
|
3221
|
+
delete deviceData.ipTimes[ip];
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
// --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
|
|
3225
|
+
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
3226
|
+
let webauthnVerified = false;
|
|
3227
|
+
if (behaviorHeader) {
|
|
3228
|
+
try {
|
|
3229
|
+
const metrics = JSON.parse(behaviorHeader);
|
|
3230
|
+
if (metrics && metrics.webauthnAnchor) {
|
|
3231
|
+
if (verifyWebAuthnHardwareAnchor(metrics.webauthnAnchor, deviceData)) {
|
|
3232
|
+
webauthnVerified = true;
|
|
3233
|
+
deviceData.webauthnVerified = true;
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
} catch (e) {
|
|
3237
|
+
// Ignorer les erreurs de parsing
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
2975
3241
|
// NOUVELLE LOGIQUE : Le score d'historique est basé sur le nombre d'IPs utilisées par l'appareil.
|
|
2976
3242
|
// Très efficace contre la rotation de proxy.
|
|
2977
3243
|
const maxIpsForDevice = isSharedIp
|
|
@@ -3015,57 +3281,65 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3015
3281
|
context._newCookies = context._newCookies || [];
|
|
3016
3282
|
context._newCookies.push(newCookie);
|
|
3017
3283
|
}
|
|
3018
|
-
await store.set(`ip-device:${clientIp}`, deviceId, 600); // Link the IP to the device for 10 minutes
|
|
3019
3284
|
|
|
3020
3285
|
// Periodically clean up device data
|
|
3021
3286
|
if (Date.now() - deviceData.lastUpdate > 10 * 60 * 1000) { // 10 minutes
|
|
3022
3287
|
deviceData.ips.clear();
|
|
3023
3288
|
deviceData.rapidChangeCount = 0;
|
|
3289
|
+
deviceData.ipTimes = {};
|
|
3024
3290
|
}
|
|
3025
3291
|
deviceData.lastUpdate = Date.now();
|
|
3026
3292
|
|
|
3027
|
-
const
|
|
3028
|
-
const
|
|
3029
|
-
// Calculate the inconsistency score here, separately.
|
|
3030
|
-
let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
|
|
3031
|
-
|
|
3032
|
-
// NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
|
|
3033
|
-
if (consistencyScore < 0.7) { // Seuil de rupture
|
|
3034
|
-
inconsistencyScore = 100;
|
|
3035
|
-
}
|
|
3036
|
-
|
|
3037
|
-
const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
|
|
3038
|
-
|
|
3039
|
-
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
3040
|
-
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
3293
|
+
const stableFp = extractStablePart(currentDeviceHash);
|
|
3294
|
+
const stableFpHash = cyrb53(stableFp).toString();
|
|
3041
3295
|
|
|
3042
|
-
|
|
3296
|
+
// Execute non-interdependent asynchronous operations in parallel
|
|
3297
|
+
const [
|
|
3298
|
+
behavioral,
|
|
3299
|
+
{ tlsSpoofingScore },
|
|
3300
|
+
{ subnetScore },
|
|
3301
|
+
ipReputationScore,
|
|
3302
|
+
{ botnetClusterScore },
|
|
3303
|
+
_ // store.set result
|
|
3304
|
+
] = await Promise.all([
|
|
3305
|
+
getBehavioralIndicators(context, deviceData),
|
|
3306
|
+
getTlsSpoofingScore(context),
|
|
3307
|
+
getSubnetScore(context, deviceId),
|
|
3308
|
+
getIpReputationScore(clientIp),
|
|
3309
|
+
getBotnetClusterScore(context, stableFpHash),
|
|
3310
|
+
store.set(`ip-device:${clientIp}`, deviceId, 600) // Link the IP to the device for 10 minutes
|
|
3311
|
+
]);
|
|
3312
|
+
|
|
3313
|
+
// Synchronous calculations
|
|
3314
|
+
const { headerAnomalyScore } = getHeaderAnomalies(context);
|
|
3315
|
+
let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
|
|
3316
|
+
|
|
3317
|
+
// NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
|
|
3318
|
+
if (consistencyScore < 0.7) { // Seuil de rupture
|
|
3319
|
+
inconsistencyScore = 100;
|
|
3320
|
+
}
|
|
3043
3321
|
|
|
3044
|
-
|
|
3322
|
+
const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
|
|
3045
3323
|
|
|
3046
|
-
|
|
3047
|
-
|
|
3324
|
+
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
3325
|
+
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
3048
3326
|
|
|
3049
|
-
|
|
3050
|
-
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
3327
|
+
const { botScore } = getBotScore(context);
|
|
3051
3328
|
|
|
3052
|
-
|
|
3053
|
-
|
|
3329
|
+
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
3330
|
+
const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'), deviceData);
|
|
3054
3331
|
|
|
3055
|
-
|
|
3056
|
-
|
|
3332
|
+
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
3333
|
+
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
3057
3334
|
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
const { subnetScore } = await getSubnetScore(context, deviceId);
|
|
3335
|
+
// NOUVEAU: On calcule le score de variance des clics.
|
|
3336
|
+
const { clickVarianceScore } = getClickVarianceScore(context);
|
|
3061
3337
|
|
|
3062
|
-
|
|
3338
|
+
// NOUVEAU: On calcule le score d'incohérence des Client-Hints.
|
|
3339
|
+
const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
|
|
3063
3340
|
|
|
3064
|
-
|
|
3341
|
+
const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
|
|
3065
3342
|
|
|
3066
|
-
const stableFp = extractStablePart(currentDeviceHash);
|
|
3067
|
-
const stableFpHash = cyrb53(stableFp).toString();
|
|
3068
|
-
const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
|
|
3069
3343
|
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
3070
3344
|
const { quicAnomalyScore } = getQuicAnomalyScore(context);
|
|
3071
3345
|
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
@@ -3204,7 +3478,40 @@ export function generateCpuTargetChallenge(
|
|
|
3204
3478
|
};
|
|
3205
3479
|
}
|
|
3206
3480
|
|
|
3207
|
-
|
|
3481
|
+
class SimpleLRUCache {
|
|
3482
|
+
constructor(maxSize = 100) {
|
|
3483
|
+
this.maxSize = maxSize;
|
|
3484
|
+
this.cache = new Map();
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
get(key) {
|
|
3488
|
+
if (!this.cache.has(key)) return undefined;
|
|
3489
|
+
const value = this.cache.get(key);
|
|
3490
|
+
this.cache.delete(key);
|
|
3491
|
+
this.cache.set(key, value);
|
|
3492
|
+
return value;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
set(key, value) {
|
|
3496
|
+
if (this.cache.has(key)) {
|
|
3497
|
+
this.cache.delete(key);
|
|
3498
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
3499
|
+
const lruKey = this.cache.keys().next().value;
|
|
3500
|
+
this.cache.delete(lruKey);
|
|
3501
|
+
}
|
|
3502
|
+
this.cache.set(key, value);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
has(key) {
|
|
3506
|
+
return this.cache.has(key);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
clear() {
|
|
3510
|
+
this.cache.clear();
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
const htmlTemplateCache = new SimpleLRUCache(100);
|
|
3208
3515
|
|
|
3209
3516
|
/**
|
|
3210
3517
|
* Generates the HTML page for the CPU target challenge.
|
|
@@ -3812,13 +4119,19 @@ export class FingerprintEngine {
|
|
|
3812
4119
|
|
|
3813
4120
|
this._log('Processing request', { clientIp, path, isStatic });
|
|
3814
4121
|
|
|
4122
|
+
// Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
|
|
4123
|
+
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
4124
|
+
if (deviceData && deviceData.webauthnVerified) {
|
|
4125
|
+
this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
|
|
4126
|
+
return { action: 'next', score: 0, vector: { webauthn_verified: 100 } };
|
|
4127
|
+
}
|
|
4128
|
+
|
|
3815
4129
|
if (isStatic) {
|
|
3816
4130
|
this._log('Static resource - skipping checks');
|
|
3817
4131
|
return { action: 'next', score: 0, vector: {} };
|
|
3818
4132
|
}
|
|
3819
4133
|
|
|
3820
4134
|
// Resolve identity and check for persisted "condemned" status early.
|
|
3821
|
-
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
3822
4135
|
const currentDeviceHash = getCompositeDeviceHash(requestContext);
|
|
3823
4136
|
const isNewDevice = !!newCookie;
|
|
3824
4137
|
const allowRoaming = this.securityConfig?.allowCrossNetworkRoaming ?? false;
|
|
@@ -4237,17 +4550,22 @@ export class FingerprintEngine {
|
|
|
4237
4550
|
|
|
4238
4551
|
const isBlocked = finalScore >= blockThreshold;
|
|
4239
4552
|
|
|
4240
|
-
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
4553
|
+
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked && finalScore > 0;
|
|
4241
4554
|
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
4242
4555
|
const isSuspicious = finalScore >= thresholds.low;
|
|
4243
4556
|
const isVerySuspicious = finalScore >= thresholds.medium; // Seuil pour le challenge d'optimisation
|
|
4244
4557
|
|
|
4245
4558
|
// Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
|
|
4246
|
-
const suspicionFactor = isSuspicious
|
|
4247
|
-
?
|
|
4248
|
-
|
|
4249
|
-
(
|
|
4250
|
-
|
|
4559
|
+
const suspicionFactor = isSuspicious // eslint-disable-line no-nested-ternary
|
|
4560
|
+
? (() => {
|
|
4561
|
+
const denominator = thresholds.high - thresholds.low;
|
|
4562
|
+
if (denominator === 0) {
|
|
4563
|
+
// If the range is zero, and finalScore is at or above low threshold,
|
|
4564
|
+
// return 0 to avoid NaN.
|
|
4565
|
+
return 0;
|
|
4566
|
+
}
|
|
4567
|
+
return Math.min(1.5, (finalScore - thresholds.low) / denominator);
|
|
4568
|
+
})()
|
|
4251
4569
|
: 0;
|
|
4252
4570
|
|
|
4253
4571
|
this._log('Suspicion levels evaluated', {
|
|
@@ -4661,6 +4979,42 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
|
|
|
4661
4979
|
return { error: 'Missing node_id' };
|
|
4662
4980
|
}
|
|
4663
4981
|
|
|
4982
|
+
// --- VÉRIFICATION DE LA SIGNATURE COOPÉRATIVE ---
|
|
4983
|
+
const challengeContext = await store.get(`secret:${nodeId}`);
|
|
4984
|
+
if (!challengeContext || !challengeContext.clientSecret) {
|
|
4985
|
+
return { error: 'Invalid or expired node_id' };
|
|
4986
|
+
}
|
|
4987
|
+
|
|
4988
|
+
const clientSecret = challengeContext.clientSecret;
|
|
4989
|
+
const coopSig = params.coop_sig || '';
|
|
4990
|
+
|
|
4991
|
+
let expectedMsg = '';
|
|
4992
|
+
switch (op) {
|
|
4993
|
+
case 'register':
|
|
4994
|
+
expectedMsg = `${clientSecret}:register:${nodeId}:${params.seed || ''}`;
|
|
4995
|
+
break;
|
|
4996
|
+
case 'request_peer_block':
|
|
4997
|
+
expectedMsg = `${clientSecret}:request_peer_block:${nodeId}:${params.peer_id || ''}:${params.block_idx || '0'}:${params.req_id || ''}`;
|
|
4998
|
+
break;
|
|
4999
|
+
case 'poll_requests':
|
|
5000
|
+
expectedMsg = `${clientSecret}:poll_requests:${nodeId}`;
|
|
5001
|
+
break;
|
|
5002
|
+
case 'respond_block':
|
|
5003
|
+
expectedMsg = `${clientSecret}:respond_block:${nodeId}:${params.requester_id || ''}:${params.req_id || ''}:${params.block_data || ''}`;
|
|
5004
|
+
break;
|
|
5005
|
+
case 'poll_response':
|
|
5006
|
+
expectedMsg = `${clientSecret}:poll_response:${nodeId}:${params.req_id || ''}`;
|
|
5007
|
+
break;
|
|
5008
|
+
default:
|
|
5009
|
+
return { error: 'Invalid cooperative operation' };
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
const expectedSig = crypto.createHash('sha256').update(expectedMsg).digest('hex');
|
|
5013
|
+
if (coopSig.length !== expectedSig.length || !crypto.timingSafeEqual(Buffer.from(coopSig, 'hex'), Buffer.from(expectedSig, 'hex'))) {
|
|
5014
|
+
return { error: 'Invalid cooperative signature' };
|
|
5015
|
+
}
|
|
5016
|
+
// --- FIN DE LA VÉRIFICATION ---
|
|
5017
|
+
|
|
4664
5018
|
switch (op) {
|
|
4665
5019
|
case 'register':
|
|
4666
5020
|
const seed = params.seed || '';
|
|
@@ -4773,66 +5127,25 @@ function sanitizeProxyHeaders(context, securityConfig) {
|
|
|
4773
5127
|
|
|
4774
5128
|
/**
|
|
4775
5129
|
* Exécute l'optimisation des TTL en tâche de fond de manière asynchrone et non-bloquante.
|
|
4776
|
-
*
|
|
5130
|
+
* Déporté dans un worker thread dédié pour libérer l'Event Loop principale de Node.js.
|
|
4777
5131
|
*/
|
|
4778
5132
|
export async function runBackgroundTtlOptimization() {
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
// Rend la main à la boucle d'événements Node.js à chaque itération pour ne pas bloquer les requêtes web actives
|
|
4786
|
-
await new Promise(resolve => {
|
|
4787
|
-
if (typeof setImmediate === 'function') {
|
|
4788
|
-
setImmediate(resolve);
|
|
4789
|
-
} else {
|
|
4790
|
-
setTimeout(resolve, 0);
|
|
5133
|
+
return new Promise((resolve, reject) => {
|
|
5134
|
+
const worker = new Worker(new URL('./ttl-optimization.worker.js', import.meta.url));
|
|
5135
|
+
worker.on("message", (tempCache) => {
|
|
5136
|
+
const tempMap = new Map();
|
|
5137
|
+
for (const [key, value] of Object.entries(tempCache)) {
|
|
5138
|
+
tempMap.set(Number(key), value);
|
|
4791
5139
|
}
|
|
5140
|
+
optimizedTtlCache = tempMap;
|
|
5141
|
+
resolve();
|
|
5142
|
+
worker.terminate();
|
|
4792
5143
|
});
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
const mutate = (ttl) => {
|
|
4799
|
-
const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1;
|
|
4800
|
-
return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
|
|
4801
|
-
};
|
|
4802
|
-
|
|
4803
|
-
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
4804
|
-
createIndividual,
|
|
4805
|
-
fitnessFunction,
|
|
4806
|
-
crossover,
|
|
4807
|
-
mutate,
|
|
4808
|
-
{
|
|
4809
|
-
generations: 40,
|
|
4810
|
-
populationSize: 30,
|
|
4811
|
-
}
|
|
4812
|
-
);
|
|
4813
|
-
|
|
4814
|
-
if (!paretoFront || paretoFront.length === 0) {
|
|
4815
|
-
return { solution: null, fitness: Infinity };
|
|
4816
|
-
}
|
|
4817
|
-
|
|
4818
|
-
let bestSolutionInFront;
|
|
4819
|
-
if (suspicionScore < 50) {
|
|
4820
|
-
bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
|
|
4821
|
-
} else {
|
|
4822
|
-
bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
|
|
4823
|
-
}
|
|
4824
|
-
return { solution: bestSolutionInFront, fitness: 0 };
|
|
4825
|
-
};
|
|
4826
|
-
|
|
4827
|
-
const { bestResult } = Optimization.runMultiple(solverFunction, 20);
|
|
4828
|
-
if (bestResult && bestResult.solution && bestResult.solution !== Infinity) {
|
|
4829
|
-
tempCache.set(suspicionScore, Math.round(bestResult.solution));
|
|
4830
|
-
} else {
|
|
4831
|
-
tempCache.set(suspicionScore, Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL));
|
|
4832
|
-
}
|
|
4833
|
-
}
|
|
4834
|
-
|
|
4835
|
-
optimizedTtlCache = tempCache;
|
|
5144
|
+
worker.on("error", (err) => {
|
|
5145
|
+
console.error('[Fingerprint] TTL optimization worker error:', err);
|
|
5146
|
+
reject(err);
|
|
5147
|
+
});
|
|
5148
|
+
});
|
|
4836
5149
|
}
|
|
4837
5150
|
|
|
4838
5151
|
// Lancement de l'optimisation initiale immédiate en arrière-plan
|
|
@@ -5337,7 +5650,7 @@ function getTlsSessionId(context) {
|
|
|
5337
5650
|
}
|
|
5338
5651
|
|
|
5339
5652
|
// --- Proof-of-Work Middleware (The Tollbooth) ---
|
|
5340
|
-
const staticFileCache = new
|
|
5653
|
+
const staticFileCache = new SimpleLRUCache(50);
|
|
5341
5654
|
|
|
5342
5655
|
export const powMiddleware = (securityConfig) => {
|
|
5343
5656
|
const engine = new FingerprintEngine(securityConfig);
|
|
@@ -5369,6 +5682,36 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5369
5682
|
}
|
|
5370
5683
|
|
|
5371
5684
|
return async (req, res, next) => {
|
|
5685
|
+
if (req.query && req.query.fp_handshake) {
|
|
5686
|
+
const clientKeyHex = req.headers['x-client-ephemeral-key'];
|
|
5687
|
+
if (clientKeyHex) {
|
|
5688
|
+
try {
|
|
5689
|
+
const serverECDH = crypto.createECDH('prime256v1');
|
|
5690
|
+
serverECDH.generateKeys();
|
|
5691
|
+
const serverPubKeyHex = serverECDH.getKeys('hex');
|
|
5692
|
+
const sharedSecret = serverECDH.computeSecret(clientKeyHex, 'hex');
|
|
5693
|
+
const hmacKey = crypto.createHash('sha256').update(sharedSecret).digest('hex');
|
|
5694
|
+
|
|
5695
|
+
const requestContext = {
|
|
5696
|
+
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
5697
|
+
headers: req.headers,
|
|
5698
|
+
cookies: req.cookies
|
|
5699
|
+
};
|
|
5700
|
+
const { deviceId, deviceData } = await resolveRequestIdentity(requestContext, securityConfig);
|
|
5701
|
+
if (deviceData) {
|
|
5702
|
+
deviceData.sessionHmacKey = hmacKey;
|
|
5703
|
+
await store.set(`device:${deviceId}`, deviceData);
|
|
5704
|
+
}
|
|
5705
|
+
|
|
5706
|
+
res.setHeader('x-server-ephemeral-key', serverPubKeyHex);
|
|
5707
|
+
return res.status(200).json({ status: 'success' });
|
|
5708
|
+
} catch (e) {
|
|
5709
|
+
return res.status(400).json({ error: 'Handshake failed' });
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
return res.status(400).json({ error: 'Missing client key' });
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5372
5715
|
if (!req.headers_translated) {
|
|
5373
5716
|
req.headers_translated = true;
|
|
5374
5717
|
const matchedMapping = getActiveMappingForRequest(req.headers);
|
|
@@ -5523,7 +5866,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5523
5866
|
* This is a common pattern to allow mocking of ES module functions.
|
|
5524
5867
|
*/
|
|
5525
5868
|
export const __internal = {
|
|
5526
|
-
store, // Export the store for testing
|
|
5869
|
+
get store() { return store; }, // Export the store for testing
|
|
5527
5870
|
getDeviceHash,
|
|
5528
5871
|
getCompositeDeviceHash,
|
|
5529
5872
|
getSuspicionVector,
|
|
@@ -5543,6 +5886,7 @@ export const __internal = {
|
|
|
5543
5886
|
getTlsFingerprint, // NOUVEAU: Expose pour les tests
|
|
5544
5887
|
sanitizeTrafficData, // NOUVEAU: Expose pour l'auto-tuner/tests
|
|
5545
5888
|
getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
|
|
5889
|
+
verifyWebAuthnHardwareAnchor,
|
|
5546
5890
|
generateStatelessTicket,
|
|
5547
5891
|
parseStatelessTicket,
|
|
5548
5892
|
parseJa3,
|
|
@@ -5559,6 +5903,8 @@ export const __internal = {
|
|
|
5559
5903
|
setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
|
|
5560
5904
|
verifyZkpProof,
|
|
5561
5905
|
modPow,
|
|
5906
|
+
generateSessionMapping,
|
|
5907
|
+
compilePolymorphicJs,
|
|
5562
5908
|
parseTcpSyn, // Expose for testing
|
|
5563
5909
|
classifyTcpOs, // Expose for testing
|
|
5564
5910
|
getTcpAnomalyScore, // Expose for testing,
|
|
@@ -5839,6 +6185,43 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
5839
6185
|
// Plus le ratio est équilibré et le volume important, plus nous faisons confiance au Front de Pareto.
|
|
5840
6186
|
const trafficConfidence = Math.min(1.5, Math.max(0.3, highConfidenceRatio * 4));
|
|
5841
6187
|
|
|
6188
|
+
// --- VALIDATION POST-CALCUL (Anti-empoisonnement & Validation Croisée) ---
|
|
6189
|
+
const tempConfig = {
|
|
6190
|
+
thresholds: { ...securityConfig.thresholds },
|
|
6191
|
+
weights: { ...securityConfig.weights },
|
|
6192
|
+
patterns: { ...securityConfig.patterns }
|
|
6193
|
+
};
|
|
6194
|
+
|
|
6195
|
+
applyInertialUpdate(tempConfig.thresholds, newConfig.thresholds, 'thresholds', trafficConfidence);
|
|
6196
|
+
applyInertialUpdate(tempConfig.weights, newConfig.weights, 'weights', trafficConfidence);
|
|
6197
|
+
applyInertialUpdate(tempConfig.patterns, newConfig.patterns, 'patterns', trafficConfidence);
|
|
6198
|
+
|
|
6199
|
+
const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator({ trafficData: sanitizedData });
|
|
6200
|
+
const currentObjectives = fitnessFunction(securityConfig);
|
|
6201
|
+
const proposedObjectives = fitnessFunction(tempConfig);
|
|
6202
|
+
|
|
6203
|
+
const currentFPR = currentObjectives[0];
|
|
6204
|
+
const currentFNR = currentObjectives[1];
|
|
6205
|
+
const proposedFPR = proposedObjectives[0];
|
|
6206
|
+
const proposedFNR = proposedObjectives[1];
|
|
6207
|
+
|
|
6208
|
+
const validationTolerance = securityConfig?.autotuning?.validationTolerance ?? tuningOptions?.validationTolerance ?? 0.15;
|
|
6209
|
+
|
|
6210
|
+
if (proposedFPR > currentFPR + validationTolerance || proposedFNR > currentFNR + validationTolerance) {
|
|
6211
|
+
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)})`);
|
|
6212
|
+
if (securityConfig.logger && typeof securityConfig.logger === 'function') {
|
|
6213
|
+
securityConfig.logger({
|
|
6214
|
+
type: 'autotuning_instability_alert',
|
|
6215
|
+
proposedFPR,
|
|
6216
|
+
currentFPR,
|
|
6217
|
+
proposedFNR,
|
|
6218
|
+
currentFNR,
|
|
6219
|
+
timestamp: Date.now()
|
|
6220
|
+
});
|
|
6221
|
+
}
|
|
6222
|
+
return; // Rollback automatique : On arrête l'application
|
|
6223
|
+
}
|
|
6224
|
+
|
|
5842
6225
|
applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds, 'thresholds', trafficConfidence);
|
|
5843
6226
|
applyInertialUpdate(securityConfig.weights, newConfig.weights, 'weights', trafficConfidence);
|
|
5844
6227
|
applyInertialUpdate(securityConfig.patterns, newConfig.patterns, 'patterns', trafficConfidence);
|