@anonympins/fingerprint 0.6.1 → 0.6.4

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.
@@ -6,7 +6,7 @@ import {getProblemManager, problemManager} from "./problem-manager.js";
6
6
  import {Optimization} from "./library.js";
7
7
  import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
8
8
  import {DynamicWasmGenerator} from "./dynamic-wasm.js";
9
- import {readFileSync, existsSync} from "node:fs";
9
+ import {writeFileSync, readFileSync, existsSync} from "node:fs";
10
10
  import {fileURLToPath} from "node:url";
11
11
  import {dirname, join, resolve} from "node:path";
12
12
  import {
@@ -564,6 +564,7 @@ const securityProfiles = {
564
564
  },
565
565
  allowCrossNetworkRoaming: true, // Profil balancé : tolérant par défaut
566
566
  wasm: true,
567
+ filterWhitelist: 85.0, // Stratégie d'inspection modérée pour les IP/chemins en liste blanche
567
568
  useAsymmetricTickets: true,
568
569
  },
569
570
  /**
@@ -603,6 +604,7 @@ const securityProfiles = {
603
604
  },
604
605
  challengeNewDevices: true, // Challenge all new devices
605
606
  allowCrossNetworkRoaming: false, // Strict : interdiction de changer complètement de réseau sans re-challenge
607
+ filterWhitelist: true, // Tout comportement d'attaque certain bypass immédiatement la liste blanche
606
608
  wasm: true,
607
609
  useAsymmetricTickets: true,
608
610
  },
@@ -643,6 +645,7 @@ const securityProfiles = {
643
645
  },
644
646
  isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json'),
645
647
  allowCrossNetworkRoaming: false, // Les API ne doivent pas subir de roaming inter-IP suspect
648
+ filterWhitelist: 75.0, // Seuil bas pour parer au vol de clés/tokens API légitimes
646
649
  wasm: true,
647
650
  useAsymmetricTickets: true,
648
651
  }
@@ -663,13 +666,17 @@ const securityProfiles = {
663
666
  crossLayerInconsistencyScore: 0.4,
664
667
  timeInconsistencyScore: 0.8,
665
668
  tlsSpoofingScore: 0.6, // Moins critique pour les blogs
666
- subnetScore: 0.2,
669
+ botScore: 0.8,
670
+ cookieDroppingScore: 0.7, // Moins critique, mais toujours un signal
671
+ threatIntelScore: 0.3, // Lower priority for a blog
672
+ clientHintsInconsistencyScore: 0.5,
673
+ clickVarianceScore: 0.5, // Moderate weight for click variance
674
+ subnetScore: 0.4, // Utile contre le spam de commentaires coordonné
667
675
  ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
668
676
  botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
669
677
  tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
670
678
  quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
671
679
  renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
672
- threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
673
680
  },
674
681
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
675
682
  patterns: {
@@ -686,6 +693,7 @@ const securityProfiles = {
686
693
  },
687
694
  allowCrossNetworkRoaming: true,
688
695
  wasm: true,
696
+ filterWhitelist: 90.0, // Très tolérant, n'inspecte que si le score est presque au blocage
689
697
  useAsymmetricTickets: true,
690
698
  },
691
699
  /**
@@ -705,13 +713,17 @@ const securityProfiles = {
705
713
  crossLayerInconsistencyScore: 0.7,
706
714
  timeInconsistencyScore: 0.9,
707
715
  tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
708
- subnetScore: 0.5,
716
+ botScore: 1.0,
717
+ cookieDroppingScore: 1.0, // Crucial pour la détection de bots e-commerce
718
+ threatIntelScore: 0.8, // Very important for e-commerce (scalping proxies)
719
+ clientHintsInconsistencyScore: 0.9, // Very important for e-commerce
720
+ clickVarianceScore: 0.8, // Very high weight for click variance
721
+ subnetScore: 0.9, // Crucial contre les attaques de scalping distribuées
709
722
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
710
723
  botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
711
724
  tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
712
725
  quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
713
726
  renderingAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie de rendu,threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
714
- threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
715
727
  },
716
728
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
717
729
  patterns: {
@@ -729,6 +741,7 @@ const securityProfiles = {
729
741
  challengeNewDevices: true, // New devices are suspicious in e-commerce
730
742
  isApiRequest: (req) => req.path.startsWith('/api/cart') || req.path.startsWith('/api/stock') || req.path.startsWith('/api/checkout'),
731
743
  allowCrossNetworkRoaming: false, // E-commerce : interdiction de changer de réseau sans re-challenge
744
+ filterWhitelist: true, // Tolérance zéro pour le scraping / scalping distribué
732
745
  wasm: true,
733
746
  useAsymmetricTickets: true,
734
747
  }
@@ -2793,12 +2806,20 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
2793
2806
  highScoreCount: 0,
2794
2807
  deviceIds: [],
2795
2808
  highScoreDevices: {},
2796
- lastActivity: 0
2809
+ lastActivity: 0,
2810
+ ips: [],
2811
+ uas: []
2797
2812
  };
2798
2813
 
2799
2814
  if (!subnetData.highScoreDevices) {
2800
2815
  subnetData.highScoreDevices = {};
2801
2816
  }
2817
+ if (subnetData.ips instanceof Set) {
2818
+ subnetData.ips = Array.from(subnetData.ips);
2819
+ } else if (!subnetData.ips) {
2820
+ subnetData.ips = [];
2821
+ }
2822
+ if (!subnetData.uas) subnetData.uas = [];
2802
2823
 
2803
2824
  // Utilisation d'un identifiant d'appareil stable (fingerprint matériel) plutôt que l'ID de cookie volatil
2804
2825
  const currentDeviceHash = getCompositeDeviceHash(context);
@@ -2813,6 +2834,16 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
2813
2834
  if (!subnetData.deviceIds.includes(stableFpId)) {
2814
2835
  subnetData.deviceIds.push(stableFpId);
2815
2836
  }
2837
+
2838
+ if (!subnetData.ips.includes(context.clientIp)) {
2839
+ subnetData.ips.push(context.clientIp);
2840
+ }
2841
+
2842
+ const userAgent = context.headers?.['user-agent'] || '';
2843
+ if (userAgent && !subnetData.uas.includes(userAgent)) {
2844
+ subnetData.uas.push(userAgent);
2845
+ }
2846
+
2816
2847
  subnetData.lastActivity = Date.now();
2817
2848
 
2818
2849
  if (subnetData.deviceIds.length > 100) {
@@ -2823,6 +2854,8 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
2823
2854
  delete subnetData.highScoreDevices[oldDeviceId];
2824
2855
  }
2825
2856
  }
2857
+ if (subnetData.ips.length > 100) subnetData.ips.shift();
2858
+ if (subnetData.uas.length > 50) subnetData.uas.shift();
2826
2859
 
2827
2860
  await store.set(key, subnetData, 86400); // 24-hour TTL
2828
2861
  }
@@ -2846,16 +2879,37 @@ async function getSubnetScore(context) {
2846
2879
 
2847
2880
  let highScoreCount = subnetData.highScoreCount || 0;
2848
2881
  let deviceCount = subnetData.deviceIds ? subnetData.deviceIds.length : 0;
2882
+ let ipCount = 1;
2883
+ if (subnetData.ips) {
2884
+ ipCount = subnetData.ips instanceof Set ? subnetData.ips.size : (subnetData.ips.length || 1);
2885
+ }
2886
+ let uaCount = subnetData.uas ? subnetData.uas.length : 1;
2849
2887
 
2850
2888
  if (halfLives > 0) {
2851
- highScoreCount = Math.max(0, Math.floor(highScoreCount / Math.pow(2, halfLives)));
2852
- deviceCount = Math.max(0, Math.floor(deviceCount / Math.pow(2, halfLives)));
2889
+ const decay = Math.pow(2, halfLives);
2890
+ highScoreCount = Math.max(0, Math.floor(highScoreCount / decay));
2891
+ deviceCount = Math.max(0, Math.floor(deviceCount / decay));
2892
+ ipCount = Math.max(1, Math.floor(ipCount / decay));
2893
+ uaCount = Math.max(1, Math.floor(uaCount / decay));
2894
+ }
2895
+
2896
+ if (deviceCount === 0) {
2897
+ return { subnetScore: 0.0 };
2853
2898
  }
2854
2899
 
2855
- const deviceCountPenalty = Math.min(80, Math.max(0, deviceCount - 10) * 5);
2856
- const highScorePenalty = Math.min(40, highScoreCount * 2);
2900
+ // Calculs analogues continus (sans sauts brusques)
2901
+ const suspicionDensity = highScoreCount / deviceCount;
2902
+ const ipDeviceRatio = ipCount / deviceCount;
2903
+
2904
+ // Base score continu basé sur le volume de menaces
2905
+ const baseScore = 100 * (1 - Math.exp(-0.15 * highScoreCount));
2906
+
2907
+ // Multiplicateurs continus
2908
+ const densityMultiplier = 0.4 + (1.6 * suspicionDensity); // Favorise les densités de suspicion élevées
2909
+ const distributionMultiplier = 0.5 + (1.0 * ipDeviceRatio); // NAT (faible ratio IP/Device) vs Proxy distribué (fort ratio)
2857
2910
 
2858
- return { subnetScore: Math.min(100, deviceCountPenalty + highScorePenalty) };
2911
+ const finalScore = Math.min(100, Math.round(baseScore * densityMultiplier * distributionMultiplier * 10) / 10);
2912
+ return { subnetScore: finalScore };
2859
2913
  }
2860
2914
 
2861
2915
  /**
@@ -2877,17 +2931,31 @@ async function getBotnetClusterScore(context, stableFpHash) {
2877
2931
 
2878
2932
  clusterData = clusterData.filter(entry => entry.timestamp > tenMinutesAgo);
2879
2933
  const existingIndex = clusterData.findIndex(entry => entry.ip === context.clientIp);
2934
+ const userAgent = context.headers?.['user-agent'] || '';
2935
+ const subnet = getIpSubnet(context.clientIp) || 'unknown';
2936
+
2880
2937
  if (existingIndex !== -1) {
2881
2938
  clusterData[existingIndex].timestamp = now;
2939
+ clusterData[existingIndex].ua = userAgent;
2940
+ clusterData[existingIndex].subnet = subnet;
2882
2941
  } else {
2883
- clusterData.push({ ip: context.clientIp, timestamp: now });
2942
+ clusterData.push({ ip: context.clientIp, timestamp: now, ua: userAgent, subnet: subnet });
2884
2943
  }
2885
2944
 
2886
2945
  await store.set(key, clusterData, 600);
2887
2946
  const uniqueIpsCount = clusterData.length;
2888
2947
  let botnetClusterScore = 0;
2889
2948
  if (uniqueIpsCount >= 2) {
2890
- botnetClusterScore = Math.min(100, Math.round(1000 * (1 - Math.exp(-0.35 * (uniqueIpsCount - 1)))) / 10);
2949
+ // Calcul de la diversité des sous-réseaux et de la rotation des User-Agents
2950
+ const uniqueSubnets = new Set(clusterData.map(e => e.subnet)).size;
2951
+ const uniqueUserAgents = new Set(clusterData.map(e => e.ua).filter(Boolean)).size;
2952
+
2953
+ // Facteurs d'ajustement
2954
+ const subnetMultiplier = uniqueSubnets > 1 ? 1.3 : 0.6; // Réduit le score si même sous-réseau (NAT), l'augmente si distribué
2955
+ const uaRotationMultiplier = uniqueUserAgents > 1 ? 1.5 : 1.0; // Forte pénalité en cas de rotation d'en-tête UA
2956
+
2957
+ const baseScore = 100 * (1 - Math.exp(-0.35 * (uniqueIpsCount - 1)));
2958
+ botnetClusterScore = Math.min(100, Math.round(baseScore * subnetMultiplier * uaRotationMultiplier * 10) / 10);
2891
2959
  }
2892
2960
  return { botnetClusterScore };
2893
2961
  }
@@ -3105,6 +3173,7 @@ function verifyTrapUrl(path, signature, nonce) {
3105
3173
  * @property {(key: string, value: any, ttl?: number) => Promise<void>} set
3106
3174
  * @property {(key: string) => Promise<boolean>} has
3107
3175
  * @property {(key: string) => Promise<void>} delete
3176
+ * @property {() => Promise<void>} clear
3108
3177
  */
