@anonympins/fingerprint 0.5.0 → 0.5.1

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,25 @@
1
+ ## Version 0.5.1
2
+
3
+ ### ✨ New Features
4
+
5
+ - **GPU Proof-of-Work (PoW) Challenge**: Introduced a highly parallelized chaotic logistic map float computation challenge utilizing WebGPU (with a fallback to WebGL2). This challenge is specifically designed to exhaust CPU-based headless emulators (such as SwiftShader). It includes sample-based server-side verification to prevent DoS vectors.
6
+ - **Biometric Keystroke Dynamics (Dwell & Flight Times)**: Introduced advanced behavioral biometric tracking by measuring key press duration (*dwell time*) and key-to-key transition intervals (*flight time*) to build a unique digraph/trigraph motor profile for the user.
7
+ - *Why it's a Killer Feature*: Automated text-injecting bots often simulate simple randomized delays between characters, but they fail to replicate natural human muscle memory patterns (such as ultra-fast cognitive transitions between adjacent keys on physical or virtual layouts). Server-side statistical checks (utilizing standard deviation, variance thresholds, and Benford's Law) immediately flag these robotic, uniform input patterns.
8
+ - **Stealthy Honeypot Traps (Shadow DOM)**: Implemented a new honeypot mechanism that conceals trap links and form fields within a closed Shadow DOM, with dynamic rendering styles calculated by nested CSS variables. This makes them invisible to legitimate users and standard browser automation tools, but highly detectable by bots that inject specific JS or use complex selectors, significantly increasing their behavioral signature.
9
+ - **Hot-Reloadable Security Configuration**: Enabled dynamic, in-memory updates of security configurations (weights, thresholds, patterns) without requiring a server restart. This ensures continuous adaptability to evolving threats (e.g., DDoS L7, scraping campaigns) without service interruption or latency.
10
+
11
+ ### 🚀 Improvements
12
+
13
+ - **Display & Protocol Anomaly Scoring**: Fully integrated `renderingAnomalyScore` and `quicAnomalyScore` across all backend engines (Node.js, PHP, Python). This enables real-time detection of virtual software framebuffers (like `Xvfb`) lacking physical V-Sync through jitter analysis, as well as HTTP/3 stream setting inconsistencies.
14
+ - **Security Profile Tuning**: Integrated display and QUIC anomaly detectors into the default security profiles (`balanced`, `strict`, `blog`, `ecommerce`) with custom weights.
15
+ - **Layout-Agnostic Client Tracking**: Enhanced the keystroke dynamics tracker to prioritize physical key locations (`KeyboardEvent.code`) over localized characters (`KeyboardEvent.key`), ensuring robust detection across different keyboard layouts (QWERTY, AZERTY) and virtual mobile keyboards.
16
+
17
+ ### 🐛 Bug Fixes
18
+
19
+ - **Node.js 24 Test Suite Compatibility**: Fixed unit test suite execution and environment configuration issues specifically encountered on Node.js 24.
20
+
21
+ ---
22
+
1
23
  ## Version 0.5.0
2
24
 
3
25
  ### ✨ New Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anonympins/fingerprint",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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",
@@ -340,6 +340,52 @@ const ClientLibrary = {
340
340
  document.addEventListener('touchend', handleTouch, { passive: true });
341
341
  },
342
342
 
