@anonympins/fingerprint 0.0.5 → 0.0.6
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 +23 -38
- package/fingerprint.js +167 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ The process unfolds in three steps:
|
|
|
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.
|
|
@@ -92,7 +105,15 @@ const securityConfig = {
|
|
|
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
107
|
detectInjections: true
|
|
95
|
-
}
|
|
108
|
+
},
|
|
109
|
+
// The logger is required for auto-tuning. It collects data on requests.
|
|
110
|
+
logger: (log) => trafficData.push(log),
|
|
111
|
+
// (Optional) Configuration for the automatic threshold and pattern tuning.
|
|
112
|
+
autotuning: {
|
|
113
|
+
trafficData: trafficData, // The data source for the genetic algorithm.
|
|
114
|
+
interval: 1800000, // Optimization cycle every 30 minutes (in ms).
|
|
115
|
+
minDataPoints: 200 // Minimum requests before starting an optimization cycle.
|
|
116
|
+
},
|
|
96
117
|
};
|
|
97
118
|
|
|
98
119
|
// Create an instance of the middleware with your security configuration.
|
|
@@ -268,42 +289,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
268
289
|
server.listen(3000, () => console.log('Server with manual fingerprint engine started on port 3000'));
|
|
269
290
|
```
|
|
270
291
|
|
|
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
292
|
---
|
|
308
293
|
|
|
309
294
|
## License
|
package/fingerprint.js
CHANGED
|
@@ -437,16 +437,20 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
437
437
|
// 3. Check for injection attempts in values
|
|
438
438
|
if (detectInjections) {
|
|
439
439
|
// Regex for common SQL injection patterns
|
|
440
|
+
// WARNING: These are generic and may cause false positives.
|
|
441
|
+
// Consider using a dedicated WAF library or more specific regex for your application.
|
|
440
442
|
const sqlRegex = new RegExp(
|
|
441
|
-
"('|\"|;|--|#|/\\*.*\\*/)|\\b(union|select|insert|update|delete|drop|truncate)\\b",
|
|
443
|
+
"('|\"|;|--|#|/\\*.*\\*/)|\\b(union|select|insert|update|delete|drop|truncate|from|where|and|or)\\b",
|
|
442
444
|
"i"
|
|
443
445
|
);
|
|
444
446
|
// Regex for common NoSQL (MongoDB) injection patterns (e.g., keys starting with '$')
|
|
445
|
-
|
|
447
|
+
// This looks for keys like "$where", "$ne", etc. in a stringified JSON.
|
|
448
|
+
const nosqlKeyRegex = /"\$(where|ne|gt|lt|in|nin)":/;
|
|
446
449
|
// Regex for common Remote Code Execution (RCE) patterns
|
|
447
450
|
const rceRegex = new RegExp(
|
|
448
451
|
// File traversal, command execution functions, and shell commands
|
|
449
|
-
|
|
452
|
+
// Added process, child_process to catch Node.js specific RCE.
|
|
453
|
+
"(\\.\\./|\\.\\.\\\\)|\\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
454
|
"i"
|
|
451
455
|
);
|
|
452
456
|
|
|
@@ -478,6 +482,93 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
478
482
|
return { honeypotScore: 0 };
|
|
479
483
|
}
|
|
480
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Analyzes server-side request patterns for a given device to detect bot-like behavior.
|
|
487
|
+
* This is a stateful check that looks for repetitive or unnaturally fast requests.
|
|
488
|
+
* @param {object} context - The request context.
|
|
489
|
+
* @param {object} deviceData - The device's activity data from the store.
|
|
490
|
+
* @returns {{requestPatternScore: number}}
|
|
491
|
+
*/
|
|
492
|
+
function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
493
|
+
if (!deviceData) return { requestPatternScore: 0 };
|
|
494
|
+
|
|
495
|
+
// Default values for the pattern detection logic, which can be overridden by the auto-tuner.
|
|
496
|
+
const {
|
|
497
|
+
velocityThreshold = 200, velocityWeight = 30,
|
|
498
|
+
burstThreshold = 500, burstWeight = 50,
|
|
499
|
+
scrapeThreshold = 1000, scrapeWeight = 20, scrapeBurstWeight = 40,
|
|
500
|
+
historySize = 10,
|
|
501
|
+
decayFactor = 0.9,
|
|
502
|
+
inactivityReset = 30000
|
|
503
|
+
} = patternConfig;
|
|
504
|
+
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
const currentPath = context.path;
|
|
507
|
+
// Make the function robust to handle both URLSearchParams and plain objects for query.
|
|
508
|
+
// Ensure query parameters are consistently handled, whether they come from a URLSearchParams object or a plain object.
|
|
509
|
+
const params = context.query instanceof URLSearchParams ? context.query : new URLSearchParams(context.query);
|
|
510
|
+
params.sort(); // Sort for deterministic order
|
|
511
|
+
const currentQueryString = params.toString();
|
|
512
|
+
|
|
513
|
+
// Initialize request history if it doesn't exist
|
|
514
|
+
if (!deviceData.requestHistory) {
|
|
515
|
+
deviceData.requestHistory = [];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const history = deviceData.requestHistory;
|
|
519
|
+
let score = 0;
|
|
520
|
+
|
|
521
|
+
// --- Analyze patterns based on the last few requests ---
|
|
522
|
+
if (history.length > 0) {
|
|
523
|
+
const lastRequest = history[history.length - 1];
|
|
524
|
+
const timeSinceLast = now - lastRequest.timestamp; // 150
|
|
525
|
+
|
|
526
|
+
// 1. Velocity Check: Penalize requests that are too fast to be human.
|
|
527
|
+
if (timeSinceLast < velocityThreshold) { // 150 < 200 -> true
|
|
528
|
+
score += velocityWeight; // score = 30
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 2. Burst Check: Add additional penalty for identical requests in a very short time frame.
|
|
532
|
+
if (currentPath === lastRequest.path && currentQueryString === lastRequest.queryString && timeSinceLast < burstThreshold) { // 150 < 500 -> true
|
|
533
|
+
score += burstWeight; // score = 30 + 50 = 80
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 3. Sequential Scraping Check: Add additional penalty for same path with different query params (potential scraping).
|
|
537
|
+
// This is a simplified check, now independent of the burst check.
|
|
538
|
+
if (currentPath === lastRequest.path && currentQueryString !== lastRequest.queryString && timeSinceLast < scrapeThreshold) {
|
|
539
|
+
const previousRequest = history.length > 2 ? history[history.length - 2] : null;
|
|
540
|
+
if (previousRequest && previousRequest.path === currentPath) {
|
|
541
|
+
score += scrapeBurstWeight; // This is at least the 3rd request in a sequence to the same path.
|
|
542
|
+
} else {
|
|
543
|
+
score += scrapeWeight; // First sign of a potential scraping pattern
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// --- Update history ---
|
|
549
|
+
history.push({
|
|
550
|
+
timestamp: now,
|
|
551
|
+
path: currentPath,
|
|
552
|
+
queryString: currentQueryString,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Keep history to a reasonable size (e.g., last 10 requests)
|
|
556
|
+
if (history.length > historySize) {
|
|
557
|
+
history.shift();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Decay the score over time if behavior becomes normal again.
|
|
561
|
+
// We can store the score in deviceData and decay it.
|
|
562
|
+
deviceData.lastPatternScore = (deviceData.lastPatternScore || 0) * decayFactor + score; // Decay old score and add new
|
|
563
|
+
|
|
564
|
+
// If there hasn't been a request in a while, reset the pattern score.
|
|
565
|
+
if (history.length > 1 && (now - history[history.length - 2].timestamp > inactivityReset)) { // X ms inactivity
|
|
566
|
+
deviceData.lastPatternScore = 0;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return { requestPatternScore: Math.min(100, deviceData.lastPatternScore) };
|
|
570
|
+
}
|
|
571
|
+
|
|
481
572
|
const trapUrlTemplates = [
|
|
482
573
|
'/includes/config-{RANDOM}.php', // Classic PHP config file
|
|
483
574
|
'/.env.{RANDOM}', // Environment file
|
|
@@ -512,7 +603,13 @@ function generateTrapUrl(nonce) {
|
|
|
512
603
|
*/
|
|
513
604
|
function verifyTrapUrl(path, signature, nonce) {
|
|
514
605
|
const expectedSignature = crypto.createHmac('sha256', getPowSecret()).update(nonce + path).digest('hex').substring(0, 16);
|
|
515
|
-
|
|
606
|
+
try {
|
|
607
|
+
// Use timingSafeEqual to prevent timing attacks where an attacker could guess the signature byte by byte.
|
|
608
|
+
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
|
|
609
|
+
} catch {
|
|
610
|
+
// This will catch errors if buffers have different lengths or contain invalid hex characters, which is a failure case.
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
516
613
|
}
|
|
517
614
|
/**
|
|
518
615
|
* @typedef {object} IStore
|
|
@@ -589,6 +686,7 @@ async function resolveRequestIdentity(context) {
|
|
|
589
686
|
deviceData = {
|
|
590
687
|
initialDeviceHash: currentDeviceHash, // Anchor the initial fingerprint.
|
|
591
688
|
ips: new Set(),
|
|
689
|
+
requestHistory: [], // Initialize state for the new pattern score
|
|
592
690
|
lastUpdate: Date.now(),
|
|
593
691
|
lastFpHash: currentDeviceHash,
|
|
594
692
|
lastChangeTimestamp: 0,
|
|
@@ -935,27 +1033,45 @@ class FingerprintEngine {
|
|
|
935
1033
|
|
|
936
1034
|
async processRequest(requestContext) {
|
|
937
1035
|
const { clientIp, path, cookies, query, isStatic } = requestContext;
|
|
938
|
-
const { weights, thresholds, logger } = this.securityConfig;
|
|
1036
|
+
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
939
1037
|
|
|
940
1038
|
if (isStatic) {
|
|
941
1039
|
return { action: 'next', score: 0, vector: {} };
|
|
942
1040
|
}
|
|
943
1041
|
|
|
1042
|
+
const { pow_nonce } = query;
|
|
1043
|
+
|
|
1044
|
+
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
1045
|
+
// A legitimate user only hits these endpoints via the challenge page itself, which is only served to suspicious users.
|
|
1046
|
+
// If we see a pow_nonce on a request that isn't (yet) considered suspicious, it's a bot.
|
|
1047
|
+
// We check this early, before the main suspicion calculation.
|
|
1048
|
+
if (pow_nonce) {
|
|
1049
|
+
const powCookie = cookies?.pow_clearance;
|
|
1050
|
+
if (!isTicketValid(clientIp, powCookie)) { // Only check if there's no valid ticket
|
|
1051
|
+
// This is a potential probe. We'll let the main logic confirm if it's not a legitimate challenge response.
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
944
1055
|
// Check for persisted "condemned" status early.
|
|
945
1056
|
const { deviceData } = await resolveRequestIdentity(requestContext);
|
|
946
1057
|
if (deviceData?.condemned) {
|
|
1058
|
+
if (onDeviceCompromised) {
|
|
1059
|
+
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
1060
|
+
}
|
|
947
1061
|
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
948
1062
|
}
|
|
949
1063
|
|
|
950
1064
|
// The engine now works with the context directly, no more rawReq dependency here.
|
|
951
1065
|
const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
952
1066
|
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
1067
|
+
const { requestPatternScore } = getRequestPatternScore(requestContext, deviceData, this.securityConfig.patterns);
|
|
953
1068
|
suspicionVector.honeypotScore = honeypotScore;
|
|
1069
|
+
suspicionVector.requestPatternScore = requestPatternScore;
|
|
954
1070
|
|
|
955
1071
|
const finalScore =
|
|
956
1072
|
suspicionVector.historyScore * (weights.historyScore || 0) +
|
|
957
1073
|
suspicionVector.rotationScore * (weights.rotationScore || 0) +
|
|
958
|
-
suspicionVector.headerAnomalyScore * (weights.headerAnomalyScore || 0) +
|
|
1074
|
+
suspicionVector.headerAnomalyScore * (weights.headerAnomalyScore || 0) + suspicionVector.requestPatternScore * (weights.requestPatternScore || 0) +
|
|
959
1075
|
suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0) +
|
|
960
1076
|
honeypotScore * (weights.honeypotScore || 0);
|
|
961
1077
|
|
|
@@ -973,7 +1089,7 @@ class FingerprintEngine {
|
|
|
973
1089
|
)
|
|
974
1090
|
: 0;
|
|
975
1091
|
const powCookie = cookies?.pow_clearance;
|
|
976
|
-
const { pow_type,
|
|
1092
|
+
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
977
1093
|
|
|
978
1094
|
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
979
1095
|
// A legitimate user only hits these endpoints via the challenge page itself.
|
|
@@ -983,12 +1099,15 @@ class FingerprintEngine {
|
|
|
983
1099
|
}
|
|
984
1100
|
suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
|
|
985
1101
|
// Recalculate score and block immediately.
|
|
986
|
-
const newFinalScore = finalScore + (100 * (weights.honeypotScore || 0));
|
|
1102
|
+
const newFinalScore = finalScore - (honeypotScore * (weights.honeypotScore || 0)) + (100 * (weights.honeypotScore || 0));
|
|
987
1103
|
return { action: 'block', status: 403, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
|
|
988
1104
|
}
|
|
989
1105
|
|
|
990
1106
|
// If the action is to block, we should still include the score and vector for logging/testing.
|
|
991
1107
|
if (isBlocked) {
|
|
1108
|
+
if (onDeviceCompromised) {
|
|
1109
|
+
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
|
|
1110
|
+
}
|
|
992
1111
|
return { action: 'block', status: 403, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
993
1112
|
}
|
|
994
1113
|
|
|
@@ -997,6 +1116,9 @@ class FingerprintEngine {
|
|
|
997
1116
|
const lastNonce = deviceData?.lastChallengeNonce;
|
|
998
1117
|
if (lastNonce && query.sig && verifyTrapUrl(path, query.sig, lastNonce)) {
|
|
999
1118
|
deviceData.condemned = true; // This device is a bot. Condemn it.
|
|
1119
|
+
if (onDeviceCompromised) {
|
|
1120
|
+
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
|
|
1121
|
+
}
|
|
1000
1122
|
await store.set(`device:${cookies.device_id}`, deviceData);
|
|
1001
1123
|
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1002
1124
|
}
|
|
@@ -1163,12 +1285,14 @@ class FingerprintEngine {
|
|
|
1163
1285
|
|
|
1164
1286
|
const vector = await __internal.getSuspicionVector(requestContext, this.securityConfig); // Pass the config
|
|
1165
1287
|
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
1288
|
+
const { requestPatternScore } = getRequestPatternScore(requestContext, (await store.get(`device:${requestContext.cookies?.device_id}`)), this.securityConfig.patterns);
|
|
1166
1289
|
const score =
|
|
1167
1290
|
vector.historyScore * (this.securityConfig.weights.historyScore || 0.3) +
|
|
1168
1291
|
vector.rotationScore * (this.securityConfig.weights.rotationScore || 0.5) +
|
|
1169
1292
|
vector.headerAnomalyScore * (this.securityConfig.weights.headerAnomalyScore || 0.1) +
|
|
1170
1293
|
vector.inconsistencyScore * (this.securityConfig.weights.inconsistencyScore || 0.8) +
|
|
1171
|
-
honeypotScore * (this.securityConfig.weights.honeypotScore || 0)
|
|
1294
|
+
honeypotScore * (this.securityConfig.weights.honeypotScore || 0) +
|
|
1295
|
+
requestPatternScore * (this.securityConfig.weights.requestPatternScore || 0);
|
|
1172
1296
|
|
|
1173
1297
|
if (score >= this.securityConfig.thresholds.high) return `suspicious_high:${clientIp}`;
|
|
1174
1298
|
if (score >= this.securityConfig.thresholds.low) return `suspicious_medium:${clientIp}`; // Use medium for any suspicion
|
|
@@ -1252,6 +1376,7 @@ export const __internal = {
|
|
|
1252
1376
|
FingerprintBuilder, // Export for testing
|
|
1253
1377
|
calculateTarget,
|
|
1254
1378
|
FingerprintEngine, // Expose for advanced testing
|
|
1379
|
+
getRequestPatternScore, // Expose for testing
|
|
1255
1380
|
};
|
|
1256
1381
|
|
|
1257
1382
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -1286,9 +1411,11 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
|
1286
1411
|
// The "fitness" function evaluates the quality of a set of thresholds.
|
|
1287
1412
|
// A lower score is better.
|
|
1288
1413
|
const fitnessFunction = (solution) => {
|
|
1289
|
-
const [low, medium, high] = solution;
|
|
1414
|
+
const [low, medium, high, velocityThreshold, burstThreshold, scrapeThreshold] = solution;
|
|
1290
1415
|
// Constraints: thresholds must be ordered and within a reasonable range.
|
|
1291
|
-
if (low >= medium || medium >= high || low
|
|
1416
|
+
if (low >= medium || medium >= high || low < 10 || high > 90) return Infinity;
|
|
1417
|
+
// Constraints for pattern thresholds
|
|
1418
|
+
if (velocityThreshold < 50 || velocityThreshold > burstThreshold || burstThreshold > scrapeThreshold) return Infinity;
|
|
1292
1419
|
|
|
1293
1420
|
let falsePositives = 0; // Humans challenged unnecessarily.
|
|
1294
1421
|
let falseNegatives = 0; // Undetected bots.
|
|
@@ -1305,12 +1432,21 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
|
1305
1432
|
};
|
|
1306
1433
|
|
|
1307
1434
|
// Functions for the genetic algorithm.
|
|
1308
|
-
const createIndividual = () => [
|
|
1309
|
-
|
|
1435
|
+
const createIndividual = () => [
|
|
1436
|
+
10 + Math.random() * 20, // low
|
|
1437
|
+
30 + Math.random() * 30, // medium
|
|
1438
|
+
60 + Math.random() * 30, // high
|
|
1439
|
+
100 + Math.random() * 150, // velocityThreshold (100-250ms)
|
|
1440
|
+
300 + Math.random() * 400, // burstThreshold (300-700ms)
|
|
1441
|
+
800 + Math.random() * 700, // scrapeThreshold (800-1500ms)
|
|
1442
|
+
];
|
|
1443
|
+
const crossover = (p1, p2) => p1.map((val, i) => (val + p2[i]) / 2);
|
|
1310
1444
|
const mutate = (s) => {
|
|
1311
1445
|
const n = [...s];
|
|
1312
|
-
const i = Math.floor(Math.random() *
|
|
1313
|
-
|
|
1446
|
+
const i = Math.floor(Math.random() * n.length);
|
|
1447
|
+
// Adjust mutation range based on parameter
|
|
1448
|
+
const mutationRange = i < 3 ? 5 : 50;
|
|
1449
|
+
n[i] += (Math.random() - 0.5) * mutationRange;
|
|
1314
1450
|
return n;
|
|
1315
1451
|
};
|
|
1316
1452
|
|
|
@@ -1320,16 +1456,26 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
|
1320
1456
|
populationSize: 40
|
|
1321
1457
|
});
|
|
1322
1458
|
|
|
1323
|
-
const [newLow, newMedium, newHigh] = result.solution;
|
|
1459
|
+
const [newLow, newMedium, newHigh, newVelocity, newBurst, newScrape] = result.solution;
|
|
1324
1460
|
|
|
1325
1461
|
// Update the configuration live.
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1462
|
+
// Ensure thresholds object exists
|
|
1463
|
+
if (!securityConfig.thresholds) securityConfig.thresholds = {};
|
|
1464
|
+
securityConfig.thresholds.low = Math.round(newLow);
|
|
1465
|
+
securityConfig.thresholds.medium = Math.round(newMedium);
|
|
1466
|
+
securityConfig.thresholds.high = Math.round(newHigh);
|
|
1467
|
+
|
|
1468
|
+
// Update pattern detection parameters
|
|
1469
|
+
if (!securityConfig.patterns) securityConfig.patterns = {};
|
|
1470
|
+
securityConfig.patterns.velocityThreshold = Math.round(newVelocity);
|
|
1471
|
+
securityConfig.patterns.burstThreshold = Math.round(newBurst);
|
|
1472
|
+
securityConfig.patterns.scrapeThreshold = Math.round(newScrape);
|
|
1473
|
+
// Weights could also be optimized, but let's keep it to thresholds for now for simplicity.
|
|
1331
1474
|
|
|
1332
1475
|
console.log("[AutoTuning] Nouveaux seuils optimisés appliqués :", securityConfig.thresholds);
|
|
1476
|
+
if (securityConfig.patterns) {
|
|
1477
|
+
console.log("[AutoTuning] Nouveaux paramètres de pattern appliqués :", securityConfig.patterns);
|
|
1478
|
+
}
|
|
1333
1479
|
}
|
|
1334
1480
|
|
|
1335
1481
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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",
|