3109
3178
 
3110
3179
  /**
@@ -3130,6 +3199,13 @@ const inMemoryStore = {
3130
3199
  },
3131
3200
  async has(key) { return this._map.has(key); },
3132
3201
  async delete(key) { this._map.delete(key); },
3202
+ async clear() {
3203
+ this._map.clear();
3204
+ for (const timeoutId of this._timeouts.values()) {
3205
+ clearTimeout(timeoutId);
3206
+ }
3207
+ this._timeouts.clear();
3208
+ }
3133
3209
  };
3134
3210
 
3135
3211
  /** @type {IStore} */
@@ -3513,13 +3589,15 @@ function calculateTarget(suspicionFactor, securityConfig = {}) {
3513
3589
  * @param {string} nonce
3514
3590
  * @param {string} clientSecret
3515
3591
  * @param {string} fingerprint
3592
+ * @param {string} clientIp
3593
+ * @param {string} tlsSessionId
3516
3594
  * @returns {Buffer}
3517
3595
  */
3518
- function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint) {
3596
+ function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp = '', tlsSessionId = '') {
3519
3597
  const sortedFingerprint = (fingerprint || '').split('|').filter(p => p).sort().join('|');
3520
3598
  // On concatène les chaînes, puis on les convertit en buffer une seule fois.
3521
3599
  // Cela garantit que le client et le serveur travaillent sur la même base binaire.
3522
- const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:`; // Le ':' final est le séparateur pour la solution.
3600
+ const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:${clientIp}:${tlsSessionId}:`; // Le ':' final est le séparateur pour la solution.
3523
3601
  return Buffer.from(messageBase, 'utf8');
3524
3602
  }
