@anonympins/fingerprint 0.7.2 → 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 (33) hide show
  1. package/CHANGELOG.md +22 -9
  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 +68 -23
  6. package/src/js/fingerprint.utils.js +32 -5
  7. package/src/js/tests/fingerprint.test.js +1 -1
  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 +106 -224
  11. package/src/php/DirectFingerprint.php +16 -4
  12. package/src/php/FingerprintClient.php +17 -19
  13. package/src/php/FingerprintEngine.php +77 -39
  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 +6 -1
  20. package/src/php/Tests/ThreatIntelTest.php +64 -0
  21. package/src/php/Utils/Env.php +51 -49
  22. package/src/php/Utils/MaliciousPatterns.php +74 -74
  23. package/src/php/Utils/RequestUtils.php +22 -5
  24. package/src/php/WordPress/WpDbStore.php +4 -0
  25. package/src/php/WordPress/{fingerprint-wordpress.php → fingerprint-anti-bot.php} +131 -126
  26. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  27. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
  28. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  29. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
  30. package/src/php/WordPress/package.php +6 -3
  31. package/src/php/WordPress/readme.txt +36 -0
  32. package/public/fingerprint-wordpress.zip +0 -0
  33. package/src/js/fingerprint.client.obfuscated.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,21 +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
+
1
14
  ## Version 0.7.2
2
15
 
3
16
  - 🔌 **WordPress Plugin Enhancements**:
4
- - `🌐 **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.`
5
- - `🐛 **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.`
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.
6
19
  - 🛡️ **Security & Bot Detection Hardening**:
7
- - `🔐 **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.`
8
- - `🤖 **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.`
9
- - `📡 **Updated Googlebot IPs**: The list of official Googlebot IP ranges has been updated to ensure the latest crawlers are correctly identified and allowed.`
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.
10
23
  - 🔧 **Developer Experience & Debugging**:
11
- - `📝 **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.`
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.
12
25
 
13
26
  ## Version 0.7.1
14
27
 
15
- - 🔌 **Native WordPress Plugin Integration (`fingerprint-wordpress`)**:
16
- - 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.
17
30
  - Automatic SQL cache table provisioning and periodic cleanup of expired tickets via WP-Cron (`fingerprint_prune_expired_entries`).
18
- - 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`).
19
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`).
20
33
  - Admin dashboard security notice if running over unencrypted HTTP (warning about modern browsers disabling native `crypto.subtle` APIs in insecure contexts).
21
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.2</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.2",
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 {
@@ -4603,6 +4638,9 @@ export class FingerprintEngine {
4603
4638
  (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
4604
4639
  (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
4605
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) +
4606
4644
  (suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
4607
4645
  (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
4608
4646
 
@@ -4802,7 +4840,7 @@ export class FingerprintEngine {
4802
4840
  const { whitelist = [] } = this.securityConfig;
4803
4841
  const botRules = whitelist.filter(rule => rule.hostnameSuffix);
4804
4842
  if (botRules.length === 0) {
4805
- return false;
4843
+ return null;
4806
4844
  }
4807
4845
 
4808
4846
  const { clientIp, headers } = requestContext;
@@ -4818,7 +4856,7 @@ export class FingerprintEngine {
4818
4856
  }
4819
4857
  });
4820
4858
  if (!matchedRule) {
4821
- return false;
4859
+ return null;
4822
4860
  }
4823
4861
 
4824
4862
  const botName = matchedRule.userAgent; // e.g., 'Googlebot'
@@ -4885,6 +4923,13 @@ export class FingerprintEngine {
4885
4923
  }
4886
4924
 
4887
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
+ }
4888
4933
  sanitizeProxyHeaders(requestContext, this.securityConfig);
4889
4934
 
4890
4935
  const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
@@ -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) {
@@ -1759,7 +1759,7 @@ describe('Fingerprint & PoW Security Suite', () => {
1759
1759
  };
1760
1760
 
1761
1761
  const isVerified = await engine._verifyWhitelistedBot({ ...requestContext, fingerprint: {} });
1762
- expect(isVerified).toBe(false);
1762
+ expect(isVerified).toBeNull();
1763
1763
  expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('[Fingerprint] Invalid regex in whitelist rule'));
1764
1764
  consoleErrorSpy.mockRestore();
1765
1765
  });
@@ -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
+ });
@@ -129,7 +129,8 @@ class AutoTuner
129
129
  try {
130
130
  call_user_func($this->onCleanup, $removed);
131
131
  } catch (\Throwable $e) {
132
- error_log("[AutoTuning] Error in onCleanup callback: " . $e->getMessage());
132
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
133
+ $this->logError("[AutoTuning] Error in onCleanup callback: " . $e->getMessage());
133
134
  }
