@anonympins/fingerprint 0.7.0 → 0.7.1
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 +17 -0
- package/README.md +1 -1
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/build-client.js +8 -10
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +1531 -1463
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/src/js/fingerprint.js +152 -10
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +340 -376
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +154 -56
- package/src/js/pow.solver.js +180 -92
- package/src/js/pow.worker.js +48 -48
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +34 -7
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +56 -10
- package/src/php/Config/SecurityProfiles.php +20 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +147 -148
- package/src/php/FingerprintEngine.php +7 -5
- package/src/php/Ja3AnomalyDetector.php +33 -32
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +26 -25
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +662 -662
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +129 -7
- package/src/php/Utils/TLSClientHelloParser.php +369 -369
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +217 -0
- package/src/php/WordPress/package.php +123 -0
- package/src/php/bin/auto-tune.php +118 -118
|
@@ -1,210 +1,210 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
-
|
|
7
|
-
class MetricsManager
|
|
8
|
-
{
|
|
9
|
-
/** @var array Stockage temporaire des compteurs Prometheus */
|
|
10
|
-
private static array $counters = [];
|
|
11
|
-
|
|
12
|
-
/** @var array Stockage temporaire des observations Prometheus */
|
|
13
|
-
private static array $observations = [];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Incrémente un compteur Prometheus.
|
|
17
|
-
*
|
|
18
|
-
* @param string $name Nom du compteur.
|
|
19
|
-
* @param array $labels Libellés/Labels associés.
|
|
20
|
-
*/
|
|
21
|
-
public static function incrementCounter(string $name, array $labels = []): void
|
|
22
|
-
{
|
|
23
|
-
if (strpos($name, 'fingerprint_') !== 0) {
|
|
24
|
-
$name = 'fingerprint_' . $name;
|
|
25
|
-
}
|
|
26
|
-
ksort($labels);
|
|
27
|
-
$labelPairs = [];
|
|
28
|
-
foreach ($labels as $k => $v) {
|
|
29
|
-
$labelPairs[] = "{$k}=\"{$v}\"";
|
|
30
|
-
}
|
|
31
|
-
$labelsStr = !empty($labelPairs) ? '{' . implode(',', $labelPairs) . '}' : '';
|
|
32
|
-
$key = $name . $labelsStr;
|
|
33
|
-
|
|
34
|
-
if (!isset(self::$counters[$key])) {
|
|
35
|
-
self::$counters[$key] = [
|
|
36
|
-
'name' => $name,
|
|
37
|
-
'labelsStr' => $labelsStr,
|
|
38
|
-
'value' => 0
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
self::$counters[$key]['value']++;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Enregistre une observation de valeur (ex: temps d'exécution, score).
|
|
46
|
-
*
|
|
47
|
-
* @param string $name Nom de la métrique.
|
|
48
|
-
* @param float $value Valeur observée.
|
|
49
|
-
* @param array $labels Libellés/Labels associés.
|
|
50
|
-
*/
|
|
51
|
-
public static function observeValue(string $name, float $value, array $labels = []): void
|
|
52
|
-
{
|
|
53
|
-
if (strpos($name, 'fingerprint_') !== 0) {
|
|
54
|
-
$name = 'fingerprint_' . $name;
|
|
55
|
-
}
|
|
56
|
-
ksort($labels);
|
|
57
|
-
$labelPairs = [];
|
|
58
|
-
foreach ($labels as $k => $v) {
|
|
59
|
-
$labelPairs[] = "{$k}=\"{$v}\"";
|
|
60
|
-
}
|
|
61
|
-
$labelsStr = !empty($labelPairs) ? '{' . implode(',', $labelPairs) . '}' : '';
|
|
62
|
-
$key = $name . $labelsStr;
|
|
63
|
-
|
|
64
|
-
self::$observations[$key] = [
|
|
65
|
-
'name' => $name,
|
|
66
|
-
'labelsStr' => $labelsStr,
|
|
67
|
-
'value' => $value
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Helper method to retrieve a specific metric's values by name.
|
|
73
|
-
*
|
|
74
|
-
* @param string $name Name of the metric.
|
|
75
|
-
* @param array $securityConfig The security configuration.
|
|
76
|
-
* @return array Associative array of labels to values.
|
|
77
|
-
*/
|
|
78
|
-
public static function getMetric(string $name, array $securityConfig = []): array
|
|
79
|
-
{
|
|
80
|
-
$result = [];
|
|
81
|
-
$queryName = str_starts_with($name, 'fingerprint_') ? $name : 'fingerprint_' . $name;
|
|
82
|
-
|
|
83
|
-
if ($queryName === 'fingerprint_security_weight') {
|
|
84
|
-
$weights = $securityConfig['weights'] ?? [];
|
|
85
|
-
foreach ($weights as $indicator => $weight) {
|
|
86
|
-
if (is_numeric($weight)) {
|
|
87
|
-
$result[$indicator] = (float)$weight;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} elseif ($queryName === 'fingerprint_security_threshold') {
|
|
91
|
-
$thresholds = $securityConfig['thresholds'] ?? [];
|
|
92
|
-
foreach ($thresholds as $level => $threshold) {
|
|
93
|
-
if (is_numeric($threshold)) {
|
|
94
|
-
$result[$level] = (float)$threshold;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
foreach (self::$counters as $counter) {
|
|
99
|
-
if ($counter['name'] === $queryName || $counter['name'] === $name) {
|
|
100
|
-
$labelStr = trim($counter['labelsStr'], '{}');
|
|
101
|
-
$result[$labelStr !== '' ? $labelStr : 'value'] = $counter['value'];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
foreach (self::$observations as $obs) {
|
|
105
|
-
if ($obs['name'] === $queryName || $obs['name'] === $name) {
|
|
106
|
-
$labelStr = trim($obs['labelsStr'], '{}');
|
|
107
|
-
$result[$labelStr !== '' ? $labelStr : 'value'] = $obs['value'];
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return $result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Réinitialise les compteurs enregistrés (utile pour l'isolation des tests).
|
|
116
|
-
*/
|
|
117
|
-
public static function clearMetrics(): void
|
|
118
|
-
{
|
|
119
|
-
self::$counters = [];
|
|
120
|
-
self::$observations = [];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Génère les métriques au format Prometheus text/plain.
|
|
125
|
-
*
|
|
126
|
-
* @param array $securityConfig La configuration de sécurité active.
|
|
127
|
-
* @param array|null $lastBestSolution La dernière solution calculée par l'Auto-Tuner.
|
|
128
|
-
* @return string
|
|
129
|
-
*/
|
|
130
|
-
public static function getPrometheusMetrics(array $securityConfig = [], ?array $lastBestSolution = null): string
|
|
131
|
-
{
|
|
132
|
-
$metrics = "";
|
|
133
|
-
|
|
134
|
-
if (empty(self::$counters)) {
|
|
135
|
-
$metrics .= "# HELP fingerprint_requests_total Total requests processed.\n";
|
|
136
|
-
$metrics .= "# TYPE fingerprint_requests_total counter\n";
|
|
137
|
-
$metrics .= "fingerprint_requests_total{status=\"passed\"} 1\n";
|
|
138
|
-
} else {
|
|
139
|
-
$grouped = [];
|
|
140
|
-
foreach (self::$counters as $c) {
|
|
141
|
-
$grouped[$c['name']][] = $c;
|
|
142
|
-
}
|
|
143
|
-
foreach ($grouped as $name => $instances) {
|
|
144
|
-
$metrics .= "# HELP {$name} Total requests processed.\n";
|
|
145
|
-
$metrics .= "# TYPE {$name} counter\n";
|
|
146
|
-
foreach ($instances as $instance) {
|
|
147
|
-
$metrics .= "{$name}{$instance['labelsStr']} {$instance['value']}\n";
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Export des observations (Gauges)
|
|
153
|
-
if (!empty(self::$observations)) {
|
|
154
|
-
$groupedObs = [];
|
|
155
|
-
foreach (self::$observations as $obs) {
|
|
156
|
-
$groupedObs[$obs['name']][] = $obs;
|
|
157
|
-
}
|
|
158
|
-
foreach ($groupedObs as $name => $instances) {
|
|
159
|
-
$metrics .= "\n# HELP {$name} Value observation.\n";
|
|
160
|
-
$metrics .= "# TYPE {$name} gauge\n";
|
|
161
|
-
foreach ($instances as $instance) {
|
|
162
|
-
$metrics .= "{$name}{$instance['labelsStr']} {$instance['value']}\n";
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 1. Export des poids actifs (Weights)
|
|
168
|
-
if (isset($securityConfig['weights']) && is_array($securityConfig['weights'])) {
|
|
169
|
-
$metrics .= "\n# HELP fingerprint_security_weight Active weight for each suspicion indicator.\n";
|
|
170
|
-
$metrics .= "# TYPE fingerprint_security_weight gauge\n";
|
|
171
|
-
foreach ($securityConfig['weights'] as $indicator => $weight) {
|
|
172
|
-
if (is_numeric($weight)) {
|
|
173
|
-
$metrics .= "fingerprint_security_weight{indicator=\"{$indicator}\"} {$weight}\n";
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 2. Export des seuils actifs (Thresholds)
|
|
179
|
-
if (isset($securityConfig['thresholds']) && is_array($securityConfig['thresholds'])) {
|
|
180
|
-
$metrics .= "\n# HELP fingerprint_security_threshold Active score threshold for each enforcement action level.\n";
|
|
181
|
-
$metrics .= "# TYPE fingerprint_security_threshold gauge\n";
|
|
182
|
-
foreach ($securityConfig['thresholds'] as $level => $threshold) {
|
|
183
|
-
if (is_numeric($threshold)) {
|
|
184
|
-
$metrics .= "fingerprint_security_threshold{level=\"{$level}\"} {$threshold}\n";
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 3. Récupération auto de la dernière solution d'auto-tuning depuis le cache (savePath) si non fournie
|
|
190
|
-
if ($lastBestSolution === null && isset($securityConfig['autotuning']['savePath'])) {
|
|
191
|
-
$savePath = $securityConfig['autotuning']['savePath'];
|
|
192
|
-
if (file_exists($savePath)) {
|
|
193
|
-
$savedData = json_decode(file_get_contents($savePath), true);
|
|
194
|
-
if (is_array($savedData) && isset($savedData['objectives'])) {
|
|
195
|
-
$lastBestSolution = $savedData;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// 4. Export des objectifs d'Auto-Tuning (Faux positifs & Faux négatifs calculés)
|
|
201
|
-
if ($lastBestSolution !== null && isset($lastBestSolution['objectives']) && is_array($lastBestSolution['objectives'])) {
|
|
202
|
-
$fpr = $lastBestSolution['objectives'][0] ?? 0.0;
|
|
203
|
-
$fnr = $lastBestSolution['objectives'][1] ?? 0.0;
|
|
204
|
-
$metrics .= "\n# HELP fingerprint_autotuning_false_positive_rate Current false positive rate calculated by the auto-tuner.\n# TYPE fingerprint_autotuning_false_positive_rate gauge\nfingerprint_autotuning_false_positive_rate {$fpr}\n";
|
|
205
|
-
$metrics .= "\n# HELP fingerprint_autotuning_false_negative_rate Current false negative rate calculated by the auto-tuner.\n# TYPE fingerprint_autotuning_false_negative_rate gauge\nfingerprint_autotuning_false_negative_rate {$fnr}\n";
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return $metrics;
|
|
209
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
+
|
|
7
|
+
class MetricsManager
|
|
8
|
+
{
|
|
9
|
+
/** @var array Stockage temporaire des compteurs Prometheus */
|
|
10
|
+
private static array $counters = [];
|
|
11
|
+
|
|
12
|
+
/** @var array Stockage temporaire des observations Prometheus */
|
|
13
|
+
private static array $observations = [];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Incrémente un compteur Prometheus.
|
|
17
|
+
*
|
|
18
|
+
* @param string $name Nom du compteur.
|
|
19
|
+
* @param array $labels Libellés/Labels associés.
|
|
20
|
+
*/
|
|
21
|
+
public static function incrementCounter(string $name, array $labels = []): void
|
|
22
|
+
{
|
|
23
|
+
if (strpos($name, 'fingerprint_') !== 0) {
|
|
24
|
+
$name = 'fingerprint_' . $name;
|
|
25
|
+
}
|
|
26
|
+
ksort($labels);
|
|
27
|
+
$labelPairs = [];
|
|
28
|
+
foreach ($labels as $k => $v) {
|
|
29
|
+
$labelPairs[] = "{$k}=\"{$v}\"";
|
|
30
|
+
}
|
|
31
|
+
$labelsStr = !empty($labelPairs) ? '{' . implode(',', $labelPairs) . '}' : '';
|
|
32
|
+
$key = $name . $labelsStr;
|
|
33
|
+
|
|
34
|
+
if (!isset(self::$counters[$key])) {
|
|
35
|
+
self::$counters[$key] = [
|
|
36
|
+
'name' => $name,
|
|
37
|
+
'labelsStr' => $labelsStr,
|
|
38
|
+
'value' => 0
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
self::$counters[$key]['value']++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Enregistre une observation de valeur (ex: temps d'exécution, score).
|
|
46
|
+
*
|
|
47
|
+
* @param string $name Nom de la métrique.
|
|
48
|
+
* @param float $value Valeur observée.
|
|
49
|
+
* @param array $labels Libellés/Labels associés.
|
|
50
|
+
*/
|
|
51
|
+
public static function observeValue(string $name, float $value, array $labels = []): void
|
|
52
|
+
{
|
|
53
|
+
if (strpos($name, 'fingerprint_') !== 0) {
|
|
54
|
+
$name = 'fingerprint_' . $name;
|
|
55
|
+
}
|
|
56
|
+
ksort($labels);
|
|
57
|
+
$labelPairs = [];
|
|
58
|
+
foreach ($labels as $k => $v) {
|
|
59
|
+
$labelPairs[] = "{$k}=\"{$v}\"";
|
|
60
|
+
}
|
|
61
|
+
$labelsStr = !empty($labelPairs) ? '{' . implode(',', $labelPairs) . '}' : '';
|
|
62
|
+
$key = $name . $labelsStr;
|
|
63
|
+
|
|
64
|
+
self::$observations[$key] = [
|
|
65
|
+
'name' => $name,
|
|
66
|
+
'labelsStr' => $labelsStr,
|
|
67
|
+
'value' => $value
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Helper method to retrieve a specific metric's values by name.
|
|
73
|
+
*
|
|
74
|
+
* @param string $name Name of the metric.
|
|
75
|
+
* @param array $securityConfig The security configuration.
|
|
76
|
+
* @return array Associative array of labels to values.
|
|
77
|
+
*/
|
|
78
|
+
public static function getMetric(string $name, array $securityConfig = []): array
|
|
79
|
+
{
|
|
80
|
+
$result = [];
|
|
81
|
+
$queryName = str_starts_with($name, 'fingerprint_') ? $name : 'fingerprint_' . $name;
|
|
82
|
+
|
|
83
|
+
if ($queryName === 'fingerprint_security_weight') {
|
|
84
|
+
$weights = $securityConfig['weights'] ?? [];
|
|
85
|
+
foreach ($weights as $indicator => $weight) {
|
|
86
|
+
if (is_numeric($weight)) {
|
|
87
|
+
$result[$indicator] = (float)$weight;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} elseif ($queryName === 'fingerprint_security_threshold') {
|
|
91
|
+
$thresholds = $securityConfig['thresholds'] ?? [];
|
|
92
|
+
foreach ($thresholds as $level => $threshold) {
|
|
93
|
+
if (is_numeric($threshold)) {
|
|
94
|
+
$result[$level] = (float)$threshold;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
foreach (self::$counters as $counter) {
|
|
99
|
+
if ($counter['name'] === $queryName || $counter['name'] === $name) {
|
|
100
|
+
$labelStr = trim($counter['labelsStr'], '{}');
|
|
101
|
+
$result[$labelStr !== '' ? $labelStr : 'value'] = $counter['value'];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
foreach (self::$observations as $obs) {
|
|
105
|
+
if ($obs['name'] === $queryName || $obs['name'] === $name) {
|
|
106
|
+
$labelStr = trim($obs['labelsStr'], '{}');
|
|
107
|
+
$result[$labelStr !== '' ? $labelStr : 'value'] = $obs['value'];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return $result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Réinitialise les compteurs enregistrés (utile pour l'isolation des tests).
|
|
116
|
+
*/
|
|
117
|
+
public static function clearMetrics(): void
|
|
118
|
+
{
|
|
119
|
+
self::$counters = [];
|
|
120
|
+
self::$observations = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Génère les métriques au format Prometheus text/plain.
|
|
125
|
+
*
|
|
126
|
+
* @param array $securityConfig La configuration de sécurité active.
|
|
127
|
+
* @param array|null $lastBestSolution La dernière solution calculée par l'Auto-Tuner.
|
|
128
|
+
* @return string
|
|
129
|
+
*/
|
|
130
|
+
public static function getPrometheusMetrics(array $securityConfig = [], ?array $lastBestSolution = null): string
|
|
131
|
+
{
|
|
132
|
+
$metrics = "";
|
|
133
|
+
|
|
134
|
+
if (empty(self::$counters)) {
|
|
135
|
+
$metrics .= "# HELP fingerprint_requests_total Total requests processed.\n";
|
|
136
|
+
$metrics .= "# TYPE fingerprint_requests_total counter\n";
|
|
137
|
+
$metrics .= "fingerprint_requests_total{status=\"passed\"} 1\n";
|
|
138
|
+
} else {
|
|
139
|
+
$grouped = [];
|
|
140
|
+
foreach (self::$counters as $c) {
|
|
141
|
+
$grouped[$c['name']][] = $c;
|
|
142
|
+
}
|
|
143
|
+
foreach ($grouped as $name => $instances) {
|
|
144
|
+
$metrics .= "# HELP {$name} Total requests processed.\n";
|
|
145
|
+
$metrics .= "# TYPE {$name} counter\n";
|
|
146
|
+
foreach ($instances as $instance) {
|
|
147
|
+
$metrics .= "{$name}{$instance['labelsStr']} {$instance['value']}\n";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Export des observations (Gauges)
|
|
153
|
+
if (!empty(self::$observations)) {
|
|
154
|
+
$groupedObs = [];
|
|
155
|
+
foreach (self::$observations as $obs) {
|
|
156
|
+
$groupedObs[$obs['name']][] = $obs;
|
|
157
|
+
}
|
|
158
|
+
foreach ($groupedObs as $name => $instances) {
|
|
159
|
+
$metrics .= "\n# HELP {$name} Value observation.\n";
|
|
160
|
+
$metrics .= "# TYPE {$name} gauge\n";
|
|
161
|
+
foreach ($instances as $instance) {
|
|
162
|
+
$metrics .= "{$name}{$instance['labelsStr']} {$instance['value']}\n";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 1. Export des poids actifs (Weights)
|
|
168
|
+
if (isset($securityConfig['weights']) && is_array($securityConfig['weights'])) {
|
|
169
|
+
$metrics .= "\n# HELP fingerprint_security_weight Active weight for each suspicion indicator.\n";
|
|
170
|
+
$metrics .= "# TYPE fingerprint_security_weight gauge\n";
|
|
171
|
+
foreach ($securityConfig['weights'] as $indicator => $weight) {
|
|
172
|
+
if (is_numeric($weight)) {
|
|
173
|
+
$metrics .= "fingerprint_security_weight{indicator=\"{$indicator}\"} {$weight}\n";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. Export des seuils actifs (Thresholds)
|
|
179
|
+
if (isset($securityConfig['thresholds']) && is_array($securityConfig['thresholds'])) {
|
|
180
|
+
$metrics .= "\n# HELP fingerprint_security_threshold Active score threshold for each enforcement action level.\n";
|
|
181
|
+
$metrics .= "# TYPE fingerprint_security_threshold gauge\n";
|
|
182
|
+
foreach ($securityConfig['thresholds'] as $level => $threshold) {
|
|
183
|
+
if (is_numeric($threshold)) {
|
|
184
|
+
$metrics .= "fingerprint_security_threshold{level=\"{$level}\"} {$threshold}\n";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 3. Récupération auto de la dernière solution d'auto-tuning depuis le cache (savePath) si non fournie
|
|
190
|
+
if ($lastBestSolution === null && isset($securityConfig['autotuning']['savePath'])) {
|
|
191
|
+
$savePath = $securityConfig['autotuning']['savePath'];
|
|
192
|
+
if (file_exists($savePath)) {
|
|
193
|
+
$savedData = json_decode(file_get_contents($savePath), true);
|
|
194
|
+
if (is_array($savedData) && isset($savedData['objectives'])) {
|
|
195
|
+
$lastBestSolution = $savedData;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 4. Export des objectifs d'Auto-Tuning (Faux positifs & Faux négatifs calculés)
|
|
201
|
+
if ($lastBestSolution !== null && isset($lastBestSolution['objectives']) && is_array($lastBestSolution['objectives'])) {
|
|
202
|
+
$fpr = $lastBestSolution['objectives'][0] ?? 0.0;
|
|
203
|
+
$fnr = $lastBestSolution['objectives'][1] ?? 0.0;
|
|
204
|
+
$metrics .= "\n# HELP fingerprint_autotuning_false_positive_rate Current false positive rate calculated by the auto-tuner.\n# TYPE fingerprint_autotuning_false_positive_rate gauge\nfingerprint_autotuning_false_positive_rate {$fpr}\n";
|
|
205
|
+
$metrics .= "\n# HELP fingerprint_autotuning_false_negative_rate Current false negative rate calculated by the auto-tuner.\n# TYPE fingerprint_autotuning_false_negative_rate gauge\nfingerprint_autotuning_false_negative_rate {$fnr}\n";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return $metrics;
|
|
209
|
+
}
|
|
210
210
|
}
|
|
@@ -411,6 +411,9 @@ class RequestUtils
|
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
$score = 0.0;
|
|
414
|
+
if (!empty($metrics['prototypeTampered'])) {
|
|
415
|
+
$score += 80.0;
|
|
416
|
+
}
|
|
414
417
|
|
|
415
418
|
$mouseAnalysis = self::analyzeMouseMovements($metrics['mouseMovementsHistory'] ?? null);
|
|
416
419
|
$touch = self::analyzeTouchMovements($metrics['touchMovementsHistory'] ?? null);
|
|
@@ -501,6 +504,15 @@ class RequestUtils
|
|
|
501
504
|
$score += 35;
|
|
502
505
|
}
|
|
503
506
|
}
|
|
507
|
+
|
|
508
|
+
// Détection de ferme mobile : Touch actif sur mobile sans aucune vibration physique (châssis/rack ADB)
|
|
509
|
+
$ua = $context->getHeader('user-agent') ?? '';
|
|
510
|
+
$isMobileDevice = str_contains($ua, 'Mobile');
|
|
511
|
+
if ($isMobileDevice && count($touchHistory) >= 5 && isset($metrics['motionVariance']) && is_numeric($metrics['motionVariance'])) {
|
|
512
|
+
if ((float)$metrics['motionVariance'] === 0.0) {
|
|
513
|
+
$score += 50.0;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
504
516
|
}
|
|
505
517
|
|
|
506
518
|
return ['behaviorScore' => min(100.0, $score)];
|
|
@@ -1244,6 +1256,36 @@ class RequestUtils
|
|
|
1244
1256
|
return null;
|
|
1245
1257
|
}
|
|
1246
1258
|
|
|
1259
|
+
/**
|
|
1260
|
+
* Calcule la longueur du préfixe commun (en bits) entre deux adresses IP (IPv4 ou IPv6).
|
|
1261
|
+
*/
|
|
1262
|
+
public static function getIpCommonPrefixLength(string $ip1, string $ip2): int
|
|
1263
|
+
{
|
|
1264
|
+
$bin1 = @inet_pton($ip1);
|
|
1265
|
+
$bin2 = @inet_pton($ip2);
|
|
1266
|
+
if ($bin1 === false || $bin2 === false || strlen($bin1) !== strlen($bin2)) {
|
|
1267
|
+
return 0;
|
|
1268
|
+
}
|
|
1269
|
+
$len = strlen($bin1);
|
|
1270
|
+
$bits = 0;
|
|
1271
|
+
for ($i = 0; $i < $len; $i++) {
|
|
1272
|
+
$xor = ord($bin1[$i]) ^ ord($bin2[$i]);
|
|
1273
|
+
if ($xor === 0) {
|
|
1274
|
+
$bits += 8;
|
|
1275
|
+
} else {
|
|
1276
|
+
for ($b = 7; $b >= 0; $b--) {
|
|
1277
|
+
if (($xor & (1 << $b)) === 0) {
|
|
1278
|
+
$bits++;
|
|
1279
|
+
} else {
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return $bits;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1247
1289
|
/**
|
|
1248
1290
|
* Met à jour les métriques agrégées pour un sous-réseau IP.
|
|
1249
1291
|
* @param RequestContext $context
|
|
@@ -1263,7 +1305,8 @@ class RequestUtils
|
|
|
1263
1305
|
'highScoreDevices' => [],
|
|
1264
1306
|
'lastActivity' => 0,
|
|
1265
1307
|
'ips' => [],
|
|
1266
|
-
'uas' => []
|
|
1308
|
+
'uas' => [],
|
|
1309
|
+
'attackerIps' => []
|
|
1267
1310
|
];
|
|
1268
1311
|
|
|
1269
1312
|
if (!isset($subnetData['highScoreDevices'])) {
|
|
@@ -1275,6 +1318,9 @@ class RequestUtils
|
|
|
1275
1318
|
if (!isset($subnetData['uas'])) {
|
|
1276
1319
|
$subnetData['uas'] = [];
|
|
1277
1320
|
}
|
|
1321
|
+
if (!isset($subnetData['attackerIps']) || !is_array($subnetData['attackerIps'])) {
|
|
1322
|
+
$subnetData['attackerIps'] = [];
|
|
1323
|
+
}
|
|
1278
1324
|
$now = time();
|
|
1279
1325
|
self::decaySubnetData($subnetData, $now);
|
|
1280
1326
|
|
|
@@ -1297,6 +1343,28 @@ class RequestUtils
|
|
|
1297
1343
|
}
|
|
1298
1344
|
|
|
1299
1345
|
$userAgent = $context->getHeader('user-agent') ?? '';
|
|
1346
|
+
if ($finalScore >= 70.0) {
|
|
1347
|
+
$foundAttacker = false;
|
|
1348
|
+
foreach ($subnetData['attackerIps'] as &$att) {
|
|
1349
|
+
if (isset($att['ip']) && $att['ip'] === $context->clientIp) {
|
|
1350
|
+
$att['lastSeen'] = $now;
|
|
1351
|
+
$att['score'] = max((float)($att['score'] ?? 0.0), $finalScore);
|
|
1352
|
+
$foundAttacker = true;
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
unset($att);
|
|
1357
|
+
if (!$foundAttacker) {
|
|
1358
|
+
$subnetData['attackerIps'][] = [
|
|
1359
|
+
'ip' => $context->clientIp,
|
|
1360
|
+
'lastSeen' => $now,
|
|
1361
|
+
'score' => $finalScore
|
|
1362
|
+
];
|
|
1363
|
+
if (count($subnetData['attackerIps']) > 30) {
|
|
1364
|
+
array_shift($subnetData['attackerIps']);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1300
1368
|
if (!empty($userAgent) && !in_array($userAgent, $subnetData['uas'], true)) {
|
|
1301
1369
|
$subnetData['uas'][] = $userAgent;
|
|
1302
1370
|
}
|
|
@@ -1326,9 +1394,10 @@ class RequestUtils
|
|
|
1326
1394
|
* Calcule un score de suspicion basé sur l'activité historique du sous-réseau IP.
|
|
1327
1395
|
* @param RequestContext $context
|
|
1328
1396
|
* @param string $currentDeviceId
|
|
1397
|
+
* @param array<string, mixed> $securityConfig
|
|
1329
1398
|
* @return array{'subnetScore': float}
|
|
1330
1399
|
*/
|
|
1331
|
-
public static function getSubnetScore(RequestContext $context, string $currentDeviceId): array
|
|
1400
|
+
public static function getSubnetScore(RequestContext $context, string $currentDeviceId = '', array $securityConfig = []): array
|
|
1332
1401
|
{
|
|
1333
1402
|
$subnet = self::getIpSubnet($context->clientIp);
|
|
1334
1403
|
if ($subnet === null) {
|
|
@@ -1373,14 +1442,61 @@ class RequestUtils
|
|
|
1373
1442
|
$uaDispersion = min(3.0, (float)max(1, $uaCount) / (float)$deviceCount);
|
|
1374
1443
|
$uaMultiplier = 0.7 + 0.3 * tanh($uaDispersion - 1.0);
|
|
1375
1444
|
|
|
1376
|
-
// 4. Intensité
|
|
1445
|
+
// 4. Intensité brute globale de la menace
|
|
1377
1446
|
$rawIntensity = (float)$highScoreCount * $bayesianDensity * $ipMultiplier * $uaMultiplier;
|
|
1378
1447
|
|
|
1379
|
-
// 5.
|
|
1380
|
-
$
|
|
1381
|
-
$
|
|
1382
|
-
$
|
|
1448
|
+
// 5. Composante 1 : Score ambiant plafonné en zone Medium (asymptote au seuil medium)
|
|
1449
|
+
$mediumThreshold = (float)($securityConfig['thresholds']['medium'] ?? 45.0);
|
|
1450
|
+
$ambientAsymptote = $mediumThreshold;
|
|
1451
|
+
$maxProximityBoost = max(0.0, 100.0 - $ambientAsymptote);
|
|
1452
|
+
$scale = 10.0;
|
|
1453
|
+
$ambientScore = $ambientAsymptote * tanh($rawIntensity / $scale);
|
|
1454
|
+
|
|
1455
|
+
// 6. Composante 2 : Boost de proximité micro-réseau avec des attaquants récents (le reste va jusqu'à 100)
|
|
1456
|
+
$proximityBoost = 0.0;
|
|
1457
|
+
$attackerIps = $subnetData['attackerIps'] ?? [];
|
|
1458
|
+
$clientIp = $context->clientIp;
|
|
1459
|
+
|
|
1460
|
+
if (!empty($attackerIps) && !empty($clientIp)) {
|
|
1461
|
+
$maxCommonPrefix = 0;
|
|
1462
|
+
$isV4 = filter_var($clientIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
|
|
1463
|
+
$isV6 = filter_var($clientIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
|
1464
|
+
|
|
1465
|
+
foreach ($attackerIps as $att) {
|
|
1466
|
+
$attIp = $att['ip'] ?? '';
|
|
1467
|
+
$lastSeen = $att['lastSeen'] ?? $now;
|
|
1468
|
+
if ($attIp !== '' && ($now - $lastSeen) <= 1800) {
|
|
1469
|
+
$prefixLen = self::getIpCommonPrefixLength($clientIp, $attIp);
|
|
1470
|
+
if ($prefixLen > $maxCommonPrefix) {
|
|
1471
|
+
$maxCommonPrefix = $prefixLen;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1383
1475
|
|
|
1476
|
+
if ($isV4) {
|
|
1477
|
+
if ($maxCommonPrefix >= 32) {
|
|
1478
|
+
$proximityBoost = $maxProximityBoost; // Même adresse IP exacte
|
|
1479
|
+
} elseif ($maxCommonPrefix >= 30) {
|
|
1480
|
+
$proximityBoost = $maxProximityBoost * (45.0 / 55.0); // Même /30 (écart <= 3 adresses)
|
|
1481
|
+
} elseif ($maxCommonPrefix >= 28) {
|
|
1482
|
+
$proximityBoost = $maxProximityBoost * (30.0 / 55.0); // Même /28 (bloc de 16 adresses)
|
|
1483
|
+
} elseif ($maxCommonPrefix >= 26) {
|
|
1484
|
+
$proximityBoost = $maxProximityBoost * (15.0 / 55.0); // Même /26 (bloc de 64 adresses)
|
|
1485
|
+
}
|
|
1486
|
+
} elseif ($isV6) {
|
|
1487
|
+
if ($maxCommonPrefix >= 128) {
|
|
1488
|
+
$proximityBoost = $maxProximityBoost; // Même IPv6 exacte
|
|
1489
|
+
} elseif ($maxCommonPrefix >= 120) {
|
|
1490
|
+
$proximityBoost = $maxProximityBoost * (45.0 / 55.0); // Même /120
|
|
1491
|
+
} elseif ($maxCommonPrefix >= 112) {
|
|
1492
|
+
$proximityBoost = $maxProximityBoost * (30.0 / 55.0); // Même /112
|
|
1493
|
+
} elseif ($maxCommonPrefix >= 96) {
|
|
1494
|
+
$proximityBoost = $maxProximityBoost * (15.0 / 55.0); // Même /96
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
$finalScore = min(100.0, round(($ambientScore + $proximityBoost) * 10.0) / 10.0);
|
|
1384
1500
|
return ['subnetScore' => $finalScore];
|
|
1385
1501
|
}
|
|
1386
1502
|
/**
|
|
@@ -1416,6 +1532,12 @@ class RequestUtils
|
|
|
1416
1532
|
}
|
|
1417
1533
|
}
|
|
1418
1534
|
|
|
1535
|
+
if (isset($subnetData['attackerIps']) && is_array($subnetData['attackerIps'])) {
|
|
1536
|
+
$subnetData['attackerIps'] = array_values(array_filter($subnetData['attackerIps'], function ($att) use ($now) {
|
|
1537
|
+
return ($now - ($att['lastSeen'] ?? $now)) < 3600;
|
|
1538
|
+
}));
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1419
1541
|
$subnetData['lastActivity'] = $now - ($inactivitySec % 1800);
|
|
1420
1542
|
}
|
|
1421
1543
|
}
|