@anonympins/fingerprint 0.6.1 → 0.6.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 +14 -0
- package/README.md +128 -119
- package/composer.json +2 -1
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +6802 -6601
- package/src/js/library.js +18 -11
- package/src/js/problem-manager.js +6 -2
- package/src/js/tests/fingerprint.test.js +47 -10
- package/src/js/tests/metrics.test.js +157 -103
- package/src/php/Challenge/ChallengeUtils.php +6 -4
- package/src/php/Config/SecurityProfiles.php +3 -2
- package/src/php/FingerprintEngine.php +56 -19
- package/src/php/Optimization/OptimizationOperators.php +21 -21
- package/src/php/RequestContext.php +3 -0
- package/src/php/Tests/ChallengeUtilsTest.php +6 -4
- package/src/php/Tests/FingerprintEngineTest.php +186 -0
- package/src/php/Tests/MetricsTest.php +97 -45
- package/src/php/Tests/RequestUtilsTest.php +14 -14
- package/src/php/Tests/config/ed25519_key.json +4 -0
- package/src/php/Utils/Env.php +50 -0
- package/src/php/Utils/MetricsManager.php +209 -166
- package/src/php/Utils/RequestUtils.php +63 -13
|
@@ -1,167 +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
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
$
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
$metrics .= "\
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
210
|
}
|
|
@@ -1254,12 +1254,20 @@ class RequestUtils
|
|
|
1254
1254
|
'highScoreCount' => 0,
|
|
1255
1255
|
'deviceIds' => [],
|
|
1256
1256
|
'highScoreDevices' => [],
|
|
1257
|
-
'lastActivity' => 0
|
|
1257
|
+
'lastActivity' => 0,
|
|
1258
|
+
'ips' => [],
|
|
1259
|
+
'uas' => []
|
|
1258
1260
|
];
|
|
1259
1261
|
|
|
1260
1262
|
if (!isset($subnetData['highScoreDevices'])) {
|
|
1261
1263
|
$subnetData['highScoreDevices'] = [];
|
|
1262
1264
|
}
|
|
1265
|
+
if (!isset($subnetData['ips'])) {
|
|
1266
|
+
$subnetData['ips'] = [];
|
|
1267
|
+
}
|
|
1268
|
+
if (!isset($subnetData['uas'])) {
|
|
1269
|
+
$subnetData['uas'] = [];
|
|
1270
|
+
}
|
|
1263
1271
|
|
|
1264
1272
|
// Utilisation de la partie stable du fingerprint matériel plutôt que l'ID de cookie volatil
|
|
1265
1273
|
$currentDeviceHash = self::getCompositeDeviceHash($context);
|
|
@@ -1274,6 +1282,16 @@ class RequestUtils
|
|
|
1274
1282
|
if (!in_array($stableFpId, $subnetData['deviceIds'], true)) {
|
|
1275
1283
|
$subnetData['deviceIds'][] = $stableFpId;
|
|
1276
1284
|
}
|
|
1285
|
+
|
|
1286
|
+
if (!in_array($context->clientIp, $subnetData['ips'], true)) {
|
|
1287
|
+
$subnetData['ips'][] = $context->clientIp;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
$userAgent = $context->getHeader('user-agent') ?? '';
|
|
1291
|
+
if (!empty($userAgent) && !in_array($userAgent, $subnetData['uas'], true)) {
|
|
1292
|
+
$subnetData['uas'][] = $userAgent;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1277
1295
|
$subnetData['lastActivity'] = time();
|
|
1278
1296
|
|
|
1279
1297
|
// Limiter la taille du tableau des deviceIds pour éviter une consommation mémoire excessive.
|
|
@@ -1285,6 +1303,12 @@ class RequestUtils
|
|
|
1285
1303
|
unset($subnetData['highScoreDevices'][$oldDeviceId]);
|
|
1286
1304
|
}
|
|
1287
1305
|
}
|
|
1306
|
+
if (count($subnetData['ips']) > 100) {
|
|
1307
|
+
array_shift($subnetData['ips']);
|
|
1308
|
+
}
|
|
1309
|
+
if (count($subnetData['uas']) > 50) {
|
|
1310
|
+
array_shift($subnetData['uas']);
|
|
1311
|
+
}
|
|
1288
1312
|
|
|
1289
1313
|
// TTL de 24 heures pour les données de sous-réseau.
|
|
1290
1314
|
$store->set($key, $subnetData, 86400);
|
|
@@ -1318,23 +1342,32 @@ class RequestUtils
|
|
|
1318
1342
|
|
|
1319
1343
|
$highScoreCount = $subnetData['highScoreCount'] ?? 0;
|
|
1320
1344
|
$deviceCount = isset($subnetData['deviceIds']) ? count($subnetData['deviceIds']) : 0;
|
|
1345
|
+
$ipCount = isset($subnetData['ips']) ? count($subnetData['ips']) : 1;
|
|
1346
|
+
$uaCount = isset($subnetData['uas']) ? count($subnetData['uas']) : 1;
|
|
1321
1347
|
|
|
1322
1348
|
if ($halfLives > 0) {
|
|
1323
|
-
$
|
|
1324
|
-
$
|
|
1349
|
+
$decay = pow(2, $halfLives);
|
|
1350
|
+
$highScoreCount = max(0, (int)floor($highScoreCount / $decay));
|
|
1351
|
+
$deviceCount = max(0, (int)floor($deviceCount / $decay));
|
|
1352
|
+
$ipCount = max(1, (int)floor($ipCount / $decay));
|
|
1353
|
+
$uaCount = max(1, (int)floor($uaCount / $decay));
|
|
1325
1354
|
}
|
|
1326
1355
|
|
|
1327
|
-
$
|
|
1328
|
-
|
|
1329
|
-
// Pénalité basée sur le nombre de devices uniques vus depuis ce sous-réseau.
|
|
1330
|
-
if ($deviceCount > 10) {
|
|
1331
|
-
$score += min(80.0, ($deviceCount - 10) * 5);
|
|
1356
|
+
if ($deviceCount === 0) {
|
|
1357
|
+
return ['subnetScore' => 0.0];
|
|
1332
1358
|
}
|
|
1333
1359
|
|
|
1334
|
-
|
|
1335
|
-
$
|
|
1360
|
+
$suspicionDensity = $highScoreCount / $deviceCount;
|
|
1361
|
+
$ipDeviceRatio = $ipCount / $deviceCount;
|
|
1362
|
+
|
|
1363
|
+
$baseScore = 100.0 * (1.0 - exp(-0.15 * $highScoreCount));
|
|
1364
|
+
|
|
1365
|
+
$densityMultiplier = 0.4 + (1.6 * $suspicionDensity);
|
|
1366
|
+
$distributionMultiplier = 0.5 + (1.0 * $ipDeviceRatio);
|
|
1367
|
+
|
|
1368
|
+
$finalScore = min(100.0, round($baseScore * $densityMultiplier * $distributionMultiplier * 10.0) / 10.0);
|
|
1336
1369
|
|
|
1337
|
-
return ['subnetScore' =>
|
|
1370
|
+
return ['subnetScore' => $finalScore];
|
|
1338
1371
|
}
|
|
1339
1372
|
|
|
1340
1373
|
/**
|
|
@@ -1364,10 +1397,15 @@ class RequestUtils
|
|
|
1364
1397
|
});
|
|
1365
1398
|
$clusterData = array_values($clusterData);
|
|
1366
1399
|
|
|
1400
|
+
$userAgent = $context->getHeader('user-agent') ?? '';
|
|
1401
|
+
$subnet = self::getIpSubnet($context->clientIp) ?? 'unknown';
|
|
1402
|
+
|
|
1367
1403
|
$found = false;
|
|
1368
1404
|
foreach ($clusterData as &$entry) {
|
|
1369
1405
|
if (isset($entry['ip']) && $entry['ip'] === $context->clientIp) {
|
|
1370
1406
|
$entry['timestamp'] = $now;
|
|
1407
|
+
$entry['ua'] = $userAgent;
|
|
1408
|
+
$entry['subnet'] = $subnet;
|
|
1371
1409
|
$found = true;
|
|
1372
1410
|
break;
|
|
1373
1411
|
}
|
|
@@ -1375,14 +1413,26 @@ class RequestUtils
|
|
|
1375
1413
|
unset($entry);
|
|
1376
1414
|
|
|
1377
1415
|
if (!$found) {
|
|
1378
|
-
$clusterData[] = [
|
|
1416
|
+
$clusterData[] = [
|
|
1417
|
+
'ip' => $context->clientIp,
|
|
1418
|
+
'timestamp' => $now,
|
|
1419
|
+
'ua' => $userAgent,
|
|
1420
|
+
'subnet' => $subnet
|
|
1421
|
+
];
|
|
1379
1422
|
}
|
|
1380
1423
|
|
|
1381
1424
|
$store->set($key, $clusterData, 600);
|
|
1382
1425
|
$uniqueIpsCount = count($clusterData);
|
|
1383
1426
|
$botnetClusterScore = 0.0;
|
|
1384
1427
|
if ($uniqueIpsCount >= 2) {
|
|
1385
|
-
$
|
|
1428
|
+
$uniqueSubnets = count(array_unique(array_filter(array_column($clusterData, 'subnet'))));
|
|
1429
|
+
$uniqueUserAgents = count(array_unique(array_filter(array_column($clusterData, 'ua'))));
|
|
1430
|
+
|
|
1431
|
+
$subnetMultiplier = $uniqueSubnets > 1 ? 1.3 : 0.6;
|
|
1432
|
+
$uaRotationMultiplier = $uniqueUserAgents > 1 ? 1.5 : 1.0;
|
|
1433
|
+
|
|
1434
|
+
$baseScore = 100.0 * (1.0 - exp(-0.35 * ($uniqueIpsCount - 1)));
|
|
1435
|
+
$botnetClusterScore = min(100.0, round($baseScore * $subnetMultiplier * $uaRotationMultiplier * 10.0) / 10.0);
|
|
1386
1436
|
}
|
|
1387
1437
|
return ['botnetClusterScore' => $botnetClusterScore];
|
|
1388
1438
|
}
|