@anonympins/fingerprint 0.7.0 → 0.7.2

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 (66) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/composer.json +8 -1
  4. package/package.json +1 -1
  5. package/public/fingerprint-wordpress.zip +0 -0
  6. package/src/js/build-client.js +8 -10
  7. package/src/js/fingerprint.builder.js +37 -38
  8. package/src/js/fingerprint.client.js +1531 -1463
  9. package/src/js/fingerprint.client.obfuscated.js +1 -0
  10. package/src/js/fingerprint.js +190 -16
  11. package/src/js/fingerprint.utils.js +213 -213
  12. package/src/js/library.js +340 -376
  13. package/src/js/mongodb-store.js +1 -1
  14. package/src/js/optimization.worker.js +27 -27
  15. package/src/js/pow.solver.inline.js +154 -56
  16. package/src/js/pow.solver.js +180 -92
  17. package/src/js/pow.worker.js +48 -48
  18. package/src/js/redis-store.js +1 -1
  19. package/src/js/tests/fingerprint.client.init.test.js +146 -143
  20. package/src/js/tests/fingerprint.test.js +124 -10
  21. package/src/js/tests/ip-reputation.test.js +1 -0
  22. package/src/js/upow-model-task.js +14 -15
  23. package/src/php/AutoTuner.php +34 -34
  24. package/src/php/Challenge/ChallengeUtils.php +57 -11
  25. package/src/php/Config/SecurityProfiles.php +20 -0
  26. package/src/php/DirectFingerprint.php +190 -145
  27. package/src/php/FingerprintBuilder.php +29 -30
  28. package/src/php/FingerprintClient.php +147 -148
  29. package/src/php/FingerprintEngine.php +44 -17
  30. package/src/php/Ja3AnomalyDetector.php +33 -32
  31. package/src/php/Optimization/FunctionRegistry.php +7 -7
  32. package/src/php/Optimization/Optimization.php +24 -24
  33. package/src/php/Optimization/OptimizationOperators.php +26 -25
  34. package/src/php/Optimization/ProblemInitializers.php +52 -52
  35. package/src/php/ProblemManager.php +31 -34
  36. package/src/php/RequestContext.php +8 -1
  37. package/src/php/Store/IStore.php +7 -8
  38. package/src/php/Store/InMemoryStore.php +66 -66
  39. package/src/php/Store/MongoDbStore.php +5 -5
  40. package/src/php/Store/RedisStore.php +5 -5
  41. package/src/php/Store/StoreManager.php +35 -35
  42. package/src/php/Tests/ChallengeUtilsTest.php +453 -453
  43. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  44. package/src/php/Tests/FingerprintEngineTest.php +38 -0
  45. package/src/php/Tests/IpReputationTest.php +10 -0
  46. package/src/php/Tests/MaliciousPatternsTest.php +103 -103
  47. package/src/php/Tests/MetricsTest.php +97 -97
  48. package/src/php/Tests/ProblemManagerTest.php +376 -376
  49. package/src/php/Tests/QuicFingerprintTest.php +53 -53
  50. package/src/php/Tests/RequestUtilsTest.php +471 -471
  51. package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
  52. package/src/php/Tests/config/ed25519_key.json +3 -3
  53. package/src/php/Utils/BlockList.php +99 -99
  54. package/src/php/Utils/Env.php +49 -49
  55. package/src/php/Utils/MaliciousPatterns.php +74 -74
  56. package/src/php/Utils/MetricsManager.php +209 -209
  57. package/src/php/Utils/RequestUtils.php +129 -7
  58. package/src/php/Utils/TLSClientHelloParser.php +369 -369
  59. package/src/php/WordPress/WpDbStore.php +155 -0
  60. package/src/php/WordPress/fingerprint-wordpress.php +554 -0
  61. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  62. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
  63. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  64. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
  65. package/src/php/WordPress/package.php +218 -0
  66. package/src/php/bin/auto-tune.php +118 -118
