@anonympins/fingerprint 0.0.6 → 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 +35 -6
- package/fingerprint.js +150 -23
- package/package.json +1 -1
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
|
|
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).
|
|
@@ -104,7 +104,33 @@ const securityConfig = {
|
|
|
104
104
|
// A request to one of these paths will immediately flag the device as malicious.
|
|
105
105
|
trapUrls: ['/wp-admin', '/.env', '/admin.php', '/phpmyadmin'], // (Optional)
|
|
106
106
|
// Automatically detect common SQL/NoSQL injection and RCE patterns in request values. (Optional, default: true)
|
|
107
|
-
detectInjections: true
|
|
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
|
+
]
|
|
108
134
|
},
|
|
109
135
|
// The logger is required for auto-tuning. It collects data on requests.
|
|
110
136
|
logger: (log) => trafficData.push(log),
|
|
@@ -152,6 +178,8 @@ The main Express middleware. It orchestrates identification, suspicion calculati
|
|
|
152
178
|
#### `configureStore(store)`
|
|
153
179
|
Allows replacing the in-memory store with an external datastore (like Redis) for persistence and scaling.
|
|
154
180
|
|
|
181
|
+
See the Datastore Integration Guide for a complete example of creating a Redis store.
|
|
182
|
+
|
|
155
183
|
```javascript
|
|
156
184
|
import { configureStore } from './fingerprint.js';
|
|
157
185
|
import { createRedisStore } from './redis-store.js'; // Assuming a redis store implementation exists
|
|
@@ -234,22 +262,23 @@ Although not exported for direct public use, understanding its role can be usefu
|
|
|
234
262
|
|
|
235
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.
|
|
236
264
|
|
|
237
|
-
|
|
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.
|
|
238
268
|
|
|
239
269
|
**Workflow:**
|
|
240
270
|
|
|
241
271
|
1. **Instantiate the Engine**: Create an instance with your `securityConfig`.
|
|
242
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.
|
|
243
|
-
3. **Process the Request**: Call `engine.processRequest(requestContext)`.
|
|
273
|
+
3. **Process the Request**: Call `engine.processRequest(requestContext)`. This method is asynchronous.
|
|
244
274
|
4. **Handle the Decision**: The engine returns a decision object (`{ action: 'challenge' | 'redirect' | 'next', ... }`). You are responsible for implementing the corresponding HTTP response.
|
|
245
275
|
|
|
246
276
|
**Example with native Node.js `http` server:**
|
|
247
277
|
|
|
248
278
|
```javascript
|
|
249
279
|
import http from 'http';
|
|
250
|
-
import {
|
|
280
|
+
import { FingerprintEngine } from './fingerprint.js'; // Adjust path
|
|
251
281
|
|
|
252
|
-
const { FingerprintEngine } = __internal;
|
|
253
282
|
const securityConfig = { /* ... your config ... */ };
|
|
254
283
|
const engine = new FingerprintEngine(securityConfig);
|
|
255
284
|
|
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,6 +490,17 @@ 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
|
|
@@ -453,13 +520,19 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
453
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",
|
|
454
521
|
"i"
|
|
455
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
|
+
);
|
|
456
529
|
|
|
457
530
|
const inspect = (obj) => {
|
|
458
531
|
for (const key in obj) {
|
|
459
532
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
460
533
|
const value = obj[key];
|
|
461
534
|
if (typeof value === 'string') {
|
|
462
|
-
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;
|
|
463
536
|
} else if (typeof value === 'object' && value !== null) {
|
|
464
537
|
// For NoSQL, we check the stringified version of the object to find keys like "$gt"
|
|
465
538
|
// This is more accurate when done on the object itself.
|
|
@@ -499,7 +572,9 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
499
572
|
scrapeThreshold = 1000, scrapeWeight = 20, scrapeBurstWeight = 40,
|
|
500
573
|
historySize = 10,
|
|
501
574
|
decayFactor = 0.9,
|
|
502
|
-
inactivityReset = 30000
|
|
575
|
+
inactivityReset = 30000,
|
|
576
|
+
// Nouveau paramètre pour la détection de séquences
|
|
577
|
+
sequenceLength = 3, sequenceWeight = 60
|
|
503
578
|
} = patternConfig;
|
|
504
579
|
|
|
505
580
|
const now = Date.now();
|
|
@@ -543,6 +618,17 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
543
618
|
score += scrapeWeight; // First sign of a potential scraping pattern
|
|
544
619
|
}
|
|
545
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
|
+
}
|
|
546
632
|
}
|
|
547
633
|
|
|
548
634
|
// --- Update history ---
|
|
@@ -691,6 +777,8 @@ async function resolveRequestIdentity(context) {
|
|
|
691
777
|
lastFpHash: currentDeviceHash,
|
|
692
778
|
lastChangeTimestamp: 0,
|
|
693
779
|
rapidChangeCount: 0,
|
|
780
|
+
highScoreCount: 0,
|
|
781
|
+
lastHighScoreTimestamp: 0,
|
|
694
782
|
};
|
|
695
783
|
// The write will happen in getSuspicionVector after all modifications.
|
|
696
784
|
}
|
|
@@ -790,6 +878,11 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
790
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.
|
|
791
879
|
await store.set(`device:${deviceId}`, deviceData);
|
|
792
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
|
+
}
|
|
793
886
|
return { ...behavioral, headerAnomalyScore, inconsistencyScore };
|
|
794
887
|
};
|
|
795
888
|
|
|
@@ -1024,7 +1117,7 @@ const staticExtensions = new RegExp(
|
|
|
1024
1117
|
const isStaticResource = (path) => staticExtensions.test(path);
|
|
1025
1118
|
|
|
1026
1119
|
// --- Middleware Proof-of-Work (Le péage) ---
|
|
1027
|
-
class FingerprintEngine {
|
|
1120
|
+
export class FingerprintEngine {
|
|
1028
1121
|
constructor(securityConfig) {
|
|
1029
1122
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
1030
1123
|
this.securityConfig = securityConfig;
|
|
@@ -1061,6 +1154,9 @@ class FingerprintEngine {
|
|
|
1061
1154
|
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1062
1155
|
}
|
|
1063
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
|
+
|
|
1064
1160
|
// The engine now works with the context directly, no more rawReq dependency here.
|
|
1065
1161
|
const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
1066
1162
|
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
@@ -1075,6 +1171,11 @@ class FingerprintEngine {
|
|
|
1075
1171
|
suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0) +
|
|
1076
1172
|
honeypotScore * (weights.honeypotScore || 0);
|
|
1077
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
|
+
|
|
1078
1179
|
const isBlocked = finalScore >= (thresholds.block || 95);
|
|
1079
1180
|
|
|
1080
1181
|
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
@@ -1119,11 +1220,14 @@ class FingerprintEngine {
|
|
|
1119
1220
|
if (onDeviceCompromised) {
|
|
1120
1221
|
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
|
|
1121
1222
|
}
|
|
1223
|
+
if (logger) {
|
|
1224
|
+
logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
|
|
1225
|
+
}
|
|
1122
1226
|
await store.set(`device:${cookies.device_id}`, deviceData);
|
|
1123
1227
|
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1124
1228
|
}
|
|
1125
1229
|
|
|
1126
|
-
if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
|
|
1230
|
+
if ((isSuspicious || requiresChallengeForNewDevice) && !isTicketValid(clientIp, powCookie)) {
|
|
1127
1231
|
// --- CHALLENGE SOLUTION HANDLING ---
|
|
1128
1232
|
if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
|
|
1129
1233
|
let isValid = false,
|
|
@@ -1219,7 +1323,7 @@ class FingerprintEngine {
|
|
|
1219
1323
|
}
|
|
1220
1324
|
|
|
1221
1325
|
// UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
|
|
1222
|
-
if (isSuspicious) { // Couvre à la fois low et medium
|
|
1326
|
+
if (isSuspicious || requiresChallengeForNewDevice) { // Couvre à la fois low et medium
|
|
1223
1327
|
// Generate some trap URLs to embed in the challenge page.
|
|
1224
1328
|
// These links are visually hidden but present in the DOM to trap bots.
|
|
1225
1329
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
@@ -1328,6 +1432,8 @@ export const powMiddleware = (securityConfig) => {
|
|
|
1328
1432
|
isStatic: isStaticResource(req.path),
|
|
1329
1433
|
// Add the newly required properties for full decoupling
|
|
1330
1434
|
rawHeaders: req.rawHeaders,
|
|
1435
|
+
// Pass the raw request object for advanced inspection (e.g., JA3)
|
|
1436
|
+
rawReq: req,
|
|
1331
1437
|
httpVersion: req.httpVersion,
|
|
1332
1438
|
};
|
|
1333
1439
|
|
|
@@ -1375,7 +1481,6 @@ export const __internal = {
|
|
|
1375
1481
|
cyrb53, // Export for testing
|
|
1376
1482
|
FingerprintBuilder, // Export for testing
|
|
1377
1483
|
calculateTarget,
|
|
1378
|
-
FingerprintEngine, // Expose for advanced testing
|
|
1379
1484
|
getRequestPatternScore, // Expose for testing
|
|
1380
1485
|
};
|
|
1381
1486
|
|
|
@@ -1397,38 +1502,60 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
|
1397
1502
|
}
|
|
1398
1503
|
console.log(`[AutoTuning] Démarrage du cycle d'optimisation avec ${trafficData.length} points de données.`);
|
|
1399
1504
|
|
|
1400
|
-
//
|
|
1401
|
-
// and "humans" (those who passed the challenge or never received one).
|
|
1505
|
+
// Classify historical requests with a confidence weight.
|
|
1402
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
|
+
|
|
1403
1509
|
const historicalRequests = trafficData.map(log => {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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;
|
|
1407
1537
|
}
|
|
1408
|
-
|
|
1409
|
-
});
|
|
1538
|
+
}).filter(Boolean); // Remove null entries
|
|
1410
1539
|
|
|
1411
1540
|
// The "fitness" function evaluates the quality of a set of thresholds.
|
|
1412
1541
|
// A lower score is better.
|
|
1413
1542
|
const fitnessFunction = (solution) => {
|
|
1414
1543
|
const [low, medium, high, velocityThreshold, burstThreshold, scrapeThreshold] = solution;
|
|
1415
|
-
// Constraints: thresholds must be ordered and within a reasonable range.
|
|
1416
1544
|
if (low >= medium || medium >= high || low < 10 || high > 90) return Infinity;
|
|
1417
|
-
// Constraints for pattern thresholds
|
|
1418
1545
|
if (velocityThreshold < 50 || velocityThreshold > burstThreshold || burstThreshold > scrapeThreshold) return Infinity;
|
|
1419
1546
|
|
|
1420
|
-
let
|
|
1421
|
-
let
|
|
1547
|
+
let weightedFalsePositives = 0; // Humans challenged unnecessarily.
|
|
1548
|
+
let weightedFalseNegatives = 0; // Undetected bots.
|
|
1422
1549
|
|
|
1423
1550
|
for (const req of historicalRequests) {
|
|
1424
|
-
if (req.
|
|
1425
|
-
if (req.score < low)
|
|
1426
|
-
} else { //
|
|
1427
|
-
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;
|
|
1428
1555
|
}
|
|
1429
1556
|
}
|
|
1430
|
-
//
|
|
1431
|
-
return
|
|
1557
|
+
// The penalty for false negatives is implicitly higher due to the higher confidence scores of bot signals.
|
|
1558
|
+
return weightedFalsePositives + weightedFalseNegatives;
|
|
1432
1559
|
};
|
|
1433
1560
|
|
|
1434
1561
|
// Functions for the genetic algorithm.
|
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",
|