@anonympins/fingerprint 0.7.4 → 0.7.5

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.
@@ -68,7 +68,8 @@ function canAttemptDns() {
68
68
  }
69
69
 
70
70
  /**
71
- * Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante).
71
+ * Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante)
72
+ * avec protection par Anonymat Différentiel (Differential Privacy / Bruit de Laplace & Leurres).
72
73
  * @private
73
74
  * @param {string} zkpY - La clé publique ZKP du terminal banni.
74
75
  * @param {object} config - La configuration de sécurité.
@@ -77,46 +78,87 @@ async function broadcastBannedZkp(zkpY, config) {
77
78
  const peers = config.federatedPeers || [];
78
79
  if (peers.length === 0) return;
79
80
 
80
- const timestamp = Date.now();
81
- const msg = `${timestamp}:${zkpY}`;
82
-
83
- let signature = '';
84
- let isAsymmetric = false;
81
+ const dpConfig = config.differentialPrivacy || {};
82
+ const dpEnabled = dpConfig.enabled !== false;
83
+ const epsilon = typeof dpConfig.epsilon === 'number' ? dpConfig.epsilon : (config.dpEpsilon || 1.0);
85
84
 
86
- if (process.env.ED25519_PRIVATE_KEY) {
87
- try {
88
- const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
89
- const signBuffer = crypto.sign(null, Buffer.from(msg), {
90
- key: cleanKey,
91
- format: 'pem',
92
- type: 'pkcs8'
93
- });
94
- signature = signBuffer.toString('hex');
95
- isAsymmetric = true;
96
- } catch (e) {
97
- console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
85
+ const sendReport = (targetZkpY, ts) => {
86
+ const msg = `${ts}:${targetZkpY}`;
87
+ let signature = '';
88
+ let isAsymmetric = false;
89
+
90
+ if (process.env.ED25519_PRIVATE_KEY) {
91
+ try {
92
+ const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
93
+ const signBuffer = crypto.sign(null, Buffer.from(msg), {
94
+ key: cleanKey,
95
+ format: 'pem',
96
+ type: 'pkcs8'
97
+ });
98
+ signature = signBuffer.toString('hex');
99
+ isAsymmetric = true;
100
+ } catch (e) {
101
+ console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
102
+ }
98
103
  }
99
- }
100
104
 
101
- if (!isAsymmetric) {
102
- const secret = config.federationSecret || getPowSecret();
103
- signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
105
+ if (!isAsymmetric) {
106
+ const secret = config.federationSecret || getPowSecret();
107
+ signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
108
+ }
109
+
110
+ peers.forEach(peerUrl => {
111
+ fetch(peerUrl + '?coop_op=share_threat_intel', {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ 'X-Federation-Signature': isAsymmetric ? '' : signature,
116
+ 'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
117
+ 'X-Federation-Timestamp': String(ts)
118
+ },
119
+ body: JSON.stringify({ zkpY: targetZkpY })
120
+ }).catch(() => {
121
+ // Échec de propagation silencieux pour ne pas perturber le thread principal
122
+ });
123
+ });
124
+ };
125
+
126
+ let reportTimestamp = Date.now();
127
+
128
+ // 1. Differential Privacy : Perturbation laplacienne de l'horodatage
129
+ if (dpEnabled) {
130
+ const deltaT = 5000; // Sensibilité de 5s
131
+ const b = deltaT / Math.max(0.1, epsilon);
132
+ const u = secureRandomFloat() - 0.5;
133
+ const safeU = Math.abs(u) < 1e-7 ? (u >= 0 ? 1e-7 : -1e-7) : u;
134
+ const laplaceNoise = -b * Math.sign(safeU) * Math.log(1 - 2 * Math.abs(safeU));
135
+ const clampedNoise = Math.max(-60000, Math.min(60000, Math.round(laplaceNoise)));
136
+ reportTimestamp = Date.now() + clampedNoise;
104
137
  }
105
138
 
106
- peers.forEach(peerUrl => {
107
- fetch(peerUrl + '?coop_op=share_threat_intel', {
108
- method: 'POST',
109
- headers: {
110
- 'Content-Type': 'application/json',
111
- 'X-Federation-Signature': isAsymmetric ? '' : signature,
112
- 'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
113
- 'X-Federation-Timestamp': String(timestamp)
114
- },
115
- body: JSON.stringify({ zkpY })
116
- }).catch(() => {
117
- // Échec de propagation silencieux pour ne pas perturber le thread principal
118
- });
119
- });
139
+ sendReport(zkpY, reportTimestamp);
140
+
141
+ // 2. Differential Privacy : Réponse randomisée par injection de leurres (Decoy ZKP)
142
+ if (dpEnabled) {
143
+ const decoyProbability = dpConfig.dummyRate !== undefined
144
+ ? dpConfig.dummyRate
145
+ : (1 / (1 + Math.exp(epsilon)));
146
+
147
+ if (secureRandomFloat() < decoyProbability) {
148
+ const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
149
+ const decoyBytes = crypto.randomBytes(32);
150
+ const decoyInt = (BigInt('0x' + decoyBytes.toString('hex')) % (ZKP_P - 2n)) + 1n;
151
+ const decoyZkpY = decoyInt.toString(16);
152
+
153
+ const uDecoy = secureRandomFloat() - 0.5;
154
+ const safeUDecoy = Math.abs(uDecoy) < 1e-7 ? (uDecoy >= 0 ? 1e-7 : -1e-7) : uDecoy;
155
+ const b = 5000 / Math.max(0.1, epsilon);
156
+ const decoyNoise = Math.max(-60000, Math.min(60000, Math.round(-b * Math.sign(safeUDecoy) * Math.log(1 - 2 * Math.abs(safeUDecoy)))));
157
+ const decoyTimestamp = Date.now() + decoyNoise;
158
+
159
+ sendReport(decoyZkpY, decoyTimestamp);
160
+ }
161
+ }
120
162
  }
121
163
 
122
164
  const withTimeout = (promise, ms) => {
@@ -166,6 +208,9 @@ const bingbotEntries = loadBotWhitelist('bingbot.json', [
166
208
  const yandexEntries = loadBotWhitelist('yandex.json', [
167
209
  ]);
168
210
 
211
+ const facebookEntries = loadBotWhitelist('facebook.json', [
212
+ ]);
213
+
169
214
  function generateSessionMapping() {
170
215
  const randomStr = (len = 6) => Array.from({ length: len }, () => String.fromCharCode(crypto.randomInt(97, 123))).join('');
171
216
  const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
@@ -706,6 +751,7 @@ const securityProfiles = {
706
751
  http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
707
752
  threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
708
753
  virtualizationScore: 0.8,
754
+ mtuAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie MTU/fragmentation
709
755
  },
710
756
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
711
757
  patterns: {
@@ -754,6 +800,7 @@ const securityProfiles = {
754
800
  renderingAnomalyScore: 1.0,
755
801
  http2AnomalyScore: 1.0,
756
802
  threatIntelScore: 1.0,
803
+ mtuAnomalyScore: 0.9,
757
804
  virtualizationScore: 1.0,
758
805
  },
759
806
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
@@ -805,6 +852,7 @@ const securityProfiles = {
805
852
  renderingAnomalyScore: 0.2,
806
853
  threatIntelScore: 0.6,
807
854
  virtualizationScore: 0.8,
855
+ mtuAnomalyScore: 0.9,
808
856
  },
809
857
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
810
858
  patterns: {
@@ -854,6 +902,7 @@ const securityProfiles = {
854
902
  quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
855
903
  http2AnomalyScore: 0.5,
856
904
  renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
905
+ mtuAnomalyScore: 0.9,
857
906
  virtualizationScore: 0.8,
858
907
  },
859
908
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
@@ -902,6 +951,7 @@ const securityProfiles = {
902
951
  quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
903
952
  http2AnomalyScore: 0.9,
904
953
  renderingAnomalyScore: 0.9,
954
+ mtuAnomalyScore: 0.9,
905
955
  virtualizationScore: 0.8,
906
956
  },
907
957
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
@@ -1694,6 +1744,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
1694
1744
  if (!expiry || Date.now() > expiry) {
1695
1745
  return false;
1696
1746
  }
1747
+ if (statelessData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
1748
+ if (!deviceId || deviceId === storedDeviceId) {
1749
+ return true;
1750
+ }
1751
+ }
1697
1752
  if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1698
1753
  const expectedY = storedDeviceHash.split(':')[1];
1699
1754
  if (zkpProof) {
@@ -1721,6 +1776,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
1721
1776
  await store.delete(`ticket:${ticket}`);
1722
1777
  return false;
1723
1778
  }
1779
+ if (ticketData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
1780
+ if (!deviceId || deviceId === storedDeviceId) {
1781
+ return true;
1782
+ }
1783
+ }
1724
1784
  if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1725
1785
  const expectedY = storedDeviceHash.split(':')[1];
1726
1786
  if (zkpProof) {
@@ -3912,6 +3972,52 @@ function getVirtualizationAnomalyScore(context) {
3912
3972
  return Math.min(100.0, score);
3913
3973
  }
3914
3974
 
3975
+ /**
3976
+ * @private
3977
+ * Analyzes TCP MTU and fragmentation flags to detect network tunnels (VPN/Proxy).
3978
+ * @param {object} context - The request context.
3979
+ * @returns {{mtuAnomalyScore: number}}
3980
+ */
3981
+ function getMtuAnomalyScore(context) {
3982
+ const mtuHeader = context.headers?.['x-tcp-mtu-info'];
3983
+ if (!mtuHeader || typeof mtuHeader !== 'string') {
3984
+ return { mtuAnomalyScore: 0.0 };
3985
+ }
3986
+
3987
+ const parts = mtuHeader.split(':');
3988
+ if (parts.length < 2) {
3989
+ return { mtuAnomalyScore: 0.0 };
3990
+ }
3991
+
3992
+ const mtu = parseInt(parts[0], 10);
3993
+ const df = parseInt(parts[1], 10); // Don't Fragment bit (1 or 0)
3994
+
3995
+ if (isNaN(mtu) || isNaN(df)) {
3996
+ return { mtuAnomalyScore: 0.0 };
3997
+ }
3998
+
3999
+ let score = 0.0;
4000
+
4001
+ // Pénalité modérée pour les MTU typiques des VPNs/tunnels.
4002
+ if (mtu > 1200 && mtu <= 1420) {
4003
+ score += 35.0;
4004
+ } else if (mtu > 1420 && mtu < 1492) {
4005
+ score += 20.0;
4006
+ }
4007
+
4008
+ const ua = context.headers?.['user-agent'] || '';
4009
+ const uaParts = parseUserAgent(ua);
4010
+ const os = uaParts.os;
4011
+
4012
+ // Pénalités additionnelles en cas d'incohérence OS vs signature réseau.
4013
+ if (os) {
4014
+ if (os.startsWith('Windows') && mtu < 1492) score += 20.0;
4015
+ if ((os.startsWith('Android') || os.startsWith('iOS')) && mtu < 1480) score += 15.0;
4016
+ if (df === 0 && (os.startsWith('Windows') || os.startsWith('Mac') || os.startsWith('Linux'))) score += 40.0;
4017
+ }
4018
+
4019
+ return { mtuAnomalyScore: Math.min(100.0, score) };
4020
+ }
3915
4021
 