@@ -0,0 +1,218 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * Script d'empaquetage automatique du plugin WordPress.
7
+ * Copie les classes du moteur PHP, les assets JS et génère un ZIP prêt à être installé.
8
+ *
9
+ * Usage:
10
+ * php src/php/WordPress/package.php
11
+ */
12
+
13
+ if (!extension_loaded('zip')) {
14
+ fwrite(STDERR, "Erreur : L'extension PHP 'zip' est requise pour créer l'archive.\n");
15
+ exit(1);
16
+ }
17
+
18
+ $rootDir = dirname(__DIR__, 3);
19
+ $phpSrcDir = $rootDir . '/src/php';
20
+ $jsSrcDir = $rootDir . '/src/js';
21
+ $wpDir = $phpSrcDir . '/WordPress';
22
+ $distDir = $rootDir . '/public';
23
+ $pluginSlug = 'fingerprint-wordpress';
24
+ $buildDir = $distDir . '/' . $pluginSlug;
25
+ $zipFile = $distDir . '/' . $pluginSlug . '.zip';
26
+
27
+ echo "==> Début du packaging du plugin WordPress...\n";
28
+
29
+ // 1. Nettoyage du répertoire de build précédent
30
+ if (is_dir($buildDir)) {
31
+ $iterator = new RecursiveIteratorIterator(
32
+ new RecursiveDirectoryIterator($buildDir, FilesystemIterator::SKIP_DOTS),
33
+ RecursiveIteratorIterator::CHILD_FIRST
34
+ );
35
+ foreach ($iterator as $item) {
36
+ $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
37
+ }
38
+ rmdir($buildDir);
39
+ }
40
+ if (file_exists($zipFile)) {
41
+ unlink($zipFile);
42
+ }
43
+
44
+ @mkdir($buildDir . '/src', 0777, true);
45
+ @mkdir($buildDir . '/assets', 0777, true);
46
+ @mkdir($buildDir . '/languages', 0777, true);
47
+
48
+ /**
49
+ * Compile un fichier .po gettext en binaire .mo compatible WordPress.
50
+ */
51
+ function compilePoToMo(string $poFile, string $moFile): bool {
52
+ if (!file_exists($poFile)) return false;
53
+ $content = file_get_contents($poFile);
54
+ if ($content === false) return false;
55
+
56
+ $entries = [];
57
+ $currentMsgId = null;
58
+ $currentMsgStr = null;
59
+ $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $content));
60
+ $state = '';
61
+
62
+ foreach ($lines as $line) {
63
+ $line = trim($line);
64
+ if ($line === '' || str_starts_with($line, '#')) continue;
65
+ if (preg_match('/^msgid\s+"(.*)"$/', $line, $m)) {
66
+ if ($currentMsgId !== null && $currentMsgStr !== null) {
67
+ $entries[$currentMsgId] = $currentMsgStr;
68
+ }
69
+ $currentMsgId = stripcslashes($m[1]);
70
+ $currentMsgStr = null;
71
+ $state = 'msgid';
72
+ } elseif (preg_match('/^msgstr\s+"(.*)"$/', $line, $m)) {
73
+ $currentMsgStr = stripcslashes($m[1]);
74
+ $state = 'msgstr';
75
+ } elseif (preg_match('/^"(.*)"$/', $line, $m)) {
76
+ if ($state === 'msgid') {
77
+ $currentMsgId .= stripcslashes($m[1]);
78
+ } elseif ($state === 'msgstr') {
79
+ $currentMsgStr .= stripcslashes($m[1]);
80
+ }
81
+ }
82
+ }
83
+ if ($currentMsgId !== null && $currentMsgStr !== null) {
84
+ $entries[$currentMsgId] = $currentMsgStr;
85
+ }
86
+
87
+ ksort($entries);
88
+ $count = count($entries);
89
+ $originals = '';
90
+ $translations = '';
91
+ $origTable = [];
92
+ $transTable = [];
93
+
94
+ $headerSize = 28;
95
+ $tablesSize = $count * 8 * 2;
96
+ $curOffset = $headerSize + $tablesSize;
97
+
98
+ foreach ($entries as $orig => $trans) {
99
+ $origLen = strlen($orig);
100
+ $origTable[] = ['len' => $origLen, 'off' => $curOffset + strlen($originals)];
101
+ $originals .= $orig . "\0";
102
+ }
103
+
104
+ $transOffset = $curOffset + strlen($originals);
105
+ foreach ($entries as $orig => $trans) {
106
+ $transLen = strlen($trans);
107
+ $transTable[] = ['len' => $transLen, 'off' => $transOffset + strlen($translations)];
108
+ $translations .= $trans . "\0";
109
+ }
110
+
111
+ $mo = pack('V*',
112
+ 0x950412de, // Magic Number
113
+ 0, // Revision
114
+ $count, // Number of strings
115
+ 28, // Offset of table with original string lengths and offsets
116
+ 28 + ($count * 8), // Offset of table with translation string lengths and offsets
117
+ 0, 0 // Hash table size and offset
118
+ );
119
+
120
+ foreach ($origTable as $t) {
121
+ $mo .= pack('VV', $t['len'], $t['off']);
122
+ }
123
+ foreach ($transTable as $t) {
124
+ $mo .= pack('VV', $t['len'], $t['off']);
125
+ }
126
+ $mo .= $originals . $translations;
127
+ return file_put_contents($moFile, $mo) !== false;
128
+ }
129
+
130
+ // 2. Copie des fichiers principaux du plugin WordPress
131
+ copy($wpDir . '/fingerprint-wordpress.php', $buildDir . '/fingerprint-wordpress.php');
132
+ copy($wpDir . '/WpDbStore.php', $buildDir . '/WpDbStore.php');
133
+
134
+ // 3. Copie des assets clients nécessaires (solveur PoW)
135
+ $solverSource = $jsSrcDir . '/pow.solver.inline.js';
136
+ if (file_exists($solverSource)) {
137
+ copy($solverSource, $buildDir . '/assets/pow.solver.inline.js');
138
+ }
139
+
140
+ // 4. Copie et compilation des fichiers de traduction i18n (FR / DE / EN)
141
+ $langSourceDir = $wpDir . '/languages';
142
+ if (is_dir($langSourceDir)) {
143
+ foreach (glob($langSourceDir . '/*.po') as $poFile) {
144
+ $baseName = basename($poFile);
145
+ copy($poFile, $buildDir . '/languages/' . $baseName);
146
+ $moTarget = $buildDir . '/languages/' . preg_replace('/\.po$/', '.mo', $baseName);
147
+ compilePoToMo($poFile, $moTarget);
148
+ compilePoToMo($poFile, $langSourceDir . '/' . preg_replace('/\.po$/', '.mo', $baseName));
149
+ }
150
+ }
151
+
152
+ // 5. Copie récursive de la bibliothèque PHP (src/php -> build/src), en excluant les dossiers de dev
153
+ $excludeDirs = ['WordPress', 'Tests', 'bin'];
154
+ $phpIterator = new RecursiveIteratorIterator(
155
+ new RecursiveDirectoryIterator($phpSrcDir, FilesystemIterator::SKIP_DOTS),
156
+ RecursiveIteratorIterator::SELF_FIRST
157
+ );
158
+
159
+ foreach ($phpIterator as $item) {
160
+ $relativePath = substr($item->getPathname(), strlen($phpSrcDir) + 1);
161
+ $topDir = explode(DIRECTORY_SEPARATOR, str_replace('/', DIRECTORY_SEPARATOR, $relativePath))[0];
162
+
163
+ if (in_array($topDir, $excludeDirs, true)) {
164
+ continue;
165
+ }
166
+
167
+ $destPath = $buildDir . '/src/' . $relativePath;
168
+ if ($item->isDir()) {
169
+ if (!is_dir($destPath)) {
170
+ mkdir($destPath, 0777, true);
171
+ }
172
+ } else {
173
+ copy($item->getPathname(), $destPath);
174
+ }
175
+ }
176
+
177
+ echo "==> Fichiers copiés dans {$buildDir}\n";
178
+
179
+ // 5. Création de l'archive ZIP
180
+ $zip = new ZipArchive();
181
+ if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
182
+ fwrite(STDERR, "Erreur : Impossible de créer le fichier {$zipFile}\n");
183
+ exit(1);
184
+ }
185
+
186
+ $distIterator = new RecursiveIteratorIterator(
187
+ new RecursiveDirectoryIterator($buildDir, FilesystemIterator::SKIP_DOTS),
188
+ RecursiveIteratorIterator::LEAVES_ONLY
189
+ );
190
+
191
+ foreach ($distIterator as $file) {
192
+ if (!$file->isDir()) {
193
+ $filePath = $file->getPathname();
194
+ // Préserve le préfixe du dossier racine dans le ZIP : fingerprint-wordpress/...
195
+ $relativePath = $pluginSlug . '/' . substr($filePath, strlen($buildDir) + 1);
196
+ $zip->addFile($filePath, str_replace('\\', '/', $relativePath));
197
+ }
198
+ }
199
+ function removeDirectory(string $dir): void
200
+ {
201
+ if (!is_dir($dir)) {
202
+ return;
203
+ }
204
+ $iterator = new RecursiveIteratorIterator(
205
+ new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
206
+ RecursiveIteratorIterator::CHILD_FIRST
207
+ );
208
+ foreach ($iterator as $item) {
209
+ $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
210
+ }
211
+ rmdir($dir);
212
+ }
213
+ $zip->close();
214
+ removeDirectory($buildDir);
215
+ $sizeKb = round(filesize($zipFile) / 1024, 2);
216
+ echo "==> Archive ZIP générée avec succès :\n";
217
+ echo " Fichier : {$zipFile} ({$sizeKb} Ko)\n";
218
+ echo " Prêt à être installé via wp-admin ou déployé en production !\n";
@@ -1,118 +1,118 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- /**
6
- * CLI Script d'auto-tuning périodique des configurations de sécurité.
7
- * Ce script lit la configuration courante, récupère les logs, les assainit,
8
- * exécute l'optimiseur génétique, et écrase le fichier JSON d'origine.
9
- */
10
-
11
- $autoloaderPaths = [
12
- __DIR__ . '/../../vendor/autoload.php',
13
- __DIR__ . '/../vendor/autoload.php',
14
- __DIR__ . '/../../../autoload.php',
15
- ];
16
-
17
- $autoloaded = false;
18
- foreach ($autoloaderPaths as $path) {
19
- if (file_exists($path)) {
20
- require_once $path;
21
- $autoloaded = true;
22
- break;
23
- }
24
- }
25
-
26
- if (!$autoloaded) {
27
- fwrite(STDERR, "Erreur : Impossible de charger l'autoloader. Exécutez 'composer install'.\n");
28
- exit(1);
29
- }
30
-
31
- use Anonympins\Fingerprint\Optimization\Optimization;
32
- use Anonympins\Fingerprint\Store\StoreManager;
33
- use Anonympins\Fingerprint\Utils\RequestUtils;
34
-
35
- // 1. Récupération des arguments CLI
36
- $configPath = $argv[1] ?? null;
37
- if (!$configPath) {
38
- echo "Usage: php auto-tune.php [chemin_vers_security-config.json]\n";
39
- exit(1);
40
- }
41
-
42
- if (!file_exists($configPath)) {
43
- fwrite(STDERR, "Erreur : Fichier de configuration introuvable : {$configPath}\n");
44
- exit(1);
45
- }
46
-
47
- $config = json_decode(file_get_contents($configPath), true);
48
- if (json_last_error() !== JSON_ERROR_NONE) {
49
- fwrite(STDERR, "Erreur : Fichier de configuration JSON invalide.\n");
50
- exit(1);
51
- }
52
-
53
- // 2. Connexion au store pour récupérer les logs accumulés
54
- $store = StoreManager::getStore();
55
- if (!$store) {
56
- fwrite(STDERR, "Erreur : Aucun store de persistance actif.\n");
57
- exit(1);
58
- }
59
-
60
- $rawTrafficLogs = $store->get('traffic_logs') ?? [];
61
- if (empty($rawTrafficLogs)) {
62
- echo "[AutoTuning] Aucun log de trafic disponible pour l'optimisation.\n";
63
- exit(0);
64
- }
65
-
66
- // 3. Nettoyage des données pour prévenir l'empoisonnement (Sybil attacks)
67
- $sanitizedLogs = RequestUtils::sanitizeTrafficData($rawTrafficLogs);
68
-
69
- $minDataPoints = $config['autotuning']['minDataPoints'] ?? 200;
70
- if (count($sanitizedLogs) < $minDataPoints) {
71
- echo "[AutoTuning] Reporté : Pas assez de données assainies (" . count($sanitizedLogs) . "/{$minDataPoints}).\n";
72
- exit(0);
73
- }
74
-
75
- echo "[AutoTuning] Lancement de l'optimisation sur " . count($sanitizedLogs) . " données de trafic...\n";
76
-
77
- // 4. Résolution du Front de Pareto
78
- $paretoFront = Optimization::solveFullSecurityTuning(['trafficData' => $sanitizedLogs]);
79
- if (empty($paretoFront)) {
80
- fwrite(STDERR, "[AutoTuning] L'optimisation n'a retourné aucun résultat.\n");
81
- exit(1);
82
- }
83
-
84
- // 5. Sélection de la solution la plus équilibrée
85
- $bestSolution = $paretoFront[0];
86
- $minDistance = sqrt(pow($bestSolution['objectives'][0], 2) + pow($bestSolution['objectives'][1], 2));
87
- foreach ($paretoFront as $candidate) {
88
- $distance = sqrt(pow($candidate['objectives'][0], 2) + pow($candidate['objectives'][1], 2));
89
- if ($distance < $minDistance) {
90
- $minDistance = $distance;
91
- $bestSolution = $candidate;
92
- }
93
- }
94
-
95
- $newConfig = $bestSolution['solution'];
96
- $maxChangeVelocity = 0.15; // Inertie de 15% maximum par cycle
97
-
98
- $applyInertialUpdate = function (array &$current, array $target) use ($maxChangeVelocity) {
99
- $sumCurrent = array_sum($current);
100
- if ($sumCurrent === 0) return;
101
- $sumTarget = 0;
102
- foreach ($current as $k => $v) {
103
- if (isset($target[$k])) $sumTarget += $target[$k];
104
- }
105
- $ratio = ($sumTarget - $sumCurrent) / $sumCurrent;
106
- $factor = 1 + max(-$maxChangeVelocity, min($maxChangeVelocity, $ratio));
107
- foreach ($current as $k => &$v) {
108
- if (isset($target[$k])) $v *= $factor;
109
- }
110
- };
111
-
112
- $applyInertialUpdate($config['thresholds'], $newConfig['thresholds']);
113
- $applyInertialUpdate($config['weights'], $newConfig['weights']);
114
- $applyInertialUpdate($config['patterns'], $newConfig['patterns']);
115
-
116
- // 6. Écrasement propre du fichier de configuration original
117
- file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
118
- echo "[AutoTuning] Succès : Fichier {$configPath} mis à jour avec les paramètres optimisés.\n";
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * Periodic security configuration auto-tuning CLI script.
7
+ * Reads current configuration, prunes and sanitizes traffic logs,
8
+ * executes the genetic optimizer, and writes back the optimized profile.
9
+ */
10
+
11
+ $autoloaderPaths = [
12
+ __DIR__ . '/../../vendor/autoload.php',
13
+ __DIR__ . '/../vendor/autoload.php',
14
+ __DIR__ . '/../../../autoload.php',
15
+ ];
16
+
17
+ $autoloaded = false;
18
+ foreach ($autoloaderPaths as $path) {
19
+ if (file_exists($path)) {
20
+ require_once $path;
21
+ $autoloaded = true;
22
+ break;
23
+ }
24
+ }
25
+
26
+ if (!$autoloaded) {
27
+ fwrite(STDERR, "Error: Unable to load autoloader. Run 'composer install'.\n");
28
+ exit(1);
29
+ }
30
+
31
+ use Anonympins\Fingerprint\Optimization\Optimization;
32
+ use Anonympins\Fingerprint\Store\StoreManager;
33
+ use Anonympins\Fingerprint\Utils\RequestUtils;
34
+
35
+ // 1. Récupération des arguments CLI
36
+ $configPath = $argv[1] ?? null;
37
+ if (!$configPath) {
38
+ echo "Usage: php auto-tune.php [path_to_security-config.json]\n";
39
+ exit(1);
40
+ }
41
+
42
+ if (!file_exists($configPath)) {
43
+ fwrite(STDERR, "Error: Configuration file not found: {$configPath}\n");
44
+ exit(1);
45
+ }
46
+
47
+ $config = json_decode(file_get_contents($configPath), true);
48
+ if (json_last_error() !== JSON_ERROR_NONE) {
49
+ fwrite(STDERR, "Error: Invalid JSON configuration file.\n");
50
+ exit(1);
51
+ }
52
+
53
+ // 2. Connect to datastore to retrieve accumulated traffic logs
54
+ $store = StoreManager::getStore();
55
+ if (!$store) {
56
+ fwrite(STDERR, "Error: No active persistent datastore available.\n");
57
+ exit(1);
58
+ }
59
+
60
+ $rawTrafficLogs = $store->get('traffic_logs') ?? [];
61
+ if (empty($rawTrafficLogs)) {
62
+ echo "[AutoTuning] No traffic logs available for optimization.\n";
63
+ exit(0);
64
+ }
65
+
66
+ // 3. Sanitize data to prevent poisoning / Sybil attacks
67
+ $sanitizedLogs = RequestUtils::sanitizeTrafficData($rawTrafficLogs);
68
+
69
+ $minDataPoints = $config['autotuning']['minDataPoints'] ?? 200;
70
+ if (count($sanitizedLogs) < $minDataPoints) {
71
+ echo "[AutoTuning] Postponed: Insufficient sanitized logs (" . count($sanitizedLogs) . "/{$minDataPoints}).\n";
72
+ exit(0);
73
+ }
74
+
75
+ echo "[AutoTuning] Starting optimization pass on " . count($sanitizedLogs) . " traffic data points...\n";
76
+
77
+ // 4. Compute Pareto front
78
+ $paretoFront = Optimization::solveFullSecurityTuning(['trafficData' => $sanitizedLogs]);
79
+ if (empty($paretoFront)) {
80
+ fwrite(STDERR, "[AutoTuning] Optimization returned no solutions.\n");
81
+ exit(1);
82
+ }
83
+
84
+ // 5. Select the most balanced solution (closest to origin)
85
+ $bestSolution = $paretoFront[0];
86
+ $minDistance = sqrt(pow($bestSolution['objectives'][0], 2) + pow($bestSolution['objectives'][1], 2));
87
+ foreach ($paretoFront as $candidate) {
88
+ $distance = sqrt(pow($candidate['objectives'][0], 2) + pow($candidate['objectives'][1], 2));
89
+ if ($distance < $minDistance) {
90
+ $minDistance = $distance;
91
+ $bestSolution = $candidate;
92
+ }
93
+ }
94
+
95
+ $newConfig = $bestSolution['solution'];
96
+ $maxChangeVelocity = 0.15; // Max 15% inertial step per cycle
97
+
98
+ $applyInertialUpdate = function (array &$current, array $target) use ($maxChangeVelocity) {
99
+ $sumCurrent = array_sum($current);
100
+ if ($sumCurrent === 0) return;
101
+ $sumTarget = 0;
102
+ foreach ($current as $k => $v) {
103
+ if (isset($target[$k])) $sumTarget += $target[$k];
104
+ }
105
+ $ratio = ($sumTarget - $sumCurrent) / $sumCurrent;
106
+ $factor = 1 + max(-$maxChangeVelocity, min($maxChangeVelocity, $ratio));
107
+ foreach ($current as $k => &$v) {
108
+ if (isset($target[$k])) $v *= $factor;
109
+ }
110
+ };
111
+
112
+ $applyInertialUpdate($config['thresholds'], $newConfig['thresholds']);
113
+ $applyInertialUpdate($config['weights'], $newConfig['weights']);
114
+ $applyInertialUpdate($config['patterns'], $newConfig['patterns']);
115
+
116
+ // 6. Write back optimized parameters to configuration file
117
+ file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
118
+ echo "[AutoTuning] Success: File {$configPath} updated with optimized parameters.\n";