@anonympins/fingerprint 0.2.4 → 0.3.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/README.md +100 -11
- package/fingerprint.client.obfuscated.js +1 -0
- package/fingerprint.js +265 -43
- package/library.js +1727 -1694
- package/package.json +94 -88
- package/pow.solver.inline.js +255 -0
- package/problem-manager.js +523 -423
package/fingerprint.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { BlockList } from "node:net";
|
|
3
3
|
import dns from "node:dns/promises";
|
|
4
|
-
import { problemManager } from "./problem-manager.js";
|
|
4
|
+
import { getProblemManager, problemManager } from "./problem-manager.js";
|
|
5
5
|
import { Optimization } from "./library.js";
|
|
6
6
|
import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
@@ -349,7 +349,7 @@ function getCompositeDeviceHash(context) {
|
|
|
349
349
|
const tcpFingerprint = context.headers['x-tcp-fingerprint'];
|
|
350
350
|
if (tcpFingerprint) srv.add("tcp", tcpFingerprint);
|
|
351
351
|
|
|
352
|
-
// 3. SIGNAUX DE HAUT NIVEAU (Applicatif)
|
|
352
|
+
// 3. SIGNAUX DE HAUT NIVEAU (Applicatif) Moins fiables, mais utiles pour la corroboration
|
|
353
353
|
const headersToCapture = {
|
|
354
354
|
"ch_ua": "sec-ch-ua",
|
|
355
355
|
"ch_platform": "sec-ch-ua-platform",
|
|
@@ -385,6 +385,46 @@ function getCompositeDeviceHash(context) {
|
|
|
385
385
|
}
|
|
386
386
|
export { getCompositeDeviceHash };
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* @private
|
|
390
|
+
* A knowledge base of known TLS (JA3) fingerprints for common browsers.
|
|
391
|
+
* This helps in detecting inconsistencies between the TLS layer and the HTTP User-Agent.
|
|
392
|
+
* The key is the JA3 hash, and the value is the browser family.
|
|
393
|
+
* This list is not exhaustive but covers many common cases.
|
|
394
|
+
*/
|
|
395
|
+
const tlsFingerprintDb = {
|
|
396
|
+
// --- Chrome (Desktop) ---
|
|
397
|
+
'e188a442b87f422c5a1e80b05399435b': 'Chrome', // Chrome 107, Windows 10
|
|
398
|
+
'd8e35855049321c6042a4325c697858f': 'Chrome', // Chrome 114, Windows 11
|
|
399
|
+
'a9f90958d44533748c139a5d1895b925': 'Chrome', // Chrome 116, macOS
|
|
400
|
+
'3b5379916d2b3882253c42885956a350': 'Chrome', // Chrome 124, Linux
|
|
401
|
+
|
|
402
|
+
// --- Chrome (Mobile) ---
|
|
403
|
+
'59822058c95c33d2d06e52f410855c8c': 'Chrome', // Chrome 120, Android 13
|
|
404
|
+
|
|
405
|
+
// --- Firefox (Desktop) ---
|
|
406
|
+
'b386946a5a586163c7c533636b45c355': 'Firefox', // Firefox 102, Windows 10
|
|
407
|
+
'66236495a523c1785f8f3a105b248b11': 'Firefox', // Firefox 115, Windows 11
|
|
408
|
+
'b73d470006575b5e35167a0b5a8540e2': 'Firefox', // Firefox 121, macOS
|
|
409
|
+
'8443d7562933834333943465d52363cf': 'Firefox', // Firefox 125, Linux
|
|
410
|
+
|
|
411
|
+
// --- Firefox (Mobile) ---
|
|
412
|
+
'02720628957d38c6111a18433abe833f': 'Firefox', // Firefox 125, Android 14
|
|
413
|
+
|
|
414
|
+
// --- Safari & iOS (Shared TLS Stack) ---
|
|
415
|
+
// On iOS, all browsers (Chrome, Firefox, etc.) must use WebKit, which uses Apple's TLS stack.
|
|
416
|
+
// Therefore, they all share the same JA3 fingerprint as Safari on that OS version.
|
|
417
|
+
'b633f21d532d35967c8753c38536b4d3': 'Safari', // Safari 16, macOS
|
|
418
|
+
'4d7a28d5f55b359b69100a311013f03e': ['Safari', 'Chrome', 'Firefox'], // Safari 17, iOS 17 (and other browsers on iOS 17)
|
|
419
|
+
'8dd3d7532873575314df23c447543001': ['Safari', 'Chrome', 'Firefox'], // Safari 17.4, iOS 17.4
|
|
420
|
+
|
|
421
|
+
// --- Common Libraries & Bots (for spoofing detection) ---
|
|
422
|
+
'47344a349b75c4e82333475553b5f358': 'Python', // Python 3.10 `requests` library
|
|
423
|
+
'b29587b8a143c42546133ad7704b3310': 'Go', // Go 1.19 `http` library
|
|
424
|
+
'd435b5223b2884c5a832b842637e245f': 'Java', // Java 11 `HttpClient`
|
|
425
|
+
'c72366b9551263d990b7fa574225332c': 'curl', // curl 7.81.0
|
|
426
|
+
};
|
|
427
|
+
|
|
388
428
|
// Fonctions utilitaires
|
|
389
429
|
function parseUserAgent(ua) {
|
|
390
430
|
// Parser basique du User-Agent
|
|
@@ -520,7 +560,10 @@ export const verifyTspChallenge = (
|
|
|
520
560
|
targetMaxDistance,
|
|
521
561
|
cities,
|
|
522
562
|
) => {
|
|
523
|
-
|
|
563
|
+
// Input validation: ensure the solution is a non-empty string before trying to parse it.
|
|
564
|
+
if (typeof solutionPathJson !== 'string' || solutionPathJson.length === 0) return false;
|
|
565
|
+
|
|
566
|
+
try {
|
|
524
567
|
const solutionPath = JSON.parse(solutionPathJson);
|
|
525
568
|
if (!Array.isArray(solutionPath) || solutionPath.length !== numCities)
|
|
526
569
|
return false;
|
|
@@ -701,6 +744,13 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
701
744
|
* The server performs the same calculation to validate.
|
|
702
745
|
*/
|
|
703
746
|
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
|
|
747
|
+
// Hard cap on memory difficulty to prevent DoS attacks from malicious clients
|
|
748
|
+
// submitting an arbitrarily large difficulty value.
|
|
749
|
+
const MAX_ALLOWED_MEM_DIFFICULTY = 128; // 128MB
|
|
750
|
+
if (difficulty > MAX_ALLOWED_MEM_DIFFICULTY) {
|
|
751
|
+
console.warn(`[Security] Memory PoW verification attempt with excessive difficulty: ${difficulty}MB. Denied.`);
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
704
754
|
const size = difficulty * 1024 * 1024;
|
|
705
755
|
const iterations = size / 16;
|
|
706
756
|
const buffer = new Uint32Array(size / 4);
|
|
@@ -720,8 +770,9 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
|
|
|
720
770
|
return finalHash === parseInt(solution, 10);
|
|
721
771
|
};
|
|
722
772
|
export const isTicketValid = (ip, ticket) => {
|
|
723
|
-
|
|
724
|
-
|
|
773
|
+
// Input validation: ensure the ticket is a non-empty string with the correct format.
|
|
774
|
+
if (typeof ticket !== 'string' || !ticket.includes(':')) return false;
|
|
775
|
+
const [expiry, sig] = ticket.split(':');
|
|
725
776
|
if (!expiry || !sig || Date.now() > parseInt(expiry, 10)) return false;
|
|
726
777
|
const expectedSig = crypto
|
|
727
778
|
.createHmac("sha256", getPowSecret())
|
|
@@ -1023,36 +1074,46 @@ function getCrossLayerInconsistency(context) {
|
|
|
1023
1074
|
* @returns {{tlsSpoofingScore: number}}
|
|
1024
1075
|
*/
|
|
1025
1076
|
function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerprint) {
|
|
1026
|
-
let score = 0;
|
|
1027
1077
|
const { ja3, ja4 } = getTlsFingerprintFn(context) || { ja3: null, ja4: null }; // Defensive check
|
|
1028
1078
|
const ua = context.headers["user-agent"] || '';
|
|
1029
1079
|
|
|
1030
|
-
//
|
|
1080
|
+
// 1. Penalize if a TLS fingerprint is present but the User-Agent is generic or missing.
|
|
1081
|
+
// This is a strong indicator of a non-browser client trying to look legitimate.
|
|
1031
1082
|
if ((ja3 || ja4) && (!ua || ua.length < 10 || ua.toLowerCase().includes('python') || ua.toLowerCase().includes('curl'))) {
|
|
1032
|
-
|
|
1083
|
+
return { tlsSpoofingScore: 50 };
|
|
1033
1084
|
}
|
|
1034
1085
|
|
|
1035
|
-
//
|
|
1036
|
-
// Ceci nécessiterait une base de données de JA3/JA4 connus ou une logique de parsing avancée.
|
|
1037
|
-
// Pour l'instant, une implémentation simplifiée:
|
|
1038
|
-
// Si JA3/JA4 est présent et le UA est un navigateur connu, mais ils ne correspondent pas.
|
|
1086
|
+
// 2. If no JA3 hash is available, we cannot perform the consistency check.
|
|
1039
1087
|
if (ja3 && ua) {
|
|
1040
|
-
//
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1088
|
+
// Look up the expected browser family (or families) from our database.
|
|
1089
|
+
let expectedBrowsers = tlsFingerprintDb[ja3];
|
|
1090
|
+
|
|
1091
|
+
if (expectedBrowsers) {
|
|
1092
|
+
// Ensure it's always an array for consistent logic.
|
|
1093
|
+
if (!Array.isArray(expectedBrowsers)) {
|
|
1094
|
+
expectedBrowsers = [expectedBrowsers];
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Parse the User-Agent to get the claimed browser.
|
|
1098
|
+
const { browser: claimedBrowser } = parseUserAgent(ua);
|
|
1099
|
+
|
|
1100
|
+
// Check if the claimed browser is one of the legitimate possibilities for this JA3 hash.
|
|
1101
|
+
// We use `some` to see if the claimed browser starts with any of the expected browser names.
|
|
1102
|
+
// (e.g., "Chrome/116" starts with "Chrome").
|
|
1103
|
+
const isMatch = expectedBrowsers.some(expected => claimedBrowser?.startsWith(expected));
|
|
1104
|
+
|
|
1105
|
+
if (claimedBrowser && !isMatch) {
|
|
1106
|
+
return { tlsSpoofingScore: 80 }; // High score for a clear mismatch.
|
|
1107
|
+
}
|
|
1051
1108
|
}
|
|
1052
1109
|
}
|
|
1053
|
-
// On pourrait ajouter des vérifications similaires pour JA4 si on avait une base de données de JA4.
|
|
1054
1110
|
|
|
1055
|
-
|
|
1111
|
+
// If we reach here, either:
|
|
1112
|
+
// - No JA3 was available.
|
|
1113
|
+
// - The JA3 was not in our database (we can't make a decision).
|
|
1114
|
+
// - The JA3 and User-Agent were consistent.
|
|
1115
|
+
// In all these cases, the score is 0.
|
|
1116
|
+
return { tlsSpoofingScore: 0 };
|
|
1056
1117
|
}
|
|
1057
1118
|
|
|
1058
1119
|
|
|
@@ -1733,15 +1794,74 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1733
1794
|
return null;
|
|
1734
1795
|
}
|
|
1735
1796
|
|
|
1797
|
+
/**
|
|
1798
|
+
* @private
|
|
1799
|
+
* Parses a GraphQL query string to extract the operation type and name.
|
|
1800
|
+
* Uses a lightweight regex to avoid pulling in a heavy AST parser.
|
|
1801
|
+
* @param {object} body - The request body, which might contain the query.
|
|
1802
|
+
* @returns {{type: string, name: string}|null}
|
|
1803
|
+
*/
|
|
1804
|
+
function parseGraphQLQuery(body) {
|
|
1805
|
+
const query = body?.query;
|
|
1806
|
+
if (typeof query !== 'string') {
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
// Regex to capture operation type (query, mutation, subscription) and optional operation name.
|
|
1810
|
+
// Handles whitespace and potential comments.
|
|
1811
|
+
const match = query.match(/(?:^|\s)(query|mutation|subscription)\s+([_A-Za-z][_0-9A-Za-z]*)?/);
|
|
1812
|
+
if (match) {
|
|
1813
|
+
return {
|
|
1814
|
+
type: match[1],
|
|
1815
|
+
name: match[2] || 'Anonymous', // Default to 'Anonymous' if name is missing
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1736
1820
|
export class FingerprintEngine {
|
|
1737
1821
|
constructor(securityConfig) {
|
|
1738
1822
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
1739
1823
|
this.securityConfig = securityConfig;
|
|
1740
1824
|
this.isProduction = isProduction;
|
|
1741
1825
|
this._allowlist = this._buildAllowlist();
|
|
1826
|
+
this._validateConfig(securityConfig); // Validate the configuration
|
|
1742
1827
|
this.verbose = securityConfig.verbose || false;
|
|
1743
1828
|
}
|
|
1744
1829
|
|
|
1830
|
+
/**
|
|
1831
|
+
* Validates the security configuration object to detect potential typos or missing essential keys.
|
|
1832
|
+
* @private
|
|
1833
|
+
* @param {object} config - The security configuration object.
|
|
1834
|
+
*/
|
|
1835
|
+
_validateConfig(config) {
|
|
1836
|
+
if (!config) {
|
|
1837
|
+
console.warn('[Fingerprint] Warning: No securityConfig provided. Using default behaviors, which may not be secure.');
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
const knownKeys = new Set([
|
|
1842
|
+
'weights', 'thresholds', 'cpu', 'ticketMaxAge', 'challengeTtl',
|
|
1843
|
+
'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
|
|
1844
|
+
'honeypot', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger',
|
|
1845
|
+
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist',
|
|
1846
|
+
'similarityThreshold'
|
|
1847
|
+
]);
|
|
1848
|
+
|
|
1849
|
+
// 1. Check for essential keys
|
|
1850
|
+
if (!config.weights) {
|
|
1851
|
+
console.warn('[Fingerprint] Warning: `securityConfig.weights` is not defined. Suspicion scores will be 0.');
|
|
1852
|
+
}
|
|
1853
|
+
if (!config.thresholds) {
|
|
1854
|
+
console.warn('[Fingerprint] Warning: `securityConfig.thresholds` is not defined. Challenges may not be issued correctly.');
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// 2. Check for unknown (potentially misspelled) keys
|
|
1858
|
+
for (const key in config) {
|
|
1859
|
+
if (!knownKeys.has(key)) {
|
|
1860
|
+
console.warn(`[Fingerprint] Warning: Unknown key '${key}' found in securityConfig. This might be a typo.`);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1745
1865
|
_log(message, data = {}) {
|
|
1746
1866
|
if (this.verbose) {
|
|
1747
1867
|
console.log(`[FingerprintEngine] ${message}`, data);
|
|
@@ -1911,6 +2031,41 @@ export class FingerprintEngine {
|
|
|
1911
2031
|
|
|
1912
2032
|
return false;
|
|
1913
2033
|
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Checks if the GraphQL operation matches an entry in the GraphQL operation allowlist.
|
|
2036
|
+
* Supports wildcards for operation names.
|
|
2037
|
+
* @private
|
|
2038
|
+
* @param {string} operationType - The type of the GraphQL operation (e.g., 'query', 'mutation').
|
|
2039
|
+
* @param {string} operationName - The name of the GraphQL operation.
|
|
2040
|
+
* @returns {boolean} True if the operation is in the allowlist.
|
|
2041
|
+
*/
|
|
2042
|
+
_isGraphqlOperationInAllowlist(operationType, operationName) {
|
|
2043
|
+
const { whitelist = [] } = this.securityConfig;
|
|
2044
|
+
const graphqlRule = whitelist.find(rule => rule.type === 'graphql_operation_allowlist');
|
|
2045
|
+
|
|
2046
|
+
if (!graphqlRule || !graphqlRule.entries || !operationType || !operationName) {
|
|
2047
|
+
return false;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
for (const entry of graphqlRule.entries) {
|
|
2051
|
+
const [entryType, entryName] = entry.split(':');
|
|
2052
|
+
if (entryType !== operationType) {
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Check for exact name match or full wildcard
|
|
2057
|
+
if (entryName === operationName || entryName === '*') {
|
|
2058
|
+
return true;
|
|
2059
|
+
}
|
|
2060
|
+
// Check for partial wildcard (e.g., "Search*")
|
|
2061
|
+
if (entryName.endsWith('*') && operationName.startsWith(entryName.slice(0, -1))) {
|
|
2062
|
+
return true;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1914
2069
|
/**
|
|
1915
2070
|
* Verifies if a request comes from a legitimate, whitelisted bot (e.g., Googlebot)
|
|
1916
2071
|
* using reverse and forward DNS lookups. The result is cached.
|
|
@@ -1977,7 +2132,7 @@ export class FingerprintEngine {
|
|
|
1977
2132
|
|
|
1978
2133
|
async processRequest(requestContext) {
|
|
1979
2134
|
|
|
1980
|
-
const { clientIp = "unknown", path, cookies, query, isStatic } = requestContext;
|
|
2135
|
+
const { clientIp = "unknown", path, cookies, query, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
1981
2136
|
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
1982
2137
|
|
|
1983
2138
|
this._log('Processing request', { clientIp, path, isStatic });
|
|
@@ -2012,6 +2167,12 @@ export class FingerprintEngine {
|
|
|
2012
2167
|
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'path_allowlist' } };
|
|
2013
2168
|
}
|
|
2014
2169
|
|
|
2170
|
+
// 5. Check GraphQL operation allowlist.
|
|
2171
|
+
if (graphqlOperationType && this._isGraphqlOperationInAllowlist(graphqlOperationType, graphqlOperationName)) {
|
|
2172
|
+
this._log('GraphQL operation in allowlist - allowing request', { operation: `${graphqlOperationType}:${graphqlOperationName}` });
|
|
2173
|
+
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'graphql_operation_allowlist' } };
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2015
2176
|
const { pow_nonce } = query;
|
|
2016
2177
|
|
|
2017
2178
|
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
@@ -2181,6 +2342,15 @@ export class FingerprintEngine {
|
|
|
2181
2342
|
}
|
|
2182
2343
|
} else {
|
|
2183
2344
|
this._log('Challenge context not found or expired', { pow_nonce });
|
|
2345
|
+
// --- NOUVELLE MESURE DE SÉCURITÉ ---
|
|
2346
|
+
// Si un client soumet un nonce invalide ou expiré, c'est une tentative de probing.
|
|
2347
|
+
// On applique une pénalité maximale pour bloquer ou re-challenger lourdement.
|
|
2348
|
+
suspicionVector.honeypotScore = 100;
|
|
2349
|
+
finalScore = this.calculateFinalScore(suspicionVector);
|
|
2350
|
+
this._log('Invalid nonce submitted (probing attempt) - applying max penalty', { newFinalScore: finalScore });
|
|
2351
|
+
// La logique continue vers la section `if (isValid)` qui échouera,
|
|
2352
|
+
// puis le score élevé sera utilisé pour bloquer ou re-challenger.
|
|
2353
|
+
isValid = false; // On s'assure que la validation échoue.
|
|
2184
2354
|
}
|
|
2185
2355
|
if (isValid) {
|
|
2186
2356
|
// La solution est valide. On supprime le secret et on redirige.
|
|
@@ -2278,7 +2448,7 @@ export class FingerprintEngine {
|
|
|
2278
2448
|
if (challengeContext) {
|
|
2279
2449
|
try {
|
|
2280
2450
|
const workResult = JSON.parse(pow_solution_work_result);
|
|
2281
|
-
|
|
2451
|
+
getProblemManager(this.securityConfig.usefulWorkConfigPath).integrateSolution(pow_problem_id, workResult);
|
|
2282
2452
|
|
|
2283
2453
|
await store.delete(`secret:${pow_nonce}`);
|
|
2284
2454
|
// Accorder un ticket de passage comme pour un PoW normal
|
|
@@ -2358,10 +2528,13 @@ export class FingerprintEngine {
|
|
|
2358
2528
|
|
|
2359
2529
|
// Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
|
|
2360
2530
|
// Cela rend l'automatisation plus difficile pour un attaquant.
|
|
2361
|
-
|
|
2531
|
+
// Utilisation de crypto pour un choix plus sécurisé.
|
|
2532
|
+
const shouldUseUsefulWork = this.securityConfig.enableUsefulWork && crypto.randomBytes(1).readUInt8(0) / 255 > 0.5;
|
|
2533
|
+
|
|
2534
|
+
if (isSuspicious && shouldUseUsefulWork) {
|
|
2362
2535
|
this._log('Issuing a useful work challenge', { finalScore });
|
|
2363
2536
|
|
|
2364
|
-
const { problemId, task } =
|
|
2537
|
+
const { problemId, task } = getProblemManager(this.securityConfig.usefulWorkConfigPath).dispatchWork(suspicionFactor);
|
|
2365
2538
|
|
|
2366
2539
|
await store.set(`secret:${nonce}`, { clientSecret, originalPath: path }, 300);
|
|
2367
2540
|
|
|
@@ -2664,6 +2837,7 @@ export const xss_analyzer = async (data) => {
|
|
|
2664
2837
|
*/
|
|
2665
2838
|
export const modsecurity_analyzer = (rulesPath) => {
|
|
2666
2839
|
let wafInstance = null; // Singleton instance for the WAF
|
|
2840
|
+
let isModSecurityAvailable = true; // Flag specific to this analyzer instance
|
|
2667
2841
|
|
|
2668
2842
|
return async (data) => {
|
|
2669
2843
|
if (!rulesPath) {
|
|
@@ -2671,8 +2845,12 @@ export const modsecurity_analyzer = (rulesPath) => {
|
|
|
2671
2845
|
return false;
|
|
2672
2846
|
}
|
|
2673
2847
|
|
|
2848
|
+
if (!isModSecurityAvailable) {
|
|
2849
|
+
return false; // Skip if the module is known to be unavailable
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2674
2852
|
try {
|
|
2675
|
-
if (!wafInstance) {
|
|
2853
|
+
if (!wafInstance && isModSecurityAvailable) {
|
|
2676
2854
|
// Dynamically import the library only when needed.
|
|
2677
2855
|
const { ModSecurity } = await import('modsecurity-nodejs');
|
|
2678
2856
|
wafInstance = new ModSecurity();
|
|
@@ -2687,7 +2865,8 @@ export const modsecurity_analyzer = (rulesPath) => {
|
|
|
2687
2865
|
return result !== null; // A non-null result means a threat was detected.
|
|
2688
2866
|
} catch (error) {
|
|
2689
2867
|
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
2690
|
-
console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is disabled. Run "npm install modsecurity-nodejs" to enable it.');
|
|
2868
|
+
console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is now disabled. Run "npm install modsecurity-nodejs" to enable it.');
|
|
2869
|
+
isModSecurityAvailable = false; // Disable for future calls
|
|
2691
2870
|
}
|
|
2692
2871
|
return false; // Assume data is safe if any error occurs.
|
|
2693
2872
|
}
|
|
@@ -2802,6 +2981,11 @@ export const default_whitelist = () => [
|
|
|
2802
2981
|
export const powMiddleware = (securityConfig) => {
|
|
2803
2982
|
const engine = new FingerprintEngine(securityConfig);
|
|
2804
2983
|
|
|
2984
|
+
// Initialize the problem manager with the configured path, if provided.
|
|
2985
|
+
if (securityConfig.enableUsefulWork && securityConfig.usefulWorkConfigPath) {
|
|
2986
|
+
getProblemManager(securityConfig.usefulWorkConfigPath, store); // This correctly initializes the singleton
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2805
2989
|
if (securityConfig.autotuning) {
|
|
2806
2990
|
startThresholdAutoTuning({
|
|
2807
2991
|
securityConfig: securityConfig,
|
|
@@ -2834,6 +3018,15 @@ export const powMiddleware = (securityConfig) => {
|
|
|
2834
3018
|
httpVersion: req.httpVersion,
|
|
2835
3019
|
};
|
|
2836
3020
|
|
|
3021
|
+
// New GraphQL parsing logic
|
|
3022
|
+
// It's common for GraphQL endpoints to be at '/graphql'
|
|
3023
|
+
if (req.path === '/graphql' && req.body) {
|
|
3024
|
+
const gqlInfo = parseGraphQLQuery(req.body);
|
|
3025
|
+
if (gqlInfo) {
|
|
3026
|
+
requestContext.graphqlOperationType = gqlInfo.type;
|
|
3027
|
+
requestContext.graphqlOperationName = gqlInfo.name;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
2837
3030
|
const decision = await engine.processRequest(requestContext);
|
|
2838
3031
|
|
|
2839
3032
|
// Attach the fingerprinting result to the request object for downstream middlewares.
|
|
@@ -2894,6 +3087,7 @@ export const __internal = {
|
|
|
2894
3087
|
getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
|
|
2895
3088
|
generateCpuTargetChallengePage,
|
|
2896
3089
|
generateCombinedPoWChallengePage,
|
|
3090
|
+
problemManager, // Re-export the problemManager promise
|
|
2897
3091
|
};
|
|
2898
3092
|
|
|
2899
3093
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -2909,8 +3103,16 @@ let autoTuningJobId = null;
|
|
|
2909
3103
|
* @param {number} maxDataPoints - The maximum number of data points to keep after an optimization cycle.
|
|
2910
3104
|
*/
|
|
2911
3105
|
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
|
|
2912
|
-
|
|
3106
|
+
const highConfidenceLogs = trafficData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
|
|
3107
|
+
const highConfidenceRatio = trafficData.length > 0 ? highConfidenceLogs / trafficData.length : 0;
|
|
3108
|
+
const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
|
|
3109
|
+
|
|
3110
|
+
if (trafficData.length < minDataPoints || highConfidenceRatio < MIN_CONFIDENCE_RATIO) {
|
|
3111
|
+
if (trafficData.length < minDataPoints) {
|
|
2913
3112
|
console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
|
|
3113
|
+
} else {
|
|
3114
|
+
console.log(`[AutoTuning] Reporté : Ratio de confiance insuffisant (${(highConfidenceRatio * 100).toFixed(2)}% < ${(MIN_CONFIDENCE_RATIO * 100).toFixed(2)}%).`);
|
|
3115
|
+
}
|
|
2914
3116
|
return;
|
|
2915
3117
|
}
|
|
2916
3118
|
|
|
@@ -2941,23 +3143,43 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
2941
3143
|
}
|
|
2942
3144
|
}
|
|
2943
3145
|
|
|
2944
|
-
//
|
|
3146
|
+
// --- NOUVEAU : Logique d'inertie pour l'application de la configuration ---
|
|
3147
|
+
// Au lieu d'appliquer directement la nouvelle configuration, on fait "glisser"
|
|
3148
|
+
// l'ancienne vers la nouvelle, avec une vélocité de changement maximale.
|
|
2945
3149
|
const newConfig = bestSolution.solution;
|
|
3150
|
+
const MAX_CHANGE_VELOCITY = 0.15; // 15% de changement maximum par cycle
|
|
2946
3151
|
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3152
|
+
/**
|
|
3153
|
+
* Met à jour un objet de configuration (ex: thresholds, weights) en douceur.
|
|
3154
|
+
* @param {object} currentConfig - La configuration actuelle à modifier.
|
|
3155
|
+
* @param {object} targetConfig - La configuration cible proposée par l'optimiseur.
|
|
3156
|
+
*/
|
|
3157
|
+
const applyInertialUpdate = (currentConfig, targetConfig) => {
|
|
3158
|
+
if (!currentConfig || !targetConfig) return; // Vérifier aussi currentConfig
|
|
3159
|
+
for (const key in targetConfig) {
|
|
3160
|
+
if (Object.hasOwnProperty.call(currentConfig, key)) {
|
|
3161
|
+
const currentValue = currentConfig[key];
|
|
3162
|
+
const targetValue = targetConfig[key];
|
|
3163
|
+
const delta = targetValue - currentValue;
|
|
3164
|
+
const maxChange = Math.abs(currentValue * MAX_CHANGE_VELOCITY);
|
|
3165
|
+
|
|
3166
|
+
// Limite le changement à la vélocité maximale
|
|
3167
|
+
const change = Math.max(-maxChange, Math.min(maxChange, delta));
|
|
3168
|
+
|
|
3169
|
+
currentConfig[key] += change;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
2951
3173
|
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3174
|
+
applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds);
|
|
3175
|
+
applyInertialUpdate(securityConfig.weights, newConfig.weights);
|
|
3176
|
+
applyInertialUpdate(securityConfig.patterns, newConfig.patterns);
|
|
2955
3177
|
|
|
2956
3178
|
console.log("[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.");
|
|
2957
3179
|
console.log("[AutoTuning] Objectifs atteints :", { falsePositiveRate: bestSolution.objectives[0].toFixed(4), falseNegativeRate: bestSolution.objectives[1].toFixed(4) });
|
|
2958
|
-
console.log("[AutoTuning]
|
|
2959
|
-
console.log("[AutoTuning]
|
|
2960
|
-
console.log("[AutoTuning]
|
|
3180
|
+
console.log("[AutoTuning] Nouveaux seuils :", securityConfig.thresholds);
|
|
3181
|
+
console.log("[AutoTuning] Nouveaux poids :", securityConfig.weights);
|
|
3182
|
+
console.log("[AutoTuning] Nouveaux patterns :", securityConfig.patterns);
|
|
2961
3183
|
}
|
|
2962
3184
|
|
|
2963
3185
|
/**
|