@anonympins/fingerprint 0.0.6 → 0.0.8

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/README.md CHANGED
@@ -13,7 +13,7 @@ This system identifies and slows down bots and automated scripts by evaluating t
13
13
 
14
14
  The process unfolds in three steps:
15
15
 
16
- 1. **Identification & Fingerprinting**: A unique fingerprint is generated for each device based on browser characteristics (client-side) and request headers (server-side). A `device_id` cookie is used to track the device over time.
16
+ 1. **Identification & Fingerprinting**: A unique fingerprint is generated for each device. This combines a client-side browser fingerprint, server-side request headers, and the **JA3 fingerprint** from the TLS handshake, which reliably identifies the underlying HTTP client library (e.g., Chrome vs. a Python script). A `device_id` cookie is used to track the device over time.
17
17
  2. **Suspicion Score Calculation**: Several indicators are analyzed to calculate a suspicion score:
18
18
  * **Header Anomalies**: Missing `User-Agent`, `Accept-Language`, etc.
19
19
  * **Device Behavior**: Rapid fingerprint changes (User-Agent rotation).
@@ -34,6 +34,7 @@ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie
34
34
  - **Pluggable Datastore**: Supports external datastores like Redis for state persistence and scalability across multiple server instances.
35
35
  - **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`. The datastore must support setting a Time-To-Live (TTL) for challenge secrets.
36
36
  - **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure ticket validation.
37
+ - **Bot Whitelisting**: Includes a DNS-based verification mechanism to reliably identify and whitelist legitimate crawlers like Googlebot and Bingbot, preventing them from being challenged. The results are cached for optimal performance.
37
38
  - **Automatic Parameter Tuning**: Includes a genetic algorithm-based optimizer (`startThresholdAutoTuning`) that analyzes real traffic to dynamically adjust not only suspicion thresholds (`low`, `medium`, `high`) but also the parameters for behavioral pattern detection, improving accuracy and reducing false positives over time.
38
39
 
39
40
  ## Installation and Usage
@@ -60,7 +61,7 @@ The `powMiddleware` requires a configuration object defining the weights of susp
60
61
  import express from 'express';
61
62
  import bodyParser from 'body-parser';
62
63
  import cookieParser from 'cookie-parser';
63
- import { powMiddleware /*, configurePow */ } from './fingerprint.js'; // Adjust the path
64
+ import { powMiddleware, default_whitelist } from './fingerprint.js'; // Adjust the path
64
65
 
65
66
  const app = express();
66
67
  app.use(cookieParser());
@@ -104,8 +105,54 @@ const securityConfig = {
104
105
  // A request to one of these paths will immediately flag the device as malicious.
105
106
  trapUrls: ['/wp-admin', '/.env', '/admin.php', '/phpmyadmin'], // (Optional)
106
107
  // Automatically detect common SQL/NoSQL injection and RCE patterns in request values. (Optional, default: true)
107
- detectInjections: true
108
+ detectInjections: true,
109
+ // (Optional) Plug in external, more robust analyzers. This allows you to extend the default detection with specialized libraries (e.g., WAFs, anti-spam) or your own custom logic.
110
+ // Each function receives an object with all query and body data and should return `true` if a threat is detected.
111
+ analyzers: [
112
+ // Example 1: Using a general-purpose WAF library.
113
+ // (npm install generic-waf)
114
+ (data) => {
115
+ const WAF = require('generic-waf');
116
+ const waf = new WAF();
117
+ // This WAF expects a string, so we stringify the data to check all values at once.
118
+ return waf.isMalicious(JSON.stringify(data));
119
+ },
120
+ // Example 2: Using a specialized library for XSS detection.
121
+ // (npm install xss)
122
+ (data) => {
123
+ const xss = require('xss');
124
+ const originalData = JSON.stringify(data);
125
+ // If the sanitized string is different from the original, it means malicious HTML/JS was found and removed.
126
+ return xss(originalData) !== originalData;
127
+ },
128
+ // Example 3: A custom function to detect specific keywords (e.g., for anti-spam).
129
+ (data) => {
130
+ const spamKeywords = ['viagra', 'free money', 'crypto pump'];
131
+ const dataString = JSON.stringify(data).toLowerCase();
132
+ return spamKeywords.some(keyword => dataString.includes(keyword));
133
+ }
134
+ ]
108
135
  },
136
+ // (Optional) Whitelisting configuration.
137
+ whitelist: [
138
+ // Option 1: Static IP Allowlist.
139
+ // A simple array of IPs or CIDR ranges that are always allowed, bypassing all checks.
140
+ // Useful for internal tools, trusted partners, or monitoring services.
141
+ // This check is performed first for maximum efficiency.
142
+ { type: 'allowlist', entries: [
143
+ '192.168.1.100', // A specific internal IP
144
+ '203.0.113.0/24', // A partner's network range
145
+ '2001:db8::/32' // An IPv6 range
146
+ ]},
147
+ // Option 2: DNS-verified bots (e.g., search engine crawlers).
148
+ // This uses a secure DNS lookup (reverse then forward) to verify the bot's identity.
149
+ // The result is cached per IP to avoid repeated DNS lookups.
150
+ // You can use the provided default list, which contains over 50 common bots, and extend it.
151
+ ...default_whitelist(), // Use the defaults
152
+ { userAgent: 'MyIndustrySpecificBot', hostnameSuffix: '.my-bot-verifier.com' }, // Add a custom bot
153
+ ],
154
+ // Or, if you only want the defaults:
155
+ // whitelist: default_whitelist(),
109
156
  // The logger is required for auto-tuning. It collects data on requests.
110
157
  logger: (log) => trafficData.push(log),
111
158
  // (Optional) Configuration for the automatic threshold and pattern tuning.
@@ -152,6 +199,8 @@ The main Express middleware. It orchestrates identification, suspicion calculati
152
199
  #### `configureStore(store)`
153
200
  Allows replacing the in-memory store with an external datastore (like Redis) for persistence and scaling.
154
201
 
202
+ See the Datastore Integration Guide for a complete example of creating a Redis store.
203
+
155
204
  ```javascript
156
205
  import { configureStore } from './fingerprint.js';
157
206
  import { createRedisStore } from './redis-store.js'; // Assuming a redis store implementation exists
@@ -217,6 +266,117 @@ const signature = generateRequestSignature({ action: 'update', id: 123 });
217
266
  **Security Note:** This function is powerful but should be used with caution. The `secret` must be managed securely. It is typically used with a temporary, single-use secret provided by the server for a specific action, rather than a long-lived shared secret embedded in the client-side code.
218
267
 
219
268
  ---