343
+ /**
344
+ * Démarre le suivi de la régularité d'affichage (V-Sync/rAF) pour détecter les framebuffers logiciels sans V-Sync.
345
+ */
346
+ startRenderingTracker() {
347
+ if (this._renderingTrackerAttached) return;
348
+ this._renderingTrackerAttached = true;
349
+
350
+ if (typeof window === 'undefined' || !window.requestAnimationFrame) return;
351
+
352
+ const rAfTimestamps = [];
353
+ let lastTime = performance.now();
354
+ const maxSamples = 15;
355
+
356
+ const checkOffscreenAnom = () => {
357
+ try {
358
+ if ('OffscreenCanvas' in window && HTMLCanvasElement.prototype.transferControlToOffscreen) {
359
+ const nativeToString = Function.prototype.toString.call(HTMLCanvasElement.prototype.transferControlToOffscreen);
360
+ return !nativeToString.includes('[native code]');
361
+ }
362
+ } catch (e) {}
363
+ return false;
364
+ };
365
+
366
+ const loop = (time) => {
367
+ const delta = time - lastTime;
368
+ lastTime = time;
369
+ if (rAfTimestamps.length < maxSamples) {
370
+ if (rAfTimestamps.length > 0) { // Skip first delta
371
+ rAfTimestamps.push(delta);
372
+ }
373
+ window.requestAnimationFrame(loop);
374
+ } else {
375
+ const avg = rAfTimestamps.reduce((a, b) => a + b, 0) / rAfTimestamps.length;
376
+ const sqDiffs = rAfTimestamps.map(v => Math.pow(v - avg, 2));
377
+ const avgSqDiff = sqDiffs.reduce((a, b) => a + b, 0) / sqDiffs.length;
378
+
379
+ metrics.rendering = {
380
+ fps: Math.round((1000 / avg) * 100) / 100,
381
+ jitter: Math.round(Math.sqrt(avgSqDiff) * 100) / 100,
382
+ offscreenAnom: checkOffscreenAnom()
383
+ };
384
+ }
385
+ };
386
+ window.requestAnimationFrame(loop);
387
+ },
388
+
343
389
  /**
344
390
  * Initialise l'espace Proof-of-Space persistant dans l'IndexedDB locale.
345
391
  */
@@ -449,27 +495,77 @@ const ClientLibrary = {
449
495
  },
450
496
 
451
497
  /**
452
- * Démarre le suivi de la dynamique de frappe pour calculer la latence.
498
+ * Démarre le suivi de la dynamique de frappe pour calculer le dwell time et le flight time (digraphie/trigraphie).
453
499
  * À appeler une fois sur la page.
454
500
  */
