@anonympins/fingerprint 0.3.2 → 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 +276 -34
- package/composer.json +38 -0
- package/index.js +5 -0
- package/package.json +23 -18
- package/phpunit.xml +20 -0
- package/public/fp.js +2 -0
- package/src/js/build-client.js +69 -0
- package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -175
- package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -538
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/{fingerprint.js → src/js/fingerprint.js} +255 -101
- package/{library.js → src/js/library.js} +1729 -1729
- 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: {
|
|
@@ -657,47 +654,6 @@ export const verifyTspChallenge = (
|
|
|
657
654
|
}
|
|
658
655
|
};
|
|
659
656
|
|
|
660
|
-
/**
|
|
661
|
-
* Generates the HTML content for the CPU PoW challenge (SHA-256).
|
|
662
|
-
*/
|
|
663
|
-
const generateCpuPoWChallenge = (
|
|
664
|
-
clientIp,
|
|
665
|
-
nonce,
|
|
666
|
-
difficulty = 4,
|
|
667
|
-
path = "",
|
|
668
|
-
) => {
|
|
669
|
-
return `
|
|
670
|
-
<html>
|
|
671
|
-
<head><title>Security Check</title></head>
|
|
672
|
-
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
673
|
-
<h1>One moment... (Level 1)</h1>
|
|
674
|
-
<p>We are verifying that you are not a bot. This takes a few seconds.</p>
|
|
675
|
-
<div id="loader" style="margin:20px;">⚙️ Performing CPU security calculation...</div>
|
|
676
|
-
<script>
|
|
677
|
-
async function solve() {
|
|
678
|
-
const ip = "${clientIp}";
|
|
679
|
-
const nonce = "${nonce}";
|
|
680
|
-
const diff = ${difficulty};
|
|
681
|
-
const target = "0".repeat(diff);
|
|
682
|
-
let solution = 0;
|
|
683
|
-
|
|
684
|
-
while (true) {
|
|
685
|
-
const msg = "${ip}" + ":" + "${nonce}" + ":" + solution;
|
|
686
|
-
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
687
|
-
const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
688
|
-
if (hash.startsWith(target)) break;
|
|
689
|
-
solution++;
|
|
690
|
-
if (solution % 100000 === 0) await new Promise(resolve => setTimeout(resolve, 0)); // To avoid freezing the browser
|
|
691
|
-
}
|
|
692
|
-
window.location.href = "${path}" + "?pow_type=cpu&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
693
|
-
}
|
|
694
|
-
solve();
|
|
695
|
-
</script>
|
|
696
|
-
</body>
|
|
697
|
-
</html>
|
|
698
|
-
`;
|
|
699
|
-
};
|
|
700
|
-
|
|
701
657
|
/**
|
|
702
658
|
* Generates the HTML content for a memory-intensive PoW challenge.
|
|
703
659
|
*/
|
|
@@ -955,6 +911,68 @@ const injectionPatterns = {
|
|
|
955
911
|
rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
|
|
956
912
|
};
|
|
957
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
|
+
|
|
958
976
|
/**
|
|
959
977
|
* Calcule un score basé sur les métriques comportementales envoyées par le client.
|
|
960
978
|
* @param {object} context - Le contexte de la requête, contenant les en-têtes.
|
|
@@ -975,8 +993,11 @@ function getBehaviorScore(context) {
|
|
|
975
993
|
return { behaviorScore: 100 };
|
|
976
994
|
}
|
|
977
995
|
|
|
978
|
-
// 2.
|
|
979
|
-
|
|
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) {
|
|
980
1001
|
score += 40;
|
|
981
1002
|
}
|
|
982
1003
|
|
|
@@ -991,32 +1012,25 @@ function getBehaviorScore(context) {
|
|
|
991
1012
|
score -= 10; // Petit bonus pour une navigation de base.
|
|
992
1013
|
}
|
|
993
1014
|
}
|
|
994
|
-
// 3. Vérification de la plausibilité et de la distribution des métriques.
|
|
995
|
-
// Un bot pourrait envoyer des valeurs aléatoires, mais elles ne suivront probablement pas
|
|
996
|
-
// des distributions naturelles (comme la loi de Benford pour les premiers chiffres).
|
|
997
1015
|
|
|
998
|
-
//
|
|
999
|
-
if (
|
|
1000
|
-
|
|
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
|
+
}
|
|
1001
1023
|
|
|
1002
1024
|
// Plausibilité de la latence de frappe
|
|
1003
1025
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
1004
1026
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
1005
1027
|
|
|
1006
1028
|
// 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const mouseDeviation = Optimization.Operators.benfordTest(mouseEntropyStr);
|
|
1013
|
-
// Une déviation > 0.15 est fortement suspecte.
|
|
1014
|
-
if (mouseDeviation > 0.15) score += 40;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (keystrokeLatencyStr.length > 2) {
|
|
1018
|
-
const keystrokeDeviation = Optimization.Operators.benfordTest(keystrokeLatencyStr);
|
|
1019
|
-
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
|
+
}
|
|
1020
1034
|
}
|
|
1021
1035
|
|
|
1022
1036
|
return { behaviorScore: Math.min(100, score) }; // Assure que le score ne dépasse pas 100, mais peut être négatif (bonus)
|
|
@@ -1152,6 +1166,64 @@ function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerprint) {
|
|
|
1152
1166
|
return { tlsSpoofingScore: 0 };
|
|
1153
1167
|
}
|
|
1154
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
|
+
|
|
1155
1227
|
|
|
1156
1228
|
/**
|
|
1157
1229
|
* Calcule un score basé sur la détection explicite de frameworks d'automatisation.
|
|
@@ -1511,10 +1583,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1511
1583
|
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
1512
1584
|
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
1513
1585
|
|
|
1514
|
-
// NOUVEAU: On calcule le score de spoofing TLS.
|
|
1515
1586
|
const { tlsSpoofingScore } = getTlsSpoofingScore(context);
|
|
1516
1587
|
|
|
1517
|
-
// NOUVEAU: On appelle getBotScore pour détecter les marqueurs d'automatisation.
|
|
1518
1588
|
const { botScore } = getBotScore(context);
|
|
1519
1589
|
|
|
1520
1590
|
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
@@ -1523,6 +1593,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1523
1593
|
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
1524
1594
|
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
1525
1595
|
|
|
1596
|
+
// NOUVEAU: On calcule le score de variance des clics.
|
|
1597
|
+
const { clickVarianceScore } = getClickVarianceScore(context);
|
|
1598
|
+
|
|
1526
1599
|
const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
|
|
1527
1600
|
|
|
1528
1601
|
// Save the updated device state to the store
|
|
@@ -1535,7 +1608,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1535
1608
|
deviceData.ips = new Set(deviceData.ips);
|
|
1536
1609
|
}
|
|
1537
1610
|
// Le vecteur de suspicion est maintenant complet.
|
|
1538
|
-
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 };
|
|
1539
1612
|
};
|
|
1540
1613
|
|
|
1541
1614
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -1698,7 +1771,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1698
1771
|
* @param {string} clientIp - The client's IP address.
|
|
1699
1772
|
* @returns {string} HTML content.
|
|
1700
1773
|
*/
|
|
1701
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig,
|
|
1774
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
|
|
1702
1775
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
1703
1776
|
const solverCode = getPowSolverCode();
|
|
1704
1777
|
// On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
|
|
@@ -1707,6 +1780,13 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1707
1780
|
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
|
|
1708
1781
|
const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
|
|
1709
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
|
+
|
|
1710
1790
|
const challengeScript = `
|
|
1711
1791
|
async function solve() {
|
|
1712
1792
|
const nonce = ${JSON.stringify(nonce)};
|
|
@@ -1738,6 +1818,12 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1738
1818
|
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1739
1819
|
window.location.href = finalUrl;
|
|
1740
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
|
+
|
|
1741
1827
|
solve();
|
|
1742
1828
|
`;
|
|
1743
1829
|
|
|
@@ -1753,13 +1839,12 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1753
1839
|
}
|
|
1754
1840
|
|
|
1755
1841
|
if (!htmlTemplate) {
|
|
1756
|
-
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
|
|
1757
1843
|
}
|
|
1758
1844
|
|
|
1759
1845
|
return htmlTemplate
|
|
1760
1846
|
.replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
|
|
1761
|
-
.replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript)
|
|
1762
|
-
.replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
|
|
1847
|
+
.replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
|
|
1763
1848
|
}
|
|
1764
1849
|
|
|
1765
1850
|
/**
|
|
@@ -1861,6 +1946,7 @@ export class FingerprintEngine {
|
|
|
1861
1946
|
this._allowlist = this._buildAllowlist();
|
|
1862
1947
|
this._validateConfig(securityConfig); // Validate the configuration
|
|
1863
1948
|
this.verbose = securityConfig.verbose || false;
|
|
1949
|
+
this.dryRun = securityConfig.dryRun || false;
|
|
1864
1950
|
}
|
|
1865
1951
|
|
|
1866
1952
|
/**
|
|
@@ -1878,7 +1964,7 @@ export class FingerprintEngine {
|
|
|
1878
1964
|
'weights', 'thresholds', 'cpu', 'ticketMaxAge', 'challengeTtl',
|
|
1879
1965
|
'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
|
|
1880
1966
|
'honeypot', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger',
|
|
1881
|
-
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist',
|
|
1967
|
+
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
1882
1968
|
'similarityThreshold'
|
|
1883
1969
|
]);
|
|
1884
1970
|
|
|
@@ -1918,7 +2004,8 @@ export class FingerprintEngine {
|
|
|
1918
2004
|
(suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
|
|
1919
2005
|
(suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
|
|
1920
2006
|
(suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
|
|
1921
|
-
(suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0)
|
|
2007
|
+
(suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
|
|
2008
|
+
(suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0);
|
|
1922
2009
|
|
|
1923
2010
|
return Math.min(100, score);
|
|
1924
2011
|
}
|
|
@@ -2239,7 +2326,15 @@ export class FingerprintEngine {
|
|
|
2239
2326
|
if (onDeviceCompromised) {
|
|
2240
2327
|
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
2241
2328
|
}
|
|
2242
|
-
|
|
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;
|
|
2243
2338
|
}
|
|
2244
2339
|
|
|
2245
2340
|
// The engine now works with the context directly, no more rawReq dependency here.
|
|
@@ -2440,7 +2535,15 @@ export class FingerprintEngine {
|
|
|
2440
2535
|
const newBlockThreshold = thresholds.block ?? 95;
|
|
2441
2536
|
if (finalScore >= newBlockThreshold) {
|
|
2442
2537
|
this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
|
|
2443
|
-
|
|
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;
|
|
2444
2547
|
}
|
|
2445
2548
|
// If not blocked, the request will proceed to be re-challenged.
|
|
2446
2549
|
}
|
|
@@ -2510,7 +2613,15 @@ export class FingerprintEngine {
|
|
|
2510
2613
|
if (logger) {
|
|
2511
2614
|
logger({ type: 'request_blocked', deviceId: deviceId, score: finalScore, vector: suspicionVector, timestamp: Date.now() });
|
|
2512
2615
|
}
|
|
2513
|
-
|
|
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;
|
|
2514
2625
|
}
|
|
2515
2626
|
|
|
2516
2627
|
// Honeypot: Check if the request is for a trap URL generated in a previous challenge.
|
|
@@ -2526,7 +2637,15 @@ export class FingerprintEngine {
|
|
|
2526
2637
|
logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now(), vector: { honeypotScore: 100 } });
|
|
2527
2638
|
}
|
|
2528
2639
|
await store.set(`device:${cookies.device_id}`, deviceData); // No TTL for condemned status
|
|
2529
|
-
|
|
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;
|
|
2530
2649
|
}
|
|
2531
2650
|
|
|
2532
2651
|
// --- NOUVELLE LOGIQUE DE RE-CHALLENGE ---
|
|
@@ -2555,7 +2674,15 @@ export class FingerprintEngine {
|
|
|
2555
2674
|
suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
|
|
2556
2675
|
// Recalculate the final score with the updated vector.
|
|
2557
2676
|
const newFinalScore = this.calculateFinalScore(suspicionVector);
|
|
2558
|
-
|
|
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;
|
|
2559
2686
|
}
|
|
2560
2687
|
|
|
2561
2688
|
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
@@ -2583,11 +2710,18 @@ export class FingerprintEngine {
|
|
|
2583
2710
|
}
|
|
2584
2711
|
};
|
|
2585
2712
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
2586
|
-
|
|
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
|
+
}
|
|
2587
2722
|
// Generate some trap URLs to embed in the challenge page.
|
|
2588
2723
|
// These links are visually hidden but present in the DOM to trap bots.
|
|
2589
|
-
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
2590
|
-
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
|
|
2591
2725
|
|
|
2592
2726
|
// On passe la configuration pour que la difficulté soit calculée correctement.
|
|
2593
2727
|
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
@@ -2653,16 +2787,17 @@ export class FingerprintEngine {
|
|
|
2653
2787
|
}
|
|
2654
2788
|
};
|
|
2655
2789
|
this._log('API challenge response generated', { challengePayload });
|
|
2656
|
-
|
|
2790
|
+
decision.body = challengePayload;
|
|
2657
2791
|
} else {
|
|
2658
2792
|
// For browsers, send the HTML page.
|
|
2659
|
-
const
|
|
2660
|
-
this._log('Browser challenge page generated', {
|
|
2661
|
-
pageLength: page.length,
|
|
2662
|
-
|
|
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
|
|
2663
2797
|
});
|
|
2664
|
-
|
|
2798
|
+
decision.body = page;
|
|
2665
2799
|
}
|
|
2800
|
+
return decision;
|
|
2666
2801
|
}
|
|
2667
2802
|
}
|
|
2668
2803
|
|
|
@@ -2673,7 +2808,7 @@ export class FingerprintEngine {
|
|
|
2673
2808
|
logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
|
|
2674
2809
|
}
|
|
2675
2810
|
|
|
2676
|
-
return { action: 'next', score: finalScore, vector: suspicionVector };
|
|
2811
|
+
return { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
|
|
2677
2812
|
}
|
|
2678
2813
|
|
|
2679
2814
|
/**
|
|
@@ -3018,8 +3153,11 @@ export const powMiddleware = (securityConfig) => {
|
|
|
3018
3153
|
const engine = new FingerprintEngine(securityConfig);
|
|
3019
3154
|
|
|
3020
3155
|
// Initialize the problem manager with the configured path, if provided.
|
|
3021
|
-
if (securityConfig.enableUsefulWork
|
|
3022
|
-
getProblemManager(
|
|
3156
|
+
if (securityConfig.enableUsefulWork) {
|
|
3157
|
+
getProblemManager({
|
|
3158
|
+
configPath: securityConfig.usefulWorkConfigPath,
|
|
3159
|
+
config: securityConfig.usefulWorkConfig
|
|
3160
|
+
}, store);
|
|
3023
3161
|
}
|
|
3024
3162
|
|
|
3025
3163
|
if (securityConfig.autotuning) {
|
|
@@ -3069,6 +3207,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
3069
3207
|
req.fingerprint = {
|
|
3070
3208
|
score: decision.score,
|
|
3071
3209
|
vector: decision.vector,
|
|
3210
|
+
intendedAction: decision.intendedAction, // Add intended action for logging
|
|
3072
3211
|
};
|
|
3073
3212
|
|
|
3074
3213
|
// After getSuspicionVector runs, it might have attached cookies to be set.
|
|
@@ -3119,6 +3258,7 @@ export const __internal = {
|
|
|
3119
3258
|
getCrossLayerInconsistency, // Expose for testing
|
|
3120
3259
|
// Expose page generators for security testing
|
|
3121
3260
|
getTimeInconsistencyScore,
|
|
3261
|
+
getClickVarianceScore, // NOUVEAU: Expose pour les tests
|
|
3122
3262
|
getTlsFingerprint, // NOUVEAU: Expose pour les tests
|
|
3123
3263
|
getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
|
|
3124
3264
|
generateCpuTargetChallengePage,
|
|
@@ -3138,8 +3278,9 @@ let lastBestSolution = null; // NOUVEAU: Stocke la meilleure solution trouvée
|
|
|
3138
3278
|
* @param {Array<object>} trafficData - The array containing traffic logs.
|
|
3139
3279
|
* @param {number} minDataPoints - The minimum number of data points required to start optimization.
|
|
3140
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.
|
|
3141
3282
|
*/
|
|
3142
|
-
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
|
|
3283
|
+
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
|
|
3143
3284
|
const highConfidenceLogs = trafficData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
|
|
3144
3285
|
const highConfidenceRatio = trafficData.length > 0 ? highConfidenceLogs / trafficData.length : 0;
|
|
3145
3286
|
const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
|
|
@@ -3234,6 +3375,17 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
3234
3375
|
console.log("[AutoTuning] Nouveaux seuils :", securityConfig.thresholds);
|
|
3235
3376
|
console.log("[AutoTuning] Nouveaux poids :", securityConfig.weights);
|
|
3236
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
|
+
}
|
|
3237
3389
|
}
|
|
3238
3390
|
|
|
3239
3391
|
/**
|
|
@@ -3245,6 +3397,7 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
3245
3397
|
* @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
|
|
3246
3398
|
* @param {number} [options.minDataPoints=200] - The minimum number of requests to have before starting a cycle (default: 200).
|
|
3247
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.
|
|
3248
3401
|
*/
|
|
3249
3402
|
export function startThresholdAutoTuning(options) {
|
|
3250
3403
|
if (autoTuningJobId) {
|
|
@@ -3257,7 +3410,8 @@ export function startThresholdAutoTuning(options) {
|
|
|
3257
3410
|
trafficData,
|
|
3258
3411
|
interval = 1800000, // 30 minutes
|
|
3259
3412
|
minDataPoints = 200,
|
|
3260
|
-
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
|
|
3261
3415
|
} = options;
|
|
3262
3416
|
|
|
3263
3417
|
if (!securityConfig || !trafficData) {
|
|
@@ -3267,7 +3421,7 @@ export function startThresholdAutoTuning(options) {
|
|
|
3267
3421
|
console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
|
|
3268
3422
|
|
|
3269
3423
|
autoTuningJobId = setInterval(() => {
|
|
3270
|
-
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints);
|
|
3424
|
+
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
|
|
3271
3425
|
}, interval);
|
|
3272
3426
|
}
|
|
3273
3427
|
|