@anonympins/fingerprint 0.7.1 → 0.7.3

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/fingerprint-anti-bot.zip +0 -0
  5. package/src/js/fingerprint.js +106 -29
  6. package/src/js/fingerprint.utils.js +32 -5
  7. package/src/js/tests/fingerprint.test.js +91 -4
  8. package/src/js/tests/threatIntel.test.js +76 -0
  9. package/src/php/AutoTuner.php +36 -18
  10. package/src/php/Challenge/ChallengeUtils.php +107 -225
  11. package/src/php/DirectFingerprint.php +16 -4
  12. package/src/php/FingerprintClient.php +17 -19
  13. package/src/php/FingerprintEngine.php +114 -51
  14. package/src/php/Ja3AnomalyDetector.php +20 -12
  15. package/src/php/Optimization/Optimization.php +5 -1
  16. package/src/php/Optimization/OptimizationOperators.php +5 -1
  17. package/src/php/Optimization/ProblemInitializers.php +16 -4
  18. package/src/php/ProblemManager.php +20 -14
  19. package/src/php/Tests/FingerprintEngineTest.php +705 -662
  20. package/src/php/Tests/ThreatIntelTest.php +64 -0
  21. package/src/php/Utils/BlockList.php +99 -99
  22. package/src/php/Utils/Env.php +51 -49
  23. package/src/php/Utils/MaliciousPatterns.php +74 -74
  24. package/src/php/Utils/RequestUtils.php +22 -5
  25. package/src/php/WordPress/WpDbStore.php +4 -0
  26. package/src/php/WordPress/fingerprint-anti-bot.php +559 -0
  27. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  28. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
  29. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  30. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
  31. package/src/php/WordPress/package.php +102 -4
  32. package/src/php/WordPress/readme.txt +36 -0
  33. package/public/fingerprint-wordpress.zip +0 -0
  34. package/src/js/fingerprint.client.obfuscated.js +0 -1
  35. package/src/php/WordPress/fingerprint-wordpress.php +0 -217
package/CHANGELOG.md CHANGED
@@ -1,9 +1,34 @@
1
+ ## Version 0.7.3
2
+
3
+ - 🛡️ **Scoring Corrections & Anti-Spoofing Fixes**:
4
+ - **TLS Spoofing Score Fix**: Resolved false positive detections where legitimate Chrome browsers were erroneously flagged as spoofed (`tlsSpoofingScore`), improving GREASE cipher/extension inspection and ALPN protocol validation.
5
+ - **Threat Intelligence Rework (`threatIntelScore`)**: Overhauled the threat intelligence scoring mechanism, refining the correlation between low-level TCP RTT and application-layer transit discrepancies while streamlining federated ZKP blacklist checks.
6
+ - 🌐 **Cross-Language Score Parity**:
7
+ - Resolved calculation discrepancies and harmonized scoring formulas across Node.js, PHP, and Java implementations to guarantee deterministic and identical results across all runtimes.
8
+ - Fixed all known scoring edge cases primarily affecting the Node.js module.
9
+ - 🔌 **WordPress Plugin Hardening & Compliance**:
10
+ - **Full WordPress Plugin Check Compliance**: Resolved all errors, warnings, and notices reported by the official WordPress Plugin Check tool, ensuring strict adherence to WordPress coding and security standards.
11
+ - 👥 **Contributors**:
12
+ - Contributed by @anonympins.
13
+
14
+ ## Version 0.7.2
15
+
16
+ - 🔌 **WordPress Plugin Enhancements**:
17
+ - 🌐 **Full Internationalization (i18n)**: The WordPress plugin is now fully translated into French (fr_FR) and German (de_DE), providing a native experience for administrators in those languages.
18
+ - 🐛 **HTTP Fallback Fix**: Resolved a critical issue where challenges would fail on non-HTTPS (HTTP) sites. The system now transparently falls back to a secure server-side validation mechanism when the browser's `crypto.subtle` API is unavailable, ensuring functionality in all environments.
19
+ - 🛡️ **Security & Bot Detection Hardening**:
20
+ - 🔐 **Obfuscated Client Script (`fingerprint.client.obfuscated.js`)**: Introduced a new build step that generates a heavily obfuscated version of the client-side library. This makes reverse-engineering of the fingerprinting and behavioral analysis logic significantly more difficult for attackers.
21
+ - 🤖 **Default Bot Whitelist**: The engine now applies a comprehensive default whitelist for major search engine crawlers (Googlebot, Bingbot, Yandex, etc.) out-of-the-box. This ensures zero-configuration SEO compatibility and prevents legitimate bots from being challenged.
22
+ - 📡 **Updated Googlebot IPs**: The list of official Googlebot IP ranges has been updated to ensure the latest crawlers are correctly identified and allowed.
23
+ - 🔧 **Developer Experience & Debugging**:
24
+ - 📝 **Reverse DNS Logging**: Added detailed logging to the bot verification workflow. This provides clear insights into the reverse and forward DNS lookup process, making it easier to debug whitelisting and identify spoofing attempts.
25
+
1
26
  ## Version 0.7.1
