@anonympins/fingerprint 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,6 +63,8 @@ export POW_SECRET="your_secret_key_of_at_least_32_characters"
63
63
 
64
64
  The `powMiddleware` requires a configuration object defining the weights of suspicion indicators and the challenge trigger thresholds.
65
65
 
66
+ All following `securityConfig` parameters are optional.
67
+
66
68
  ```javascript
67
69
  import express from 'express';
68
70
  import bodyParser from 'body-parser';
@@ -110,11 +112,15 @@ const securityConfig = {
110
112
  verbose: process.env.NODE_ENV !== 'production', // Log detailed info in development, but not in production.
111
113
  patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
112
114
  velocityThreshold: 800, // ms between requests to be considered "fast"
113
- burstThreshold: 1500, // ms for identical requests to be a "burst"
115
+ burstThreshold: 1500, // ms for identical requests to be a "burst"
114
116
  scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
115
117
  historySize: 10, // Number of requests to keep for pattern analysis
116
- decayFactor: 0.9, // How quickly the pattern score decays over time
117
- inactivityReset: 30000, // ms of inactivity after which the pattern score is reset
118
+ minSamples: 5, // Minimum number of timings to collect before statistical analysis.
119
+ regularityThreshold: 50, // Standard deviation (ms) below which behavior is "too regular".
120
+ benfordThreshold: 0.15, // Benford's Law deviation threshold above which the distribution is "unnatural".
121
+ patternWeight: 80, // Strong, one-time penalty when a pattern is detected.
122
+ decayFactor: 0.9, // Factor by which the pattern score decreases over time.
123
+ inactivityReset: 5000, // Time (ms) after which the pattern score is reset.
118
124
  },
119
125
  honeypot: {
120
126
  // List of field names that are traps for bots.
@@ -156,6 +162,9 @@ const securityConfig = {
156
162
  '203.0.113.0/24', // A partner's network range
157
163
  '2001:db8::/32' // An IPv6 range
158
164
  ]},
165
+ { type: 'hostname_allowlist', entries: [
166
+ 'google.com', // A specific hostname
167
+ ]},
159
168
  // Option 2: DNS-verified bots (e.g., search engine crawlers).
160
169
  // This uses a secure DNS lookup (reverse then forward) to verify the bot's identity.
161
170
  // The result is cached per IP to avoid repeated DNS lookups.
@@ -173,7 +182,8 @@ const securityConfig = {
173
182
  autotuning: {
174
183
  trafficData: trafficData, // The data source for the genetic algorithm.
175
184
  interval: 1800000, // Optimization cycle every 30 minutes (in ms).
176
- minDataPoints: 200 // Minimum requests before starting an optimization cycle.
185
+ minDataPoints: 200, // Minimum requests before starting an optimization cycle.
186
+ maxDataPoints: 20000 // Minimum requests before starting an optimization cycle.
177
187
  },
178
188
  // Enables problem solving for suspicious activity (configurable in problems.config.json)
179
189
  enableUsefulWork: true
@@ -540,6 +550,48 @@ While `powMiddleware` is convenient for Express, you can use the `FingerprintEng
540
550
 
541
551
  The engine is a named export from the main module.
542
552
 