455
501
  startKeystrokeDynamicsTracker() {
456
502
  // S'assurer de ne pas attacher l'écouteur plusieurs fois
457
- if (keystrokeTimestamps.length > 0) return;
503
+ if (this._keystrokeTrackerAttached) return;
504
+ this._keystrokeTrackerAttached = true;
458
505
 
459
- document.addEventListener('keydown', () => {
506
+ const activeKeys = new Map();
507
+ let lastKeyDownTime = 0;
508
+ let lastKeyName = '';
509
+
510
+ document.addEventListener('keydown', (e) => {
460
511
  const now = performance.now();
512
+ const key = e.key;
513
+ const code = e.code;
514
+ if (!key && !code) return;
515
+
516
+ const keyIdentifier = code || key;
517
+
518
+ // Prevent key repeat triggering multiple events
519
+ if (activeKeys.has(keyIdentifier)) return;
520
+ activeKeys.set(keyIdentifier, now);
521
+
461
522
  if (keystrokeTimestamps.length > 0) {
462
523
  const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
463
524
  const latency = now - lastTimestamp;
464
- // On ignore les latences irréalistes (trop longues ou trop courtes)
465
- if (latency > 10 && latency < 2000) { // Augmenté à 2s
525
+ if (latency > 10 && latency < 2000) {
466
526
  if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
467
- keystrokeLatencies.shift(); // Garder la taille de l'historique
527
+ keystrokeLatencies.shift();
468
528
  }
469
529
  keystrokeLatencies.push(latency);
470
530
  }
471
531
  }
472
532
  keystrokeTimestamps.push(now);
533
+
534
+ // Flight Time (KeyDown to KeyDown)
535
+ if (lastKeyDownTime > 0) {
536
+ const flightTime = now - lastKeyDownTime;
537
+ if (flightTime > 10 && flightTime < 2000) {
538
+ if (keystrokeFlightTimes.length >= KEYSTROKE_HISTORY_MAX) {
539
+ keystrokeFlightTimes.shift();
540
+ }
541
+ const digraph = lastKeyName ? this._hasher(lastKeyName + "_" + keyIdentifier).toString() : "unknown";
542
+ keystrokeFlightTimes.push({ digraph, time: flightTime });
543
+ }
544
+ }
545
+ lastKeyDownTime = now;
546
+ lastKeyName = keyIdentifier;
547
+ }, {passive: true});
548
+
549
+ document.addEventListener('keyup', (e) => {
550
+ const now = performance.now();
551
+ const key = e.key;
552
+ const code = e.code;
553
+ if (!key && !code) return;
554
+
555
+ const keyIdentifier = code || key;
556
+
557
+ if (activeKeys.has(keyIdentifier)) {
558
+ const pressTime = activeKeys.get(keyIdentifier);
559
+ const dwellTime = now - pressTime;
560
+ activeKeys.delete(keyIdentifier);
561
+
562
+ if (dwellTime > 5 && dwellTime < 1000) {
563
+ if (keystrokeDwellTimes.length >= KEYSTROKE_HISTORY_MAX) {
564
+ keystrokeDwellTimes.shift();
565
+ }
566
+ keystrokeDwellTimes.push(dwellTime);
567
+ }
568
+ }
473
569
  }, {passive: true});
474
570
  },
475
571
 
@@ -509,22 +605,75 @@ const ClientLibrary = {
509
605
  });
510
606
  activeHoneypotListeners.clear();
511
607
 
512
- // 2. Ajouter les nouveaux écouteurs
608
+ // 2. Ajouter les nouveaux écouteurs sur le DOM classique
513
609
  honeypotFieldNames.forEach(fieldName => {
514
610
  const field = document.querySelector(`[name="${fieldName}"]`);
515
611
  if (field) {
516
- // On utilise une fonction nommée (ou une référence) pour pouvoir la supprimer plus tard.
517
- // L'option { once: true } est excellente, mais pour une réinitialisation complète,
518
- // il est plus propre de gérer le nettoyage nous-mêmes.
519
612
  const listener = () => {
520
613
  this.onHoneypotTrigger();
521
- // Se supprime lui-même après exécution, comme { once: true }
522
614
  field.removeEventListener('input', listener);
523
615
  };
524
616
  field.addEventListener('input', listener);
525
617
  activeHoneypotListeners.set(field, listener); // On stocke la référence
526
618
  }
527
619
  });
620
+
621
+ // 3. Générer des champs d'input pièges masqués dans un Shadow DOM fermé
622
+ if (typeof document !== 'undefined' && honeypotFieldNames.length > 0) {
623
+ const host = document.createElement('div');
624
+ host.setAttribute('aria-hidden', 'true');
625
+ host.style.position = 'absolute';
626
+ host.style.width = '0';
627
+ host.style.height = '0';
628
+ host.style.overflow = 'hidden';
629
+
630
+ const shadow = host.attachShadow({ mode: 'closed' });
631
+
632
+ const style = document.createElement('style');
633
+ style.textContent = `
634
+ :host {
635
+ --trap-pos-state: absolute;
636
+ --trap-off-val: -9999px;
637
+ --trap-vis-state: hidden;
638
+ --trap-scale-val: 0;
639
+ }
640
+ .shadow-form-wrapper {
641
+ position: var(--trap-pos-state);
642
+ left: var(--trap-off-val);
643
+ top: var(--trap-off-val);
644
+ visibility: var(--trap-vis-state);
645
+ transform: scale(var(--trap-scale-val));
646
+ }
647
+ `;
648
+ shadow.appendChild(style);
649
+
650
+ const wrapper = document.createElement('div');
651
+ wrapper.className = 'shadow-form-wrapper';
652
+
653
+ honeypotFieldNames.forEach(fieldName => {
654
+ const label = document.createElement('label');
655
+ label.textContent = fieldName;
656
+ const input = document.createElement('input');
657
+ input.type = 'text';
658
+ input.name = fieldName;
659
+ input.tabIndex = -1;
660
+ input.autocomplete = 'off';
661
+
662
+ const trigger = () => {
663
+ this.onHoneypotTrigger();
664
+ };
665
+
666
+ input.addEventListener('input', trigger, { passive: true });
667
+ input.addEventListener('change', trigger, { passive: true });
668
+ input.addEventListener('focus', trigger, { passive: true });
669
+
670
+ wrapper.appendChild(label);
671
+ wrapper.appendChild(input);
672
+ });
673
+
674
+ shadow.appendChild(wrapper);
675
+ document.body.appendChild(host);
676
+ }
528
677
  },
