@anonympins/fingerprint 0.0.5 → 0.0.7
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 +58 -44
- package/fingerprint.js +315 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,12 +13,13 @@ 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
|
|
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).
|
|
20
20
|
* **IP Behavior**: An excessive number of different devices seen from the same IP, or a single device using a large number of IPs (proxy rotation).
|
|
21
21
|
* **Inconsistency**: A low similarity score between the current fingerprint and the initial one associated with the `device_id` (cookie theft detection).
|
|
22
|
+
* **Request Patterns**: Repetitive, rapid-fire, or sequential requests typical of scraping bots. The parameters for detecting these patterns (e.g., request velocity, burst detection) are dynamically adjusted by the auto-tuner for optimal performance.
|
|
22
23
|
* **Honeypot Trap**: Detection of bots that automatically fill hidden form fields or probe for common but unused URL parameters (e.g., `?debug=true`).
|
|
23
24
|
3. **Dynamic Challenge**: If the suspicion score exceeds a certain threshold, a challenge is presented to the user. The difficulty and type of challenge depend on the score:
|
|
24
25
|
* **Low to Medium Suspicion**: A combined CPU and Memory Proof-of-Work (PoW) challenge is issued. The difficulty of both the CPU (hash calculation) and Memory (allocation and computation) components scales progressively with the suspicion score. For low scores, the memory challenge is negligible, making it primarily a CPU task.
|
|
@@ -33,7 +34,7 @@ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie
|
|
|
33
34
|
- **Pluggable Datastore**: Supports external datastores like Redis for state persistence and scalability across multiple server instances.
|
|
34
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.
|
|
35
36
|
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure ticket validation.
|
|
36
|
-
- **Automatic
|
|
37
|
+
- **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.
|
|
37
38
|
|
|
38
39
|
## Installation and Usage
|
|
39
40
|
|
|
@@ -66,6 +67,10 @@ app.use(cookieParser());
|
|
|
66
67
|
app.use(bodyParser.json()); // For parsing application/json
|
|
67
68
|
app.use(bodyParser.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
|
|
68
69
|
|
|
70
|
+
// Array to store traffic analysis data for the auto-tuner.
|
|
71
|
+
// In a real application, this could be a more robust logging system (e.g., writing to a file or a database).
|
|
72
|
+
const trafficData = [];
|
|
73
|
+
|
|
69
74
|
// Configuration of weights and thresholds for calculating the suspicion score.
|
|
70
75
|
// These values should be adjusted based on traffic and expected user behavior.
|
|
71
76
|
const securityConfig = {
|
|
@@ -73,6 +78,7 @@ const securityConfig = {
|
|
|
73
78
|
historyScore: 0.3, // Penalizes IP rotation (proxy)
|
|
74
79
|
rotationScore: 0.5, // Penalizes rapid fingerprint changes (user-agent, etc.)
|
|
75
80
|
headerAnomalyScore: 0.1, // Penalizes abnormal headers (missing UA, etc.)
|
|
81
|
+
requestPatternScore: 0.6,// Penalizes bot-like request sequences (scraping, etc.)
|
|
76
82
|
inconsistencyScore: 0.8, // Strongly penalizes inconsistency between the current and initial fingerprint (stolen cookie)
|
|
77
83
|
honeypotScore: 1.0 // Strongly penalizes bots filling hidden form fields
|
|
78
84
|
},
|
|
@@ -83,6 +89,13 @@ const securityConfig = {
|
|
|
83
89
|
block: 95, // Score above which the request is blocked outright (HTTP 403)
|
|
84
90
|
isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
|
|
85
91
|
},
|
|
92
|
+
patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
|
|
93
|
+
velocityThreshold: 200, // ms between requests to be considered "fast"
|
|
94
|
+
burstThreshold: 500, // ms for identical requests to be a "burst"
|
|
95
|
+
scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
|
|
96
|
+
historySize: 10, // Number of requests to keep for pattern analysis
|
|
97
|
+
decayFactor: 0.9, // How quickly the pattern score decays over time
|
|
98
|
+
},
|
|
86
99
|
honeypot: {
|
|
87
100
|
// List of field names that are traps for bots.
|
|
88
101
|
// These should be hidden in forms for humans, or be URL parameters your app never uses.
|
|
@@ -91,8 +104,42 @@ const securityConfig = {
|
|
|
91
104
|
// A request to one of these paths will immediately flag the device as malicious.
|
|
92
105
|
trapUrls: ['/wp-admin', '/.env', '/admin.php', '/phpmyadmin'], // (Optional)
|
|
93
106
|
// Automatically detect common SQL/NoSQL injection and RCE patterns in request values. (Optional, default: true)
|
|
94
|
-
detectInjections: true
|
|
95
|
-
|
|
107
|
+
detectInjections: true,
|
|
108
|
+
// (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.
|
|
109
|
+
// Each function receives an object with all query and body data and should return `true` if a threat is detected.
|
|
110
|
+
analyzers: [
|
|
111
|
+
// Example 1: Using a general-purpose WAF library.
|
|
112
|
+
// (npm install generic-waf)
|
|
113
|
+
(data) => {
|
|
114
|
+
const WAF = require('generic-waf');
|
|
115
|
+
const waf = new WAF();
|
|
116
|
+
// This WAF expects a string, so we stringify the data to check all values at once.
|
|
117
|
+
return waf.isMalicious(JSON.stringify(data));
|
|
118
|
+
},
|
|
119
|
+
// Example 2: Using a specialized library for XSS detection.
|
|
120
|
+
// (npm install xss)
|
|
121
|
+
(data) => {
|
|
122
|
+
const xss = require('xss');
|
|
123
|
+
const originalData = JSON.stringify(data);
|
|
124
|
+
// If the sanitized string is different from the original, it means malicious HTML/JS was found and removed.
|
|
125
|
+
return xss(originalData) !== originalData;
|
|
126
|
+
},
|
|
127
|
+
// Example 3: A custom function to detect specific keywords (e.g., for anti-spam).
|
|
128
|
+
(data) => {
|
|
129
|
+
const spamKeywords = ['viagra', 'free money', 'crypto pump'];
|
|
130
|
+
const dataString = JSON.stringify(data).toLowerCase();
|
|
131
|
+
return spamKeywords.some(keyword => dataString.includes(keyword));
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
// The logger is required for auto-tuning. It collects data on requests.
|
|
136
|
+
logger: (log) => trafficData.push(log),
|
|
137
|
+
// (Optional) Configuration for the automatic threshold and pattern tuning.
|
|
138
|
+
autotuning: {
|
|
139
|
+
trafficData: trafficData, // The data source for the genetic algorithm.
|
|
140
|
+
interval: 1800000, // Optimization cycle every 30 minutes (in ms).
|
|
141
|
+
minDataPoints: 200 // Minimum requests before starting an optimization cycle.
|
|
142
|
+
},
|
|
96
143
|
};
|
|
97
144
|
|
|
98
145
|
// Create an instance of the middleware with your security configuration.
|
|
@@ -131,6 +178,8 @@ The main Express middleware. It orchestrates identification, suspicion calculati
|
|
|
131
178
|
#### `configureStore(store)`
|
|
132
179
|
Allows replacing the in-memory store with an external datastore (like Redis) for persistence and scaling.
|
|
133
180
|
|
|
181
|
+
See the Datastore Integration Guide for a complete example of creating a Redis store.
|
|
182
|
+
|
|
134
183
|
```javascript
|
|
135
184
|
import { configureStore } from './fingerprint.js';
|
|
136
185
|
import { createRedisStore } from './redis-store.js'; // Assuming a redis store implementation exists
|
|
@@ -213,22 +262,23 @@ Although not exported for direct public use, understanding its role can be usefu
|
|
|
213
262
|
|
|
214
263
|
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.
|
|
215
264
|
|
|
216
|
-
|
|
265
|
+
**For concrete examples with Koa and Fastify, see our Framework Integration Guide.**
|
|
266
|
+
|
|
267
|
+
The engine is a named export from the main module.
|
|
217
268
|
|
|
218
269
|
**Workflow:**
|
|
219
270
|
|
|
220
271
|
1. **Instantiate the Engine**: Create an instance with your `securityConfig`.
|
|
221
272
|
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.
|
|
222
|
-
3. **Process the Request**: Call `engine.processRequest(requestContext)`.
|
|
273
|
+
3. **Process the Request**: Call `engine.processRequest(requestContext)`. This method is asynchronous.
|
|
223
274
|
4. **Handle the Decision**: The engine returns a decision object (`{ action: 'challenge' | 'redirect' | 'next', ... }`). You are responsible for implementing the corresponding HTTP response.
|
|
224
275
|
|
|
225
276
|
**Example with native Node.js `http` server:**
|
|
226
277
|
|
|
227
278
|
```javascript
|
|
228
279
|
import http from 'http';
|
|
229
|
-
import {
|
|
280
|
+
import { FingerprintEngine } from './fingerprint.js'; // Adjust path
|
|
230
281
|
|
|
231
|
-
const { FingerprintEngine } = __internal;
|
|
232
282
|
const securityConfig = { /* ... your config ... */ };
|
|
233
283
|
const engine = new FingerprintEngine(securityConfig);
|
|
234
284
|
|
|
@@ -268,42 +318,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
268
318
|
server.listen(3000, () => console.log('Server with manual fingerprint engine started on port 3000'));
|
|
269
319
|
```
|
|
270
320
|
|
|
271
|
-
### Automatic Threshold Tuning
|
|
272
|
-
|
|
273
|
-
Manually setting the `low`, `medium`, and `high` thresholds can be challenging. This library provides a powerful tool to automate this process based on real traffic data. It uses a genetic algorithm to find the optimal thresholds that maximize bot detection while minimizing the impact on legitimate users.
|
|
274
|
-
|
|
275
|
-
#### How to use it:
|
|
276
|
-
|
|
277
|
-
1. **Enable Logging**: The auto-tuner needs data. You must provide a `logger` function in your security configuration. This function will be called for significant events (`challenge_issued`, `challenge_solved`, etc.).
|
|
278
|
-
|
|
279
|
-
2. **Enable Auto-tuning**: Add an `autotuning` property to your security configuration. The middleware will automatically start the tuning process.
|
|
280
|
-
|
|
281
|
-
```javascript
|
|
282
|
-
import { powMiddleware } from './fingerprint.js';
|
|
283
|
-
|
|
284
|
-
// Array to store traffic analysis data. In a real application, this could be
|
|
285
|
-
// a more robust logging system.
|
|
286
|
-
const trafficData = [];
|
|
287
|
-
|
|
288
|
-
const securityConfig = {
|
|
289
|
-
weights: { /* ... */ },
|
|
290
|
-
thresholds: {
|
|
291
|
-
low: 20, // Initial values, will be optimized
|
|
292
|
-
medium: 45,
|
|
293
|
-
high: 75
|
|
294
|
-
},
|
|
295
|
-
logger: (log) => trafficData.push(log), // The logger is required for auto-tuning
|
|
296
|
-
autotune: {
|
|
297
|
-
trafficData: trafficData, // The data source for the algorithm
|
|
298
|
-
interval: 1800000, // Optimization cycle every 30 minutes (optional)
|
|
299
|
-
minDataPoints: 200 // Minimum requests before starting optimization (optional)
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const powMiddlewareInstance = powMiddleware(securityConfig);
|
|
304
|
-
app.use(powMiddlewareInstance);
|
|
305
|
-
```
|
|
306
|
-
|
|
307
321
|
---
|
|
308
322
|
|
|
309
323
|
## License
|
package/fingerprint.js
CHANGED
|
@@ -15,6 +15,50 @@ const getPowSecret = () => {
|
|
|
15
15
|
return secret || "fallback-dev-secret-32-chars-minimum";
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Calculates the JA3 fingerprint hash from the TLS Client Hello message.
|
|
20
|
+
* JA3 is a more reliable way to identify client applications (e.g., a specific browser or a script)
|
|
21
|
+
* based on the specifics of its TLS handshake.
|
|
22
|
+
* @param {object} context - The request context, containing the raw request object.
|
|
23
|
+
* @returns {string|null} The MD5 hash of the JA3 string, or null if it cannot be computed.
|
|
24
|
+
*/
|
|
25
|
+
function getJa3Hash(context) {
|
|
26
|
+
// 1. Prefer the JA3 hash from a trusted reverse proxy (e.g., Nginx, Cloudflare).
|
|
27
|
+
const ja3FromHeader = context.headers['x-ja3-hash'];
|
|
28
|
+
if (ja3FromHeader) {
|
|
29
|
+
return ja3FromHeader;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Fallback to calculating from the raw socket if available (requires Node.js to handle TLS).
|
|
33
|
+
const clientHello = context.rawReq?.socket?.clientHello;
|
|
34
|
+
if (!clientHello) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const { version, ciphers, extensions, ellipticCurves, ellipticCurvePointFormats } = clientHello;
|
|
40
|
+
|
|
41
|
+
// The official JA3 spec includes the TLS version.
|
|
42
|
+
// Node.js provides it as a string like 'TLSv1.3', we need the corresponding decimal value.
|
|
43
|
+
const tlsVersionMap = {
|
|
44
|
+
'TLSv1': 769, 'TLSv1.1': 770, 'TLSv1.2': 771, 'TLSv1.3': 772
|
|
45
|
+
};
|
|
46
|
+
const tlsVersionId = tlsVersionMap[version] || 0;
|
|
47
|
+
|
|
48
|
+
const ja3String = [
|
|
49
|
+
tlsVersionId,
|
|
50
|
+
// The ciphers array from clientHello is an array of objects, not just IDs.
|
|
51
|
+
Array.isArray(ciphers) ? ciphers.join('-') : '',
|
|
52
|
+
extensions?.join('-') || '',
|
|
53
|
+
ellipticCurves?.join('-') || '',
|
|
54
|
+
ellipticCurvePointFormats?.join('-') || ''
|
|
55
|
+
].join(',');
|
|
56
|
+
|
|
57
|
+
return crypto.createHash('md5').update(ja3String).digest('hex');
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return null; // Could fail if clientHello structure is unexpected.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
18
62
|
/**
|
|
19
63
|
* Creates a stable hash based on device characteristics, independent of the IP.
|
|
20
64
|
* This is our "level 2 fingerprint".
|
|
@@ -43,6 +87,10 @@ export function getDeviceHash(context) {
|
|
|
43
87
|
if (context.headers["sec-ch-ua-platform"])
|
|
44
88
|
srv.add("os", context.headers["sec-ch-ua-platform"]);
|
|
45
89
|
if (context.headers["sec-ch-ua"]) srv.add("ch", context.headers["sec-ch-ua"]);
|
|
90
|
+
// Add JA3 hash if available. This is a very strong signal.
|
|
91
|
+
const ja3 = getJa3Hash(context);
|
|
92
|
+
if (ja3) srv.add("ja3", ja3);
|
|
93
|
+
|
|
46
94
|
srv.add("h_ord", getHeaderSignature(context));
|
|
47
95
|
return srv.toString();
|
|
48
96
|
}
|
|
@@ -406,6 +454,14 @@ function getHeaderAnomalies(context) {
|
|
|
406
454
|
*/
|
|
407
455
|
function getHoneypotScore(context, honeypotConfig = {}) {
|
|
408
456
|
const { fields = [], trapUrls = [], detectInjections = true } = honeypotConfig;
|
|
457
|
+
// (NOUVEAU) Permettre de brancher des analyseurs externes plus robustes.
|
|
458
|
+
// L'utilisateur pourrait passer une fonction qui prend les données de la requête
|
|
459
|
+
// et retourne `true` si une menace est détectée.
|
|
460
|
+
// Exemple: `(data) => myWafLibrary.isMalicious(data)`
|
|
461
|
+
const externalAnalyzers = honeypotConfig.analyzers || [];
|
|
462
|
+
if (typeof detectInjections === 'object' && detectInjections.analyzers) {
|
|
463
|
+
externalAnalyzers.push(...detectInjections.analyzers);
|
|
464
|
+
}
|
|
409
465
|
|
|
410
466
|
// 1. Check for trap URL access
|
|
411
467
|
if (trapUrls.some(trap => context.path.startsWith(trap))) {
|
|
@@ -434,28 +490,49 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
434
490
|
}
|
|
435
491
|
}
|
|
436
492
|
|
|
493
|
+
// 3. (NOUVEAU) Utiliser les analyseurs externes
|
|
494
|
+
const allData = { ...queryData, ...bodyData };
|
|
495
|
+
if (externalAnalyzers.length > 0) {
|
|
496
|
+
for (const analyzer of externalAnalyzers) {
|
|
497
|
+
// On passe à l'analyseur l'ensemble des données de la requête.
|
|
498
|
+
if (analyzer(allData)) {
|
|
499
|
+
return { honeypotScore: 100 };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
437
504
|
// 3. Check for injection attempts in values
|
|
438
505
|
if (detectInjections) {
|
|
439
506
|
// Regex for common SQL injection patterns
|
|
507
|
+
// WARNING: These are generic and may cause false positives.
|
|
508
|
+
// Consider using a dedicated WAF library or more specific regex for your application.
|
|
440
509
|
const sqlRegex = new RegExp(
|
|
441
|
-
"('|\"|;|--|#|/\\*.*\\*/)|\\b(union|select|insert|update|delete|drop|truncate)\\b",
|
|
510
|
+
"('|\"|;|--|#|/\\*.*\\*/)|\\b(union|select|insert|update|delete|drop|truncate|from|where|and|or)\\b",
|
|
442
511
|
"i"
|
|
443
512
|
);
|
|
444
513
|
// Regex for common NoSQL (MongoDB) injection patterns (e.g., keys starting with '$')
|
|
445
|
-
|
|
514
|
+
// This looks for keys like "$where", "$ne", etc. in a stringified JSON.
|
|
515
|
+
const nosqlKeyRegex = /"\$(where|ne|gt|lt|in|nin)":/;
|
|
446
516
|
// Regex for common Remote Code Execution (RCE) patterns
|
|
447
517
|
const rceRegex = new RegExp(
|
|
448
518
|
// File traversal, command execution functions, and shell commands
|
|
449
|
-
|
|
519
|
+
// Added process, child_process to catch Node.js specific RCE.
|
|
520
|
+
"(\\.\\./|\\.\\.\\\\)|\\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",
|
|
450
521
|
"i"
|
|
451
522
|
);
|
|
523
|
+
// Regex for Log4Shell (JNDI injection)
|
|
524
|
+
const log4shellRegex = new RegExp("\\$\\{jndi:", "i");
|
|
525
|
+
// Regex for Server-Side Template Injection (SSTI)
|
|
526
|
+
const sstiRegex = new RegExp(
|
|
527
|
+
"(\\{\\{|\\{%|#\\{)[^}]+(config|settings|self|class|application|request|session|process|env)", "i"
|
|
528
|
+
);
|
|
452
529
|
|
|
453
530
|
const inspect = (obj) => {
|
|
454
531
|
for (const key in obj) {
|
|
455
532
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
456
533
|
const value = obj[key];
|
|
457
534
|
if (typeof value === 'string') {
|
|
458
|
-
if (rceRegex.test(value) || sqlRegex.test(value)) return true;
|
|
535
|
+
if (rceRegex.test(value) || sqlRegex.test(value) || log4shellRegex.test(value) || sstiRegex.test(value)) return true;
|
|
459
536
|
} else if (typeof value === 'object' && value !== null) {
|
|
460
537
|
// For NoSQL, we check the stringified version of the object to find keys like "$gt"
|
|
461
538
|
// This is more accurate when done on the object itself.
|
|
@@ -478,6 +555,106 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
478
555
|
return { honeypotScore: 0 };
|
|
479
556
|
}
|
|
480
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Analyzes server-side request patterns for a given device to detect bot-like behavior.
|
|
560
|
+
* This is a stateful check that looks for repetitive or unnaturally fast requests.
|
|
561
|
+
* @param {object} context - The request context.
|
|
562
|
+
* @param {object} deviceData - The device's activity data from the store.
|
|
563
|
+
* @returns {{requestPatternScore: number}}
|
|
564
|
+
*/
|
|
565
|
+
function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
566
|
+
if (!deviceData) return { requestPatternScore: 0 };
|
|
567
|
+
|
|
568
|
+
// Default values for the pattern detection logic, which can be overridden by the auto-tuner.
|
|
569
|
+
const {
|
|
570
|
+
velocityThreshold = 200, velocityWeight = 30,
|
|
571
|
+
burstThreshold = 500, burstWeight = 50,
|
|
572
|
+
scrapeThreshold = 1000, scrapeWeight = 20, scrapeBurstWeight = 40,
|
|
573
|
+
historySize = 10,
|
|
574
|
+
decayFactor = 0.9,
|
|
575
|
+
inactivityReset = 30000,
|
|
576
|
+
// Nouveau paramètre pour la détection de séquences
|
|
577
|
+
sequenceLength = 3, sequenceWeight = 60
|
|
578
|
+
} = patternConfig;
|
|
579
|
+
|
|
580
|
+
const now = Date.now();
|
|
581
|
+
const currentPath = context.path;
|
|
582
|
+
// Make the function robust to handle both URLSearchParams and plain objects for query.
|
|
583
|
+
// Ensure query parameters are consistently handled, whether they come from a URLSearchParams object or a plain object.
|
|
584
|
+
const params = context.query instanceof URLSearchParams ? context.query : new URLSearchParams(context.query);
|
|
585
|
+
params.sort(); // Sort for deterministic order
|
|
586
|
+
const currentQueryString = params.toString();
|
|
587
|
+
|
|
588
|
+
// Initialize request history if it doesn't exist
|
|
589
|
+
if (!deviceData.requestHistory) {
|
|
590
|
+
deviceData.requestHistory = [];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const history = deviceData.requestHistory;
|
|
594
|
+
let score = 0;
|
|
595
|
+
|
|
596
|
+
// --- Analyze patterns based on the last few requests ---
|
|
597
|
+
if (history.length > 0) {
|
|
598
|
+
const lastRequest = history[history.length - 1];
|
|
599
|
+
const timeSinceLast = now - lastRequest.timestamp; // 150
|
|
600
|
+
|
|
601
|
+
// 1. Velocity Check: Penalize requests that are too fast to be human.
|
|
602
|
+
if (timeSinceLast < velocityThreshold) { // 150 < 200 -> true
|
|
603
|
+
score += velocityWeight; // score = 30
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 2. Burst Check: Add additional penalty for identical requests in a very short time frame.
|
|
607
|
+
if (currentPath === lastRequest.path && currentQueryString === lastRequest.queryString && timeSinceLast < burstThreshold) { // 150 < 500 -> true
|
|
608
|
+
score += burstWeight; // score = 30 + 50 = 80
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 3. Sequential Scraping Check: Add additional penalty for same path with different query params (potential scraping).
|
|
612
|
+
// This is a simplified check, now independent of the burst check.
|
|
613
|
+
if (currentPath === lastRequest.path && currentQueryString !== lastRequest.queryString && timeSinceLast < scrapeThreshold) {
|
|
614
|
+
const previousRequest = history.length > 2 ? history[history.length - 2] : null;
|
|
615
|
+
if (previousRequest && previousRequest.path === currentPath) {
|
|
616
|
+
score += scrapeBurstWeight; // This is at least the 3rd request in a sequence to the same path.
|
|
617
|
+
} else {
|
|
618
|
+
score += scrapeWeight; // First sign of a potential scraping pattern
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// 4. (NOUVEAU) Détection de séquences répétitives (ex: A -> B -> C -> A -> B -> C)
|
|
623
|
+
if (history.length >= sequenceLength * 2) {
|
|
624
|
+
const lastSequence = history.slice(-sequenceLength);
|
|
625
|
+
const previousSequence = history.slice(-sequenceLength * 2, -sequenceLength);
|
|
626
|
+
|
|
627
|
+
const isRepeating = lastSequence.every((req, i) =>
|
|
628
|
+
req.path === previousSequence[i].path && req.queryString === previousSequence[i].queryString
|
|
629
|
+
);
|
|
630
|
+
if (isRepeating) score += sequenceWeight;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// --- Update history ---
|
|
635
|
+
history.push({
|
|
636
|
+
timestamp: now,
|
|
637
|
+
path: currentPath,
|
|
638
|
+
queryString: currentQueryString,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Keep history to a reasonable size (e.g., last 10 requests)
|
|
642
|
+
if (history.length > historySize) {
|
|
643
|
+
history.shift();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Decay the score over time if behavior becomes normal again.
|
|
647
|
+
// We can store the score in deviceData and decay it.
|
|
648
|
+
deviceData.lastPatternScore = (deviceData.lastPatternScore || 0) * decayFactor + score; // Decay old score and add new
|
|
649
|
+
|
|
650
|
+
// If there hasn't been a request in a while, reset the pattern score.
|
|
651
|
+
if (history.length > 1 && (now - history[history.length - 2].timestamp > inactivityReset)) { // X ms inactivity
|
|
652
|
+
deviceData.lastPatternScore = 0;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return { requestPatternScore: Math.min(100, deviceData.lastPatternScore) };
|
|
656
|
+
}
|
|
657
|
+
|
|
481
658
|
const trapUrlTemplates = [
|
|
482
659
|
'/includes/config-{RANDOM}.php', // Classic PHP config file
|
|
483
660
|
'/.env.{RANDOM}', // Environment file
|
|
@@ -512,7 +689,13 @@ function generateTrapUrl(nonce) {
|
|
|
512
689
|
*/
|
|
513
690
|
function verifyTrapUrl(path, signature, nonce) {
|
|
514
691
|
const expectedSignature = crypto.createHmac('sha256', getPowSecret()).update(nonce + path).digest('hex').substring(0, 16);
|
|
515
|
-
|
|
692
|
+
try {
|
|
693
|
+
// Use timingSafeEqual to prevent timing attacks where an attacker could guess the signature byte by byte.
|
|
694
|
+
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
|
|
695
|
+
} catch {
|
|
696
|
+
// This will catch errors if buffers have different lengths or contain invalid hex characters, which is a failure case.
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
516
699
|
}
|
|
517
700
|
/**
|
|
518
701
|
* @typedef {object} IStore
|
|
@@ -589,10 +772,13 @@ async function resolveRequestIdentity(context) {
|
|
|
589
772
|
deviceData = {
|
|
590
773
|
initialDeviceHash: currentDeviceHash, // Anchor the initial fingerprint.
|
|
591
774
|
ips: new Set(),
|
|
775
|
+
requestHistory: [], // Initialize state for the new pattern score
|
|
592
776
|
lastUpdate: Date.now(),
|
|
593
777
|
lastFpHash: currentDeviceHash,
|
|
594
778
|
lastChangeTimestamp: 0,
|
|
595
779
|
rapidChangeCount: 0,
|
|
780
|
+
highScoreCount: 0,
|
|
781
|
+
lastHighScoreTimestamp: 0,
|
|
596
782
|
};
|
|
597
783
|
// The write will happen in getSuspicionVector after all modifications.
|
|
598
784
|
}
|
|
@@ -692,6 +878,11 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
692
878
|
// 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.
|
|
693
879
|
await store.set(`device:${deviceId}`, deviceData);
|
|
694
880
|
|
|
881
|
+
// Ensure deviceData.ips is a Set for subsequent operations within the same request,
|
|
882
|
+
// even if the store returns an array.
|
|
883
|
+
if (Array.isArray(deviceData.ips)) {
|
|
884
|
+
deviceData.ips = new Set(deviceData.ips);
|
|
885
|
+
}
|
|
695
886
|
return { ...behavioral, headerAnomalyScore, inconsistencyScore };
|
|
696
887
|
};
|
|
697
888
|
|
|
@@ -926,7 +1117,7 @@ const staticExtensions = new RegExp(
|
|
|
926
1117
|
const isStaticResource = (path) => staticExtensions.test(path);
|
|
927
1118
|
|
|
928
1119
|
// --- Middleware Proof-of-Work (Le péage) ---
|
|
929
|
-
class FingerprintEngine {
|
|
1120
|
+
export class FingerprintEngine {
|
|
930
1121
|
constructor(securityConfig) {
|
|
931
1122
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
932
1123
|
this.securityConfig = securityConfig;
|
|
@@ -935,30 +1126,56 @@ class FingerprintEngine {
|
|
|
935
1126
|
|
|
936
1127
|
async processRequest(requestContext) {
|
|
937
1128
|
const { clientIp, path, cookies, query, isStatic } = requestContext;
|
|
938
|
-
const { weights, thresholds, logger } = this.securityConfig;
|
|
1129
|
+
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
939
1130
|
|
|
940
1131
|
if (isStatic) {
|
|
941
1132
|
return { action: 'next', score: 0, vector: {} };
|
|
942
1133
|
}
|
|
943
1134
|
|
|
1135
|
+
const { pow_nonce } = query;
|
|
1136
|
+
|
|
1137
|
+
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
1138
|
+
// A legitimate user only hits these endpoints via the challenge page itself, which is only served to suspicious users.
|
|
1139
|
+
// If we see a pow_nonce on a request that isn't (yet) considered suspicious, it's a bot.
|
|
1140
|
+
// We check this early, before the main suspicion calculation.
|
|
1141
|
+
if (pow_nonce) {
|
|
1142
|
+
const powCookie = cookies?.pow_clearance;
|
|
1143
|
+
if (!isTicketValid(clientIp, powCookie)) { // Only check if there's no valid ticket
|
|
1144
|
+
// This is a potential probe. We'll let the main logic confirm if it's not a legitimate challenge response.
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
944
1148
|
// Check for persisted "condemned" status early.
|
|
945
1149
|
const { deviceData } = await resolveRequestIdentity(requestContext);
|
|
946
1150
|
if (deviceData?.condemned) {
|
|
1151
|
+
if (onDeviceCompromised) {
|
|
1152
|
+
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
1153
|
+
}
|
|
947
1154
|
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
948
1155
|
}
|
|
949
1156
|
|
|
1157
|
+
// (NOUVEAU) Vérifier si un nouveau device_id a été créé lors de cette requête
|
|
1158
|
+
const isNewDevice = requestContext._newCookies?.some(c => c.name === 'device_id');
|
|
1159
|
+
|
|
950
1160
|
// The engine now works with the context directly, no more rawReq dependency here.
|
|
951
1161
|
const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
952
1162
|
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
1163
|
+
const { requestPatternScore } = getRequestPatternScore(requestContext, deviceData, this.securityConfig.patterns);
|
|
953
1164
|
suspicionVector.honeypotScore = honeypotScore;
|
|
1165
|
+
suspicionVector.requestPatternScore = requestPatternScore;
|
|
954
1166
|
|
|
955
1167
|
const finalScore =
|
|
956
1168
|
suspicionVector.historyScore * (weights.historyScore || 0) +
|
|
957
1169
|
suspicionVector.rotationScore * (weights.rotationScore || 0) +
|
|
958
|
-
suspicionVector.headerAnomalyScore * (weights.headerAnomalyScore || 0) +
|
|
1170
|
+
suspicionVector.headerAnomalyScore * (weights.headerAnomalyScore || 0) + suspicionVector.requestPatternScore * (weights.requestPatternScore || 0) +
|
|
959
1171
|
suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0) +
|
|
960
1172
|
honeypotScore * (weights.honeypotScore || 0);
|
|
961
1173
|
|
|
1174
|
+
// Si c'est un nouvel appareil, on lui impose un challenge de base, même si son score est bas.
|
|
1175
|
+
// Cela augmente le coût pour les bots qui tentent de simplement supprimer leurs cookies.
|
|
1176
|
+
const requiresChallengeForNewDevice = isNewDevice && finalScore < thresholds.low;
|
|
1177
|
+
|
|
1178
|
+
|
|
962
1179
|
const isBlocked = finalScore >= (thresholds.block || 95);
|
|
963
1180
|
|
|
964
1181
|
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
@@ -973,7 +1190,7 @@ class FingerprintEngine {
|
|
|
973
1190
|
)
|
|
974
1191
|
: 0;
|
|
975
1192
|
const powCookie = cookies?.pow_clearance;
|
|
976
|
-
const { pow_type,
|
|
1193
|
+
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
977
1194
|
|
|
978
1195
|
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
979
1196
|
// A legitimate user only hits these endpoints via the challenge page itself.
|
|
@@ -983,12 +1200,15 @@ class FingerprintEngine {
|
|
|
983
1200
|
}
|
|
984
1201
|
suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
|
|
985
1202
|
// Recalculate score and block immediately.
|
|
986
|
-
const newFinalScore = finalScore + (100 * (weights.honeypotScore || 0));
|
|
1203
|
+
const newFinalScore = finalScore - (honeypotScore * (weights.honeypotScore || 0)) + (100 * (weights.honeypotScore || 0));
|
|
987
1204
|
return { action: 'block', status: 403, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
|
|
988
1205
|
}
|
|
989
1206
|
|
|
990
1207
|
// If the action is to block, we should still include the score and vector for logging/testing.
|
|
991
1208
|
if (isBlocked) {
|
|
1209
|
+
if (onDeviceCompromised) {
|
|
1210
|
+
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
|
|
1211
|
+
}
|
|
992
1212
|
return { action: 'block', status: 403, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
993
1213
|
}
|
|
994
1214
|
|
|
@@ -997,11 +1217,17 @@ class FingerprintEngine {
|
|
|
997
1217
|
const lastNonce = deviceData?.lastChallengeNonce;
|
|
998
1218
|
if (lastNonce && query.sig && verifyTrapUrl(path, query.sig, lastNonce)) {
|
|
999
1219
|
deviceData.condemned = true; // This device is a bot. Condemn it.
|
|
1220
|
+
if (onDeviceCompromised) {
|
|
1221
|
+
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
|
|
1222
|
+
}
|
|
1223
|
+
if (logger) {
|
|
1224
|
+
logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
|
|
1225
|
+
}
|
|
1000
1226
|
await store.set(`device:${cookies.device_id}`, deviceData);
|
|
1001
1227
|
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1002
1228
|
}
|
|
1003
1229
|
|
|
1004
|
-
if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
|
|
1230
|
+
if ((isSuspicious || requiresChallengeForNewDevice) && !isTicketValid(clientIp, powCookie)) {
|
|
1005
1231
|
// --- CHALLENGE SOLUTION HANDLING ---
|
|
1006
1232
|
if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
|
|
1007
1233
|
let isValid = false,
|
|
@@ -1097,7 +1323,7 @@ class FingerprintEngine {
|
|
|
1097
1323
|
}
|
|
1098
1324
|
|
|
1099
1325
|
// UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
|
|
1100
|
-
if (isSuspicious) { // Couvre à la fois low et medium
|
|
1326
|
+
if (isSuspicious || requiresChallengeForNewDevice) { // Couvre à la fois low et medium
|
|
1101
1327
|
// Generate some trap URLs to embed in the challenge page.
|
|
1102
1328
|
// These links are visually hidden but present in the DOM to trap bots.
|
|
1103
1329
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
@@ -1163,12 +1389,14 @@ class FingerprintEngine {
|
|
|
1163
1389
|
|
|
1164
1390
|
const vector = await __internal.getSuspicionVector(requestContext, this.securityConfig); // Pass the config
|
|
1165
1391
|
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
1392
|
+
const { requestPatternScore } = getRequestPatternScore(requestContext, (await store.get(`device:${requestContext.cookies?.device_id}`)), this.securityConfig.patterns);
|
|
1166
1393
|
const score =
|
|
1167
1394
|
vector.historyScore * (this.securityConfig.weights.historyScore || 0.3) +
|
|
1168
1395
|
vector.rotationScore * (this.securityConfig.weights.rotationScore || 0.5) +
|
|
1169
1396
|
vector.headerAnomalyScore * (this.securityConfig.weights.headerAnomalyScore || 0.1) +
|
|
1170
1397
|
vector.inconsistencyScore * (this.securityConfig.weights.inconsistencyScore || 0.8) +
|
|
1171
|
-
honeypotScore * (this.securityConfig.weights.honeypotScore || 0)
|
|
1398
|
+
honeypotScore * (this.securityConfig.weights.honeypotScore || 0) +
|
|
1399
|
+
requestPatternScore * (this.securityConfig.weights.requestPatternScore || 0);
|
|
1172
1400
|
|
|
1173
1401
|
if (score >= this.securityConfig.thresholds.high) return `suspicious_high:${clientIp}`;
|
|
1174
1402
|
if (score >= this.securityConfig.thresholds.low) return `suspicious_medium:${clientIp}`; // Use medium for any suspicion
|
|
@@ -1204,6 +1432,8 @@ export const powMiddleware = (securityConfig) => {
|
|
|
1204
1432
|
isStatic: isStaticResource(req.path),
|
|
1205
1433
|
// Add the newly required properties for full decoupling
|
|
1206
1434
|
rawHeaders: req.rawHeaders,
|
|
1435
|
+
// Pass the raw request object for advanced inspection (e.g., JA3)
|
|
1436
|
+
rawReq: req,
|
|
1207
1437
|
httpVersion: req.httpVersion,
|
|
1208
1438
|
};
|
|
1209
1439
|
|
|
@@ -1251,7 +1481,7 @@ export const __internal = {
|
|
|
1251
1481
|
cyrb53, // Export for testing
|
|
1252
1482
|
FingerprintBuilder, // Export for testing
|
|
1253
1483
|
calculateTarget,
|
|
1254
|
-
|
|
1484
|
+
getRequestPatternScore, // Expose for testing
|
|
1255
1485
|
};
|
|
1256
1486
|
|
|
1257
1487
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -1272,45 +1502,78 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
|
1272
1502
|
}
|
|
1273
1503
|
console.log(`[AutoTuning] Démarrage du cycle d'optimisation avec ${trafficData.length} points de données.`);
|
|
1274
1504
|
|
|
1275
|
-
//
|
|
1276
|
-
// and "humans" (those who passed the challenge or never received one).
|
|
1505
|
+
// Classify historical requests with a confidence weight.
|
|
1277
1506
|
const solvedDevices = new Set(trafficData.filter(e => e.type === 'challenge_solved').map(e => e.deviceId));
|
|
1507
|
+
const challengedDevices = new Set(trafficData.filter(e => e.type === 'challenge_issued').map(e => e.deviceId));
|
|
1508
|
+
|
|
1278
1509
|
const historicalRequests = trafficData.map(log => {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1510
|
+
// Assign a label ('bot' or 'human') and a confidence weight to each log entry.
|
|
1511
|
+
switch (log.type) {
|
|
1512
|
+
case 'honeypot_probe':
|
|
1513
|
+
case 'trap_triggered':
|
|
1514
|
+
return { score: log.score, label: 'bot', confidence: 10.0 }; // Very high confidence
|
|
1515
|
+
|
|
1516
|
+
case 'challenge_issued':
|
|
1517
|
+
// A challenge issued to a device that never solved it is a strong bot signal.
|
|
1518
|
+
if (!solvedDevices.has(log.deviceId)) {
|
|
1519
|
+
return { score: log.score, label: 'bot', confidence: 3.0 }; // High confidence
|
|
1520
|
+
}
|
|
1521
|
+
// If the challenge was eventually solved, this specific log is neutral.
|
|
1522
|
+
return null;
|
|
1523
|
+
|
|
1524
|
+
case 'challenge_solved':
|
|
1525
|
+
return { score: log.score, label: 'human', confidence: 5.0 }; // High confidence
|
|
1526
|
+
|
|
1527
|
+
case 'request_passed':
|
|
1528
|
+
// A passed request from a device that was never even challenged is likely a human.
|
|
1529
|
+
if (!challengedDevices.has(log.deviceId)) {
|
|
1530
|
+
return { score: log.score, label: 'human', confidence: 0.5 }; // Low confidence
|
|
1531
|
+
}
|
|
1532
|
+
// If the device was challenged at some point, this log is ambiguous.
|
|
1533
|
+
return null;
|
|
1534
|
+
|
|
1535
|
+
default:
|
|
1536
|
+
return null;
|
|
1282
1537
|
}
|
|
1283
|
-
|
|
1284
|
-
});
|
|
1538
|
+
}).filter(Boolean); // Remove null entries
|
|
1285
1539
|
|
|
1286
1540
|
// The "fitness" function evaluates the quality of a set of thresholds.
|
|
1287
1541
|
// A lower score is better.
|
|
1288
1542
|
const fitnessFunction = (solution) => {
|
|
1289
|
-
const [low, medium, high] = solution;
|
|
1290
|
-
|
|
1291
|
-
if (
|
|
1543
|
+
const [low, medium, high, velocityThreshold, burstThreshold, scrapeThreshold] = solution;
|
|
1544
|
+
if (low >= medium || medium >= high || low < 10 || high > 90) return Infinity;
|
|
1545
|
+
if (velocityThreshold < 50 || velocityThreshold > burstThreshold || burstThreshold > scrapeThreshold) return Infinity;
|
|
1292
1546
|
|
|
1293
|
-
let
|
|
1294
|
-
let
|
|
1547
|
+
let weightedFalsePositives = 0; // Humans challenged unnecessarily.
|
|
1548
|
+
let weightedFalseNegatives = 0; // Undetected bots.
|
|
1295
1549
|
|
|
1296
1550
|
for (const req of historicalRequests) {
|
|
1297
|
-
if (req.
|
|
1298
|
-
if (req.score < low)
|
|
1299
|
-
} else { //
|
|
1300
|
-
if (req.score >= low)
|
|
1551
|
+
if (req.label === 'bot') {
|
|
1552
|
+
if (req.score < low) weightedFalseNegatives += req.confidence;
|
|
1553
|
+
} else { // 'human'
|
|
1554
|
+
if (req.score >= low) weightedFalsePositives += req.confidence;
|
|
1301
1555
|
}
|
|
1302
1556
|
}
|
|
1303
|
-
//
|
|
1304
|
-
return
|
|
1557
|
+
// The penalty for false negatives is implicitly higher due to the higher confidence scores of bot signals.
|
|
1558
|
+
return weightedFalsePositives + weightedFalseNegatives;
|
|
1305
1559
|
};
|
|
1306
1560
|
|
|
1307
1561
|
// Functions for the genetic algorithm.
|
|
1308
|
-
const createIndividual = () => [
|
|
1309
|
-
|
|
1562
|
+
const createIndividual = () => [
|
|
1563
|
+
10 + Math.random() * 20, // low
|
|
1564
|
+
30 + Math.random() * 30, // medium
|
|
1565
|
+
60 + Math.random() * 30, // high
|
|
1566
|
+
100 + Math.random() * 150, // velocityThreshold (100-250ms)
|
|
1567
|
+
300 + Math.random() * 400, // burstThreshold (300-700ms)
|
|
1568
|
+
800 + Math.random() * 700, // scrapeThreshold (800-1500ms)
|
|
1569
|
+
];
|
|
1570
|
+
const crossover = (p1, p2) => p1.map((val, i) => (val + p2[i]) / 2);
|
|
1310
1571
|
const mutate = (s) => {
|
|
1311
1572
|
const n = [...s];
|
|
1312
|
-
const i = Math.floor(Math.random() *
|
|
1313
|
-
|
|
1573
|
+
const i = Math.floor(Math.random() * n.length);
|
|
1574
|
+
// Adjust mutation range based on parameter
|
|
1575
|
+
const mutationRange = i < 3 ? 5 : 50;
|
|
1576
|
+
n[i] += (Math.random() - 0.5) * mutationRange;
|
|
1314
1577
|
return n;
|
|
1315
1578
|
};
|
|
1316
1579
|
|
|
@@ -1320,16 +1583,26 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
|
1320
1583
|
populationSize: 40
|
|
1321
1584
|
});
|
|
1322
1585
|
|
|
1323
|
-
const [newLow, newMedium, newHigh] = result.solution;
|
|
1586
|
+
const [newLow, newMedium, newHigh, newVelocity, newBurst, newScrape] = result.solution;
|
|
1324
1587
|
|
|
1325
1588
|
// Update the configuration live.
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1589
|
+
// Ensure thresholds object exists
|
|
1590
|
+
if (!securityConfig.thresholds) securityConfig.thresholds = {};
|
|
1591
|
+
securityConfig.thresholds.low = Math.round(newLow);
|
|
1592
|
+
securityConfig.thresholds.medium = Math.round(newMedium);
|
|
1593
|
+
securityConfig.thresholds.high = Math.round(newHigh);
|
|
1594
|
+
|
|
1595
|
+
// Update pattern detection parameters
|
|
1596
|
+
if (!securityConfig.patterns) securityConfig.patterns = {};
|
|
1597
|
+
securityConfig.patterns.velocityThreshold = Math.round(newVelocity);
|
|
1598
|
+
securityConfig.patterns.burstThreshold = Math.round(newBurst);
|
|
1599
|
+
securityConfig.patterns.scrapeThreshold = Math.round(newScrape);
|
|
1600
|
+
// Weights could also be optimized, but let's keep it to thresholds for now for simplicity.
|
|
1331
1601
|
|
|
1332
1602
|
console.log("[AutoTuning] Nouveaux seuils optimisés appliqués :", securityConfig.thresholds);
|
|
1603
|
+
if (securityConfig.patterns) {
|
|
1604
|
+
console.log("[AutoTuning] Nouveaux paramètres de pattern appliqués :", securityConfig.patterns);
|
|
1605
|
+
}
|
|
1333
1606
|
}
|
|
1334
1607
|
|
|
1335
1608
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
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
5
|
"main": "fingerprint.js",
|
|
6
6
|
"type": "module",
|