553
+ ### Useful Proof-of-Work (`ProblemManager`)
554
+
555
+ Instead of issuing a generic Proof-of-Work, the system can dispatch a "useful" computational problem to a suspicious client. This allows harnessing the client's CPU cycles to solve complex problems (like optimization tasks) over time. This feature is managed by the `ProblemManager` class, which is enabled via the `enableUsefulWork: true` flag in the security configuration.
556
+
557
+ The `ProblemManager` reads its configuration from `problems.config.json`, which defines the problems to be solved, the type of work units, and the current state of the solutions.
558
+
559
+ While you typically won't interact with it directly, its methods are exported and can be used for monitoring or manual administration. The main instance is exported as `problemManager`.
560
+
561
+ #### `problemManager.dispatchWork(suspicionFactor)`
562
+
563
+ Selects a problem and generates a work unit for a client. The difficulty of the task (e.g., number of iterations) is scaled based on the client's `suspicionFactor`.
564
+
565
+ * **`suspicionFactor`** (`number`): A factor to adjust the difficulty of the work unit.
566
+ * **Returns**: (`object|null`) An object containing the `problemId` and the `task` to be sent to the client, or `null` if no problems are available.
567
+
568
+ #### `problemManager.integrateSolution(problemId, solutionData)`
569
+
570
+ Integrates a solution returned by a client into the problem's state. If the new solution is better than the existing one, it is saved as the new best solution.
571
+
572
+ * **`problemId`** (`string`): The ID of the problem being updated.
573
+ * **`solutionData`** (`object`): The solution data returned by the client (e.g., `{ solution, energy }`).
574
+
575
+ #### `problemManager.getBestSolutions([problemId])`
576
+
577
+ Retrieves the best solution currently known for one or all problems. This is useful for creating an API endpoint to view the progress of the distributed computation.
578
+
579
+ * **`problemId`** (`string`, optional): The ID of a specific problem.
580
+ * **Returns**: (`object|Array<object>|null`)
581
+ * If a `problemId` is provided, it returns an object with the best solution for that problem (`{ id, solution, score, lastUpdate }`).
582
+ * If no `problemId` is provided, it returns an array of these objects for all problems.
583
+
584
+ **Example: Creating an API endpoint to view solutions**
585
+
586
+ ```javascript
587
+ import { problemManager } from './fingerprint.js'; // Adjust path
588
+
589
+ app.get('/api/problems/solutions', (req, res) => {
590
+ const solutions = problemManager.getBestSolutions();
591
+ res.json(solutions);
592
+ });
593
+ ```
594
+
543
595
  **Workflow:**
544
596
 
545
597
  1. **Instantiate the Engine**: Create an instance with your `securityConfig`.
