@anonympins/fingerprint 0.2.2 → 0.2.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/fingerprint.js CHANGED
@@ -10,6 +10,206 @@ import { dirname, join } from "node:path";
10
10
  export { createRedisStore } from "./redis-store.js";
11
11
  export { createMongoDbStore } from "./mongodb-store.js";
12
12
 
13
+
14
+ /**
15
+ * @private
16
+ * Deep merges two objects. The `source` object's properties overwrite the `target`'s.
17
+ * @param {object} target - The target object.
18
+ * @param {object} source - The source object.
19
+ * @returns {object} The merged object.
20
+ */
21
+ function deepMerge(target, source) {
22
+ const output = { ...target };
23
+ if (target && typeof target === 'object' && source && typeof source === 'object') {
24
+ Object.keys(source).forEach(key => {
25
+ if (source[key] && typeof source[key] === 'object' && key in target) {
26
+ output[key] = deepMerge(target[key], source[key]);
27
+ } else {
28
+ output[key] = source[key];
29
+ }
30
+ });
31
+ }
32
+ return output;
33
+ }
34
+
35
+ const securityProfiles = {
36
+ /**
37
+ * @summary **Balanced Profile (Default)**
38
+ * @description A general-purpose configuration suitable for most websites, offering a good mix of security and user experience. It's sensitive enough to catch common bots without being overly aggressive towards legitimate users.
39
+ */
40
+ balanced: {
41
+ weights: {
42
+ historyScore: 0.3,
43
+ rotationScore: 0.5,
44
+ headerAnomalyScore: 0.1,
45
+ requestPatternScore: 0.6,
46
+ inconsistencyScore: 0.8,
47
+ behaviorScore: 0.7,
48
+ honeypotScore: 1.0,
49
+ crossLayerInconsistencyScore: 0.4,
50
+ timeInconsistencyScore: 0.9,
51
+ tlsSpoofingScore: 0.8 // NOUVEAU: Poids pour la détection de spoofing TLS
52
+ },
53
+ thresholds: { low: 20, medium: 45, high: 75, block: 95 },
54
+ patterns: {
55
+ velocityThreshold: 800,
56
+ burstThreshold: 1500,
57
+ scrapeThreshold: 1000,
58
+ historySize: 10,
59
+ minSamples: 5,
60
+ regularityThreshold: 50,
61
+ benfordThreshold: 0.15,
62
+ patternWeight: 80,
63
+ decayFactor: 0.9,
64
+ inactivityReset: 5000,
65
+ },
66
+ },
67
+ /**
68
+ * @summary **Strict Profile**
69
+ * @description An aggressive configuration for sensitive applications (e.g., financial services, admin panels). It uses lower suspicion thresholds and higher penalties for anomalies, prioritizing security over user convenience. All new devices are challenged by default.
70
+ */
71
+ strict: {
72
+ weights: {
73
+ historyScore: 0.4,
74
+ rotationScore: 0.6,
75
+ headerAnomalyScore: 0.2,
76
+ requestPatternScore: 0.8,
77
+ inconsistencyScore: 1.0,
78
+ behaviorScore: 0.8,
79
+ honeypotScore: 1.0,
80
+ crossLayerInconsistencyScore: 0.6,
81
+ timeInconsistencyScore: 1.0,
82
+ tlsSpoofingScore: 1.0 // NOUVEAU: Plus agressif pour le spoofing TLS
83
+ },
84
+ thresholds: { low: 10, medium: 35, high: 65, block: 90 },
85
+ patterns: {
86
+ velocityThreshold: 1000,
87
+ burstThreshold: 1800,
88
+ scrapeThreshold: 1200,
89
+ historySize: 15,
90
+ minSamples: 4,
91
+ regularityThreshold: 40,
92
+ benfordThreshold: 0.12,
93
+ patternWeight: 90,
94
+ decayFactor: 0.85,
95
+ inactivityReset: 4000,
96
+ },
97
+ challengeNewDevices: true, // Challenge all new devices
98
+ },
99
+ /**
100
+ * @summary **API Profile**
101
+ * @description Optimized for protecting API endpoints. This profile is highly sensitive to request patterns (velocity, bursts) and less reliant on browser-specific behavioral metrics. It's designed to quickly identify and throttle scrapers and automated clients.
102
+ */
103
+ api: {
104
+ weights: {
105
+ historyScore: 0.5,
106
+ rotationScore: 0.5,
107
+ headerAnomalyScore: 0.3,
108
+ requestPatternScore: 1.0, // Very high weight for API patterns
109
+ inconsistencyScore: 0.7,
110
+ behaviorScore: 0.2, // Lower weight, as browser behavior is not applicable
111
+ honeypotScore: 1.0,
112
+ crossLayerInconsistencyScore: 0.5,
113
+ timeInconsistencyScore: 0.8,
114
+ tlsSpoofingScore: 0.7 // NOUVEAU: Important pour les API
115
+ },
116
+ thresholds: { low: 25, medium: 50, high: 80, block: 95 },
117
+ patterns: {
118
+ velocityThreshold: 200, // APIs are expected to be fast
119
+ burstThreshold: 500,
120
+ scrapeThreshold: 400,
121
+ historySize: 20,
122
+ minSamples: 8,
123
+ regularityThreshold: 20,
124
+ benfordThreshold: 0.18,
125
+ patternWeight: 85,
126
+ decayFactor: 0.9,
127
+ inactivityReset: 10000,
128
+ },
129
+ isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json'),
130
+ }
131
+ ,
132
+ /**
133
+ * @summary **Blog Profile**
134
+ * @description Tuned for blogs and content-heavy websites. This profile focuses on detecting content scraping and comment spam by placing a high weight on request patterns and honeypot traps, while being more lenient on behavioral metrics typical of readers.
135
+ */
136
+ blog: {
137
+ weights: {
138
+ historyScore: 0.2,
139
+ rotationScore: 0.3,
140
+ headerAnomalyScore: 0.1,
141
+ requestPatternScore: 0.8, // High weight to detect content scraping
142
+ inconsistencyScore: 0.7,
143
+ behaviorScore: 0.5, // Less emphasis on complex interactions
144
+ honeypotScore: 1.0, // Crucial for comment spam
145
+ crossLayerInconsistencyScore: 0.4,
146
+ timeInconsistencyScore: 0.8,
147
+ tlsSpoofingScore: 0.6 // NOUVEAU: Moins critique pour les blogs
148
+ },
149
+ thresholds: { low: 25, medium: 55, high: 80, block: 95 },
150
+ patterns: {
151
+ velocityThreshold: 1000, // Readers can be fast
152
+ burstThreshold: 2000,
153
+ scrapeThreshold: 800, // Very sensitive to scraping patterns
154
+ historySize: 12,
155
+ minSamples: 5,
156
+ regularityThreshold: 60,
157
+ benfordThreshold: 0.16,
158
+ patternWeight: 85,
159
+ decayFactor: 0.92,
160
+ inactivityReset: 10000,
161
+ },
162
+ },
163
+ /**
164
+ * @summary **E-commerce Profile**
165
+ * @description A strict profile tailored for e-commerce sites. It's designed to combat inventory scalping, price scraping, and account takeover attempts by using high weights for request patterns and fingerprint inconsistency. It also challenges all new devices to increase the cost for bots.
166
+ */
167
+ ecommerce: {
168
+ weights: {
169
+ historyScore: 0.4,
170
+ rotationScore: 0.6,
171
+ headerAnomalyScore: 0.2,
172
+ // Scission du requestPatternScore pour un contrôle plus fin
173
+ velocityScore: 0.8, // Pénalise la vitesse globale
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"
177
+ inconsistencyScore: 1.0, // Crucial for preventing account takeover
178
+ behaviorScore: 0.8, // Important for checkout/login forms
179
+ honeypotScore: 1.0,
180
+ crossLayerInconsistencyScore: 0.7,
181
+ timeInconsistencyScore: 0.9,
182
+ tlsSpoofingScore: 0.9 // NOUVEAU: Très important pour l'e-commerce
183
+ },
184
+ thresholds: { low: 15, medium: 40, high: 70, block: 90 },
185
+ patterns: {
186
+ velocityThreshold: 500, // Bots are very fast
187
+ burstThreshold: 1000, // Detects rapid retries on the same product/action
188
+ scrapeThreshold: 600,
189
+ historySize: 15,
190
+ minSamples: 6,
191
+ regularityThreshold: 30,
192
+ benfordThreshold: 0.14,
193
+ patternWeight: 95,
194
+ decayFactor: 0.88,
195
+ inactivityReset: 3000,
196
+ },
197
+ challengeNewDevices: true, // New devices are suspicious in e-commerce
198
+ isApiRequest: (req) => req.path.startsWith('/api/cart') || req.path.startsWith('/api/stock') || req.path.startsWith('/api/checkout'),
199
+ }
200
+ };
201
+
202
+ /**
203
+ * Creates a security configuration based on a named profile, with optional overrides.
204
+ * @param {'balanced' | 'strict' | 'api'} [profileName='balanced'] - The name of the profile to use.
205
+ * @param {object} [overrides={}] - An object to deeply merge with the profile, allowing for customization.
206
+ * @returns {object} The final security configuration object.
207
+ */
208
+ export function createSecurityProfile(profileName = 'balanced', overrides = {}) { // eslint-disable-line no-unused-vars
209
+ const baseProfile = securityProfiles[profileName] || securityProfiles.balanced;
210
+ return deepMerge(baseProfile, overrides);
211
+ }
212
+
13
213
  /**
14
214
  * Retrieves the POW_SECRET from environment variables with appropriate checks.
15
215
  * @returns {string} The secret key.
@@ -36,48 +236,56 @@ const getPowSolverCode = () => {
36
236
  };
37
237
 
38
238
  /**
39
- * Calculates the JA3 fingerprint hash from the TLS Client Hello message.
40
- * JA3 is a more reliable way to identify client applications (e.g., a specific browser or a script)
41
- * based on the specifics of its TLS handshake.
239
+ * Extracts TLS fingerprints (JA3 and JA4) from request context.
240
+ * Prioritizes headers from reverse proxies (x-ja4-hash) and falls back to JA3 calculation
241
+ * from raw socket data if available.
42
242
  * @param {object} context - The request context, containing the raw request object.
43
- * @returns {string|null} The MD5 hash of the JA3 string, or null if it cannot be computed.
243
+ * @returns {{ja3: string|null, ja4: string|null}} An object containing JA3 and JA4 hashes.
44
244
  */