269
+
270
+ ## Why Use the Client-Side Library? The Client + Server Synergy
271
+
272
+ At first glance, client-side checks might seem redundant with server-side honeypots and analysis. In reality, they form two complementary and synergistic lines of defense.
273
+
274
+ Imagine your server is a fortified castle:
275
+
276
+ - **Server-Side Defense (the guards on the walls):** They inspect anyone who knocks on the gate. They are effective, but this means the enemy is already at your door, and your resources (guards) are mobilized for every interaction, legitimate or not.
277
+ - **Client-Side Defense (scouts and traps in the forest):** They detect suspicious movements and neutralize threats *before* they even reach the castle walls. This saves the castle's resources for genuine visitors.
278
+
279
+ The client-side library (`fingerprint.client.js`) gives your server "eyes and ears" where it was previously blind, offering three major advantages:
280
+
281
+ 1. **Early Detection & Resource Savings:** A bot filling a client-side honeypot is flagged in its own browser. The server can then immediately block it based on the `X-Behavior-Metrics` header, saving CPU, memory, and bandwidth that would have been wasted processing a malicious request.
282
+ 2. **Richer Behavioral Data:** The server cannot see how a user interacts with a page. The client-side library can detect non-human behavior (no mouse movement, instant form fills) that is impossible to spot from the server alone.
283
+ 3. **More Robust Fingerprinting:** Server-side signals (IP, User-Agent) are easy to spoof. Client-side fingerprinting adds much stronger, hardware-based signals (Canvas, WebGL, CPU cores) that are significantly harder for bots to fake consistently.
284
+
285
+ ### Strengths at a Glance
286
+
287
+ | Feature | Server-Side Only Approach | Client + Server Approach (with the library) |
288
+ | :---------------------- | :------------------------------------------------------ | :------------------------------------------------------------------------- |
289
+ | **Detection Point** | **Reactive** (after the request is received) | **Proactive** (before or during the request) |
290
+ | **Resource Usage** | Server is engaged for every request, good or bad. | Client-side pre-filtering saves significant server resources. |
291
+ | **Behavioral Analysis** | Limited to request velocity and sequence. | Rich data: mouse movement, typing rhythm, interaction with hidden elements. |
292
+ | **Fingerprint Evasion** | **Easy** for bots to spoof headers and rotate IPs. | **Hard** for bots to fake hardware-level fingerprints (Canvas/WebGL). |
293
+ | **Overall Strategy** | Guards at the gate. | Scouts in the field + Guards at the gate. |
294
+
295
+ In short, the client-side library is not an alternative, but a **force multiplier** for the server-side defenses.
296
+
297
+ ### Client-Side Integration: The `initializeClient` function
298
+
299
+ To simplify setup, all client-side features can be enabled and configured through a single, unified function: `initializeClient(config)`. This is the recommended approach.
300
+
301
+ ```javascript
302
+ import { initializeClient } from './path/to/fingerprint.client.js';
303
+
304
+ // --- EXAMPLES ---
305
+
306
+ // Example 1: Enable all default protections and protect same-origin fetch requests.
307
+ initializeClient({
308
+ honeypots: ['email_confirm', 'user_nickname'], // Your honeypot field names
309
+ fetch: {} // An empty object enables fetch protection for same-origin
310
+ });
311
+
312
+ // Example 2: Enable everything and protect a specific API domain.
313
+ initializeClient({
314
+ honeypots: ['email_confirm'],
315
+ fetch: {
316
+ targetDomains: ['api.yourdomain.com']
317
+ }
318
+ });
319
+
320
+ // Example 3: Enable only mouse and keystroke tracking, without patching fetch.
321
+ initializeClient({
322
+ mouse: true,
323
+ keystrokes: true
324
+ });
325
+
326
+ // Example 4: Disable keystroke tracking but keep other defaults.
327
+ initializeClient({
328
+ keystrokes: false,
329
+ fetch: {}
330
+ });
331
+ ```
332
+
333
+ ### Client-Side Behavioral Analysis
334
+
335
+ The following functions, available in `fingerprint.client.js`, allow for proactive, client-side detection of bot-like behavior. They collect metrics on user interaction which can be sent to the server for more accurate suspicion scoring. The server-side logic to interpret these metrics (via the `X-Behavior-Metrics` header) would need to be implemented as part of a custom scoring extension.
336
+
337
+ #### `startKeystrokeDynamicsTracker()`
338
+ *Client-side function only.* Starts tracking the timing between keystrokes. The average latency between key presses is a strong behavioral indicator. Humans have a natural, somewhat variable typing rhythm, whereas bots often simulate keystrokes with a fixed, unnaturally consistent delay, or paste text instantly (zero latency).
339
+
340
+ #### `startMouseEntropyTracker()`
341
+ *Client-side function only.* Starts tracking mouse movements on the page. It calculates a simple entropy score based on movement patterns. Human mouse movements are typically chaotic, whereas bots often have linear or no movement at all. This should be called once when your application's main component mounts.
342
+
343
+ #### `initializeHoneypots(fieldNames)`
344
+ *Client-side function only.* Sets up "traps" on hidden form fields. If a script automatically fills one of these fields, it's immediately flagged as a bot on the client side.
345
+
346
+ This provides a proactive, first-line defense against simple bots. By setting up traps directly in the browser, you can detect a bot the moment it interacts with a hidden field, rather than waiting for it to submit a form and consume server resources. This detection is then reported to the server via the `X-Behavior-Metrics` header, allowing for an immediate and efficient block.
347
+
348
+ - `fieldNames`: An array of strings corresponding to the `name` attributes of the honeypot input fields in your HTML.
349
+
350
+ ### Advanced: Manual Wrapping with `protectedFetch`
351
+
352
+ If you prefer not to modify global functions or need fine-grained control over which requests are protected, you can use the `protectedFetch` wrapper. You must use this function instead of the standard `fetch` for your API calls.
353
+
354
+ - **`protectedFetch(resource, options)`**: A wrapper around the native `fetch` API that automatically enriches requests with security headers. It adds:
355
+ - `X-Device-Fingerprint`: The client's device fingerprint.
356
+ - `X-Behavior-Metrics`: A JSON string containing the metrics collected by the behavioral trackers (e.g., mouse entropy, honeypot interaction).
357
+
358
+ **Example:**
359
+
360
+ ```javascript
361
+ import {
362
+ initializeClient,
363
+ protectedFetch
364
+ } from './path/to/fingerprint.client.js';
365
+
366
+ // Start tracking user behavior as soon as the app loads.
367
+ // Note: You still need to initialize the trackers even if you use protectedFetch manually.
368
+ initializeClient({ fetch: false }); // Disables automatic fetch patching
369
+
370
+ // Now, use protectedFetch for your specific API calls.
371
+ async function submitForm(data) {
372
+ const response = await protectedFetch('/api/submit-data', {
373
+ method: 'POST',
374
+ body: JSON.stringify(data),
375
+ headers: {'Content-Type': 'application/json'}
376
+ });
377
+ }
378
+ ```
379
+
220
380
  ## Advanced Features
221
381
 
222
382
  ### Architecture: `FingerprintEngine`
@@ -234,22 +394,23 @@ Although not exported for direct public use, understanding its role can be usefu
234
394
 
235
395
  While `powMiddleware` is convenient for Express, you can use the `FingerprintEngine` directly in any Node.js server environment (e.g., native `http`, Fastify, Koa). This gives you full control over the request/response cycle.
236
396
 
237
- The engine is available via the internal exports: `import { __internal } from './fingerprint.js'`.
397
+ **For concrete examples with Koa and Fastify, see our Framework Integration Guide.**
398
+
399
+ The engine is a named export from the main module.
238
400
 
239
401
  **Workflow:**
240
402
 
241
403
  1. **Instantiate the Engine**: Create an instance with your `securityConfig`.
242
404
  2. **Build the `requestContext`**: On each request, manually create a context object. It must include `clientIp`, `path`, `cookies`, `query`, `headers`, and mock `rawReq`/`rawRes` objects for cookie handling.
243
- 3. **Process the Request**: Call `engine.processRequest(requestContext)`.
405
+ 3. **Process the Request**: Call `engine.processRequest(requestContext)`. This method is asynchronous.
244
406
  4. **Handle the Decision**: The engine returns a decision object (`{ action: 'challenge' | 'redirect' | 'next', ... }`). You are responsible for implementing the corresponding HTTP response.
245
407
 
246
408
  **Example with native Node.js `http` server:**
247
409
 