@@ -348,25 +348,13 @@ const ClientLibrary = {
348
348
  console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
349
349
  // L'empreinte de l'appareil qui résout le challenge est cruciale.
350
350
  const solverFp = this.getDeviceFingerprint();
351
- const solution = await solveChallenge(challengeData.challenge, solverFp);
351
+ const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
352
352
  console.log('[Fingerprint] Challenge solved. Retrying original request.');
353
353
 
354
354
  // Ajouter la solution aux paramètres de la requête pour le nouvel essai
355
355
  const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
356
- // Le type de challenge est maintenant `cpu_mem` pour les API
357
- url.searchParams.set('pow_type', 'cpu_mem');
358
- url.searchParams.set('pow_nonce', challengeData.challenge.nonce);
359
-
360
- // La solution est un objet { cpu: ..., mem: ... }. Le serveur attend pow_solution_cpu et pow_solution_mem.
361
- Object.entries(solution).forEach(([key, value]) => {
362
- url.searchParams.set(`pow_solution_${key}`, String(value));
363
- });
364
-
365
- // Pour le challenge de travail utile
366
- if (solution.work_result) {
367
- url.searchParams.set('pow_solution_work_result', JSON.stringify(solution.work_result));
368
- url.searchParams.set('pow_problem_id', solution.problem_id);
369
- }
356
+ // La logique de formatage est maintenant cachée dans la classe ChallengeSolution.
357
+ solutionWrapper.applyToUrl(url);
370
358
 
371
359
  // On ajoute l'empreinte du solveur à la requête de réessai.
372
360
  url.searchParams.set('pow_fp', solverFp);
package/fingerprint.js CHANGED
@@ -822,18 +822,15 @@ function getCrossLayerInconsistency(context) {
822
822
  function getRequestPatternScore(context, deviceData, patternConfig = {}) {
823
823
  if (!deviceData) return { requestPatternScore: 0 };
824
824
 
825
- // Default values for the pattern detection logic, which can be overridden by the auto-tuner.
825
+ // (NOUVEAU) Logique de détection de pattern simplifiée et unifiée.
826
826
  const {
827
- velocityThreshold = 800, velocityWeight = 30,
828
- burstThreshold = 1500, burstWeight = 50,
829
- scrapeThreshold = 1000, scrapeWeight = 20, scrapeBurstWeight = 40,
830
- historySize = 10,
831
- decayFactor = 0.9,
832
- inactivityReset = 30000, // 30 secondes
833
- // Nouveaux paramètres pour l'analyse de distribution
834
- benfordMinSamples = 15, benfordWeight = 50,
835
- // Nouveau paramètre pour la détection de séquences
836
- sequenceLength = 3, sequenceWeight = 60
827
+ historySize = 20, // Nombre de requêtes à conserver pour l'analyse.
828
+ minSamples = 10, // Nombre d'intervalles de temps à analyser avant de calculer.
829
+ regularityThreshold = 150, // Écart-type (ms) en dessous duquel le comportement est "trop régulier".
830
+ benfordThreshold = 0.15, // Seuil de déviation de Benford au-dessus duquel la distribution est "non naturelle".
831
+ patternWeight = 80, // Pénalité FORTE et unique si un pattern est détecté.
832
+ decayFactor = 0.95, // Décroissance du score dans le temps.
833
+ inactivityReset = 180000 // Réinitialisation du score après 3 minutes d'inactivité.
837
834
  } = patternConfig;
838
835
 
839
836
  const now = Date.now();
@@ -846,91 +843,65 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
846
843
  params.sort(); // Sort for deterministic order
847
844
  const currentQueryString = params.toString();
848
845
 
849
- // Initialize request history if it doesn't exist
850
846
  if (!deviceData.requestHistory) deviceData.requestHistory = [];
851
- // NOUVEAU: S'assurer que timingHistory est toujours initialisé.
852
- // Cette vérification est séparée car deviceData peut exister avec requestHistory mais sans timingHistory.
853
847
  if (!deviceData.timingHistory) deviceData.timingHistory = [];
854
848
 
855
849
  const history = deviceData.requestHistory;
856
- let score = 0;
857
-
858
- // --- Analyze patterns based on the last few requests ---
859
- if (history.length > 0) {
860
- const lastRequest = history[history.length - 1];
861
- const timeSinceLast = now - lastRequest.timestamp;
850
+ const lastRequest = history.length > 0 ? history[history.length - 1] : null;
851
+ const timeSinceLast = lastRequest ? now - lastRequest.timestamp : Infinity;
862
852
 
863
- // Stocker le délai pour l'analyse de distribution
853
+ // Mise à jour de l'historique
854
+ history.push({
855
+ timestamp: now,
856
+ path: currentPath,
857
+ queryString: currentQueryString,
858
+ });
859
+ if (lastRequest) {
864
860
  deviceData.timingHistory.push(timeSinceLast);
861
+ }
865
862
 
866
- // 1. Velocity Check: Penalize requests that are too fast to be human.
867
- if (timeSinceLast < velocityThreshold) { // 150 < 200 -> true
868
- score += velocityWeight; // score = 30
869
- }
870
-
871
- // 2. Burst Check: Add additional penalty for identical requests in a very short time frame.
872
- if (currentPath === lastRequest.path && currentQueryString === lastRequest.queryString && timeSinceLast < burstThreshold) { // 150 < 500 -> true
873
- score += burstWeight; // score = 30 + 50 = 80
874
- }
863
+ let instantScore = 0;
864
+ const timings = deviceData.timingHistory;
875
865
 
876
- // 3. Sequential Scraping Check: Add additional penalty for same path with different query params (potential scraping).
877
- // This is a simplified check, now independent of the burst check.
878
- if (currentPath === lastRequest.path && currentQueryString !== lastRequest.queryString && timeSinceLast < scrapeThreshold) {
879
- const previousRequest = history.length > 2 ? history[history.length - 2] : null;
880
- if (previousRequest && previousRequest.path === currentPath) {
881
- score += scrapeBurstWeight; // This is at least the 3rd request in a sequence to the same path.
882
- } else {
883
- score += scrapeWeight; // First sign of a potential scraping pattern
884
- }
885
- }
866
+ // Analyse statistique unifiée si nous avons assez de données
867
+ if (timings.length >= minSamples) {
868
+ const timings = deviceData.timingHistory;
869
+ const mean = timings.reduce((a, b) => a + b, 0) / timings.length;
870
+ const variance = timings.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / timings.length;
871
+ const stdDev = Math.sqrt(variance);
872
+ const benfordDeviation = Optimization.Operators.benfordTest(timings);
886
873
 
887
- // 4. (NOUVEAU) Détection de séquences répétitives (ex: A -> B -> C -> A -> B -> C)
888
- if (history.length >= sequenceLength * 2) {
889
- const lastSequence = history.slice(-sequenceLength);
890
- const previousSequence = history.slice(-sequenceLength * 2, -sequenceLength);
891
-
892
- const isRepeating = lastSequence.every((req, i) =>
893
- req.path === previousSequence[i].path && req.queryString === previousSequence[i].queryString
894
- );
895
- if (isRepeating) score += sequenceWeight;
874
+ // Détection de régularité (bots de type "cron")
875
+ if (stdDev < regularityThreshold) {
876
+ instantScore = patternWeight;
896
877
  }
897
-
898
- // 5. (NOUVEAU) Analyse de la distribution des délais avec la loi de Benford
899
- if (deviceData.timingHistory.length >= benfordMinSamples) {
900
- // On concatène tous les délais en une seule chaîne de chiffres.
901
- const benfordDeviation = Optimization.Operators.benfordTest(deviceData.timingHistory);
902
-
903
- // Une déviation > 0.15 est suspecte. On peut pondérer la pénalité.
904
- // Une déviation de 0.3 (très suspecte) donnerait un score de 100 (0.3 / 0.3 * 100).
905
- score += Math.min(100, (benfordDeviation / 0.3) * benfordWeight);
878
+ // Détection de distribution non-naturelle (bots "faussement aléatoires")
879
+ else if (benfordDeviation > benfordThreshold) {
880
+ instantScore = patternWeight;
906
881
  }
907
882
  }
908
883
 
909
- // --- Update history ---
910
- history.push({
911
- timestamp: now,
912
- path: currentPath,
913
- queryString: currentQueryString,
914
- });
915
-
916
- // Keep history to a reasonable size (e.g., last 10 requests)
884
+ // Garder l'historique à une taille raisonnable
917
885
  if (history.length > historySize) {
918
886
  history.shift();
919
887
  }
920
- if (deviceData.timingHistory.length > benfordMinSamples * 2) { // Garder un historique plus long pour Benford
888
+ if (deviceData.timingHistory.length > historySize) {
921
889
  deviceData.timingHistory.shift();
922
890
  }
923
891
 
924
- // Decay the score over time if behavior becomes normal again.
925
- // We can store the score in deviceData and decay it.
926
- deviceData.lastPatternScore = Math.min(100, (deviceData.lastPatternScore || 0) * decayFactor + score); // Decay old score and add new, plafonné à 100
892
+ // Logique de décroissance et de score final
893
+ let newPatternScore = deviceData.lastPatternScore || 0;
927
894
 
928
- // If there hasn't been a request in a while, reset the pattern score.
929
- if (history.length > 1 && (now - history[history.length - 2].timestamp > inactivityReset)) { // X ms inactivity
930
- deviceData.lastPatternScore = 0;
895
+ if (timeSinceLast > inactivityReset) {
896
+ newPatternScore = 0; // Réinitialisation complète après une longue inactivité
897
+ } else {
898
+ newPatternScore *= decayFactor;
931
899
  }
900
+ newPatternScore = Math.max(0, newPatternScore);
932
901
 
933
- return { requestPatternScore: deviceData.lastPatternScore };
902
+ deviceData.lastPatternScore = newPatternScore + instantScore;
903
+
904
+ return { requestPatternScore: Math.min(100, deviceData.lastPatternScore) };
934
905
  }
935
906
 
936
907
  const trapUrlTemplates = [
@@ -1553,6 +1524,46 @@ export class FingerprintEngine {
1553
1524
  }
1554
1525
  return blockList;
1555
1526
  }
1527
+
1528
+ /**
1529
+ * Checks if the client's IP resolves to any of the hostnames in the hostname allowlist.
1530
+ * The result is cached to avoid repeated DNS lookups.
1531
+ * @private
1532
+ * @param {string} clientIp - The IP address of the client.
1533
+ * @returns {Promise<boolean>} True if the IP is in the hostname allowlist.
1534
+ */
1535
+ async _isIpInHostnameAllowlist(clientIp) {
1536
+ const { whitelist = [] } = this.securityConfig;
1537
+ const hostnameRule = whitelist.find(rule => rule.type === 'hostname_allowlist');
1538
+
1539
+ if (!hostnameRule || !hostnameRule.entries || hostnameRule.entries.length === 0) {
1540
+ return false;
1541
+ }
1542
+
1543
+ const cacheKey = `ip-hostname-allowlist:${clientIp}`;
1544
+ const cachedStatus = await store.get(cacheKey);
1545
+
1546
+ if (cachedStatus === 'verified') return true;
1547
+ if (cachedStatus === 'failed') return false;
1548
+
1549
+ try {
1550
+ // Reverse DNS lookup to get hostnames for the IP
1551
+ const hostnames = await dns.reverse(clientIp);
1552
+
1553
+ // Check if any of the resolved hostnames is in our allowlist
1554
+ const isAllowed = hostnames.some(hostname => hostnameRule.entries.includes(hostname));
1555
+
1556
+ if (isAllowed) {
1557
+ await store.set(cacheKey, 'verified', 86400); // Cache success for 24h
1558
+ return true;
1559
+ }
1560
+ } catch (error) {
1561
+ // DNS errors (like no rDNS record) are treated as a failure.
1562
+ }
1563
+
1564
+ await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h
1565
+ return false;
1566
+ }
1556
1567
  _isIpInAllowlist(clientIp) {
1557
1568
  return this._allowlist.check(clientIp);
1558
1569
  }
@@ -1638,6 +1649,12 @@ export class FingerprintEngine {
1638
1649
  return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'allowlist' } };
1639
1650
  }
1640
1651
 
1652
+ // 2. Check hostname-based allowlist.
1653
+ if (await this._isIpInHostnameAllowlist(clientIp)) {
1654
+ this._log('IP resolves to a whitelisted hostname - allowing request', { clientIp });
1655
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'hostname_allowlist' } };
1656
+ }
1657
+
1641
1658
  const { pow_nonce } = query;
1642
1659
 
1643
1660
  // Honeypot: Direct probing of challenge endpoints is highly suspicious.
@@ -1814,7 +1831,7 @@ export class FingerprintEngine {
1814
1831
  this._log('Challenge solution valid - issuing ticket', { ticketMaxAge: finalTtl, isProbationary });
1815
1832
 
1816
1833
  if (logger) {
1817
- logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now() });
1834
+ logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now(), vector: preliminaryVector });
1818
1835
  }
