@anonympins/fingerprint 0.4.3 → 0.4.4
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 +21 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +831 -635
- package/src/js/fingerprint.js +239 -29
- package/src/js/tests/fingerprint.client.init.test.js +140 -119
- package/src/js/tests/fingerprint.test.js +110 -10
- package/src/php/Challenge/ChallengeUtils.php +415 -361
- package/src/php/Config/SecurityProfiles.php +276 -271
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +26 -0
- package/src/php/RequestContext.php +90 -90
- package/src/php/Store/IStore.php +41 -41
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +130 -0
- package/src/php/Utils/RequestUtils.php +200 -16
package/src/js/fingerprint.js
CHANGED
|
@@ -15,6 +15,37 @@ export { createRedisStore } from "./redis-store.js";
|
|
|
15
15
|
export { createMongoDbStore } from "./mongodb-store.js";
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
|
|
20
|
+
* @param {string} clientIp - L'adresse IP du client.
|
|
21
|
+
* @returns {Promise<boolean>} True si la requête est autorisée, false si elle est limitée.
|
|
22
|
+
*/
|
|
23
|
+
async function checkChallengeRateLimit(clientIp) {
|
|
24
|
+
const subnet = getIpSubnet(clientIp);
|
|
25
|
+
if (!subnet) return false;
|
|
26
|
+
|
|
27
|
+
const key = `rate-limit:${subnet}`;
|
|
28
|
+
const rateLimitData = (await store.get(key)) || {
|
|
29
|
+
tokens: 5.0,
|
|
30
|
+
lastRefill: Date.now() / 1000
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const capacity = 5.0;
|
|
34
|
+
const refillRate = 0.1; // 1 token toutes les 10 secondes
|
|
35
|
+
const now = Date.now() / 1000;
|
|
36
|
+
|
|
37
|
+
const elapsed = now - rateLimitData.lastRefill;
|
|
38
|
+
const tokens = Math.min(capacity, rateLimitData.tokens + elapsed * refillRate);
|
|
39
|
+
|
|
40
|
+
if (tokens < 1.0) {
|
|
41
|
+
await store.set(key, { tokens, lastRefill: now }, 60);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, 60);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
18
49
|
/**
|
|
19
50
|
* @private
|
|
20
51
|
* Deep merges two objects. The `source` object's properties overwrite the `target`'s.
|
|
@@ -54,7 +85,8 @@ const securityProfiles = {
|
|
|
54
85
|
timeInconsistencyScore: 0.9,
|
|
55
86
|
tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
|
|
56
87
|
subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
|
|
57
|
-
ipReputationScore: 0.5 // NOUVEAU: Poids pour la réputation IP
|
|
88
|
+
ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
89
|
+
botnetClusterScore: 0.6 // NOUVEAU: Poids pour le clustering botnet
|
|
58
90
|
},
|
|
59
91
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
60
92
|
patterns: {
|
|
@@ -89,7 +121,8 @@ const securityProfiles = {
|
|
|
89
121
|
timeInconsistencyScore: 1.0,
|
|
90
122
|
tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
|
|
91
123
|
subnetScore: 0.5,
|
|
92
|
-
ipReputationScore: 0.6 // NOUVEAU: Poids pour la réputation IP
|
|
124
|
+
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
125
|
+
botnetClusterScore: 0.8 // NOUVEAU: Poids pour le clustering botnet
|
|
93
126
|
},
|
|
94
127
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
95
128
|
patterns: {
|
|
@@ -125,7 +158,8 @@ const securityProfiles = {
|
|
|
125
158
|
timeInconsistencyScore: 0.8,
|
|
126
159
|
tlsSpoofingScore: 0.7, // Important pour les API
|
|
127
160
|
subnetScore: 0.4,
|
|
128
|
-
ipReputationScore: 0.5 // NOUVEAU: Poids pour la réputation IP
|
|
161
|
+
ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
162
|
+
botnetClusterScore: 0.7 // NOUVEAU: Poids pour le clustering botnet
|
|
129
163
|
},
|
|
130
164
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
131
165
|
patterns: {
|
|
@@ -162,7 +196,8 @@ const securityProfiles = {
|
|
|
162
196
|
timeInconsistencyScore: 0.8,
|
|
163
197
|
tlsSpoofingScore: 0.6, // Moins critique pour les blogs
|
|
164
198
|
subnetScore: 0.2,
|
|
165
|
-
ipReputationScore: 0.3 // NOUVEAU: Poids pour la réputation IP
|
|
199
|
+
ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
200
|
+
botnetClusterScore: 0.5 // NOUVEAU: Poids pour le clustering botnet
|
|
166
201
|
},
|
|
167
202
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
168
203
|
patterns: {
|
|
@@ -198,7 +233,8 @@ const securityProfiles = {
|
|
|
198
233
|
timeInconsistencyScore: 0.9,
|
|
199
234
|
tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
|
|
200
235
|
subnetScore: 0.5,
|
|
201
|
-
ipReputationScore: 0.6 // NOUVEAU: Poids pour la réputation IP
|
|
236
|
+
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
237
|
+
botnetClusterScore: 0.9 // NOUVEAU: Poids pour le clustering botnet
|
|
202
238
|
},
|
|
203
239
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
204
240
|
patterns: {
|
|
@@ -319,12 +355,12 @@ function getTlsFingerprint(context) {
|
|
|
319
355
|
let ja4 = null;
|
|
320
356
|
|
|
321
357
|
// 1. Prefer JA4 hash from a trusted reverse proxy header.
|
|
322
|
-
const ja4FromHeader = context.headers['x-ja4-hash'];
|
|
358
|
+
const ja4FromHeader = context.headers ? context.headers['x-ja4-hash'] : null;
|
|
323
359
|
if (ja4FromHeader) {
|
|
324
360
|
ja4 = ja4FromHeader;
|
|
325
361
|
}
|
|
326
362
|
// 2. Prefer JA3 hash from a trusted reverse proxy header.
|
|
327
|
-
const ja3FromHeader = context.headers['x-ja3-hash']; // Assuming a proxy might provide JA3 too
|
|
363
|
+
const ja3FromHeader = context.headers ? context.headers['x-ja3-hash'] : null; // Assuming a proxy might provide JA3 too
|
|
328
364
|
if (ja3FromHeader) {
|
|
329
365
|
ja3 = ja3FromHeader;
|
|
330
366
|
}
|
|
@@ -425,7 +461,7 @@ function getCompositeDeviceHash(context) {
|
|
|
425
461
|
// notre propre fingerprint serveur pour le comparer.
|
|
426
462
|
// Un attaquant qui forge un `clientFp` mais oublie de forger les en-têtes
|
|
427
463
|
// correspondants sera détecté par l'incohérence.
|
|
428
|
-
const clientFp = context.headers['x-device-fingerprint'];
|
|
464
|
+
const clientFp = context.headers ? context.headers['x-device-fingerprint'] : null;
|
|
429
465
|
if (clientFp && typeof clientFp === 'string' && clientFp.includes('cvs:')) {
|
|
430
466
|
// On ajoute le hash du fingerprint client comme un composant du fingerprint serveur.
|
|
431
467
|
// Si le clientFp change, le hash serveur changera aussi.
|
|
@@ -433,7 +469,7 @@ function getCompositeDeviceHash(context) {
|
|
|
433
469
|
}
|
|
434
470
|
|
|
435
471
|
// 1. SIGNAL FORT: User Agent (poids élevé)
|
|
436
|
-
const ua = context.headers["user-agent"];
|
|
472
|
+
const ua = context.headers ? context.headers["user-agent"] : null;
|
|
437
473
|
if (ua) {
|
|
438
474
|
srv.add("ua", ua); // User-Agent
|
|
439
475
|
}
|
|
@@ -443,10 +479,10 @@ function getCompositeDeviceHash(context) {
|
|
|
443
479
|
if (ja3) srv.add("ja3", ja3);
|
|
444
480
|
if (ja4) srv.add("ja4", ja4);
|
|
445
481
|
|
|
446
|
-
const h2Fingerprint = context.headers['x-http2-fingerprint'];
|
|
482
|
+
const h2Fingerprint = context.headers ? context.headers['x-http2-fingerprint'] : null;
|
|
447
483
|
if (h2Fingerprint) srv.add("h2", h2Fingerprint);
|
|
448
484
|
|
|
449
|
-
const tcpFingerprint = context.headers['x-tcp-fingerprint'];
|
|
485
|
+
const tcpFingerprint = context.headers ? context.headers['x-tcp-fingerprint'] : null;
|
|
450
486
|
if (tcpFingerprint) srv.add("tcp", tcpFingerprint);
|
|
451
487
|
|
|
452
488
|
// 3. SIGNAUX DE HAUT NIVEAU (Applicatif) Moins fiables, mais utiles pour la corroboration
|
|
@@ -464,7 +500,7 @@ function getCompositeDeviceHash(context) {
|
|
|
464
500
|
};
|
|
465
501
|
|
|
466
502
|
for (const [key, headerName] of Object.entries(headersToCapture)) {
|
|
467
|
-
const headerValue = context.headers[headerName];
|
|
503
|
+
const headerValue = context.headers ? context.headers[headerName] : null;
|
|
468
504
|
if (headerValue) {
|
|
469
505
|
srv.add(key, headerValue);
|
|
470
506
|
}
|
|
@@ -1149,6 +1185,79 @@ function analyzeMouseMovements(history) {
|
|
|
1149
1185
|
return { avgSpeed, avgAcceleration, straightness, pauses, segments: segments.map(s => s.distance) };
|
|
1150
1186
|
}
|
|
1151
1187
|
|
|
1188
|
+
/**
|
|
1189
|
+
* @private
|
|
1190
|
+
* Analyse une série d'événements tactiles mobiles pour en extraire des indicateurs comportementaux robustes.
|
|
1191
|
+
* @param {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} history
|
|
1192
|
+
* @returns {{avgSpeed: number, avgAcceleration: number, straightness: number, pauses: number, segments: Array<number>, avgPressure: number, avgRadius: number, pressureVariance: number, radiusVariance: number, maxTouches: number}}
|
|
1193
|
+
*/
|
|
1194
|
+
function analyzeTouchMovements(history) {
|
|
1195
|
+
if (!history || history.length < 3) {
|
|
1196
|
+
return { avgSpeed: 0, avgAcceleration: 0, straightness: 1, pauses: 0, segments: [], avgPressure: 0, avgRadius: 0, pressureVariance: 0, radiusVariance: 0, maxTouches: 1 };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const segments = [];
|
|
1200
|
+
let totalDistance = 0;
|
|
1201
|
+
let pauses = 0;
|
|
1202
|
+
let totalPressure = 0;
|
|
1203
|
+
let totalRadius = 0;
|
|
1204
|
+
let maxTouches = 1;
|
|
1205
|
+
|
|
1206
|
+
for (let i = 1; i < history.length; i++) {
|
|
1207
|
+
const p1 = history[i - 1];
|
|
1208
|
+
const p2 = history[i];
|
|
1209
|
+
const dx = p2.x - p1.x;
|
|
1210
|
+
const dy = p2.y - p1.y;
|
|
1211
|
+
const dt = p2.t - p1.t;
|
|
1212
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
1213
|
+
|
|
1214
|
+
totalPressure += p2.p || 0;
|
|
1215
|
+
totalRadius += p2.r || 0;
|
|
1216
|
+
if (p2.num > maxTouches) {
|
|
1217
|
+
maxTouches = p2.num;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (dt > 0) {
|
|
1221
|
+
const speed = distance / dt;
|
|
1222
|
+
segments.push({ distance, dt, speed });
|
|
1223
|
+
totalDistance += distance;
|
|
1224
|
+
}
|
|
1225
|
+
if (dt > 100 && distance < 5) {
|
|
1226
|
+
pauses++;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
totalPressure += history[0].p || 0;
|
|
1231
|
+
totalRadius += history[0].r || 0;
|
|
1232
|
+
|
|
1233
|
+
const avgPressure = totalPressure / history.length;
|
|
1234
|
+
const avgRadius = totalRadius / history.length;
|
|
1235
|
+
|
|
1236
|
+
let sqDiffPressureSum = 0;
|
|
1237
|
+
let sqDiffRadiusSum = 0;
|
|
1238
|
+
for (const pt of history) {
|
|
1239
|
+
sqDiffPressureSum += Math.pow((pt.p || 0) - avgPressure, 2);
|
|
1240
|
+
sqDiffRadiusSum += Math.pow((pt.r || 0) - avgRadius, 2);
|
|
1241
|
+
}
|
|
1242
|
+
const pressureVariance = sqDiffPressureSum / history.length;
|
|
1243
|
+
const radiusVariance = sqDiffRadiusSum / history.length;
|
|
1244
|
+
|
|
1245
|
+
if (segments.length < 2) {
|
|
1246
|
+
return { avgSpeed: 0, avgAcceleration: 0, straightness: 1, pauses, segments: [], avgPressure, avgRadius, pressureVariance, radiusVariance, maxTouches };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const totalTime = history[history.length - 1].t - history[0].t;
|
|
1250
|
+
const avgSpeed = totalTime > 0 ? segments.reduce((sum, s) => sum + s.speed, 0) / segments.length : 0;
|
|
1251
|
+
const avgAcceleration = segments.reduce((sum, s) => sum + (s.speed / s.dt), 0) / segments.length;
|
|
1252
|
+
|
|
1253
|
+
const startPoint = history[0];
|
|
1254
|
+
const endPoint = history[history.length - 1];
|
|
1255
|
+
const straightDistance = Math.sqrt(Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2));
|
|
1256
|
+
const straightness = totalDistance > 0 ? straightDistance / totalDistance : 1;
|
|
1257
|
+
|
|
1258
|
+
return { avgSpeed, avgAcceleration, straightness, pauses, segments: segments.map(s => s.distance), avgPressure, avgRadius, pressureVariance, radiusVariance, maxTouches };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1152
1261
|
/**
|
|
1153
1262
|
* Calcule un score basé sur les métriques comportementales envoyées par le client.
|
|
1154
1263
|
* @param {object} context - Le contexte de la requête, contenant les en-têtes.
|
|
@@ -1171,9 +1280,10 @@ function getBehaviorScore(context) {
|
|
|
1171
1280
|
|
|
1172
1281
|
// 2. Analyse des mouvements de la souris
|
|
1173
1282
|
const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
|
|
1283
|
+
const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
|
|
1174
1284
|
|
|
1175
1285
|
// Pénalité pour absence totale d'interaction (pas de mouvements, pas de frappes).
|
|
1176
|
-
if (avgSpeed === 0 && metrics.keystrokeLatency === 0) {
|
|
1286
|
+
if (avgSpeed === 0 && touchAnalysis.avgSpeed === 0 && metrics.keystrokeLatency === 0) {
|
|
1177
1287
|
score += 40;
|
|
1178
1288
|
}
|
|
1179
1289
|
|
|
@@ -1197,6 +1307,30 @@ function getBehaviorScore(context) {
|
|
|
1197
1307
|
if (pauses === 0 && segments.length > 20) score += 15; // Mouvement continu sans micro-pauses
|
|
1198
1308
|
}
|
|
1199
1309
|
|
|
1310
|
+
// 4. Analyse comportementale des événements tactiles (Touch Move)
|
|
1311
|
+
const touchHistory = metrics.touchMovementsHistory;
|
|
1312
|
+
if (touchHistory && touchHistory.length > 0) {
|
|
1313
|
+
const touch = analyzeTouchMovements(touchHistory);
|
|
1314
|
+
if (touch.avgSpeed > 0) {
|
|
1315
|
+
if (touch.avgSpeed > 5) score += 30; // Touch d'une vitesse anormale/robotique
|
|
1316
|
+
if (touch.avgAcceleration > 0.8) score += 20;
|
|
1317
|
+
if (touch.straightness > 0.98) score += 35; // Un tracé de doigt humain n'est jamais parfaitement rectiligne
|
|
1318
|
+
if (touch.pauses === 0 && touch.segments.length > 25) score += 15;
|
|
1319
|
+
|
|
1320
|
+
// Détection de l'émulation (pression et rayon de contact constants)
|
|
1321
|
+
if (touch.avgPressure > 0 && touch.pressureVariance === 0) {
|
|
1322
|
+
score += 30; // Spoofed force/pressure
|
|
1323
|
+
}
|
|
1324
|
+
if (touch.avgRadius > 0 && touch.radiusVariance === 0) {
|
|
1325
|
+
score += 30; // Spoofed pointer area size
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (touch.segments.length > 10) {
|
|
1329
|
+
const benfordDev = Optimization.Operators.benfordTest(touch.segments);
|
|
1330
|
+
if (benfordDev > 0.18) score += 35;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1200
1334
|
// Plausibilité de la latence de frappe
|
|
1201
1335
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
1202
1336
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
@@ -1738,14 +1872,18 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
1738
1872
|
subnetData.highScoreDevices = {};
|
|
1739
1873
|
}
|
|
1740
1874
|
|
|
1741
|
-
|
|
1875
|
+
// Utilisation d'un identifiant d'appareil stable (fingerprint matériel) plutôt que l'ID de cookie volatil
|
|
1876
|
+
const currentDeviceHash = getCompositeDeviceHash(context);
|
|
1877
|
+
const stableFpId = cyrb53(extractStablePart(currentDeviceHash)).toString();
|
|
1878
|
+
|
|
1879
|
+
const currentDeviceContributions = subnetData.highScoreDevices[stableFpId] || 0;
|
|
1742
1880
|
if (currentDeviceContributions < 5) {
|
|
1743
|
-
subnetData.highScoreDevices[
|
|
1881
|
+
subnetData.highScoreDevices[stableFpId] = currentDeviceContributions + 1;
|
|
1744
1882
|
subnetData.highScoreCount++;
|
|
1745
1883
|
}
|
|
1746
1884
|
|
|
1747
|
-
if (!subnetData.deviceIds.includes(
|
|
1748
|
-
subnetData.deviceIds.push(
|
|
1885
|
+
if (!subnetData.deviceIds.includes(stableFpId)) {
|
|
1886
|
+
subnetData.deviceIds.push(stableFpId);
|
|
1749
1887
|
}
|
|
1750
1888
|
subnetData.lastActivity = Date.now();
|
|
1751
1889
|
|
|
@@ -1792,6 +1930,40 @@ async function getSubnetScore(context) {
|
|
|
1792
1930
|
return { subnetScore: Math.min(100, deviceCountPenalty + highScorePenalty) };
|
|
1793
1931
|
}
|
|
1794
1932
|
|
|
1933
|
+
/**
|
|
1934
|
+
* Calcule le score d'anomalie de similarité réseau (Botnet Clustering).
|
|
1935
|
+
* @param {object} context - Le contexte de la requête.
|
|
1936
|
+
* @param {string} stableFpHash - Le hash de la partie stable de l'empreinte.
|
|
1937
|
+
* @returns {Promise<{botnetClusterScore: number}>}
|
|
1938
|
+
*/
|
|
1939
|
+
async function getBotnetClusterScore(context, stableFpHash) {
|
|
1940
|
+
if (!stableFpHash) return { botnetClusterScore: 0 };
|
|
1941
|
+
const key = `botnet-cluster:${stableFpHash}`;
|
|
1942
|
+
const now = Date.now();
|
|
1943
|
+
const tenMinutesAgo = now - 600 * 1000;
|
|
1944
|
+
|
|
1945
|
+
let clusterData = (await store.get(key)) || [];
|
|
1946
|
+
if (!Array.isArray(clusterData)) {
|
|
1947
|
+
clusterData = [];
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
clusterData = clusterData.filter(entry => entry.timestamp > tenMinutesAgo);
|
|
1951
|
+
const existingIndex = clusterData.findIndex(entry => entry.ip === context.clientIp);
|
|
1952
|
+
if (existingIndex !== -1) {
|
|
1953
|
+
clusterData[existingIndex].timestamp = now;
|
|
1954
|
+
} else {
|
|
1955
|
+
clusterData.push({ ip: context.clientIp, timestamp: now });
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
await store.set(key, clusterData, 600);
|
|
1959
|
+
const uniqueIpsCount = clusterData.length;
|
|
1960
|
+
let botnetClusterScore = 0;
|
|
1961
|
+
if (uniqueIpsCount >= 2) {
|
|
1962
|
+
botnetClusterScore = Math.min(100, Math.round(1000 * (1 - Math.exp(-0.35 * (uniqueIpsCount - 1)))) / 10);
|
|
1963
|
+
}
|
|
1964
|
+
return { botnetClusterScore };
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1795
1967
|
/**
|
|
1796
1968
|
* Retrieves the current local IP reputation score, applying time-based decay.
|
|
1797
1969
|
* @param {string} ip - The client's IP address.
|
|
@@ -1858,7 +2030,10 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
1858
2030
|
benfordThreshold = 0.15, // Seuil de déviation de Benford au-dessus duquel la distribution est "non naturelle".
|
|
1859
2031
|
patternWeight = 80, // Pénalité FORTE et unique si un pattern est détecté.
|
|
1860
2032
|
decayFactor = 0.95, // Décroissance du score dans le temps.
|
|
1861
|
-
inactivityReset = 180000
|
|
2033
|
+
inactivityReset = 180000, // Réinitialisation du score après 3 minutes d'inactivité.
|
|
2034
|
+
regularityRatio = 0.4, // (NOUVEAU) Poids relatif de l'écart-type
|
|
2035
|
+
benfordRatio = 0.3, // (NOUVEAU) Poids relatif de Benford
|
|
2036
|
+
enumerationRatio = 0.3 // (NOUVEAU) Poids relatif de l'énumération de chemins
|
|
1862
2037
|
} = patternConfig;
|
|
1863
2038
|
|
|
1864
2039
|
const now = Date.now();
|
|
@@ -1888,24 +2063,24 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
1888
2063
|
deviceData.timingHistory.push(timeSinceLast);
|
|
1889
2064
|
}
|
|
1890
2065
|
|
|
1891
|
-
let
|
|
2066
|
+
let regularityScore = 0;
|
|
2067
|
+
let benfordScore = 0;
|
|
1892
2068
|
const timings = deviceData.timingHistory;
|
|
1893
2069
|
|
|
1894
2070
|
// Analyse statistique unifiée si nous avons assez de données
|
|
1895
2071
|
if (timings.length >= minSamples) {
|
|
1896
|
-
const timings = deviceData.timingHistory;
|
|
1897
2072
|
const mean = timings.reduce((a, b) => a + b, 0) / timings.length;
|
|
1898
2073
|
const variance = timings.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / timings.length;
|
|
1899
2074
|
const stdDev = Math.sqrt(variance);
|
|
1900
2075
|
const benfordDeviation = Optimization.Operators.benfordTest(timings);
|
|
1901
2076
|
|
|
1902
|
-
//
|
|
2077
|
+
// Calcul progressif de la régularité (stdDev proche de 0 = score max)
|
|
1903
2078
|
if (stdDev < regularityThreshold) {
|
|
1904
|
-
|
|
2079
|
+
regularityScore = 1 - (stdDev / regularityThreshold);
|
|
1905
2080
|
}
|
|
1906
|
-
//
|
|
1907
|
-
|
|
1908
|
-
|
|
2081
|
+
// Calcul progressif de Benford (excès par rapport au seuil)
|
|
2082
|
+
if (benfordDeviation > benfordThreshold) {
|
|
2083
|
+
benfordScore = Math.min(1, (benfordDeviation - benfordThreshold) / (0.5 - benfordThreshold));
|
|
1909
2084
|
}
|
|
1910
2085
|
}
|
|
1911
2086
|
|
|
@@ -1919,12 +2094,18 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
1919
2094
|
templates.forEach(t => templateCounts[t] = (templateCounts[t] || 0) + 1);
|
|
1920
2095
|
|
|
1921
2096
|
const maxTemplateRepetition = Math.max(...Object.values(templateCounts), 0);
|
|
1922
|
-
// Si une même structure de route est répétée mais sur des URLs réelles différentes
|
|
1923
2097
|
if (maxTemplateRepetition >= 3 && uniquePaths.size === history.length) {
|
|
1924
|
-
enumerationScore =
|
|
2098
|
+
enumerationScore = Math.min(1, (maxTemplateRepetition - 2) / 5);
|
|
1925
2099
|
}
|
|
1926
2100
|
}
|
|
1927
2101
|
|
|
2102
|
+
// Score instantané combiné linéaire pondéré
|
|
2103
|
+
const weightedScore = (regularityScore * regularityRatio) +
|
|
2104
|
+
(benfordScore * benfordRatio) +
|
|
2105
|
+
(enumerationScore * enumerationRatio);
|
|
2106
|
+
|
|
2107
|
+
const instantScore = weightedScore * patternWeight;
|
|
2108
|
+
|
|
1928
2109
|
// Garder l'historique à une taille raisonnable
|
|
1929
2110
|
if (history.length > historySize) {
|
|
1930
2111
|
history.shift();
|
|
@@ -1943,7 +2124,7 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
1943
2124
|
}
|
|
1944
2125
|
newPatternScore = Math.max(0, newPatternScore);
|
|
1945
2126
|
|
|
1946
|
-
deviceData.lastPatternScore =
|
|
2127
|
+
deviceData.lastPatternScore = Math.max(instantScore, newPatternScore);
|
|
1947
2128
|
|
|
1948
2129
|
return { requestPatternScore: Math.min(100, deviceData.lastPatternScore) };
|
|
1949
2130
|
}
|
|
@@ -2175,6 +2356,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2175
2356
|
const { deviceId, deviceData, consistencyScore, newCookie } = await resolveRequestIdentity(context, securityConfig);
|
|
2176
2357
|
|
|
2177
2358
|
const clientIp = context.clientIp;
|
|
2359
|
+
const currentDeviceHash = getCompositeDeviceHash(context);
|
|
2178
2360
|
|
|
2179
2361
|
// If a new cookie needs to be set, attach it to the request object
|
|
2180
2362
|
// so the middleware can handle it. This is a temporary state holder.
|
|
@@ -2230,6 +2412,10 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2230
2412
|
|
|
2231
2413
|
const ipReputationScore = await getIpReputationScore(clientIp);
|
|
2232
2414
|
|
|
2415
|
+
const stableFp = extractStablePart(currentDeviceHash);
|
|
2416
|
+
const stableFpHash = cyrb53(stableFp).toString();
|
|
2417
|
+
const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
|
|
2418
|
+
|
|
2233
2419
|
// Save the updated device state to the store
|
|
2234
2420
|
// Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
|
|
2235
2421
|
await store.set(`device:${deviceId}`, deviceData);
|
|
@@ -2240,7 +2426,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2240
2426
|
deviceData.ips = new Set(deviceData.ips);
|
|
2241
2427
|
}
|
|
2242
2428
|
// Le vecteur de suspicion est maintenant complet.
|
|
2243
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore };
|
|
2429
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore };
|
|
2244
2430
|
};
|
|
2245
2431
|
|
|
2246
2432
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -2644,6 +2830,7 @@ export class FingerprintEngine {
|
|
|
2644
2830
|
(suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
|
|
2645
2831
|
(suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
|
|
2646
2832
|
(suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
|
|
2833
|
+
(suspicionVector.botnetClusterScore || 0) * (weights.botnetClusterScore || 0) +
|
|
2647
2834
|
(suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
|
|
2648
2835
|
(suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
|
|
2649
2836
|
(suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0) +
|
|
@@ -3374,6 +3561,28 @@ export class FingerprintEngine {
|
|
|
3374
3561
|
if (mustReChallenge) {
|
|
3375
3562
|
this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
|
|
3376
3563
|
}
|
|
3564
|
+
|
|
3565
|
+
// --- AJOUT : Limiteur de débit (Token Bucket) ---
|
|
3566
|
+
const rateLimitPassed = await checkChallengeRateLimit(clientIp);
|
|
3567
|
+
if (!rateLimitPassed) {
|
|
3568
|
+
this._log('Challenge rate limit exceeded - blocking with 429', { clientIp });
|
|
3569
|
+
const decision = {
|
|
3570
|
+
action: 'block',
|
|
3571
|
+
status: 429,
|
|
3572
|
+
body: 'Too Many Requests',
|
|
3573
|
+
score: finalScore,
|
|
3574
|
+
vector: suspicionVector
|
|
3575
|
+
};
|
|
3576
|
+
if (this.dryRun) {
|
|
3577
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
3578
|
+
decision.intendedAction = decision.action;
|
|
3579
|
+
decision.action = 'next';
|
|
3580
|
+
delete decision.status;
|
|
3581
|
+
delete decision.body;
|
|
3582
|
+
}
|
|
3583
|
+
return decision;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3377
3586
|
this._log('Suspicious request without valid ticket - issuing challenge', { finalScore, hasPowCookie: !!powCookie });
|
|
3378
3587
|
|
|
3379
3588
|
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
@@ -4144,6 +4353,7 @@ export const __internal = {
|
|
|
4144
4353
|
sanitizeTrafficData, // NOUVEAU: Expose pour l'auto-tuner/tests
|
|
4145
4354
|
getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
|
|
4146
4355
|
parseJa3,
|
|
4356
|
+
getBotnetClusterScore, // NOUVEAU: Expose pour les tests
|
|
4147
4357
|
generateCpuTargetChallengePage,
|
|
4148
4358
|
getClientHintsInconsistencyScore, // Expose for testing
|
|
4149
4359
|
generateCombinedPoWChallengePage,
|