248
410
  ```javascript
249
411
  import http from 'http';
250
- import { __internal } from './fingerprint.js'; // Adjust path
412
+ import { FingerprintEngine } from './fingerprint.js'; // Adjust path
251
413
 
252
- const { FingerprintEngine } = __internal;
253
414
  const securityConfig = { /* ... your config ... */ };
254
415
  const engine = new FingerprintEngine(securityConfig);
255
416
 
@@ -0,0 +1,399 @@
1
+ import { cyrb53, FingerprintBuilder } from './fingerprint.builder.js';
2
+
3
+ const ClientLibrary = {
4
+ // Cache pour éviter de recalculer les constantes (Hardware, etc.)
5
+ _cachedBuilder: null,
6
+ /**
7
+ * Génère l'empreinte de l'appareil actuel.
8
+ */
9
+ getDeviceFingerprint() {
10
+ if (typeof window === "undefined") {
11
+ console.error("getDeviceFingerprint can only be called on the client-side.");
12
+ return "";
13
+ }
14
+
15
+ if (!this._cachedBuilder) {
16
+ const nav = window.navigator;
17
+ const screen = window.screen;
18
+
19
+ this._cachedBuilder = new FingerprintBuilder();
20
+
21
+ // 1. Hardware (Très stable) : Cœurs, RAM, GPU (si dispo via canvas), Touch
22
+ this._cachedBuilder.add(
23
+ "hw",
24
+ `${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
25
+ );
26
+
27
+ // 2. Geo/Locale (Stable sauf voyage/VPN) : Timezone, Langue
28
+ this._cachedBuilder.add(
29
+ "geo",
30
+ `${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
31
+ );
32
+
33
+ // 3. Screen (Stable sauf changement moniteur/zoom) : Dimensions, ColorDepth
34
+ this._cachedBuilder.add(
35
+ "scr",
36
+ `${screen.width}x${screen.height}_${screen.colorDepth}`,
37
+ );
38
+
39
+ // 4. Platform (Stable) : OS, Engine
40
+ this._cachedBuilder.add("os", nav.platform);
41
+
42
+ // 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
43
+ try {
44
+ const canvas = document.createElement("canvas");
45
+ const gl =
46
+ canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
47
+ if (gl) {
48
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
49
+ if (debugInfo) {
50
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
51
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
52
+ this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
53
+ }
54
+ }
55
+ } catch (e) {
56
+ }
57
+
58
+ // 6. Canvas Fingerprinting (Rendering quirks)
59
+ try {
60
+ const canvas = document.createElement("canvas");
61
+ const ctx = canvas.getContext("2d");
62
+ if (ctx) {
63
+ canvas.width = 200;
64
+ canvas.height = 50;
65
+ ctx.textBaseline = "alphabetic";
66
+ ctx.font = "14px 'Arial'";
67
+ ctx.fillStyle = "#f60";
68
+ ctx.fillRect(125, 1, 62, 20);
69
+ ctx.fillStyle = "#069";
70
+ ctx.fillText("fingerprint", 2, 15);
71
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
72
+ ctx.fillText("fingerprint", 4, 17);
73
+ this._cachedBuilder.add("cvs", canvas.toDataURL());
74
+ }
75
+ } catch (e) {
76
+ }
77
+
78
+ // 7. Bot Detection (Indication cachée)
79
+ if (nav.webdriver) this._cachedBuilder.add("bot", "true");
80
+ }
81
+
82
+ return this._cachedBuilder.toString();
83
+ },
84
+
85
+ /**
86
+ * Génère une signature de requête incluant le contexte.
87
+ * @param {object} payload
88
+ */
89
+ generateRequestSignature(payload = {}) {
90
+ const deviceFp = this.getDeviceFingerprint();
91
+ const sortedPayload = Object.keys(payload)
92
+ .sort()
93
+ .map((k) => `${k}=${payload[k]}`)
94
+ .join("&");
95
+ const payloadHash = cyrb53(sortedPayload);
96
+ return `${deviceFp}|req:${payloadHash}`;
97
+ },
98
+
99
+ /**
100
+ * Génère une signature HMAC-SHA256 en utilisant l'API Web Crypto.
101
+ * @param {object} payload - Les données à signer.
102
+ * @param {string} secret - La clé secrète partagée.
103
+ * @returns {Promise<string>} La signature hexadécimale.
104
+ */
105
+ async generateClientSideSignature(payload, secret) {
106
+ const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
107
+ const encoder = new TextEncoder();
108
+ const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
109
+ name: "HMAC",
110
+ hash: "SHA-256"
111
+ }, false, ["sign"]);
112
+ const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
113
+ const hashArray = Array.from(new Uint8Array(signatureBuffer));
114
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
115
+ },
116
+
117
+ /**
118
+ * @internal
119
+ * Resets the cached fingerprint builder. Used for testing purposes.
120
+ */
121
+ _resetCache() {
122
+ this._cachedBuilder = null;
123
+ },
124
+
125
+ /**
126
+ * Démarre le suivi des mouvements de la souris pour calculer l'entropie.
127
+ * À appeler une fois sur la page.
128
+ */
129
+ startMouseEntropyTracker() {
130
+ // S'assurer de ne pas attacher l'écouteur plusieurs fois
131
+ if (mouseMovements > 0) return;
132
+
133
+ document.addEventListener('mousemove', (e) => {
134
+ const dx = e.clientX - lastMousePos.x;
135
+ const dy = e.clientY - lastMousePos.y;
136
+ // Une métrique simple : la somme des distances. Un bot aura souvent 0.
137
+ metrics.mouseEntropy += Math.sqrt(dx * dx + dy * dy);
138
+ lastMousePos = {x: e.clientX, y: e.clientY};
139
+ mouseMovements++;
140
+ }, {passive: true});
141
+ },
142
+
143
+ /**
144
+ * Démarre le suivi de la dynamique de frappe pour calculer la latence.
145
+ * À appeler une fois sur la page.
146
+ */
147
+ startKeystrokeDynamicsTracker() {
148
+ // S'assurer de ne pas attacher l'écouteur plusieurs fois
149
+ if (keystrokeTimestamps.length > 0) return;
150
+
151
+ document.addEventListener('keydown', () => {
152
+ const now = performance.now();
153
+ if (keystrokeTimestamps.length > 0) {
154
+ const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
155
+ const latency = now - lastTimestamp;
156
+ // On ignore les latences irréalistes (trop longues ou trop courtes)
157
+ if (latency > 10 && latency < 2000) { // Augmenté à 2s
158
+ if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
159
+ keystrokeLatencies.shift(); // Garder la taille de l'historique
160
+ }
161
+ keystrokeLatencies.push(latency);
162
+ }
163
+ }
164
+ keystrokeTimestamps.push(now);
165
+ }, {passive: true});
166
+ },
167
+
168
+ /**
169
+ * Initialise ou réinitialise les honeypots côté client pour une détection immédiate.
170
+ * Les anciens écouteurs sont supprimés avant d'en ajouter de nouveaux.
171
+ * @param {string[]} honeypotFieldNames - Noms des champs de formulaire cachés.
172
+ */
173
+ initializeHoneypots(honeypotFieldNames) {
174
+ // 1. Nettoyer les anciens écouteurs
175
+ activeHoneypotListeners.forEach((listener, field) => {
176
+ field.removeEventListener('input', listener);
177
+ });
178
+ activeHoneypotListeners.clear();
179
+
180
+ // 2. Ajouter les nouveaux écouteurs
181
+ honeypotFieldNames.forEach(fieldName => {
182
+ const field = document.querySelector(`[name="${fieldName}"]`);
183
+ if (field) {
184
+ // On utilise une fonction nommée (ou une référence) pour pouvoir la supprimer plus tard.
185
+ // L'option { once: true } est excellente, mais pour une réinitialisation complète,
186
+ // il est plus propre de gérer le nettoyage nous-mêmes.
187
+ const listener = () => {
188
+ this.onHoneypotTrigger();
189
+ // Se supprime lui-même après exécution, comme { once: true }
190
+ field.removeEventListener('input', listener);
191
+ };
192
+ field.addEventListener('input', listener);
193
+ activeHoneypotListeners.set(field, listener); // On stocke la référence
194
+ }
195
+ });
196
+ },
197
+
198
+ /**
199
+ * Récupère les métriques comportementales collectées.
200
+ * À appeler avant d'envoyer une requête sensible.
201
+ * @returns {ClientBehaviorMetrics}
202
+ */
203
+ getClientBehaviorMetrics() {
204
+ // Normalise l'entropie de la souris
205
+ if (mouseMovements > 10) {
206
+ metrics.mouseEntropy /= mouseMovements;
207
+ }
208
+ // Calcule la latence moyenne des frappes
209
+ if (keystrokeLatencies.length > 0) {
210
+ const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
211
+ metrics.keystrokeLatency = sum / keystrokeLatencies.length;
212
+ } else {
213
+ metrics.keystrokeLatency = 0;
214
+ }
215
+ return metrics;
216
+ },
217
+
218
+ /**
219
+ * Enrichit une requête fetch avec les en-têtes de fingerprinting et de comportement.
220
+ * @param {RequestInfo} resource
221
+ * @param {RequestInit} [options]
222
+ * @returns {Promise<Response>}
223
+ */
224
+ async protectedFetch(resource, options = {}) {
225
+ const fp = this.getDeviceFingerprint();
226
+ const behavior = this.getClientBehaviorMetrics();
227
+
228
+ const headers = new Headers(options.headers || {});
229
+ headers.set('X-Device-Fingerprint', fp);
230
+ headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
231
+
232
+ options.headers = headers;
233
+ return fetch(resource, options);
234
+ },
235
+
236
+ // --- Système d'interception de Fetch robuste et anti-conflit ---
237
+
238
+ _isFetchPatched: false,
239
+ _interceptorChain: [],
240
+ _originalFetch: (typeof window !== 'undefined') ? window.fetch : () => Promise.reject(new Error('fetch is not available')),
241
+
242
+ /**
243
+ * Adds an interceptor function to the `fetch` chain.
244
+ * Chaque intercepteur reçoit `resource`, `options`, et une fonction `next`.
245
+ * Il DOIT appeler `next(resource, options)` pour continuer la chaîne.
246
+ * @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
247
+ */
248
+ addFetchInterceptor(interceptor) {
249
+ if (!this._isFetchPatched) {
250
+ this.patchGlobalFetch();
251
+ }
252
+ this._interceptorChain.push(interceptor);
253
+ },
254
+
255
+ patchGlobalFetch() {
256
+ if (this._isFetchPatched || typeof window === 'undefined') return;
257
+
258
+ this._isFetchPatched = true;
259
+ window.fetch = (resource, options) => {
260
+ // Le "dispatcher" qui exécute la chaîne.
261
+ const dispatch = (index, res, opts) => {
262
+ if (index >= this._interceptorChain.length) {
263
+ // Fin de la chaîne, on appelle le fetch original.
264
+ return this._originalFetch(res, opts);
265
+ }
266
+ const nextInterceptor = this._interceptorChain[index];
267
+ // Appelle l'intercepteur actuel en lui passant la fonction pour appeler le suivant.
268
+ return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
269
+ };
270
+ return dispatch(0, resource, options || {});
271
+ };
272
+ },
273
+
274
+ /**
275
+ * La fonction qui est appelée lorsqu'un honeypot est déclenché.
276
+ * @private
277
+ */
278
+ onHoneypotTrigger : () => {
279
+ metrics.honeypotInteraction = true;
280
+ // On pourrait même envoyer un signalement au serveur immédiatement.
281
+ },
282
+
283
+ /**
284
+ * Initialise l'intercepteur de fingerprinting.
285
+ * Il s'ajoute à la chaîne d'interception sans écraser les autres.
286
+ * @param {string[]} [targetDomains] - Optionnel. Liste de domaines à protéger.
287
+ * Si non fourni, protège les requêtes de même origine.
288
+ */
289
+ initializeFetch(targetDomains = []) {
290
+ const fingerprintInterceptor = (resource, options, next) => {
291
+ const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
292
+ let shouldProtect = false;
293
+
294
+ try {
295
+ const url = new URL(requestUrl, window.location.origin);
296
+ // Protéger si la liste de domaines est vide ET que la requête est de même origine,
297
+ // OU si le domaine de la requête est dans la liste fournie.
298
+ shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
299
+ (targetDomains.length > 0 && targetDomains.includes(url.hostname));
300
+ } catch (e) {
301
+ // Si l'URL est relative (ex: '/api/data'), new URL() ne lèvera pas d'erreur.
302
+ // Ce bloc est une sécurité pour les cas où l'URL serait malformée.
303
+ // On protège par défaut si aucune liste de domaines n'est spécifiée.
304
+ shouldProtect = targetDomains.length === 0;
305
+ }
306
+
307
+ if (shouldProtect) {
308
+ const fp = this.getDeviceFingerprint();
309
+ const behavior = this.getClientBehaviorMetrics();
310
+ const headers = new Headers(options.headers || {});
311
+ headers.set('X-Device-Fingerprint', fp);
312
+ headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
313
+ options.headers = headers;
314
+ }
315
+
316
+ // Passe la main à l'intercepteur suivant dans la chaîne.
317
+ return next(resource, options);
318
+ };
319
+
320
+ this.addFetchInterceptor(fingerprintInterceptor);
321
+ },
322
+
323
+ /**
324
+ * @typedef {object} ClientConfig
325
+ * @property {boolean} [mouse=true] - Activer le suivi de l'entropie de la souris.
326
+ * @property {boolean} [keystrokes=true] - Activer le suivi de la dynamique de frappe.
327
+ * @property {string[]} [honeypots] - Noms des champs de formulaire honeypot à initialiser.
328
+ * @property {object} [fetch] - Configuration pour l'interception de fetch.
329
+ * @property {string[]} [fetch.targetDomains] - Domaines à protéger. Si non fourni, protège les requêtes de même origine.
330
+ */
331
+
332
+ /**
333
+ * Initialise toutes les protections côté client en une seule fois.
334
+ * C'est la méthode d'initialisation recommandée.
335
+ * @param {ClientConfig} [config={}] - L'objet de configuration.
336
+ */
337
+ initializeClient(config = {}) {
338
+ const {
339
+ mouse = true,
340
+ keystrokes = true,
341
+ honeypots = [],
342
+ fetch: fetchConfig,
343
+ } = config;
344
+
345
+ if (mouse) {
346
+ this.startMouseEntropyTracker();
347
+ }
348
+ if (keystrokes) {
349
+ this.startKeystrokeDynamicsTracker();
350
+ }
351
+ if (honeypots.length > 0) {
352
+ this.initializeHoneypots(honeypots);
353
+ }
354
+ if (fetchConfig !== undefined) {
355
+ this.initializeFetch(fetchConfig.targetDomains);
356
+ }
357
+ }
358
+ };
359
+
360
+ /**
361
+ * @typedef {object} ClientBehaviorMetrics
362
+ * @property {number} mouseEntropy - Entropie des mouvements de la souris.
363
+ * @property {number} keystrokeLatency - Latence moyenne entre les frappes.
364
+ * @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
365
+ */
366
+
367
+ /** @type {ClientBehaviorMetrics} */
368
+ const metrics = {
369
+ mouseEntropy: 0,
370
+ keystrokeLatency: 0,
371
+ honeypotInteraction: false,
372
+ };
373
+
374
+ let lastMousePos = { x: 0, y: 0 };
375
+ let mouseMovements = 0;
376
+ let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
377
+ let keystrokeTimestamps = [];
378
+ let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
379
+ const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
380
+
381
+
382
+
383
+ // Exporter les fonctions individuellement pour la compatibilité ascendante
384
+ export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
385
+ export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
386
+ export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
387
+ export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
388
+ export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
389
+ export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
390
+ export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
391
+ export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
392
+ export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
393
+ export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
394
+ export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
395
+ export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
396
+ export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
397
+
398
+ // Export the internal object for testing purposes
399
+ export default ClientLibrary;
package/fingerprint.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // C:/Dev/games.primals.net/src/utils/fingerprint.js
2
2
  import crypto from "node:crypto";
3
+ import { parse } from "node:net";
4
+ import dns from "node:dns/promises";
3
5
  import { Optimization } from "./library.js";
4
6
  import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
5
7
 
@@ -15,6 +17,50 @@ const getPowSecret = () => {
15
17
  return secret || "fallback-dev-secret-32-chars-minimum";
16
18
  };
17
19
 
20
+ /**
21
+ * Calculates the JA3 fingerprint hash from the TLS Client Hello message.
22
+ * JA3 is a more reliable way to identify client applications (e.g., a specific browser or a script)
23
+ * based on the specifics of its TLS handshake.
24
+ * @param {object} context - The request context, containing the raw request object.
25
+ * @returns {string|null} The MD5 hash of the JA3 string, or null if it cannot be computed.
26
+ */
27
+ function getJa3Hash(context) {
28
+ // 1. Prefer the JA3 hash from a trusted reverse proxy (e.g., Nginx, Cloudflare).
29
+ const ja3FromHeader = context.headers['x-ja3-hash'];
30
+ if (ja3FromHeader) {
31
+ return ja3FromHeader;
32
+ }
33
+
34
+ // 2. Fallback to calculating from the raw socket if available (requires Node.js to handle TLS).
35
+ const clientHello = context.rawReq?.socket?.clientHello;
36
+ if (!clientHello) {
37
+ return null;
38
+ }
39
+
40
+ try {
41
+ const { version, ciphers, extensions, ellipticCurves, ellipticCurvePointFormats } = clientHello;
42
+
43
+ // The official JA3 spec includes the TLS version.
44
+ // Node.js provides it as a string like 'TLSv1.3', we need the corresponding decimal value.
45
+ const tlsVersionMap = {
46
+ 'TLSv1': 769, 'TLSv1.1': 770, 'TLSv1.2': 771, 'TLSv1.3': 772
47
+ };
48
+ const tlsVersionId = tlsVersionMap[version] || 0;
49
+
50
+ const ja3String = [
51
+ tlsVersionId,
52
+ // The ciphers array from clientHello is an array of objects, not just IDs.
53
+ Array.isArray(ciphers) ? ciphers.join('-') : '',
54
+ extensions?.join('-') || '',
55
+ ellipticCurves?.join('-') || '',
56
+ ellipticCurvePointFormats?.join('-') || ''
57
+ ].join(',');
58
+
59
+ return crypto.createHash('md5').update(ja3String).digest('hex');
60
+ } catch (e) {
61
+ return null; // Could fail if clientHello structure is unexpected.
62
+ }
63
+ }
18
64
  /**
19
65
  * Creates a stable hash based on device characteristics, independent of the IP.
20
66
  * This is our "level 2 fingerprint".
@@ -43,6 +89,10 @@ export function getDeviceHash(context) {
43
89
  if (context.headers["sec-ch-ua-platform"])
44
90
  srv.add("os", context.headers["sec-ch-ua-platform"]);
45
91
  if (context.headers["sec-ch-ua"]) srv.add("ch", context.headers["sec-ch-ua"]);
92
+ // Add JA3 hash if available. This is a very strong signal.
93
+ const ja3 = getJa3Hash(context);
94
+ if (ja3) srv.add("ja3", ja3);
95
+
46
96
  srv.add("h_ord", getHeaderSignature(context));
47
97
  return srv.toString();
48
98
  }
@@ -406,6 +456,14 @@ function getHeaderAnomalies(context) {
406
456
  */
407
457
  function getHoneypotScore(context, honeypotConfig = {}) {
408
458
  const { fields = [], trapUrls = [], detectInjections = true } = honeypotConfig;
459
+ // (NOUVEAU) Permettre de brancher des analyseurs externes plus robustes.
460
+ // L'utilisateur pourrait passer une fonction qui prend les données de la requête
461
+ // et retourne `true` si une menace est détectée.
462
+ // Exemple: `(data) => myWafLibrary.isMalicious(data)`
463
+ const externalAnalyzers = honeypotConfig.analyzers || [];
464
+ if (typeof detectInjections === 'object' && detectInjections.analyzers) {
465
+ externalAnalyzers.push(...detectInjections.analyzers);
466
+ }
409
467
 
410
468
  // 1. Check for trap URL access
411
469
  if (trapUrls.some(trap => context.path.startsWith(trap))) {
@@ -434,6 +492,17 @@ function getHoneypotScore(context, honeypotConfig = {}) {
434
492
  }
435
493
  }
436
494
 
495
+ // 3. (NOUVEAU) Utiliser les analyseurs externes
496
+ const allData = { ...queryData, ...bodyData };
497
+ if (externalAnalyzers.length > 0) {
498
+ for (const analyzer of externalAnalyzers) {
499
+ // On passe à l'analyseur l'ensemble des données de la requête.
500
+ if (analyzer(allData)) {
501
+ return { honeypotScore: 100 };
502
+ }
503
+ }
504
+ }
505
+
437
506
  // 3. Check for injection attempts in values
438
507
  if (detectInjections) {
439
508
  // Regex for common SQL injection patterns
@@ -453,13 +522,19 @@ function getHoneypotScore(context, honeypotConfig = {}) {
453
522
  "(\\.\\./|\\.\\.\\\\)|\\b(exec|system|shell_exec|passthru|popen|proc_open|eval|assert|require|include|process|child_process)(_once)?\\s*\\(|\\b(wget|curl|bash|sh|powershell|php)\\b",
454
523
  "i"
455
524
  );
525
+ // Regex for Log4Shell (JNDI injection)
526
+ const log4shellRegex = new RegExp("\\$\\{jndi:", "i");
527
+ // Regex for Server-Side Template Injection (SSTI)
528
+ const sstiRegex = new RegExp(
529
+ "(\\{\\{|\\{%|#\\{)[^}]+(config|settings|self|class|application|request|session|process|env)", "i"
530
+ );
456
531
 
457
532
  const inspect = (obj) => {
458
533
  for (const key in obj) {
459
534
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
460
535
  const value = obj[key];
461
536
  if (typeof value === 'string') {
462
- if (rceRegex.test(value) || sqlRegex.test(value)) return true;
537
+ if (rceRegex.test(value) || sqlRegex.test(value) || log4shellRegex.test(value) || sstiRegex.test(value)) return true;
463
538
  } else if (typeof value === 'object' && value !== null) {
464
539
  // For NoSQL, we check the stringified version of the object to find keys like "$gt"
465
540
  // This is more accurate when done on the object itself.
@@ -499,7 +574,9 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
499
574
  scrapeThreshold = 1000, scrapeWeight = 20, scrapeBurstWeight = 40,
500
575
  historySize = 10,
501
576
  decayFactor = 0.9,
502
- inactivityReset = 30000
577
+ inactivityReset = 30000,
578
+ // Nouveau paramètre pour la détection de séquences
579
+ sequenceLength = 3, sequenceWeight = 60
503
580
  } = patternConfig;
504
581
 
505
582
  const now = Date.now();
@@ -543,6 +620,17 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
543
620
  score += scrapeWeight; // First sign of a potential scraping pattern
544
621
  }
545
622
  }
623
+
624
+ // 4. (NOUVEAU) Détection de séquences répétitives (ex: A -> B -> C -> A -> B -> C)
625
+ if (history.length >= sequenceLength * 2) {
626
+ const lastSequence = history.slice(-sequenceLength);
627
+ const previousSequence = history.slice(-sequenceLength * 2, -sequenceLength);
628
+
629
+ const isRepeating = lastSequence.every((req, i) =>
630
+ req.path === previousSequence[i].path && req.queryString === previousSequence[i].queryString
631
+ );
632
+ if (isRepeating) score += sequenceWeight;
633
+ }
546
634
  }
547
635
 
548
636
  // --- Update history ---
@@ -691,6 +779,8 @@ async function resolveRequestIdentity(context) {
691
779
  lastFpHash: currentDeviceHash,
692
780
  lastChangeTimestamp: 0,
693
781
  rapidChangeCount: 0,
782
+ highScoreCount: 0,
783
+ lastHighScoreTimestamp: 0,
694
784
  };
695
785
  // The write will happen in getSuspicionVector after all modifications.
696
786
  }
@@ -790,6 +880,11 @@ export const getSuspicionVector = async (context, securityConfig) => {
790
880
  // 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.
791
881
  await store.set(`device:${deviceId}`, deviceData);
792
882
 
883
+ // Ensure deviceData.ips is a Set for subsequent operations within the same request,
884
+ // even if the store returns an array.
885
+ if (Array.isArray(deviceData.ips)) {
886
+ deviceData.ips = new Set(deviceData.ips);
887
+ }
793
888
  return { ...behavioral, headerAnomalyScore, inconsistencyScore };
794
889
  };
795
890
 
@@ -1024,21 +1119,141 @@ const staticExtensions = new RegExp(
1024
1119
  const isStaticResource = (path) => staticExtensions.test(path);
1025
1120
 
1026
1121
  // --- Middleware Proof-of-Work (Le péage) ---
1027
- class FingerprintEngine {
1122
+ export class FingerprintEngine {
1028
1123
  constructor(securityConfig) {
1029
1124
  const isProduction = process.env.NODE_ENV === 'production';
1030
1125
  this.securityConfig = securityConfig;
1031
1126
  this.isProduction = isProduction;
1032
1127
  }
1033
1128
 
1129
+ /**
1130
+ * Checks if an IP address is in the static allowlist (IPs or CIDR ranges).
1131
+ * This is the fastest check and should be performed first.
1132
+ * @private
1133
+ * @param {string} clientIp - The IP address of the client.
1134
+ * @returns {boolean} True if the IP is in the allowlist.
1135
+ */
1136
+ _isIpInAllowlist(clientIp) {
1137
+ const { whitelist = [] } = this.securityConfig;
1138
+ const allowlistRule = whitelist.find(rule => rule.type === 'allowlist');
1139
+
1140
+ if (!allowlistRule || !allowlistRule.entries || allowlistRule.entries.length === 0) {
1141
+ return false;
1142
+ }
1143
+
1144
+ const ip = parse(clientIp);
1145
+ const ipVersion = ip.family;
1146
+
1147
+ for (const entry of allowlistRule.entries) {
1148
+ if (entry.includes('/')) { // CIDR range
1149
+ try {
1150
+ const [range, prefixStr] = entry.split('/');
1151
+ const prefix = parseInt(prefixStr, 10);
1152
+ const rangeIp = parse(range);
1153
+
1154
+ if (rangeIp.family !== ipVersion) continue;
1155
+
1156
+ const ipBytes = ip.toBuffer();
1157
+ const rangeBytes = rangeIp.toBuffer();
1158
+ const mask = Buffer.alloc(ipBytes.length, 0xff);
1159
+
1160
+ for (let i = 0; i < Math.floor(prefix / 8); i++) {
1161
+ if (ipBytes[i] !== rangeBytes[i]) {
1162
+ break; // Mismatch in full byte, move to next entry
1163
+ }
1164
+ }
1165
+ const remainingBits = prefix % 8;
1166
+ if (remainingBits > 0) {
1167
+ const byteIndex = Math.floor(prefix / 8);
1168
+ const bitmask = (0xff << (8 - remainingBits)) & 0xff;
1169
+ if ((ipBytes[byteIndex] & bitmask) !== (rangeBytes[byteIndex] & bitmask)) {
1170
+ continue; // Mismatch in partial byte
1171
+ }
1172
+ }
1173
+ return true; // IP is in CIDR range
1174
+ } catch (e) { continue; /* Ignore invalid CIDR entries */ }
1175
+ } else if (entry === clientIp) { // Direct IP match
1176
+ return true;
1177
+ }
1178
+ }
1179
+ return false;
1180
+ }
1181
+ /**
1182
+ * Verifies if a request comes from a legitimate, whitelisted bot (e.g., Googlebot)
1183
+ * using reverse and forward DNS lookups. The result is cached.
1184
+ * @private
1185
+ * @param {object} requestContext - The request context.
1186
+ * @returns {Promise<boolean>} True if the request is from a verified whitelisted bot.
1187
+ */
1188
+ async _verifyWhitelistedBot(requestContext) {
1189
+ const { whitelist = [] } = this.securityConfig;
1190
+ const botRules = whitelist.filter(rule => rule.hostnameSuffix);
1191
+ if (botRules.length === 0) {
1192
+ return false;
1193
+ }
1194
+
1195
+ const { clientIp, headers } = requestContext;
1196
+ const userAgent = headers['user-agent'] || '';
1197
+
1198
+ const matchedRule = botRules.find(rule => {
1199
+ if (!rule.userAgent) return false;
1200
+ try {
1201
+ return new RegExp(rule.userAgent).test(userAgent);
1202
+ } catch (e) {
1203
+ console.error(`[Fingerprint] Invalid regex in whitelist rule: ${rule.userAgent}`);
1204
+ return false;
1205
+ }
1206
+ });
1207
+ if (!matchedRule) {
1208
+ return false;
1209
+ }
1210
+
1211
+ const cacheKey = `ip-whitelist:${clientIp}`;
1212
+ const cachedStatus = await store.get(cacheKey);
1213
+
1214
+ if (cachedStatus === 'verified') {
1215
+ return true;
1216
+ }
1217
+ if (cachedStatus === 'failed') {
1218
+ return false;
1219
+ }
1220
+
1221
+ try {
1222
+ // 1. Reverse DNS lookup
1223
+ const hostnames = await dns.reverse(clientIp);
1224
+ const validHostname = hostnames.find(h => h.endsWith(matchedRule.hostnameSuffix));
1225
+
1226
+ if (!validHostname) {
1227
+ await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h
1228
+ return false;
1229
+ }
1230
+
1231
+ // 2. Forward DNS lookup
1232
+ const addresses = await dns.resolve(validHostname);
1233
+ if (addresses.includes(clientIp)) {
1234
+ await store.set(cacheKey, 'verified', 86400); // Cache success for 24h
1235
+ return true;
1236
+ }
1237
+ } catch (error) {
1238
+ // DNS errors are common (e.g., for IPs with no rDNS record), treat as failure.
1239
+ }
1240
+
1241
+ await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h
1242
+ return false;
1243
+ }
1244
+
1034
1245
  async processRequest(requestContext) {
1035
1246
  const { clientIp, path, cookies, query, isStatic } = requestContext;
1036
1247
  const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
1037
-
1038
1248
  if (isStatic) {
1039
1249
  return { action: 'next', score: 0, vector: {} };
1040
1250
  }
1041
1251
 
1252
+ // 1. Check static IP allowlist first for maximum performance.
1253
+ if (this._isIpInAllowlist(clientIp)) {
1254
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'allowlist' } };
1255
+ }
1256
+
1042
1257
  const { pow_nonce } = query;
1043
1258
 
1044
1259
  // Honeypot: Direct probing of challenge endpoints is highly suspicious.
@@ -1052,6 +1267,11 @@ class FingerprintEngine {
1052
1267
  }
1053
1268
  }
1054
1269
 
1270
+ // Check if the request is from a verified, whitelisted bot (e.g., Googlebot)
1271
+ if (await this._verifyWhitelistedBot(requestContext)) {
1272
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
1273
+ }
1274
+
1055
1275
  // Check for persisted "condemned" status early.
1056
1276
  const { deviceData } = await resolveRequestIdentity(requestContext);
1057
1277
  if (deviceData?.condemned) {
@@ -1061,6 +1281,9 @@ class FingerprintEngine {
1061
1281
  return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
1062
1282
  }
1063
1283
 
1284
+ // (NOUVEAU) Vérifier si un nouveau device_id a été créé lors de cette requête
1285
+ const isNewDevice = requestContext._newCookies?.some(c => c.name === 'device_id');
1286
+
1064
1287
  // The engine now works with the context directly, no more rawReq dependency here.
1065
1288
  const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
1066
1289
  const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
@@ -1075,6 +1298,11 @@ class FingerprintEngine {
1075
1298
  suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0) +
1076
1299
  honeypotScore * (weights.honeypotScore || 0);
1077
1300
 
1301
+ // Si c'est un nouvel appareil, on lui impose un challenge de base, même si son score est bas.
1302
+ // Cela augmente le coût pour les bots qui tentent de simplement supprimer leurs cookies.
1303
+ const requiresChallengeForNewDevice = isNewDevice && finalScore < thresholds.low;
1304
+
1305
+
1078
1306
  const isBlocked = finalScore >= (thresholds.block || 95);
1079
1307
 
1080
1308
  const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
@@ -1119,11 +1347,14 @@ class FingerprintEngine {
1119
1347
  if (onDeviceCompromised) {
1120
1348
  onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
1121
1349
  }
1350
+ if (logger) {
1351
+ logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
1352
+ }
1122
1353
  await store.set(`device:${cookies.device_id}`, deviceData);
1123
1354
  return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
1124
1355
  }
1125
1356
 
1126
- if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
1357
+ if ((isSuspicious || requiresChallengeForNewDevice) && !isTicketValid(clientIp, powCookie)) {
1127
1358
  // --- CHALLENGE SOLUTION HANDLING ---
1128
1359
  if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
1129
1360
  let isValid = false,
@@ -1219,7 +1450,7 @@ class FingerprintEngine {
1219
1450
  }
1220
1451
 
1221
1452
  // UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
1222
- if (isSuspicious) { // Couvre à la fois low et medium
1453
+ if (isSuspicious || requiresChallengeForNewDevice) { // Couvre à la fois low et medium
1223
1454
  // Generate some trap URLs to embed in the challenge page.
1224
1455
  // These links are visually hidden but present in the DOM to trap bots.
1225
1456
  const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
@@ -1306,6 +1537,111 @@ class FingerprintEngine {
1306
1537
  }
1307
1538
  }
1308
1539
 
1540
+ /**
1541
+ * Returns a default list of whitelisting rules for common and legitimate web crawlers.
1542
+ * This list can be used as a base and extended with custom rules.
1543
+ * @returns {Array<{userAgent: string, hostnameSuffix: string}>}
1544
+ */
1545
+ export const default_whitelist = () => [
1546
+ // === Moteurs de recherche majeurs ===
1547
+ { userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
1548
+ { userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
1549
+ { userAgent: 'AdsBot-Google', hostnameSuffix: '.googlebot.com' },
1550
+ { userAgent: 'Mediapartners-Google', hostnameSuffix: '.google.com' },
1551
+ { userAgent: 'Google-InspectionTool', hostnameSuffix: '.google.com' },
1552
+ { userAgent: '(bingbot|adidxbot)', hostnameSuffix: '.search.msn.com' },
1553
+ { userAgent: 'DuckDuckBot', hostnameSuffix: '.duckduckgo.com' },
1554
+ { userAgent: 'YandexBot', hostnameSuffix: '.yandex.com' },
1555
+ { userAgent: 'YandexImages', hostnameSuffix: '.yandex.com' },
1556
+ { userAgent: 'Baiduspider', hostnameSuffix: '.crawl.baidu.com' },
1557
+ { userAgent: 'Slurp', hostnameSuffix: '.crawl.yahoo.net' },
1558
+ { userAgent: 'Sogou web spider', hostnameSuffix: '.sogou.com' },
1559
+ { userAgent: 'Exabot', hostnameSuffix: '.exabot.com' },
1560
+ { userAgent: 'ia_archiver', hostnameSuffix: '.alexa.com' },
1561
+ { userAgent: 'SeznamBot', hostnameSuffix: '.seznam.cz' },
1562
+ { userAgent: 'Mail.RU_Bot', hostnameSuffix: '.mail.ru' },
1563
+ { userAgent: 'Yeti', hostnameSuffix: '.naver.com' }, // Naver
1564
+
1565
+ // === Outils SEO et d'analyse ===
1566
+ { userAgent: 'AhrefsBot', hostnameSuffix: '.ahrefs.com' },
1567
+ { userAgent: 'SemrushBot', hostnameSuffix: '.semrush.com' },
1568
+ { userAgent: 'MJ12bot', hostnameSuffix: '.mj12bot.com' }, // Majestic
1569
+ { userAgent: 'rogerbot', hostnameSuffix: '.moz.com' }, // Moz
1570
+ { userAgent: 'DotBot', hostnameSuffix: '.moz.com' }, // Moz (anciennement opensiteexplorer.org)
1571
+ { userAgent: 'Screaming Frog SEO Spider', hostnameSuffix: '.screamingfrog.co.uk' },
1572
+ { userAgent: 'cognitiveseo', hostnameSuffix: '.cognitiveseo.com' },
1573
+ { userAgent: 'SEOkicks', hostnameSuffix: '.seokicks.com' },
1574
+ { userAgent: 'serpstatbot', hostnameSuffix: '.serpstatbot.com' },
1575
+ { userAgent: 'MegaIndex', hostnameSuffix: '.megaindex.com' },
1576
+ { userAgent: 'LinkpadBot', hostnameSuffix: '.linkpad.ru' },
1577
+ { userAgent: 'Sistrix', hostnameSuffix: '.sistrix.com' },
1578
+ { userAgent: 'RyteBot', hostnameSuffix: '.ryte.com' },
1579
+ { userAgent: 'linkfluence', hostnameSuffix: '.linkfluence.com' },
1580
+ { userAgent: 'TurnitinBot', hostnameSuffix: '.turnitin.com' },
1581
+ { userAgent: 'GrapeshotCrawler', hostnameSuffix: '.grapeshot.co.uk' },
1582
+
1583
+ // === Robots d'IA et de données ===
1584
+ { userAgent: 'GPTBot', hostnameSuffix: '.openai.com' },
1585
+ { userAgent: 'ChatGPT-User', hostnameSuffix: '.openai.com' },
1586
+ { userAgent: 'Applebot', hostnameSuffix: '.applebot.apple.com' },
1587
+ { userAgent: 'CCBot', hostnameSuffix: '.commoncrawl.org' },
1588
+ { userAgent: 'Bytespider', hostnameSuffix: '.bytespider.com' }, // ByteDance (TikTok)
1589
+ { userAgent: 'Diffbot', hostnameSuffix: '.diffbot.com' },
1590
+ { userAgent: 'PerplexityBot', hostnameSuffix: '.perplexity.ai' },
1591
+ { userAgent: 'ClaudeBot', hostnameSuffix: '.anthropic.com' },
1592
+ { userAgent: 'cohere.io', hostnameSuffix: '.cohere.io' },
1593
+ { userAgent: 'DataForSeoBot', hostnameSuffix: '.dataforseo.com' },
1594
+ { userAgent: 'YouBot', hostnameSuffix: '.you.com' },
1595
+ { userAgent: 'omgili', hostnameSuffix: '.omgili.com' },
1596
+
1597
+ // === Réseaux sociaux et partage ===
1598
+ { userAgent: 'facebookexternalhit', hostnameSuffix: '.facebook.com' },
1599
+ { userAgent: 'facebot', hostnameSuffix: '.facebook.com' },
1600
+ { userAgent: 'Twitterbot', hostnameSuffix: '.twttr.com' },
1601
+ { userAgent: 'Pinterestbot', hostnameSuffix: '.pinterest.com' },
1602
+ { userAgent: 'LinkedInBot', hostnameSuffix: '.linkedin.com' },
1603
+ { userAgent: 'Slackbot', hostnameSuffix: '.slack.com' },
1604
+ { userAgent: 'Discordbot', hostnameSuffix: '.discord.com' },
1605
+ { userAgent: 'TelegramBot', hostnameSuffix: '.telegram.org' },
1606
+ { userAgent: 'WhatsApp', hostnameSuffix: '.wa.me' },
1607
+ { userAgent: 'SkypeUriPreview', hostnameSuffix: '.skype.com' },
1608
+ { userAgent: 'redditbot', hostnameSuffix: '.reddit.com' },
1609
+
1610
+ // === Services de monitoring et d'uptime ===
1611
+ { userAgent: 'UptimeRobot', hostnameSuffix: '.uptimerobot.com' },
1612
+ { userAgent: 'Pingdom', hostnameSuffix: '.pingdom.com' },
1613
+ { userAgent: 'StatusCake', hostnameSuffix: '.statuscake.com' },
1614
+ { userAgent: 'Site24x7', hostnameSuffix: '.site24x7.com' },
1615
+ { userAgent: 'Freshping', hostnameSuffix: '.freshping.io' },
1616
+ { userAgent: 'Better Uptime', hostnameSuffix: '.betteruptime.com' },
1617
+ { userAgent: 'Checkly', hostnameSuffix: '.checkly-infra.com' },
1618
+ { userAgent: 'Datadog', hostnameSuffix: '.datadoghq.com' },
1619
+ { userAgent: 'NewRelicPinger', hostnameSuffix: '.newrelic.com' },
1620
+
1621
+ // === Archives et agrégateurs de contenu ===
1622
+ { userAgent: 'archive.org_bot', hostnameSuffix: '.archive.org' },
1623
+ { userAgent: 'Feedly', hostnameSuffix: '.feedly.com' },
1624
+ { userAgent: 'FeedFetcher-Google', hostnameSuffix: '.google.com' },
1625
+ { userAgent: 'TheOldReader', hostnameSuffix: '.theoldreader.com' },
1626
+ { userAgent: 'Inoreader', hostnameSuffix: '.inoreader.com' },
1627
+ { userAgent: 'FlipboardProxy', hostnameSuffix: '.flipboard.com' },
1628
+ { userAgent: 'PaperLiBot', hostnameSuffix: '.paper.li' },
1629
+
1630
+ // === Services Cloud et Plateformes ===
1631
+ { userAgent: 'Amazon Route 53 Health Check', hostnameSuffix: '.amazonaws.com' },
1632
+ { userAgent: 'Google-Cloud-Scheduler', hostnameSuffix: '.google.com' },
1633
+ { userAgent: 'APIs-Google', hostnameSuffix: '.google.com' },
1634
+
1635
+ // === Divers ===
1636
+ { userAgent: 'W3C_Validator', hostnameSuffix: '.w3.org' },
1637
+ { userAgent: 'GTmetrix', hostnameSuffix: '.gtmetrix.com' },
1638
+ { userAgent: 'WebPageTest', hostnameSuffix: '.webpagetest.org' },
1639
+ { userAgent: 'Google-Site-Verification', hostnameSuffix: '.google.com' },
1640
+ { userAgent: 'KeyCDN', hostnameSuffix: '.keycdn.com' },
1641
+ ];
1642
+
1643
+
1644
+
1309
1645
  // --- Proof-of-Work Middleware (The Tollbooth) ---
1310
1646
  export const powMiddleware = (securityConfig) => {
1311
1647
  const engine = new FingerprintEngine(securityConfig);
@@ -1328,6 +1664,8 @@ export const powMiddleware = (securityConfig) => {
1328
1664
  isStatic: isStaticResource(req.path),
1329
1665
  // Add the newly required properties for full decoupling
1330
1666
  rawHeaders: req.rawHeaders,
1667
+ // Pass the raw request object for advanced inspection (e.g., JA3)
1668
+ rawReq: req,
1331
1669
  httpVersion: req.httpVersion,
1332
1670
  };
1333
1671
 
@@ -1375,7 +1713,6 @@ export const __internal = {
1375
1713
  cyrb53, // Export for testing
1376
1714
  FingerprintBuilder, // Export for testing
1377
1715
  calculateTarget,
1378
- FingerprintEngine, // Expose for advanced testing
1379
1716
  getRequestPatternScore, // Expose for testing
1380
1717
  };
1381
1718
 
@@ -1397,38 +1734,60 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
1397
1734
  }
1398
1735
  console.log(`[AutoTuning] Démarrage du cycle d'optimisation avec ${trafficData.length} points de données.`);
1399
1736
 
1400
- // Identify "bots" (those who received a challenge but never solved it)
1401
- // and "humans" (those who passed the challenge or never received one).
1737
+ // Classify historical requests with a confidence weight.
1402
1738
  const solvedDevices = new Set(trafficData.filter(e => e.type === 'challenge_solved').map(e => e.deviceId));
1739
+ const challengedDevices = new Set(trafficData.filter(e => e.type === 'challenge_issued').map(e => e.deviceId));
1740
+
1403
1741
  const historicalRequests = trafficData.map(log => {
1404
- let isBot = false;
1405
- if (log.type === 'challenge_issued' && !solvedDevices.has(log.deviceId)) {
1406
- isBot = true; // Assumption: a challenge issued and not solved is a bot.
1742
+ // Assign a label ('bot' or 'human') and a confidence weight to each log entry.
1743
+ switch (log.type) {
1744
+ case 'honeypot_probe':
1745
+ case 'trap_triggered':
1746
+ return { score: log.score, label: 'bot', confidence: 10.0 }; // Very high confidence
1747
+
1748
+ case 'challenge_issued':
1749
+ // A challenge issued to a device that never solved it is a strong bot signal.
1750
+ if (!solvedDevices.has(log.deviceId)) {
1751
+ return { score: log.score, label: 'bot', confidence: 3.0 }; // High confidence
1752
+ }
1753
+ // If the challenge was eventually solved, this specific log is neutral.
1754
+ return null;
1755
+
1756
+ case 'challenge_solved':
1757
+ return { score: log.score, label: 'human', confidence: 5.0 }; // High confidence
1758
+
1759
+ case 'request_passed':
1760
+ // A passed request from a device that was never even challenged is likely a human.
1761
+ if (!challengedDevices.has(log.deviceId)) {
1762
+ return { score: log.score, label: 'human', confidence: 0.5 }; // Low confidence
1763
+ }
1764
+ // If the device was challenged at some point, this log is ambiguous.
1765
+ return null;
1766
+
1767
+ default:
1768
+ return null;
1407
1769
  }
1408
- return { score: log.score, isBot };
1409
- });
1770
+ }).filter(Boolean); // Remove null entries
1410
1771
 
