@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
@@ -34,17 +34,25 @@ class DirectFingerprint
34
34
  public function protect(): ?array
35
35
  {
36
36
  // 1. Build request context from PHP superglobals
37
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Direct WAF inspection firewall before WP core logic
37
38
  $body = $_POST ?: json_decode(file_get_contents('php://input'), true);
38
39
  $headers = function_exists('getallheaders') ? getallheaders() : [];
39
40
 
41
+ $remoteAddr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '127.0.0.1';
42
+ $rawUri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '/';
43
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Uses wp_parse_url when available
44
+ $path = function_exists('wp_parse_url') ? (string)wp_parse_url($rawUri, PHP_URL_PATH) : (string)parse_url($rawUri, PHP_URL_PATH);
45
+ $serverProtocol = isset($_SERVER['SERVER_PROTOCOL']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_PROTOCOL'])) : '1.1';
46
+
40
47
  $context = new RequestContext(
41
- $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
42
- parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/',
48
+ $remoteAddr,
49
+ !empty($path) ? $path : '/',
43
50
  $headers,
51
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
44
52
  $_GET,
45
53
  $body,
46
54
  $_COOKIE,
47
- $_SERVER['SERVER_PROTOCOL'] ?? '1.1'
55
+ $serverProtocol
48
56
  );
49
57
 
50
58
  // 2. Process request with engine
@@ -62,9 +70,11 @@ class DirectFingerprint
62
70
  http_response_code($decision['status'] ?? 403);
63
71
  if (is_array($decision['body'])) {
64
72
  header('Content-Type: application/json');
73
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON API challenge payload
65
74
  echo json_encode($decision['body']);
66
75
  } else {
67
76
  header('Content-Type: text/html; charset=utf-8');
77
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Proof-of-Work challenge HTML page
68
78
  echo $decision['body'];
69
79
  }
70
80
  exit();
@@ -152,7 +162,8 @@ class DirectFingerprint
152
162
  switch ($decision['action']) {
153
163
  case 'block':
154
164
  http_response_code($decision['status'] ?? 403);
155
- echo $decision['body'] ?? "Access denied.";
165
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
166
+ echo esc_html($decision['body'] ?? "Access denied.");
156
167
  exit();
157
168
  case 'redirect':
158
169
  header('Location: ' . $decision['path'], true, $decision['status'] ?? 302);
@@ -176,6 +187,7 @@ class DirectFingerprint
176
187
 
177
188
  // 3. If authorized, stream metrics output
178
189
  header('Content-Type: text/plain; version=0.0.4; charset=utf-8');
190
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Prometheus exposition format output
179
191
  echo MetricsManager::getPrometheusMetrics();
180
192
  exit();
181
193
  }
@@ -87,7 +87,7 @@ class FingerprintClient
87
87
  $containerTags = ['div', 'span', 'p', 'section'];
88
88
  $tag = $containerTags[array_rand($containerTags)];
89
89
 
90
- $nestingType = rand(0, 1);
90
+ $nestingType = function_exists('wp_rand') ? wp_rand(0, 1) : random_int(0, 1);
91
91
  if ($nestingType === 1) {
92
92
  return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
93
93
  . '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName)
@@ -113,26 +113,24 @@ class FingerprintClient
113
113
  $nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
114
114
 
115
115
  // Inline initialization script embedded in HTML
116
- $initScript = <<<JS
117
- document.addEventListener('DOMContentLoaded', function() {
118
- const config = {$configJson};
119
- if (window.ClientLibrary) {
120
- if (config.wasmPath) {
121
- const wasmScript = document.createElement('script');
122
- wasmScript.src = config.wasmPath;
123
- wasmScript.async = true;
124
- wasmScript.nonce = '{$this->nonce}';
125
- document.head.appendChild(wasmScript);
126
- }
127
-
128
- window.ClientLibrary.initializeClient(config);
129
- } else {
130
- console.error('Fingerprint client library not loaded.');
131
- }
132
- });
133
- JS;
116
+ $initScript = "document.addEventListener('DOMContentLoaded', function() {\n"
117
+ . " const config = " . $configJson . ";\n"
118
+ . " if (window.ClientLibrary) {\n"
119
+ . " if (config.wasmPath) {\n"
120
+ . " const wasmScript = document.createElement('script');\n"
121
+ . " wasmScript.src = config.wasmPath;\n"
122
+ . " wasmScript.async = true;\n"
123
+ . " wasmScript.nonce = '" . addslashes((string)$this->nonce) . "';\n"
124
+ . " document.head.appendChild(wasmScript);\n"
125
+ . " }\n"
126
+ . " window.ClientLibrary.initializeClient(config);\n"
127
+ . " } else {\n"
128
+ . " console.error('Fingerprint client library not loaded.');\n"
129
+ . " }\n"
130
+ . "});";
134
131
 
135
132
  // Combine library loader script and inline initialization
133
+ // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- Polymorphic dynamic client script tag generation
136
134
  return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
137
135
  . '<script' . $nonceAttr . '>' . $initScript . '</script>';
138
136
  }
@@ -127,41 +127,27 @@
127
127
  $useAsymmetric = ($securityConfig['useAsymmetricTickets'] ?? false) === true || ($securityConfig['ed25519'] ?? '') === 'auto';
128
128
  $envPrivate = Env::get('ED25519_PRIVATE_KEY');
129
129
  if ($useAsymmetric && ($envPrivate === null || $envPrivate === '')) {
130
- $configDir = dirname(__DIR__, 1) . '/config';
131
- $persistentKeyPath = $configDir . '/ed25519_key.json';
132
-
133
- if (file_exists($persistentKeyPath)) {
134
- try {
135
- $keys = json_decode(file_get_contents($persistentKeyPath), true);
136
- if (isset($keys['privateKey'], $keys['publicKey'])) {
137
- Env::set('ED25519_PRIVATE_KEY', $keys['privateKey']);
138
- Env::set('ED25519_PUBLIC_KEY', $keys['publicKey']);
130
+ if (function_exists('get_option') && function_exists('update_option')) {
131
+ // In WordPress, persist cryptographic keys in the database options table
132
+ $storedKeys = get_option('fingerprint_ed25519_keys');
133
+ if (is_array($storedKeys) && isset($storedKeys['privateKey'], $storedKeys['publicKey'])) {
134
+ Env::set('ED25519_PRIVATE_KEY', $storedKeys['privateKey']);
135
+ Env::set('ED25519_PUBLIC_KEY', $storedKeys['publicKey']);
136
+ } elseif (defined('OPENSSL_KEYTYPE_ED25519')) {
137
+ $pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
138
+ if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
139
+ $details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
140
+ $publicKeyPem = $details['key'] ?? ''; // @phpstan-ignore-line
141
+ Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
142
+ Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
143
+ update_option('fingerprint_ed25519_keys', [
144
+ 'privateKey' => $privateKeyPem,
145
+ 'publicKey' => $publicKeyPem,
146
+ ], false);
139
147
  }
140
- } catch (\Throwable $e) {
141
- error_log('[Fingerprint] Failed to load persistent Ed25519 keys: ' . $e->getMessage());
142
148
  }
143
149
  } else {
144
- try {
145
- if (defined('OPENSSL_KEYTYPE_ED25519')) {
146
- $pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
147
- if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
148
- $details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
149
- $publicKeyPem = $details['key']; // @phpstan-ignore-line
150
- Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
151
- Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
152
- // Also update $_ENV and $_SERVER for consistency
153
- if (!is_dir($configDir)) {
154
- mkdir($configDir, 0777, true);
155
- }
156
- file_put_contents($persistentKeyPath, json_encode([
157
- 'privateKey' => $privateKeyPem,
158
- 'publicKey' => $publicKeyPem
159
- ], JSON_PRETTY_PRINT));
160
- }
161
- }
162
- } catch (\Throwable $e) {
163
- error_log('[Fingerprint] Native Ed25519 key generation failed: ' . $e->getMessage());
164
- }
150
+ self::initStandaloneEd25519Keys();
165
151
  }
166
152
  }
167
153
 
@@ -175,10 +161,16 @@
175
161
  $securityConfig = SecurityProfiles::deepMerge($securityConfig, $savedConfig);
176
162
  }
177
163
  } catch (\Throwable $e) {
178
- error_log("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
164
+ self::logError("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
179
165
  }
180
166
  }
181
167
  }
168
+
169
+ // Si aucune liste blanche n'est explicitement fournie, appliquer la liste par défaut.
170
+ if (!isset($securityConfig['whitelist'])) {
171
+ $securityConfig['whitelist'] = self::default_whitelist();
172
+ }
173
+
182
174
  $this->securityConfig = $securityConfig;
183
175
  $this->verbose = $securityConfig['verbose'] ?? false;
184
176
  $this->allowlist = $this->buildAllowlist();
@@ -190,6 +182,49 @@
190
182
  }