3525
3603
 
@@ -3532,10 +3610,11 @@ export function generateCpuTargetChallenge(
3532
3610
  suspicionFactor,
3533
3611
  originalUrl,
3534
3612
  securityConfig,
3613
+ tlsSessionId = '',
3535
3614
  ) {
3536
3615
  const target = calculateTarget(suspicionFactor, securityConfig);
3537
3616
  // Le baseBlock est créé ici et sera stocké dans le contexte du challenge.
3538
- const baseBlock = createCpuChallengeBaseBlock(nonce, null, ''); // Pour le challenge simple, le secret et le fingerprint sont vides.
3617
+ const baseBlock = createCpuChallengeBaseBlock(nonce, null, '', clientIp, tlsSessionId);
3539
3618
  return {
3540
3619
  type: "cpu_target",
3541
3620
  nonce: nonce,
@@ -3619,14 +3698,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
3619
3698
  * @param {string} clientIp - The client's IP address.
3620
3699
  * @returns {string} HTML content.
3621
3700
  */
3622
- function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
3701
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint, tlsSessionId = '') { // eslint-disable-line max-len
3623
3702
  const { nonce, target, path } = cpuChallengeDetails;
3624
3703
  const safePath = sanitizeRedirectPath(path);
3625
3704
  const solverCode = getPowSolverCode();
3626
3705
  // On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
3627
3706
  // Le fingerprint est maintenant passé directement en paramètre.
3628
3707
  const fingerprint = originalFingerprint;
3629
- const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
3708
+ const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp, tlsSessionId);
3630
3709
  const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
3631
3710
 
3632
3711
  // Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
@@ -3825,16 +3904,31 @@ export class FingerprintEngine {
3825
3904
 
3826
3905
  // Auto-generate Ed25519 key pair on load if indicated and keys are not set
3827
3906
  if (securityConfig && (securityConfig.useAsymmetricTickets || securityConfig.ed25519 === 'auto') && !process.env.ED25519_PRIVATE_KEY) {
3828
- try {
3829
- const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
3830
- privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
3831
- publicKeyEncoding: { format: 'pem', type: 'spki' }
3832
- });
3833
- process.env.ED25519_PRIVATE_KEY = privateKey;
3834
- process.env.ED25519_PUBLIC_KEY = publicKey;
3835
- } catch (e) {
3836
- console.error('[Fingerprint] Native Ed25519 key generation failed:', e.message);
3837
- }
3907
+ const persistentKeyPath = join(configDir, 'ed25519_key.json');
3908
+ if (existsSync(persistentKeyPath)) {
3909
+ try {
3910
+ const keys = JSON.parse(readFileSync(persistentKeyPath, 'utf-8'));
3911
+ process.env.ED25519_PRIVATE_KEY = keys.privateKey;
3912
+ process.env.ED25519_PUBLIC_KEY = keys.publicKey;
3913
+ this._log('Persistent Ed25519 keys loaded from disk');
3914
+ } catch (e) {
3915
+ console.error('[Fingerprint] Failed to load persistent Ed25519 keys:', e.message);
3916
+ }
3917
+ } else {
3918
+ try {
3919
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
3920
+ privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
3921
+ publicKeyEncoding: { format: 'pem', type: 'spki' }
3922
+ });
3923
+ process.env.ED25519_PRIVATE_KEY = privateKey;
3924
+ process.env.ED25519_PUBLIC_KEY = publicKey;
3925
+ // Persist keys on disk for subsequent restarts
3926
+ writeFileSync(persistentKeyPath, JSON.stringify({ privateKey, publicKey }, null, 2), 'utf-8');
3927
+ this._log('New persistent Ed25519 keys generated and saved to disk');
3928
+ } catch (e) {
3929
+ console.error('[Fingerprint] Native Ed25519 key generation failed:', e.message);
3930
+ }
3931
+ }
3838
3932
  }
3839
3933
 
3840
3934
  let finalConfig = securityConfig;