3916
4022
  /**
3917
4023
  * Returns a vector of raw (unweighted) suspicion scores.
@@ -4002,6 +4108,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
4002
4108
  const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
4003
4109
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
4004
4110
  const virtualizationScore = getVirtualizationAnomalyScore(context);
4111
+ const { mtuAnomalyScore } = getMtuAnomalyScore(context);
4005
4112
 
4006
4113
  // Save the updated device state to the store
4007
4114
  // Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
@@ -4013,7 +4120,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
4013
4120
  deviceData.ips = new Set(deviceData.ips);
4014
4121
  }
4015
4122
  // Le vecteur de suspicion est maintenant complet.
4016
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
4123
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore, mtuAnomalyScore };
4017
4124
  };
4018
4125
 
4019
4126
  // A residential user can change networks (home, 4G, public wifi).
@@ -4232,6 +4339,24 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
4232
4339
  </body></html>`;
4233
4340
  }
4234
4341
 
4342
+ /**
4343
+ * Generates closed-shadow DOM honeypot markup for server-rendered challenge pages.
4344
+ * Utilizes Declarative Shadow DOM with mode="closed" to prevent screen reader false positives
4345
+ * while capturing bot web scrapers and crawlers.
4346
+ * @param {string[]} trapUrls
4347
+ * @returns {string}
4348
+ */
4349
+ function generateClosedShadowTraps(trapUrls = []) {
4350
+ const list = Array.isArray(trapUrls) ? trapUrls : [];
4351
+ if (list.length === 0) return '';
4352
+
4353
+ const trapLinksHtml = list.map((url, idx) => {
4354
+ return `<a href="${url}" rel="nofollow" tabindex="-1"><span>&gt; ${idx + 1}</span></a>`;
4355
+ }).join(' ');
4356
+
4357
+ return `<div style="position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;" aria-hidden="true"><template shadowrootmode="closed" shadowroot="closed"><style>:host { position: absolute; left: -9999px; top: -9999px; width: 0; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; } a { color: transparent; text-decoration: none; }</style>${trapLinksHtml}</template></div>`;
4358
+ }
4359
+
4235
4360
  /**
4236
4361
  * Generates the HTML content for a combined CPU + Memory PoW challenge.
4237
4362
  * @param {object} cpuChallengeDetails - Details from generateCpuTargetChallenge.
@@ -4248,6 +4373,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
4248
4373
  const fingerprint = originalFingerprint;
4249
4374
  const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp, tlsSessionId);
4250
4375
  const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
4376
+ const trapContainerHtml = generateClosedShadowTraps(trapUrls);
4251
4377
 
4252
4378
  // Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
4253
4379
  const clientInitConfig = {
@@ -4315,12 +4441,24 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
4315
4441
  }
4316
4442
 
4317
4443
  if (!htmlTemplate) {
4318
- htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script></body></html>`; // eslint-disable-line max-len
4444
+ htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>`; // eslint-disable-line max-len
4319
4445
  }
4320
4446
 
4321
- return htmlTemplate
4447
+ let renderedHtml = htmlTemplate
4322
4448
  .replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
4323
4449
  .replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
4450
+
4451
+ if (renderedHtml.includes('<!-- FINGERPRINT_TRAPS -->')) {
4452
+ renderedHtml = renderedHtml.replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
4453
+ } else if (trapContainerHtml) {
4454
+ if (renderedHtml.includes('</body>')) {
4455
+ renderedHtml = renderedHtml.replace('</body>', `${trapContainerHtml}</body>`);
4456
+ } else {
4457
+ renderedHtml += trapContainerHtml;
4458
+ }
4459
+ }
4460
+
4461
+ return renderedHtml;
4324
4462
  }
4325
4463
 
4326
4464
  /**
@@ -4611,7 +4749,8 @@ export class FingerprintEngine {
4611
4749
  'similarityThreshold', 'reset',
4612
4750
  'ed25519_private_key', 'ed25519_public_key', 'upowModel',
4613
4751
  'federatedPeers', 'federationSecret', 'filterWhitelist',
4614
- 'challengeRateLimit'
4752
+ 'challengeRateLimit',
4753
+ 'differentialPrivacy', 'dpEpsilon'
4615
4754
  ]);
4616
4755
 
4617
4756
  // 1. Check for essential keys
@@ -4649,35 +4788,31 @@ export class FingerprintEngine {
4649
4788
  }
4650
4789
  }
4651
4790
  calculateFinalScore(suspicionVector) {
4652
- const { weights } = this.securityConfig;
4653
- if (!weights) return 0;
4654
-
4655
- const score =
4656
- (suspicionVector.historyScore || 0) * (weights.historyScore || 0) +
4657
- (suspicionVector.rotationScore || 0) * (weights.rotationScore || 0) +
4658
- (suspicionVector.headerAnomalyScore || 0) * (weights.headerAnomalyScore || 0) +
4659
- (suspicionVector.requestPatternScore || 0) * (weights.requestPatternScore || 0) +
4660
- (suspicionVector.inconsistencyScore || 0) * (weights.inconsistencyScore || 0) +
4661
- (suspicionVector.honeypotScore || 0) * (weights.honeypotScore || 0) +
4662
- (suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
4663
- (suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
4664
- (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
4665
- (suspicionVector.botnetClusterScore || 0) * (weights.botnetClusterScore || 0) +
4666
- (suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
4667
- (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
4668
- (suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0) +
4669
- (suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
4670
- (suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
4671
- (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
4672
- (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
4673
- (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
4674
- (suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) +
4675
- (suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
4676
- (suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
4677
- (suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
4678
- (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
4679
-
4680
- return Math.min(100, score);
4791
+ const baseWeights = this.securityConfig.weights;
4792
+ if (!baseWeights) return 0;
4793
+
4794
+ const dynamicWeights = { ...baseWeights };
4795
+ if ((suspicionVector.mtuAnomalyScore || 0) > 50.0) {
4796
+ this._log('Tunnel detected, amplifying suspicion weights.', { mtuScore: suspicionVector.mtuAnomalyScore });
4797
+
4798
+ // Augmente le poids des incohérences de bas niveau (difficiles à falsifier)
4799
+ dynamicWeights.tlsSpoofingScore = (baseWeights.tlsSpoofingScore || 0.8) * 1.25;
4800
+ dynamicWeights.crossLayerInconsistencyScore = (baseWeights.crossLayerInconsistencyScore || 0.4) * 1.4;
4801
+ dynamicWeights.clientHintsInconsistencyScore = (baseWeights.clientHintsInconsistencyScore || 0.7) * 1.2;
4802
+
4803
+ // Augmente le poids des anomalies comportementales (un bot derrière un VPN est plus suspect)
4804
+ dynamicWeights.behaviorScore = (baseWeights.behaviorScore || 0.7) * 1.15;
4805
+ dynamicWeights.requestPatternScore = (baseWeights.requestPatternScore || 0.6) * 1.2;
4806
+ }
4807
+
4808
+ let score = 0;
4809
+ for (const key in dynamicWeights) {
4810
+ const metricScore = suspicionVector[key] || 0.0;
4811
+ if (metricScore > 0) {
4812
+ score += metricScore * dynamicWeights[key];
4813
+ }
4814
+ }
4815
+ return Math.min(100.0, score);
4681
4816
  }
4682
4817
 
4683
4818
 
@@ -4998,9 +5133,84 @@ export class FingerprintEngine {
4998
5133
 
4999
5134
  // Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
5000
5135
  const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
5136
+
5137
+ // Règle Anti-Ferme : Même avec du matériel sécurisé, un appareil condamné ou menant une attaque certaine est révoqué et bloqué
5138
+ if (deviceData?.condemned || this._hasCertainAttack(requestContext)) {
5139
+ if (deviceData) {
5140
+ deviceData.condemned = true;
5141
+ deviceData.webauthnVerified = false;
5142
+ await store.set(`device:${deviceId}`, deviceData);
5143
+ }
5144
+ this._log('Condemned device or attack detected - revoking hardware trust and blocking', { deviceId });
5145
+ const decision = { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
5146
+ if (this.dryRun) {
5147
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
5148
+ decision.intendedAction = decision.action;
5149
+ decision.action = 'next';
5150
+ delete decision.status;
5151
+ delete decision.body;
5152
+ if (requestContext._newCookies) {
5153
+ const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
5154
+ if (deviceCookie) decision.newCookieForResponse = deviceCookie;
5155
+ }
5156
+ }
5157
+ return decision;
5158
+ }
5159
+
5160
+ // Validation immédiate de l'attestation matérielle WebAuthn si présente dans la requête
5161
+ const behaviorHeader = requestContext.headers?.['x-behavior-metrics'];
5162
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
5163
+ try {
5164
+ const parsedMetrics = JSON.parse(behaviorHeader);
5165
+ if (parsedMetrics && parsedMetrics.webauthnAnchor && deviceData) {
5166
+ if (verifyWebAuthnHardwareAnchor(parsedMetrics.webauthnAnchor, deviceData)) {
5167
+ deviceData.webauthnVerified = true;
5168
+ await store.set(`device:${deviceId}`, deviceData);
5169
+ }
5170
+ }
5171
+ } catch (e) {}
5172
+ }
5173
+
5001
5174
  if (deviceData && deviceData.webauthnVerified) {
5002
5175
  this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
5003
- return { action: 'next', score: 0, vector: { webauthn_verified: 100 } };
5176
+ const greenlistTtl = 365 * 86400 * 1000; // 1 an (permanent)
5177
+ const greenlistTicket = generateStatelessTicket({
5178
+ expiry: Date.now() + greenlistTtl,
5179
+ originalIp: clientIp,
5180
+ deviceId,
5181
+ deviceHash: 'webauthn:greenlist:' + (deviceData.webauthnCredentialId || deviceId),
5182
+ greenlist: true
5183
+ });
5184
+ const isHttps = requestContext.headers?.['x-forwarded-proto'] === 'https' ||
5185
+ requestContext.rawReq?.secure ||
5186
+ requestContext.rawReq?.protocol === 'https' ||
5187
+ requestContext.rawReq?.connection?.encrypted;
5188
+ const secureOption = isHttps || this.isProduction;
5189
+
5190
+ const decision = {
5191
+ action: 'next',
5192
+ score: 0,
5193
+ vector: { webauthn_verified: 100 },
5194
+ cookie: {
5195
+ name: 'pow_clearance',
5196
+ value: greenlistTicket,
5197
+ options: {
5198
+ httpOnly: true,
5199
+ secure: secureOption,
5200
+ sameSite: 'strict',
5201
+ ...(secureOption && { partitioned: true }),
5202
+ maxAge: greenlistTtl,
5203
+ path: '/'
5204
+ }
5205
+ }
5206
+ };
5207
+ if (requestContext._newCookies) {
5208
+ const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
5209
+ if (deviceCookie) {
5210
+ decision.newCookieForResponse = deviceCookie;
5211
+ }
5212
+ }
5213
+ return decision;
5004
5214
  }
5005
5215
 
5006
5216
  if (isStatic) {
@@ -5043,7 +5253,7 @@ export class FingerprintEngine {
5043
5253
  if (filterWhitelist === true) {
5044
5254
  bypassWhitelist = this._hasCertainAttack(requestContext);
5045
5255
  } else if (typeof filterWhitelist === 'number') {
5046
- const res = await getScoreAndVector();
5256
+ const res = await getScoreAndVector();
5047
5257
  if (res.score > filterWhitelist) {
5048
5258
  bypassWhitelist = true;
5049
5259
  }
@@ -6633,6 +6843,7 @@ export const default_whitelist = () => [
6633
6843
  googlebot_whitelist(),
6634
6844
  bingbot_whitelist(),
6635
6845
  yandex_whitelist(),
6846
+ facebook_whitelist(),
6636
6847
  // === Moteurs de recherche majeurs ===
6637
6848
  { userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
6638
6849
  { userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
@@ -6750,6 +6961,11 @@ export const bingbot_whitelist = () => ({
6750
6961
  entries: bingbotEntries
6751
6962
  });
6752
6963
 
6964
+ export const facebook_whitelist = () => ({
6965
+ type: 'allowlist',
6966
+ entries: facebookEntries
6967
+ });
6968
+
6753
6969
 
6754
6970
  /**
6755
6971
  * Extracts the TLS Session ID or ticket hash from the request context.
@@ -6983,6 +7199,9 @@ export const powMiddleware = (securityConfig) => {
6983
7199
 
6984
7200
  case 'next':
6985
7201
  default:
7202
+ if (decision.cookie && res && typeof res.cookie === 'function') {
7203
+ res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
7204
+ }
6986
7205
  return next();
6987
7206
  }
6988
7207
  };
@@ -6999,6 +7218,7 @@ export const __internal = {
6999
7218
  checkChallengeRateLimit,
7000
7219
  getDeviceHash,
7001
7220
  getCompositeDeviceHash,
7221
+ generateClosedShadowTraps,
7002
7222
  getSuspicionVector,
7003
7223
  getTlsSessionId,
7004
7224
  pruneTrafficData,