45
- function getJa3Hash(context) {
46
- // 1. Prefer the JA3 hash from a trusted reverse proxy (e.g., Nginx, Cloudflare).
47
- const ja3FromHeader = context.headers['x-ja3-hash'];
245
+ function getTlsFingerprint(context) {
246
+ let ja3 = null;
247
+ let ja4 = null;
248
+
249
+ // 1. Prefer JA4 hash from a trusted reverse proxy header.
250
+ const ja4FromHeader = context.headers['x-ja4-hash'];
251
+ if (ja4FromHeader) {
252
+ ja4 = ja4FromHeader;
253
+ }
254
+ // 2. Prefer JA3 hash from a trusted reverse proxy header.
255
+ const ja3FromHeader = context.headers['x-ja3-hash']; // Assuming a proxy might provide JA3 too
48
256
  if (ja3FromHeader) {
49
- return ja3FromHeader;
257
+ ja3 = ja3FromHeader;
50
258
  }
51
259
 
52
- // 2. Fallback to calculating from the raw socket if available (requires Node.js to handle TLS).
260
+ // 3. Fallback to calculating from the raw socket if available and if headers were not present.
53
261
  const clientHello = context.rawReq?.socket?.clientHello;
54
- if (!clientHello) {
55
- return null;
56
- }
57
-
58
- try {
59
- const { version, ciphers, extensions, ellipticCurves, ellipticCurvePointFormats } = clientHello;
262
+ if (clientHello && !ja3) { // Only calculate if ja3 is not already set
263
+ try {
264
+ const { version, ciphers, extensions, ellipticCurves, ellipticCurvePointFormats } = clientHello;
60
265
 
61
- // The official JA3 spec includes the TLS version.
62
- // Node.js provides it as a string like 'TLSv1.3', we need the corresponding decimal value.
63
- const tlsVersionMap = {
64
- 'TLSv1': 769, 'TLSv1.1': 770, 'TLSv1.2': 771, 'TLSv1.3': 772
65
- };
66
- const tlsVersionId = tlsVersionMap[version] || 0;
67
-
68
- const ja3String = [
69
- tlsVersionId,
70
- // The ciphers array from clientHello is an array of objects, not just IDs.
71
- Array.isArray(ciphers) ? ciphers.join('-') : '',
72
- extensions?.join('-') || '',
73
- ellipticCurves?.join('-') || '',
74
- ellipticCurvePointFormats?.join('-') || ''
75
- ].join(',');
76
-
77
- return crypto.createHash('md5').update(ja3String).digest('hex');
78
- } catch (e) {
79
- return null; // Could fail if clientHello structure is unexpected.
266
+ // The official JA3 spec includes the TLS version.
267
+ // Node.js provides it as a string like 'TLSv1.3', we need the corresponding decimal value.
268
+ const tlsVersionMap = {
269
+ 'TLSv1': 769, 'TLSv1.1': 770, 'TLSv1.2': 771, 'TLSv1.3': 772
270
+ };
271
+ const tlsVersionId = tlsVersionMap[version] || 0;
272
+
273
+ const ja3String = [
274
+ tlsVersionId,
275
+ // The ciphers array from clientHello is an array of objects, not just IDs.
276
+ Array.isArray(ciphers) ? ciphers.join('-') : '',
277
+ extensions?.join('-') || '',
278
+ ellipticCurves?.join('-') || '',
279
+ ellipticCurvePointFormats?.join('-') || ''
280
+ ].join(',');
281
+
282
+ ja3 = crypto.createHash('md5').update(ja3String).digest('hex');
283
+ } catch (e) {
284
+ // Could fail if clientHello structure is unexpected.
285
+ ja3 = null;
286
+ }
80
287
  }
288
+ return { ja3, ja4 };
81
289
  }
82
290
  /**
83
291
  * Creates a stable hash based on device characteristics, independent of the IP.
@@ -109,7 +317,7 @@ export function getDeviceHash(context) {
109
317
  return getCompositeDeviceHash(context);
110
318
  }
111
319
 
112
- export function getCompositeDeviceHash(context) {
320
+ function getCompositeDeviceHash(context) {
113
321
  const srv = new FingerprintBuilder();
114
322
 
115
323
  // Si un fingerprint client est fourni, on l'intègre comme un signal fort,
@@ -127,70 +335,55 @@ export function getCompositeDeviceHash(context) {
127
335
  // 1. SIGNAL FORT: User Agent (poids élevé)
128
336
  const ua = context.headers["user-agent"];
129
337
  if (ua) {
130
- srv.add("ua", ua);
338
+ srv.add("ua", ua); // User-Agent
131
339
  }
132
340
 
133
- // 2. SIGNAL FORT: JA3 TLS Fingerprint
134
- const ja3 = getJa3Hash(context);
341
+ // 2. SIGNAUX DE BAS NIVEAU (Transport & Réseau) - Très fiables si fournis par un proxy
342
+ const { ja3, ja4 } = getTlsFingerprint(context);
135
343
  if (ja3) srv.add("ja3", ja3);
344
+ if (ja4) srv.add("ja4", ja4);
345
+
346
+ const h2Fingerprint = context.headers['x-http2-fingerprint'];
347
+ if (h2Fingerprint) srv.add("h2", h2Fingerprint);
348
+
349
+ const tcpFingerprint = context.headers['x-tcp-fingerprint'];
350
+ if (tcpFingerprint) srv.add("tcp", tcpFingerprint);
351
+
352
+ // 3. SIGNAUX DE HAUT NIVEAU (Applicatif) - Moins fiables, mais utiles pour la corroboration
353
+ const headersToCapture = {
354
+ "ch_ua": "sec-ch-ua",
355
+ "ch_platform": "sec-ch-ua-platform",
356
+ "ch_mobile": "sec-ch-ua-mobile",
357
+ "ch_model": "sec-ch-ua-model",
358
+ "ch_arch": "sec-ch-ua-arch",
359
+ "ch_bitness": "sec-ch-ua-bitness",
360
+ "upgrade_req": "upgrade-insecure-requests",
361
+ "accept_lang": "accept-language",
362
+ "accept_enc": "accept-encoding",
363
+ "accept": "accept"
364
+ };
136
365
 
137
- // 3. SIGNAL MOYEN: Client Hints (modern browsers)
138
- if (context.headers["sec-ch-ua"]) {
139
- srv.add("ch_ua", context.headers["sec-ch-ua"]);
140
- }
141
- if (context.headers["sec-ch-ua-platform"]) {
142
- srv.add("ch_platform", context.headers["sec-ch-ua-platform"]);
143
- }
144
- if (context.headers["sec-ch-ua-mobile"]) {
145
- srv.add("ch_mobile", context.headers["sec-ch-ua-mobile"]);
146
- }
147
- if (context.headers["sec-ch-ua-model"]) {
148
- srv.add("ch_model", context.headers["sec-ch-ua-model"]);
149
- }
150
- if (context.headers["sec-ch-ua-arch"]) {
151
- srv.add("ch_arch", context.headers["sec-ch-ua-arch"]);
152
- }
153
- if (context.headers["sec-ch-ua-bitness"]) {
154
- srv.add("ch_bitness", context.headers["sec-ch-ua-bitness"]);
366
+ for (const [key, headerName] of Object.entries(headersToCapture)) {
367
+ const headerValue = context.headers[headerName];
368
+ if (headerValue) {
369
+ srv.add(key, headerValue);
370
+ }
155
371
  }
156
372
 
157
- // 4. SIGNAL MOYEN: HTTP Version et protocole
373
+ // 4. SIGNAUX DE CONTEXTE (HTTP Version, Cookies)
158
374
  if (context.httpVersion) {
159
375
  srv.add("http_ver", context.httpVersion);
160
376
  }
161
- if (context.headers["upgrade-insecure-requests"]) {
162
- srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
163
- }
164
-
165
- // 11. SIGNAL AVANCÉ: Cookies (si disponible)
166
377
  if (context.cookies) {
167
378
  const cookieKeys = Object.keys(context.cookies).sort().join(',');
168
- srv.add("cookie_keys", cookieKeys);
169
- }
170
-
171
- // 12. SIGNAL AVANCÉ: Format de la requête
172
- if (context.rawHeaders) {
173
- // Vérifier des headers spécifiques qui indiquent le client
174
- const clientHeaders = ['x-requested-with', 'x-forwarded-for', 'x-real-ip', 'cf-connecting-ip'];
175
- clientHeaders.forEach(h => {
176
- if (context.headers[h]) {
177
- srv.add(h.replace(/-/g, '_'), context.headers[h]);
178
- }
179
- });
180
- }
181
-
182
- // 13. OPTIONNEL: IP (version simplifiée pour les réseaux partagés)
183
- // Ne pas inclure l'IP complète, mais un hash du réseau /24 ou /16
184
- // pour détecter les changements de réseau tout en protégeant la vie privée
185
- const ip = context.clientIp || context.headers['x-forwarded-for']?.split(',')[0]?.trim();
186
- if (ip && isPrivateIp(ip)) {
187
- // Pour les IP privées, on peut prendre le /24
188
- const networkHash = hashNetwork(ip, 24);
189
- srv.add("network", networkHash);
379
+ if (cookieKeys) {
380
+ srv.add("cookie_keys", cookieKeys);
381
+ }
190
382
  }
191
383
 
192
384
  return srv.toString();
193
385
  }
386
+ export { getCompositeDeviceHash };
194
387
 
195
388
  // Fonctions utilitaires
196
389
  function parseUserAgent(ua) {
@@ -700,6 +893,17 @@ function getBehaviorScore(context) {
700
893
  score += 40;
701
894
  }
702
895
 
896
+ // 3. (NOUVEAU) Analyse de la longueur de l'historique de navigation.
897
+ // Un historique court est suspect (nouvel onglet, bot), un historique long est un bon signe.
898
+ if (typeof metrics.historyLength === 'number') {
899
+ if (metrics.historyLength === 1) {
900
+ score += 15; // Légère pénalité pour un historique de session vierge.
901
+ } else if (metrics.historyLength >= 5) {
902
+ score -= 20; // Bonus : un historique long est un fort indicateur humain.
903
+ } else if (metrics.historyLength >= 2) {
904
+ score -= 10; // Petit bonus pour une navigation de base.
905
+ }
906
+ }
703
907
  // 3. Vérification de la plausibilité et de la distribution des métriques.
704
908
  // Un bot pourrait envoyer des valeurs aléatoires, mais elles ne suivront probablement pas
705
909
  // des distributions naturelles (comme la loi de Benford pour les premiers chiffres).
@@ -728,7 +932,7 @@ function getBehaviorScore(context) {
728
932
  if (keystrokeDeviation > 0.15) score += 40;
729
933
  }
730
934
 
731
- return { behaviorScore: Math.min(100, score) };
935
+ return { behaviorScore: Math.max(0, Math.min(100, score)) }; // Assure que le score reste entre 0 et 100
732
936
  } catch (e) {
733
937
  return { behaviorScore: 10 }; // En-tête malformé = légèrement suspect.
734
938
  }
@@ -812,6 +1016,66 @@ function getCrossLayerInconsistency(context) {
812
1016
  }
813
1017
  }
814
1018
 
1019
+ /**
1020
+ * Calcule un score d'incohérence entre les données du fingerprint TLS (JA3/JA4) et les en-têtes serveur (User-Agent).
1021
+ * Cela permet de détecter le spoofing de fingerprint TLS.
1022
+ * @param {object} context - Le contexte de la requête.
1023
+ * @returns {{tlsSpoofingScore: number}}
1024
+ */
1025
+ function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerprint) {
1026
+ let score = 0;
1027
+ const { ja3, ja4 } = getTlsFingerprintFn(context) || { ja3: null, ja4: null }; // Defensive check
1028
+ const ua = context.headers["user-agent"] || '';
1029
+
1030
+ // Si un fingerprint TLS est présent, mais le User-Agent est générique ou manquant.
1031
+ if ((ja3 || ja4) && (!ua || ua.length < 10 || ua.toLowerCase().includes('python') || ua.toLowerCase().includes('curl'))) {
1032
+ score += 50; // Forte suspicion
1033
+ }
1034
+
1035
+ // Plus complexe: Comparer le navigateur/OS déduit du JA3/JA4 avec le User-Agent.
1036
+ // Ceci nécessiterait une base de données de JA3/JA4 connus ou une logique de parsing avancée.
1037
+ // Pour l'instant, une implémentation simplifiée:
1038
+ // Si JA3/JA4 est présent et le UA est un navigateur connu, mais ils ne correspondent pas.
1039
+ if (ja3 && ua) {
1040
+ // Exemple très simplifié: si JA3 est typique de Chrome, mais UA est Firefox.
1041
+ // Ceci est une heuristique et peut générer des faux positifs sans une base de données robuste.
1042
+ // JA3 de Chrome commence souvent par 'e' (TLS 1.3) ou 'd' (TLS 1.2)
1043
+ const isJa3Chrome = ja3.startsWith('e') || ja3.startsWith('d');
1044
+ const isUaChrome = ua.includes('Chrome') && !ua.includes('Edg');
1045
+ // JA3 de Firefox commence souvent par 'c' (TLS 1.3) ou 'b' (TLS 1.2)
1046
+ const isJa3Firefox = ja3.startsWith('c') || ja3.startsWith('b');
1047
+ const isUaFirefox = ua.includes('Firefox');
1048
+
1049
+ if ((isJa3Chrome && isUaFirefox) || (isJa3Firefox && isUaChrome)) {
1050
+ score += 80; // Très forte incohérence
1051
+ }
1052
+ }
1053
+ // On pourrait ajouter des vérifications similaires pour JA4 si on avait une base de données de JA4.
1054
+
1055
+ return { tlsSpoofingScore: Math.min(100, score) };
1056
+ }
1057
+
1058
+
1059
+ /**
1060
+ * Calcule un score basé sur la détection explicite de frameworks d'automatisation.
1061
+ * @param {object} context - Le contexte de la requête.
1062
+ * @returns {{botScore: number}}
1063
+ */
1064
+ function getBotScore(context) {
1065
+ const clientFpString = context.headers['x-device-fingerprint'];
1066
+ if (!clientFpString) return { botScore: 0 };
1067
+
1068
+ try {
1069
+ const clientFpMap = new Map(clientFpString.split("|").map(part => part.split(":")));
1070
+ // Pénalité maximale si l'un des marqueurs d'automatisation est présent.
1071
+ if (clientFpMap.has('bot') || clientFpMap.has('cdp')) {
1072
+ return { botScore: 100 };
1073
+ }
1074
+ } catch (e) { /* Ignorer les erreurs de parsing */ }
1075
+
1076
+ return { botScore: 0 };
1077
+ }
1078
+
815
1079
  /**
816
1080
  * Analyzes server-side request patterns for a given device to detect bot-like behavior.
817
1081
  * This is a stateful check that looks for repetitive or unnaturally fast requests.
@@ -1150,6 +1414,12 @@ export const getSuspicionVector = async (context, securityConfig) => {
1150
1414
  // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
1151
1415
  const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
1152
1416
 
1417
+ // NOUVEAU: On calcule le score de spoofing TLS.
1418
+ const { tlsSpoofingScore } = getTlsSpoofingScore(context);
1419
+
1420
+ // NOUVEAU: On appelle getBotScore pour détecter les marqueurs d'automatisation.
1421
+ const { botScore } = getBotScore(context);
1422
+
1153
1423
  // NOUVEAU: On calcule le score d'incohérence temporelle.
1154
1424
  const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'));
1155
1425
 
@@ -1168,7 +1438,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
1168
1438
  deviceData.ips = new Set(deviceData.ips);
1169
1439
  }
1170
1440
  // Le vecteur de suspicion est maintenant complet.
1171
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore };
1441
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore };
1172
1442
  };
1173
1443
 
1174
1444
  // A residential user can change networks (home, 4G, public wifi).
@@ -1489,7 +1759,9 @@ export class FingerprintEngine {
1489
1759
  (suspicionVector.inconsistencyScore || 0) * (weights.inconsistencyScore || 0) +
1490
1760
  (suspicionVector.honeypotScore || 0) * (weights.honeypotScore || 0) +
1491
1761
  (suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
1762
+ (suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
1492
1763
  (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
1764
+ (suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
1493
1765
  (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0);
1494
1766
 
1495
1767
  return Math.min(100, score);
@@ -1567,6 +1839,78 @@ export class FingerprintEngine {
1567
1839
  _isIpInAllowlist(clientIp) {
1568
1840
  return this._allowlist.check(clientIp);
1569
1841
  }
1842
+ /**
1843
+ * Checks if the request's host and path match an entry in the host+path allowlist.
1844
+ * @private
1845
+ * @param {string} requestHost - The host from the request headers.
1846
+ * @param {string} requestPath - The path of the incoming request.
1847
+ * @returns {boolean} True if the combination is in the allowlist.
1848
+ */
1849
+ _isHostPathInAllowlist(requestHost, requestPath) {
1850
+ const { whitelist = [] } = this.securityConfig;
1851
+ const hostPathRule = whitelist.find(rule => rule.type === 'host_path_allowlist');
1852
+
1853
+ if (!hostPathRule || !hostPathRule.entries || hostPathRule.entries.length === 0) {
1854
+ return false;
1855
+ }
1856
+
1857
+ for (const entry of hostPathRule.entries) {
1858
+ // Find the first slash to separate host and path
1859
+ const firstSlashIndex = entry.indexOf('/');
1860
+ if (firstSlashIndex === -1) continue; // Invalid entry
1861
+
1862
+ const hostPattern = entry.substring(0, firstSlashIndex);
1863
+ const pathPattern = entry.substring(firstSlashIndex);
1864
+
1865
+ // Check if the request host matches the host pattern
1866
+ if (requestHost !== hostPattern) {
1867
+ continue;
1868
+ }
1869
+
1870
+ // Check if the request path matches the path pattern (with wildcard support)
1871
+ if (pathPattern.endsWith('*')) {
1872
+ const basePath = pathPattern.slice(0, -1);
1873
+ if (requestPath.startsWith(basePath)) {
1874
+ return true; // Wildcard match
1875
+ }
1876
+ } else if (requestPath === pathPattern) {
1877
+ return true; // Exact match
1878
+ }
1879
+ }
1880
+ return false;
1881
+ }
1882
+ /**
1883
+ * Checks if the request path matches any entry in the path allowlist.
1884
+ * Supports simple wildcards (*) at the end of a path.
1885
+ * @private
1886
+ * @param {string} requestPath - The path of the incoming request.
1887
+ * @returns {boolean} True if the path is in the allowlist.
1888
+ */
1889
+ _isPathInAllowlist(requestPath) {
1890
+ const { whitelist = [] } = this.securityConfig;
1891
+ const pathAllowlistRule = whitelist.find(rule => rule.type === 'path_allowlist');
1892
+
1893
+ if (!pathAllowlistRule || !pathAllowlistRule.entries || pathAllowlistRule.entries.length === 0) {
1894
+ return false;
1895
+ }
1896
+
1897
+ for (const entry of pathAllowlistRule.entries) {
1898
+ if (entry.endsWith('*')) {
1899
+ // Handle wildcard matching
1900
+ const base = entry.slice(0, -1);
1901
+ if (requestPath.startsWith(base)) {
1902
+ return true;
1903
+ }
1904
+ } else {
1905
+ // Handle exact path matching
1906
+ if (requestPath === entry) {
1907
+ return true;
1908
+ }
1909
+ }
1910
+ }
1911
+
1912
+ return false;
1913
+ }
1570
1914
  /**
1571
1915
  * Verifies if a request comes from a legitimate, whitelisted bot (e.g., Googlebot)
1572
1916
  * using reverse and forward DNS lookups. The result is cached.
@@ -1655,6 +1999,19 @@ export class FingerprintEngine {
1655
1999
  return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'hostname_allowlist' } };
1656
2000
  }
1657
2001
 
2002
+ // 3. Check host+path based allowlist.
2003
+ const requestHost = requestContext.headers?.host;
2004
+ if (requestHost && this._isHostPathInAllowlist(requestHost, path)) {
2005
+ this._log('Host and path in allowlist - allowing request', { host: requestHost, path });
2006
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'host_path_allowlist' } };
2007
+ }
2008
+
2009
+ // 3. Check path-based allowlist.
2010
+ if (this._isPathInAllowlist(path)) {
2011
+ this._log('Path in allowlist - allowing request', { path });
2012
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'path_allowlist' } };
2013
+ }
2014
+
1658
2015
  const { pow_nonce } = query;
1659
2016
 
1660
2017
  // Honeypot: Direct probing of challenge endpoints is highly suspicious.
@@ -1790,7 +2147,7 @@ export class FingerprintEngine {
1790
2147
  // We compare the fingerprint of the request that triggered the challenge
1791
2148
  // with the fingerprint of the request that is submitting the solution.
1792
2149
  // They should be very similar.
1793
- similarity = FingerprintBuilder.compare(originalFingerprint, getCompositeDeviceHash(requestContext));
2150
+ similarity = FingerprintBuilder.compare(originalFingerprint, solverFingerprint);
1794
2151
  }
1795
2152
 
1796
2153
  if (similarity < similarityThreshold) {
@@ -2533,6 +2890,8 @@ export const __internal = {
2533
2890
  getCrossLayerInconsistency, // Expose for testing
2534
2891
  // Expose page generators for security testing
2535
2892
  getTimeInconsistencyScore,
2893
+ getTlsFingerprint, // NOUVEAU: Expose pour les tests
2894
+ getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
2536
2895
  generateCpuTargetChallengePage,
2537
2896
  generateCombinedPoWChallengePage,
2538
2897
  };