@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 +56 -4
- package/fingerprint.client.js +3 -15
- package/fingerprint.js +161 -185
- package/library.js +83 -8
- package/mongodb-store.js +52 -52
- package/package.json +1 -1
- package/pow.solver.js +49 -13
- package/pow.worker.js +26 -26
- package/problem-manager.js +72 -0
- package/redis-store.js +42 -42
- package/sql-store.js +77 -77
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,
|
|
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
|
-
|
|
117
|
-
|
|
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`.
|
package/fingerprint.client.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
357
|
-
|
|
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
|
-
//
|
|
825
|
+
// (NOUVEAU) Logique de détection de pattern simplifiée et unifiée.
|
|
826
826
|
const {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
//
|
|
888
|
-
if (
|
|
889
|
-
|
|
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
|
-
|
|
899
|
-
|
|
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
|
-
//
|
|
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 >
|
|
888
|
+
if (deviceData.timingHistory.length > historySize) {
|
|
921
889
|
deviceData.timingHistory.shift();
|
|
922
890
|
}
|
|
923
891
|
|
|
924
|
-
//
|
|
925
|
-
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
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
|
-
|
|
2547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
});
|
|
2567
|
+
if (!paretoFront || paretoFront.length === 0) {
|
|
2568
|
+
console.warn("[AutoTuning] L'optimisation n'a retourné aucune solution.");
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2607
2571
|
|
|
2608
|
-
|
|
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
|
-
|
|
2611
|
-
|
|
2612
|
-
if (
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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
|
-
|
|
2618
|
-
|
|
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
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
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
|
|
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
|
|