@anonympins/fingerprint 0.3.6 → 0.3.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/CHANGELOG.md +20 -1
- package/README.md +119 -6
- package/package.json +1 -1
- package/src/js/build-client.js +4 -5
- package/src/js/fingerprint.client.js +2 -2
- package/src/js/fingerprint.js +202 -54
- package/src/js/library.js +1 -1
- package/src/js/mongodb-store.js +79 -52
- package/src/js/optimization.worker.js +2 -2
- package/src/js/problem-manager.js +2 -2
- package/src/php/DirectFingerprint.php +145 -80
- package/src/php/FingerprintEngine.php +19 -3
- package/src/php/ProblemManager.php +1 -1
- package/src/php/RequestContext.php +90 -90
- package/src/php/Store/MongoDbStore.php +105 -0
- package/src/php/Store/RedisStore.php +54 -0
- package/src/php/Tests/ChallengeUtilsTest.php +1 -1
- package/src/php/Tests/FingerprintBuilderTest.php +1 -1
- package/src/php/Tests/FingerprintEngineTest.php +4 -4
- package/src/php/Tests/IpReputationTest.php +4 -4
- package/src/php/Tests/Ja3AnomalyDetectorTest.php +1 -1
- package/src/php/Tests/MetricsTest.php +46 -0
- package/src/php/Tests/PowTest.php +2 -2
- package/src/php/Tests/ProblemManagerTest.php +5 -3
- package/src/php/Tests/RequestUtilsTest.php +1 -1
- package/src/php/Utils/MetricsManager.php +167 -0
- package/src/php/Utils/RequestUtils.php +1142 -1142
- package/src/php/bin/auto-tune.php +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## Version 0.3.7
|
|
2
|
+
|
|
3
|
+
### 📊 Prometheus Metrics & Monitoring (JS/PHP)
|
|
4
|
+
- **Prometheus metrics export support**:
|
|
5
|
+
- Implemented a metrics generator using the standard Prometheus format (`text/plain`).
|
|
6
|
+
- Exported active configuration indicators: weight (`fingerprint_security_weight`) and thresholds (`fingerprint_security_threshold`).
|
|
7
|
+
- Real-time export of Auto-Tuner performance metrics: false positive rate (`fingerprint_autotuning_false_positive_rate`) and false negative rate (`fingerprint_autotuning_false_negative_rate`).
|
|
8
|
+
- Secured access via a customizable authorization callback (`metricsAuthorizationCallback`) supporting blocking and secure redirects.
|
|
9
|
+
|
|
10
|
+
### ⚙️ Background Pareto-Optimal TTL Tuning (JS)
|
|
11
|
+
- **Non-blocking asynchronous optimization**:
|
|
12
|
+
- Added `runBackgroundTtlOptimization`, which runs periodically in the background without blocking the event loop.
|
|
13
|
+
- Utilized a multi-objective genetic algorithm to dynamically calculate a Pareto front of optimal TTLs based on suspicion scores.
|
|
14
|
+
- Implemented instant linear interpolation from the optimization cache for efficient ticket TTL assignment (`determineOptimalTicketTtl`).
|
|
15
|
+
|
|
16
|
+
### 🚀 Maintenance
|
|
17
|
+
- **Repository cleanup**:
|
|
18
|
+
- Removed obsolete temporary specification files (`enhancements.md`).
|
|
19
|
+
|
|
1
20
|
## Version 0.3.6
|
|
2
21
|
|
|
3
22
|
### 🛡️ Sybil Protection & Auto-Tuner Hardening (JS/PHP)
|
|
@@ -114,7 +133,7 @@ This release marks a major expansion of the library, introducing a full-featured
|
|
|
114
133
|
|
|
115
134
|
### ✨ New Features
|
|
116
135
|
|
|
117
|
-
* **Full PHP Support**: The library is now available for PHP
|
|
136
|
+
* **Full PHP Support**: The library is now available for PHP 8.0+ with a feature set equivalent to the Node.js version.
|
|
118
137
|
* **Direct Integration**: A `DirectFingerprint` class allows for easy integration into any PHP application, including legacy codebases, by interacting directly with PHP's superglobals.
|
|
119
138
|
* **PSR-15 Middleware**: A `FingerprintMiddleware` is provided for modern, framework-agnostic integration with applications that follow PSR-7, PSR-15, and PSR-17 standards (e.g., Slim, Laminas).
|
|
120
139
|
* **Pluggable Datastores**: The PHP version supports the same pluggable store architecture, allowing state to be persisted in Redis, databases, or other external systems.
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ For API clients, the challenge is delivered as a `404` JSON response, and the cl
|
|
|
59
59
|
|
|
60
60
|
### Prerequisites
|
|
61
61
|
|
|
62
|
-
* **PHP
|
|
62
|
+
* **PHP 8.0+**
|
|
63
63
|
* The **BCMath** extension (`php-bcmath`) is required. It is included by default in most PHP installations.
|
|
64
64
|
* **Composer** for package management.
|
|
65
65
|
* The **GMP** extension (`php-gmp`) is highly recommended for performance. If not available, the library will fall back to a slower BCMath-based implementation for cryptographic operations.
|
|
@@ -74,7 +74,7 @@ This guide shows the simplest way to integrate the library into any PHP applicat
|
|
|
74
74
|
|
|
75
75
|
### Prerequisites
|
|
76
76
|
|
|
77
|
-
* **PHP
|
|
77
|
+
* **PHP 8.0+**
|
|
78
78
|
* The **BCMath** extension (`php-bcmath`) is required. It is included by default in most PHP installations.
|
|
79
79
|
* **Composer** for package management.
|
|
80
80
|
* The **GMP** extension (`php-gmp`) is highly recommended for performance. If not available, the library will fall back to a slower BCMath-based implementation for cryptographic operations.
|
|
@@ -375,6 +375,68 @@ This architecture is common in high-performance environments and offers great fl
|
|
|
375
375
|
|
|
376
376
|
---
|
|
377
377
|
|
|
378
|
+
### Exposing Prometheus Metrics (Optional & Secure)
|
|
379
|
+
|
|
380
|
+
You can expose a Prometheus-compatible endpoint.
|
|
381
|
+
|
|
382
|
+
**It is CRUCIAL to secure this endpoint.** By default, the endpoint will be accessible to anyone. You can provide a `metricsAuthorizationCallback` in your `securityConfig` to implement custom authorization rules.
|
|
383
|
+
|
|
384
|
+
The `metricsAuthorizationCallback` receives a `RequestContext` object and should return:
|
|
385
|
+
- `true`: Access granted.
|
|
386
|
+
- `false`: Access denied (will result in a 403 Forbidden).
|
|
387
|
+
- An array like `['action' => 'redirect', 'path' => '/login', 'status' => 302]`: To redirect the client (e.g., to an SSO login page).
|
|
388
|
+
- An array like `['action' => 'block', 'status' => 401, 'body' => 'Unauthorized']`: To block access with a specific status and message.
|
|
389
|
+
|
|
390
|
+
```php
|
|
391
|
+
<?php
|
|
392
|
+
|
|
393
|
+
// Example of a custom authorization callback
|
|
394
|
+
$securityConfig['metricsAuthorizationCallback'] = function (\Anonympins\Fingerprint\RequestContext $context) {
|
|
395
|
+
// Implement your custom authorization logic here.
|
|
396
|
+
// For example, check an API key in headers, or a session.
|
|
397
|
+
|
|
398
|
+
// Example 1: Allow only from specific IP
|
|
399
|
+
if ($context->clientIp === '127.0.0.1' || $context->clientIp === '::1') {
|
|
400
|
+
return true; // Authorized
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Example 2: Require a specific header (e.g., an API key)
|
|
404
|
+
if (($context->headers['X-Metrics-API-Key'] ?? '') === 'your-secret-api-key') {
|
|
405
|
+
return true; // Authorized
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Example 3: Redirect to an SSO login page if not authenticated
|
|
409
|
+
// (Assuming you have a session or token check here)
|
|
410
|
+
// if (!isAuthenticated($context)) {
|
|
411
|
+
// return ['action' => 'redirect', 'path' => 'https://sso.yourdomain.com/login?redirect_to=/metrics', 'status' => 302];
|
|
412
|
+
// }
|
|
413
|
+
|
|
414
|
+
return false; // Deny by default if no rule matches
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Re-create the protector with the updated securityConfig
|
|
418
|
+
$protector = new DirectFingerprint($securityConfig);
|
|
419
|
+
|
|
420
|
+
// Handle the /metrics route
|
|
421
|
+
if (isset($_GET['metrics'])) { // Or check a specific path like $_SERVER['REQUEST_URI'] === '/metrics'
|
|
422
|
+
// Create a RequestContext for the metrics request
|
|
423
|
+
$metricsContext = new \Anonympins\Fingerprint\RequestContext(
|
|
424
|
+
$_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
|
|
425
|
+
parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/',
|
|
426
|
+
function_exists('getallheaders') ? getallheaders() : [],
|
|
427
|
+
$_GET,
|
|
428
|
+
$_POST ?: json_decode(file_get_contents('php://input'), true),
|
|
429
|
+
$_COOKIE,
|
|
430
|
+
$_SERVER['SERVER_PROTOCOL'] ?? '1.1'
|
|
431
|
+
);
|
|
432
|
+
$protector->handleMetricsRequest($metricsContext);
|
|
433
|
+
// handleMetricsRequest will call exit() upon completion
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
?>
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
|
|
378
440
|
<a id="nodejs-quickstart"></a>
|
|
379
441
|
## NodeJS Quickstart
|
|
380
442
|
|
|
@@ -409,9 +471,10 @@ Available profiles:
|
|
|
409
471
|
|
|
410
472
|
```javascript
|
|
411
473
|
import express from 'express';
|
|
474
|
+
import http from 'http'; // Required for Node.js RequestContext
|
|
412
475
|
import bodyParser from 'body-parser';
|
|
413
476
|
import cookieParser from 'cookie-parser';
|
|
414
|
-
import { powMiddleware, createSecurityProfile } from '@anonympins/fingerprint'; // Adjust the path
|
|
477
|
+
import { powMiddleware, createSecurityProfile, handleMetricsRequest } from '@anonympins/fingerprint'; // Adjust the path
|
|
415
478
|
|
|
416
479
|
const app = express();
|
|
417
480
|
app.use(cookieParser());
|
|
@@ -440,6 +503,8 @@ const securityConfig = createSecurityProfile('api', {
|
|
|
440
503
|
minDataPoints: 200,
|
|
441
504
|
savePath: './security-config.optimized.json' // (Optional) Save the best config found.
|
|
442
505
|
},
|
|
506
|
+
// (Optional) Custom authorization callback for the /metrics endpoint
|
|
507
|
+
metricsAuthorizationCallback: async (context) => { /* ... your auth logic ... */ return true; },
|
|
443
508
|
});
|
|
444
509
|
|
|
445
510
|
// Create an instance of the middleware with your security configuration.
|
|
@@ -449,6 +514,16 @@ const powMiddlewareInstance = powMiddleware(securityConfig);
|
|
|
449
514
|
// to correctly retrieve the client's IP.
|
|
450
515
|
app.set('trust proxy', 1);
|
|
451
516
|
|
|
517
|
+
// --- Metrics Endpoint (Node.js) ---
|
|
518
|
+
// This route should typically be placed BEFORE the general powMiddleware to avoid unnecessary processing
|
|
519
|
+
// for metrics requests, especially if the metricsAuthorizationCallback is simple.
|
|
520
|
+
app.get('/metrics', async (req, res) => {
|
|
521
|
+
// The handleMetricsRequest function (conceptually part of the library)
|
|
522
|
+
// will handle authorization and serving of metrics.
|
|
523
|
+
await handleMetricsRequest(req, res, securityConfig);
|
|
524
|
+
});
|
|
525
|
+
// --- End Metrics Endpoint ---
|
|
526
|
+
|
|
452
527
|
// Apply the protection middleware to all routes or to specific ones.
|
|
453
528
|
app.use(powMiddlewareInstance);
|
|
454
529
|
|
|
@@ -456,6 +531,45 @@ app.get('/', (req, res) => {
|
|
|
456
531
|
res.send('Welcome to the protected page!');
|
|
457
532
|
});
|
|
458
533
|
|
|
534
|
+
// The powMiddleware itself (or the FingerprintEngine it wraps) should internally
|
|
535
|
+
// call MetricsManager to record events like requests passed, blocked, challenged,
|
|
536
|
+
// and suspicion scores. This example shows how you might observe the results
|
|
537
|
+
// if the middleware attaches them to `req.fingerprint`.
|
|
538
|
+
app.use((req, res, next) => {
|
|
539
|
+
// Assuming powMiddleware attaches a 'fingerprint' object to the request
|
|
540
|
+
if (req.fingerprint) {
|
|
541
|
+
const { score, action, intendedAction } = req.fingerprint;
|
|
542
|
+
|
|
543
|
+
// Record suspicion score
|
|
544
|
+
if (score !== undefined) {
|
|
545
|
+
// MetricsManager.observeValue('suspicion_score', score, { action: action || 'processed' });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Record request status
|
|
549
|
+
if (action) {
|
|
550
|
+
// if (action === 'block') {
|
|
551
|
+
// MetricsManager.incrementCounter('requests_total', { status: 'blocked' });
|
|
552
|
+
// } else if (action === 'challenge') {
|
|
553
|
+
// MetricsManager.incrementCounter('requests_total', { status: 'challenged' });
|
|
554
|
+
// } else if (action === 'next') {
|
|
555
|
+
// MetricsManager.incrementCounter('requests_total', { status: 'passed' });
|
|
556
|
+
// }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Record dry run actions
|
|
560
|
+
if (securityConfig.dryRun && intendedAction) {
|
|
561
|
+
// if (intendedAction === 'block') {
|
|
562
|
+
// MetricsManager.incrementCounter('requests_total', { status: 'dry_run_block' });
|
|
563
|
+
// } else if (intendedAction === 'challenge') {
|
|
564
|
+
// MetricsManager.incrementCounter('requests_total', { status: 'dry_run_challenge' });
|
|
565
|
+
// }
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
next();
|
|
569
|
+
});
|
|
570
|
+
// --- End Conceptual Metrics Collection ---
|
|
571
|
+
|
|
572
|
+
|
|
459
573
|
// Example of accessing the suspicion score in a subsequent middleware or route.
|
|
460
574
|
// The `fingerprint` object is attached to the request object by the middleware.
|
|
461
575
|
app.use((req, res, next) => {
|
|
@@ -471,7 +585,8 @@ app.listen(3000, () => console.log('Server started on port 3000'));
|
|
|
471
585
|
If you prefer to define the entire configuration manually instead of using a profile, you can create a `securityConfig` object with all the parameters. All parameters are optional, but it is highly recommended to review and adjust them for your specific needs. The engine will warn you about any unknown keys in this configuration, helping you catch typos.
|
|
472
586
|
|
|
473
587
|
```javascript
|
|
474
|
-
import {
|
|
588
|
+
import {
|
|
589
|
+
powMiddleware, default_whitelist, default_analyzers } from '@anonympins/fingerprint';
|
|
475
590
|
|
|
476
591
|
const app = express();
|
|
477
592
|
app.use(cookieParser());
|
|
@@ -620,7 +735,6 @@ const securityConfig = {
|
|
|
620
735
|
dryRun: false,
|
|
621
736
|
};
|
|
622
737
|
|
|
623
|
-
|
|
624
738
|
// Create an instance of the middleware with your security configuration.
|
|
625
739
|
const powMiddlewareInstance = powMiddleware(securityConfig);
|
|
626
740
|
```
|
|
@@ -648,7 +762,6 @@ These parameters form the basis of the statistical request pattern analysis:
|
|
|
648
762
|
* `regularityThreshold`: (Default: 50ms) The standard deviation in milliseconds below which the request timing is considered "too regular" and robotic.
|
|
649
763
|
* `benfordThreshold`: (Default: 0.15) The deviation score from Benford's Law above which the timing distribution is considered "unnatural".
|
|
650
764
|
* `patternWeight`: (Default: 80) A strong, one-time penalty applied to the suspicion score if either a regularity or Benford's Law anomaly is detected.
|
|
651
|
-
|
|
652
765
|
* `benfordMinSamples`: (Default: 15) The minimum number of request timings to collect before performing a Benford's Law test.
|
|
653
766
|
* `benfordWeight`: (Default: 50) The weight applied to the suspicion score if the distribution of timings significantly deviates from Benford's Law.
|
|
654
767
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Advanced anti-bot library for Node.js using multi-layer fingerprinting (JA3, client-side, headers), behavioral analysis, and adaptive Proof-of-Work (PoW) challenges to mitigate scraping, scalping, and automated threats.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
package/src/js/build-client.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import {promises as fs} from 'node:fs';
|
|
2
|
+
import {dirname, join} from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {exec} from 'node:child_process';
|
|
5
5
|
import JavaScriptObfuscator from 'javascript-obfuscator';
|
|
6
|
-
import { minify } from 'terser';
|
|
7
6
|
|
|
8
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
8
|
const __dirname = dirname(__filename);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {cyrb53 as jsCyrb53, FingerprintBuilder} from './fingerprint.builder.js';
|
|
2
|
+
import {solveChallenge} from './pow.solver.js';
|
|
3
3
|
|
|
4
4
|
// Variable pour stocker la fonction de hachage active.
|
|
5
5
|
// Par défaut, c'est l'implémentation JavaScript.
|
package/src/js/fingerprint.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {BlockList, isIPv4, isIPv6} from "node:net";
|
|
3
3
|
import * as dns from "node:dns/promises";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
4
|
+
import {getProblemManager, problemManager} from "./problem-manager.js";
|
|
5
|
+
import {Optimization} from "./library.js";
|
|
6
|
+
import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
|
|
7
|
+
import {readFileSync} from "node:fs";
|
|
8
|
+
import {fileURLToPath} from "node:url";
|
|
9
|
+
import {dirname, join} from "node:path";
|
|
10
|
+
|
|
10
11
|
export { createRedisStore } from "./redis-store.js";
|
|
11
12
|
export { createMongoDbStore } from "./mongodb-store.js";
|
|
12
13
|
|
|
@@ -3456,68 +3457,123 @@ const staticExtensions = new RegExp(
|
|
|
3456
3457
|
const isStaticResource = (path) => staticExtensions.test(path);
|
|
3457
3458
|
|
|
3458
3459
|
|
|
3460
|
+
/** @type {Map<number, number>} Cache des TTL optimisés par score de suspicion (clés de 0 à 100 par pas de 10) */
|
|
3461
|
+
let optimizedTtlCache = new Map();
|
|
3462
|
+
|
|
3459
3463
|
/**
|
|
3460
|
-
*
|
|
3461
|
-
*
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
function determineOptimalTicketTtl(suspicionScore) {
|
|
3465
|
-
// Définir les bornes pour la durée de vie du ticket (5 minutes à 24 heures)
|
|
3464
|
+
* Exécute l'optimisation des TTL en tâche de fond de manière asynchrone et non-bloquante.
|
|
3465
|
+
* Utilise l'algorithme génétique multi-objectifs de Pareto pour trouver des solutions stables.
|
|
3466
|
+
*/
|
|
3467
|
+
export async function runBackgroundTtlOptimization() {
|
|
3466
3468
|
const MIN_TTL = 300000;
|
|
3467
3469
|
const MAX_TTL = 86400000;
|
|
3470
|
+
const tempCache = new Map();
|
|
3471
|
+
const keyScores = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
3472
|
+
|
|
3473
|
+
for (const suspicionScore of keyScores) {
|
|
3474
|
+
// Rend la main à la boucle d'événements Node.js à chaque itération pour ne pas bloquer les requêtes web actives
|
|
3475
|
+
await new Promise(resolve => {
|
|
3476
|
+
if (typeof setImmediate === 'function') {
|
|
3477
|
+
setImmediate(resolve);
|
|
3478
|
+
} else {
|
|
3479
|
+
setTimeout(resolve, 0);
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3468
3482
|
|
|
3469
|
-
|
|
3470
|
-
|
|
3483
|
+
const solverFunction = () => {
|
|
3484
|
+
const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
|
|
3485
|
+
const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
|
|
3486
|
+
const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
|
|
3487
|
+
const mutate = (ttl) => {
|
|
3488
|
+
const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1;
|
|
3489
|
+
return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
|
|
3490
|
+
};
|
|
3471
3491
|
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3492
|
+
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
3493
|
+
createIndividual,
|
|
3494
|
+
fitnessFunction,
|
|
3495
|
+
crossover,
|
|
3496
|
+
mutate,
|
|
3497
|
+
{
|
|
3498
|
+
generations: 40,
|
|
3499
|
+
populationSize: 30,
|
|
3500
|
+
}
|
|
3501
|
+
);
|
|
3479
3502
|
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
fitnessFunction,
|
|
3483
|
-
crossover,
|
|
3484
|
-
mutate,
|
|
3485
|
-
{
|
|
3486
|
-
generations: 40,
|
|
3487
|
-
populationSize: 30,
|
|
3503
|
+
if (!paretoFront || paretoFront.length === 0) {
|
|
3504
|
+
return { solution: null, fitness: Infinity };
|
|
3488
3505
|
}
|
|
3489
|
-
);
|
|
3490
3506
|
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3507
|
+
let bestSolutionInFront;
|
|
3508
|
+
if (suspicionScore < 50) {
|
|
3509
|
+
bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
|
|
3510
|
+
} else {
|
|
3511
|
+
bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
|
|
3512
|
+
}
|
|
3513
|
+
return { solution: bestSolutionInFront, fitness: 0 };
|
|
3514
|
+
};
|
|
3497
3515
|
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
let bestSolutionInFront;
|
|
3502
|
-
if (suspicionScore < 50) {
|
|
3503
|
-
bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
|
|
3516
|
+
const { bestResult } = Optimization.runMultiple(solverFunction, 20);
|
|
3517
|
+
if (bestResult && bestResult.solution && bestResult.solution !== Infinity) {
|
|
3518
|
+
tempCache.set(suspicionScore, Math.round(bestResult.solution));
|
|
3504
3519
|
} else {
|
|
3505
|
-
|
|
3520
|
+
tempCache.set(suspicionScore, Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL));
|
|
3506
3521
|
}
|
|
3507
|
-
|
|
3508
|
-
};
|
|
3522
|
+
}
|
|
3509
3523
|
|
|
3510
|
-
|
|
3511
|
-
|
|
3524
|
+
optimizedTtlCache = tempCache;
|
|
3525
|
+
}
|
|
3512
3526
|
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3527
|
+
// Lancement de l'optimisation initiale immédiate en arrière-plan
|
|
3528
|
+
runBackgroundTtlOptimization().catch(err => {
|
|
3529
|
+
console.error('[Fingerprint] Error in background TTL optimization:', err);
|
|
3530
|
+
});
|
|
3531
|
+
|
|
3532
|
+
// Planification périodique toutes les 30 minutes sans bloquer la fermeture du processus Node.js (via unref)
|
|
3533
|
+
const ttlInterval = setInterval(() => {
|
|
3534
|
+
runBackgroundTtlOptimization().catch(err => {
|
|
3535
|
+
console.error('[Fingerprint] Error in background TTL optimization:', err);
|
|
3536
|
+
});
|
|
3537
|
+
}, 1800000);
|
|
3538
|
+
if (ttlInterval && typeof ttlInterval.unref === 'function') {
|
|
3539
|
+
ttlInterval.unref();
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
/**
|
|
3543
|
+
* Détermine le TTL optimal pour un ticket.
|
|
3544
|
+
* Utilise les valeurs pré-calculées de la tâche d'optimisation en arrière-plan et effectue
|
|
3545
|
+
* une interpolation linéaire instantanée pour le score requis.
|
|
3546
|
+
*
|
|
3547
|
+
* @param {number} suspicionScore - Le score de suspicion de la requête.
|
|
3548
|
+
* @returns {number} Le TTL optimal calculé en millisecondes.
|
|
3549
|
+
*/
|
|
3550
|
+
function determineOptimalTicketTtl(suspicionScore) {
|
|
3551
|
+
const MIN_TTL = 300000;
|
|
3552
|
+
const MAX_TTL = 86400000;
|
|
3553
|
+
const score = Math.max(0, Math.min(100, suspicionScore));
|
|
3554
|
+
|
|
3555
|
+
if (!optimizedTtlCache || optimizedTtlCache.size === 0) {
|
|
3556
|
+
// Formule mathématique instantanée de secours si le cache de fond n'est pas encore prêt
|
|
3557
|
+
return Math.round(MAX_TTL - (score / 100) * (MAX_TTL - MIN_TTL));
|
|
3516
3558
|
}
|
|
3517
3559
|
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3560
|
+
const lowerKey = Math.floor(score / 10) * 10;
|
|
3561
|
+
const upperKey = Math.ceil(score / 10) * 10;
|
|
3562
|
+
|
|
3563
|
+
const lowerTtl = optimizedTtlCache.get(lowerKey);
|
|
3564
|
+
const upperTtl = optimizedTtlCache.get(upperKey);
|
|
3565
|
+
|
|
3566
|
+
if (lowerTtl === undefined || upperTtl === undefined) {
|
|
3567
|
+
return Math.round(MAX_TTL - (score / 100) * (MAX_TTL - MIN_TTL));
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
if (lowerKey === upperKey) {
|
|
3571
|
+
return lowerTtl;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
// Interpolation linéaire entre les deux points clés optimisés du front de Pareto
|
|
3575
|
+
const fraction = (score - lowerKey) / (upperKey - lowerKey);
|
|
3576
|
+
return Math.round(lowerTtl + fraction * (upperTtl - lowerTtl));
|
|
3521
3577
|
}
|
|
3522
3578
|
|
|
3523
3579
|
/**
|
|
@@ -3835,6 +3891,7 @@ export const __internal = {
|
|
|
3835
3891
|
FingerprintBuilder, // Export for testing
|
|
3836
3892
|
calculateTarget,
|
|
3837
3893
|
determineOptimalTicketTtl,
|
|
3894
|
+
runBackgroundTtlOptimization,
|
|
3838
3895
|
getRequestPatternScore, // Expose for testing
|
|
3839
3896
|
getBehaviorScore, // Expose for testing
|
|
3840
3897
|
getCrossLayerInconsistency, // Expose for testing
|
|
@@ -3854,6 +3911,7 @@ export const __internal = {
|
|
|
3854
3911
|
getSubnetScore, // Expose for testing
|
|
3855
3912
|
getIpReputationScore, // Expose for testing
|
|
3856
3913
|
updateIpReputationScore, // Expose for testing
|
|
3914
|
+
setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
|
|
3857
3915
|
};
|
|
3858
3916
|
|
|
3859
3917
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -4071,3 +4129,93 @@ export function stopThresholdAutoTuning() {
|
|
|
4071
4129
|
export function getBestTuningSolution() {
|
|
4072
4130
|
return lastBestSolution;
|
|
4073
4131
|
}
|
|
4132
|
+
|
|
4133
|
+
class RequestContext {
|
|
4134
|
+
constructor(ip, path, headers, query, body, cookies, httpVersion) {
|
|
4135
|
+
this.clientIp = ip || '127.0.0.1';
|
|
4136
|
+
this.path = path || '/';
|
|
4137
|
+
this.headers = headers || {};
|
|
4138
|
+
this.query = query || {};
|
|
4139
|
+
this.body = body || null;
|
|
4140
|
+
this.cookies = cookies || {};
|
|
4141
|
+
this.httpVersion = httpVersion || '1.1';
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
const MetricsManager = {
|
|
4146
|
+
getPrometheusMetrics(securityConfig = {}) {
|
|
4147
|
+
let metrics = `# HELP fingerprint_requests_total Total requests processed.\n# TYPE fingerprint_requests_total counter\nfingerprint_requests_total{status="passed"} 1\n`;
|
|
4148
|
+
|
|
4149
|
+
if (securityConfig.weights) {
|
|
4150
|
+
metrics += `\n# HELP fingerprint_security_weight Active weight for each suspicion indicator.\n# TYPE fingerprint_security_weight gauge\n`;
|
|
4151
|
+
for (const [indicator, weight] of Object.entries(securityConfig.weights)) {
|
|
4152
|
+
if (typeof weight === 'number') {
|
|
4153
|
+
metrics += `fingerprint_security_weight{indicator="${indicator}"} ${weight}\n`;
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
if (securityConfig.thresholds) {
|
|
4159
|
+
metrics += `\n# HELP fingerprint_security_threshold Active score threshold for each enforcement action level.\n# TYPE fingerprint_security_threshold gauge\n`;
|
|
4160
|
+
for (const [level, threshold] of Object.entries(securityConfig.thresholds)) {
|
|
4161
|
+
if (typeof threshold === 'number') {
|
|
4162
|
+
metrics += `fingerprint_security_threshold{level="${level}"} ${threshold}\n`;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
// Include auto-tuning objectives metrics if the auto-tuner has run
|
|
4168
|
+
if (lastBestSolution && lastBestSolution.objectives) {
|
|
4169
|
+
metrics += `\n# HELP fingerprint_autotuning_false_positive_rate Current false positive rate calculated by the auto-tuner.\n# TYPE fingerprint_autotuning_false_positive_rate gauge\nfingerprint_autotuning_false_positive_rate ${lastBestSolution.objectives[0]}\n`;
|
|
4170
|
+
metrics += `\n# HELP fingerprint_autotuning_false_negative_rate Current false negative rate calculated by the auto-tuner.\n# TYPE fingerprint_autotuning_false_negative_rate gauge\nfingerprint_autotuning_false_negative_rate ${lastBestSolution.objectives[1]}\n`;
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
return metrics;
|
|
4174
|
+
}
|
|
4175
|
+
};
|
|
4176
|
+
|
|
4177
|
+
/**
|
|
4178
|
+
* Gère une requête vers le point de terminaison /metrics, en appliquant les règles d'autorisation.
|
|
4179
|
+
* Si les métriques sont activées et autorisées, elle renvoie les métriques au format Prometheus.
|
|
4180
|
+
* Sinon, elle gère l'accès non autorisé ou renvoie un 404 si les métriques ne sont pas activées.
|
|
4181
|
+
*
|
|
4182
|
+
* @param {object} req L'objet requête Express.
|
|
4183
|
+
* @param {object} res L'objet réponse Express.
|
|
4184
|
+
* @param {object} securityConfig La configuration de sécurité.
|
|
4185
|
+
*/
|
|
4186
|
+
export async function handleMetricsRequest(req, res, securityConfig) {
|
|
4187
|
+
// 2. Appliquer le callback d'autorisation personnalisé si défini.
|
|
4188
|
+
const authorizationCallback = securityConfig.metricsAuthorizationCallback;
|
|
4189
|
+
if (typeof authorizationCallback === 'function') {
|
|
4190
|
+
const context = new RequestContext(
|
|
4191
|
+
req.ip,
|
|
4192
|
+
req.path,
|
|
4193
|
+
req.headers,
|
|
4194
|
+
req.query,
|
|
4195
|
+
req.body,
|
|
4196
|
+
req.cookies,
|
|
4197
|
+
req.httpVersion
|
|
4198
|
+
);
|
|
4199
|
+
|
|
4200
|
+
const decision = await authorizationCallback(context); // Supposons que le callback peut être asynchrone
|
|
4201
|
+
|
|
4202
|
+
if (typeof decision === 'boolean') {
|
|
4203
|
+
if (!decision) {
|
|
4204
|
+
res.status(403).send('Access to metrics denied.');
|
|
4205
|
+
return;
|
|
4206
|
+
}
|
|
4207
|
+
} else if (typeof decision === 'object' && decision !== null && decision.action) {
|
|
4208
|
+
if (decision.action === 'block') {
|
|
4209
|
+
res.status(decision.status || 403).send(decision.body || 'Access denied.');
|
|
4210
|
+
return;
|
|
4211
|
+
} else if (decision.action === 'redirect') {
|
|
4212
|
+
res.redirect(decision.status || 302, decision.path);
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
// 3. Si autorisé, servir les métriques.
|
|
4219
|
+
res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
4220
|
+
res.send(MetricsManager.getPrometheusMetrics(securityConfig));
|
|
4221
|
+
}
|