191
183
  }
192
184
 
185
+ /**
186
+ * Initialise et persiste les cles Ed25519 pour les environnements PHP hors WordPress.
187
+ */
188
+ private static function initStandaloneEd25519Keys(): void
189
+ {
190
+ $configDir = dirname(__DIR__, 1) . '/config';
191
+ $persistentKeyPath = $configDir . '/ed25519_key.json';
192
+
193
+ if (file_exists($persistentKeyPath)) {
194
+ try {
195
+ $keys = json_decode((string)file_get_contents($persistentKeyPath), true);
196
+ if (isset($keys['privateKey'], $keys['publicKey'])) {
197
+ Env::set('ED25519_PRIVATE_KEY', $keys['privateKey']);
198
+ Env::set('ED25519_PUBLIC_KEY', $keys['publicKey']);
199
+ }
200
+ } catch (\Throwable $e) {
201
+ self::logError('[Fingerprint] Failed to load persistent Ed25519 keys: ' . $e->getMessage());
202
+ }
203
+ } else {
204
+ try {
205
+ if (defined('OPENSSL_KEYTYPE_ED25519')) {
206
+ $pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
207
+ if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
208
+ $details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
209
+ $publicKeyPem = $details['key'] ?? ''; // @phpstan-ignore-line
210
+ Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
211
+ Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
212
+ if (!is_dir($configDir)) {
213
+ mkdir($configDir, 0777, true);
214
+ }
215
+ // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite -- Non-WordPress standalone environment fallback
216
+ file_put_contents($persistentKeyPath, json_encode([
217
+ 'privateKey' => $privateKeyPem,
218
+ 'publicKey' => $publicKeyPem
219
+ ], JSON_PRETTY_PRINT));
220
+ }
221
+ }
222
+ } catch (\Throwable $e) {
223
+ self::logError('[Fingerprint] Native Ed25519 key generation failed: ' . $e->getMessage());
224
+ }
225
+ }
226
+ }
227
+
193
228
  /**
194
229
  * Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
195
230
  * sans nécessiter de redémarrage.
@@ -260,7 +295,7 @@
260
295
  }
261
296
  }
262
297
  } catch (\Throwable $e) {
263
- error_log("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
298
+ self::logError("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
264
299
  }
265
300
  }
266
301
  return $fallbackEntries;
@@ -352,7 +387,7 @@
352
387
  } else {
353
388
  $logMessage = "[FingerprintEngine] {$message}";
354
389
  if (!empty($data)) $logMessage .= ' ' . json_encode($data);
355
- error_log($logMessage);
390
+ self::logError($logMessage);
356
391
  }
357
392
  }
358
393
  }
@@ -551,10 +586,8 @@
551
586
 
552
587
  $deviceId = bin2hex(random_bytes(16)); // UUID-like
553
588
 
554
- $isHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
555
- (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
556
- ($context->getHeader('x-forwarded-proto') === 'https');
557
- $secureOption = $isHttps || $this->isProduction;
589
+ $isHttps = !empty($context->isHttps);
590
+ $secureOption = $isHttps;
558
591
 
559
592
  // Préparer le cookie à envoyer
560
593
  $newCookie = [
@@ -985,13 +1018,36 @@
985
1018
  // Le fingerprint est cohérent, on peut valider la solution
986
1019
  if ($powType === 'cpu_target' || $powType === 'cpu_mem') {
987
1020
  $cpuSolution = $context->query['pow_solution_cpu'] ?? $context->query['pow_solution'] ?? null;
988
- if ($cpuSolution) {
989
- $ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket($context->clientIp, 3600000, $powNonce, $cpuSolution, $challengeContext);
1021
+ if ($cpuSolution !== null && $cpuSolution !== '') {
1022
+ $identity = $this->resolveRequestIdentity($context, $suspicionVector);
1023
+ $deviceId = (string)($identity['deviceId'] ?? '');
1024
+ $currentDeviceHash = (string)($identity['currentDeviceHash'] ?? RequestUtils::getCompositeDeviceHash($context));
1025
+
1026
+ $ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket(
1027
+ $context->clientIp,
1028
+ 3600000,
1029
+ $powNonce,
1030
+ (string)$cpuSolution,
1031
+ $challengeContext,
1032
+ $deviceId,
1033
+ $currentDeviceHash
1034
+ );
990
1035
  $isValid = $ticket !== null;
991
1036
 
992
1037
  if ($powType === 'cpu_mem') {
993
- $memSolution = $context->query['pow_solution_mem'] ?? null;
994
- $isMemValid = $memSolution ? ChallengeUtils::verifyMemoryPoW($powNonce, $memSolution, $challengeContext['memDifficulty'] ?? 0, $challengeContext['clientSecret'] ?? '') : false;
1038
+ if (!empty($challengeContext['isHttp'])) {
1039
+ $isMemValid = true;
1040
+ } else {
1041
+ $memSolution = $context->query['pow_solution_mem'] ?? null;
1042
+ $isMemValid = ($memSolution !== null && $memSolution !== '')
1043
+ ? ChallengeUtils::verifyMemoryPoW(
1044
+ $powNonce,
1045
+ (string)$memSolution,
1046
+ $challengeContext['memDifficulty'] ?? 0,
1047
+ $challengeContext['clientSecret'] ?? ''
1048
+ )
1049
+ : false;
1050
+ }
995
1051
  $isValid = $isValid && $isMemValid;
996
1052
  }
997
1053
  }
@@ -1073,10 +1129,8 @@
1073
1129
  MetricsManager::incrementCounter('challenges_solved_total');
1074
1130
  $this->log('Challenge solution valid - issuing ticket', ['ticketMaxAge' => $ticketTtl]);
1075
1131
 
1076
- $isHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
1077
- (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
1078
- ($context->getHeader('x-forwarded-proto') === 'https');
1079
- $secureOption = $isHttps || $this->isProduction;
1132
+ $isHttps = !empty($context->isHttps);
1133
+ $secureOption = $isHttps;
1080
1134
 
1081
1135
  return [
1082
1136
  'action' => 'redirect',
@@ -1529,7 +1583,7 @@
1529
1583
  $isAsymmetric = true;
1530
1584
  }
1531
1585
  } catch (\Throwable $e) {
1532
- error_log('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
1586
+ self::logError('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
1533
1587
  }
1534
1588
  }
1535
1589
 
@@ -1550,7 +1604,8 @@
1550
1604
 
1551
1605
  private function asyncPost(string $url, array $params): void
1552
1606
  {
1553
- $parts = parse_url($url);
1607
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url used if available
1608
+ $parts = function_exists('wp_parse_url') ? wp_parse_url($url) : parse_url($url);
1554
1609
  if ($parts === false) return;
1555
1610
 
1556
1611
  $host = $parts['host'];
@@ -1580,7 +1635,9 @@
1580
1635
  $out .= "Connection: Close\r\n\r\n";
1581
1636
  $out .= $postData;
1582
1637
 
1638
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Socket stream write
1583
1639
  @fwrite($fp, $out);
1640
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Socket stream close
1584
1641
  @fclose($fp);
1585
1642
  }
1586
1643
  }
@@ -1707,7 +1764,7 @@
1707
1764
  $b3 = ord($str[$offset++]); $b4 = ord($str[$offset++]);
1708
1765
  return ($b1 << 24) | ($b2 << 16) | ($b3 << 8) | $b4;
1709
1766
  }
1710
- throw new \Exception("Unsupported integer size: " . $val);
1767
+ throw new \Exception("Unsupported integer size: " . (int)$val);
1711
1768
  };
1712
1769
 
1713
1770
  if ($major === 0) {
@@ -1829,8 +1886,14 @@
1829
1886
  return openssl_verify($verifyBuffer, base64_decode($anchor['signature']), $publicKey, OPENSSL_ALGO_SHA256) === 1;
1830
1887
  }
1831
1888
  } catch (\Throwable $e) {
1832
- error_log('[WebAuthn-Server] PHP verification failed: ' . $e->getMessage());
1889
+ self::logError('[WebAuthn-Server] PHP verification failed: ' . $e->getMessage());
1833
1890
  }
1834
1891
  return false;
1835
1892
  }
1893
+
1894
+ private static function logError(string $message): void
1895
+ {
1896
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Engine diagnostic error log
1897
+ error_log($message);
1898
+ }
1836
1899
  }
@@ -212,10 +212,12 @@ class Ja3AnomalyDetector
212
212
 
213
213
  /**
214
214
  * Correlates low-level TCP RTT with application layer latency to detect residential proxy hops.
215
+ * Evaluates continuous physical transit discrepancies rather than arbitrary step numbers.
216
+ *
215
217
  * @param RequestContext $context
216
- * @return int Suspicion score
218
+ * @return float Suspicion score between 0.0 and 95.0
217
219
  */
