@anonympins/fingerprint 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +56 -11
- package/package.json +2 -1
- package/src/js/fingerprint.client.js +139 -0
- package/src/js/fingerprint.js +360 -191
- package/src/js/fingerprint.utils.js +184 -0
- package/src/js/library.js +1 -1
- package/src/js/pow.solver.inline.js +12 -4
- package/src/js/pow.solver.js +12 -4
- package/src/js/tests/fingerprint.isMalicious.test.js +234 -116
- package/src/js/tests/fingerprint.test.js +132 -1
- package/src/js/tests/ja3AnomalyDetector.test.js +1 -0
- package/src/php/AutoTuner.php +71 -0
- package/src/php/Challenge/ChallengeUtils.php +302 -9
- package/src/php/FingerprintEngine.php +40 -2
- package/src/php/Tests/ChallengeUtilsTest.php +208 -81
- package/src/php/Tests/MaliciousPatternsTest.php +104 -0
- package/src/php/Utils/BigInt.php +202 -144
- package/src/php/Utils/MaliciousPatterns.php +74 -58
package/src/js/fingerprint.js
CHANGED
|
@@ -8,6 +8,8 @@ import {DynamicWasmGenerator} from "./dynamic-wasm.js";
|
|
|
8
8
|
import {readFileSync, existsSync} from "node:fs";
|
|
9
9
|
import {fileURLToPath} from "node:url";
|
|
10
10
|
import {dirname, join, resolve} from "node:path";
|
|
11
|
+
import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "./fingerprint.utils.js";
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
15
|
const __dirname = dirname(__filename);
|
|
@@ -140,23 +142,7 @@ function getActiveMappingForRequest(headers) {
|
|
|
140
142
|
return null;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
if (!fpString || !mapping || !mapping.keys) return fpString;
|
|
145
|
-
const reverseKeys = {};
|
|
146
|
-
for (const [orig, rand] of Object.entries(mapping.keys)) {
|
|
147
|
-
reverseKeys[rand] = orig;
|
|
148
|
-
}
|
|
149
|
-
const parts = fpString.split('|');
|
|
150
|
-
const mappedParts = parts.map(part => {
|
|
151
|
-
const pair = part.split(':');
|
|
152
|
-
if (pair.length === 2) {
|
|
153
|
-
const origKey = reverseKeys[pair[0]] || pair[0];
|
|
154
|
-
return `${origKey}:${pair[1]}`;
|
|
155
|
-
}
|
|
156
|
-
return part;
|
|
157
|
-
});
|
|
158
|
-
return mappedParts.join('|');
|
|
159
|
-
}
|
|
145
|
+
|
|
160
146
|
|
|
161
147
|
const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
162
148
|
const base64UrlDecode = (str) => {
|
|
@@ -168,6 +154,31 @@ const base64UrlDecode = (str) => {
|
|
|
168
154
|
};
|
|
169
155
|
|
|
170
156
|
export function generateStatelessTicket(payload) {
|
|
157
|
+
let ed25519Key = process.env.ED25519_PRIVATE_KEY;
|
|
158
|
+
if (ed25519Key) {
|
|
159
|
+
try {
|
|
160
|
+
ed25519Key = ed25519Key.replace(/\\n/g, '\n');
|
|
161
|
+
const serialized = JSON.stringify(payload);
|
|
162
|
+
let signature;
|
|
163
|
+
try {
|
|
164
|
+
signature = crypto.sign(undefined, Buffer.from(serialized), {
|
|
165
|
+
key: ed25519Key,
|
|
166
|
+
format: 'pem',
|
|
167
|
+
type: 'pkcs8'
|
|
168
|
+
});
|
|
169
|
+
} catch (signErr) {
|
|
170
|
+
signature = crypto.sign(null, Buffer.from(serialized), {
|
|
171
|
+
key: ed25519Key,
|
|
172
|
+
format: 'pem',
|
|
173
|
+
type: 'pkcs8'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return `ed25519.${base64UrlEncode(Buffer.from(serialized))}.${base64UrlEncode(signature)}`;
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error('[Fingerprint] Ed25519 signing failed, falling back to symmetric:', e.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
171
182
|
const secret = getPowSecret();
|
|
172
183
|
const key = crypto.createHash('sha256').update(secret).digest();
|
|
173
184
|
const iv = crypto.randomBytes(16);
|
|
@@ -181,6 +192,40 @@ export function generateStatelessTicket(payload) {
|
|
|
181
192
|
|
|
182
193
|
export function parseStatelessTicket(ticket) {
|
|
183
194
|
try {
|
|
195
|
+
if (ticket.startsWith('ed25519.')) {
|
|
196
|
+
const parts = ticket.split('.');
|
|
197
|
+
if (parts.length !== 3) return null;
|
|
198
|
+
const payloadBuffer = base64UrlDecode(parts[1]);
|
|
199
|
+
const signatureBuffer = base64UrlDecode(parts[2]);
|
|
200
|
+
let publicKey = process.env.ED25519_PUBLIC_KEY;
|
|
201
|
+
if (!publicKey) {
|
|
202
|
+
console.error('[Fingerprint] ED25519_PUBLIC_KEY is not defined in environment.');
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
publicKey = publicKey.replace(/\\n/g, '\n');
|
|
206
|
+
|
|
207
|
+
let isVerified = false;
|
|
208
|
+
try {
|
|
209
|
+
isVerified = crypto.verify(undefined, payloadBuffer, {
|
|
210
|
+
key: publicKey,
|
|
211
|
+
format: 'pem',
|
|
212
|
+
type: 'spki'
|
|
213
|
+
}, signatureBuffer);
|
|
214
|
+
} catch (verifyErr) {
|
|
215
|
+
try {
|
|
216
|
+
isVerified = crypto.verify(null, payloadBuffer, {
|
|
217
|
+
key: publicKey,
|
|
218
|
+
format: 'pem',
|
|
219
|
+
type: 'spki'
|
|
220
|
+
}, signatureBuffer);
|
|
221
|
+
} catch (verifyErr2) {
|
|
222
|
+
isVerified = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!isVerified) return null;
|
|
226
|
+
return JSON.parse(payloadBuffer.toString('utf8'));
|
|
227
|
+
}
|
|
228
|
+
|
|
184
229
|
const parts = ticket.split('.');
|
|
185
230
|
if (parts.length !== 3) return null;
|
|
186
231
|
|
|
@@ -239,26 +284,6 @@ async function checkChallengeRateLimit(clientIp) {
|
|
|
239
284
|
return true;
|
|
240
285
|
}
|
|
241
286
|
|
|
242
|
-
/**
|
|
243
|
-
* @private
|
|
244
|
-
* Deep merges two objects. The `source` object's properties overwrite the `target`'s.
|
|
245
|
-
* @param {object} target - The target object.
|
|
246
|
-
* @param {object} source - The source object.
|
|
247
|
-
* @returns {object} The merged object.
|
|
248
|
-
*/
|
|
249
|
-
function deepMerge(target, source) {
|
|
250
|
-
const output = { ...target };
|
|
251
|
-
if (target && typeof target === 'object' && source && typeof source === 'object') {
|
|
252
|
-
Object.keys(source).forEach(key => {
|
|
253
|
-
if (source[key] && typeof source[key] === 'object' && key in target) {
|
|
254
|
-
output[key] = deepMerge(target[key], source[key]);
|
|
255
|
-
} else {
|
|
256
|
-
output[key] = source[key];
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
return output;
|
|
261
|
-
}
|
|
262
287
|
|
|
263
288
|
const securityProfiles = {
|
|
264
289
|
/**
|
|
@@ -593,44 +618,6 @@ function getTlsFingerprint(context) {
|
|
|
593
618
|
return { ja3, ja4 };
|
|
594
619
|
}
|
|
595
620
|
|
|
596
|
-
/**
|
|
597
|
-
* Analyses a raw JA3 string.
|
|
598
|
-
* Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
|
|
599
|
-
* @param {string} ja3String
|
|
600
|
-
* @returns {object|null}
|
|
601
|
-
*/
|
|
602
|
-
export function parseJa3(ja3String) {
|
|
603
|
-
if (!ja3String || typeof ja3String !== 'string') {
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
const parts = ja3String.split(',');
|
|
607
|
-
if (parts.length !== 5) {
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
return {
|
|
611
|
-
tlsVersion: parseInt(parts[0], 10),
|
|
612
|
-
ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
|
|
613
|
-
extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
|
|
614
|
-
curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
|
|
615
|
-
points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Creates a stable hash based on device characteristics, independent of the IP.
|
|
621
|
-
* This is our "level 2 fingerprint".
|
|
622
|
-
* @param {object} context - The request context.
|
|
623
|
-
* @returns {string} A hash representing the device.
|
|
624
|
-
*/
|
|
625
|
-
function getHeaderSignature(context) {
|
|
626
|
-
if (!context.rawHeaders) return '';
|
|
627
|
-
const headerKeys = [];
|
|
628
|
-
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
629
|
-
headerKeys.push(context.rawHeaders[i]);
|
|
630
|
-
}
|
|
631
|
-
return cyrb53(headerKeys.sort().join(','));
|
|
632
|
-
}
|
|
633
|
-
|
|
634
621
|
/**
|
|
635
622
|
* Returns the client-side fingerprint if available, otherwise computes a server-side hash.
|
|
636
623
|
* This aligns with the test's expectation for prioritization.
|
|
@@ -766,79 +753,6 @@ function hasGrease(values) {
|
|
|
766
753
|
return values.some(val => GREASE_VALUES.includes(val));
|
|
767
754
|
}
|
|
768
755
|
|
|
769
|
-
// Fonctions utilitaires
|
|
770
|
-
function parseUserAgent(ua) {
|
|
771
|
-
// Parser basique du User-Agent
|
|
772
|
-
const result = {};
|
|
773
|
-
|
|
774
|
-
// Détection du navigateur
|
|
775
|
-
if (ua.includes('Chrome') && !ua.includes('Edg')) {
|
|
776
|
-
result.browser = 'Chrome';
|
|
777
|
-
const match = ua.match(/Chrome\/(\d+)/);
|
|
778
|
-
if (match) result.browser += `/${match[1]}`;
|
|
779
|
-
} else if (ua.includes('Firefox')) {
|
|
780
|
-
result.browser = 'Firefox';
|
|
781
|
-
const match = ua.match(/Firefox\/(\d+)/);
|
|
782
|
-
if (match) result.browser += `/${match[1]}`;
|
|
783
|
-
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
|
|
784
|
-
result.browser = 'Safari';
|
|
785
|
-
const match = ua.match(/Version\/(\d+)/);
|
|
786
|
-
if (match) result.browser += `/${match[1]}`;
|
|
787
|
-
} else if (ua.includes('Edg')) {
|
|
788
|
-
result.browser = 'Edge';
|
|
789
|
-
const match = ua.match(/Edg\/(\d+)/);
|
|
790
|
-
if (match) result.browser += `/${match[1]}`;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Détection de l'OS
|
|
794
|
-
if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
|
|
795
|
-
else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
|
|
796
|
-
else if (ua.includes('Mac OS X')) result.os = 'macOS';
|
|
797
|
-
else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
|
|
798
|
-
else if (ua.includes('Android')) result.os = 'Android';
|
|
799
|
-
else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
|
|
800
|
-
|
|
801
|
-
// Détection du type d'appareil
|
|
802
|
-
if (ua.includes('Mobile')) result.device = 'mobile';
|
|
803
|
-
else if (ua.includes('Tablet')) result.device = 'tablet';
|
|
804
|
-
else result.device = 'desktop';
|
|
805
|
-
|
|
806
|
-
return result;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function normalizeReferer(referer) {
|
|
810
|
-
try {
|
|
811
|
-
const url = new URL(referer);
|
|
812
|
-
return `${url.protocol}//${url.hostname}`;
|
|
813
|
-
} catch {
|
|
814
|
-
return referer;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function isPrivateIp(ip) {
|
|
819
|
-
// Vérifier si l'IP est privée
|
|
820
|
-
const parts = ip.split('.');
|
|
821
|
-
if (parts.length !== 4) return false;
|
|
822
|
-
const first = parseInt(parts[0]);
|
|
823
|
-
return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function hashNetwork(ip, prefix = 24) {
|
|
827
|
-
// Hash du réseau (masque /24 ou /16)
|
|
828
|
-
const parts = ip.split('.');
|
|
829
|
-
if (parts.length !== 4) return null;
|
|
830
|
-
const maskBytes = prefix / 8;
|
|
831
|
-
const network = parts.slice(0, maskBytes).join('.');
|
|
832
|
-
// Hash simple
|
|
833
|
-
let hash = 0;
|
|
834
|
-
for (let i = 0; i < network.length; i++) {
|
|
835
|
-
const char = network.charCodeAt(i);
|
|
836
|
-
hash = ((hash << 5) - hash) + char;
|
|
837
|
-
hash = hash & hash;
|
|
838
|
-
}
|
|
839
|
-
return hash.toString(16);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
756
|
/**
|
|
843
757
|
* Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
|
|
844
758
|
* @param {string} nonce - Unique nonce for the challenge.
|
|
@@ -1091,19 +1005,30 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
|
|
|
1091
1005
|
return finalHash === parseInt(solution, 10);
|
|
1092
1006
|
};
|
|
1093
1007
|
|
|
1094
|
-
export function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1008
|
+
export async function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
|
|
1009
|
+
let combined = [];
|
|
1010
|
+
for (let i = 0; i < queries.length; i++) {
|
|
1011
|
+
const idx = queries[i];
|
|
1012
|
+
const block = generateBlock(seed, idx);
|
|
1013
|
+
combined.push(...block);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const assoc = await store.get(`coop-assoc:${nonce}`);
|
|
1017
|
+
if (assoc) {
|
|
1018
|
+
const peerSeed = assoc.peerSeed;
|
|
1019
|
+
const peerBlockIdx = assoc.peerBlockIdx;
|
|
1020
|
+
if (peerSeed !== undefined && peerBlockIdx !== undefined) {
|
|
1021
|
+
const peerBlock = generateBlock(peerSeed, peerBlockIdx);
|
|
1022
|
+
combined.push(...peerBlock);
|
|
1023
|
+
}
|
|
1024
|
+
await store.delete(`coop-assoc:${nonce}`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const nonceBytes = Buffer.from(nonce + ":" + clientSecret, "utf8");
|
|
1028
|
+
const finalBlock = Buffer.concat([Buffer.from(combined), nonceBytes]);
|
|
1029
|
+
|
|
1030
|
+
const hash = crypto.createHash("sha256").update(finalBlock).digest("hex");
|
|
1031
|
+
return hash === solution;
|
|
1107
1032
|
}
|
|
1108
1033
|
|
|
1109
1034
|
function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
@@ -1116,26 +1041,31 @@ function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
|
1116
1041
|
return block;
|
|
1117
1042
|
}
|
|
1118
1043
|
|
|
1119
|
-
export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false) => {
|
|
1044
|
+
export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false, zkpProof = '') => {
|
|
1120
1045
|
// Input validation: ensure the ticket is a non-empty string with the correct format.
|
|
1121
|
-
|
|
1122
|
-
|
|
1046
|
+
if (typeof ticket !== 'string' || ticket.length === 0) return false;
|
|
1123
1047
|
// 1. Resolve stateless ticket first (zero database I/O cost)
|
|
1124
1048
|
const statelessData = parseStatelessTicket(ticket);
|
|
1125
1049
|
if (statelessData) {
|
|
1126
1050
|
const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
|
|
1127
|
-
|
|
1128
1051
|
if (!expiry || Date.now() > expiry) {
|
|
1129
1052
|
return false;
|
|
1130
1053
|
}
|
|
1131
|
-
|
|
1054
|
+
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1055
|
+
const expectedY = storedDeviceHash.split(':')[1];
|
|
1056
|
+
if (zkpProof) {
|
|
1057
|
+
const [y, t, s] = zkpProof.split(':');
|
|
1058
|
+
if (y === expectedY && verifyZkpProof(y, t, s)) {
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1132
1064
|
if (ip === originalIp) return true;
|
|
1133
1065
|
const currentSubnet = getIpSubnet(ip);
|
|
1134
1066
|
const originalSubnet = getIpSubnet(originalIp);
|
|
1135
1067
|
if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
|
|
1136
|
-
|
|
1137
1068
|
if (!allowCrossNetworkRoaming) return false;
|
|
1138
|
-
|
|
1139
1069
|
return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
|
|
1140
1070
|
}
|
|
1141
1071
|
|
|
@@ -1148,6 +1078,16 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
|
|
|
1148
1078
|
await store.delete(`ticket:${ticket}`);
|
|
1149
1079
|
return false;
|
|
1150
1080
|
}
|
|
1081
|
+
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1082
|
+
const expectedY = storedDeviceHash.split(':')[1];
|
|
1083
|
+
if (zkpProof) {
|
|
1084
|
+
const [y, t, s] = zkpProof.split(':');
|
|
1085
|
+
if (y === expectedY && verifyZkpProof(y, t, s)) {
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1151
1091
|
|
|
1152
1092
|
if (ip === originalIp) return true;
|
|
1153
1093
|
const currentSubnet = getIpSubnet(ip);
|
|
@@ -1255,7 +1195,7 @@ function getHeaderAnomalies(context) {
|
|
|
1255
1195
|
};
|
|
1256
1196
|
}
|
|
1257
1197
|
|
|
1258
|
-
export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
|
|
1198
|
+
export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
|
|
1259
1199
|
const sizeMb = securityConfig?.pospace?.sizeMb || 100;
|
|
1260
1200
|
const numQueries = securityConfig?.pospace?.numQueries || 10;
|
|
1261
1201
|
|
|
@@ -1268,13 +1208,27 @@ export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, origina
|
|
|
1268
1208
|
}
|
|
1269
1209
|
}
|
|
1270
1210
|
|
|
1271
|
-
|
|
1211
|
+
const challenge = {
|
|
1272
1212
|
type: "pospace",
|
|
1273
1213
|
nonce: nonce,
|
|
1274
1214
|
sizeMb,
|
|
1275
1215
|
queries,
|
|
1276
1216
|
path: originalUrl
|
|
1277
1217
|
};
|
|
1218
|
+
|
|
1219
|
+
const peer = await findPeerInSubnet(clientIp, nonce);
|
|
1220
|
+
if (peer) {
|
|
1221
|
+
challenge.peerId = peer.nodeId;
|
|
1222
|
+
challenge.peerBlockIdx = Math.floor(Math.random() * maxBlocks);
|
|
1223
|
+
|
|
1224
|
+
await store.set(`coop-assoc:${nonce}`, {
|
|
1225
|
+
peerNodeId: peer.nodeId,
|
|
1226
|
+
peerSeed: peer.seed,
|
|
1227
|
+
peerBlockIdx: challenge.peerBlockIdx
|
|
1228
|
+
}, 120);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return challenge;
|
|
1278
1232
|
}
|
|
1279
1233
|
|
|
1280
1234
|
function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
|
|
@@ -1418,6 +1372,20 @@ const injectionPatterns = {
|
|
|
1418
1372
|
traversal: /(\.\.\/|\.\.\\)/,
|
|
1419
1373
|
// Remote Command Execution (RCE)
|
|
1420
1374
|
rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
|
|
1375
|
+
// Server-Side Request Forgery (SSRF) - Detects local/private IPs and hosts
|
|
1376
|
+
ssrf: /((?:https?:\/\/)?(?:127\.\d+\.\d+\.\d+\b|169\.254\.169\.254\b|10\.\d+\.\d+\.\d+\b|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+\b|192\.168\.\d+\.\d+\b|localhost\b|0\.0\.0\.0\b|\[[0:]+1\](?=\W|$)))/i,
|
|
1377
|
+
// Carriage Return Line Feed (CRLF) Injection / HTTP Response Splitting
|
|
1378
|
+
crlf: /[\r\n]|%0[ad]/i,
|
|
1379
|
+
// Cross-Site Scripting (XSS) - Fast native regex fallback
|
|
1380
|
+
xss: /(<script|javascript:|on\w+\s*=|alert\s*\(|confirm\s*\(|prompt\s*\(|<img\s+src[^>]+onerror|<iframe)/i,
|
|
1381
|
+
// Open Redirect - Basic detection of external protocol/URLs
|
|
1382
|
+
openRedirect: /^(https?:)?\/\/(?![^\/]*?(localhost|127\.0\.0\.1))[^\s\/]+/i,
|
|
1383
|
+
// Local/Remote File Inclusion (LFI/RFI)
|
|
1384
|
+
lfi: /(?:etc\/passwd|win\.ini|boot\.ini|php:\/\/filter|data:\/\/|zip:\/\/)/i,
|
|
1385
|
+
// Shellshock (CVE-2014-6271)
|
|
1386
|
+
shellshock: /\(\)\s*\{\s*:\s*;\s*\}\s*/i,
|
|
1387
|
+
// NoSQL Injection (MongoDB query operators)
|
|
1388
|
+
nosql: /\$(?:eq|ne|gt|gte|lt|lte|in|nin|and|or|nor|not|expr|jsonSchema|mod|regex|text|where|elemMatch)/i
|
|
1421
1389
|
};
|
|
1422
1390
|
|
|
1423
1391
|
/**
|
|
@@ -3147,12 +3115,33 @@ function parseGraphQLQuery(body) {
|
|
|
3147
3115
|
export class FingerprintEngine {
|
|
3148
3116
|
constructor(securityConfig) {
|
|
3149
3117
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
3150
|
-
|
|
3118
|
+
|
|
3119
|
+
// Dynamically bind Ed25519 keys if passed via config
|
|
3120
|
+
if (securityConfig && securityConfig.ed25519_private_key) {
|
|
3121
|
+
process.env.ED25519_PRIVATE_KEY = securityConfig.ed25519_private_key;
|
|
3122
|
+
}
|
|
3123
|
+
if (securityConfig && securityConfig.ed25519_public_key) {
|
|
3124
|
+
process.env.ED25519_PUBLIC_KEY = securityConfig.ed25519_public_key;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
let finalConfig = securityConfig;
|
|
3128
|
+
if (securityConfig && securityConfig.autotuning && securityConfig.autotuning.savePath) {
|
|
3129
|
+
const sPath = securityConfig.autotuning.savePath;
|
|
3130
|
+
if (existsSync(sPath)) {
|
|
3131
|
+
try {
|
|
3132
|
+
const savedConfig = JSON.parse(readFileSync(sPath, 'utf-8'));
|
|
3133
|
+
finalConfig = deepMerge(securityConfig, savedConfig);
|
|
3134
|
+
} catch (e) {
|
|
3135
|
+
console.warn(`[Fingerprint] Failed to auto-load optimized config from ${sPath}:`, e.message);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
this.securityConfig = finalConfig;
|
|
3151
3140
|
this.isProduction = isProduction;
|
|
3152
3141
|
this._allowlist = this._buildAllowlist();
|
|
3153
|
-
this._validateConfig(
|
|
3154
|
-
this.verbose =
|
|
3155
|
-
this.dryRun =
|
|
3142
|
+
this._validateConfig(finalConfig); // Validate the configuration
|
|
3143
|
+
this.verbose = finalConfig.verbose || false;
|
|
3144
|
+
this.dryRun = finalConfig.dryRun || false;
|
|
3156
3145
|
}
|
|
3157
3146
|
|
|
3158
3147
|
/**
|
|
@@ -3173,7 +3162,8 @@ export class FingerprintEngine {
|
|
|
3173
3162
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
3174
3163
|
'trustedProxies',
|
|
3175
3164
|
'wasm',
|
|
3176
|
-
'similarityThreshold'
|
|
3165
|
+
'similarityThreshold',
|
|
3166
|
+
'ed25519_private_key', 'ed25519_public_key'
|
|
3177
3167
|
]);
|
|
3178
3168
|
|
|
3179
3169
|
// 1. Check for essential keys
|
|
@@ -3477,7 +3467,17 @@ export class FingerprintEngine {
|
|
|
3477
3467
|
sanitizeProxyHeaders(requestContext, this.securityConfig);
|
|
3478
3468
|
|
|
3479
3469
|
const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
3480
|
-
|
|
3470
|
+
|
|
3471
|
+
if (query.coop_op) {
|
|
3472
|
+
const result = await handleCooperativeRequest(query, clientIp);
|
|
3473
|
+
return {
|
|
3474
|
+
action: 'challenge',
|
|
3475
|
+
status: 200,
|
|
3476
|
+
body: result
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
3481
3481
|
|
|
3482
3482
|
this._log('Processing request', { clientIp, path, isStatic });
|
|
3483
3483
|
|
|
@@ -3699,7 +3699,7 @@ export class FingerprintEngine {
|
|
|
3699
3699
|
isValid
|
|
3700
3700
|
});
|
|
3701
3701
|
} else if (pow_type === "pospace" && pow_solution_space) {
|
|
3702
|
-
const isSpaceValid = verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
|
|
3702
|
+
const isSpaceValid = await verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
|
|
3703
3703
|
isValid = isSpaceValid;
|
|
3704
3704
|
if (isValid) {
|
|
3705
3705
|
const ttl = finalTtl || 3600000;
|
|
@@ -3707,7 +3707,7 @@ export class FingerprintEngine {
|
|
|
3707
3707
|
expiry: Date.now() + ttl,
|
|
3708
3708
|
originalIp: clientIp,
|
|
3709
3709
|
deviceId,
|
|
3710
|
-
deviceHash
|
|
3710
|
+
deviceHash: currentDeviceHash
|
|
3711
3711
|
});
|
|
3712
3712
|
}
|
|
3713
3713
|
}
|
|
@@ -3980,7 +3980,8 @@ export class FingerprintEngine {
|
|
|
3980
3980
|
// 1. La requête est suspecte ET il n'y a pas de ticket valide.
|
|
3981
3981
|
// OU
|
|
3982
3982
|
// 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
|
|
3983
|
-
const
|
|
3983
|
+
const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
|
|
3984
|
+
const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
|
|
3984
3985
|
const mustReChallenge = isSuspiciousHigh && hasValidTicket;
|
|
3985
3986
|
|
|
3986
3987
|
if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
|
|
@@ -4081,7 +4082,7 @@ export class FingerprintEngine {
|
|
|
4081
4082
|
return decision;
|
|
4082
4083
|
}
|
|
4083
4084
|
if (this.securityConfig.enableProofOfSpace) {
|
|
4084
|
-
const spaceChallenge = generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
4085
|
+
const spaceChallenge = await generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
4085
4086
|
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
4086
4087
|
await store.set(`secret:${nonce}`, {
|
|
4087
4088
|
clientSecret,
|
|
@@ -4262,6 +4263,117 @@ const staticExtensions = new RegExp(
|
|
|
4262
4263
|
);
|
|
4263
4264
|
const isStaticResource = (path) => staticExtensions.test(path);
|
|
4264
4265
|
|
|
4266
|
+
export async function registerCooperativeNode(clientIp, nodeId, seed) {
|
|
4267
|
+
const subnet = getIpSubnet(clientIp);
|
|
4268
|
+
if (!subnet) return;
|
|
4269
|
+
|
|
4270
|
+
const key = `coop-pospace:subnet:${subnet}`;
|
|
4271
|
+
const nodes = (await store.get(key)) || {};
|
|
4272
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4273
|
+
|
|
4274
|
+
// Clean up expired nodes (older than 120 seconds)
|
|
4275
|
+
const cleanedNodes = {};
|
|
4276
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
4277
|
+
if (now - node.timestamp < 120) {
|
|
4278
|
+
cleanedNodes[id] = node;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
cleanedNodes[nodeId] = {
|
|
4283
|
+
nodeId,
|
|
4284
|
+
seed,
|
|
4285
|
+
timestamp: now
|
|
4286
|
+
};
|
|
4287
|
+
|
|
4288
|
+
await store.set(key, cleanedNodes, 120);
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
export async function findPeerInSubnet(clientIp, excludeNodeId) {
|
|
4292
|
+
const subnet = getIpSubnet(clientIp);
|
|
4293
|
+
if (!subnet) return null;
|
|
4294
|
+
|
|
4295
|
+
const key = `coop-pospace:subnet:${subnet}`;
|
|
4296
|
+
const nodes = (await store.get(key)) || {};
|
|
4297
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4298
|
+
|
|
4299
|
+
const activePeers = [];
|
|
4300
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
4301
|
+
if (id !== excludeNodeId && now - node.timestamp < 120) {
|
|
4302
|
+
activePeers.push(node);
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
if (activePeers.length === 0) return null;
|
|
4307
|
+
|
|
4308
|
+
// Select a random peer
|
|
4309
|
+
const randomIndex = Math.floor(Math.random() * activePeers.length);
|
|
4310
|
+
return activePeers[randomIndex];
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
|
|
4314
|
+
const op = params.coop_op;
|
|
4315
|
+
if (!op) return null;
|
|
4316
|
+
|
|
4317
|
+
const nodeId = params.node_id || '';
|
|
4318
|
+
if (!nodeId) {
|
|
4319
|
+
return { error: 'Missing node_id' };
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
switch (op) {
|
|
4323
|
+
case 'register':
|
|
4324
|
+
const seed = params.seed || '';
|
|
4325
|
+
await registerCooperativeNode(clientIp, nodeId, seed);
|
|
4326
|
+
return { status: 'registered' };
|
|
4327
|
+
|
|
4328
|
+
case 'request_peer_block':
|
|
4329
|
+
const peerId = params.peer_id || '';
|
|
4330
|
+
const blockIdx = parseInt(params.block_idx || '0', 10);
|
|
4331
|
+
const requestId = params.req_id || '';
|
|
4332
|
+
if (!peerId || !requestId) {
|
|
4333
|
+
return { error: 'Invalid parameters' };
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
const queueKey = `coop-mailbox:queue:${peerId}`;
|
|
4337
|
+
const requests = (await store.get(queueKey)) || [];
|
|
4338
|
+
requests.push({
|
|
4339
|
+
req_id: requestId,
|
|
4340
|
+
requester_id: nodeId,
|
|
4341
|
+
block_idx: blockIdx
|
|
4342
|
+
});
|
|
4343
|
+
await store.set(queueKey, requests, 30);
|
|
4344
|
+
return { status: 'queued' };
|
|
4345
|
+
|
|
4346
|
+
case 'poll_requests':
|
|
4347
|
+
const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
|
|
4348
|
+
const polledRequests = (await store.get(pollQueueKey)) || [];
|
|
4349
|
+
await store.delete(pollQueueKey);
|
|
4350
|
+
return { requests: polledRequests };
|
|
4351
|
+
|
|
4352
|
+
case 'respond_block':
|
|
4353
|
+
const requesterId = params.requester_id || '';
|
|
4354
|
+
const respondRequestId = params.req_id || '';
|
|
4355
|
+
const blockData = params.block_data || '';
|
|
4356
|
+
if (!requesterId || !respondRequestId) {
|
|
4357
|
+
return { error: 'Invalid parameters' };
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
const responseKey = `coop-mailbox:res:${requesterId}:${respondRequestId}`;
|
|
4361
|
+
await store.set(responseKey, { block_data: blockData }, 30);
|
|
4362
|
+
return { status: 'delivered' };
|
|
4363
|
+
|
|
4364
|
+
case 'poll_response':
|
|
4365
|
+
const pollResponseRequestId = params.req_id || '';
|
|
4366
|
+
const pollResponseKey = `coop-mailbox:res:${nodeId}:${pollResponseRequestId}`;
|
|
4367
|
+
const data = await store.get(pollResponseKey);
|
|
4368
|
+
if (data) {
|
|
4369
|
+
await store.delete(pollResponseKey);
|
|
4370
|
+
return { status: 'ready', block_data: data.block_data };
|
|
4371
|
+
}
|
|
4372
|
+
return { status: 'pending' };
|
|
4373
|
+
}
|
|
4374
|
+
return null;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4265
4377
|
|
|
4266
4378
|
/** @type {Map<number, number>} Cache des TTL optimisés par score de suspicion (clés de 0 à 100 par pas de 10) */
|
|
4267
4379
|
let optimizedTtlCache = new Map();
|
|
@@ -4630,7 +4742,7 @@ function getTcpAnomalyScore(context) {
|
|
|
4630
4742
|
* @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
|
|
4631
4743
|
* @returns {boolean} - True si un pattern malveillant est détecté.
|
|
4632
4744
|
*/
|
|
4633
|
-
function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
|
|
4745
|
+
function isMalicious(str, typesToDetect = Object.keys(injectionPatterns).filter(k => k !== 'openRedirect')) {
|
|
4634
4746
|
if (typeof str !== 'string') return false;
|
|
4635
4747
|
|
|
4636
4748
|
for (const type of typesToDetect) {
|
|
@@ -5040,6 +5152,7 @@ export const __internal = {
|
|
|
5040
5152
|
getCompositeDeviceHash,
|
|
5041
5153
|
getSuspicionVector,
|
|
5042
5154
|
getTlsSessionId,
|
|
5155
|
+
pruneTrafficData,
|
|
5043
5156
|
cyrb53, // Export for testing
|
|
5044
5157
|
FingerprintBuilder, // Export for testing
|
|
5045
5158
|
calculateTarget,
|
|
@@ -5068,9 +5181,14 @@ export const __internal = {
|
|
|
5068
5181
|
getIpReputationScore, // Expose for testing
|
|
5069
5182
|
updateIpReputationScore, // Expose for testing
|
|
5070
5183
|
setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
|
|
5184
|
+
verifyZkpProof,
|
|
5185
|
+
modPow,
|
|
5071
5186
|
parseTcpSyn, // Expose for testing
|
|
5072
5187
|
classifyTcpOs, // Expose for testing
|
|
5073
|
-
getTcpAnomalyScore // Expose for testing
|
|
5188
|
+
getTcpAnomalyScore, // Expose for testing,
|
|
5189
|
+
registerCooperativeNode,
|
|
5190
|
+
findPeerInSubnet,
|
|
5191
|
+
handleCooperativeRequest
|
|
5074
5192
|
};
|
|
5075
5193
|
|
|
5076
5194
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -5132,13 +5250,55 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
5132
5250
|
|
|
5133
5251
|
return [...suspiciousLogs, ...selectedPassed];
|
|
5134
5252
|
}
|
|
5253
|
+
/**
|
|
5254
|
+
* Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
|
|
5255
|
+
* @private
|
|
5256
|
+
*/
|
|
5257
|
+
function pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup) {
|
|
5258
|
+
if (!Array.isArray(trafficData)) return;
|
|
5259
|
+
const now = Date.now();
|
|
5260
|
+
const removed = [];
|
|
5261
|
+
|
|
5262
|
+
// 1. Politique temporelle d'expiration
|
|
5263
|
+
if (maxAgeMs && maxAgeMs > 0) {
|
|
5264
|
+
const threshold = now - maxAgeMs;
|
|
5265
|
+
let i = 0;
|
|
5266
|
+
while (i < trafficData.length) {
|
|
5267
|
+
const log = trafficData[i];
|
|
5268
|
+
const logTs = log.timestamp || log.requestTimestamp || now;
|
|
5269
|
+
if (logTs < threshold) {
|
|
5270
|
+
removed.push(trafficData.splice(i, 1)[0]);
|
|
5271
|
+
} else {
|
|
5272
|
+
i++;
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5135
5276
|
|
|
5277
|
+
// 2. Politique de taille maximale (conserver les plus récents)
|
|
5278
|
+
if (maxDataPoints && maxDataPoints > 0 && trafficData.length > maxDataPoints) {
|
|
5279
|
+
const overflowCount = trafficData.length - maxDataPoints;
|
|
5280
|
+
const spliced = trafficData.splice(0, overflowCount);
|
|
5281
|
+
removed.push(...spliced);
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
// 3. Callback de nettoyage
|
|
5285
|
+
if (onCleanup && typeof onCleanup === 'function' && removed.length > 0) {
|
|
5286
|
+
try {
|
|
5287
|
+
onCleanup(removed);
|
|
5288
|
+
} catch (e) {
|
|
5289
|
+
console.error('[AutoTuning] Error in onCleanup callback:', e);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5136
5293
|
/**
|
|
5137
5294
|
* Executes a threshold optimization pass using collected traffic data.
|
|
5138
5295
|
* @private
|
|
5139
5296
|
*/
|
|
5140
|
-
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
|
|
5141
|
-
|
|
5297
|
+
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, tuningOptions = {}) {
|
|
5298
|
+
const { maxAgeMs, clearAfterTuning = false, onCleanup } = tuningOptions;
|
|
5299
|
+
|
|
5300
|
+
pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup);
|
|
5301
|
+
const sanitizedData = sanitizeTrafficData(trafficData);
|
|
5142
5302
|
|
|
5143
5303
|
const highConfidenceLogs = sanitizedData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
|
|
5144
5304
|
const highConfidenceRatio = sanitizedData.length > 0 ? highConfidenceLogs / sanitizedData.length : 0;
|
|
@@ -5155,12 +5315,6 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
5155
5315
|
}
|
|
5156
5316
|
return;
|
|
5157
5317
|
}
|
|
5158
|
-
|
|
5159
|
-
if (trafficData.length > maxDataPoints) {
|
|
5160
|
-
console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
|
|
5161
|
-
trafficData.splice(0, trafficData.length - maxDataPoints);
|
|
5162
|
-
}
|
|
5163
|
-
|
|
5164
5318
|
console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${sanitizedData.length} points de données assainis.`);
|
|
5165
5319
|
|
|
5166
5320
|
const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
|
|
@@ -5295,6 +5449,18 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
5295
5449
|
console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
|
|
5296
5450
|
}
|
|
5297
5451
|
}
|
|
5452
|
+
|
|
5453
|
+
if (clearAfterTuning) {
|
|
5454
|
+
const cleared = trafficData.splice(0, trafficData.length);
|
|
5455
|
+
if (onCleanup && typeof onCleanup === 'function' && cleared.length > 0) {
|
|
5456
|
+
try {
|
|
5457
|
+
onCleanup(cleared);
|
|
5458
|
+
} catch (e) {
|
|
5459
|
+
console.error('[AutoTuning] Error in onCleanup callback after clearing:', e);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
console.log(`[AutoTuning] Explicitly cleared ${cleared.length} processed traffic data points.`);
|
|
5463
|
+
}
|
|
5298
5464
|
}
|
|
5299
5465
|
|
|
5300
5466
|
/**
|
|
@@ -5321,6 +5487,9 @@ export function startThresholdAutoTuning(options) {
|
|
|
5321
5487
|
minDataPoints = 200,
|
|
5322
5488
|
maxDataPoints = 10000, // Limite par défaut à 10 000 entrées
|
|
5323
5489
|
savePath, // NOUVEAU: Chemin de sauvegarde optionnel
|
|
5490
|
+
maxAgeMs,
|
|
5491
|
+
clearAfterTuning = false,
|
|
5492
|
+
onCleanup,
|
|
5324
5493
|
} = options;
|
|
5325
5494
|
|
|
5326
5495
|
if (!securityConfig || !trafficData) {
|
|
@@ -5330,7 +5499,7 @@ export function startThresholdAutoTuning(options) {
|
|
|
5330
5499
|
console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
|
|
5331
5500
|
|
|
5332
5501
|
autoTuningJobId = setInterval(() => {
|
|
5333
|
-
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
|
|
5502
|
+
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, { maxAgeMs, clearAfterTuning, onCleanup });
|
|
5334
5503
|
}, interval);
|
|
5335
5504
|
}
|
|
5336
5505
|
|