@anonympins/fingerprint 0.3.1 → 0.3.3
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 +172 -0
- package/README.md +298 -37
- package/composer.json +38 -0
- package/index.js +5 -0
- package/package.json +100 -94
- package/phpunit.xml +20 -0
- package/public/fp.js +2 -0
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +69 -0
- package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -171
- package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -483
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/{fingerprint.js → src/js/fingerprint.js} +338 -120
- package/{library.js → src/js/library.js} +1729 -1727
- package/{problem-manager.js → src/js/problem-manager.js} +539 -522
- package/src/php/AutoTuner.php +155 -0
- package/src/php/Challenge/ChallengeUtils.php +306 -0
- package/src/php/Config/SecurityProfiles.php +257 -0
- package/src/php/DirectFingerprint.php +81 -0
- package/src/php/FingerprintBuilder.php +185 -0
- package/src/php/FingerprintClient.php +118 -0
- package/src/php/FingerprintEngine.php +850 -0
- package/src/php/Optimization/FunctionRegistry.php +63 -0
- package/src/php/Optimization/Optimization.php +256 -0
- package/src/php/Optimization/OptimizationOperators.php +305 -0
- package/src/php/Optimization/ProblemInitializers.php +53 -0
- package/src/php/ProblemManager.php +255 -0
- package/src/php/RequestContext.php +87 -0
- package/src/php/Store/IStore.php +42 -0
- package/src/php/Store/InMemoryStore.php +67 -0
- package/src/php/Store/StoreManager.php +26 -0
- package/src/php/Tests/ChallengeUtilsTest.php +82 -0
- package/src/php/Tests/FingerprintBuilderTest.php +58 -0
- package/src/php/Tests/FingerprintEngineTest.php +219 -0
- package/src/php/Tests/PowTest.php +40 -0
- package/src/php/Tests/ProblemManagerTest.php +295 -0
- package/src/php/Tests/RequestUtilsTest.php +81 -0
- package/src/php/Tests/problems.config.json +9 -0
- package/src/php/Utils/BigInt.php +102 -0
- package/src/php/Utils/BlockList.php +100 -0
- package/src/php/Utils/Logger.php +30 -0
- package/src/php/Utils/MaliciousPatterns.php +59 -0
- package/src/php/Utils/RequestUtils.php +673 -0
- package/fingerprint.client.obfuscated.js +0 -1
- /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
- /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
- /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
- /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
- /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
- /package/{redis-store.js → src/js/redis-store.js} +0 -0
- /package/{sql-store.js → src/js/sql-store.js} +0 -0
|
@@ -44,7 +44,7 @@ const securityProfiles = {
|
|
|
44
44
|
headerAnomalyScore: 0.1,
|
|
45
45
|
requestPatternScore: 0.6,
|
|
46
46
|
inconsistencyScore: 0.8,
|
|
47
|
-
behaviorScore: 0.7,
|
|
47
|
+
behaviorScore: 0.7, // Poids pour les métriques comportementales (souris, clavier)
|
|
48
48
|
honeypotScore: 1.0,
|
|
49
49
|
crossLayerInconsistencyScore: 0.4,
|
|
50
50
|
timeInconsistencyScore: 0.9,
|
|
@@ -79,7 +79,7 @@ const securityProfiles = {
|
|
|
79
79
|
honeypotScore: 1.0,
|
|
80
80
|
crossLayerInconsistencyScore: 0.6,
|
|
81
81
|
timeInconsistencyScore: 1.0,
|
|
82
|
-
tlsSpoofingScore: 1.0 //
|
|
82
|
+
tlsSpoofingScore: 1.0 // Plus agressif pour le spoofing TLS
|
|
83
83
|
},
|
|
84
84
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
85
85
|
patterns: {
|
|
@@ -111,7 +111,7 @@ const securityProfiles = {
|
|
|
111
111
|
honeypotScore: 1.0,
|
|
112
112
|
crossLayerInconsistencyScore: 0.5,
|
|
113
113
|
timeInconsistencyScore: 0.8,
|
|
114
|
-
tlsSpoofingScore: 0.7 //
|
|
114
|
+
tlsSpoofingScore: 0.7 // Important pour les API
|
|
115
115
|
},
|
|
116
116
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
117
117
|
patterns: {
|
|
@@ -144,7 +144,7 @@ const securityProfiles = {
|
|
|
144
144
|
honeypotScore: 1.0, // Crucial for comment spam
|
|
145
145
|
crossLayerInconsistencyScore: 0.4,
|
|
146
146
|
timeInconsistencyScore: 0.8,
|
|
147
|
-
tlsSpoofingScore: 0.6 //
|
|
147
|
+
tlsSpoofingScore: 0.6 // Moins critique pour les blogs
|
|
148
148
|
},
|
|
149
149
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
150
150
|
patterns: {
|
|
@@ -169,17 +169,14 @@ const securityProfiles = {
|
|
|
169
169
|
historyScore: 0.4,
|
|
170
170
|
rotationScore: 0.6,
|
|
171
171
|
headerAnomalyScore: 0.2,
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
burstScore: 1.0, // Pénalise fortement les rafales sur la même ressource (scalping)
|
|
175
|
-
scrapeScore: 0.9, // Pénalise le parcours de pages/produits
|
|
176
|
-
regularityScore: 0.7, // Détecte les bots de type "cron"
|
|
172
|
+
// Utilisation d'un score de pattern unifié avec un poids très élevé
|
|
173
|
+
requestPatternScore: 0.9,
|
|
177
174
|
inconsistencyScore: 1.0, // Crucial for preventing account takeover
|
|
178
175
|
behaviorScore: 0.8, // Important for checkout/login forms
|
|
179
176
|
honeypotScore: 1.0,
|
|
180
177
|
crossLayerInconsistencyScore: 0.7,
|
|
181
178
|
timeInconsistencyScore: 0.9,
|
|
182
|
-
tlsSpoofingScore: 0.9 //
|
|
179
|
+
tlsSpoofingScore: 0.9 // Très important pour l'e-commerce
|
|
183
180
|
},
|
|
184
181
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
185
182
|
patterns: {
|
|
@@ -235,6 +232,38 @@ const getPowSolverCode = () => {
|
|
|
235
232
|
return readFileSync(solverPath, 'utf-8');
|
|
236
233
|
};
|
|
237
234
|
|
|
235
|
+
/**
|
|
236
|
+
* @private
|
|
237
|
+
* A mapping of IANA cipher suite names (as used by Node.js) to their decimal IDs.
|
|
238
|
+
* This is essential for correct JA3 fingerprint calculation.
|
|
239
|
+
* The list is not exhaustive but covers the most common cipher suites.
|
|
240
|
+
*/
|
|
241
|
+
const cipherSuiteMap = {
|
|
242
|
+
'TLS_AES_128_GCM_SHA256': 4865,
|
|
243
|
+
'TLS_AES_256_GCM_SHA384': 4866,
|
|
244
|
+
'TLS_CHACHA20_POLY1305_SHA256': 4867,
|
|
245
|
+
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256': 49195,
|
|
246
|
+
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256': 49199,
|
|
247
|
+
'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384': 49196,
|
|
248
|
+
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384': 49200,
|
|
249
|
+
'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256': 52393,
|
|
250
|
+
'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256': 52392,
|
|
251
|
+
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 49171,
|
|
252
|
+
'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 49172,
|
|
253
|
+
'TLS_RSA_WITH_AES_128_GCM_SHA256': 156,
|
|
254
|
+
'TLS_RSA_WITH_AES_256_GCM_SHA384': 157,
|
|
255
|
+
'TLS_RSA_WITH_AES_128_CBC_SHA': 47,
|
|
256
|
+
'TLS_RSA_WITH_AES_256_CBC_SHA': 53,
|
|
257
|
+
// Older/Less common suites
|
|
258
|
+
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA': 49161,
|
|
259
|
+
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA': 49162,
|
|
260
|
+
'TLS_DHE_RSA_WITH_AES_128_GCM_SHA256': 158,
|
|
261
|
+
'TLS_DHE_RSA_WITH_AES_256_GCM_SHA384': 159,
|
|
262
|
+
'TLS_DHE_RSA_WITH_AES_128_CBC_SHA': 51,
|
|
263
|
+
'TLS_DHE_RSA_WITH_AES_256_CBC_SHA': 57,
|
|
264
|
+
'TLS_RSA_WITH_3DES_EDE_CBC_SHA': 10,
|
|
265
|
+
};
|
|
266
|
+
|
|
238
267
|
/**
|
|
239
268
|
* Extracts TLS fingerprints (JA3 and JA4) from request context.
|
|
240
269
|
* Prioritizes headers from reverse proxies (x-ja4-hash) and falls back to JA3 calculation
|
|
@@ -265,15 +294,19 @@ function getTlsFingerprint(context) {
|
|
|
265
294
|
|
|
266
295
|
// The official JA3 spec includes the TLS version.
|
|
267
296
|
// Node.js provides it as a string like 'TLSv1.3', we need the corresponding decimal value.
|
|
268
|
-
const tlsVersionMap = {
|
|
297
|
+
const tlsVersionMap = { // NOSONAR
|
|
269
298
|
'TLSv1': 769, 'TLSv1.1': 770, 'TLSv1.2': 771, 'TLSv1.3': 772
|
|
270
299
|
};
|
|
271
300
|
const tlsVersionId = tlsVersionMap[version] || 0;
|
|
272
301
|
|
|
302
|
+
// Convert cipher suite names to their decimal IDs.
|
|
303
|
+
const cipherIds = Array.isArray(ciphers)
|
|
304
|
+
? ciphers.map(c => cipherSuiteMap[c.name] || c).join('-') // Use the raw ID if name is not in map
|
|
305
|
+
: '';
|
|
306
|
+
|
|
273
307
|
const ja3String = [
|
|
274
308
|
tlsVersionId,
|
|
275
|
-
|
|
276
|
-
Array.isArray(ciphers) ? ciphers.join('-') : '',
|
|
309
|
+
cipherIds,
|
|
277
310
|
extensions?.join('-') || '',
|
|
278
311
|
ellipticCurves?.join('-') || '',
|
|
279
312
|
ellipticCurvePointFormats?.join('-') || ''
|
|
@@ -621,47 +654,6 @@ export const verifyTspChallenge = (
|
|
|
621
654
|
}
|
|
622
655
|
};
|
|
623
656
|
|
|
624
|
-
/**
|
|
625
|
-
* Generates the HTML content for the CPU PoW challenge (SHA-256).
|
|
626
|
-
*/
|
|
627
|
-
const generateCpuPoWChallenge = (
|
|
628
|
-
clientIp,
|
|
629
|
-
nonce,
|
|
630
|
-
difficulty = 4,
|
|
631
|
-
path = "",
|
|
632
|
-
) => {
|
|
633
|
-
return `
|
|
634
|
-
<html>
|
|
635
|
-
<head><title>Security Check</title></head>
|
|
636
|
-
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
637
|
-
<h1>One moment... (Level 1)</h1>
|
|
638
|
-
<p>We are verifying that you are not a bot. This takes a few seconds.</p>
|
|
639
|
-
<div id="loader" style="margin:20px;">⚙️ Performing CPU security calculation...</div>
|
|
640
|
-
<script>
|
|
641
|
-
async function solve() {
|
|
642
|
-
const ip = "${clientIp}";
|
|
643
|
-
const nonce = "${nonce}";
|
|
644
|
-
const diff = ${difficulty};
|
|
645
|
-
const target = "0".repeat(diff);
|
|
646
|
-
let solution = 0;
|
|
647
|
-
|
|
648
|
-
while (true) {
|
|
649
|
-
const msg = "${ip}" + ":" + "${nonce}" + ":" + solution;
|
|
650
|
-
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
651
|
-
const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
652
|
-
if (hash.startsWith(target)) break;
|
|
653
|
-
solution++;
|
|
654
|
-
if (solution % 100000 === 0) await new Promise(resolve => setTimeout(resolve, 0)); // To avoid freezing the browser
|
|
655
|
-
}
|
|
656
|
-
window.location.href = "${path}" + "?pow_type=cpu&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
657
|
-
}
|
|
658
|
-
solve();
|
|
659
|
-
</script>
|
|
660
|
-
</body>
|
|
661
|
-
</html>
|
|
662
|
-
`;
|
|
663
|
-
};
|
|
664
|
-
|
|
665
657
|
/**
|
|
666
658
|
* Generates the HTML content for a memory-intensive PoW challenge.
|
|
667
659
|
*/
|
|
@@ -906,7 +898,7 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
906
898
|
*/
|
|
907
899
|
const injectionPatterns = {
|
|
908
900
|
// SQL/NoSQL injections, including time-based attacks
|
|
909
|
-
sql: /(\$ne
|
|
901
|
+
sql: /(\$ne|\' *OR *\'1\'=\'1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|(?:SLEEP|BENCHMARK)\s*\(|WAITFOR DELAY)/i,
|
|
910
902
|
// Log4Shell (JNDI injection)
|
|
911
903
|
log4shell: /\$\{jndi:(ldap|rmi|dns):/i,
|
|
912
904
|
// Server-Side Template Injection (SSTI) for engines like Jinja2, Twig, etc.
|
|
@@ -919,6 +911,68 @@ const injectionPatterns = {
|
|
|
919
911
|
rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
|
|
920
912
|
};
|
|
921
913
|
|
|
914
|
+
/**
|
|
915
|
+
* @private
|
|
916
|
+
* Analyse une série de mouvements de souris pour en extraire des métriques comportementales.
|
|
917
|
+
* @param {Array<{x: number, y: number, t: number}>} history - L'historique des points de la souris.
|
|
918
|
+
* @returns {{avgSpeed: number, avgAcceleration: number, straightness: number, pauses: number, segments: Array<number>}}
|
|
919
|
+
*/
|
|
920
|
+
function analyzeMouseMovements(history) {
|
|
921
|
+
if (!history || history.length < 3) {
|
|
922
|
+
return { avgSpeed: 0, avgAcceleration: 0, straightness: 1, pauses: 0, segments: [] };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const segments = [];
|
|
926
|
+
let totalDistance = 0;
|
|
927
|
+
let pauses = 0;
|
|
928
|
+
|
|
929
|
+
for (let i = 1; i < history.length; i++) {
|
|
930
|
+
const p1 = history[i - 1];
|
|
931
|
+
const p2 = history[i];
|
|
932
|
+
const dx = p2.x - p1.x;
|
|
933
|
+
const dy = p2.y - p1.y;
|
|
934
|
+
const dt = p2.t - p1.t;
|
|
935
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
936
|
+
|
|
937
|
+
if (dt > 0) {
|
|
938
|
+
const speed = distance / dt;
|
|
939
|
+
segments.push({ distance, dt, speed });
|
|
940
|
+
totalDistance += distance;
|
|
941
|
+
}
|
|
942
|
+
// Une "micro-pause" est un intervalle de temps long sans mouvement significatif.
|
|
943
|
+
if (dt > 100 && distance < 5) {
|
|
944
|
+
pauses++;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (segments.length < 2) {
|
|
949
|
+
return { avgSpeed: 0, avgAcceleration: 0, straightness: 1, pauses, segments: [] };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const totalTime = history[history.length - 1].t - history[0].t;
|
|
953
|
+
const avgSpeed = totalTime > 0 ? segments.reduce((sum, s) => sum + s.speed, 0) / segments.length : 0;
|
|
954
|
+
|
|
955
|
+
let totalAbsAcceleration = 0;
|
|
956
|
+
for (let i = 1; i < segments.length; i++) {
|
|
957
|
+
const s1 = segments[i - 1];
|
|
958
|
+
const s2 = segments[i];
|
|
959
|
+
if (s2.dt > 0) {
|
|
960
|
+
const acceleration = (s2.speed - s1.speed) / s2.dt;
|
|
961
|
+
totalAbsAcceleration += Math.abs(acceleration);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const avgAcceleration = totalAbsAcceleration / (segments.length - 1);
|
|
965
|
+
|
|
966
|
+
// Le score de rectitude compare la distance totale parcourue à la distance en ligne droite.
|
|
967
|
+
// Un score proche de 1 signifie un mouvement très droit (suspect).
|
|
968
|
+
const startPoint = history[0];
|
|
969
|
+
const endPoint = history[history.length - 1];
|
|
970
|
+
const straightDistance = Math.sqrt(Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2));
|
|
971
|
+
const straightness = totalDistance > 0 ? straightDistance / totalDistance : 1;
|
|
972
|
+
|
|
973
|
+
return { avgSpeed, avgAcceleration, straightness, pauses, segments: segments.map(s => s.distance) };
|
|
974
|
+
}
|
|
975
|
+
|
|
922
976
|
/**
|
|
923
977
|
* Calcule un score basé sur les métriques comportementales envoyées par le client.
|
|
924
978
|
* @param {object} context - Le contexte de la requête, contenant les en-têtes.
|
|
@@ -939,8 +993,11 @@ function getBehaviorScore(context) {
|
|
|
939
993
|
return { behaviorScore: 100 };
|
|
940
994
|
}
|
|
941
995
|
|
|
942
|
-
// 2.
|
|
943
|
-
|
|
996
|
+
// 2. Analyse des mouvements de la souris
|
|
997
|
+
const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
|
|
998
|
+
|
|
999
|
+
// Pénalité pour absence totale d'interaction (pas de mouvements, pas de frappes).
|
|
1000
|
+
if (avgSpeed === 0 && metrics.keystrokeLatency === 0) {
|
|
944
1001
|
score += 40;
|
|
945
1002
|
}
|
|
946
1003
|
|
|
@@ -955,35 +1012,28 @@ function getBehaviorScore(context) {
|
|
|
955
1012
|
score -= 10; // Petit bonus pour une navigation de base.
|
|
956
1013
|
}
|
|
957
1014
|
}
|
|
958
|
-
// 3. Vérification de la plausibilité et de la distribution des métriques.
|
|
959
|
-
// Un bot pourrait envoyer des valeurs aléatoires, mais elles ne suivront probablement pas
|
|
960
|
-
// des distributions naturelles (comme la loi de Benford pour les premiers chiffres).
|
|
961
1015
|
|
|
962
|
-
//
|
|
963
|
-
if (
|
|
964
|
-
|
|
1016
|
+
// 3. Analyse des métriques de la souris
|
|
1017
|
+
if (avgSpeed > 0) {
|
|
1018
|
+
if (avgSpeed > 3) score += 25; // Vitesse irréaliste (3 pixels/ms)
|
|
1019
|
+
if (avgAcceleration > 0.5) score += 20; // Accélération trop brutale
|
|
1020
|
+
if (straightness > 0.95) score += 30; // Mouvement trop droit
|
|
1021
|
+
if (pauses === 0 && segments.length > 20) score += 15; // Mouvement continu sans micro-pauses
|
|
1022
|
+
}
|
|
965
1023
|
|
|
966
1024
|
// Plausibilité de la latence de frappe
|
|
967
1025
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
968
1026
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
969
1027
|
|
|
970
1028
|
// 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
const mouseDeviation = Optimization.Operators.benfordTest(mouseEntropyStr);
|
|
977
|
-
// Une déviation > 0.15 est fortement suspecte.
|
|
978
|
-
if (mouseDeviation > 0.15) score += 40;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
if (keystrokeLatencyStr.length > 2) {
|
|
982
|
-
const keystrokeDeviation = Optimization.Operators.benfordTest(keystrokeLatencyStr);
|
|
983
|
-
if (keystrokeDeviation > 0.15) score += 40;
|
|
1029
|
+
if (segments.length > 10) {
|
|
1030
|
+
const benfordDeviation = Optimization.Operators.benfordTest(segments);
|
|
1031
|
+
if (benfordDeviation > 0.18) { // Seuil légèrement plus élevé pour cette métrique
|
|
1032
|
+
score += 35;
|
|
1033
|
+
}
|
|
984
1034
|
}
|
|
985
1035
|
|
|
986
|
-
return { behaviorScore: Math.
|
|
1036
|
+
return { behaviorScore: Math.min(100, score) }; // Assure que le score ne dépasse pas 100, mais peut être négatif (bonus)
|
|
987
1037
|
} catch (e) {
|
|
988
1038
|
return { behaviorScore: 10 }; // En-tête malformé = légèrement suspect.
|
|
989
1039
|
}
|
|
@@ -1116,6 +1166,64 @@ function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerprint) {
|
|
|
1116
1166
|
return { tlsSpoofingScore: 0 };
|
|
1117
1167
|
}
|
|
1118
1168
|
|
|
1169
|
+
/**
|
|
1170
|
+
* @private
|
|
1171
|
+
* Analyzes click positions from client-side metrics to detect unnaturally low variance,
|
|
1172
|
+
* which can be a sign of automated clicking.
|
|
1173
|
+
* @param {Array<{x: number, y: number, targetId: string}>|null} history - The click history from the client.
|
|
1174
|
+
* @returns {number} A score from 0 to 100, where a higher score indicates lower variance (more bot-like).
|
|
1175
|
+
*/
|
|
1176
|
+
function analyzeClickPositions(history) {
|
|
1177
|
+
if (!history || history.length < 3) {
|
|
1178
|
+
return 0;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const clicksByTarget = {};
|
|
1182
|
+
for (const click of history) {
|
|
1183
|
+
if (!click.targetId) continue;
|
|
1184
|
+
if (!clicksByTarget[click.targetId]) {
|
|
1185
|
+
clicksByTarget[click.targetId] = [];
|
|
1186
|
+
}
|
|
1187
|
+
clicksByTarget[click.targetId].push(click);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
let maxScore = 0;
|
|
1191
|
+
|
|
1192
|
+
for (const targetId in clicksByTarget) {
|
|
1193
|
+
const clicks = clicksByTarget[targetId];
|
|
1194
|
+
if (clicks.length < 3) continue;
|
|
1195
|
+
|
|
1196
|
+
const n = clicks.length;
|
|
1197
|
+
const meanX = clicks.reduce((sum, c) => sum + c.x, 0) / n;
|
|
1198
|
+
const meanY = clicks.reduce((sum, c) => sum + c.y, 0) / n;
|
|
1199
|
+
|
|
1200
|
+
const variance = clicks.reduce((sum, c) => sum + Math.pow(c.x - meanX, 2) + Math.pow(c.y - meanY, 2), 0) / n;
|
|
1201
|
+
|
|
1202
|
+
// If variance is extremely low (e.g., less than 1 pixel), it's highly suspicious.
|
|
1203
|
+
// The score increases as variance approaches zero.
|
|
1204
|
+
if (variance < 1.0) {
|
|
1205
|
+
// A simple scoring model: score is 100 if variance is 0, and decreases.
|
|
1206
|
+
const score = (1 - Math.sqrt(variance) / 5) * 100;
|
|
1207
|
+
if (score > maxScore) {
|
|
1208
|
+
maxScore = score;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return Math.min(100, maxScore);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Calculates a score based on click variance metrics sent by the client.
|
|
1218
|
+
* @param {object} context - The request context.
|
|
1219
|
+
* @returns {{clickVarianceScore: number}}
|
|
1220
|
+
*/
|
|
1221
|
+
function getClickVarianceScore(context) {
|
|
1222
|
+
const metrics = JSON.parse(context.headers['x-behavior-metrics'] || '{}');
|
|
1223
|
+
const score = analyzeClickPositions(metrics.clicksHistory);
|
|
1224
|
+
return { clickVarianceScore: score };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1119
1227
|
|
|
1120
1228
|
/**
|
|
1121
1229
|
* Calcule un score basé sur la détection explicite de frameworks d'automatisation.
|
|
@@ -1475,10 +1583,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1475
1583
|
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
1476
1584
|
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
1477
1585
|
|
|
1478
|
-
// NOUVEAU: On calcule le score de spoofing TLS.
|
|
1479
1586
|
const { tlsSpoofingScore } = getTlsSpoofingScore(context);
|
|
1480
1587
|
|
|
1481
|
-
// NOUVEAU: On appelle getBotScore pour détecter les marqueurs d'automatisation.
|
|
1482
1588
|
const { botScore } = getBotScore(context);
|
|
1483
1589
|
|
|
1484
1590
|
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
@@ -1487,6 +1593,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1487
1593
|
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
1488
1594
|
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
1489
1595
|
|
|
1596
|
+
// NOUVEAU: On calcule le score de variance des clics.
|
|
1597
|
+
const { clickVarianceScore } = getClickVarianceScore(context);
|
|
1598
|
+
|
|
1490
1599
|
const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
|
|
1491
1600
|
|
|
1492
1601
|
// Save the updated device state to the store
|
|
@@ -1499,7 +1608,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1499
1608
|
deviceData.ips = new Set(deviceData.ips);
|
|
1500
1609
|
}
|
|
1501
1610
|
// Le vecteur de suspicion est maintenant complet.
|
|
1502
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore };
|
|
1611
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore: clickVarianceScore };
|
|
1503
1612
|
};
|
|
1504
1613
|
|
|
1505
1614
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -1662,7 +1771,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1662
1771
|
* @param {string} clientIp - The client's IP address.
|
|
1663
1772
|
* @returns {string} HTML content.
|
|
1664
1773
|
*/
|
|
1665
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig,
|
|
1774
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
|
|
1666
1775
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
1667
1776
|
const solverCode = getPowSolverCode();
|
|
1668
1777
|
// On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
|
|
@@ -1671,6 +1780,13 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1671
1780
|
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
|
|
1672
1781
|
const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
|
|
1673
1782
|
|
|
1783
|
+
// Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
|
|
1784
|
+
const clientInitConfig = {
|
|
1785
|
+
mouse: true,
|
|
1786
|
+
keystrokes: true,
|
|
1787
|
+
trapUrls: trapUrls // On passe directement le tableau d'URL
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1674
1790
|
const challengeScript = `
|
|
1675
1791
|
async function solve() {
|
|
1676
1792
|
const nonce = ${JSON.stringify(nonce)};
|
|
@@ -1702,6 +1818,12 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1702
1818
|
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1703
1819
|
window.location.href = finalUrl;
|
|
1704
1820
|
}
|
|
1821
|
+
|
|
1822
|
+
// Initialise la bibliothèque client avec les URL pièges
|
|
1823
|
+
// On crée un alias pour un appel plus propre, tout en s'assurant que la bibliothèque est chargée.
|
|
1824
|
+
const initializeClient = window.ClientLibrary?.initializeClient;
|
|
1825
|
+
if (initializeClient) initializeClient(${JSON.stringify(clientInitConfig)});
|
|
1826
|
+
|
|
1705
1827
|
solve();
|
|
1706
1828
|
`;
|
|
1707
1829
|
|
|
@@ -1717,13 +1839,12 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1717
1839
|
}
|
|
1718
1840
|
|
|
1719
1841
|
if (!htmlTemplate) {
|
|
1720
|
-
htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script
|
|
1842
|
+
htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script></body></html>`; // eslint-disable-line max-len
|
|
1721
1843
|
}
|
|
1722
1844
|
|
|
1723
1845
|
return htmlTemplate
|
|
1724
1846
|
.replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
|
|
1725
|
-
.replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript)
|
|
1726
|
-
.replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
|
|
1847
|
+
.replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
|
|
1727
1848
|
}
|
|
1728
1849
|
|
|
1729
1850
|
/**
|
|
@@ -1825,6 +1946,7 @@ export class FingerprintEngine {
|
|
|
1825
1946
|
this._allowlist = this._buildAllowlist();
|
|
1826
1947
|
this._validateConfig(securityConfig); // Validate the configuration
|
|
1827
1948
|
this.verbose = securityConfig.verbose || false;
|
|
1949
|
+
this.dryRun = securityConfig.dryRun || false;
|
|
1828
1950
|
}
|
|
1829
1951
|
|
|
1830
1952
|
/**
|
|
@@ -1842,7 +1964,7 @@ export class FingerprintEngine {
|
|
|
1842
1964
|
'weights', 'thresholds', 'cpu', 'ticketMaxAge', 'challengeTtl',
|
|
1843
1965
|
'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
|
|
1844
1966
|
'honeypot', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger',
|
|
1845
|
-
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist',
|
|
1967
|
+
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
1846
1968
|
'similarityThreshold'
|
|
1847
1969
|
]);
|
|
1848
1970
|
|
|
@@ -1882,7 +2004,8 @@ export class FingerprintEngine {
|
|
|
1882
2004
|
(suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
|
|
1883
2005
|
(suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
|
|
1884
2006
|
(suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
|
|
1885
|
-
(suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0)
|
|
2007
|
+
(suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
|
|
2008
|
+
(suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0);
|
|
1886
2009
|
|
|
1887
2010
|
return Math.min(100, score);
|
|
1888
2011
|
}
|
|
@@ -2203,7 +2326,15 @@ export class FingerprintEngine {
|
|
|
2203
2326
|
if (onDeviceCompromised) {
|
|
2204
2327
|
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
2205
2328
|
}
|
|
2206
|
-
|
|
2329
|
+
const decision = { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
2330
|
+
if (this.dryRun) {
|
|
2331
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
2332
|
+
decision.intendedAction = decision.action;
|
|
2333
|
+
decision.action = 'next';
|
|
2334
|
+
delete decision.status;
|
|
2335
|
+
delete decision.body;
|
|
2336
|
+
}
|
|
2337
|
+
return decision;
|
|
2207
2338
|
}
|
|
2208
2339
|
|
|
2209
2340
|
// The engine now works with the context directly, no more rawReq dependency here.
|
|
@@ -2404,7 +2535,15 @@ export class FingerprintEngine {
|
|
|
2404
2535
|
const newBlockThreshold = thresholds.block ?? 95;
|
|
2405
2536
|
if (finalScore >= newBlockThreshold) {
|
|
2406
2537
|
this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
|
|
2407
|
-
|
|
2538
|
+
const decision = { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
2539
|
+
if (this.dryRun) {
|
|
2540
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
2541
|
+
decision.intendedAction = decision.action;
|
|
2542
|
+
decision.action = 'next';
|
|
2543
|
+
delete decision.status;
|
|
2544
|
+
delete decision.body;
|
|
2545
|
+
}
|
|
2546
|
+
return decision;
|
|
2408
2547
|
}
|
|
2409
2548
|
// If not blocked, the request will proceed to be re-challenged.
|
|
2410
2549
|
}
|
|
@@ -2474,7 +2613,15 @@ export class FingerprintEngine {
|
|
|
2474
2613
|
if (logger) {
|
|
2475
2614
|
logger({ type: 'request_blocked', deviceId: deviceId, score: finalScore, vector: suspicionVector, timestamp: Date.now() });
|
|
2476
2615
|
}
|
|
2477
|
-
|
|
2616
|
+
const decision = { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
2617
|
+
if (this.dryRun) {
|
|
2618
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
2619
|
+
decision.intendedAction = decision.action;
|
|
2620
|
+
decision.action = 'next';
|
|
2621
|
+
delete decision.status;
|
|
2622
|
+
delete decision.body;
|
|
2623
|
+
}
|
|
2624
|
+
return decision;
|
|
2478
2625
|
}
|
|
2479
2626
|
|
|
2480
2627
|
// Honeypot: Check if the request is for a trap URL generated in a previous challenge.
|
|
@@ -2490,7 +2637,15 @@ export class FingerprintEngine {
|
|
|
2490
2637
|
logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now(), vector: { honeypotScore: 100 } });
|
|
2491
2638
|
}
|
|
2492
2639
|
await store.set(`device:${cookies.device_id}`, deviceData); // No TTL for condemned status
|
|
2493
|
-
|
|
2640
|
+
const decision = { action: 'block', status: 404, score: 100, vector: { honeypotScore: 100 } };
|
|
2641
|
+
if (this.dryRun) {
|
|
2642
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
2643
|
+
decision.intendedAction = decision.action;
|
|
2644
|
+
decision.action = 'next';
|
|
2645
|
+
delete decision.status;
|
|
2646
|
+
delete decision.body;
|
|
2647
|
+
}
|
|
2648
|
+
return decision;
|
|
2494
2649
|
}
|
|
2495
2650
|
|
|
2496
2651
|
// --- NOUVELLE LOGIQUE DE RE-CHALLENGE ---
|
|
@@ -2519,7 +2674,15 @@ export class FingerprintEngine {
|
|
|
2519
2674
|
suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
|
|
2520
2675
|
// Recalculate the final score with the updated vector.
|
|
2521
2676
|
const newFinalScore = this.calculateFinalScore(suspicionVector);
|
|
2522
|
-
|
|
2677
|
+
const decision = { action: 'block', status: 404, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
|
|
2678
|
+
if (this.dryRun) {
|
|
2679
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
2680
|
+
decision.intendedAction = decision.action;
|
|
2681
|
+
decision.action = 'next';
|
|
2682
|
+
delete decision.status;
|
|
2683
|
+
delete decision.body;
|
|
2684
|
+
}
|
|
2685
|
+
return decision;
|
|
2523
2686
|
}
|
|
2524
2687
|
|
|
2525
2688
|
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
@@ -2547,11 +2710,18 @@ export class FingerprintEngine {
|
|
|
2547
2710
|
}
|
|
2548
2711
|
};
|
|
2549
2712
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
2550
|
-
|
|
2713
|
+
} else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi
|
|
2714
|
+
const decision = { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404 };
|
|
2715
|
+
if (this.dryRun) {
|
|
2716
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
2717
|
+
decision.intendedAction = decision.action;
|
|
2718
|
+
decision.action = 'next';
|
|
2719
|
+
delete decision.status;
|
|
2720
|
+
return decision;
|
|
2721
|
+
}
|
|
2551
2722
|
// Generate some trap URLs to embed in the challenge page.
|
|
2552
2723
|
// These links are visually hidden but present in the DOM to trap bots.
|
|
2553
|
-
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
2554
|
-
const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
|
|
2724
|
+
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce)); // Génère les URL
|
|
2555
2725
|
|
|
2556
2726
|
// On passe la configuration pour que la difficulté soit calculée correctement.
|
|
2557
2727
|
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
@@ -2617,16 +2787,17 @@ export class FingerprintEngine {
|
|
|
2617
2787
|
}
|
|
2618
2788
|
};
|
|
2619
2789
|
this._log('API challenge response generated', { challengePayload });
|
|
2620
|
-
|
|
2790
|
+
decision.body = challengePayload;
|
|
2621
2791
|
} else {
|
|
2622
2792
|
// For browsers, send the HTML page.
|
|
2623
|
-
const
|
|
2624
|
-
this._log('Browser challenge page generated', {
|
|
2625
|
-
pageLength: page.length,
|
|
2626
|
-
|
|
2793
|
+
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapUrls, originalFingerprint);
|
|
2794
|
+
this._log('Browser challenge page generated', {
|
|
2795
|
+
pageLength: page.length,
|
|
2796
|
+
trapUrlsInjected: trapUrls.length
|
|
2627
2797
|
});
|
|
2628
|
-
|
|
2798
|
+
decision.body = page;
|
|
2629
2799
|
}
|
|
2800
|
+
return decision;
|
|
2630
2801
|
}
|
|
2631
2802
|
}
|
|
2632
2803
|
|
|
@@ -2637,7 +2808,7 @@ export class FingerprintEngine {
|
|
|
2637
2808
|
logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
|
|
2638
2809
|
}
|
|
2639
2810
|
|
|
2640
|
-
return { action: 'next', score: finalScore, vector: suspicionVector };
|
|
2811
|
+
return { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
|
|
2641
2812
|
}
|
|
2642
2813
|
|
|
2643
2814
|
/**
|
|
@@ -2982,8 +3153,11 @@ export const powMiddleware = (securityConfig) => {
|
|
|
2982
3153
|
const engine = new FingerprintEngine(securityConfig);
|
|
2983
3154
|
|
|
2984
3155
|
// Initialize the problem manager with the configured path, if provided.
|
|
2985
|
-
if (securityConfig.enableUsefulWork
|
|
2986
|
-
getProblemManager(
|
|
3156
|
+
if (securityConfig.enableUsefulWork) {
|
|
3157
|
+
getProblemManager({
|
|
3158
|
+
configPath: securityConfig.usefulWorkConfigPath,
|
|
3159
|
+
config: securityConfig.usefulWorkConfig
|
|
3160
|
+
}, store);
|
|
2987
3161
|
}
|
|
2988
3162
|
|
|
2989
3163
|
if (securityConfig.autotuning) {
|
|
@@ -3033,6 +3207,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
3033
3207
|
req.fingerprint = {
|
|
3034
3208
|
score: decision.score,
|
|
3035
3209
|
vector: decision.vector,
|
|
3210
|
+
intendedAction: decision.intendedAction, // Add intended action for logging
|
|
3036
3211
|
};
|
|
3037
3212
|
|
|
3038
3213
|
// After getSuspicionVector runs, it might have attached cookies to be set.
|
|
@@ -3083,6 +3258,7 @@ export const __internal = {
|
|
|
3083
3258
|
getCrossLayerInconsistency, // Expose for testing
|
|
3084
3259
|
// Expose page generators for security testing
|
|
3085
3260
|
getTimeInconsistencyScore,
|
|
3261
|
+
getClickVarianceScore, // NOUVEAU: Expose pour les tests
|
|
3086
3262
|
getTlsFingerprint, // NOUVEAU: Expose pour les tests
|
|
3087
3263
|
getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
|
|
3088
3264
|
generateCpuTargetChallengePage,
|
|
@@ -3093,6 +3269,7 @@ export const __internal = {
|
|
|
3093
3269
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
3094
3270
|
|
|
3095
3271
|
let autoTuningJobId = null;
|
|
3272
|
+
let lastBestSolution = null; // NOUVEAU: Stocke la meilleure solution trouvée
|
|
3096
3273
|
|
|
3097
3274
|
/**
|
|
3098
3275
|
* Executes a threshold optimization pass using collected traffic data.
|
|
@@ -3101,8 +3278,9 @@ let autoTuningJobId = null;
|
|
|
3101
3278
|
* @param {Array<object>} trafficData - The array containing traffic logs.
|
|
3102
3279
|
* @param {number} minDataPoints - The minimum number of data points required to start optimization.
|
|
3103
3280
|
* @param {number} maxDataPoints - The maximum number of data points to keep after an optimization cycle.
|
|
3281
|
+
* @param {string} [savePath] - Optional path to save the best configuration to a file.
|
|
3104
3282
|
*/
|
|
3105
|
-
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
|
|
3283
|
+
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
|
|
3106
3284
|
const highConfidenceLogs = trafficData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
|
|
3107
3285
|
const highConfidenceRatio = trafficData.length > 0 ? highConfidenceLogs / trafficData.length : 0;
|
|
3108
3286
|
const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
|
|
@@ -3151,35 +3329,63 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
3151
3329
|
|
|
3152
3330
|
/**
|
|
3153
3331
|
* Met à jour un objet de configuration (ex: thresholds, weights) en douceur.
|
|
3332
|
+
* Cette nouvelle version préserve la proportionnalité des valeurs initiales.
|
|
3154
3333
|
* @param {object} currentConfig - La configuration actuelle à modifier.
|
|
3155
3334
|
* @param {object} targetConfig - La configuration cible proposée par l'optimiseur.
|
|
3156
3335
|
*/
|
|
3157
3336
|
const applyInertialUpdate = (currentConfig, targetConfig) => {
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
currentConfig[key] += change;
|
|
3170
|
-
}
|
|
3337
|
+
if (!currentConfig || !targetConfig) return; // Vérifier aussi currentConfig
|
|
3338
|
+
|
|
3339
|
+
// --- NOUVELLE LOGIQUE PROPORTIONNELLE ---
|
|
3340
|
+
let totalCurrentWeight = 0;
|
|
3341
|
+
let totalTargetWeight = 0;
|
|
3342
|
+
|
|
3343
|
+
// 1. Calculer la somme des poids actuels et cibles pour les clés communes.
|
|
3344
|
+
for (const key in currentConfig) {
|
|
3345
|
+
if (Object.hasOwnProperty.call(targetConfig, key)) {
|
|
3346
|
+
totalCurrentWeight += currentConfig[key];
|
|
3347
|
+
totalTargetWeight += targetConfig[key];
|
|
3171
3348
|
}
|
|
3172
|
-
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
if (totalCurrentWeight === 0) return; // Éviter la division par zéro
|
|
3352
|
+
|
|
3353
|
+
// 2. Déterminer le ratio de changement global et le limiter par la vélocité.
|
|
3354
|
+
// Cela crée un "facteur d'ajustement" unique pour l'ensemble de la configuration.
|
|
3355
|
+
const globalChangeRatio = (totalTargetWeight - totalCurrentWeight) / totalCurrentWeight;
|
|
3356
|
+
const adjustmentFactor = Math.max(-MAX_CHANGE_VELOCITY, Math.min(MAX_CHANGE_VELOCITY, globalChangeRatio));
|
|
3173
3357
|
|
|
3358
|
+
// 3. Appliquer ce facteur à chaque valeur de la configuration actuelle.
|
|
3359
|
+
// Cela fait "glisser" l'ensemble de la configuration tout en préservant les proportions.
|
|
3360
|
+
for (const key in currentConfig) {
|
|
3361
|
+
if (Object.hasOwnProperty.call(targetConfig, key)) {
|
|
3362
|
+
currentConfig[key] *= (1 + adjustmentFactor);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3174
3366
|
applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds);
|
|
3175
3367
|
applyInertialUpdate(securityConfig.weights, newConfig.weights);
|
|
3176
3368
|
applyInertialUpdate(securityConfig.patterns, newConfig.patterns);
|
|
3177
3369
|
|
|
3370
|
+
// NOUVEAU: Stocker la meilleure solution pour une consultation externe
|
|
3371
|
+
lastBestSolution = bestSolution;
|
|
3372
|
+
|
|
3178
3373
|
console.log("[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.");
|
|
3179
3374
|
console.log("[AutoTuning] Objectifs atteints :", { falsePositiveRate: bestSolution.objectives[0].toFixed(4), falseNegativeRate: bestSolution.objectives[1].toFixed(4) });
|
|
3180
3375
|
console.log("[AutoTuning] Nouveaux seuils :", securityConfig.thresholds);
|
|
3181
3376
|
console.log("[AutoTuning] Nouveaux poids :", securityConfig.weights);
|
|
3182
3377
|
console.log("[AutoTuning] Nouveaux patterns :", securityConfig.patterns);
|
|
3378
|
+
|
|
3379
|
+
// NOUVEAU: Sauvegarder la meilleure configuration si un chemin est fourni.
|
|
3380
|
+
if (savePath) {
|
|
3381
|
+
try {
|
|
3382
|
+
const configToSave = JSON.stringify(bestSolution.solution, null, 2);
|
|
3383
|
+
fs.writeFileSync(savePath, configToSave, 'utf-8');
|
|
3384
|
+
console.log(`[AutoTuning] Meilleure configuration sauvegardée dans : ${savePath}`);
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3183
3389
|
}
|
|
3184
3390
|
|
|
3185
3391
|
/**
|
|
@@ -3191,6 +3397,7 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
3191
3397
|
* @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
|
|
3192
3398
|
* @param {number} [options.minDataPoints=200] - The minimum number of requests to have before starting a cycle (default: 200).
|
|
3193
3399
|
* @param {number} [options.maxDataPoints=10000] - The maximum number of log entries to keep in memory (default: 10,000).
|
|
3400
|
+
* @param {string} [options.savePath] - Optional. If provided, the best configuration found will be saved to this file path.
|
|
3194
3401
|
*/
|
|
3195
3402
|
export function startThresholdAutoTuning(options) {
|
|
3196
3403
|
if (autoTuningJobId) {
|
|
@@ -3203,7 +3410,8 @@ export function startThresholdAutoTuning(options) {
|
|
|
3203
3410
|
trafficData,
|
|
3204
3411
|
interval = 1800000, // 30 minutes
|
|
3205
3412
|
minDataPoints = 200,
|
|
3206
|
-
maxDataPoints = 10000 // Limite par défaut à 10 000 entrées
|
|
3413
|
+
maxDataPoints = 10000, // Limite par défaut à 10 000 entrées
|
|
3414
|
+
savePath, // NOUVEAU: Chemin de sauvegarde optionnel
|
|
3207
3415
|
} = options;
|
|
3208
3416
|
|
|
3209
3417
|
if (!securityConfig || !trafficData) {
|
|
@@ -3213,7 +3421,7 @@ export function startThresholdAutoTuning(options) {
|
|
|
3213
3421
|
console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
|
|
3214
3422
|
|
|
3215
3423
|
autoTuningJobId = setInterval(() => {
|
|
3216
|
-
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints);
|
|
3424
|
+
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
|
|
3217
3425
|
}, interval);
|
|
3218
3426
|
}
|
|
3219
3427
|
|
|
@@ -3228,3 +3436,13 @@ export function stopThresholdAutoTuning() {
|
|
|
3228
3436
|
console.log("[AutoTuning] Job d'optimisation des seuils arrêté.");
|
|
3229
3437
|
}
|
|
3230
3438
|
}
|
|
3439
|
+
|
|
3440
|
+
/**
|
|
3441
|
+
* Returns the last best solution found by the auto-tuner.
|
|
3442
|
+
* This is useful for logging or creating a "finops" security configuration.
|
|
3443
|
+
* @export
|
|
3444
|
+
* @returns {object|null} The best solution object { solution, objectives } or null if no tuning has run.
|
|
3445
|
+
*/
|
|
3446
|
+
export function getBestTuningSolution() {
|
|
3447
|
+
return lastBestSolution;
|
|
3448
|
+
}
|