218
- public static function getRttProxyScore(RequestContext $context): int
220
+ public static function getRttProxyScore(RequestContext $context): float
219
221
  {
220
222
  $tcpRttHeader = $context->headers['x-tcp-rtt'] ?? $context->headers['x-real-rtt'] ?? null;
221
223
  $tcpRtt = $tcpRttHeader !== null ? (int)$tcpRttHeader : null;
@@ -223,22 +225,28 @@ class Ja3AnomalyDetector
223
225
  $behaviorHeader = $context->headers['x-behavior-metrics'] ?? null;
224
226
  if ($behaviorHeader) {
225
227
  $metrics = json_decode($behaviorHeader, true);
226
- if (json_last_error() === JSON_ERROR_NONE && isset($metrics['clientTimestamp'])) {
228
+ if (json_last_error() === JSON_ERROR_NONE && isset($metrics['clientTimestamp']) && is_numeric($metrics['clientTimestamp'])) {
227
229
  $clientTimestamp = (int)$metrics['clientTimestamp'];
228
230
  $appLatency = $context->requestTimestamp - $clientTimestamp;
229
- if ($tcpRtt !== null && $tcpRtt > 0) {
230
- $clientToProxyDelta = $appLatency - $tcpRtt;
231
- if ($tcpRtt < 35 && $clientToProxyDelta > 150) {
232
- return 85;
233
- }
234
- } else {
235
- if ($appLatency > 350) {
236
- return 40;
231
+
232
+ if ($tcpRtt !== null && $tcpRtt > 0 && $appLatency > 0) {
233
+ // Marge de tolérance défensive contre le jitter réseau et le scheduling JS / Garbage Collection
234
+ $jitterAllowance = 60.0;
235
+ $effectiveRtt = max((float)$tcpRtt, 5.0);
236
+ $tunnelDelta = $appLatency - ($tcpRtt + $jitterAllowance);
237
+ $divergenceRatio = $appLatency / $effectiveRtt;
238
+
239
+ // Condition de disjonction : liaison edge ultra-proche (< 40ms) + transit applicatif au moins 3x supérieur
240
+ if ($tcpRtt <= 40 && $divergenceRatio >= 3.0 && $tunnelDelta > 0) {
241
+ $scaling = 120.0;
242
+ $ratioWeight = min(1.0, ($divergenceRatio - 3.0) / 5.0);
243
+ $proxyScore = min(95.0, 40.0 + 55.0 * tanh($tunnelDelta / $scaling) * $ratioWeight);
244
+ return round($proxyScore, 1);
237
245
  }
238
246
  }
239
247
  }
240
248
  }
241
- return 0;
249
+ return 0.0;
242
250
  }
243
251
  }
244
252
 
@@ -17,7 +17,11 @@ class Optimization
17
17
  try {
18
18
  return random_int(0, PHP_INT_MAX - 1) / PHP_INT_MAX;
19
19
  } catch (\Exception $e) {
20
- return (float)mt_rand() / (float)mt_getrandmax(); // Fallback
20
+ if (function_exists('wp_rand')) {
21
+ return (float)wp_rand(0, 1000000) / 1000000.0;
22
+ }
23
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- Standalone fallback
24
+ return (float)mt_rand() / (float)mt_getrandmax();
21
25
  }
22
26
  }
23
27
 
@@ -44,7 +44,11 @@ class OptimizationOperators
44
44
  try {
45
45
  return random_int(0, PHP_INT_MAX - 1) / PHP_INT_MAX;
46
46
  } catch (\Exception $e) {
47
- return (float)mt_rand() / (float)mt_getrandmax(); // Fallback
47
+ if (function_exists('wp_rand')) {
48
+ return (float)wp_rand(0, 1000000) / 1000000.0;
49
+ }
50
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- Standalone fallback
51
+ return (float)mt_rand() / (float)mt_getrandmax();
48
52
  }
49
53
  }