134
135
  }
135
136
  }
@@ -155,24 +156,24 @@ class AutoTuner
155
156
 
156
157
  if (count($sanitizedData) < $this->minDataPoints || !$hasEnoughSignal) {
157
158
  if (count($sanitizedData) < $this->minDataPoints) {
158
- echo sprintf("[AutoTuning] Postponed: %d/%d data points collected.\n", count($sanitizedData), $this->minDataPoints);
159
+ $this->logMessage(sprintf("[AutoTuning] Postponed: %d/%d data points collected.\n", count($sanitizedData), $this->minDataPoints));
159
160
  } else {
160
- echo sprintf("[AutoTuning] Postponed: Insufficient confidence signals (Ratio: %.2f%% < %.2f%% and count: %d < %d).\n", $highConfidenceRatio * 100, $minConfidenceRatio * 100, $highConfidenceLogs, $minHighConfidenceCount);
161
+ $this->logMessage(sprintf("[AutoTuning] Postponed: Insufficient confidence signals (Ratio: %.2f%% < %.2f%% and count: %d < %d).\n", $highConfidenceRatio * 100, $minConfidenceRatio * 100, $highConfidenceLogs, $minHighConfidenceCount));
161
162
  }
162
163
  return;
163
164
  }
164
165
 
165
166
  if (count($this->trafficData) > $this->maxDataPoints) {
166
- echo sprintf("[AutoTuning] Traffic log reached %d entries (max: %d). Truncating oldest logs.\n", count($this->trafficData), $this->maxDataPoints);
167
+ $this->logMessage(sprintf("[AutoTuning] Traffic log reached %d entries (max: %d). Truncating oldest logs.\n", count($this->trafficData), $this->maxDataPoints));
167
168
  $this->trafficData = array_slice($this->trafficData, count($this->trafficData) - $this->maxDataPoints);
168
169
  }
169
170
 
170
- echo sprintf("[AutoTuning] Starting complete optimization cycle with %d sanitized data points.\n", count($sanitizedData));
171
+ $this->logMessage(sprintf("[AutoTuning] Starting complete optimization cycle with %d sanitized data points.\n", count($sanitizedData)));
171
172
 
172
173
  $paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData, 'currentConfig' => $this->securityConfig], []);
173
174
 
174
175
  if (empty($paretoFront)) {
175
- echo "[AutoTuning] Optimization returned no solutions.\n";
176
+ $this->logMessage("[AutoTuning] Optimization returned no solutions.\n");
176
177
  return;
177
178
  }
178
179
 
@@ -192,7 +193,7 @@ class AutoTuner
192
193
 
193
194
  $filteredFront = array_filter($paretoFront, fn ($p) => $isValidSecurityConfig($p['solution']));