1819
1836
 
1820
1837
  // NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
@@ -1927,6 +1944,9 @@ export class FingerprintEngine {
1927
1944
  if (onDeviceCompromised) {
1928
1945
  onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
1929
1946
  }
1947
+ if (logger) {
1948
+ logger({ type: 'request_blocked', deviceId: deviceId, score: finalScore, vector: suspicionVector, timestamp: Date.now() });
1949
+ }
1930
1950
  return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1931
1951
  }
1932
1952
 
@@ -1940,13 +1960,24 @@ export class FingerprintEngine {
1940
1960
  onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
1941
1961
  }
1942
1962
  if (logger) {
1943
- logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
1963
+ logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now(), vector: { honeypotScore: 100 } });
1944
1964
  }
1945
1965
  await store.set(`device:${cookies.device_id}`, deviceData); // No TTL for condemned status
1946
1966
  return { action: 'block', status: 404, score: 100, vector: { honeypotScore: 100 } };
1947
1967
  }
1948
-
1949
- if (isSuspicious && !isTicketValid(clientIp, powCookie)) { // The powCookie check is now after the PoW solution check
1968
+
1969
+ // --- NOUVELLE LOGIQUE DE RE-CHALLENGE ---
1970
+ // Un challenge est nécessaire si :
1971
+ // 1. La requête est suspecte ET il n'y a pas de ticket valide.
1972
+ // OU
1973
+ // 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
1974
+ const hasValidTicket = isTicketValid(clientIp, powCookie);
1975
+ const mustReChallenge = isSuspiciousHigh && hasValidTicket;
1976
+
1977
+ if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
1978
+ if (mustReChallenge) {
1979
+ this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
1980
+ }
1950
1981
  this._log('Suspicious request without valid ticket - issuing challenge', { finalScore, hasPowCookie: !!powCookie });
1951
1982
  // Honeypot: Direct probing of challenge endpoints is highly suspicious.
1952
1983
  // A legitimate user only hits these endpoints via the challenge page itself.
@@ -1956,7 +1987,7 @@ export class FingerprintEngine {
1956
1987
  if (pow_nonce && !isChallengeResponse) {
1957
1988
  this._log('Honeypot probe detected - blocking request', { path, pow_nonce });
1958
1989
  if (logger) {
1959
- logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now() });
1990
+ logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now(), vector: suspicionVector });
1960
1991
  }