50
54
 
@@ -12,6 +12,18 @@ class ProblemInitializers
12
12
  /** @var array<string, callable> */
13
13
  private static array $initializers = [];
14
14
 
15
+ private static function randomFloat(): float
16
+ {
17
+ try {
18
+ return (float)random_int(0, PHP_INT_MAX - 1) / (float)PHP_INT_MAX;
19
+ } catch (\Throwable $e) {
20
+ if (function_exists('wp_rand')) {
21
+ return (float)wp_rand(0, 1000000) / 1000000.0;
22
+ }
23
+ return 0.5;
24
+ }
25
+ }
26
+
15
27
  private static function initialize(): void
16
28
  {
17
29
  if (empty(self::$initializers)) {
@@ -22,8 +34,8 @@ class ProblemInitializers
22
34
  $points = [];
23
35
  for ($i = 0; $i < $count; $i++) {
24
36
  $points[] = [
25
- 'x' => mt_rand() / mt_getrandmax() * $bounds['x'],
26
- 'y' => mt_rand() / mt_getrandmax() * $bounds['y']
37
+ 'x' => self::randomFloat() * $bounds['x'],
38
+ 'y' => self::randomFloat() * $bounds['y']
27
39
  ];
28
40
  }
29
41
  return $points;
@@ -36,8 +48,8 @@ class ProblemInitializers
36
48
  for ($i = 0; $i < $count; $i++) {
37
49
  $assets[] = [
38
50
  'name' => 'Asset ' . ($i + 1),
39
- 'expectedReturn' => mt_rand() / mt_getrandmax() * 0.2,
40
- 'volatility' => 0.1 + mt_rand() / mt_getrandmax() * 0.3
51
+ 'expectedReturn' => self::randomFloat() * 0.2,
52
+ 'volatility' => 0.1 + self::randomFloat() * 0.3
41
53
  ];
42
54
  }
43
55
  return $assets;
@@ -59,17 +59,17 @@ class ProblemManager
59
59
  private function loadProblems(): void
60
60
  {
61
61
  if (!file_exists($this->configPath)) {
62
- error_log("[ProblemManager] Problem config file not found: {$this->configPath}");
62
+ self::logError("[ProblemManager] Problem config file not found: {$this->configPath}");
63
63
  return;
64
64
  }
65
65
  $data = file_get_contents($this->configPath);
66
66
  if ($data === false) {
67
- error_log("[ProblemManager] Failed to read problem config file: {$this->configPath}");
67
+ self::logError("[ProblemManager] Failed to read problem config file: {$this->configPath}");
68
68
  return;
69
69
  }
70
70
  $problemsFromFile = json_decode($data, true);
71
71
  if (json_last_error() !== JSON_ERROR_NONE) {
72
- error_log("[ProblemManager] Failed to parse problem config JSON: " . json_last_error_msg());
72
+ self::logError("[ProblemManager] Failed to parse problem config JSON: " . json_last_error_msg());
73
73
  return;
74
74
  }
75
75
 
@@ -88,7 +88,7 @@ class ProblemManager
88
88
  $problem['workUnit']['scoreFunction'] = FunctionRegistry::get($problem['workUnit']['scoreFunction']);
89
89
  if ($problem['workUnit']['scoreFunction'] === null) {
90
90
  // @codeCoverageIgnoreStart
91
- error_log("[ProblemManager] Warning: scoreFunction '{$problem['workUnit']['scoreFunction']}' not found in registry for problem '{$problem['id']}'.");
91
+ self::logError("[ProblemManager] Warning: scoreFunction '{$problem['workUnit']['scoreFunction']}' not found in registry for problem '{$problem['id']}'.");
92
92
  // @codeCoverageIgnoreEnd
93
93
  }
94
94
  }
@@ -167,7 +167,7 @@ class ProblemManager
167
167
  $task['solverName'] = $problem['workUnit']['solverName'];
168
168
  break;
169
169
  default:
170
- error_log("[ProblemManager] Unknown useful work type: {$problem['workUnit']['type']}");
170
+ self::logError("[ProblemManager] Unknown useful work type: {$problem['workUnit']['type']}");
171
171
  return null;
172
172
  }
173
173
 
@@ -198,17 +198,17 @@ class ProblemManager
198
198
  if (isset($solutionData['solution']) && isset($solutionData['energy'])) {
199
199
  $scoreFunction = $problem['workUnit']['scoreFunction'] ?? null;
200
200
  if (!$scoreFunction) {
201
- error_log("[ProblemManager] No score function defined for {$problemId}.");
201
+ self::logError("[ProblemManager] No score function defined for {$problemId}.");
202
202
  return;
203
203
  }
204
204
  // DoS mitigation: validate solution size and structure
205
205
  if (is_array($solutionData['solution']) && count($solutionData['solution']) > 500) {
206
- error_log("[ProblemManager] Solution array too large for {$problemId} verification.");
206
+ self::logError("[ProblemManager] Solution array too large for {$problemId} verification.");
207
207
  return;
208
208
  }
209
209
  $serializedSolution = json_encode($solutionData['solution']);
210
210
  if ($serializedSolution !== false && strlen($serializedSolution) > 65536) {
211
- error_log("[ProblemManager] Solution payload size exceeds safe limit for {$problemId}.");
211
+ self::logError("[ProblemManager] Solution payload size exceeds safe limit for {$problemId}.");
212
212
  return;
213
213
  }
214
214
  // 1. Never trust client-reported score. Recalculate server-side.
@@ -222,19 +222,19 @@ class ProblemManager
222
222
  $problem['state']['bestEnergy'] = $recalculatedEnergy; // 3. Store verified score
223
223
  $problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
224
224
  $stateChanged = true;
225
- error_log("[ProblemManager] New best solution for {$problemId}: {$recalculatedEnergy}"); // @phpstan-ignore-line
225
+ self::logError("[ProblemManager] New best solution for {$problemId}: {$recalculatedEnergy}"); // @phpstan-ignore-line
226
226
  }
227
227
  }