1411
1772
  // The "fitness" function evaluates the quality of a set of thresholds.
1412
1773
  // A lower score is better.
1413
1774
  const fitnessFunction = (solution) => {
1414
1775
  const [low, medium, high, velocityThreshold, burstThreshold, scrapeThreshold] = solution;
1415
- // Constraints: thresholds must be ordered and within a reasonable range.
1416
1776
  if (low >= medium || medium >= high || low < 10 || high > 90) return Infinity;
1417
- // Constraints for pattern thresholds
1418
1777
  if (velocityThreshold < 50 || velocityThreshold > burstThreshold || burstThreshold > scrapeThreshold) return Infinity;
1419
1778
 
1420
- let falsePositives = 0; // Humans challenged unnecessarily.
1421
- let falseNegatives = 0; // Undetected bots.
1779
+ let weightedFalsePositives = 0; // Humans challenged unnecessarily.
1780
+ let weightedFalseNegatives = 0; // Undetected bots.
1422
1781
 
1423
1782
  for (const req of historicalRequests) {
1424
- if (req.isBot) {
1425
- if (req.score < low) falseNegatives++;
1426
- } else { // Human
1427
- if (req.score >= low) falsePositives++;
1783
+ if (req.label === 'bot') {
1784
+ if (req.score < low) weightedFalseNegatives += req.confidence;
1785
+ } else { // 'human'
1786
+ if (req.score >= low) weightedFalsePositives += req.confidence;
1428
1787
  }
1429
1788
  }
1430
- // Penalize passing bots 2x more than inconvenienced humans.
1431
- return (falsePositives * 1.0) + (falseNegatives * 2.0);
1789
+ // The penalty for false negatives is implicitly higher due to the higher confidence scores of bot signals.
1790
+ return weightedFalsePositives + weightedFalseNegatives;
1432
1791
  };
1433
1792
 
1434
1793
  // Functions for the genetic algorithm.
package/package.json CHANGED
@@ -1,48 +1,61 @@
1
- {
2
- "name": "@anonympins/fingerprint",
3
- "version": "0.0.6",
4
- "description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
5
- "main": "fingerprint.js",
6
- "type": "module",
7
- "engines": {
8
- "node": ">=20.0.0"
9
- },
10
- "scripts": {
11
- "test": "vitest run"
12
- },
13
- "files": [
14
- "fingerprint.js",
15
- "library.js",
16
- "README.md",
17
- "LICENSE"
18
- ],
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/anonympins/fingerprint.git"
22
- },
23
- "keywords": [
24
- "fingerprint",
25
- "bot",
26
- "anti-bot",
27
- "security",
28
- "express",
29
- "middleware",
30
- "proof-of-work",
31
- "pow",
32
- "rate-limiting",
33
- "mitigation",
34
- "captcha"
35
- ],
36
- "author": "anonympins",
37
- "license": "MIT",
38
- "bugs": {
39
- "url": "https://github.com/anonympins/fingerprint/issues"
40
- },
41
- "homepage": "https://github.com/anonympins/fingerprint#readme",
42
- "devDependencies": {
43
- "body-parser": "^1.20.2",
44
- "cookie-parser": "^1.4.6",
45
- "express": "^4.18.2",
46
- "vitest": "^4.1.11"
47
- }
48
- }
1
+ {
2
+ "name": "@anonympins/fingerprint",
3
+ "version": "0.0.8",
4
+ "description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
5
+ "main": "fingerprint.js",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20.0.0"
9
+ },
10
+ "scripts": {
11
+ "test": "vitest run"
12
+ },
13
+ "files": [
14
+ "fingerprint.js",
15
+ "fingerprint.client.js",
16
+ "library.js",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/anonympins/fingerprint.git"
23
+ },
24
+ "keywords": [
25
+ "fingerprint",
26
+ "bot",
27
+ "anti-bot",
28
+ "security",
29
+ "express",
30
+ "middleware",
31
+ "proof-of-work",
32
+ "pow",
33
+ "rate-limiting",
34
+ "mitigation",
35
+ "captcha",
36
+ "dns"
37
+ ],
38
+ "author": "anonympins",
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://github.com/anonympins/fingerprint/issues"
42
+ },
43
+ "homepage": "https://github.com/anonympins/fingerprint#readme",
44
+ "devDependencies": {
45
+ "body-parser": "^1.20.2",
46
+ "cookie-parser": "^1.4.6",
47
+ "express": "^4.18.2",
48
+ "vitest": "^4.1.11",
49
+ "ioredis": "^5.3.2",
50
+ "mongodb": "^6.3.0",
51
+ "prom-client": "^15.1.2"
52
+ },
53
+ "peerDependencies": {
54
+ "ioredis": "^5.3.2",
55
+ "mongodb": "^6.3.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "ioredis": { "optional": true },
59
+ "mongodb": { "optional": true }
60
+ }
61
+ }