@@ -3855,6 +3949,11 @@ export class FingerprintEngine {
3855
3949
  this._validateConfig(finalConfig); // Validate the configuration
3856
3950
  this.verbose = finalConfig.verbose || false;
3857
3951
  this.dryRun = finalConfig.dryRun || false;
3952
+ if (finalConfig.reset) {
3953
+ this.resetStore().catch(err => {
3954
+ console.error('[FingerprintEngine] Failed to reset store on startup:', err.message);
3955
+ });
3956
+ }
3858
3957
  }
3859
3958
 
3860
3959
  /**
@@ -3869,6 +3968,16 @@ export class FingerprintEngine {
3869
3968
  this._log('Configuration mise à jour à chaud (Hot-Reloaded)', this.securityConfig);
3870
3969
  }
3871
3970
 
3971
+ /**
3972
+ * Réinitialise le store de persistance actif.
3973
+ */
3974
+ async resetStore() {
3975
+ if (store && typeof store.clear === 'function') {
3976
+ await store.clear();
3977
+ this._log('Store has been reset/cleared.');
3978
+ }
3979
+ }
3980
+
3872
3981
  /**
3873
3982
  * Validates the security configuration object to detect potential typos or missing essential keys.
3874
3983
  * @private
@@ -3887,9 +3996,9 @@ export class FingerprintEngine {
3887
3996
  'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
3888
3997
  'trustedProxies',
3889
3998
  'wasm',
3890
- 'similarityThreshold',
3999
+ 'similarityThreshold', 'reset',
3891
4000
  'ed25519_private_key', 'ed25519_public_key',
3892
- 'federatedPeers', 'federationSecret'
4001
+ 'federatedPeers', 'federationSecret', 'filterWhitelist'
3893
4002
  ]);
3894
4003
 
3895
4004
  // 1. Check for essential keys
@@ -3908,6 +4017,19 @@ export class FingerprintEngine {
3908
4017
  }
3909
4018
  }
3910
4019
 
4020
+ _hasCertainAttack(context) {
4021
+ const honeypotConfig = this.securityConfig.honeypot || {};
4022
+ const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
4023
+ if (honeypotScore >= 100) {
4024
+ return true;
4025
+ }
4026
+ const { botScore } = getBotScore(context);
4027
+ if (botScore >= 100) {
4028
+ return true;
4029
+ }
4030
+ return false;
4031
+ }
4032
+
3911
4033
  _log(message, data = {}) {
3912
4034
  if (this.verbose) {
3913
4035
  console.log(`[FingerprintEngine] ${message}`, data);
@@ -4223,6 +4345,16 @@ export class FingerprintEngine {
4223
4345
 
4224
4346
  const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
4225
4347
 
4348
+ let preCalculatedVector = null;
4349
+ let preCalculatedScore = null;
4350
+ const getScoreAndVector = async () => {
4351
+ if (preCalculatedScore === null) {
4352
+ preCalculatedVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
4353
+ preCalculatedScore = this.calculateFinalScore(preCalculatedVector);
4354
+ }
4355
+ return { score: preCalculatedScore, vector: preCalculatedVector };
4356
+ };
4357
+
4226
4358
  this._log('Processing request', { clientIp, path, isStatic });
4227
4359
 
4228
4360
  // Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
@@ -4243,34 +4375,47 @@ export class FingerprintEngine {
4243
4375
  const allowRoaming = this.securityConfig?.allowCrossNetworkRoaming ?? false;
4244
4376
 
4245
4377
  // 1. Check static IP allowlist first for maximum performance.
4246
- if (this._isIpInAllowlist(clientIp)) {
4247
- this._log('IP in allowlist - allowing request', { clientIp });
4248
- return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'allowlist' } };
4249
- }
4250
-
4251
- // 2. Check hostname-based allowlist.
4252
- if (await this._isIpInHostnameAllowlist(clientIp)) {
4253
- this._log('IP resolves to a whitelisted hostname - allowing request', { clientIp });
4254
- return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'hostname_allowlist' } };
4255
- }
4378
+ let whitelisted = false;
4379
+ let whitelistType = '';
4256
4380
 
4257
- // 3. Check host+path based allowlist.
4258
- const requestHost = requestContext.headers?.host;
4259
- if (requestHost && this._isHostPathInAllowlist(requestHost, path)) {
4260
- this._log('Host and path in allowlist - allowing request', { host: requestHost, path });
4261
- return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'host_path_allowlist' } };
4381
+ if (this._isIpInAllowlist(clientIp)) {
4382
+ whitelisted = true;
4383
+ whitelistType = 'allowlist';
4384
+ } else if (await this._isIpInHostnameAllowlist(clientIp)) {
4385
+ whitelisted = true;
4386
+ whitelistType = 'hostname_allowlist';
4387
+ } else {
4388
+ const requestHost = requestContext.headers?.host;
4389
+ if (requestHost && this._isHostPathInAllowlist(requestHost, path)) {
4390
+ whitelisted = true;
4391
+ whitelistType = 'host_path_allowlist';
4392
+ } else if (this._isPathInAllowlist(path)) {
4393
+ whitelisted = true;
4394
+ whitelistType = 'path_allowlist';
4395
+ } else if (graphqlOperationType && this._isGraphqlOperationInAllowlist(graphqlOperationType, graphqlOperationName)) {
4396
+ whitelisted = true;
4397
+ whitelistType = 'graphql_operation_allowlist';
4398
+ }
4262
4399
  }
4263
4400
 
4264
- // 3. Check path-based allowlist.
4265
- if (this._isPathInAllowlist(path)) {
4266
- this._log('Path in allowlist - allowing request', { path });
4267
- return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'path_allowlist' } };
4268
- }
4401
+ if (whitelisted) {
4402
+ const filterWhitelist = this.securityConfig.filterWhitelist || false;
4403
+ let bypassWhitelist = false;
4404
+ if (filterWhitelist === true) {
4405
+ bypassWhitelist = this._hasCertainAttack(requestContext);
4406
+ } else if (typeof filterWhitelist === 'number') {
4407
+ const res = await getScoreAndVector();
4408
+ if (res.score > filterWhitelist) {
4409
+ bypassWhitelist = true;
4410
+ }
4411
+ }
4269
4412
 
4270
- // 5. Check GraphQL operation allowlist.
4271
- if (graphqlOperationType && this._isGraphqlOperationInAllowlist(graphqlOperationType, graphqlOperationName)) {
4272
- this._log('GraphQL operation in allowlist - allowing request', { operation: `${graphqlOperationType}:${graphqlOperationName}` });
4273
- return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'graphql_operation_allowlist' } };
4413
+ if (bypassWhitelist) {
4414
+ this._log('Whitelisted request exceeds filter threshold - bypassing whitelist bypass', { clientIp, path });
4415
+ } else {
4416
+ this._log(`IP/Path in allowlist (${whitelistType}) - allowing request`, { clientIp, path });
4417
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: whitelistType } };
4418
+ }
4274
4419
  }
4275
4420
 
4276
4421
  const { pow_nonce } = query;
@@ -4288,8 +4433,23 @@ export class FingerprintEngine {
4288
4433
 
4289
4434
  // Check if the request is from a verified, whitelisted bot (e.g., Googlebot)
4290
4435
  if (await this._verifyWhitelistedBot(requestContext)) {
4291
- this._log('Whitelisted bot verified - allowing request', { clientIp });
4292
- return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
4436
+ const filterWhitelist = this.securityConfig.filterWhitelist || false;
4437
+ let bypassWhitelist = false;
4438
+ if (filterWhitelist === true) {
4439
+ bypassWhitelist = this._hasCertainAttack(requestContext);
4440
+ } else if (typeof filterWhitelist === 'number') {
4441
+ const res = await getScoreAndVector();
4442
+ if (res.score > filterWhitelist) {
4443
+ bypassWhitelist = true;
4444
+ }
4445
+ }
4446
+
4447
+ if (bypassWhitelist) {
4448
+ this._log('Verified bot request exceeds filter threshold - bypassing bot whitelist bypass', { clientIp });
4449
+ } else {
4450
+ this._log('Whitelisted bot verified - allowing request', { clientIp });
4451
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
4452
+ }
4293
4453
  }
4294
4454
 
4295
4455
  this._log('Identity resolved', { deviceId, isNewDevice, hasDeviceData: !!deviceData });
@@ -4318,16 +4478,14 @@ export class FingerprintEngine {
4318
4478
  }
4319
4479
 
4320
4480
  // The engine now works with the context directly, no more rawReq dependency here.
4321
- const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
4322
- // honeypotScore et behaviorScore sont maintenant inclus directement dans le vecteur de suspicion.
4481
+ const suspicionVector = preCalculatedVector || await __internal.getSuspicionVector(requestContext, this.securityConfig);
4482
+ let finalScore = preCalculatedScore !== null ? preCalculatedScore : this.calculateFinalScore(suspicionVector);
4323
4483
 
4324
4484
  this._log('Suspicion vector calculated', {
4325
4485
  vector: suspicionVector,
4326
4486
  weights: this.securityConfig.weights
4327
4487
  });
4328
4488
 
4329
- let finalScore = this.calculateFinalScore(suspicionVector);
4330
-
4331
4489
  this._log('Final score calculated', { finalScore });
4332
4490
 
4333
4491
  const blockThreshold = thresholds.block ?? 95;
@@ -4609,6 +4767,27 @@ export class FingerprintEngine {
4609
4767
  }, store);
4610
4768
  await manager.integrateSolution(pow_problem_id, workResult);
4611
4769
 
4770
+ // Si le problème résolu est l'auto-tuning de sécurité et que l'auto-tuning est activé,
4771
+ // on applique directement la meilleure solution calculée au moteur en direct.
4772
+ if (pow_problem_id === 'security_auto_tuning' && this.securityConfig.autotuning?.enabled) {
4773
+ const paretoFront = workResult.paretoFront;
4774
+ if (Array.isArray(paretoFront) && paretoFront.length > 0) {
4775
+ let bestSolution = paretoFront[0];
4776
+ let minDistance = Math.sqrt(Math.pow(bestSolution.objectives[0], 2) + Math.pow(bestSolution.objectives[1], 2));
4777
+ for (let i = 1; i < paretoFront.length; i++) {
4778
+ const distance = Math.sqrt(Math.pow(paretoFront[i].objectives[0], 2) + Math.pow(paretoFront[i].objectives[1], 2));
4779
+ if (distance < minDistance) {
4780
+ minDistance = distance;
4781
+ bestSolution = paretoFront[i];
4782
+ }
4783
+ }
4784
+ if (bestSolution && bestSolution.solution) {
4785
+ this.updateConfig(bestSolution.solution);
4786
+ this._log('Useful Work auto-tuning applied successfully to live config.');
4787
+ }
4788
+ }
4789
+ }
4790
+
4612
4791
  await store.delete(`secret:${pow_nonce}`);
4613
4792
  // Accorder un ticket de passage comme pour un PoW normal
4614
4793
  const ticket = "valid_ticket_placeholder"; // Générer un vrai ticket ici
@@ -4896,7 +5075,8 @@ export class FingerprintEngine {
4896
5075
  const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce)); // Génère les URL
4897
5076
 
4898
5077
  // On passe la configuration pour que la difficulté soit calculée correctement.
4899
- const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
5078
+ const tlsSessionId = getTlsSessionId(requestContext) || '';
5079
+ const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig, tlsSessionId);
4900
5080
 
4901
5081
  // La difficulté mémoire augmente désormais en parfaite synergie avec le facteur de suspicion (ratio constant)
4902
5082
  const memActivationFactor = suspicionFactor;
@@ -4915,7 +5095,7 @@ export class FingerprintEngine {
4915
5095
  const originalFingerprint = requestContext.headers['x-device-fingerprint'] || __internal.getCompositeDeviceHash(requestContext);
4916
5096
 
4917
5097
  // Store the entire challenge context with a short TTL (e.g., 5 minutes)
4918
- const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint);
5098
+ const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint, clientIp, tlsSessionId);
4919
5099
 
4920
5100
  // SECURITY: Cryptographically sign the payload before storing it to prevent database tampering
4921
5101
  const payloadToSign = `${clientSecret}:${cpuChallengeDetails.target}:${originalFingerprint}:${memDifficulty}:${path}:${clientIp}`;
@@ -4964,7 +5144,7 @@ export class FingerprintEngine {
4964
5144
  decision.body = challengePayload;
4965
5145
  } else {
4966
5146
  // For browsers, send the HTML page.
4967
- const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapUrls, originalFingerprint);
5147
+ const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapUrls, originalFingerprint, tlsSessionId);
4968
5148
  this._log('Browser challenge page generated', {
4969
5149
  pageLength: page.length,
4970
5150
  trapUrlsInjected: trapUrls.length
@@ -6106,7 +6286,10 @@ export const __internal = {
6106
6286
  registerCooperativeNode,
6107
6287
  findPeerInSubnet,
6108
6288
  handleCooperativeRequest,
6109
- broadcastBannedZkp
6289
+ broadcastBannedZkp,
6290
+ getMetric,
6291
+ incrementCounter,
6292
+ observeValue
6110
6293
  };
6111
6294
 
6112
6295
  // --- THRESHOLD AUTO-TUNING SECTION ---
@@ -6125,20 +6308,21 @@ export function sanitizeTrafficData(trafficData) {
6125
6308
  if (!trafficData || trafficData.length === 0) {
6126
6309
  return [];
6127
6310
  }
6128
- const suspiciousLogs = [];
6129
- const passedLogs = [];
6130
- const deviceCounts = new Map();
6311
+
6312
+ const rawLogs = [...trafficData];
6313
+ const totalCount = rawLogs.length;
6314
+
6315
+ const maxLogsPerDevice = Math.max(3, Math.floor(totalCount * 0.02)); // Max 2% contribution per device
6316
+ const maxLogsPerIp = Math.max(3, Math.floor(totalCount * 0.02)); // Max 2% par adresse IP individuelle
6317
+ const maxLogsPerSubnet = Math.max(5, Math.floor(totalCount * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
6318
+ const maxLogsPerHardwareCluster = Math.max(3, Math.floor(totalCount * 0.02)); // Max 2% par cluster matériel stable
6319
+
6320
+ const deviceCounts = new Map();
6131
6321
  const ipCounts = new Map();
6132
6322
  const subnetCounts = new Map();
6133
6323
  const hardwareClusterCounts = new Map();
6134
6324
  const hwClusterCache = new Map();
6135
6325
 
6136
- // Calcul des quotas maximums pour éviter l'influence démesurée d'une entité
6137
- const maxLogsPerDevice = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% contribution per device
6138
- const maxLogsPerIp = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par adresse IP individuelle
6139
- const maxLogsPerSubnet = Math.max(5, Math.floor(trafficData.length * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
6140
- const maxLogsPerHardwareCluster = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par cluster matériel stable
6141
-
6142
6326
  const getHardwareCluster = (log) => {
6143
6327
  const fp = log.deviceHash || log.fingerprint || log.deviceFingerprint || '';
6144
6328
  if (!fp || typeof fp !== 'string') {
@@ -6159,8 +6343,41 @@ export function sanitizeTrafficData(trafficData) {
6159
6343
  hwClusterCache.set(fp, result);
6160
6344
  return result;
6161
6345
  };
6346
+ // --- REVOLUTION : Compression de Cohorte par Densité Vectorielle (Anti-Sybil / Anti-Poisoning) ---
6347
+ const clusteredLogs = [];
6348
+ const getVectorDistance = (v1, v2) => {
6349
+ if (!v1 || !v2) return Infinity;
6350
+ let sum = 0;
6351
+ const keys = new Set([...Object.keys(v1), ...Object.keys(v2)]);
6352
+ for (const key of keys) {
6353
+ sum += Math.pow((v1[key] || 0) - (v2[key] || 0), 2);
6354
+ }
6355
+ return Math.sqrt(sum);
6356
+ };
6357
+
6358
+ for (const log of rawLogs) {
6359
+ let matchedCluster = null;
6360
+ for (const cluster of clusteredLogs) {
6361
+ if (log.type === cluster.type && getVectorDistance(log.vector, cluster.vector) < 5.0) {
6362
+ matchedCluster = cluster;
6363
+ break;
6364
+ }
6365
+ }
6366
+ if (matchedCluster) {
6367
+ matchedCluster.instancesCount = (matchedCluster.instancesCount || 1) + 1;
6368
+ matchedCluster.weight = 1 + Math.log(matchedCluster.instancesCount); // Compression logarithmique
6369
+ } else {
6370
+ const logCopy = { ...log };
6371
+ logCopy.instancesCount = 1;
6372
+ logCopy.weight = 1.0;
6373
+ clusteredLogs.push(logCopy);
6374
+ }
6375
+ }
6162
6376
 
6163
- for (const log of trafficData) {
6377
+ const suspiciousLogs = [];
6378
+ const passedLogs = [];
6379
+
6380
+ for (const log of clusteredLogs) {
6164
6381
  const devId = log.deviceId || 'anonymous';
6165
6382
  const ip = log.clientIp || log.ip || 'unknown';
6166
6383
  const subnet = getIpSubnet(ip) || 'unknown-subnet';
@@ -6249,29 +6466,33 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
6249
6466
  pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup);
6250
6467
  const sanitizedData = sanitizeTrafficData(trafficData);
6251
6468
 
6252
- const highConfidenceLogs = sanitizedData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
6253
- const highConfidenceRatio = sanitizedData.length > 0 ? highConfidenceLogs / sanitizedData.length : 0;
6469
+ const highConfidenceLogs = sanitizedData
6470
+ .filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered')
6471
+ .reduce((sum, log) => sum + (log.instancesCount || 1), 0);
6472
+ const totalSanitizedInstances = sanitizedData.reduce((sum, log) => sum + (log.instancesCount || 1), 0);
6473
+ const highConfidenceRatio = totalSanitizedInstances > 0 ? highConfidenceLogs / totalSanitizedInstances : 0;
6254
6474
  const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
6255
6475
  const MIN_HIGH_CONFIDENCE_COUNT = 10; // Absolu de secours pour éviter le gel lors de floods
6256
6476
 
6257
6477
  const hasEnoughSignal = highConfidenceRatio >= MIN_CONFIDENCE_RATIO || highConfidenceLogs >= MIN_HIGH_CONFIDENCE_COUNT;
6258
6478
 
6259
- if (sanitizedData.length < minDataPoints || !hasEnoughSignal) {
6260
- if (sanitizedData.length < minDataPoints) {
6261
- console.log(`[AutoTuning] Reporté : ${sanitizedData.length}/${minDataPoints} points de données.`);
6479
+ if (totalSanitizedInstances < minDataPoints || !hasEnoughSignal) {
6480
+ if (totalSanitizedInstances < minDataPoints) {
6481
+ console.log(`[AutoTuning] Reporté : ${totalSanitizedInstances}/${minDataPoints} points de données.`);
6262
6482
  } else {
6263
6483
  console.log(`[AutoTuning] Reporté : Signaux de confiance insuffisants (Ratio: ${(highConfidenceRatio * 100).toFixed(2)}% < ${(MIN_CONFIDENCE_RATIO * 100).toFixed(2)}% et absolu: ${highConfidenceLogs} < ${MIN_HIGH_CONFIDENCE_COUNT}).`);
6264
6484
  }
6265
6485
  return;
6266
6486
  }
6267
- console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${sanitizedData.length} points de données assainis.`);
6487
+ console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${totalSanitizedInstances} points de données assainis.`);
6268
6488
 
6269
- const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
6489
+ try {
6490
+ const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
6270
6491
 
6271
- if (!paretoFront || paretoFront.length === 0) {
6272
- console.warn("[AutoTuning] L'optimisation n'a retourné aucune solution.");
6273
- return;
6274
- }
6492
+ if (!paretoFront || paretoFront.length === 0) {
6493
+ console.warn("[AutoTuning] L'optimisation n'a retourné aucune solution.");
6494
+ return;
6495
+ }
6275
6496
 
6276
6497
  // Règles de gardiennage (Sanity Guardrails) pour filtrer le front de Pareto
6277
6498
  const isValidSecurityConfig = (config) => {
@@ -6429,13 +6650,14 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
6429
6650
  if (savePath) {
6430
6651
  try {
6431
6652
  const configToSave = JSON.stringify(bestSolution.solution, null, 2);
6432
- fs.writeFileSync(savePath, configToSave, 'utf-8');
6653
+ writeFileSync(savePath, configToSave, 'utf-8');
6433
6654
  console.log(`[AutoTuning] Meilleure configuration sauvegardée dans : ${savePath}`);
6434
6655
  } catch (error) {
6435
6656
  console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
6436
6657
  }
6437
6658
  }
6438
6659
 
6660
+ } finally {
6439
6661
  if (clearAfterTuning) {
6440
6662
  const cleared = trafficData.splice(0, trafficData.length);
6441
6663
  if (onCleanup && typeof onCleanup === 'function' && cleared.length > 0) {
@@ -6447,6 +6669,7 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
6447
6669
  }
6448
6670
  console.log(`[AutoTuning] Explicitly cleared ${cleared.length} processed traffic data points.`);
6449
6671
  }
6672
+ }
6450
6673
  }
6451
6674
 
6452
6675
  /**
@@ -6523,36 +6746,156 @@ class RequestContext {
6523
6746
  }
6524
6747
  }
6525
6748
 
6526
- const MetricsManager = {
6527
- getPrometheusMetrics(securityConfig = {}) {
6528
- let metrics = `# HELP fingerprint_requests_total Total requests processed.\n# TYPE fingerprint_requests_total counter\nfingerprint_requests_total{status="passed"} 1\n`;
6749
+ const dynamicCounters = new Map();
6750
+ const dynamicObservations = new Map();
6529
6751
 
6530
- if (securityConfig.weights) {
6531
- metrics += `\n# HELP fingerprint_security_weight Active weight for each suspicion indicator.\n# TYPE fingerprint_security_weight gauge\n`;
6532
- for (const [indicator, weight] of Object.entries(securityConfig.weights)) {
6533
- if (typeof weight === 'number') {
6534
- metrics += `fingerprint_security_weight{indicator="${indicator}"} ${weight}\n`;
6535
- }
6536
- }
6752
+ export function incrementCounter(name, labels = {}) {
6753
+ const sortedLabels = Object.keys(labels).sort().reduce((acc, key) => {
6754
+ acc[key] = labels[key];
6755
+ return acc;
6756
+ }, {});
6757
+ const labelsKey = JSON.stringify(sortedLabels);
6758
+ const queryName = name.startsWith('fingerprint_') ? name : `fingerprint_${name}`;
6759
+ const fullKey = `${queryName}:${labelsKey}`;
6760
+
6761
+ if (!dynamicCounters.has(fullKey)) {
6762
+ dynamicCounters.set(fullKey, { name: queryName, labels: sortedLabels, value: 0 });
6537
6763
  }
6764
+ dynamicCounters.get(fullKey).value++;
6765
+ }
6538
6766
 
6539
- if (securityConfig.thresholds) {
6540
- metrics += `\n# HELP fingerprint_security_threshold Active score threshold for each enforcement action level.\n# TYPE fingerprint_security_threshold gauge\n`;
6541
- for (const [level, threshold] of Object.entries(securityConfig.thresholds)) {
6542
- if (typeof threshold === 'number') {
6543
- metrics += `fingerprint_security_threshold{level="${level}"} ${threshold}\n`;
6767
+ export function observeValue(name, value, labels = {}) {
6768
+ const sortedLabels = Object.keys(labels).sort().reduce((acc, key) => {
6769
+ acc[key] = labels[key];
6770
+ return acc;
6771
+ }, {});
6772
+ const labelsKey = JSON.stringify(sortedLabels);
6773
+ const queryName = name.startsWith('fingerprint_') ? name : `fingerprint_${name}`;
6774
+ const fullKey = `${queryName}:${labelsKey}`;
6775
+ dynamicObservations.set(fullKey, { name: queryName, labels: sortedLabels, value });
6776
+ }
6777
+
6778
+ export function getMetric(name, securityConfig = {}) {
6779
+ const result = new Map();
6780
+ const queryName = name.startsWith('fingerprint_') ? name : `fingerprint_${name}`;
6781
+
6782
+ if (queryName === 'fingerprint_security_weight') {
6783
+ const weights = securityConfig.weights || {};
6784
+ for (const [indicator, weight] of Object.entries(weights)) {
6785
+ if (typeof weight === 'number') {
6786
+ result.set(indicator, weight);
6787
+ }
6788
+ }
6789
+ } else if (queryName === 'fingerprint_security_threshold') {
6790
+ const thresholds = securityConfig.thresholds || {};
6791
+ for (const [level, threshold] of Object.entries(thresholds)) {
6792
+ if (typeof threshold === 'number') {
6793
+ result.set(level, threshold);
6794
+ }
6795
+ }
6796
+ } else if (queryName === 'fingerprint_autotuning_false_positive_rate') {
6797
+ if (lastBestSolution && lastBestSolution.objectives) {
6798
+ result.set('fpr', lastBestSolution.objectives[0]);
6799
+ }
6800
+ } else if (queryName === 'fingerprint_autotuning_false_negative_rate') {
6801
+ if (lastBestSolution && lastBestSolution.objectives) {
6802
+ result.set('fnr', lastBestSolution.objectives[1]);
6803
+ }
6804
+ } else {
6805
+ // Search dynamic counters
6806
+ for (const counter of dynamicCounters.values()) {
6807
+ if (counter.name === queryName || counter.name === name) {
6808
+ const labelPairs = Object.entries(counter.labels)
6809
+ .map(([k, v]) => `${k}="${v}"`)
6810
+ .join(',');
6811
+ result.set(labelPairs || 'value', counter.value);
6812
+ }
6544
6813
  }
6545
- }
6546
- }
6547
6814
 
6548
- // Include auto-tuning objectives metrics if the auto-tuner has run
6549
- if (lastBestSolution && lastBestSolution.objectives) {
6550
- 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`;
6551
- 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`;
6815
+ // Search dynamic observations
6816
+ for (const obs of dynamicObservations.values()) {
6817
+ if (obs.name === queryName || obs.name === name) {
6818
+ const labelPairs = Object.entries(obs.labels)
6819
+ .map(([k, v]) => `${k}="${v}"`)
6820
+ .join(',');
6821
+ result.set(labelPairs || 'value', obs.value);
6822
+ }
6823
+ }
6552
6824
  }
6825
+ return result;
6826
+ }
6553
6827
 
6554
- return metrics;
6555
- }
6828
+ const MetricsManager = {
6829
+ getPrometheusMetrics(securityConfig = {}) {
6830
+ let metrics = '';
6831
+
6832
+ if (dynamicCounters.size === 0) {
6833
+ metrics += `# HELP fingerprint_requests_total Total requests processed.\n# TYPE fingerprint_requests_total counter\nfingerprint_requests_total{status="passed"} 1\n`;
6834
+ } else {
6835
+ const grouped = {};
6836
+ for (const counter of dynamicCounters.values()) {
6837
+ if (!grouped[counter.name]) {
6838
+ grouped[counter.name] = [];
6839
+ }
6840
+ grouped[counter.name].push(counter);
6841
+ }
6842
+ for (const [name, instances] of Object.entries(grouped)) {
6843
+ metrics += `# HELP ${name} Total requests processed.\n# TYPE ${name} counter\n`;
6844
+ for (const inst of instances) {
6845
+ const labelPairs = Object.entries(inst.labels)
6846
+ .map(([k, v]) => `${k}="${v}"`)
6847
+ .join(',');
6848
+ const labelStr = labelPairs ? `{${labelPairs}}` : '';
6849
+ metrics += `${name}${labelStr} ${inst.value}\n`;
6850
+ }
6851
+ }
6852
+ }
6853
+
6854
+ if (dynamicObservations.size > 0) {
6855
+ const grouped = {};
6856
+ for (const obs of dynamicObservations.values()) {
6857
+ if (!grouped[obs.name]) {
6858
+ grouped[obs.name] = [];
6859
+ }
6860
+ grouped[obs.name].push(obs);
6861
+ }
6862
+ for (const [name, instances] of Object.entries(grouped)) {
6863
+ metrics += `\n# HELP ${name} Value observation.\n# TYPE ${name} gauge\n`;
6864
+ for (const inst of instances) {
6865
+ const labelPairs = Object.entries(inst.labels)
6866
+ .map(([k, v]) => `${k}="${v}"`)
6867
+ .join(',');
6868
+ const labelStr = labelPairs ? `{${labelPairs}}` : '';
6869
+ metrics += `${name}${labelStr} ${inst.value}\n`;
6870
+ }
6871
+ }
6872
+ }
6873
+
6874
+ if (securityConfig.weights) {
6875
+ metrics += `\n# HELP fingerprint_security_weight Active weight for each suspicion indicator.\n# TYPE fingerprint_security_weight gauge\n`;
6876
+ for (const [indicator, weight] of Object.entries(securityConfig.weights)) {
6877
+ if (typeof weight === 'number') {
6878
+ metrics += `fingerprint_security_weight{indicator="${indicator}"} ${weight}\n`;
6879
+ }
6880
+ }
6881
+ }
6882
+
6883
+ if (securityConfig.thresholds) {
6884
+ metrics += `\n# HELP fingerprint_security_threshold Active score threshold for each enforcement action level.\n# TYPE fingerprint_security_threshold gauge\n`;
6885
+ for (const [level, threshold] of Object.entries(securityConfig.thresholds)) {
6886
+ if (typeof threshold === 'number') {
6887
+ metrics += `fingerprint_security_threshold{level="${level}"} ${threshold}\n`;
6888
+ }
6889
+ }
6890
+ }
6891
+
6892
+ if (lastBestSolution && lastBestSolution.objectives) {
6893
+ 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`;
6894
+ 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`;
6895
+ }
6896
+
6897
+ return metrics;
6898
+ }
6556
6899
  };
6557
6900
 
6558
6901
  /**