@anonympins/fingerprint 0.4.2 → 0.4.4
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 +38 -0
- package/README.md +70 -60
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +831 -635
- package/src/js/fingerprint.js +249 -31
- package/src/js/pow.solver.js +531 -497
- package/src/js/problem-manager.js +24 -0
- package/src/js/tests/fingerprint.client.init.test.js +140 -119
- package/src/js/tests/fingerprint.test.js +2451 -2319
- package/src/js/tests/pow.solver.test.js +233 -197
- package/src/js/tests/problem-manager.test.js +358 -322
- package/src/php/Challenge/ChallengeUtils.php +415 -361
- package/src/php/Config/SecurityProfiles.php +276 -271
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +36 -2
- package/src/php/Optimization/FunctionRegistry.php +63 -62
- package/src/php/Optimization/OptimizationOperators.php +401 -304
- package/src/php/ProblemManager.php +29 -0
- package/src/php/RequestContext.php +90 -90
- package/src/php/Store/IStore.php +41 -41
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/FingerprintEngineTest.php +330 -299
- package/src/php/Tests/ProblemManagerTest.php +376 -296
- package/src/php/Tests/RequestUtilsTest.php +386 -253
- package/src/php/Tests/problems.config.json +3 -3
- package/src/php/Utils/RequestUtils.php +201 -17
|
@@ -1,254 +1,387 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
-
|
|
7
|
-
use Anonympins\Fingerprint\FingerprintBuilder;
|
|
8
|
-
use Anonympins\Fingerprint\RequestContext;
|
|
9
|
-
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
10
|
-
use PHPUnit\Framework\TestCase;
|
|
11
|
-
|
|
12
|
-
class RequestUtilsTest extends TestCase
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
$
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
$this->
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
$
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
$
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
$
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
$
|
|
137
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
$
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
$
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
$
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
]);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
$
|
|
216
|
-
$this->
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
$
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
$
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
$
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
$
|
|
252
|
-
$this->
|
|
253
|
-
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\FingerprintBuilder;
|
|
8
|
+
use Anonympins\Fingerprint\RequestContext;
|
|
9
|
+
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
10
|
+
use PHPUnit\Framework\TestCase;
|
|
11
|
+
|
|
12
|
+
class RequestUtilsTest extends TestCase
|
|
13
|
+
{
|
|
14
|
+
protected function setUp(): void
|
|
15
|
+
{
|
|
16
|
+
parent::setUp();
|
|
17
|
+
\Anonympins\Fingerprint\Store\StoreManager::configureStore(new \Anonympins\Fingerprint\Store\InMemoryStore());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private function createRequestContext(array $overrides = []): RequestContext
|
|
21
|
+
{
|
|
22
|
+
$defaults = [
|
|
23
|
+
'clientIp' => '127.0.0.1',
|
|
24
|
+
'path' => '/',
|
|
25
|
+
'headers' => ['user-agent' => 'Test UA'],
|
|
26
|
+
'query' => [],
|
|
27
|
+
'body' => null,
|
|
28
|
+
'cookies' => [],
|
|
29
|
+
'httpVersion' => '1.1',
|
|
30
|
+
];
|
|
31
|
+
$params = array_merge($defaults, $overrides);
|
|
32
|
+
return new RequestContext(
|
|
33
|
+
$params['clientIp'], $params['path'], $params['headers'],
|
|
34
|
+
$params['query'], $params['body'], $params['cookies'], $params['httpVersion']
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
|
|
39
|
+
{
|
|
40
|
+
$context = $this->createRequestContext([
|
|
41
|
+
'headers' => ['x-behavior-metrics' => json_encode([])]
|
|
42
|
+
]);
|
|
43
|
+
$result = RequestUtils::getClickVarianceScore($context);
|
|
44
|
+
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public function testGetClickVarianceScoreReturnsZeroForInsufficientClicks(): void
|
|
48
|
+
{
|
|
49
|
+
$metrics = [
|
|
50
|
+
'clicksHistory' => [
|
|
51
|
+
['x' => 10, 'y' => 10, 'targetId' => 'hash1'],
|
|
52
|
+
['x' => 11, 'y' => 11, 'targetId' => 'hash1'],
|
|
53
|
+
['x' => 100, 'y' => 100, 'targetId' => 'hash2']
|
|
54
|
+
]
|
|
55
|
+
];
|
|
56
|
+
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
57
|
+
$result = RequestUtils::getClickVarianceScore($context);
|
|
58
|
+
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public function testGetClickVarianceScoreReturnsHighScoreForLowVariance(): void
|
|
62
|
+
{
|
|
63
|
+
$metrics = [
|
|
64
|
+
'clicksHistory' => [
|
|
65
|
+
['x' => 100, 'y' => 100, 'targetId' => 'hash1'],
|
|
66
|
+
['x' => 100.1, 'y' => 100.2, 'targetId' => 'hash1'],
|
|
67
|
+
['x' => 99.9, 'y' => 99.8, 'targetId' => 'hash1']
|
|
68
|
+
]
|
|
69
|
+
];
|
|
70
|
+
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
71
|
+
$result = RequestUtils::getClickVarianceScore($context);
|
|
72
|
+
$this->assertGreaterThan(90, $result['clickVarianceScore']);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public function testGetClickVarianceScoreReturnsLowScoreForHighVariance(): void
|
|
76
|
+
{
|
|
77
|
+
$metrics = [
|
|
78
|
+
'clicksHistory' => [
|
|
79
|
+
['x' => 105, 'y' => 110, 'targetId' => 'hash1'],
|
|
80
|
+
['x' => 98, 'y' => 102, 'targetId' => 'hash1'],
|
|
81
|
+
['x' => 112, 'y' => 95, 'targetId' => 'hash1']
|
|
82
|
+
]
|
|
83
|
+
];
|
|
84
|
+
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
85
|
+
$result = RequestUtils::getClickVarianceScore($context);
|
|
86
|
+
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public function testSanitizeTrafficDataFiltersSybilAttacks(): void
|
|
90
|
+
{
|
|
91
|
+
$trafficData = [];
|
|
92
|
+
// Ajout de 100 logs provenant d'un attaquant (Sybil)
|
|
93
|
+
for ($i = 0; $i < 100; $i++) {
|
|
94
|
+
$trafficData[] = ['deviceId' => 'attacker_device', 'type' => 'trap_triggered'];
|
|
95
|
+
}
|
|
96
|
+
// Ajout de 10 logs d'utilisateurs légitimes distincts
|
|
97
|
+
for ($i = 0; $i < 10; $i++) {
|
|
98
|
+
$trafficData[] = ['deviceId' => "legit_device_{$i}", 'type' => 'challenge_solved'];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
$sanitized = RequestUtils::sanitizeTrafficData($trafficData);
|
|
102
|
+
|
|
103
|
+
$attackerLogs = array_filter($sanitized, fn($log) => $log['deviceId'] === 'attacker_device');
|
|
104
|
+
|
|
105
|
+
// Total de 110 logs. 2% de 110 est 2.2 -> max(3, 2) = 3 logs maximum autorisés pour l'attaquant.
|
|
106
|
+
$this->assertLessThanOrEqual(3, count($attackerLogs));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public function testChallengePayloadSigningAndVerification(): void
|
|
110
|
+
{
|
|
111
|
+
$secret = 'test-fallback-dev-secret-32-chars-minimum';
|
|
112
|
+
$clientIp = '203.0.113.42';
|
|
113
|
+
$payload = [
|
|
114
|
+
'clientSecret' => 'some_client_secret',
|
|
115
|
+
'cpuTarget' => '00000000ffffffff',
|
|
116
|
+
'fingerprint' => 'os:hash|gpu:hash2',
|
|
117
|
+
'memDifficulty' => '16',
|
|
118
|
+
'originalPath' => '/submit',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// 1. Cas nominal : Signature et vérification réussies
|
|
122
|
+
$signature = RequestUtils::signChallengePayload($secret, $payload, $clientIp);
|
|
123
|
+
$this->assertNotEmpty($signature);
|
|
124
|
+
|
|
125
|
+
$payloadWithSig = $payload;
|
|
126
|
+
$payloadWithSig['signature'] = $signature;
|
|
127
|
+
|
|
128
|
+
$isValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, $clientIp);
|
|
129
|
+
$this->assertTrue($isValid, "La signature valide doit être acceptée.");
|
|
130
|
+
|
|
131
|
+
// 2. Détection de modification (tampering) sur cpuTarget
|
|
132
|
+
$tamperedPayload = $payloadWithSig;
|
|
133
|
+
$tamperedPayload['cpuTarget'] = 'ffffffffffffffff'; // Tentative de baisse de la difficulté
|
|
134
|
+
|
|
135
|
+
$isTamperedValid = RequestUtils::verifyChallengePayload($secret, $tamperedPayload, $clientIp);
|
|
136
|
+
$this->assertFalse($isTamperedValid, "Un payload modifié doit être rejeté.");
|
|
137
|
+
|
|
138
|
+
// 3. Détection de modification sur le fingerprint
|
|
139
|
+
$tamperedFpPayload = $payloadWithSig;
|
|
140
|
+
$tamperedFpPayload['fingerprint'] = 'os:another_hash|gpu:hash2';
|
|
141
|
+
|
|
142
|
+
$isTamperedFpValid = RequestUtils::verifyChallengePayload($secret, $tamperedFpPayload, $clientIp);
|
|
143
|
+
$this->assertFalse($isTamperedFpValid, "Un fingerprint modifié doit être rejeté.");
|
|
144
|
+
|
|
145
|
+
// 4. Détection d'usurpation d'adresse IP (IP mismatch)
|
|
146
|
+
$isIpMismatchValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, '198.51.100.1');
|
|
147
|
+
$this->assertFalse($isIpMismatchValid, "Le payload ne doit pas être valide pour une autre adresse IP.");
|
|
148
|
+
|
|
149
|
+
// 5. Absence de signature
|
|
150
|
+
$isMissingSigValid = RequestUtils::verifyChallengePayload($secret, $payload, $clientIp);
|
|
151
|
+
$this->assertFalse($isMissingSigValid, "Un payload sans signature doit être rejeté.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public function testGetBehaviorScoreReturnsProperScores(): void
|
|
155
|
+
{
|
|
156
|
+
$contextEmpty = $this->createRequestContext();
|
|
157
|
+
$scoreEmpty = RequestUtils::getBehaviorScore($contextEmpty);
|
|
158
|
+
$this->assertEquals(0.0, $scoreEmpty['behaviorScore']);
|
|
159
|
+
|
|
160
|
+
$metricsHoneypot = ['honeypotInteraction' => true];
|
|
161
|
+
$contextHoneypot = $this->createRequestContext([
|
|
162
|
+
'headers' => ['x-behavior-metrics' => json_encode($metricsHoneypot)]
|
|
163
|
+
]);
|
|
164
|
+
$scoreHoneypot = RequestUtils::getBehaviorScore($contextHoneypot);
|
|
165
|
+
$this->assertEquals(100.0, $scoreHoneypot['behaviorScore']);
|
|
166
|
+
|
|
167
|
+
$metricsNoActivity = ['honeypotInteraction' => false, 'mouseMovementsHistory' => [], 'keystrokeLatency' => 0];
|
|
168
|
+
$contextNoActivity = $this->createRequestContext([
|
|
169
|
+
'headers' => ['x-behavior-metrics' => json_encode($metricsNoActivity)]
|
|
170
|
+
]);
|
|
171
|
+
$scoreNoActivity = RequestUtils::getBehaviorScore($contextNoActivity);
|
|
172
|
+
$this->assertEquals(40.0, $scoreNoActivity['behaviorScore']);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public function testGetBehaviorScoreWithBotLikeTouchMovements(): void
|
|
176
|
+
{
|
|
177
|
+
$metrics = [
|
|
178
|
+
'honeypotInteraction' => false,
|
|
179
|
+
'touchMovementsHistory' => [
|
|
180
|
+
['x' => 50, 'y' => 50, 't' => 1, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
181
|
+
['x' => 55, 'y' => 55, 't' => 100, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
182
|
+
['x' => 60, 'y' => 60, 't' => 200, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
183
|
+
['x' => 65, 'y' => 65, 't' => 300, 'p' => 0.5, 'r' => 10, 'num' => 1]
|
|
184
|
+
]
|
|
185
|
+
];
|
|
186
|
+
$context = $this->createRequestContext([
|
|
187
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
188
|
+
]);
|
|
189
|
+
$score = RequestUtils::getBehaviorScore($context);
|
|
190
|
+
$this->assertGreaterThan(60.0, $score['behaviorScore']);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public function testGetBehaviorScoreWithHumanLikeTouchMovements(): void
|
|
194
|
+
{
|
|
195
|
+
$metrics = [
|
|
196
|
+
'honeypotInteraction' => false,
|
|
197
|
+
'touchMovementsHistory' => [
|
|
198
|
+
['x' => 50, 'y' => 50, 't' => 1, 'p' => 0.45, 'r' => 8.5, 'num' => 1],
|
|
199
|
+
['x' => 60, 'y' => 52, 't' => 100, 'p' => 0.52, 'r' => 9.1, 'num' => 1],
|
|
200
|
+
['x' => 72, 'y' => 60, 't' => 200, 'p' => 0.49, 'r' => 8.8, 'num' => 1],
|
|
201
|
+
['x' => 80, 'y' => 80, 't' => 300, 'p' => 0.41, 'r' => 8.2, 'num' => 1]
|
|
202
|
+
]
|
|
203
|
+
];
|
|
204
|
+
$context = $this->createRequestContext([
|
|
205
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
206
|
+
]);
|
|
207
|
+
$score = RequestUtils::getBehaviorScore($context);
|
|
208
|
+
$this->assertLessThan(30.0, $score['behaviorScore']);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public function testGetTimeInconsistencyScore(): void
|
|
212
|
+
{
|
|
213
|
+
$requestTimestamp = time() * 1000;
|
|
214
|
+
|
|
215
|
+
$metricsNormal = ['clientTimestamp' => $requestTimestamp - 100];
|
|
216
|
+
$contextNormal = $this->createRequestContext([
|
|
217
|
+
'headers' => ['x-behavior-metrics' => json_encode($metricsNormal)]
|
|
218
|
+
]);
|
|
219
|
+
$contextNormal->requestTimestamp = $requestTimestamp;
|
|
220
|
+
$scoreNormal = RequestUtils::getTimeInconsistencyScore($contextNormal);
|
|
221
|
+
$this->assertEquals(0.0, $scoreNormal['timeInconsistencyScore']);
|
|
222
|
+
|
|
223
|
+
$metricsReplay = ['clientTimestamp' => $requestTimestamp - 10000];
|
|
224
|
+
$contextReplay = $this->createRequestContext([
|
|
225
|
+
'headers' => ['x-behavior-metrics' => json_encode($metricsReplay)]
|
|
226
|
+
]);
|
|
227
|
+
$contextReplay->requestTimestamp = $requestTimestamp;
|
|
228
|
+
$scoreReplay = RequestUtils::getTimeInconsistencyScore($contextReplay);
|
|
229
|
+
$this->assertGreaterThan(0.0, $scoreReplay['timeInconsistencyScore']);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public function testGetCrossLayerInconsistencyOSMismatch(): void
|
|
233
|
+
{
|
|
234
|
+
$windowsHash = FingerprintBuilder::cyrb53("Windows");
|
|
235
|
+
$context = $this->createRequestContext([
|
|
236
|
+
'headers' => [
|
|
237
|
+
'x-device-fingerprint' => "os:{$windowsHash}",
|
|
238
|
+
'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15'
|
|
239
|
+
]
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
243
|
+
$this->assertEquals(50.0, $score['crossLayerInconsistencyScore']);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public function testGetBotScoreDetections(): void
|
|
247
|
+
{
|
|
248
|
+
$contextBot = $this->createRequestContext([
|
|
249
|
+
'headers' => ['x-device-fingerprint' => 'ua:123|bot:true']
|
|
250
|
+
]);
|
|
251
|
+
$scoreBot = RequestUtils::getBotScore($contextBot);
|
|
252
|
+
$this->assertEquals(100.0, $scoreBot['botScore']);
|
|
253
|
+
|
|
254
|
+
$contextCdp = $this->createRequestContext([
|
|
255
|
+
'headers' => ['x-device-fingerprint' => 'ua:123|cdp:true']
|
|
256
|
+
]);
|
|
257
|
+
$scoreCdp = RequestUtils::getBotScore($contextCdp);
|
|
258
|
+
$this->assertEquals(100.0, $scoreCdp['botScore']);
|
|
259
|
+
|
|
260
|
+
$contextClean = $this->createRequestContext([
|
|
261
|
+
'headers' => ['x-device-fingerprint' => 'ua:123|os:456']
|
|
262
|
+
]);
|
|
263
|
+
$scoreClean = RequestUtils::getBotScore($contextClean);
|
|
264
|
+
$this->assertEquals(0.0, $scoreClean['botScore']);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public function testGetHeaderAnomaliesFirefoxTE(): void
|
|
268
|
+
{
|
|
269
|
+
$contextFirefoxNoTe = $this->createRequestContext([
|
|
270
|
+
'headers' => [
|
|
271
|
+
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
|
272
|
+
'accept-language' => 'en-US,en;q=0.9',
|
|
273
|
+
]
|
|
274
|
+
]);
|
|
275
|
+
$scoreFirefoxNoTe = RequestUtils::getHeaderAnomalies($contextFirefoxNoTe);
|
|
276
|
+
$this->assertEquals(30.0, $scoreFirefoxNoTe['headerAnomalyScore']);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public function testGetBehavioralIndicatorsRotationAndHistory(): void
|
|
280
|
+
{
|
|
281
|
+
$deviceData = [
|
|
282
|
+
'ips' => ['127.0.0.1'],
|
|
283
|
+
'lastFpHash' => 'cvs:original-canvas|gpu:original-gpu',
|
|
284
|
+
'lastChangeTimestamp' => (time() * 1000) - 500,
|
|
285
|
+
'rapidChangeCount' => 1
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
$context = $this->createRequestContext([
|
|
289
|
+
'headers' => [
|
|
290
|
+
'x-device-fingerprint' => 'cvs:new-canvas|gpu:new-gpu',
|
|
291
|
+
'user-agent' => 'Test UA'
|
|
292
|
+
]
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
$indicators = RequestUtils::getBehavioralIndicators($context, $deviceData);
|
|
296
|
+
$this->assertEquals(2, $deviceData['rapidChangeCount']);
|
|
297
|
+
$this->assertGreaterThan(0, $indicators['rotationScore']);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public function testGetRequestPatternScoreWeightedSubscores(): void
|
|
301
|
+
{
|
|
302
|
+
$context = $this->createRequestContext(['path' => '/search']);
|
|
303
|
+
$deviceData = [
|
|
304
|
+
'requestHistory' => [],
|
|
305
|
+
'timingHistory' => [100, 100, 100, 100, 100, 100], // stdDev = 0
|
|
306
|
+
'lastPatternScore' => 0.0
|
|
307
|
+
];
|
|
308
|
+
$patternConfig = [
|
|
309
|
+
'minSamples' => 5,
|
|
310
|
+
'regularityThreshold' => 50,
|
|
311
|
+
'benfordThreshold' => 0.15,
|
|
312
|
+
'patternWeight' => 80,
|
|
313
|
+
'decayFactor' => 0.9,
|
|
314
|
+
'inactivityReset' => 5000,
|
|
315
|
+
'regularityRatio' => 0.4,
|
|
316
|
+
'benfordRatio' => 0.3,
|
|
317
|
+
'enumerationRatio' => 0.3
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
$result = RequestUtils::getRequestPatternScore($context, $deviceData, $patternConfig);
|
|
321
|
+
// regularityScore = 1.0. regularityRatio = 0.4.
|
|
322
|
+
// instantScore = 1.0 * 0.4 * 80 = 32.
|
|
323
|
+
$this->assertEquals(32.0, $result['requestPatternScore']);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
public function testGetBotnetClusterScoreCalculations(): void
|
|
327
|
+
{
|
|
328
|
+
$stableFpHash = 'test-stable-hash';
|
|
329
|
+
|
|
330
|
+
$context1 = $this->createRequestContext(['clientIp' => '192.168.1.1']);
|
|
331
|
+
$score = RequestUtils::getBotnetClusterScore($context1, $stableFpHash);
|
|
332
|
+
$this->assertEquals(0.0, $score['botnetClusterScore']);
|
|
333
|
+
|
|
334
|
+
RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.2']), $stableFpHash);
|
|
335
|
+
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.3']), $stableFpHash);
|
|
336
|
+
$this->assertEquals(50.3, $score['botnetClusterScore']);
|
|
337
|
+
|
|
338
|
+
RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.4']), $stableFpHash);
|
|
339
|
+
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.5']), $stableFpHash);
|
|
340
|
+
$this->assertEquals(75.3, $score['botnetClusterScore']);
|
|
341
|
+
|
|
342
|
+
for ($i = 6; $i <= 10; $i++) {
|
|
343
|
+
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => "192.168.1.{$i}"]), $stableFpHash);
|
|
344
|
+
}
|
|
345
|
+
$this->assertEquals(95.7, $score['botnetClusterScore']);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
public function testRealWorldConsoleBotnetClustering(): void
|
|
349
|
+
{
|
|
350
|
+
$ps4Headers = [
|
|
351
|
+
'user-agent' => 'Mozilla/5.0 (PlayStation 4 11.50) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.50 Safari/605.1.15',
|
|
352
|
+
'x-ja3-hash' => '76993ef93bf89104037599723ab9f201',
|
|
353
|
+
'x-ja4-hash' => 't13d1516h2_8daaf6152771_390237aa04be',
|
|
354
|
+
'x-http2-fingerprint' => '1:65536;3:1000;4:6291456;6:65536',
|
|
355
|
+
'x-tcp-fingerprint' => '64240:128:1:mss,nop,ws,nop,nop,sok:df:0'
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
for ($i = 1; $i <= 10; $i++) {
|
|
359
|
+
$context = $this->createRequestContext([
|
|
360
|
+
'clientIp' => "185.15.20.{$i}",
|
|
361
|
+
'headers' => array_merge($ps4Headers, [
|
|
362
|
+
'cookie_keys' => "session_id=fake_sess_{$i}"
|
|
363
|
+
])
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
$currentHash = RequestUtils::getCompositeDeviceHash($context);
|
|
367
|
+
$stableFp = RequestUtils::extractStablePart($currentHash);
|
|
368
|
+
$stableFpHash = FingerprintBuilder::cyrb53($stableFp);
|
|
369
|
+
|
|
370
|
+
$score = RequestUtils::getBotnetClusterScore($context, $stableFpHash);
|
|
371
|
+
|
|
372
|
+
if ($i === 1) {
|
|
373
|
+
$this->assertEquals(0.0, $score['botnetClusterScore']);
|
|
374
|
+
} elseif ($i === 2) {
|
|
375
|
+
$this->assertEquals(29.5, $score['botnetClusterScore']);
|
|
376
|
+
} elseif ($i === 3) {
|
|
377
|
+
$this->assertEquals(50.3, $score['botnetClusterScore']);
|
|
378
|
+
} elseif ($i === 4) {
|
|
379
|
+
$this->assertEquals(65.0, $score['botnetClusterScore']);
|
|
380
|
+
} elseif ($i === 5) {
|
|
381
|
+
$this->assertEquals(75.3, $score['botnetClusterScore']);
|
|
382
|
+
} elseif ($i === 10) {
|
|
383
|
+
$this->assertEquals(95.7, $score['botnetClusterScore']);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
254
387
|
}
|