@anonympins/fingerprint 0.6.1 → 0.6.4
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 +25 -0
- package/README.md +128 -119
- package/composer.json +2 -1
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +452 -109
- package/src/js/library.js +131 -54
- package/src/js/mongodb-store.js +13 -0
- package/src/js/problem-manager.js +6 -2
- package/src/js/redis-store.js +13 -0
- package/src/js/sql-store.js +80 -77
- package/src/js/tests/fingerprint.engine.test.js +2 -3
- package/src/js/tests/fingerprint.test.js +49 -12
- package/src/js/tests/metrics.test.js +157 -103
- package/src/php/Challenge/ChallengeUtils.php +8 -6
- package/src/php/Config/SecurityProfiles.php +8 -2
- package/src/php/FingerprintEngine.php +155 -41
- package/src/php/Optimization/OptimizationOperators.php +60 -36
- package/src/php/RequestContext.php +7 -0
- package/src/php/Store/IStore.php +6 -0
- package/src/php/Store/MongoDbStore.php +12 -0
- package/src/php/Store/RedisStore.php +12 -0
- package/src/php/Tests/ChallengeUtilsTest.php +6 -4
- package/src/php/Tests/FingerprintEngineTest.php +232 -0
- package/src/php/Tests/IpReputationTest.php +1 -0
- package/src/php/Tests/MetricsTest.php +97 -45
- package/src/php/Tests/RequestUtilsTest.php +14 -14
- package/src/php/Tests/config/ed25519_key.json +4 -0
- package/src/php/Utils/Env.php +50 -0
- package/src/php/Utils/MetricsManager.php +209 -166
- package/src/php/Utils/RequestUtils.php +105 -22
package/src/js/fingerprint.js
CHANGED
|
@@ -6,7 +6,7 @@ import {getProblemManager, problemManager} from "./problem-manager.js";
|
|
|
6
6
|
import {Optimization} from "./library.js";
|
|
7
7
|
import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
|
|
8
8
|
import {DynamicWasmGenerator} from "./dynamic-wasm.js";
|
|
9
|
-
import {readFileSync, existsSync} from "node:fs";
|
|
9
|
+
import {writeFileSync, readFileSync, existsSync} from "node:fs";
|
|
10
10
|
import {fileURLToPath} from "node:url";
|
|
11
11
|
import {dirname, join, resolve} from "node:path";
|
|
12
12
|
import {
|
|
@@ -564,6 +564,7 @@ const securityProfiles = {
|
|
|
564
564
|
},
|
|
565
565
|
allowCrossNetworkRoaming: true, // Profil balancé : tolérant par défaut
|
|
566
566
|
wasm: true,
|
|
567
|
+
filterWhitelist: 85.0, // Stratégie d'inspection modérée pour les IP/chemins en liste blanche
|
|
567
568
|
useAsymmetricTickets: true,
|
|
568
569
|
},
|
|
569
570
|
/**
|
|
@@ -603,6 +604,7 @@ const securityProfiles = {
|
|
|
603
604
|
},
|
|
604
605
|
challengeNewDevices: true, // Challenge all new devices
|
|
605
606
|
allowCrossNetworkRoaming: false, // Strict : interdiction de changer complètement de réseau sans re-challenge
|
|
607
|
+
filterWhitelist: true, // Tout comportement d'attaque certain bypass immédiatement la liste blanche
|
|
606
608
|
wasm: true,
|
|
607
609
|
useAsymmetricTickets: true,
|
|
608
610
|
},
|
|
@@ -643,6 +645,7 @@ const securityProfiles = {
|
|
|
643
645
|
},
|
|
644
646
|
isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json'),
|
|
645
647
|
allowCrossNetworkRoaming: false, // Les API ne doivent pas subir de roaming inter-IP suspect
|
|
648
|
+
filterWhitelist: 75.0, // Seuil bas pour parer au vol de clés/tokens API légitimes
|
|
646
649
|
wasm: true,
|
|
647
650
|
useAsymmetricTickets: true,
|
|
648
651
|
}
|
|
@@ -663,13 +666,17 @@ const securityProfiles = {
|
|
|
663
666
|
crossLayerInconsistencyScore: 0.4,
|
|
664
667
|
timeInconsistencyScore: 0.8,
|
|
665
668
|
tlsSpoofingScore: 0.6, // Moins critique pour les blogs
|
|
666
|
-
|
|
669
|
+
botScore: 0.8,
|
|
670
|
+
cookieDroppingScore: 0.7, // Moins critique, mais toujours un signal
|
|
671
|
+
threatIntelScore: 0.3, // Lower priority for a blog
|
|
672
|
+
clientHintsInconsistencyScore: 0.5,
|
|
673
|
+
clickVarianceScore: 0.5, // Moderate weight for click variance
|
|
674
|
+
subnetScore: 0.4, // Utile contre le spam de commentaires coordonné
|
|
667
675
|
ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
668
676
|
botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
669
677
|
tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
|
|
670
678
|
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
671
679
|
renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
672
|
-
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
673
680
|
},
|
|
674
681
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
675
682
|
patterns: {
|
|
@@ -686,6 +693,7 @@ const securityProfiles = {
|
|
|
686
693
|
},
|
|
687
694
|
allowCrossNetworkRoaming: true,
|
|
688
695
|
wasm: true,
|
|
696
|
+
filterWhitelist: 90.0, // Très tolérant, n'inspecte que si le score est presque au blocage
|
|
689
697
|
useAsymmetricTickets: true,
|
|
690
698
|
},
|
|
691
699
|
/**
|
|
@@ -705,13 +713,17 @@ const securityProfiles = {
|
|
|
705
713
|
crossLayerInconsistencyScore: 0.7,
|
|
706
714
|
timeInconsistencyScore: 0.9,
|
|
707
715
|
tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
|
|
708
|
-
|
|
716
|
+
botScore: 1.0,
|
|
717
|
+
cookieDroppingScore: 1.0, // Crucial pour la détection de bots e-commerce
|
|
718
|
+
threatIntelScore: 0.8, // Very important for e-commerce (scalping proxies)
|
|
719
|
+
clientHintsInconsistencyScore: 0.9, // Very important for e-commerce
|
|
720
|
+
clickVarianceScore: 0.8, // Very high weight for click variance
|
|
721
|
+
subnetScore: 0.9, // Crucial contre les attaques de scalping distribuées
|
|
709
722
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
710
723
|
botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
711
724
|
tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
|
|
712
725
|
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
713
726
|
renderingAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie de rendu,threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
714
|
-
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
715
727
|
},
|
|
716
728
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
717
729
|
patterns: {
|
|
@@ -729,6 +741,7 @@ const securityProfiles = {
|
|
|
729
741
|
challengeNewDevices: true, // New devices are suspicious in e-commerce
|
|
730
742
|
isApiRequest: (req) => req.path.startsWith('/api/cart') || req.path.startsWith('/api/stock') || req.path.startsWith('/api/checkout'),
|
|
731
743
|
allowCrossNetworkRoaming: false, // E-commerce : interdiction de changer de réseau sans re-challenge
|
|
744
|
+
filterWhitelist: true, // Tolérance zéro pour le scraping / scalping distribué
|
|
732
745
|
wasm: true,
|
|
733
746
|
useAsymmetricTickets: true,
|
|
734
747
|
}
|
|
@@ -2793,12 +2806,20 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
2793
2806
|
highScoreCount: 0,
|
|
2794
2807
|
deviceIds: [],
|
|
2795
2808
|
highScoreDevices: {},
|
|
2796
|
-
lastActivity: 0
|
|
2809
|
+
lastActivity: 0,
|
|
2810
|
+
ips: [],
|
|
2811
|
+
uas: []
|
|
2797
2812
|
};
|
|
2798
2813
|
|
|
2799
2814
|
if (!subnetData.highScoreDevices) {
|
|
2800
2815
|
subnetData.highScoreDevices = {};
|
|
2801
2816
|
}
|
|
2817
|
+
if (subnetData.ips instanceof Set) {
|
|
2818
|
+
subnetData.ips = Array.from(subnetData.ips);
|
|
2819
|
+
} else if (!subnetData.ips) {
|
|
2820
|
+
subnetData.ips = [];
|
|
2821
|
+
}
|
|
2822
|
+
if (!subnetData.uas) subnetData.uas = [];
|
|
2802
2823
|
|
|
2803
2824
|
// Utilisation d'un identifiant d'appareil stable (fingerprint matériel) plutôt que l'ID de cookie volatil
|
|
2804
2825
|
const currentDeviceHash = getCompositeDeviceHash(context);
|
|
@@ -2813,6 +2834,16 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
2813
2834
|
if (!subnetData.deviceIds.includes(stableFpId)) {
|
|
2814
2835
|
subnetData.deviceIds.push(stableFpId);
|
|
2815
2836
|
}
|
|
2837
|
+
|
|
2838
|
+
if (!subnetData.ips.includes(context.clientIp)) {
|
|
2839
|
+
subnetData.ips.push(context.clientIp);
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
const userAgent = context.headers?.['user-agent'] || '';
|
|
2843
|
+
if (userAgent && !subnetData.uas.includes(userAgent)) {
|
|
2844
|
+
subnetData.uas.push(userAgent);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2816
2847
|
subnetData.lastActivity = Date.now();
|
|
2817
2848
|
|
|
2818
2849
|
if (subnetData.deviceIds.length > 100) {
|
|
@@ -2823,6 +2854,8 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
2823
2854
|
delete subnetData.highScoreDevices[oldDeviceId];
|
|
2824
2855
|
}
|
|
2825
2856
|
}
|
|
2857
|
+
if (subnetData.ips.length > 100) subnetData.ips.shift();
|
|
2858
|
+
if (subnetData.uas.length > 50) subnetData.uas.shift();
|
|
2826
2859
|
|
|
2827
2860
|
await store.set(key, subnetData, 86400); // 24-hour TTL
|
|
2828
2861
|
}
|
|
@@ -2846,16 +2879,37 @@ async function getSubnetScore(context) {
|
|
|
2846
2879
|
|
|
2847
2880
|
let highScoreCount = subnetData.highScoreCount || 0;
|
|
2848
2881
|
let deviceCount = subnetData.deviceIds ? subnetData.deviceIds.length : 0;
|
|
2882
|
+
let ipCount = 1;
|
|
2883
|
+
if (subnetData.ips) {
|
|
2884
|
+
ipCount = subnetData.ips instanceof Set ? subnetData.ips.size : (subnetData.ips.length || 1);
|
|
2885
|
+
}
|
|
2886
|
+
let uaCount = subnetData.uas ? subnetData.uas.length : 1;
|
|
2849
2887
|
|
|
2850
2888
|
if (halfLives > 0) {
|
|
2851
|
-
|
|
2852
|
-
|
|
2889
|
+
const decay = Math.pow(2, halfLives);
|
|
2890
|
+
highScoreCount = Math.max(0, Math.floor(highScoreCount / decay));
|
|
2891
|
+
deviceCount = Math.max(0, Math.floor(deviceCount / decay));
|
|
2892
|
+
ipCount = Math.max(1, Math.floor(ipCount / decay));
|
|
2893
|
+
uaCount = Math.max(1, Math.floor(uaCount / decay));
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
if (deviceCount === 0) {
|
|
2897
|
+
return { subnetScore: 0.0 };
|
|
2853
2898
|
}
|
|
2854
2899
|
|
|
2855
|
-
|
|
2856
|
-
const
|
|
2900
|
+
// Calculs analogues continus (sans sauts brusques)
|
|
2901
|
+
const suspicionDensity = highScoreCount / deviceCount;
|
|
2902
|
+
const ipDeviceRatio = ipCount / deviceCount;
|
|
2903
|
+
|
|
2904
|
+
// Base score continu basé sur le volume de menaces
|
|
2905
|
+
const baseScore = 100 * (1 - Math.exp(-0.15 * highScoreCount));
|
|
2906
|
+
|
|
2907
|
+
// Multiplicateurs continus
|
|
2908
|
+
const densityMultiplier = 0.4 + (1.6 * suspicionDensity); // Favorise les densités de suspicion élevées
|
|
2909
|
+
const distributionMultiplier = 0.5 + (1.0 * ipDeviceRatio); // NAT (faible ratio IP/Device) vs Proxy distribué (fort ratio)
|
|
2857
2910
|
|
|
2858
|
-
|
|
2911
|
+
const finalScore = Math.min(100, Math.round(baseScore * densityMultiplier * distributionMultiplier * 10) / 10);
|
|
2912
|
+
return { subnetScore: finalScore };
|
|
2859
2913
|
}
|
|
2860
2914
|
|
|
2861
2915
|
/**
|
|
@@ -2877,17 +2931,31 @@ async function getBotnetClusterScore(context, stableFpHash) {
|
|
|
2877
2931
|
|
|
2878
2932
|
clusterData = clusterData.filter(entry => entry.timestamp > tenMinutesAgo);
|
|
2879
2933
|
const existingIndex = clusterData.findIndex(entry => entry.ip === context.clientIp);
|
|
2934
|
+
const userAgent = context.headers?.['user-agent'] || '';
|
|
2935
|
+
const subnet = getIpSubnet(context.clientIp) || 'unknown';
|
|
2936
|
+
|
|
2880
2937
|
if (existingIndex !== -1) {
|
|
2881
2938
|
clusterData[existingIndex].timestamp = now;
|
|
2939
|
+
clusterData[existingIndex].ua = userAgent;
|
|
2940
|
+
clusterData[existingIndex].subnet = subnet;
|
|
2882
2941
|
} else {
|
|
2883
|
-
clusterData.push({ ip: context.clientIp, timestamp: now });
|
|
2942
|
+
clusterData.push({ ip: context.clientIp, timestamp: now, ua: userAgent, subnet: subnet });
|
|
2884
2943
|
}
|
|
2885
2944
|
|
|
2886
2945
|
await store.set(key, clusterData, 600);
|
|
2887
2946
|
const uniqueIpsCount = clusterData.length;
|
|
2888
2947
|
let botnetClusterScore = 0;
|
|
2889
2948
|
if (uniqueIpsCount >= 2) {
|
|
2890
|
-
|
|
2949
|
+
// Calcul de la diversité des sous-réseaux et de la rotation des User-Agents
|
|
2950
|
+
const uniqueSubnets = new Set(clusterData.map(e => e.subnet)).size;
|
|
2951
|
+
const uniqueUserAgents = new Set(clusterData.map(e => e.ua).filter(Boolean)).size;
|
|
2952
|
+
|
|
2953
|
+
// Facteurs d'ajustement
|
|
2954
|
+
const subnetMultiplier = uniqueSubnets > 1 ? 1.3 : 0.6; // Réduit le score si même sous-réseau (NAT), l'augmente si distribué
|
|
2955
|
+
const uaRotationMultiplier = uniqueUserAgents > 1 ? 1.5 : 1.0; // Forte pénalité en cas de rotation d'en-tête UA
|
|
2956
|
+
|
|
2957
|
+
const baseScore = 100 * (1 - Math.exp(-0.35 * (uniqueIpsCount - 1)));
|
|
2958
|
+
botnetClusterScore = Math.min(100, Math.round(baseScore * subnetMultiplier * uaRotationMultiplier * 10) / 10);
|
|
2891
2959
|
}
|
|
2892
2960
|
return { botnetClusterScore };
|
|
2893
2961
|
}
|
|
@@ -3105,6 +3173,7 @@ function verifyTrapUrl(path, signature, nonce) {
|
|
|
3105
3173
|
* @property {(key: string, value: any, ttl?: number) => Promise<void>} set
|
|
3106
3174
|
* @property {(key: string) => Promise<boolean>} has
|
|
3107
3175
|
* @property {(key: string) => Promise<void>} delete
|
|
3176
|
+
* @property {() => Promise<void>} clear
|
|
3108
3177
|
*/
|
|
3109
3178
|
|
|
3110
3179
|
/**
|
|
@@ -3130,6 +3199,13 @@ const inMemoryStore = {
|
|
|
3130
3199
|
},
|
|
3131
3200
|
async has(key) { return this._map.has(key); },
|
|
3132
3201
|
async delete(key) { this._map.delete(key); },
|
|
3202
|
+
async clear() {
|
|
3203
|
+
this._map.clear();
|
|
3204
|
+
for (const timeoutId of this._timeouts.values()) {
|
|
3205
|
+
clearTimeout(timeoutId);
|
|
3206
|
+
}
|
|
3207
|
+
this._timeouts.clear();
|
|
3208
|
+
}
|
|
3133
3209
|
};
|
|
3134
3210
|
|
|
3135
3211
|
/** @type {IStore} */
|
|
@@ -3513,13 +3589,15 @@ function calculateTarget(suspicionFactor, securityConfig = {}) {
|
|
|
3513
3589
|
* @param {string} nonce
|
|
3514
3590
|
* @param {string} clientSecret
|
|
3515
3591
|
* @param {string} fingerprint
|
|
3592
|
+
* @param {string} clientIp
|
|
3593
|
+
* @param {string} tlsSessionId
|
|
3516
3594
|
* @returns {Buffer}
|
|
3517
3595
|
*/
|
|
3518
|
-
function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint) {
|
|
3596
|
+
function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp = '', tlsSessionId = '') {
|
|
3519
3597
|
const sortedFingerprint = (fingerprint || '').split('|').filter(p => p).sort().join('|');
|
|
3520
3598
|
// On concatène les chaînes, puis on les convertit en buffer une seule fois.
|
|
3521
3599
|
// Cela garantit que le client et le serveur travaillent sur la même base binaire.
|
|
3522
|
-
const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:`; // Le ':' final est le séparateur pour la solution.
|
|
3600
|
+
const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:${clientIp}:${tlsSessionId}:`; // Le ':' final est le séparateur pour la solution.
|
|
3523
3601
|
return Buffer.from(messageBase, 'utf8');
|
|
3524
3602
|
}
|
|
3525
3603
|
|
|
@@ -3532,10 +3610,11 @@ export function generateCpuTargetChallenge(
|
|
|
3532
3610
|
suspicionFactor,
|
|
3533
3611
|
originalUrl,
|
|
3534
3612
|
securityConfig,
|
|
3613
|
+
tlsSessionId = '',
|
|
3535
3614
|
) {
|
|
3536
3615
|
const target = calculateTarget(suspicionFactor, securityConfig);
|
|
3537
3616
|
// Le baseBlock est créé ici et sera stocké dans le contexte du challenge.
|
|
3538
|
-
const baseBlock = createCpuChallengeBaseBlock(nonce, null, ''
|
|
3617
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, null, '', clientIp, tlsSessionId);
|
|
3539
3618
|
return {
|
|
3540
3619
|
type: "cpu_target",
|
|
3541
3620
|
nonce: nonce,
|
|
@@ -3619,14 +3698,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
3619
3698
|
* @param {string} clientIp - The client's IP address.
|
|
3620
3699
|
* @returns {string} HTML content.
|
|
3621
3700
|
*/
|
|
3622
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
|
|
3701
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint, tlsSessionId = '') { // eslint-disable-line max-len
|
|
3623
3702
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
3624
3703
|
const safePath = sanitizeRedirectPath(path);
|
|
3625
3704
|
const solverCode = getPowSolverCode();
|
|
3626
3705
|
// On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
|
|
3627
3706
|
// Le fingerprint est maintenant passé directement en paramètre.
|
|
3628
3707
|
const fingerprint = originalFingerprint;
|
|
3629
|
-
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
|
|
3708
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp, tlsSessionId);
|
|
3630
3709
|
const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
|
|
3631
3710
|
|
|
3632
3711
|
// Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
|
|
@@ -3825,16 +3904,31 @@ export class FingerprintEngine {
|
|
|
3825
3904
|
|
|
3826
3905
|
// Auto-generate Ed25519 key pair on load if indicated and keys are not set
|
|
3827
3906
|
if (securityConfig && (securityConfig.useAsymmetricTickets || securityConfig.ed25519 === 'auto') && !process.env.ED25519_PRIVATE_KEY) {
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3907
|
+
const persistentKeyPath = join(configDir, 'ed25519_key.json');
|
|
3908
|
+
if (existsSync(persistentKeyPath)) {
|
|
3909
|
+
try {
|
|
3910
|
+
const keys = JSON.parse(readFileSync(persistentKeyPath, 'utf-8'));
|
|
3911
|
+
process.env.ED25519_PRIVATE_KEY = keys.privateKey;
|
|
3912
|
+
process.env.ED25519_PUBLIC_KEY = keys.publicKey;
|
|
3913
|
+
this._log('Persistent Ed25519 keys loaded from disk');
|
|
3914
|
+
} catch (e) {
|
|
3915
|
+
console.error('[Fingerprint] Failed to load persistent Ed25519 keys:', e.message);
|
|
3916
|
+
}
|
|
3917
|
+
} else {
|
|
3918
|
+
try {
|
|
3919
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
|
|
3920
|
+
privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
|
|
3921
|
+
publicKeyEncoding: { format: 'pem', type: 'spki' }
|
|
3922
|
+
});
|
|
3923
|
+
process.env.ED25519_PRIVATE_KEY = privateKey;
|
|
3924
|
+
process.env.ED25519_PUBLIC_KEY = publicKey;
|
|
3925
|
+
// Persist keys on disk for subsequent restarts
|
|
3926
|
+
writeFileSync(persistentKeyPath, JSON.stringify({ privateKey, publicKey }, null, 2), 'utf-8');
|
|
3927
|
+
this._log('New persistent Ed25519 keys generated and saved to disk');
|
|
3928
|
+
} catch (e) {
|
|
3929
|
+
console.error('[Fingerprint] Native Ed25519 key generation failed:', e.message);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3838
3932
|
}
|
|
3839
3933
|
|
|
3840
3934
|
let finalConfig = securityConfig;
|
|
@@ -3855,6 +3949,11 @@ export class FingerprintEngine {
|
|
|
3855
3949
|
this._validateConfig(finalConfig); // Validate the configuration
|
|
3856
3950
|
this.verbose = finalConfig.verbose || false;
|
|
3857
3951
|
this.dryRun = finalConfig.dryRun || false;
|
|
3952
|
+
if (finalConfig.reset) {
|
|
3953
|
+
this.resetStore().catch(err => {
|
|
3954
|
+
console.error('[FingerprintEngine] Failed to reset store on startup:', err.message);
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3858
3957
|
}
|
|
3859
3958
|
|
|
3860
3959
|
/**
|
|
@@ -3869,6 +3968,16 @@ export class FingerprintEngine {
|
|
|
3869
3968
|
this._log('Configuration mise à jour à chaud (Hot-Reloaded)', this.securityConfig);
|
|
3870
3969
|
}
|
|
3871
3970
|
|
|
3971
|
+
/**
|
|
3972
|
+
* Réinitialise le store de persistance actif.
|
|
3973
|
+
*/
|
|
3974
|
+
async resetStore() {
|
|
3975
|
+
if (store && typeof store.clear === 'function') {
|
|
3976
|
+
await store.clear();
|
|
3977
|
+
this._log('Store has been reset/cleared.');
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3872
3981
|
/**
|
|
3873
3982
|
* Validates the security configuration object to detect potential typos or missing essential keys.
|
|
3874
3983
|
* @private
|
|
@@ -3887,9 +3996,9 @@ export class FingerprintEngine {
|
|
|
3887
3996
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
3888
3997
|
'trustedProxies',
|
|
3889
3998
|
'wasm',
|
|
3890
|
-
'similarityThreshold',
|
|
3999
|
+
'similarityThreshold', 'reset',
|
|
3891
4000
|
'ed25519_private_key', 'ed25519_public_key',
|
|
3892
|
-
'federatedPeers', 'federationSecret'
|
|
4001
|
+
'federatedPeers', 'federationSecret', 'filterWhitelist'
|
|
3893
4002
|
]);
|
|
3894
4003
|
|
|
3895
4004
|
// 1. Check for essential keys
|
|
@@ -3908,6 +4017,19 @@ export class FingerprintEngine {
|
|
|
3908
4017
|
}
|
|
3909
4018
|
}
|
|
3910
4019
|
|
|
4020
|
+
_hasCertainAttack(context) {
|
|
4021
|
+
const honeypotConfig = this.securityConfig.honeypot || {};
|
|
4022
|
+
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
4023
|
+
if (honeypotScore >= 100) {
|
|
4024
|
+
return true;
|
|
4025
|
+
}
|
|
4026
|
+
const { botScore } = getBotScore(context);
|
|
4027
|
+
if (botScore >= 100) {
|
|
4028
|
+
return true;
|
|
4029
|
+
}
|
|
4030
|
+
return false;
|
|
4031
|
+
}
|
|
4032
|
+
|
|
3911
4033
|
_log(message, data = {}) {
|
|
3912
4034
|
if (this.verbose) {
|
|
3913
4035
|
console.log(`[FingerprintEngine] ${message}`, data);
|
|
@@ -4223,6 +4345,16 @@ export class FingerprintEngine {
|
|
|
4223
4345
|
|
|
4224
4346
|
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
4225
4347
|
|
|
4348
|
+
let preCalculatedVector = null;
|
|
4349
|
+
let preCalculatedScore = null;
|
|
4350
|
+
const getScoreAndVector = async () => {
|
|
4351
|
+
if (preCalculatedScore === null) {
|
|
4352
|
+
preCalculatedVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
4353
|
+
preCalculatedScore = this.calculateFinalScore(preCalculatedVector);
|
|
4354
|
+
}
|
|
4355
|
+
return { score: preCalculatedScore, vector: preCalculatedVector };
|
|
4356
|
+
};
|
|
4357
|
+
|
|
4226
4358
|
this._log('Processing request', { clientIp, path, isStatic });
|
|
4227
4359
|
|
|
4228
4360
|
// Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
|
|
@@ -4243,34 +4375,47 @@ export class FingerprintEngine {
|
|
|
4243
4375
|
const allowRoaming = this.securityConfig?.allowCrossNetworkRoaming ?? false;
|
|
4244
4376
|
|
|
4245
4377
|
// 1. Check static IP allowlist first for maximum performance.
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'allowlist' } };
|
|
4249
|
-
}
|
|
4250
|
-
|
|
4251
|
-
// 2. Check hostname-based allowlist.
|
|
4252
|
-
if (await this._isIpInHostnameAllowlist(clientIp)) {
|
|
4253
|
-
this._log('IP resolves to a whitelisted hostname - allowing request', { clientIp });
|
|
4254
|
-
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'hostname_allowlist' } };
|
|
4255
|
-
}
|
|
4378
|
+
let whitelisted = false;
|
|
4379
|
+
let whitelistType = '';
|
|
4256
4380
|
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4381
|
+
if (this._isIpInAllowlist(clientIp)) {
|
|
4382
|
+
whitelisted = true;
|
|
4383
|
+
whitelistType = 'allowlist';
|
|
4384
|
+
} else if (await this._isIpInHostnameAllowlist(clientIp)) {
|
|
4385
|
+
whitelisted = true;
|
|
4386
|
+
whitelistType = 'hostname_allowlist';
|
|
4387
|
+
} else {
|
|
4388
|
+
const requestHost = requestContext.headers?.host;
|
|
4389
|
+
if (requestHost && this._isHostPathInAllowlist(requestHost, path)) {
|
|
4390
|
+
whitelisted = true;
|
|
4391
|
+
whitelistType = 'host_path_allowlist';
|
|
4392
|
+
} else if (this._isPathInAllowlist(path)) {
|
|
4393
|
+
whitelisted = true;
|
|
4394
|
+
whitelistType = 'path_allowlist';
|
|
4395
|
+
} else if (graphqlOperationType && this._isGraphqlOperationInAllowlist(graphqlOperationType, graphqlOperationName)) {
|
|
4396
|
+
whitelisted = true;
|
|
4397
|
+
whitelistType = 'graphql_operation_allowlist';
|
|
4398
|
+
}
|
|
4262
4399
|
}
|
|
4263
4400
|
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4401
|
+
if (whitelisted) {
|
|
4402
|
+
const filterWhitelist = this.securityConfig.filterWhitelist || false;
|
|
4403
|
+
let bypassWhitelist = false;
|
|
4404
|
+
if (filterWhitelist === true) {
|
|
4405
|
+
bypassWhitelist = this._hasCertainAttack(requestContext);
|
|
4406
|
+
} else if (typeof filterWhitelist === 'number') {
|
|
4407
|
+
const res = await getScoreAndVector();
|
|
4408
|
+
if (res.score > filterWhitelist) {
|
|
4409
|
+
bypassWhitelist = true;
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4269
4412
|
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4413
|
+
if (bypassWhitelist) {
|
|
4414
|
+
this._log('Whitelisted request exceeds filter threshold - bypassing whitelist bypass', { clientIp, path });
|
|
4415
|
+
} else {
|
|
4416
|
+
this._log(`IP/Path in allowlist (${whitelistType}) - allowing request`, { clientIp, path });
|
|
4417
|
+
return { action: 'next', score: 0, vector: { whitelisted: 100, type: whitelistType } };
|
|
4418
|
+
}
|
|
4274
4419
|
}
|
|
4275
4420
|
|
|
4276
4421
|
const { pow_nonce } = query;
|
|
@@ -4288,8 +4433,23 @@ export class FingerprintEngine {
|
|
|
4288
4433
|
|
|
4289
4434
|
// Check if the request is from a verified, whitelisted bot (e.g., Googlebot)
|
|
4290
4435
|
if (await this._verifyWhitelistedBot(requestContext)) {
|
|
4291
|
-
|
|
4292
|
-
|
|
4436
|
+
const filterWhitelist = this.securityConfig.filterWhitelist || false;
|
|
4437
|
+
let bypassWhitelist = false;
|
|
4438
|
+
if (filterWhitelist === true) {
|
|
4439
|
+
bypassWhitelist = this._hasCertainAttack(requestContext);
|
|
4440
|
+
} else if (typeof filterWhitelist === 'number') {
|
|
4441
|
+
const res = await getScoreAndVector();
|
|
4442
|
+
if (res.score > filterWhitelist) {
|
|
4443
|
+
bypassWhitelist = true;
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
if (bypassWhitelist) {
|
|
4448
|
+
this._log('Verified bot request exceeds filter threshold - bypassing bot whitelist bypass', { clientIp });
|
|
4449
|
+
} else {
|
|
4450
|
+
this._log('Whitelisted bot verified - allowing request', { clientIp });
|
|
4451
|
+
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
|
|
4452
|
+
}
|
|
4293
4453
|
}
|
|
4294
4454
|
|
|
4295
4455
|
this._log('Identity resolved', { deviceId, isNewDevice, hasDeviceData: !!deviceData });
|
|
@@ -4318,16 +4478,14 @@ export class FingerprintEngine {
|
|
|
4318
4478
|
}
|
|
4319
4479
|
|
|
4320
4480
|
// The engine now works with the context directly, no more rawReq dependency here.
|
|
4321
|
-
const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
4322
|
-
|
|
4481
|
+
const suspicionVector = preCalculatedVector || await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
4482
|
+
let finalScore = preCalculatedScore !== null ? preCalculatedScore : this.calculateFinalScore(suspicionVector);
|
|
4323
4483
|
|
|
4324
4484
|
this._log('Suspicion vector calculated', {
|
|
4325
4485
|
vector: suspicionVector,
|
|
4326
4486
|
weights: this.securityConfig.weights
|
|
4327
4487
|
});
|
|
4328
4488
|
|
|
4329
|
-
let finalScore = this.calculateFinalScore(suspicionVector);
|
|
4330
|
-
|
|
4331
4489
|
this._log('Final score calculated', { finalScore });
|
|
4332
4490
|
|
|
4333
4491
|
const blockThreshold = thresholds.block ?? 95;
|
|
@@ -4609,6 +4767,27 @@ export class FingerprintEngine {
|
|
|
4609
4767
|
}, store);
|
|
4610
4768
|
await manager.integrateSolution(pow_problem_id, workResult);
|
|
4611
4769
|
|
|
4770
|
+
// Si le problème résolu est l'auto-tuning de sécurité et que l'auto-tuning est activé,
|
|
4771
|
+
// on applique directement la meilleure solution calculée au moteur en direct.
|
|
4772
|
+
if (pow_problem_id === 'security_auto_tuning' && this.securityConfig.autotuning?.enabled) {
|
|
4773
|
+
const paretoFront = workResult.paretoFront;
|
|
4774
|
+
if (Array.isArray(paretoFront) && paretoFront.length > 0) {
|
|
4775
|
+
let bestSolution = paretoFront[0];
|
|
4776
|
+
let minDistance = Math.sqrt(Math.pow(bestSolution.objectives[0], 2) + Math.pow(bestSolution.objectives[1], 2));
|
|
4777
|
+
for (let i = 1; i < paretoFront.length; i++) {
|
|
4778
|
+
const distance = Math.sqrt(Math.pow(paretoFront[i].objectives[0], 2) + Math.pow(paretoFront[i].objectives[1], 2));
|
|
4779
|
+
if (distance < minDistance) {
|
|
4780
|
+
minDistance = distance;
|
|
4781
|
+
bestSolution = paretoFront[i];
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
if (bestSolution && bestSolution.solution) {
|
|
4785
|
+
this.updateConfig(bestSolution.solution);
|
|
4786
|
+
this._log('Useful Work auto-tuning applied successfully to live config.');
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4612
4791
|
await store.delete(`secret:${pow_nonce}`);
|
|
4613
4792
|
// Accorder un ticket de passage comme pour un PoW normal
|
|
4614
4793
|
const ticket = "valid_ticket_placeholder"; // Générer un vrai ticket ici
|
|
@@ -4896,7 +5075,8 @@ export class FingerprintEngine {
|
|
|
4896
5075
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce)); // Génère les URL
|
|
4897
5076
|
|
|
4898
5077
|
// On passe la configuration pour que la difficulté soit calculée correctement.
|
|
4899
|
-
|
|
5078
|
+
const tlsSessionId = getTlsSessionId(requestContext) || '';
|
|
5079
|
+
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig, tlsSessionId);
|
|
4900
5080
|
|
|
4901
5081
|
// La difficulté mémoire augmente désormais en parfaite synergie avec le facteur de suspicion (ratio constant)
|
|
4902
5082
|
const memActivationFactor = suspicionFactor;
|
|
@@ -4915,7 +5095,7 @@ export class FingerprintEngine {
|
|
|
4915
5095
|
const originalFingerprint = requestContext.headers['x-device-fingerprint'] || __internal.getCompositeDeviceHash(requestContext);
|
|
4916
5096
|
|
|
4917
5097
|
// Store the entire challenge context with a short TTL (e.g., 5 minutes)
|
|
4918
|
-
|
|
5098
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint, clientIp, tlsSessionId);
|
|
4919
5099
|
|
|
4920
5100
|
// SECURITY: Cryptographically sign the payload before storing it to prevent database tampering
|
|
4921
5101
|
const payloadToSign = `${clientSecret}:${cpuChallengeDetails.target}:${originalFingerprint}:${memDifficulty}:${path}:${clientIp}`;
|
|
@@ -4964,7 +5144,7 @@ export class FingerprintEngine {
|
|
|
4964
5144
|
decision.body = challengePayload;
|
|
4965
5145
|
} else {
|
|
4966
5146
|
// For browsers, send the HTML page.
|
|
4967
|
-
|
|
5147
|
+
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapUrls, originalFingerprint, tlsSessionId);
|
|
4968
5148
|
this._log('Browser challenge page generated', {
|
|
4969
5149
|
pageLength: page.length,
|
|
4970
5150
|
trapUrlsInjected: trapUrls.length
|
|
@@ -6106,7 +6286,10 @@ export const __internal = {
|
|
|
6106
6286
|
registerCooperativeNode,
|
|
6107
6287
|
findPeerInSubnet,
|
|
6108
6288
|
handleCooperativeRequest,
|
|
6109
|
-
broadcastBannedZkp
|
|
6289
|
+
broadcastBannedZkp,
|
|
6290
|
+
getMetric,
|
|
6291
|
+
incrementCounter,
|
|
6292
|
+
observeValue
|
|
6110
6293
|
};
|
|
6111
6294
|
|
|
6112
6295
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -6125,20 +6308,21 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
6125
6308
|
if (!trafficData || trafficData.length === 0) {
|
|
6126
6309
|
return [];
|
|
6127
6310
|
}
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6311
|
+
|
|
6312
|
+
const rawLogs = [...trafficData];
|
|
6313
|
+
const totalCount = rawLogs.length;
|
|
6314
|
+
|
|
6315
|
+
const maxLogsPerDevice = Math.max(3, Math.floor(totalCount * 0.02)); // Max 2% contribution per device
|
|
6316
|
+
const maxLogsPerIp = Math.max(3, Math.floor(totalCount * 0.02)); // Max 2% par adresse IP individuelle
|
|
6317
|
+
const maxLogsPerSubnet = Math.max(5, Math.floor(totalCount * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
|
|
6318
|
+
const maxLogsPerHardwareCluster = Math.max(3, Math.floor(totalCount * 0.02)); // Max 2% par cluster matériel stable
|
|
6319
|
+
|
|
6320
|
+
const deviceCounts = new Map();
|
|
6131
6321
|
const ipCounts = new Map();
|
|
6132
6322
|
const subnetCounts = new Map();
|
|
6133
6323
|
const hardwareClusterCounts = new Map();
|
|
6134
6324
|
const hwClusterCache = new Map();
|
|
6135
6325
|
|
|
6136
|
-
// Calcul des quotas maximums pour éviter l'influence démesurée d'une entité
|
|
6137
|
-
const maxLogsPerDevice = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% contribution per device
|
|
6138
|
-
const maxLogsPerIp = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par adresse IP individuelle
|
|
6139
|
-
const maxLogsPerSubnet = Math.max(5, Math.floor(trafficData.length * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
|
|
6140
|
-
const maxLogsPerHardwareCluster = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par cluster matériel stable
|
|
6141
|
-
|
|
6142
6326
|
const getHardwareCluster = (log) => {
|
|
6143
6327
|
const fp = log.deviceHash || log.fingerprint || log.deviceFingerprint || '';
|
|
6144
6328
|
if (!fp || typeof fp !== 'string') {
|
|
@@ -6159,8 +6343,41 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
6159
6343
|
hwClusterCache.set(fp, result);
|
|
6160
6344
|
return result;
|
|
6161
6345
|
};
|
|
6346
|
+
// --- REVOLUTION : Compression de Cohorte par Densité Vectorielle (Anti-Sybil / Anti-Poisoning) ---
|
|
6347
|
+
const clusteredLogs = [];
|
|
6348
|
+
const getVectorDistance = (v1, v2) => {
|
|
6349
|
+
if (!v1 || !v2) return Infinity;
|
|
6350
|
+
let sum = 0;
|
|
6351
|
+
const keys = new Set([...Object.keys(v1), ...Object.keys(v2)]);
|
|
6352
|
+
for (const key of keys) {
|
|
6353
|
+
sum += Math.pow((v1[key] || 0) - (v2[key] || 0), 2);
|
|
6354
|
+
}
|
|
6355
|
+
return Math.sqrt(sum);
|
|
6356
|
+
};
|
|
6357
|
+
|
|
6358
|
+
for (const log of rawLogs) {
|
|
6359
|
+
let matchedCluster = null;
|
|
6360
|
+
for (const cluster of clusteredLogs) {
|
|
6361
|
+
if (log.type === cluster.type && getVectorDistance(log.vector, cluster.vector) < 5.0) {
|
|
6362
|
+
matchedCluster = cluster;
|
|
6363
|
+
break;
|
|
6364
|
+
}
|
|
6365
|
+
}
|
|
6366
|
+
if (matchedCluster) {
|
|
6367
|
+
matchedCluster.instancesCount = (matchedCluster.instancesCount || 1) + 1;
|
|
6368
|
+
matchedCluster.weight = 1 + Math.log(matchedCluster.instancesCount); // Compression logarithmique
|
|
6369
|
+
} else {
|
|
6370
|
+
const logCopy = { ...log };
|
|
6371
|
+
logCopy.instancesCount = 1;
|
|
6372
|
+
logCopy.weight = 1.0;
|
|
6373
|
+
clusteredLogs.push(logCopy);
|
|
6374
|
+
}
|
|
6375
|
+
}
|
|
6162
6376
|
|
|
6163
|
-
|
|
6377
|
+
const suspiciousLogs = [];
|
|
6378
|
+
const passedLogs = [];
|
|
6379
|
+
|
|
6380
|
+
for (const log of clusteredLogs) {
|
|
6164
6381
|
const devId = log.deviceId || 'anonymous';
|
|
6165
6382
|
const ip = log.clientIp || log.ip || 'unknown';
|
|
6166
6383
|
const subnet = getIpSubnet(ip) || 'unknown-subnet';
|
|
@@ -6249,29 +6466,33 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
6249
6466
|
pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup);
|
|
6250
6467
|
const sanitizedData = sanitizeTrafficData(trafficData);
|
|
6251
6468
|
|
|
6252
|
-
const highConfidenceLogs = sanitizedData
|
|
6253
|
-
|
|
6469
|
+
const highConfidenceLogs = sanitizedData
|
|
6470
|
+
.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered')
|
|
6471
|
+
.reduce((sum, log) => sum + (log.instancesCount || 1), 0);
|
|
6472
|
+
const totalSanitizedInstances = sanitizedData.reduce((sum, log) => sum + (log.instancesCount || 1), 0);
|
|
6473
|
+
const highConfidenceRatio = totalSanitizedInstances > 0 ? highConfidenceLogs / totalSanitizedInstances : 0;
|
|
6254
6474
|
const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
|
|
6255
6475
|
const MIN_HIGH_CONFIDENCE_COUNT = 10; // Absolu de secours pour éviter le gel lors de floods
|
|
6256
6476
|
|
|
6257
6477
|
const hasEnoughSignal = highConfidenceRatio >= MIN_CONFIDENCE_RATIO || highConfidenceLogs >= MIN_HIGH_CONFIDENCE_COUNT;
|
|
6258
6478
|
|
|
6259
|
-
if (
|
|
6260
|
-
if (
|
|
6261
|
-
console.log(`[AutoTuning] Reporté : ${
|
|
6479
|
+
if (totalSanitizedInstances < minDataPoints || !hasEnoughSignal) {
|
|
6480
|
+
if (totalSanitizedInstances < minDataPoints) {
|
|
6481
|
+
console.log(`[AutoTuning] Reporté : ${totalSanitizedInstances}/${minDataPoints} points de données.`);
|
|
6262
6482
|
} else {
|
|
6263
6483
|
console.log(`[AutoTuning] Reporté : Signaux de confiance insuffisants (Ratio: ${(highConfidenceRatio * 100).toFixed(2)}% < ${(MIN_CONFIDENCE_RATIO * 100).toFixed(2)}% et absolu: ${highConfidenceLogs} < ${MIN_HIGH_CONFIDENCE_COUNT}).`);
|
|
6264
6484
|
}
|
|
6265
6485
|
return;
|
|
6266
6486
|
}
|
|
6267
|
-
console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${
|
|
6487
|
+
console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${totalSanitizedInstances} points de données assainis.`);
|
|
6268
6488
|
|
|
6269
|
-
|
|
6489
|
+
try {
|
|
6490
|
+
const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
|
|
6270
6491
|
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6492
|
+
if (!paretoFront || paretoFront.length === 0) {
|
|
6493
|
+
console.warn("[AutoTuning] L'optimisation n'a retourné aucune solution.");
|
|
6494
|
+
return;
|
|
6495
|
+
}
|
|
6275
6496
|
|
|
6276
6497
|
// Règles de gardiennage (Sanity Guardrails) pour filtrer le front de Pareto
|
|
6277
6498
|
const isValidSecurityConfig = (config) => {
|
|
@@ -6429,13 +6650,14 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
6429
6650
|
if (savePath) {
|
|
6430
6651
|
try {
|
|
6431
6652
|
const configToSave = JSON.stringify(bestSolution.solution, null, 2);
|
|
6432
|
-
|
|
6653
|
+
writeFileSync(savePath, configToSave, 'utf-8');
|
|
6433
6654
|
console.log(`[AutoTuning] Meilleure configuration sauvegardée dans : ${savePath}`);
|
|
6434
6655
|
} catch (error) {
|
|
6435
6656
|
console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
|
|
6436
6657
|
}
|
|
6437
6658
|
}
|
|
6438
6659
|
|
|
6660
|
+
} finally {
|
|
6439
6661
|
if (clearAfterTuning) {
|
|
6440
6662
|
const cleared = trafficData.splice(0, trafficData.length);
|
|
6441
6663
|
if (onCleanup && typeof onCleanup === 'function' && cleared.length > 0) {
|
|
@@ -6447,6 +6669,7 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
6447
6669
|
}
|
|
6448
6670
|
console.log(`[AutoTuning] Explicitly cleared ${cleared.length} processed traffic data points.`);
|
|
6449
6671
|
}
|
|
6672
|
+
}
|
|
6450
6673
|
}
|
|
6451
6674
|
|
|
6452
6675
|
/**
|
|
@@ -6523,36 +6746,156 @@ class RequestContext {
|
|
|
6523
6746
|
}
|
|
6524
6747
|
}
|
|
6525
6748
|
|
|
6526
|
-
const
|
|
6527
|
-
|
|
6528
|
-
let metrics = `# HELP fingerprint_requests_total Total requests processed.\n# TYPE fingerprint_requests_total counter\nfingerprint_requests_total{status="passed"} 1\n`;
|
|
6749
|
+
const dynamicCounters = new Map();
|
|
6750
|
+
const dynamicObservations = new Map();
|
|
6529
6751
|
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6752
|
+
export function incrementCounter(name, labels = {}) {
|
|
6753
|
+
const sortedLabels = Object.keys(labels).sort().reduce((acc, key) => {
|
|
6754
|
+
acc[key] = labels[key];
|
|
6755
|
+
return acc;
|
|
6756
|
+
}, {});
|
|
6757
|
+
const labelsKey = JSON.stringify(sortedLabels);
|
|
6758
|
+
const queryName = name.startsWith('fingerprint_') ? name : `fingerprint_${name}`;
|
|
6759
|
+
const fullKey = `${queryName}:${labelsKey}`;
|
|
6760
|
+
|
|
6761
|
+
if (!dynamicCounters.has(fullKey)) {
|
|
6762
|
+
dynamicCounters.set(fullKey, { name: queryName, labels: sortedLabels, value: 0 });
|
|
6537
6763
|
}
|
|
6764
|
+
dynamicCounters.get(fullKey).value++;
|
|
6765
|
+
}
|
|
6538
6766
|
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6767
|
+
export function observeValue(name, value, labels = {}) {
|
|
6768
|
+
const sortedLabels = Object.keys(labels).sort().reduce((acc, key) => {
|
|
6769
|
+
acc[key] = labels[key];
|
|
6770
|
+
return acc;
|
|
6771
|
+
}, {});
|
|
6772
|
+
const labelsKey = JSON.stringify(sortedLabels);
|
|
6773
|
+
const queryName = name.startsWith('fingerprint_') ? name : `fingerprint_${name}`;
|
|
6774
|
+
const fullKey = `${queryName}:${labelsKey}`;
|
|
6775
|
+
dynamicObservations.set(fullKey, { name: queryName, labels: sortedLabels, value });
|
|
6776
|
+
}
|
|
6777
|
+
|
|
6778
|
+
export function getMetric(name, securityConfig = {}) {
|
|
6779
|
+
const result = new Map();
|
|
6780
|
+
const queryName = name.startsWith('fingerprint_') ? name : `fingerprint_${name}`;
|
|
6781
|
+
|
|
6782
|
+
if (queryName === 'fingerprint_security_weight') {
|
|
6783
|
+
const weights = securityConfig.weights || {};
|
|
6784
|
+
for (const [indicator, weight] of Object.entries(weights)) {
|
|
6785
|
+
if (typeof weight === 'number') {
|
|
6786
|
+
result.set(indicator, weight);
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
} else if (queryName === 'fingerprint_security_threshold') {
|
|
6790
|
+
const thresholds = securityConfig.thresholds || {};
|
|
6791
|
+
for (const [level, threshold] of Object.entries(thresholds)) {
|
|
6792
|
+
if (typeof threshold === 'number') {
|
|
6793
|
+
result.set(level, threshold);
|
|
6794
|
+
}
|
|
6795
|
+
}
|
|
6796
|
+
} else if (queryName === 'fingerprint_autotuning_false_positive_rate') {
|
|
6797
|
+
if (lastBestSolution && lastBestSolution.objectives) {
|
|
6798
|
+
result.set('fpr', lastBestSolution.objectives[0]);
|
|
6799
|
+
}
|
|
6800
|
+
} else if (queryName === 'fingerprint_autotuning_false_negative_rate') {
|
|
6801
|
+
if (lastBestSolution && lastBestSolution.objectives) {
|
|
6802
|
+
result.set('fnr', lastBestSolution.objectives[1]);
|
|
6803
|
+
}
|
|
6804
|
+
} else {
|
|
6805
|
+
// Search dynamic counters
|
|
6806
|
+
for (const counter of dynamicCounters.values()) {
|
|
6807
|
+
if (counter.name === queryName || counter.name === name) {
|
|
6808
|
+
const labelPairs = Object.entries(counter.labels)
|
|
6809
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
6810
|
+
.join(',');
|
|
6811
|
+
result.set(labelPairs || 'value', counter.value);
|
|
6812
|
+
}
|
|
6544
6813
|
}
|
|
6545
|
-
}
|
|
6546
|
-
}
|
|
6547
6814
|
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6815
|
+
// Search dynamic observations
|
|
6816
|
+
for (const obs of dynamicObservations.values()) {
|
|
6817
|
+
if (obs.name === queryName || obs.name === name) {
|
|
6818
|
+
const labelPairs = Object.entries(obs.labels)
|
|
6819
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
6820
|
+
.join(',');
|
|
6821
|
+
result.set(labelPairs || 'value', obs.value);
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6552
6824
|
}
|
|
6825
|
+
return result;
|
|
6826
|
+
}
|
|
6553
6827
|
|
|
6554
|
-
|
|
6555
|
-
|
|
6828
|
+
const MetricsManager = {
|
|
6829
|
+
getPrometheusMetrics(securityConfig = {}) {
|
|
6830
|
+
let metrics = '';
|
|
6831
|
+
|
|
6832
|
+
if (dynamicCounters.size === 0) {
|
|
6833
|
+
metrics += `# HELP fingerprint_requests_total Total requests processed.\n# TYPE fingerprint_requests_total counter\nfingerprint_requests_total{status="passed"} 1\n`;
|
|
6834
|
+
} else {
|
|
6835
|
+
const grouped = {};
|
|
6836
|
+
for (const counter of dynamicCounters.values()) {
|
|
6837
|
+
if (!grouped[counter.name]) {
|
|
6838
|
+
grouped[counter.name] = [];
|
|
6839
|
+
}
|
|
6840
|
+
grouped[counter.name].push(counter);
|
|
6841
|
+
}
|
|
6842
|
+
for (const [name, instances] of Object.entries(grouped)) {
|
|
6843
|
+
metrics += `# HELP ${name} Total requests processed.\n# TYPE ${name} counter\n`;
|
|
6844
|
+
for (const inst of instances) {
|
|
6845
|
+
const labelPairs = Object.entries(inst.labels)
|
|
6846
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
6847
|
+
.join(',');
|
|
6848
|
+
const labelStr = labelPairs ? `{${labelPairs}}` : '';
|
|
6849
|
+
metrics += `${name}${labelStr} ${inst.value}\n`;
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
if (dynamicObservations.size > 0) {
|
|
6855
|
+
const grouped = {};
|
|
6856
|
+
for (const obs of dynamicObservations.values()) {
|
|
6857
|
+
if (!grouped[obs.name]) {
|
|
6858
|
+
grouped[obs.name] = [];
|
|
6859
|
+
}
|
|
6860
|
+
grouped[obs.name].push(obs);
|
|
6861
|
+
}
|
|
6862
|
+
for (const [name, instances] of Object.entries(grouped)) {
|
|
6863
|
+
metrics += `\n# HELP ${name} Value observation.\n# TYPE ${name} gauge\n`;
|
|
6864
|
+
for (const inst of instances) {
|
|
6865
|
+
const labelPairs = Object.entries(inst.labels)
|
|
6866
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
6867
|
+
.join(',');
|
|
6868
|
+
const labelStr = labelPairs ? `{${labelPairs}}` : '';
|
|
6869
|
+
metrics += `${name}${labelStr} ${inst.value}\n`;
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
|
|
6874
|
+
if (securityConfig.weights) {
|
|
6875
|
+
metrics += `\n# HELP fingerprint_security_weight Active weight for each suspicion indicator.\n# TYPE fingerprint_security_weight gauge\n`;
|
|
6876
|
+
for (const [indicator, weight] of Object.entries(securityConfig.weights)) {
|
|
6877
|
+
if (typeof weight === 'number') {
|
|
6878
|
+
metrics += `fingerprint_security_weight{indicator="${indicator}"} ${weight}\n`;
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6883
|
+
if (securityConfig.thresholds) {
|
|
6884
|
+
metrics += `\n# HELP fingerprint_security_threshold Active score threshold for each enforcement action level.\n# TYPE fingerprint_security_threshold gauge\n`;
|
|
6885
|
+
for (const [level, threshold] of Object.entries(securityConfig.thresholds)) {
|
|
6886
|
+
if (typeof threshold === 'number') {
|
|
6887
|
+
metrics += `fingerprint_security_threshold{level="${level}"} ${threshold}\n`;
|
|
6888
|
+
}
|
|
6889
|
+
}
|
|
6890
|
+
}
|
|
6891
|
+
|
|
6892
|
+
if (lastBestSolution && lastBestSolution.objectives) {
|
|
6893
|
+
metrics += `\n# HELP fingerprint_autotuning_false_positive_rate Current false positive rate calculated by the auto-tuner.\n# TYPE fingerprint_autotuning_false_positive_rate gauge\nfingerprint_autotuning_false_positive_rate ${lastBestSolution.objectives[0]}\n`;
|
|
6894
|
+
metrics += `\n# HELP fingerprint_autotuning_false_negative_rate Current false negative rate calculated by the auto-tuner.\n# TYPE fingerprint_autotuning_false_negative_rate gauge\nfingerprint_autotuning_false_negative_rate ${lastBestSolution.objectives[1]}\n`;
|
|
6895
|
+
}
|
|
6896
|
+
|
|
6897
|
+
return metrics;
|
|
6898
|
+
}
|
|
6556
6899
|
};
|
|
6557
6900
|
|
|
6558
6901
|
/**
|