1961
1992
  suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
1962
1993
  // Recalculate the final score with the updated vector.
@@ -2037,7 +2068,7 @@ export class FingerprintEngine {
2037
2068
  });
2038
2069
 
2039
2070
  if (logger) {
2040
- logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
2071
+ logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
2041
2072
  }
2042
2073
 
2043
2074
  // Check if the request is an API request to return a JSON challenge
@@ -2073,7 +2104,7 @@ export class FingerprintEngine {
2073
2104
  this._log('Request passed - no challenge required', { finalScore, hasValidTicket: isTicketValid(clientIp, powCookie) });
2074
2105
 
2075
2106
  if (logger) {
2076
- logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
2107
+ logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
2077
2108
  }
2078
2109
 
2079
2110
  return { action: 'next', score: finalScore, vector: suspicionVector };
@@ -2516,115 +2547,58 @@ let autoTuningJobId = null;
2516
2547
  * @param {object} securityConfig - The security configuration object to update.
2517
2548
  * @param {Array<object>} trafficData - The array containing traffic logs.
2518
2549
  * @param {number} minDataPoints - The minimum number of data points required to start optimization.
2550
+ * @param {number} maxDataPoints - The maximum number of data points to keep after an optimization cycle.
2519
2551
  */
2520
- function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
2521
- if (trafficData.length < minDataPoints) {
2522
- console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
2523
- return;
2524
- }
2525
- console.log(`[AutoTuning] Démarrage du cycle d'optimisation avec ${trafficData.length} points de données.`);
2526
-
2527
- // Classify historical requests with a confidence weight.
2528
- const solvedDevices = new Set(trafficData.filter(e => e.type === 'challenge_solved').map(e => e.deviceId));
2529
- const challengedDevices = new Set(trafficData.filter(e => e.type === 'challenge_issued').map(e => e.deviceId));
2530
-
2531
- const historicalRequests = trafficData.map(log => {
2532
- // Assign a label ('bot' or 'human') and a confidence weight to each log entry.
2533
- switch (log.type) {
2534
- case 'honeypot_probe':
2535
- case 'trap_triggered':
2536
- return { score: log.score, label: 'bot', confidence: 10.0 }; // Very high confidence
2537
-
2538
- case 'challenge_issued':
2539
- // A challenge issued to a device that never solved it is a strong bot signal.
2540
- if (!solvedDevices.has(log.deviceId)) {
2541
- return { score: log.score, label: 'bot', confidence: 3.0 }; // High confidence
2542
- }
2543
- // If the challenge was eventually solved, this specific log is neutral.
2544
- return null;
2552
+ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
2553
+ if (trafficData.length < minDataPoints) {
2554
+ console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
2555
+ return;
2556
+ }
2545
2557
 
2546
- case 'challenge_solved':
2547
- return { score: log.score, label: 'human', confidence: 5.0 }; // High confidence
2558
+ if (trafficData.length > maxDataPoints) {
2559
+ console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
2560
+ trafficData.splice(0, trafficData.length - maxDataPoints);
2561
+ }
2548
2562
 
2549
- case 'request_passed':
2550
- // A passed request from a device that was never even challenged is likely a human.
2551
- if (!challengedDevices.has(log.deviceId)) {
2552
- return { score: log.score, label: 'human', confidence: 0.5 }; // Low confidence
2553
- }
2554
- // If the device was challenged at some point, this log is ambiguous.
2555
- return null;
2556
-
2557
- default:
2558
- return null;
2559
- }
2560
- }).filter(Boolean); // Remove null entries
2561
-
2562
- // The "fitness" function evaluates the quality of a set of thresholds.
2563
- // A lower score is better.
2564
- const fitnessFunction = (solution) => {
2565
- const [low, medium, high, velocityThreshold, burstThreshold, scrapeThreshold] = solution;
2566
- if (low >= medium || medium >= high || low < 10 || high > 90) return Infinity;
2567
- if (velocityThreshold < 50 || velocityThreshold > burstThreshold || burstThreshold > scrapeThreshold) return Infinity;
2568
-
2569
- let weightedFalsePositives = 0; // Humans challenged unnecessarily.
2570
- let weightedFalseNegatives = 0; // Undetected bots.
2571
-
2572
- for (const req of historicalRequests) {
2573
- if (req.label === 'bot') {
2574
- if (req.score < low) weightedFalseNegatives += req.confidence;
2575
- } else { // 'human'
2576
- if (req.score >= low) weightedFalsePositives += req.confidence;
2577
- }
2578
- }
2579
- // The penalty for false negatives is implicitly higher due to the higher confidence scores of bot signals.
2580
- return weightedFalsePositives + weightedFalseNegatives;
2581
- };
2563
+ console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${trafficData.length} points de données.`);
2582
2564
 
2583
- // Functions for the genetic algorithm.
2584
- const createIndividual = () => [
2585
- 10 + Math.random() * 20, // low
2586
- 30 + Math.random() * 30, // medium
2587
- 60 + Math.random() * 30, // high
2588
- 100 + Math.random() * 150, // velocityThreshold (100-250ms)
2589
- 300 + Math.random() * 400, // burstThreshold (300-700ms)
2590
- 800 + Math.random() * 700, // scrapeThreshold (800-1500ms)
2591
- ];
2592
- const crossover = (p1, p2) => p1.map((val, i) => (val + p2[i]) / 2);
2593
- const mutate = (s) => {
2594
- const n = [...s];
2595
- const i = Math.floor(Math.random() * n.length);
2596
- // Adjust mutation range based on parameter
2597
- const mutationRange = i < 3 ? 5 : 50;
2598
- n[i] += (Math.random() - 0.5) * mutationRange;
2599
- return n;
2600
- };
2565
+ const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData });
2601
2566
 
2602
- // Start optimization.
2603
- const result = Optimization.geneticAlgorithm(createIndividual, fitnessFunction, crossover, mutate, {
2604
- generations: 50,
2605
- populationSize: 40
2606
- });
2567
+ if (!paretoFront || paretoFront.length === 0) {
2568
+ console.warn("[AutoTuning] L'optimisation n'a retourné aucune solution.");
2569
+ return;
2570
+ }
2607
2571
 
2608
- const [newLow, newMedium, newHigh, newVelocity, newBurst, newScrape] = result.solution;
2572
+ // Stratégie de sélection : choisir la solution la plus équilibrée du front de Pareto.
2573
+ // On cherche la solution la plus proche de l'origine (0,0) dans l'espace des objectifs.
2574
+ let bestSolution = paretoFront[0];
2575
+ let minDistance = Math.sqrt(Math.pow(bestSolution.objectives[0], 2) + Math.pow(bestSolution.objectives[1], 2));
2609
2576
 
2610
- // Update the configuration live.
2611
- // Ensure thresholds object exists
2612
- if (!securityConfig.thresholds) securityConfig.thresholds = {};
2613
- securityConfig.thresholds.low = Math.round(newLow);
2614
- securityConfig.thresholds.medium = Math.round(newMedium);
2615
- securityConfig.thresholds.high = Math.round(newHigh);
2577
+ for (let i = 1; i < paretoFront.length; i++) {
2578
+ const distance = Math.sqrt(Math.pow(paretoFront[i].objectives[0], 2) + Math.pow(paretoFront[i].objectives[1], 2));
2579
+ if (distance < minDistance) {
2580
+ minDistance = distance;
2581
+ bestSolution = paretoFront[i];
2582
+ }
2583
+ }
2616
2584
 
2617
- // Update pattern detection parameters
2618
- if (!securityConfig.patterns) securityConfig.patterns = {};
2619
- securityConfig.patterns.velocityThreshold = Math.round(newVelocity);
2620
- securityConfig.patterns.burstThreshold = Math.round(newBurst);
2621
- securityConfig.patterns.scrapeThreshold = Math.round(newScrape);
2622
- // Weights could also be optimized, but let's keep it to thresholds for now for simplicity.
2585
+ // Appliquer la nouvelle configuration optimisée
2586
+ const newConfig = bestSolution.solution;
2623
2587
 
2624
- console.log("[AutoTuning] Nouveaux seuils optimisés appliqués :", securityConfig.thresholds);
2625
- if (securityConfig.patterns) {
2626
- console.log("[AutoTuning] Nouveaux paramètres de pattern appliqués :", securityConfig.patterns);
2627
- }
2588
+ // S'assurer que les objets de configuration existent avant d'utiliser Object.assign
2589
+ if (!securityConfig.thresholds) securityConfig.thresholds = {};
2590
+ if (!securityConfig.weights) securityConfig.weights = {};
2591
+ if (!securityConfig.patterns) securityConfig.patterns = {};
2592
+
2593
+ Object.assign(securityConfig.thresholds, newConfig.thresholds || {});
2594
+ Object.assign(securityConfig.weights, newConfig.weights || {});
2595
+ Object.assign(securityConfig.patterns, newConfig.patterns || {});
2596
+
2597
+ console.log("[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.");
2598
+ console.log("[AutoTuning] Objectifs atteints :", { falsePositiveRate: bestSolution.objectives[0].toFixed(4), falseNegativeRate: bestSolution.objectives[1].toFixed(4) });
2599
+ console.log("[AutoTuning] Seuils :", securityConfig.thresholds);
2600
+ console.log("[AutoTuning] Poids :", securityConfig.weights);
2601
+ console.log("[AutoTuning] Patterns :", securityConfig.patterns);
2628
2602
  }
2629
2603
 
2630
2604
  /**
@@ -2634,7 +2608,8 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
2634
2608
  * @param {object} options.securityConfig - The live security configuration object that will be mutated.
2635
2609
  * @param {Array<object>} options.trafficData - The array where the logger pushes traffic data.
2636
2610
  * @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
2637
- * @param {number} [options.minDataPoints=200] - The minimum number of requests to analyze before starting a cycle (default: 200).
2611
+ * @param {number} [options.minDataPoints=200] - The minimum number of requests to have before starting a cycle (default: 200).
2612
+ * @param {number} [options.maxDataPoints=10000] - The maximum number of log entries to keep in memory (default: 10,000).
2638
2613
  */
2639
2614
  export function startThresholdAutoTuning(options) {
2640
2615
  if (autoTuningJobId) {
@@ -2645,8 +2620,9 @@ export function startThresholdAutoTuning(options) {
2645
2620
  const {
2646
2621
  securityConfig,
2647
2622
  trafficData,
2648
- interval = 1800000,
2649
- minDataPoints = 200
2623
+ interval = 1800000, // 30 minutes
2624
+ minDataPoints = 200,
2625
+ maxDataPoints = 10000 // Limite par défaut à 10 000 entrées
2650
2626
  } = options;
2651
2627
 
2652
2628
  if (!securityConfig || !trafficData) {
@@ -2656,7 +2632,7 @@ export function startThresholdAutoTuning(options) {
2656
2632
  console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
2657
2633
 
2658
2634
  autoTuningJobId = setInterval(() => {
2659
- runThresholdOptimization(securityConfig, trafficData, minDataPoints);
2635
+ runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints);
2660
2636
  }, interval);
2661
2637
  }
2662
2638