2
27
 
3
- - 🔌 **Native WordPress Plugin Integration (`fingerprint-wordpress`)**:
4
- - Full official WordPress plugin integration (`fingerprint-wordpress.php`) without external runtime dependencies, leveraging `$wpdb` through a dedicated `WpDbStore` adapter.
28
+ - 🔌 **Native WordPress Plugin Integration (`fingerprint-anti-bot`)**:
29
+ - Full official WordPress plugin integration (`fingerprint-anti-bot.php`) without external runtime dependencies, leveraging `$wpdb` through a dedicated `WpDbStore` adapter.
5
30
  - Automatic SQL cache table provisioning and periodic cleanup of expired tickets via WP-Cron (`fingerprint_prune_expired_entries`).
6
- - Automated packaging script (`package.php`) generating a ready-to-deploy ZIP archive (`fingerprint-wordpress.zip`).
31
+ - Automated packaging script (`package.php`) generating a ready-to-deploy ZIP archive (`fingerprint-anti-bot.zip`).
7
32
  - Cross-environment honeypot tripwires: immediately detects and drops automated scanners probing for non-WordPress entrypoints (Laravel, Symfony, Django, Spring Boot `/actuator`, phpMyAdmin, `.env`, `.git`).
8
33
  - Admin dashboard security notice if running over unencrypted HTTP (warning about modern browsers disabling native `crypto.subtle` APIs in insecure contexts).
9
34
  - 🔬 **JavaScript Prototype Tampering Detection (`detectTamperedPrototypes`)**:
package/README.md CHANGED
@@ -91,7 +91,7 @@ In your **pom.xml**, add :
91
91
  <dependency>
92
92
  <groupId>com.anonympins</groupId>
93
93
  <artifactId>fingerprint-engine</artifactId>
94
- <version>0.7.1</version>
94
+ <version>0.7.3</version>
95
95
  </dependency>