529
678
 
530
679
  /**
@@ -544,6 +693,10 @@ const ClientLibrary = {
544
693
  // NOUVEAU: Inclure l'historique des mouvements de la souris pour une analyse côté serveur.
545
694
  metrics.mouseMovementsHistory = mouseMovementsHistory;
546
695
 
696
+ // NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
697
+ metrics.keystrokeDwellTimes = keystrokeDwellTimes;
698
+ metrics.keystrokeFlightTimes = keystrokeFlightTimes;
699
+
547
700
  // Calcule la latence moyenne des frappes
548
701
  if (keystrokeLatencies.length > 0) {
549
702
  const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
@@ -672,24 +825,61 @@ const ClientLibrary = {
672
825
  return;
673
826
  }
674
827
 
675
- const trapContainer = document.createElement('div');
676
- trapContainer.setAttribute('aria-hidden', 'true');
677
- trapContainer.style.position = 'absolute';
678
- trapContainer.style.left = '-9999px';
679
- trapContainer.style.top = '-9999px';
680
- trapContainer.style.transform = 'scale(0)';
681
- trapContainer.style.pointerEvents = 'none';
828
+ const host = document.createElement('div');
829
+ host.setAttribute('aria-hidden', 'true');
830
+ host.style.position = 'absolute';
831
+ host.style.width = '0';
832
+ host.style.height = '0';
833
+ host.style.overflow = 'hidden';
834
+
835
+ const shadow = host.attachShadow({ mode: 'closed' });
836
+
837
+ const style = document.createElement('style');
838
+ style.textContent = `
839
+ :host {
840
+ --trap-layout-pos: absolute;
841
+ --trap-offset-val: -9999px;
842
+ --trap-visibility-state: hidden;
843
+ --trap-scale-factor: 0;
844
+ --trap-ptr-events: none;
845
+ }
846
+ .shadow-trap-wrapper {
847
+ position: var(--trap-layout-pos);
848
+ left: var(--trap-offset-val);
849
+ top: var(--trap-offset-val);
850
+ visibility: var(--trap-visibility-state);
851
+ transform: scale(var(--trap-scale-factor));
852
+ pointer-events: var(--trap-ptr-events);
853
+ }
854
+ a {
855
+ color: transparent;
856
+ text-decoration: none;
857
+ }
858
+ `;
859
+ shadow.appendChild(style);
860
+
861
+ const wrapper = document.createElement('div');
862
+ wrapper.className = 'shadow-trap-wrapper';
682
863
 
683
- urls.forEach((url,i) => {
864
+ urls.forEach((url, i) => {
684
865
  const link = document.createElement('a');
685
866
  link.href = url;
686
867
  link.rel = 'nofollow';
687
- link.tabIndex = -1; // Make it unfocusable
688
- link.innerHTML = `<span>&gt; ${i+1}</span>`; // SEO-insignificant content
689
- trapContainer.appendChild(link);
868
+ link.tabIndex = -1;
869
+ link.innerHTML = `<span>&gt; ${i + 1}</span>`;
870
+
871
+ const trigger = () => {
872
+ this.onHoneypotTrigger();
873
+ };
874
+ link.addEventListener('click', trigger, { passive: true });
875
+ link.addEventListener('focus', trigger, { passive: true });
876
+ link.addEventListener('mouseover', trigger, { passive: true });
877
+
878
+ wrapper.appendChild(link);
690
879
  });
691
880
 
692
- document.body.appendChild(trapContainer);
881
+ shadow.appendChild(wrapper);
882
+ document.body.appendChild(host);
693
883
  },
694
884
  /**
695
885
  * Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
@@ -749,6 +939,7 @@ const ClientLibrary = {
749
939
  keystrokes = true,
750
940
  clicks = true, // Add new option
751
941
  touches = true, // Nouveau paramètre tactiles
942
+ rendering = true,
752
943
  phantomTraps = true, // NOUVEAU
753
944
  honeypots = [],
754
945
  trapUrls = [], // Nouveau paramètre pour les URL pièges
@@ -773,6 +964,9 @@ const ClientLibrary = {
773
964
  if (touches) {
774
965
  this.startTouchEventTracker();
775
966
  }
967
+ if (rendering) {
968
+ this.startRenderingTracker();
969
+ }
776
970
  if (phantomTraps) {
777
971
  this.injectPhantomTraps();
778
972
  }
@@ -941,6 +1135,7 @@ const metrics = {
941
1135
  honeypotInteraction: false,
942
1136
  historyLength: 0,
943
1137
  clientTimestamp: 0,
1138
+ rendering: { fps: 0, jitter: 0, offscreenAnom: false },
944
1139
  };
945
1140
 
946
1141
  let lastMousePos = { x: 0, y: 0 };
@@ -953,6 +1148,8 @@ const CLICKS_HISTORY_MAX = 50;
953
1148
  let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
954
1149
  let keystrokeTimestamps = [];
955
1150
  let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
1151
+ let keystrokeDwellTimes = [];
1152
+ let keystrokeFlightTimes = [];
956
1153
  const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
957
1154
 
958
1155
 
@@ -966,6 +1163,7 @@ export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.b
966
1163
  export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
967
1164
  export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
968
1165
  export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
1166
+ export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
969
1167
  export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
970
1168
  export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
971
1169
  export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
@@ -189,7 +189,82 @@ export function generateStatelessTicket(payload) {
189
189
  const signature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
190
190
  return `${base64UrlEncode(iv)}.${base64UrlEncode(encrypted)}.${base64UrlEncode(signature)}`;
191
191
  }
192
+ /**
193
+ * Détecte les anomalies de flux QUIC/HTTP3 par rapport au User-Agent.
194
+ * @private
195
+ * @param {object} context - Le contexte de la requête.
196
+ * @returns {{quicAnomalyScore: number}}
197
+ */
198
+ function getQuicAnomalyScore(context) {
199
+ const quicFp = context.headers?.['x-quic-fp'] || context.quicFingerprint || null;
200
+ if (!quicFp || typeof quicFp !== 'string') {
201
+ return { quicAnomalyScore: 0.0 };
202
+ }
203
+
204
+ const parts = quicFp.split(';');
205
+ if (parts.length < 2) return { quicAnomalyScore: 0.0 };
206
+
207
+ const params = {};
208
+ parts[1].split(',').forEach(p => {
209
+ const kv = p.split('=');
210
+ if (kv.length === 2) params[kv[0]] = kv[1];
211
+ });
212
+ const priorityOrder = parts[2] || '';
213
+
214
+ const ua = context.headers?.['user-agent'] || '';
215
+ const uaParts = parseUserAgent(ua);
216
+ const browser = uaParts.browser;
217
+
218
+ if (!browser) return { quicAnomalyScore: 0.0 };
192
219
 
220
+ let anomaly = 0.0;
221
+ if (browser.startsWith('Chrome') || browser.startsWith('Edge')) {
222
+ const maxData = parseInt(params['1'] || '0', 10);
223
+ const maxStreams = parseInt(params['4'] || '0', 10);
224
+ if (maxData > 0 && maxData < 1048576) anomaly += 40.0;
225
+ if (maxStreams > 0 && maxStreams !== 100) anomaly += 30.0;
226
+ if (priorityOrder && !priorityOrder.includes('u=')) anomaly += 30.0;
227
+ } else if (browser.startsWith('Firefox')) {
228
+ const maxData = parseInt(params['1'] || '0', 10);
229
+ if (maxData > 0 && maxData > 5000000) anomaly += 40.0;
230
+ }
231
+
232
+ return { quicAnomalyScore: Math.max(0.0, Math.min(100.0, anomaly)) };
233
+ }
234
+ /**
235
+ * Détecte les anomalies de rendu (V-Sync, FPS, gigue) à partir des métriques d'affichage.
236
+ * @private
237
+ * @param {object} context - Le contexte de la requête.
238
+ * @returns {{renderingAnomalyScore: number}}
239
+ */
240
+ function getRenderingAnomalyScore(context) {
241
+ const behaviorHeader = context.headers?.['x-behavior-metrics'];
242
+ if (!behaviorHeader) {
243
+ return { renderingAnomalyScore: 0.0 };
244
+ }
245
+ try {
246
+ const metrics = JSON.parse(behaviorHeader);
247
+ if (!metrics || !metrics.rendering) {
248
+ return { renderingAnomalyScore: 0.0 };
249
+ }
250
+ const rendering = metrics.rendering;
251
+ let score = 0.0;
252
+ if (rendering.offscreenAnom) {
253
+ score += 100.0;
254
+ }
255
+ const fps = parseFloat(rendering.fps || 0.0);
256
+ const jitter = parseFloat(rendering.jitter || 0.0);
257
+ if (fps > 250.0 || (fps > 0.0 && fps < 15.0)) {
258
+ score += 50.0;
259
+ }
260
+ if (jitter > 6.0) {
261
+ score += Math.min(80.0, (jitter - 6.0) * 10.0);
262
+ }
263
+ return { renderingAnomalyScore: Math.min(100.0, score) };
264
+ } catch (e) {
265
+ return { renderingAnomalyScore: 0.0 };
266
+ }
267
+ }
193
268
  export function parseStatelessTicket(ticket) {
194
269
  try {
195
270
  if (ticket.startsWith('ed25519.')) {
@@ -304,7 +379,10 @@ const securityProfiles = {
304
379
  tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
305
380
  subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
306
381
  ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
307
- botnetClusterScore: 0.6 // NOUVEAU: Poids pour le clustering botnet
382
+ botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
383
+ tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
384
+ quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
385
+ renderingAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie de rendu
308
386
  },
309
387
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
310
388
  patterns: {
@@ -340,7 +418,8 @@ const securityProfiles = {
340
418
  tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
341
419
  subnetScore: 0.5,
342
420
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
343
- botnetClusterScore: 0.8 // NOUVEAU: Poids pour le clustering botnet
421
+ botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
422
+ renderingAnomalyScore: 1.0 // NOUVEAU: Poids pour l'anomalie de rendu
344
423
  },
345
424
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
346
425
  patterns: {
@@ -377,7 +456,9 @@ const securityProfiles = {
377
456
  tlsSpoofingScore: 0.7, // Important pour les API
378
457
  subnetScore: 0.4,
379
458
  ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
380
- botnetClusterScore: 0.7 // NOUVEAU: Poids pour le clustering botnet
459
+ botnetClusterScore: 0.7, // NOUVEAU: Poids pour le clustering botnet
460
+ tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
461
+ quicAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie QUIC
381
462
  },
382
463
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
383
464
  patterns: {
@@ -415,7 +496,10 @@ const securityProfiles = {
415
496
  tlsSpoofingScore: 0.6, // Moins critique pour les blogs
416
497
  subnetScore: 0.2,
417
498
  ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
418
- botnetClusterScore: 0.5 // NOUVEAU: Poids pour le clustering botnet
499
+ botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
500
+ tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
501
+ quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
502
+ renderingAnomalyScore: 0.5 // NOUVEAU: Poids pour l'anomalie de rendu
419
503
  },
420
504
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
421
505
  patterns: {
@@ -452,7 +536,10 @@ const securityProfiles = {
452
536
  tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
453
537
  subnetScore: 0.5,
454
538
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
455
- botnetClusterScore: 0.9 // NOUVEAU: Poids pour le clustering botnet
539
+ botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
540
+ tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
541
+ quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
542
+ renderingAnomalyScore: 0.9 // NOUVEAU: Poids pour l'anomalie de rendu
456
543
  },
457
544
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
458
545
  patterns: {
@@ -1600,6 +1687,41 @@ function getBehaviorScore(context) {
1600
1687
  if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
1601
1688
  if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
1602
1689
 
1690
+ // NOUVEAU: Analyse de digraphie/trigraphie (dwell & flight times)
1691
+ const dwellTimes = metrics.keystrokeDwellTimes || [];
1692
+ const flightTimes = metrics.keystrokeFlightTimes || [];
1693
+
1694
+ if (dwellTimes.length >= 5) {
1695
+ const meanDwell = dwellTimes.reduce((a, b) => a + b, 0) / dwellTimes.length;
1696
+ const varDwell = dwellTimes.reduce((a, b) => a + Math.pow(b - meanDwell, 2), 0) / dwellTimes.length;
1697
+ const stdDevDwell = Math.sqrt(varDwell);
1698
+
1699
+ if (stdDevDwell < 2.0) {
1700
+ score += 35; // Suspicion d'automatisation (pas de variation humaine de pression)
1701
+ }
1702
+ if (meanDwell < 15.0) {
1703
+ score += 25; // Dwell time irréaliste
1704
+ }
1705
+ }
1706
+
1707
+ if (flightTimes.length >= 5) {
1708
+ const times = flightTimes.map(f => f.time);
1709
+ const meanFlight = times.reduce((a, b) => a + b, 0) / times.length;
1710
+ const varFlight = times.reduce((a, b) => a + Math.pow(b - meanFlight, 2), 0) / times.length;
1711
+ const stdDevFlight = Math.sqrt(varFlight);
1712
+
1713
+ if (stdDevFlight < 3.0) {
1714
+ score += 35; // Pas de variation de transition (flight time robotique)
1715
+ }
1716
+ if (meanFlight < 25.0) {
1717
+ score += 25; // Transitions trop rapides
1718
+ }
1719
+ const benfordDev = Optimization.Operators.benfordTest(times);
1720
+ if (benfordDev > 0.18) {
1721
+ score += 30; // Les intervalles ne suivent pas la loi de Benford
1722
+ }
1723
+ }
1724
+
1603
1725
  // 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
1604
1726
  if (segments.length > 10) {
1605
1727
  const benfordDeviation = Optimization.Operators.benfordTest(segments);
@@ -2766,8 +2888,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
2766
2888
  const stableFp = extractStablePart(currentDeviceHash);
2767
2889
  const stableFpHash = cyrb53(stableFp).toString();
2768
2890
  const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
2769
-
2770
- const { tcpAnomalyScore } = getTcpAnomalyScore(context);
2891
+ const { tcpAnomalyScore } = getTcpAnomalyScore(context);
2892
+ const { quicAnomalyScore } = getQuicAnomalyScore(context);
2893
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
2771
2894
 
2772
2895
  // Save the updated device state to the store
2773
2896
  // Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
@@ -2779,7 +2902,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
2779
2902
  deviceData.ips = new Set(deviceData.ips);
2780
2903
  }
2781
2904
  // Le vecteur de suspicion est maintenant complet.
2782
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore };
2905
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore };
2783
2906
  };
2784
2907
 
2785
2908
  // A residential user can change networks (home, 4G, public wifi).
@@ -3144,6 +3267,18 @@ export class FingerprintEngine {
3144
3267
  this.dryRun = finalConfig.dryRun || false;
3145
3268
  }
3146
3269
 
3270
+ /**
3271
+ * Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
3272
+ * sans nécessiter de redémarrage.
3273
+ * @param {object} newConfig - La nouvelle configuration partielle ou complète.
3274
+ */
3275
+ updateConfig(newConfig) {
3276
+ this._validateConfig(newConfig);
3277
+ this.securityConfig = deepMerge(this.securityConfig, newConfig);
3278
+ this.dryRun = this.securityConfig.dryRun || false;
3279
+ this._log('Configuration mise à jour à chaud (Hot-Reloaded)', this.securityConfig);
3280
+ }
3281
+
3147
3282
  /**
3148
3283
  * Validates the security configuration object to detect potential typos or missing essential keys.
3149
3284
  * @private
@@ -3208,7 +3343,9 @@ export class FingerprintEngine {
3208
3343
  (suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
3209
3344
  (suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
3210
3345
  (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
3211
- (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0);
3346
+ (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
3347
+ (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
3348
+ (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
3212
3349
 
3213
3350
  return Math.min(100, score);
3214
3351
  }
@@ -5186,6 +5323,8 @@ export const __internal = {
5186
5323
  parseTcpSyn, // Expose for testing
5187
5324
  classifyTcpOs, // Expose for testing
5188
5325
  getTcpAnomalyScore, // Expose for testing,
5326
+ getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
5327
+ getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
5189
5328
  registerCooperativeNode,
5190
5329
  findPeerInSubnet,
5191
5330
  handleCooperativeRequest