228
228
  break;
229
229
  case 'genetic_algorithm_generations':
230
230
  if (isset($solutionData['population']) && is_array($solutionData['population'])) {
231
231
  if (count($solutionData['population']) > 150) {
232
- error_log("[ProblemManager] Population size exceeds safe limit for {$problemId}.");
232
+ self::logError("[ProblemManager] Population size exceeds safe limit for {$problemId}.");
233
233
  return;
234
234
  }
235
235
  foreach ($solutionData['population'] as $ind) {
236
236
  if (isset($ind['chromosome']) && is_array($ind['chromosome']) && count($ind['chromosome']) > 100) {
237
- error_log("[ProblemManager] Chromosome size too large for {$problemId}.");
237
+ self::logError("[ProblemManager] Chromosome size too large for {$problemId}.");
238
238
  return;
239
239
  }
240
240
  }
@@ -251,7 +251,7 @@ class ProblemManager
251
251
  $recalculated = $fitnessFunction($individual['chromosome'], $problem['payload'] ?? []);
252
252
  if (isset($individual['fitness']) && $individual['fitness'] !== -1) {
253
253
  if (abs($individual['fitness'] - $recalculated) > 1e-4) {
254
- error_log("[ProblemManager] Cheating detected for {$problemId}! Reported: {$individual['fitness']}, recalculated: {$recalculated}");
254
+ self::logError("[ProblemManager] Cheating detected for {$problemId}! Reported: {$individual['fitness']}, recalculated: {$recalculated}");
255
255
  return; // Reject inconsistent population
256
256
  }
257
257
  }
@@ -272,7 +272,7 @@ class ProblemManager
272
272
  }
273
273
  break;
274
274
  default:
275
- error_log("[ProblemManager] Integration not implemented for useful work type: {$problem['workUnit']['type']}");
275
+ self::logError("[ProblemManager] Integration not implemented for useful work type: {$problem['workUnit']['type']}");
276
276
  break;
277
277
  }
278
278
  // Persist updated state to datastore
@@ -294,7 +294,7 @@ class ProblemManager
294
294
  if (!empty($newFront) && json_encode($newFront) !== json_encode($currentFront)) {
295
295
  $problem['state']['paretoFront'] = $newFront;
296
296
  $problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
297
- error_log("[ProblemManager] New Pareto front for {$problem['id']} with " . count($newFront) . " solutions."); // @phpstan-ignore-line
297
+ self::logError("[ProblemManager] New Pareto front for {$problem['id']} with " . count($newFront) . " solutions."); // @phpstan-ignore-line
298
298
  return true;
299
299
  }
300
300
  return false;
@@ -407,4 +407,10 @@ class ProblemManager
407
407
  {
408
408
  return $this->problems;
409
409
  }
410
+
411
+ private static function logError(string $message): void
412
+ {
413
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- ProblemManager logging
414
+ error_log($message);
415
+ }
410
416
  }