@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.
- package/CHANGELOG.md +22 -9
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/fingerprint.js +68 -23
- package/src/js/fingerprint.utils.js +32 -5
- package/src/js/tests/fingerprint.test.js +1 -1
- package/src/js/tests/threatIntel.test.js +76 -0
- package/src/php/AutoTuner.php +36 -18
- package/src/php/Challenge/ChallengeUtils.php +106 -224
- package/src/php/DirectFingerprint.php +16 -4
- package/src/php/FingerprintClient.php +17 -19
- package/src/php/FingerprintEngine.php +77 -39
- package/src/php/Ja3AnomalyDetector.php +20 -12
- package/src/php/Optimization/Optimization.php +5 -1
- package/src/php/Optimization/OptimizationOperators.php +5 -1
- package/src/php/Optimization/ProblemInitializers.php +16 -4
- package/src/php/ProblemManager.php +20 -14
- package/src/php/Tests/FingerprintEngineTest.php +6 -1
- package/src/php/Tests/ThreatIntelTest.php +64 -0
- package/src/php/Utils/Env.php +51 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/RequestUtils.php +22 -5
- package/src/php/WordPress/WpDbStore.php +4 -0
- package/src/php/WordPress/{fingerprint-wordpress.php → fingerprint-anti-bot.php} +131 -126
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
- package/src/php/WordPress/package.php +6 -3
- package/src/php/WordPress/readme.txt +36 -0
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/fingerprint.client.obfuscated.js +0 -1
|
@@ -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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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,7 +161,7 @@
|
|
|
175
161
|
$securityConfig = SecurityProfiles::deepMerge($securityConfig, $savedConfig);
|
|
176
162
|
}
|
|
177
163
|
} catch (\Throwable $e) {
|
|
178
|
-
|
|
164
|
+
self::logError("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
|
|
179
165
|
}
|
|
180
166
|
}
|
|
181
167
|
}
|
|
@@ -196,6 +182,49 @@
|
|
|
196
182
|
}
|
|
197
183
|
}
|
|
198
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
|
+
|
|
199
228
|
/**
|
|
200
229
|
* Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
|
|
201
230
|
* sans nécessiter de redémarrage.
|
|
@@ -266,7 +295,7 @@
|
|
|
266
295
|
}
|
|
267
296
|
}
|
|
268
297
|
} catch (\Throwable $e) {
|
|
269
|
-
|
|
298
|
+
self::logError("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
|
|
270
299
|
}
|
|
271
300
|
}
|
|
272
301
|
return $fallbackEntries;
|
|
@@ -358,7 +387,7 @@
|
|
|
358
387
|
} else {
|
|
359
388
|
$logMessage = "[FingerprintEngine] {$message}";
|
|
360
389
|
if (!empty($data)) $logMessage .= ' ' . json_encode($data);
|
|
361
|
-
|
|
390
|
+
self::logError($logMessage);
|
|
362
391
|
}
|
|
363
392
|
}
|
|
364
393
|
}
|
|
@@ -1554,7 +1583,7 @@
|
|
|
1554
1583
|
$isAsymmetric = true;
|
|
1555
1584
|
}
|
|
1556
1585
|
} catch (\Throwable $e) {
|
|
1557
|
-
|
|
1586
|
+
self::logError('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
|
|
1558
1587
|
}
|
|
1559
1588
|
}
|
|
1560
1589
|
|
|
@@ -1575,7 +1604,8 @@
|
|
|
1575
1604
|
|
|
1576
1605
|
private function asyncPost(string $url, array $params): void
|
|
1577
1606
|
{
|
|
1578
|
-
|
|
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);
|
|
1579
1609
|
if ($parts === false) return;
|
|
1580
1610
|
|
|
1581
1611
|
$host = $parts['host'];
|
|
@@ -1605,7 +1635,9 @@
|
|
|
1605
1635
|
$out .= "Connection: Close\r\n\r\n";
|
|
1606
1636
|
$out .= $postData;
|
|
1607
1637
|
|
|
1638
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Socket stream write
|
|
1608
1639
|
@fwrite($fp, $out);
|
|
1640
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Socket stream close
|
|
1609
1641
|
@fclose($fp);
|
|
1610
1642
|
}
|
|
1611
1643
|
}
|
|
@@ -1732,7 +1764,7 @@
|
|
|
1732
1764
|
$b3 = ord($str[$offset++]); $b4 = ord($str[$offset++]);
|
|
1733
1765
|
return ($b1 << 24) | ($b2 << 16) | ($b3 << 8) | $b4;
|
|
1734
1766
|
}
|
|
1735
|
-
throw new \Exception("Unsupported integer size: " . $val);
|
|
1767
|
+
throw new \Exception("Unsupported integer size: " . (int)$val);
|
|
1736
1768
|
};
|
|
1737
1769
|
|
|
1738
1770
|
if ($major === 0) {
|
|
@@ -1854,8 +1886,14 @@
|
|
|
1854
1886
|
return openssl_verify($verifyBuffer, base64_decode($anchor['signature']), $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
|
1855
1887
|
}
|
|
1856
1888
|
} catch (\Throwable $e) {
|
|
1857
|
-
|
|
1889
|
+
self::logError('[WebAuthn-Server] PHP verification failed: ' . $e->getMessage());
|
|
1858
1890
|
}
|
|
1859
1891
|
return false;
|
|
1860
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
|
+
}
|
|
1861
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
|
|
218
|
+
* @return float Suspicion score between 0.0 and 95.0
|
|
217
219
|
*/
|
|
218
|
-
public static function getRttProxyScore(RequestContext $context):
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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' =>
|
|
26
|
-
'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' =>
|
|
40
|
-
'volatility' => 0.1 +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -304,6 +304,9 @@ class FingerprintEngineTest extends TestCase
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
$configDir = dirname(__FILE__, 3) . '/config'; // Match engine's behavior
|
|
307
|
+
if (!is_dir($configDir)) {
|
|
308
|
+
mkdir($configDir, 0777, true);
|
|
309
|
+
}
|
|
307
310
|
$keyPath = $configDir . '/ed25519_key.json';
|
|
308
311
|
|
|
309
312
|
Env::clear('ED25519_PRIVATE_KEY');
|
|
@@ -328,7 +331,9 @@ class FingerprintEngineTest extends TestCase
|
|
|
328
331
|
$this->assertEquals(Env::get('ED25519_PRIVATE_KEY'), $stored['privateKey']);
|
|
329
332
|
$this->assertEquals(Env::get('ED25519_PUBLIC_KEY'), $stored['publicKey']);
|
|
330
333
|
|
|
331
|
-
|
|
334
|
+
if (file_exists($keyPath)) {
|
|
335
|
+
unlink($keyPath);
|
|
336
|
+
}
|
|
332
337
|
}
|
|
333
338
|
|
|
334
339
|
public function testEd25519KeyLoadingFromDisk(): void
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\Ja3AnomalyDetector;
|
|
8
|
+
use Anonympins\Fingerprint\RequestContext;
|
|
9
|
+
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
10
|
+
use Anonympins\Fingerprint\Store\StoreManager;
|
|
11
|
+
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
12
|
+
use PHPUnit\Framework\TestCase;
|
|
13
|
+
|
|
14
|
+
class ThreatIntelTest extends TestCase
|
|
15
|
+
{
|
|
16
|
+
protected function setUp(): void
|
|
17
|
+
{
|
|
18
|
+
parent::setUp();
|
|
19
|
+
StoreManager::configureStore(new InMemoryStore());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public function testRttProxyScoreReturnsZeroForCoherentTurnaround(): void
|
|
23
|
+
{
|
|
24
|
+
$reqTime = 1700000000000;
|
|
25
|
+
$context = new RequestContext(
|
|
26
|
+
'127.0.0.1', '/',
|
|
27
|
+
[
|
|
28
|
+
'x-tcp-rtt' => '15',
|
|
29
|
+
'x-behavior-metrics' => json_encode(['clientTimestamp' => $reqTime - 30])
|
|
30
|
+
],
|
|
31
|
+
[], null, [], '1.1', $reqTime
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
$score = Ja3AnomalyDetector::getRttProxyScore($context);
|
|
35
|
+
$this->assertEquals(0.0, $score);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public function testRttProxyScoreIgnoresSlowConnectionWithoutTcpRtt(): void
|
|
39
|
+
{
|
|
40
|
+
$reqTime = 1700000000000;
|
|
41
|
+
$context = new RequestContext(
|
|
42
|
+
'127.0.0.1', '/',
|
|
43
|
+
['x-behavior-metrics' => json_encode(['clientTimestamp' => $reqTime - 450])],
|
|
44
|
+
[], null, [], '1.1', $reqTime
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
$score = Ja3AnomalyDetector::getRttProxyScore($context);
|
|
48
|
+
$this->assertEquals(0.0, $score, 'A slow connection without TCP RTT context should not trigger a false positive.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public function testRttProxyScoreContinuouslyPenalizesResidentialTunneling(): void
|
|
52
|
+
{
|
|
53
|
+
$reqTime = 1700000000000;
|
|
54
|
+
$context = new RequestContext(
|
|
55
|
+
'127.0.0.1', '/',
|
|
56
|
+
['x-tcp-rtt' => '10', 'x-behavior-metrics' => json_encode(['clientTimestamp' => $reqTime - 300])],
|
|
57
|
+
[], null, [], '1.1', $reqTime
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
$score = Ja3AnomalyDetector::getRttProxyScore($context);
|
|
61
|
+
$this->assertGreaterThan(75.0, $score);
|
|
62
|
+
$this->assertLessThanOrEqual(95.0, $score);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/php/Utils/Env.php
CHANGED
|
@@ -1,50 +1,52 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
-
|
|
7
|
-
class Env
|
|
8
|
-
{
|
|
9
|
-
/** @var array<string, mixed> */
|
|
10
|
-
private static array $localCache = [];
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param string $key
|
|
14
|
-
* @param mixed $default
|
|
15
|
-
* @return mixed
|
|
16
|
-
*/
|
|
17
|
-
public static function get(string $key, $default = null)
|
|
18
|
-
{
|
|
19
|
-
if (array_key_exists($key, self::$localCache)) {
|
|
20
|
-
return self::$localCache[$key];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
unset(
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
+
|
|
7
|
+
class Env
|
|
8
|
+
{
|
|
9
|
+
/** @var array<string, mixed> */
|
|
10
|
+
private static array $localCache = [];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param string $key
|
|
14
|
+
* @param mixed $default
|
|
15
|
+
* @return mixed
|
|
16
|
+
*/
|
|
17
|
+
public static function get(string $key, $default = null)
|
|
18
|
+
{
|
|
19
|
+
if (array_key_exists($key, self::$localCache)) {
|
|
20
|
+
return self::$localCache[$key];
|
|
21
|
+
}
|
|
22
|
+
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Environment and server variables may contain raw cryptographic keys.
|
|
23
|
+
if (isset($_ENV[$key]) && $_ENV[$key] !== '') {
|
|
24
|
+
return $_ENV[$key];
|
|
25
|
+
}
|
|
26
|
+
if (isset($_SERVER[$key]) && $_SERVER[$key] !== '') {
|
|
27
|
+
return $_SERVER[$key];
|
|
28
|
+
}
|
|
29
|
+
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
|
30
|
+
$val = getenv($key);
|
|
31
|
+
if ($val !== false && $val !== '') {
|
|
32
|
+
return $val;
|
|
33
|
+
}
|
|
34
|
+
return $default;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public static function set(string $key, $value): void
|
|
38
|
+
{
|
|
39
|
+
self::$localCache[$key] = $value;
|
|
40
|
+
$_ENV[$key] = $value;
|
|
41
|
+
$_SERVER[$key] = $value;
|
|
42
|
+
@putenv("{$key}={$value}");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static function clear(string $key): void
|
|
46
|
+
{
|
|
47
|
+
unset(self::$localCache[$key]);
|
|
48
|
+
unset($_ENV[$key]);
|
|
49
|
+
unset($_SERVER[$key]);
|
|
50
|
+
@putenv($key);
|
|
51
|
+
}
|
|
50
52
|
}
|