194
195
  if (empty($filteredFront)) {
195
- echo "[AutoTuning] Warning: All Pareto solutions violated sanity guardrails. Restoring raw front.\n";
196
+ $this->logMessage("[AutoTuning] Warning: All Pareto solutions violated sanity guardrails. Restoring raw front.\n");
196
197
  $filteredFront = $paretoFront;
197
198
  } else {
198
199
  $filteredFront = array_values($filteredFront);
@@ -277,7 +278,8 @@ class AutoTuner
277
278
  $validationTolerance = $this->securityConfig['autotuning']['validationTolerance'] ?? $this->validationTolerance;
278
279
 
279
280
  if ($proposedFPR > $currentFPR + $validationTolerance || $proposedFNR > $currentFNR + $validationTolerance) {
280
- error_log(sprintf(
281
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system alert
282
+ $this->logError(sprintf(
281
283
  "[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: %.4f (Current: %.4f), Proposed FNR: %.4f (Current: %.4f)",
282
284
  $proposedFPR, $currentFPR, $proposedFNR, $currentFNR
283
285
  ));
@@ -300,22 +302,23 @@ class AutoTuner
300
302
 
301
303
  self::$lastBestSolution = $bestSolution;
302
304
 
303
- echo "[AutoTuning] New optimized security configuration applied.\n";
304
- echo "[AutoTuning] Objectives achieved: " . json_encode([
305
+ $this->logMessage("[AutoTuning] New optimized security configuration applied.\n");
306
+ $this->logMessage("[AutoTuning] Objectives achieved: " . json_encode([
305
307
  'falsePositiveRate' => round($bestSolution['objectives'][0], 4),
306
308
  'falseNegativeRate' => round($bestSolution['objectives'][1], 4)
307
- ]) . "\n";
308
- echo "[AutoTuning] New thresholds: " . json_encode($this->securityConfig['thresholds']) . "\n";
309
- echo "[AutoTuning] New weights: " . json_encode($this->securityConfig['weights']) . "\n";
310
- echo "[AutoTuning] New patterns: " . json_encode($this->securityConfig['patterns']) . "\n";
309
+ ]) . "\n");
310
+ $this->logMessage("[AutoTuning] New thresholds: " . json_encode($this->securityConfig['thresholds']) . "\n");
311
+ $this->logMessage("[AutoTuning] New weights: " . json_encode($this->securityConfig['weights']) . "\n");
312
+ $this->logMessage("[AutoTuning] New patterns: " . json_encode($this->securityConfig['patterns']) . "\n");
311
313
 
312
314
  // Persist best configuration if savePath is configured
313
315
  if ($this->savePath !== null) {
314
316
  try {
315
317
  file_put_contents($this->savePath, json_encode($bestSolution['solution'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
316
- echo "[AutoTuning] Best configuration saved to: {$this->savePath}\n";
318
+ $this->logMessage("[AutoTuning] Best configuration saved to: {$this->savePath}\n");
317
319
  } catch (\Throwable $e) {
318
- error_log("[AutoTuning] Error saving optimized configuration: " . $e->getMessage());
320
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
321
+ $this->logError("[AutoTuning] Error saving optimized configuration: " . $e->getMessage());
319
322
  }
320
323
  }
321
324
 
@@ -325,13 +328,28 @@ class AutoTuner
325
328
  try {
326
329
  call_user_func($this->onCleanup, $cleared);
327
330
  } catch (\Throwable $e) {
328
- error_log("[AutoTuning] Error in onCleanup callback after clearing: " . $e->getMessage());
331
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
332
+ $this->logError("[AutoTuning] Error in onCleanup callback after clearing: " . $e->getMessage());
329
333
  }
330
334
  }
331
- echo sprintf("[AutoTuning] Explicitly cleared %d processed traffic data points.\n", count($cleared));
335
+ $this->logMessage(sprintf("[AutoTuning] Explicitly cleared %d processed traffic data points.\n", count($cleared)));
332
336
  }
333
337
  }
334
338
 
339
+ private function logMessage(string $message): void
340
+ {
341
+ if (PHP_SAPI === 'cli') {
342
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI message output
343
+ echo $message;
344
+ }
345
+ }
346
+
347
+ private function logError(string $message): void
348
+ {
349
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
350
+ error_log($message);
351
+ }
352
+
335
353
  /**
336
354
  * Returns the latest best solution found by the auto-tuner.
337
355
  * @return array<string, mixed>|null