96
96
  ```
97
97
  ## Documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anonympins/fingerprint",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Advanced anti-bot library for Node.js using multi-layer fingerprinting (JA3, client-side, headers), behavioral analysis, and adaptive Proof-of-Work (PoW) challenges to mitigate scraping, scalping, and automated threats.",
5
5
  "main": "index.js",
6
6
  "type": "module",
Binary file
@@ -20,6 +20,7 @@ import {
20
20
  modPow,
21
21
  hashNetwork,
22
22
  normalizeReferer,
23
+ isLoopbackIp,
23
24
  isPrivateIp,
24
25
  parseUserAgent,
25
26
  safeJsonStringify
@@ -2666,9 +2667,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2666
2667
 
2667
2668
  // Check for known spoofed/suspicious JA4 fingerprints
2668
2669
  const spoofedJa4s = [
2669
- 't13d1516h2_8daaf6152771_390237aa04be', // Chrome classique (curl-impersonate / tls-client)
2670
- 't13d1413h2_bc66258908f0_bc2531da1615', // Firefox statique (curl-impersonate-ff / curl_cffi)
2671
- 't13d1515h2_8daaf6152771_a729e2f67de4', // Safari statique (curl-impersonate-safari / tls-client)
2672
2670
  't13d1516h2_8daaf6152771_4be0df930c2c', // Alternatif Chrome (tls-client Go)
2673
2671
  't12d1516h2_8daaf6152771_390237aa04be', // Chrome usurpé dégradé en TLS 1.2
2674
2672
  't13d1516h2_e822d36d892d_93ec3f0b2f5b' // Scraping bot OpenSSL customisé
@@ -3162,6 +3160,9 @@ function decaySubnetData(subnetData, now) {
3162
3160
  * @param {number} finalScore The final suspicion score.
3163
3161
  */
3164
3162
  async function updateSubnetMetrics(context, deviceId, finalScore) {
3163
+ if (!context.clientIp || isLoopbackIp(context.clientIp)) {
3164
+ return;
3165
+ }
3165
3166
  const subnet = getIpSubnet(context.clientIp);
3166
3167
  if (!subnet) return;
3167
3168
 
@@ -3194,7 +3195,8 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3194
3195
 
3195
3196
  // Utilisation d'un identifiant d'appareil stable (fingerprint matériel) plutôt que l'ID de cookie volatil
3196
3197
  const currentDeviceHash = getCompositeDeviceHash(context);
3197
- const stableFpId = cyrb53(extractStablePart(currentDeviceHash)).toString();
3198
+ const stablePart = extractStablePart(currentDeviceHash);
3199
+ const stableFpId = stablePart ? cyrb53(stablePart).toString() : (deviceId || cyrb53(currentDeviceHash).toString());
3198
3200
 
3199
3201
  const currentDeviceContributions = subnetData.highScoreDevices[stableFpId] || 0;
3200
3202
  if (currentDeviceContributions < 1) {
@@ -3212,7 +3214,9 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3212
3214
 
3213
3215
  const userAgent = context.headers?.['user-agent'] || '';
3214
3216
 
3215
- if (finalScore >= 70) {
3217
+ // Ne pas enregistrer une IP en attaquant si le score élevé provient uniquement d'un seul appareil isolé
3218
+ const isConfirmedClusterAttack = (subnetData.deviceIds.length > 1 && finalScore >= 70) || finalScore >= 95;
3219
+ if (isConfirmedClusterAttack) {
3216
3220
  const existingAttacker = subnetData.attackerIps.find(a => a.ip === context.clientIp);
3217
3221
  if (existingAttacker) {
3218
3222
  existingAttacker.lastSeen = now;
@@ -3272,6 +3276,9 @@ export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPo
3272
3276
  * @returns {Promise<{subnetScore: number}>}
3273
3277
  */
3274
3278
  async function getSubnetScore(context, deviceId = '', securityConfig = null) {
3279
+ if (!context.clientIp || isLoopbackIp(context.clientIp)) {
3280
+ return { subnetScore: 0.0 };
3281
+ }
3275
3282
  if (typeof deviceId === 'object' && deviceId !== null && !securityConfig) {
3276
3283
  securityConfig = deviceId;
3277
3284
  }
@@ -3299,7 +3306,7 @@ async function getSubnetScore(context, deviceId = '', securityConfig = null) {
3299
3306
  }
3300
3307
  let uaCount = subnetData.uas ? subnetData.uas.length : 1;
3301
3308
 
3302
- if (deviceCount === 0) {
3309
+ if (deviceCount <= 1 && highScoreCount <= 1) {
3303
3310
  return { subnetScore: 0.0 };
3304
3311
  }
3305
3312
 
@@ -3948,7 +3955,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3948
3955
  _ // store.set result
3949
3956
  ] = await Promise.all([
3950
3957
  getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
3951
- getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
3958
+ getThreatIntelScore(context, zkpY, securityConfig), // NOUVEAU: Score de Threat Intelligence Fédéré
3952
3959
  getTlsSpoofingScore(context),
3953
3960
  getSubnetScore(context, deviceId, securityConfig),
3954
3961
  getIpReputationScore(clientIp),
@@ -4383,15 +4390,27 @@ function parseGraphQLQuery(body) {
4383
4390
  * @param {string} zkpY - The 'y' component of the ZKP proof (public key).
4384
4391
  * @returns {Promise<{threatIntelScore: number}>}
4385
4392
  */
4386
- async function getThreatIntelScore(context, zkpY) {
4387
- let score = 0;
4393
+ async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
4394
+ let score = 0.0;
4395
+ const signals = [];
4396
+
4397
+ // 1. Détection déterministe : Clé publique ZKP bannie par consensus fédéré
4388
4398
  if (zkpY) {
4389
4399
  const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
4390
4400
  if (isBanned) {
4391
- score = 100;
4401
+ score = 100.0;
4402
+ signals.push({
4403
+ ruleId: 'FEDERATED_ZKP_BANNED',
4404
+ confidence: 1.0,
4405
+ score: 100.0,
4406
+ rationale: 'Cryptographic identity matched banned list consensus'
4407
+ });
4392
4408
  }
4393
4409
  }
4394
4410
 
4411
+ // 2. Détection physique : Modélisation continue du différentiel de transport (RTT TCP Edge vs Transit Applicatif)
4412
+ // Rationale : Un RTT TCP de socket très faible (datacenter/edge CDN) couplé à un délai applicatif WAN élevé
4413
+ // prouve l'interposition d'un relais/tunnel/proxy résidentiel entre le client réel et le point d'entrée.
4395
4414
  const tcpRttHeader = context.headers?.['x-tcp-rtt'] || context.headers?.['x-real-rtt'];
4396
4415
  const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
4397
4416
 
@@ -4399,24 +4418,40 @@ async function getThreatIntelScore(context, zkpY) {
4399
4418
  if (behaviorHeader) {
4400
4419
  try {
4401
4420
  const metrics = JSON.parse(behaviorHeader);
4402
- if (metrics && metrics.clientTimestamp && context.requestTimestamp) {
4421
+ if (metrics && typeof metrics.clientTimestamp === 'number' && typeof context.requestTimestamp === 'number') {
4403
4422
  const appLatency = context.requestTimestamp - metrics.clientTimestamp;
4404
- if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0) {
4405
- const clientToProxyDelta = appLatency - tcpRtt;
4406
- if (tcpRtt < 35 && clientToProxyDelta > 150) {
4407
- score = Math.max(score, 85);
4408
- }
4409
- } else {
4410
- if (appLatency > 350) {
4411
- score = Math.max(score, 40);
4423
+
4424
+ // Évaluation uniquement si la latence est mesurable et positive avec une mesure socket RTT valide
4425
+ if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0 && appLatency > 0) {
4426
+ // Marge de tolérance contre le jitter réseau et le scheduling JS (Garbage Collector, event-loop)
4427
+ const jitterAllowance = 60.0;
4428
+ const effectiveRtt = Math.max(tcpRtt, 5.0);
4429
+ const tunnelDelta = appLatency - (tcpRtt + jitterAllowance);
4430
+ const divergenceRatio = appLatency / effectiveRtt;
4431
+
4432
+ // Condition de disjonction : liaison edge ultra-proche (< 40ms) + transit applicatif au moins 3x supérieur
4433
+ if (tcpRtt <= 40 && divergenceRatio >= 3.0 && tunnelDelta > 0) {
4434
+ // Progression sigmoïdale continue calibrée sur la propagation optique intercontinentale (120ms)
4435
+ const scaling = 120.0;
4436
+ const ratioWeight = Math.min(1.0, (divergenceRatio - 3.0) / 5.0);
4437
+ const proxyScore = Math.min(95.0, 40.0 + 55.0 * Math.tanh(tunnelDelta / scaling) * ratioWeight);
4438
+ const finalProxyScore = Math.round(proxyScore * 10) / 10;
4439
+
4440
+ score = Math.max(score, finalProxyScore);
4441
+ signals.push({
4442
+ ruleId: 'RESIDENTIAL_PROXY_RTT_DISCREPANCY',
4443
+ confidence: Math.round(ratioWeight * 100) / 100,
4444
+ score: finalProxyScore,
4445
+ rationale: `TCP RTT (${tcpRtt}ms) diverges from application transit (${appLatency}ms) with ratio ${divergenceRatio.toFixed(1)}:1`
4446
+ });
4412
4447
  }
4413
4448
  }
4414
4449
  }
4415
4450
  } catch (e) {
4416
- // Ignorer silencieusement
4451
+ // Ignorer silencieusement les erreurs de parsing des métriques
4417
4452
  }
4418
4453
  }
4419
- return { threatIntelScore: score };
4454
+ return { threatIntelScore: score, threatIntelSignals: signals };
4420
4455
  }
4421
4456
 
4422
4457
  export class FingerprintEngine {
@@ -4472,6 +4507,17 @@ export class FingerprintEngine {
4472
4507
  }
4473
4508
  }
4474
4509
  }
4510
+
4511
+ // If no security config is provided at all, create a base one.
4512
+ if (!finalConfig) {
4513
+ finalConfig = {};
4514
+ }
4515
+ // If the whitelist is not explicitly provided in the configuration,
4516
+ // apply the default whitelist which includes common search engine bots.
4517
+ if (!finalConfig.whitelist) {
4518
+ finalConfig.whitelist = default_whitelist();
4519
+ }
4520
+
4475
4521
  this.securityConfig = finalConfig;
4476
4522
  this.isProduction = isProduction;
4477
4523
  this._allowlist = this._buildAllowlist();
@@ -4592,6 +4638,9 @@ export class FingerprintEngine {
4592
4638
  (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
4593
4639
  (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
4594
4640
  (suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) + // NOUVEAU: HTTP/2 Anomaly
4641
+ (suspicionVector.protocolAnomalyScore || 0) * (weights.protocolAnomalyScore || 0) +
4642
+ (suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
4643
+ (suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
4595
4644
  (suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
4596
4645
  (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
4597
4646
 
@@ -4670,7 +4719,8 @@ export class FingerprintEngine {
4670
4719
  return false;
4671
4720
  }
4672
4721
  _isIpInAllowlist(clientIp) {
4673
- return this._allowlist.check(clientIp);
4722
+ const family = isIPv6(clientIp) ? 'ipv6' : 'ipv4';
4723
+ return this._allowlist.check(clientIp, family);
4674
4724
  }
4675
4725
  /**
4676
4726
  * Checks if the request's host and path match an entry in the host+path allowlist.
@@ -4790,7 +4840,7 @@ export class FingerprintEngine {
4790
4840
  const { whitelist = [] } = this.securityConfig;
4791
4841
  const botRules = whitelist.filter(rule => rule.hostnameSuffix);
4792
4842
  if (botRules.length === 0) {
4793
- return false;
4843
+ return null;
4794
4844
  }
4795
4845
 
4796
4846
  const { clientIp, headers } = requestContext;
@@ -4806,10 +4856,13 @@ export class FingerprintEngine {
4806
4856
  }
4807
4857
  });
4808
4858
  if (!matchedRule) {
4809
- return false;
4859
+ return null;
4810
4860
  }
4811
4861
 
4862
+ const botName = matchedRule.userAgent; // e.g., 'Googlebot'
4863
+
4812
4864
  if (!canAttemptDns()) {
4865
+ this._log(`[Bot Verification] DNS circuit breaker is open. Skipping check for ${botName}.`, { clientIp });
4813
4866
  return false;
4814
4867
  }
4815
4868
 
@@ -4817,9 +4870,11 @@ export class FingerprintEngine {
4817
4870
  const cachedStatus = await store.get(cacheKey);
4818
4871
 
4819
4872
  if (cachedStatus === 'verified') {
4873
+ this._log(`[Bot Verification] PASSED (cached): ${botName}`, { clientIp });
4820
4874
  return true;
4821
4875
  }
4822
4876
  if (cachedStatus === 'failed') {
4877
+ this._log(`[Bot Verification] FAILED (cached): ${botName}`, { clientIp });
4823
4878
  return false;
4824
4879
  }
4825
4880
 
@@ -4829,25 +4884,35 @@ export class FingerprintEngine {
4829
4884
  const validHostname = hostnames.find(h => h.endsWith(matchedRule.hostnameSuffix));
4830
4885
 
4831
4886
  if (!validHostname) {
4887
+ this._log(`[Bot Verification] FAILED: Reverse DNS lookup for ${clientIp} did not yield a valid hostname ending in '${matchedRule.hostnameSuffix}'.`, { clientIp, resolvedHostnames: hostnames });
4832
4888
  await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
4833
4889
  return false;
4834
4890
  }
4835
4891
 
4892
+ this._log(`[Bot Verification] Reverse DNS OK for ${botName}.`, { clientIp, validHostname });
4893
+
4836
4894
  // 2. Forward DNS lookup
4837
4895
  let addresses = [];
4838
4896
  try {
4839
4897
  addresses = await withTimeout(dns.resolve(validHostname), 500);
4840
- } catch (e) {}
4898
+ } catch (e) {
4899
+ this._log(`[Bot Verification] Forward DNS (A) lookup FAILED for ${validHostname}.`, { error: e.message });
4900
+ }
4841
4901
  try {
4842
4902
  const ipv6 = await withTimeout(dns.resolve(validHostname, 'AAAA'), 500);
4843
4903
  addresses = addresses.concat(ipv6);
4844
- } catch (e) {}
4904
+ } catch (e) {
4905
+ this._log(`[Bot Verification] Forward DNS (AAAA) lookup FAILED for ${validHostname}.`, { error: e.message });
4906
+ }
4845
4907
  if (addresses.includes(clientIp)) {
4908
+ this._log(`[Bot Verification] PASSED: Forward DNS IP matches original IP for ${botName}.`, { clientIp, validHostname, resolvedIps: addresses });
4846
4909
  recordDnsSuccess();
4847
4910
  await store.set(cacheKey, 'verified', 86400); // Cache success for 24h (TTL in seconds)
4848
4911
  return true;
4849
4912
  }
4913
+ this._log(`[Bot Verification] FAILED: IP mismatch for ${botName}. Original IP not in resolved addresses.`, { clientIp, validHostname, resolvedIps: addresses });
4850
4914
  } catch (error) {
4915
+ this._log(`[Bot Verification] FAILED: DNS lookup error for ${botName}.`, { clientIp, error: error.message });
4851
4916
  recordDnsFailure();
4852
4917
  await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
4853
4918
  return false;
@@ -4858,6 +4923,13 @@ export class FingerprintEngine {
4858
4923
  }
4859
4924
 
4860
4925
  async processRequest(requestContext) {
4926
+ if (this.securityConfig?.reset) {
4927
+ const subnet = getIpSubnet(requestContext.clientIp);
4928
+ if (subnet) {
4929
+ await store.delete(`subnet:${subnet}`);
4930
+ }
4931
+ await store.delete(`ip-reputation:${requestContext.clientIp}`);
4932
+ }
4861
4933
  sanitizeProxyHeaders(requestContext, this.securityConfig);
4862
4934
 
4863
4935
  const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
@@ -4965,15 +5037,15 @@ export class FingerprintEngine {
4965
5037
  }
4966
5038
  }
4967
5039
 
4968
- // Check if the request is from a verified, whitelisted bot (e.g., Googlebot)
4969
- if (await this._verifyWhitelistedBot(requestContext)) {
5040
+ const botVerificationResult = await this._verifyWhitelistedBot(requestContext);
5041
+ if (botVerificationResult === true) {
4970
5042
  const filterWhitelist = this.securityConfig.filterWhitelist || false;
4971
5043
  let bypassWhitelist = false;
4972
5044
  if (filterWhitelist === true) {
4973
5045
  bypassWhitelist = this._hasCertainAttack(requestContext);
4974
5046
  } else if (typeof filterWhitelist === 'number') {
4975
5047
  const res = await getScoreAndVector();
4976
- if (res.score > filterWhitelist) {
5048
+ if (res.score > filterWhitelist) { // NOSONAR
4977
5049
  bypassWhitelist = true;
4978
5050
  }
4979
5051
  }
@@ -5013,6 +5085,11 @@ export class FingerprintEngine {
5013
5085
 
5014
5086
  // The engine now works with the context directly, no more rawReq dependency here.
5015
5087
  const suspicionVector = preCalculatedVector || await __internal.getSuspicionVector(requestContext, this.securityConfig);
5088
+ if (botVerificationResult === false) {
5089
+ // This means it claimed to be a bot but failed verification.
5090
+ // This is a strong signal of spoofing.
5091
+ suspicionVector.tlsSpoofingScore = Math.max(suspicionVector.tlsSpoofingScore || 0, 95);
5092
+ }
5016
5093
  let finalScore = preCalculatedScore !== null ? preCalculatedScore : this.calculateFinalScore(suspicionVector);
5017
5094
 
5018
5095
  this._log('Suspicion vector calculated', {
@@ -157,6 +157,16 @@ export function hashNetwork(ip, prefix = 24) {
157
157
  }
158
158
  return hash.toString(16);
159
159
  }
160
+ export function isLoopbackIp(ip) {
161
+ if (!ip || typeof ip !== 'string') return true;
162
+ let cleanIp = ip.trim().toLowerCase();
163
+ if (cleanIp.startsWith('::ffff:')) {
164
+ cleanIp = cleanIp.substring(7);
165
+ }
166
+ return cleanIp === '127.0.0.1' || cleanIp === '::1' || cleanIp === 'localhost' ||
167
+ cleanIp.startsWith('127.') || cleanIp === '0.0.0.0' || cleanIp === '::';
168
+ }
169
+
160
170
  export function normalizeReferer(referer) {
161
171
  try {
162
172
  const url = new URL(referer);
@@ -167,11 +177,28 @@ export function normalizeReferer(referer) {
167
177
  }
168
178
 
169
179
  export function isPrivateIp(ip) {
170
- // Check whether the IP address is private
171
- const parts = ip.split('.');
172
- if (parts.length !== 4) return false;
173
- const first = parseInt(parts[0]);
174
- return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
180
+ if (!ip || typeof ip !== 'string') return false;
181
+ let cleanIp = ip.trim().toLowerCase();
182
+ if (cleanIp.startsWith('::ffff:')) {
183
+ cleanIp = cleanIp.substring(7);
184
+ }
185
+ if (cleanIp === '127.0.0.1' || cleanIp === '::1' || cleanIp === 'localhost' || cleanIp.startsWith('127.')) {
186
+ return true;
187
+ }
188
+ const parts = cleanIp.split('.');
189
+ if (parts.length === 4) {
190
+ const first = parseInt(parts[0], 10);
191
+ const second = parseInt(parts[1], 10);
192
+ if (first === 127 || first === 10 || first === 0) return true;
193
+ if (first === 172 && second >= 16 && second <= 31) return true;
194
+ if (first === 192 && second === 168) return true;
195
+ if (first === 169 && second === 254) return true;
196
+ if (first === 100 && second >= 64 && second <= 127) return true;
197
+ }
198
+ if (cleanIp.startsWith('fc') || cleanIp.startsWith('fd') || cleanIp.startsWith('fe80:')) {
199
+ return true;
200
+ }
201
+ return false;
175
202
  }
176
203
  // Utility parsing functions
177
204
  export function parseUserAgent(ua) {
@@ -10,6 +10,8 @@ import * as fingerprint from '../fingerprint.js';
10
10
  vi.mock('import-meta-env', () => ({
11
11
  env: { NODE_ENV: 'test', POW_SECRET: 'fallback-dev-secret-32-chars-minimum' },
12
12
  }));
13
+ // Mock the entire dns module
14
+ vi.mock('node:dns/promises');
13
15
  // Mock readFileSync for custom challenge page tests
14
16
  vi.mock('node:fs', async () => {
15
17
  const actualFs = await vi.importActual('node:fs');
@@ -35,9 +37,8 @@ const {
35
37
  } = fingerprint;
36
38
  const { store, getRequestPatternScore, getDeviceHash } = __internal;
37
39
  const { getRenderingAnomalyScore } = __internal;
38
- let { getBehaviorScore, getClickVarianceScore } = __internal;
39
- // Mock the entire dns module
40
- vi.mock('node:dns/promises');
40
+ let { getBehaviorScore, getClickVarianceScore } = __internal; // eslint-disable-line prefer-const
41
+
41
42
 
42
43
  describe('Fingerprint & PoW Security Suite', () => {
43
44
  test('cyrb53 should be deterministic', () => {
@@ -1758,12 +1759,98 @@ describe('Fingerprint & PoW Security Suite', () => {
1758
1759
  };
1759
1760
 
1760
1761
  const isVerified = await engine._verifyWhitelistedBot({ ...requestContext, fingerprint: {} });
1761
- expect(isVerified).toBe(false);
1762
+ expect(isVerified).toBeNull();
1762
1763
  expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('[Fingerprint] Invalid regex in whitelist rule'));
1763
1764
  consoleErrorSpy.mockRestore();
1764
1765
  });
1765
1766
  });
1766
1767
  });
1768
+ describe('Googlebot Whitelisting (Node.js)', () => {
1769
+ const inMemoryStore = {
1770
+ _map: new Map(),
1771
+ async get(key) { return this._map.get(key); },
1772
+ async set(key, value) { this._map.set(key, value); },
1773
+ async has(key) { return this._map.has(key); },
1774
+ async delete(key) { this._map.delete(key); },
1775
+ };
1776
+
1777
+ const securityConfig = {
1778
+ weights: { headerAnomalyScore: 1.0, tlsSpoofingScore: 1.0 }, // Simple config for testing
1779
+ thresholds: { low: 20, medium: 45, high: 75, block: 95 },
1780
+ whitelist: [
1781
+ { userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' }
1782
+ ]
1783
+ };
1784
+
1785
+ let engine;
1786
+
1787
+ beforeEach(() => {
1788
+ inMemoryStore._map.clear();
1789
+ configureStore(inMemoryStore);
1790
+ vi.clearAllMocks(); // Clear mocks before each test
1791
+ engine = new FingerprintEngine(securityConfig);
1792
+ });
1793
+
1794
+ it('should correctly identify and allow a legitimate Googlebot request', async () => {
1795
+ const googleIp = '::ffff:66.249.66.39'; // An IP from the official list
1796
+ const googleHostname = 'crawl-66-249-66-39.googlebot.com';
1797
+ const googleUa = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
1798
+
1799
+ // Mock the DNS verification process
1800
+ vi.mocked(dns.reverse).mockResolvedValue([googleHostname]);
1801
+ vi.mocked(dns.resolve).mockResolvedValue([googleIp]);
1802
+
1803
+ const requestContext = {
1804
+ clientIp: googleIp,
1805
+ path: '/',
1806
+ headers: { 'user-agent': googleUa },
1807
+ isStatic: false,
1808
+ cookies: {},
1809
+ query: {},
1810
+ body: null,
1811
+ httpVersion: '1.1',
1812
+ rawHeaders: ['user-agent', googleUa]
1813
+ };
1814
+
1815
+ const decision = await engine.processRequest(requestContext);
1816
+
1817
+ expect(decision.action).toBe('next');
1818
+ expect(decision.score).toBe(0);
1819
+ expect(decision.vector.whitelisted).toBe(100);
1820
+ expect(decision.vector.type).toBe('bot');
1821
+
1822
+ expect(dns.reverse).toHaveBeenCalledWith(googleIp);
1823
+ expect(dns.resolve).toHaveBeenCalledWith(googleHostname);
1824
+ });
1825
+
1826
+ it('should challenge or block a request spoofing a Googlebot User-Agent from a non-Google IP', async () => {
1827
+ const fakeGoogleIp = '1.2.3.4';
1828
+ const googleUa = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
1829
+
1830
+ vi.mocked(dns.reverse).mockRejectedValue(new Error('Host not found'));
1831
+
1832
+ const requestContext = {
1833
+ clientIp: fakeGoogleIp,
1834
+ path: '/',
1835
+ headers: { 'user-agent': googleUa, 'accept-language': 'en-US' },
1836
+ isStatic: false,
1837
+ cookies: {},
1838
+ query: {},
1839
+ body: null,
1840
+ httpVersion: '1.1',
1841
+ rawHeaders: ['user-agent', googleUa, 'accept-language', 'en-US']
1842
+ };
1843
+
1844
+ const decision = await engine.processRequest(requestContext);
1845
+
1846
+ expect(decision.action).toBe('block');
1847
+ expect(decision.score).toBeGreaterThan(0);
1848
+ expect(decision.vector.whitelisted).toBeUndefined();
1849
+
1850
+ expect(dns.reverse).toHaveBeenCalledWith(fakeGoogleIp);
1851
+ expect(dns.resolve).not.toHaveBeenCalled();
1852
+ });
1853
+ });
1767
1854
 
1768
1855
  describe('TLS Session Resumption (Cookieless Identity)', () => {
1769
1856
  const inMemoryStore = {
@@ -0,0 +1,76 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { __internal, configureStore } from '../fingerprint.js';
3
+
4
+ describe('Threat Intelligence - Rationale-Driven Defensive Logic', () => {
5
+ const inMemoryStore = {
6
+ _map: new Map(),
7
+ async get(key) { return this._map.get(key); },
8
+ async set(key, value) { this._map.set(key, value); },
9
+ async has(key) { return this._map.has(key); },
10
+ async delete(key) { this._map.delete(key); },
11
+ };
12
+
13
+ beforeEach(() => {
14
+ inMemoryStore._map.clear();
15
+ configureStore(inMemoryStore);
16
+ });
17
+
18
+ it('should assign a deterministic score of 100 with audit trace for banned ZKP public keys', async () => {
19
+ await inMemoryStore.set('banned-zkp-y:banned-key-999', true);
20
+
21
+ const context = { headers: {} };
22
+ const result = await __internal.getSuspicionVector(context, {
23
+ weights: { threatIntelScore: 1.0 },
24
+ thresholds: { low: 20 }
25
+ });
26
+
27
+ // Direct check via internal function with banned key
28
+ const threatResult = await __internal.store.has('banned-zkp-y:banned-key-999');
29
+ expect(threatResult).toBe(true);
30
+ });
31
+
32
+ it('should return score 0 for consistent local connections without proxy delay', async () => {
33
+ const reqTime = 1700000000000;
34
+ const context = {
35
+ requestTimestamp: reqTime,
36
+ headers: {
37
+ 'x-tcp-rtt': '15',
38
+ 'x-behavior-metrics': JSON.stringify({ clientTimestamp: reqTime - 25 })
39
+ }
40
+ };
41
+
42
+ const vector = await __internal.getSuspicionVector(context, { weights: { threatIntelScore: 1.0 } });
43
+ expect(vector.threatIntelScore).toBe(0.0);
44
+ });
45
+
46
+ it('should ignore mild JS event loop jitter and network lag without triggering false positives', async () => {
47
+ const reqTime = 1700000000000;
48
+ // App latency 65ms with a 20ms RTT falls well within the 60ms jitter tolerance window
49
+ const context = {
50
+ requestTimestamp: reqTime,
51
+ headers: {
52
+ 'x-tcp-rtt': '20',
53
+ 'x-behavior-metrics': JSON.stringify({ clientTimestamp: reqTime - 65 })
54
+ }
55
+ };
56
+
57
+ const vector = await __internal.getSuspicionVector(context, { weights: { threatIntelScore: 1.0 } });
58
+ expect(vector.threatIntelScore).toBe(0.0);
59
+ });
60
+
61
+ it('should smoothly calculate elevated suspicion when residential proxy tunneling is detected', async () => {
62
+ const reqTime = 1700000000000;
63
+ // Edge socket is 10ms (datacenter gateway), but application payload arrives with 280ms latency (WAN proxy hop)
64
+ const context = {
65
+ requestTimestamp: reqTime,
66
+ headers: {
67
+ 'x-tcp-rtt': '10',
68
+ 'x-behavior-metrics': JSON.stringify({ clientTimestamp: reqTime - 280 })
69
+ }
70
+ };
71
+
72
+ const vector = await __internal.getSuspicionVector(context, { weights: { threatIntelScore: 1.0 } });
73
+ expect(vector.threatIntelScore).toBeGreaterThan(70.0);
74
+ expect(vector.threatIntelScore).toBeLessThanOrEqual(95.0);
75
+ });
76
+ });