@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 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 7.4+ with a feature set equivalent to the Node.js version.
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 7.4+**
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 7.4+**
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 { powMiddleware, default_whitelist, default_analyzers } from '@anonympins/fingerprint';
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.6",
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",
@@ -1,9 +1,8 @@
1
- import { promises as fs } from 'node:fs';
2
- import { join, dirname } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { exec } from 'node:child_process';
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 { cyrb53 as jsCyrb53, FingerprintBuilder } from './fingerprint.builder.js';
2
- import { solveChallenge } from './pow.solver.js';
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.
@@ -1,12 +1,13 @@
1
1
  import crypto from "node:crypto";
2
- import { BlockList, isIPv4, isIPv6 } from "node:net";
2
+ import {BlockList, isIPv4, isIPv6} from "node:net";
3
3
  import * as dns from "node:dns/promises";
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";
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
- * Détermine le TTL optimal pour un ticket en utilisant un algorithme génétique multi-objectifs.
3461
- * @param {number} suspicionScore - Le score de suspicion de la requête.
3462
- * @returns {number} Le TTL optimal calculé en millisecondes.
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
- const solverFunction = () => {
3470
- const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
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
- // Un "individu" est simplement une valeur de TTL en millisecondes.
3473
- const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
3474
- const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
3475
- const mutate = (ttl) => {
3476
- const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1; // Mutation de +/- 10% max
3477
- return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
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
- const paretoFront = Optimization.geneticAlgorithmMultiObjective(
3481
- createIndividual,
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
- // Pour runMultiple, on doit retourner un objet avec une propriété "fitness" ou "energy".
3492
- // Pour un front de Pareto, il n'y a pas de score unique. On choisit la meilleure solution
3493
- // en fonction du score de suspicion et on lui assigne un score de 0 pour que runMultiple la sélectionne.
3494
- if (!paretoFront || paretoFront.length === 0) {
3495
- return { solution: null, fitness: Infinity };
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
- // Stratégie de sélection :
3499
- // Pour un score faible (< 50), on privilégie la solution avec le plus grand TTL (minimise la friction).
3500
- // Pour un score élevé (>= 50), on privilégie la solution avec le plus petit TTL (minimise le risque).
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
- bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
3520
+ tempCache.set(suspicionScore, Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL));
3506
3521
  }
3507
- return { solution: bestSolutionInFront, fitness: 0 }; // fitness=0 car on a déjà la meilleure solution du cycle.
3508
- };
3522
+ }
3509
3523
 
3510
- // On exécute le solveur 20 fois pour trouver une solution plus stable et robuste.
3511
- const { bestResult } = Optimization.runMultiple(solverFunction, 20);
3524
+ optimizedTtlCache = tempCache;
3525
+ }
3512
3526
 
3513
- if (!bestResult || !bestResult.solution || bestResult.solution === Infinity) {
3514
- // Fallback : si l'algo ne retourne rien, on applique une règle simple et sûre.
3515
- return Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL);
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
- // runMultiple choisit le meilleur résultat sur la base du score (ici, 0).
3519
- // La "meilleure" solution dépendra du cycle qui a trouvé le meilleur compromis.
3520
- return Math.round(bestResult.solution);
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
+ }
package/src/js/library.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  */
13
13
 
14
- import { Worker } from "node:worker_threads";
14
+ import {Worker} from "node:worker_threads";
15
15
  import os from "node:os";
16
16
  import crypto from "node:crypto";
17
17