@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.
- package/CHANGELOG.md +29 -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 +190 -16
- 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 +124 -10
- 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 +57 -11
- 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 +44 -17
- 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 +38 -0
- 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/BlockList.php +99 -99
- 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 +554 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
- package/src/php/WordPress/package.php +218 -0
- package/src/php/bin/auto-tune.php +118 -118
|
@@ -1,472 +1,472 @@
|
|
|
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 testGetCrossLayerInconsistencyViewportExceedsScreen(): void
|
|
39
|
-
{
|
|
40
|
-
$scrHash = FingerprintBuilder::cyrb53("1920x1080_24");
|
|
41
|
-
$context = $this->createRequestContext([
|
|
42
|
-
'headers' => [
|
|
43
|
-
'x-device-fingerprint' => "scr:{$scrHash}",
|
|
44
|
-
'sec-ch-viewport-width' => '2560'
|
|
45
|
-
]
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
49
|
-
$this->assertEquals(20.0, $score['crossLayerInconsistencyScore']);
|
|
50
|
-
}
|
|
51
|
-
public function testGetRenderingAnomalyScoreEmpty(): void
|
|
52
|
-
{
|
|
53
|
-
$context = $this->createRequestContext();
|
|
54
|
-
$res = RequestUtils::getRenderingAnomalyScore($context);
|
|
55
|
-
$this->assertEquals(0.0, $res['renderingAnomalyScore']);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public function testGetRenderingAnomalyScoreSpoofed(): void
|
|
59
|
-
{
|
|
60
|
-
$metrics = [
|
|
61
|
-
'rendering' => [
|
|
62
|
-
'fps' => 60,
|
|
63
|
-
'jitter' => 12.5,
|
|
64
|
-
'offscreenAnom' => false
|
|
65
|
-
]
|
|
66
|
-
];
|
|
67
|
-
$context = $this->createRequestContext([
|
|
68
|
-
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
69
|
-
]);
|
|
70
|
-
$res = RequestUtils::getRenderingAnomalyScore($context);
|
|
71
|
-
$this->assertEquals(65.0, $res['renderingAnomalyScore']);
|
|
72
|
-
}
|
|
73
|
-
public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
|
|
74
|
-
{
|
|
75
|
-
$context = $this->createRequestContext([
|
|
76
|
-
'headers' => ['x-behavior-metrics' => json_encode([])]
|
|
77
|
-
]);
|
|
78
|
-
$result = RequestUtils::getClickVarianceScore($context);
|
|
79
|
-
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
public function testGetClickVarianceScoreReturnsZeroForInsufficientClicks(): void
|
|
83
|
-
{
|
|
84
|
-
$metrics = [
|
|
85
|
-
'clicksHistory' => [
|
|
86
|
-
['x' => 10, 'y' => 10, 'targetId' => 'hash1'],
|
|
87
|
-
['x' => 11, 'y' => 11, 'targetId' => 'hash1'],
|
|
88
|
-
['x' => 100, 'y' => 100, 'targetId' => 'hash2']
|
|
89
|
-
]
|
|
90
|
-
];
|
|
91
|
-
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
92
|
-
$result = RequestUtils::getClickVarianceScore($context);
|
|
93
|
-
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
public function testGetClickVarianceScoreReturnsHighScoreForLowVariance(): void
|
|
97
|
-
{
|
|
98
|
-
$metrics = [
|
|
99
|
-
'clicksHistory' => [
|
|
100
|
-
['x' => 100, 'y' => 100, 'targetId' => 'hash1'],
|
|
101
|
-
['x' => 100.1, 'y' => 100.2, 'targetId' => 'hash1'],
|
|
102
|
-
['x' => 99.9, 'y' => 99.8, 'targetId' => 'hash1']
|
|
103
|
-
]
|
|
104
|
-
];
|
|
105
|
-
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
106
|
-
$result = RequestUtils::getClickVarianceScore($context);
|
|
107
|
-
$this->assertGreaterThan(90, $result['clickVarianceScore']);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
public function testGetClickVarianceScoreReturnsLowScoreForHighVariance(): void
|
|
111
|
-
{
|
|
112
|
-
$metrics = [
|
|
113
|
-
'clicksHistory' => [
|
|
114
|
-
['x' => 105, 'y' => 110, 'targetId' => 'hash1'],
|
|
115
|
-
['x' => 98, 'y' => 102, 'targetId' => 'hash1'],
|
|
116
|
-
['x' => 112, 'y' => 95, 'targetId' => 'hash1']
|
|
117
|
-
]
|
|
118
|
-
];
|
|
119
|
-
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
120
|
-
$result = RequestUtils::getClickVarianceScore($context);
|
|
121
|
-
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
public function testSanitizeTrafficDataFiltersSybilAttacks(): void
|
|
125
|
-
{
|
|
126
|
-
$trafficData = [];
|
|
127
|
-
// Ajout de 100 logs provenant d'un attaquant (Sybil)
|
|
128
|
-
for ($i = 0; $i < 100; $i++) {
|
|
129
|
-
$trafficData[] = ['deviceId' => 'attacker_device', 'type' => 'trap_triggered'];
|
|
130
|
-
}
|
|
131
|
-
// Ajout de 10 logs d'utilisateurs légitimes distincts
|
|
132
|
-
for ($i = 0; $i < 10; $i++) {
|
|
133
|
-
$trafficData[] = ['deviceId' => "legit_device_{$i}", 'type' => 'challenge_solved'];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
$sanitized = RequestUtils::sanitizeTrafficData($trafficData);
|
|
137
|
-
|
|
138
|
-
$attackerLogs = array_filter($sanitized, fn($log) => $log['deviceId'] === 'attacker_device');
|
|
139
|
-
|
|
140
|
-
// Total de 110 logs. 2% de 110 est 2.2 -> max(3, 2) = 3 logs maximum autorisés pour l'attaquant.
|
|
141
|
-
$this->assertLessThanOrEqual(3, count($attackerLogs));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
public function testSanitizeTrafficDataFiltersHardwareClusters(): void
|
|
145
|
-
{
|
|
146
|
-
$trafficData = [];
|
|
147
|
-
// Simulated attacker rotating deviceId and IP, but keeping identical hardware fingerprint
|
|
148
|
-
for ($i = 0; $i < 100; $i++) {
|
|
149
|
-
$trafficData[] = [
|
|
150
|
-
'deviceId' => "attacker_device_{$i}",
|
|
151
|
-
'clientIp' => "192.168.1.{$i}",
|
|
152
|
-
'type' => 'trap_triggered',
|
|
153
|
-
'fingerprint' => 'gpu:heavy_nvidia_hash|cvs:canvas_hash_1'
|
|
154
|
-
];
|
|
155
|
-
}
|
|
156
|
-
// Legit users with unique hardware fingerprints
|
|
157
|
-
for ($i = 0; $i < 10; $i++) {
|
|
158
|
-
$trafficData[] = [
|
|
159
|
-
'deviceId' => "legit_device_{$i}",
|
|
160
|
-
'clientIp' => "10.0.0.{$i}",
|
|
161
|
-
'type' => 'challenge_solved',
|
|
162
|
-
'fingerprint' => "gpu:legit_gpu_{$i}|cvs:legit_cvs_{$i}"
|
|
163
|
-
];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
$sanitized = RequestUtils::sanitizeTrafficData($trafficData);
|
|
167
|
-
$attackerLogs = array_filter($sanitized, fn($log) => str_starts_with($log['deviceId'], 'attacker_device_'));
|
|
168
|
-
|
|
169
|
-
// Max limit of 2% of 110 total = 2.2 -> max(3, 2) = 3 logs allowed for this hardware cluster
|
|
170
|
-
$this->assertLessThanOrEqual(3, count($attackerLogs));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
public function testChallengePayloadSigningAndVerification(): void
|
|
174
|
-
{
|
|
175
|
-
$secret = 'test-fallback-dev-secret-32-chars-minimum';
|
|
176
|
-
$clientIp = '203.0.113.42';
|
|
177
|
-
$payload = [
|
|
178
|
-
'clientSecret' => 'some_client_secret',
|
|
179
|
-
'cpuTarget' => '00000000ffffffff',
|
|
180
|
-
'fingerprint' => 'os:hash|gpu:hash2',
|
|
181
|
-
'memDifficulty' => '16',
|
|
182
|
-
'originalPath' => '/submit',
|
|
183
|
-
];
|
|
184
|
-
|
|
185
|
-
// 1. Cas nominal : Signature et vérification réussies
|
|
186
|
-
$signature = RequestUtils::signChallengePayload($secret, $payload, $clientIp);
|
|
187
|
-
$this->assertNotEmpty($signature);
|
|
188
|
-
|
|
189
|
-
$payloadWithSig = $payload;
|
|
190
|
-
$payloadWithSig['signature'] = $signature;
|
|
191
|
-
|
|
192
|
-
$isValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, $clientIp);
|
|
193
|
-
$this->assertTrue($isValid, "La signature valide doit être acceptée.");
|
|
194
|
-
|
|
195
|
-
// 2. Détection de modification (tampering) sur cpuTarget
|
|
196
|
-
$tamperedPayload = $payloadWithSig;
|
|
197
|
-
$tamperedPayload['cpuTarget'] = 'ffffffffffffffff'; // Tentative de baisse de la difficulté
|
|
198
|
-
|
|
199
|
-
$isTamperedValid = RequestUtils::verifyChallengePayload($secret, $tamperedPayload, $clientIp);
|
|
200
|
-
$this->assertFalse($isTamperedValid, "Un payload modifié doit être rejeté.");
|
|
201
|
-
|
|
202
|
-
// 3. Détection de modification sur le fingerprint
|
|
203
|
-
$tamperedFpPayload = $payloadWithSig;
|
|
204
|
-
$tamperedFpPayload['fingerprint'] = 'os:another_hash|gpu:hash2';
|
|
205
|
-
|
|
206
|
-
$isTamperedFpValid = RequestUtils::verifyChallengePayload($secret, $tamperedFpPayload, $clientIp);
|
|
207
|
-
$this->assertFalse($isTamperedFpValid, "Un fingerprint modifié doit être rejeté.");
|
|
208
|
-
|
|
209
|
-
// 4. Détection d'usurpation d'adresse IP (IP mismatch)
|
|
210
|
-
$isIpMismatchValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, '198.51.100.1');
|
|
211
|
-
$this->assertFalse($isIpMismatchValid, "Le payload ne doit pas être valide pour une autre adresse IP.");
|
|
212
|
-
|
|
213
|
-
// 5. Absence de signature
|
|
214
|
-
$isMissingSigValid = RequestUtils::verifyChallengePayload($secret, $payload, $clientIp);
|
|
215
|
-
$this->assertFalse($isMissingSigValid, "Un payload sans signature doit être rejeté.");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
public function testGetBehaviorScoreReturnsProperScores(): void
|
|
219
|
-
{
|
|
220
|
-
$contextEmpty = $this->createRequestContext();
|
|
221
|
-
$scoreEmpty = RequestUtils::getBehaviorScore($contextEmpty);
|
|
222
|
-
$this->assertEquals(0.0, $scoreEmpty['behaviorScore']);
|
|
223
|
-
|
|
224
|
-
$metricsHoneypot = ['honeypotInteraction' => true];
|
|
225
|
-
$contextHoneypot = $this->createRequestContext([
|
|
226
|
-
'headers' => ['x-behavior-metrics' => json_encode($metricsHoneypot)]
|
|
227
|
-
]);
|
|
228
|
-
$scoreHoneypot = RequestUtils::getBehaviorScore($contextHoneypot);
|
|
229
|
-
$this->assertEquals(100.0, $scoreHoneypot['behaviorScore']);
|
|
230
|
-
|
|
231
|
-
$metricsNoActivity = ['honeypotInteraction' => false, 'mouseMovementsHistory' => [], 'keystrokeLatency' => 0];
|
|
232
|
-
$contextNoActivity = $this->createRequestContext([
|
|
233
|
-
'headers' => ['x-behavior-metrics' => json_encode($metricsNoActivity)]
|
|
234
|
-
]);
|
|
235
|
-
$scoreNoActivity = RequestUtils::getBehaviorScore($contextNoActivity);
|
|
236
|
-
$this->assertEquals(40.0, $scoreNoActivity['behaviorScore']);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
public function testGetBehaviorScoreWithBotLikeKeystrokeDynamics(): void
|
|
240
|
-
{
|
|
241
|
-
$metrics = [
|
|
242
|
-
'honeypotInteraction' => false,
|
|
243
|
-
'keystrokeLatency' => 100.0,
|
|
244
|
-
'keystrokeDwellTimes' => [50, 50, 50, 50, 50],
|
|
245
|
-
'keystrokeFlightTimes' => [
|
|
246
|
-
['digraph' => 'ab', 'time' => 100],
|
|
247
|
-
['digraph' => 'bc', 'time' => 100],
|
|
248
|
-
['digraph' => 'cd', 'time' => 100],
|
|
249
|
-
['digraph' => 'de', 'time' => 100],
|
|
250
|
-
['digraph' => 'ef', 'time' => 100]
|
|
251
|
-
]
|
|
252
|
-
];
|
|
253
|
-
$context = $this->createRequestContext([
|
|
254
|
-
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
255
|
-
]);
|
|
256
|
-
$score = RequestUtils::getBehaviorScore($context);
|
|
257
|
-
$this->assertGreaterThanOrEqual(70.0, $score['behaviorScore']);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
public function testGetBehaviorScoreWithBotLikeTouchMovements(): void
|
|
261
|
-
{
|
|
262
|
-
$metrics = [
|
|
263
|
-
'honeypotInteraction' => false,
|
|
264
|
-
'touchMovementsHistory' => [
|
|
265
|
-
['x' => 50, 'y' => 50, 't' => 1, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
266
|
-
['x' => 55, 'y' => 55, 't' => 100, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
267
|
-
['x' => 60, 'y' => 60, 't' => 200, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
268
|
-
['x' => 65, 'y' => 65, 't' => 300, 'p' => 0.5, 'r' => 10, 'num' => 1]
|
|
269
|
-
]
|
|
270
|
-
];
|
|
271
|
-
$context = $this->createRequestContext([
|
|
272
|
-
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
273
|
-
]);
|
|
274
|
-
$score = RequestUtils::getBehaviorScore($context);
|
|
275
|
-
$this->assertGreaterThan(60.0, $score['behaviorScore']);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
public function testGetBehaviorScoreWithHumanLikeTouchMovements(): void
|
|
279
|
-
{
|
|
280
|
-
$metrics = [
|
|
281
|
-
'honeypotInteraction' => false,
|
|
282
|
-
'touchMovementsHistory' => [
|
|
283
|
-
['x' => 50, 'y' => 50, 't' => 1, 'p' => 0.45, 'r' => 8.5, 'num' => 1],
|
|
284
|
-
['x' => 60, 'y' => 52, 't' => 100, 'p' => 0.52, 'r' => 9.1, 'num' => 1],
|
|
285
|
-
['x' => 72, 'y' => 60, 't' => 200, 'p' => 0.49, 'r' => 8.8, 'num' => 1],
|
|
286
|
-
['x' => 80, 'y' => 80, 't' => 300, 'p' => 0.41, 'r' => 8.2, 'num' => 1]
|
|
287
|
-
]
|
|
288
|
-
];
|
|
289
|
-
$context = $this->createRequestContext([
|
|
290
|
-
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
291
|
-
]);
|
|
292
|
-
$score = RequestUtils::getBehaviorScore($context);
|
|
293
|
-
$this->assertLessThan(30.0, $score['behaviorScore']);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
public function testGetTimeInconsistencyScore(): void
|
|
297
|
-
{
|
|
298
|
-
$requestTimestamp = time() * 1000;
|
|
299
|
-
|
|
300
|
-
$metricsNormal = ['clientTimestamp' => $requestTimestamp - 100];
|
|
301
|
-
$contextNormal = $this->createRequestContext([
|
|
302
|
-
'headers' => ['x-behavior-metrics' => json_encode($metricsNormal)]
|
|
303
|
-
]);
|
|
304
|
-
$contextNormal->requestTimestamp = $requestTimestamp;
|
|
305
|
-
$scoreNormal = RequestUtils::getTimeInconsistencyScore($contextNormal);
|
|
306
|
-
$this->assertEquals(0.0, $scoreNormal['timeInconsistencyScore']);
|
|
307
|
-
|
|
308
|
-
$metricsReplay = ['clientTimestamp' => $requestTimestamp - 10000];
|
|
309
|
-
$contextReplay = $this->createRequestContext([
|
|
310
|
-
'headers' => ['x-behavior-metrics' => json_encode($metricsReplay)]
|
|
311
|
-
]);
|
|
312
|
-
$contextReplay->requestTimestamp = $requestTimestamp;
|
|
313
|
-
$scoreReplay = RequestUtils::getTimeInconsistencyScore($contextReplay);
|
|
314
|
-
$this->assertGreaterThan(0.0, $scoreReplay['timeInconsistencyScore']);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
public function testGetCrossLayerInconsistencyOSMismatch(): void
|
|
318
|
-
{
|
|
319
|
-
$windowsHash = FingerprintBuilder::cyrb53("Windows");
|
|
320
|
-
$context = $this->createRequestContext([
|
|
321
|
-
'headers' => [
|
|
322
|
-
'x-device-fingerprint' => "os:{$windowsHash}",
|
|
323
|
-
'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15'
|
|
324
|
-
]
|
|
325
|
-
]);
|
|
326
|
-
|
|
327
|
-
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
328
|
-
$this->assertEquals(50.0, $score['crossLayerInconsistencyScore']);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
public function testGetBotScoreDetections(): void
|
|
332
|
-
{
|
|
333
|
-
$contextBot = $this->createRequestContext([
|
|
334
|
-
'headers' => ['x-device-fingerprint' => 'ua:123|bot:true']
|
|
335
|
-
]);
|
|
336
|
-
$scoreBot = RequestUtils::getBotScore($contextBot);
|
|
337
|
-
$this->assertEquals(100.0, $scoreBot['botScore']);
|
|
338
|
-
|
|
339
|
-
$contextCdp = $this->createRequestContext([
|
|
340
|
-
'headers' => ['x-device-fingerprint' => 'ua:123|cdp:true']
|
|
341
|
-
]);
|
|
342
|
-
$scoreCdp = RequestUtils::getBotScore($contextCdp);
|
|
343
|
-
$this->assertEquals(100.0, $scoreCdp['botScore']);
|
|
344
|
-
|
|
345
|
-
$contextClean = $this->createRequestContext([
|
|
346
|
-
'headers' => ['x-device-fingerprint' => 'ua:123|os:456']
|
|
347
|
-
]);
|
|
348
|
-
$scoreClean = RequestUtils::getBotScore($contextClean);
|
|
349
|
-
$this->assertEquals(0.0, $scoreClean['botScore']);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
public function testGetHeaderAnomaliesFirefoxTE(): void
|
|
353
|
-
{
|
|
354
|
-
$contextFirefoxNoTe = $this->createRequestContext([
|
|
355
|
-
'headers' => [
|
|
356
|
-
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
|
357
|
-
'accept-language' => 'en-US,en;q=0.9',
|
|
358
|
-
]
|
|
359
|
-
]);
|
|
360
|
-
$scoreFirefoxNoTe = RequestUtils::getHeaderAnomalies($contextFirefoxNoTe);
|
|
361
|
-
$this->assertEquals(30.0, $scoreFirefoxNoTe['headerAnomalyScore']);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
public function testGetBehavioralIndicatorsRotationAndHistory(): void
|
|
365
|
-
{
|
|
366
|
-
$deviceData = [
|
|
367
|
-
'ips' => ['127.0.0.1'],
|
|
368
|
-
'lastFpHash' => 'cvs:original-canvas|gpu:original-gpu',
|
|
369
|
-
'lastChangeTimestamp' => (time() * 1000) - 500,
|
|
370
|
-
'rapidChangeCount' => 1
|
|
371
|
-
];
|
|
372
|
-
|
|
373
|
-
$context = $this->createRequestContext([
|
|
374
|
-
'headers' => [
|
|
375
|
-
'x-device-fingerprint' => 'cvs:new-canvas|gpu:new-gpu',
|
|
376
|
-
'user-agent' => 'Test UA'
|
|
377
|
-
]
|
|
378
|
-
]);
|
|
379
|
-
|
|
380
|
-
$indicators = RequestUtils::getBehavioralIndicators($context, $deviceData);
|
|
381
|
-
$this->assertEquals(2, $deviceData['rapidChangeCount']);
|
|
382
|
-
$this->assertGreaterThan(0, $indicators['rotationScore']);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
public function testGetRequestPatternScoreWeightedSubscores(): void
|
|
386
|
-
{
|
|
387
|
-
$context = $this->createRequestContext(['path' => '/search']);
|
|
388
|
-
$deviceData = [
|
|
389
|
-
'requestHistory' => [],
|
|
390
|
-
'timingHistory' => [100, 100, 100, 100, 100, 100], // stdDev = 0
|
|
391
|
-
'lastPatternScore' => 0.0
|
|
392
|
-
];
|
|
393
|
-
$patternConfig = [
|
|
394
|
-
'minSamples' => 5,
|
|
395
|
-
'regularityThreshold' => 50,
|
|
396
|
-
'benfordThreshold' => 0.15,
|
|
397
|
-
'patternWeight' => 80,
|
|
398
|
-
'decayFactor' => 0.9,
|
|
399
|
-
'inactivityReset' => 5000,
|
|
400
|
-
'regularityRatio' => 0.4,
|
|
401
|
-
'benfordRatio' => 0.3,
|
|
402
|
-
'enumerationRatio' => 0.3
|
|
403
|
-
];
|
|
404
|
-
|
|
405
|
-
$result = RequestUtils::getRequestPatternScore($context, $deviceData, $patternConfig);
|
|
406
|
-
// regularityScore = 1.0. regularityRatio = 0.4.
|
|
407
|
-
// instantScore = 1.0 * 0.4 * 80 = 32.
|
|
408
|
-
$this->assertEquals(32.0, $result['requestPatternScore']);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
public function testGetBotnetClusterScoreCalculations(): void
|
|
412
|
-
{
|
|
413
|
-
$stableFpHash = 'test-stable-hash';
|
|
414
|
-
|
|
415
|
-
$context1 = $this->createRequestContext(['clientIp' => '192.168.1.1']);
|
|
416
|
-
$score = RequestUtils::getBotnetClusterScore($context1, $stableFpHash);
|
|
417
|
-
$this->assertEquals(0.0, $score['botnetClusterScore']);
|
|
418
|
-
|
|
419
|
-
RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.2']), $stableFpHash);
|
|
420
|
-
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.3']), $stableFpHash);
|
|
421
|
-
$this->assertEquals(30.2, $score['botnetClusterScore']);
|
|
422
|
-
|
|
423
|
-
RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.4']), $stableFpHash);
|
|
424
|
-
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.5']), $stableFpHash);
|
|
425
|
-
$this->assertEquals(45.2, $score['botnetClusterScore']);
|
|
426
|
-
|
|
427
|
-
for ($i = 6; $i <= 10; $i++) {
|
|
428
|
-
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => "192.168.1.{$i}"]), $stableFpHash);
|
|
429
|
-
}
|
|
430
|
-
$this->assertEquals(57.4, $score['botnetClusterScore']);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
public function testRealWorldConsoleBotnetClustering(): void
|
|
434
|
-
{
|
|
435
|
-
$ps4Headers = [
|
|
436
|
-
'user-agent' => 'Mozilla/5.0 (PlayStation 4 11.50) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.50 Safari/605.1.15',
|
|
437
|
-
'x-ja3-hash' => '76993ef93bf89104037599723ab9f201',
|
|
438
|
-
'x-ja4-hash' => 't13d1516h2_8daaf6152771_390237aa04be',
|
|
439
|
-
'x-http2-fingerprint' => '1:65536;3:1000;4:6291456;6:65536',
|
|
440
|
-
'x-tcp-fingerprint' => '64240:128:1:mss,nop,ws,nop,nop,sok:df:0'
|
|
441
|
-
];
|
|
442
|
-
|
|
443
|
-
for ($i = 1; $i <= 10; $i++) {
|
|
444
|
-
$context = $this->createRequestContext([
|
|
445
|
-
'clientIp' => "185.15.20.{$i}",
|
|
446
|
-
'headers' => array_merge($ps4Headers, [
|
|
447
|
-
'cookie_keys' => "session_id=fake_sess_{$i}"
|
|
448
|
-
])
|
|
449
|
-
]);
|
|
450
|
-
|
|
451
|
-
$currentHash = RequestUtils::getCompositeDeviceHash($context);
|
|
452
|
-
$stableFp = RequestUtils::extractStablePart($currentHash);
|
|
453
|
-
$stableFpHash = FingerprintBuilder::cyrb53($stableFp);
|
|
454
|
-
|
|
455
|
-
$score = RequestUtils::getBotnetClusterScore($context, $stableFpHash);
|
|
456
|
-
|
|
457
|
-
if ($i === 1) {
|
|
458
|
-
$this->assertEquals(0.0, $score['botnetClusterScore'], "Expected score for 1 IP");
|
|
459
|
-
} else if ($i === 2) {
|
|
460
|
-
$this->assertEquals(17.7, $score['botnetClusterScore'], "Expected score for 2 IPs");
|
|
461
|
-
} else if ($i === 3) {
|
|
462
|
-
$this->assertEquals(30.2, $score['botnetClusterScore'], "Expected score for 3 IPs");
|
|
463
|
-
} else if ($i === 4) {
|
|
464
|
-
$this->assertEquals(39.0, $score['botnetClusterScore'], "Expected score for 4 IPs");
|
|
465
|
-
} else if ($i === 5) {
|
|
466
|
-
$this->assertEquals(45.2, $score['botnetClusterScore'], "Expected score for 5 IPs");
|
|
467
|
-
} else if ($i === 10) {
|
|
468
|
-
$this->assertEquals(57.4, $score['botnetClusterScore'], "Expected score for 10 IPs");
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
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 testGetCrossLayerInconsistencyViewportExceedsScreen(): void
|
|
39
|
+
{
|
|
40
|
+
$scrHash = FingerprintBuilder::cyrb53("1920x1080_24");
|
|
41
|
+
$context = $this->createRequestContext([
|
|
42
|
+
'headers' => [
|
|
43
|
+
'x-device-fingerprint' => "scr:{$scrHash}",
|
|
44
|
+
'sec-ch-viewport-width' => '2560'
|
|
45
|
+
]
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
49
|
+
$this->assertEquals(20.0, $score['crossLayerInconsistencyScore']);
|
|
50
|
+
}
|
|
51
|
+
public function testGetRenderingAnomalyScoreEmpty(): void
|
|
52
|
+
{
|
|
53
|
+
$context = $this->createRequestContext();
|
|
54
|
+
$res = RequestUtils::getRenderingAnomalyScore($context);
|
|
55
|
+
$this->assertEquals(0.0, $res['renderingAnomalyScore']);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public function testGetRenderingAnomalyScoreSpoofed(): void
|
|
59
|
+
{
|
|
60
|
+
$metrics = [
|
|
61
|
+
'rendering' => [
|
|
62
|
+
'fps' => 60,
|
|
63
|
+
'jitter' => 12.5,
|
|
64
|
+
'offscreenAnom' => false
|
|
65
|
+
]
|
|
66
|
+
];
|
|
67
|
+
$context = $this->createRequestContext([
|
|
68
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
69
|
+
]);
|
|
70
|
+
$res = RequestUtils::getRenderingAnomalyScore($context);
|
|
71
|
+
$this->assertEquals(65.0, $res['renderingAnomalyScore']);
|
|
72
|
+
}
|
|
73
|
+
public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
|
|
74
|
+
{
|
|
75
|
+
$context = $this->createRequestContext([
|
|
76
|
+
'headers' => ['x-behavior-metrics' => json_encode([])]
|
|
77
|
+
]);
|
|
78
|
+
$result = RequestUtils::getClickVarianceScore($context);
|
|
79
|
+
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public function testGetClickVarianceScoreReturnsZeroForInsufficientClicks(): void
|
|
83
|
+
{
|
|
84
|
+
$metrics = [
|
|
85
|
+
'clicksHistory' => [
|
|
86
|
+
['x' => 10, 'y' => 10, 'targetId' => 'hash1'],
|
|
87
|
+
['x' => 11, 'y' => 11, 'targetId' => 'hash1'],
|
|
88
|
+
['x' => 100, 'y' => 100, 'targetId' => 'hash2']
|
|
89
|
+
]
|
|
90
|
+
];
|
|
91
|
+
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
92
|
+
$result = RequestUtils::getClickVarianceScore($context);
|
|
93
|
+
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public function testGetClickVarianceScoreReturnsHighScoreForLowVariance(): void
|
|
97
|
+
{
|
|
98
|
+
$metrics = [
|
|
99
|
+
'clicksHistory' => [
|
|
100
|
+
['x' => 100, 'y' => 100, 'targetId' => 'hash1'],
|
|
101
|
+
['x' => 100.1, 'y' => 100.2, 'targetId' => 'hash1'],
|
|
102
|
+
['x' => 99.9, 'y' => 99.8, 'targetId' => 'hash1']
|
|
103
|
+
]
|
|
104
|
+
];
|
|
105
|
+
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
106
|
+
$result = RequestUtils::getClickVarianceScore($context);
|
|
107
|
+
$this->assertGreaterThan(90, $result['clickVarianceScore']);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public function testGetClickVarianceScoreReturnsLowScoreForHighVariance(): void
|
|
111
|
+
{
|
|
112
|
+
$metrics = [
|
|
113
|
+
'clicksHistory' => [
|
|
114
|
+
['x' => 105, 'y' => 110, 'targetId' => 'hash1'],
|
|
115
|
+
['x' => 98, 'y' => 102, 'targetId' => 'hash1'],
|
|
116
|
+
['x' => 112, 'y' => 95, 'targetId' => 'hash1']
|
|
117
|
+
]
|
|
118
|
+
];
|
|
119
|
+
$context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
|
|
120
|
+
$result = RequestUtils::getClickVarianceScore($context);
|
|
121
|
+
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public function testSanitizeTrafficDataFiltersSybilAttacks(): void
|
|
125
|
+
{
|
|
126
|
+
$trafficData = [];
|
|
127
|
+
// Ajout de 100 logs provenant d'un attaquant (Sybil)
|
|
128
|
+
for ($i = 0; $i < 100; $i++) {
|
|
129
|
+
$trafficData[] = ['deviceId' => 'attacker_device', 'type' => 'trap_triggered'];
|
|
130
|
+
}
|
|
131
|
+
// Ajout de 10 logs d'utilisateurs légitimes distincts
|
|
132
|
+
for ($i = 0; $i < 10; $i++) {
|
|
133
|
+
$trafficData[] = ['deviceId' => "legit_device_{$i}", 'type' => 'challenge_solved'];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
$sanitized = RequestUtils::sanitizeTrafficData($trafficData);
|
|
137
|
+
|
|
138
|
+
$attackerLogs = array_filter($sanitized, fn($log) => $log['deviceId'] === 'attacker_device');
|
|
139
|
+
|
|
140
|
+
// Total de 110 logs. 2% de 110 est 2.2 -> max(3, 2) = 3 logs maximum autorisés pour l'attaquant.
|
|
141
|
+
$this->assertLessThanOrEqual(3, count($attackerLogs));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public function testSanitizeTrafficDataFiltersHardwareClusters(): void
|
|
145
|
+
{
|
|
146
|
+
$trafficData = [];
|
|
147
|
+
// Simulated attacker rotating deviceId and IP, but keeping identical hardware fingerprint
|
|
148
|
+
for ($i = 0; $i < 100; $i++) {
|
|
149
|
+
$trafficData[] = [
|
|
150
|
+
'deviceId' => "attacker_device_{$i}",
|
|
151
|
+
'clientIp' => "192.168.1.{$i}",
|
|
152
|
+
'type' => 'trap_triggered',
|
|
153
|
+
'fingerprint' => 'gpu:heavy_nvidia_hash|cvs:canvas_hash_1'
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
// Legit users with unique hardware fingerprints
|
|
157
|
+
for ($i = 0; $i < 10; $i++) {
|
|
158
|
+
$trafficData[] = [
|
|
159
|
+
'deviceId' => "legit_device_{$i}",
|
|
160
|
+
'clientIp' => "10.0.0.{$i}",
|
|
161
|
+
'type' => 'challenge_solved',
|
|
162
|
+
'fingerprint' => "gpu:legit_gpu_{$i}|cvs:legit_cvs_{$i}"
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
$sanitized = RequestUtils::sanitizeTrafficData($trafficData);
|
|
167
|
+
$attackerLogs = array_filter($sanitized, fn($log) => str_starts_with($log['deviceId'], 'attacker_device_'));
|
|
168
|
+
|
|
169
|
+
// Max limit of 2% of 110 total = 2.2 -> max(3, 2) = 3 logs allowed for this hardware cluster
|
|
170
|
+
$this->assertLessThanOrEqual(3, count($attackerLogs));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public function testChallengePayloadSigningAndVerification(): void
|
|
174
|
+
{
|
|
175
|
+
$secret = 'test-fallback-dev-secret-32-chars-minimum';
|
|
176
|
+
$clientIp = '203.0.113.42';
|
|
177
|
+
$payload = [
|
|
178
|
+
'clientSecret' => 'some_client_secret',
|
|
179
|
+
'cpuTarget' => '00000000ffffffff',
|
|
180
|
+
'fingerprint' => 'os:hash|gpu:hash2',
|
|
181
|
+
'memDifficulty' => '16',
|
|
182
|
+
'originalPath' => '/submit',
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
// 1. Cas nominal : Signature et vérification réussies
|
|
186
|
+
$signature = RequestUtils::signChallengePayload($secret, $payload, $clientIp);
|
|
187
|
+
$this->assertNotEmpty($signature);
|
|
188
|
+
|
|
189
|
+
$payloadWithSig = $payload;
|
|
190
|
+
$payloadWithSig['signature'] = $signature;
|
|
191
|
+
|
|
192
|
+
$isValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, $clientIp);
|
|
193
|
+
$this->assertTrue($isValid, "La signature valide doit être acceptée.");
|
|
194
|
+
|
|
195
|
+
// 2. Détection de modification (tampering) sur cpuTarget
|
|
196
|
+
$tamperedPayload = $payloadWithSig;
|
|
197
|
+
$tamperedPayload['cpuTarget'] = 'ffffffffffffffff'; // Tentative de baisse de la difficulté
|
|
198
|
+
|
|
199
|
+
$isTamperedValid = RequestUtils::verifyChallengePayload($secret, $tamperedPayload, $clientIp);
|
|
200
|
+
$this->assertFalse($isTamperedValid, "Un payload modifié doit être rejeté.");
|
|
201
|
+
|
|
202
|
+
// 3. Détection de modification sur le fingerprint
|
|
203
|
+
$tamperedFpPayload = $payloadWithSig;
|
|
204
|
+
$tamperedFpPayload['fingerprint'] = 'os:another_hash|gpu:hash2';
|
|
205
|
+
|
|
206
|
+
$isTamperedFpValid = RequestUtils::verifyChallengePayload($secret, $tamperedFpPayload, $clientIp);
|
|
207
|
+
$this->assertFalse($isTamperedFpValid, "Un fingerprint modifié doit être rejeté.");
|
|
208
|
+
|
|
209
|
+
// 4. Détection d'usurpation d'adresse IP (IP mismatch)
|
|
210
|
+
$isIpMismatchValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, '198.51.100.1');
|
|
211
|
+
$this->assertFalse($isIpMismatchValid, "Le payload ne doit pas être valide pour une autre adresse IP.");
|
|
212
|
+
|
|
213
|
+
// 5. Absence de signature
|
|
214
|
+
$isMissingSigValid = RequestUtils::verifyChallengePayload($secret, $payload, $clientIp);
|
|
215
|
+
$this->assertFalse($isMissingSigValid, "Un payload sans signature doit être rejeté.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public function testGetBehaviorScoreReturnsProperScores(): void
|
|
219
|
+
{
|
|
220
|
+
$contextEmpty = $this->createRequestContext();
|
|
221
|
+
$scoreEmpty = RequestUtils::getBehaviorScore($contextEmpty);
|
|
222
|
+
$this->assertEquals(0.0, $scoreEmpty['behaviorScore']);
|
|
223
|
+
|
|
224
|
+
$metricsHoneypot = ['honeypotInteraction' => true];
|
|
225
|
+
$contextHoneypot = $this->createRequestContext([
|
|
226
|
+
'headers' => ['x-behavior-metrics' => json_encode($metricsHoneypot)]
|
|
227
|
+
]);
|
|
228
|
+
$scoreHoneypot = RequestUtils::getBehaviorScore($contextHoneypot);
|
|
229
|
+
$this->assertEquals(100.0, $scoreHoneypot['behaviorScore']);
|
|
230
|
+
|
|
231
|
+
$metricsNoActivity = ['honeypotInteraction' => false, 'mouseMovementsHistory' => [], 'keystrokeLatency' => 0];
|
|
232
|
+
$contextNoActivity = $this->createRequestContext([
|
|
233
|
+
'headers' => ['x-behavior-metrics' => json_encode($metricsNoActivity)]
|
|
234
|
+
]);
|
|
235
|
+
$scoreNoActivity = RequestUtils::getBehaviorScore($contextNoActivity);
|
|
236
|
+
$this->assertEquals(40.0, $scoreNoActivity['behaviorScore']);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public function testGetBehaviorScoreWithBotLikeKeystrokeDynamics(): void
|
|
240
|
+
{
|
|
241
|
+
$metrics = [
|
|
242
|
+
'honeypotInteraction' => false,
|
|
243
|
+
'keystrokeLatency' => 100.0,
|
|
244
|
+
'keystrokeDwellTimes' => [50, 50, 50, 50, 50],
|
|
245
|
+
'keystrokeFlightTimes' => [
|
|
246
|
+
['digraph' => 'ab', 'time' => 100],
|
|
247
|
+
['digraph' => 'bc', 'time' => 100],
|
|
248
|
+
['digraph' => 'cd', 'time' => 100],
|
|
249
|
+
['digraph' => 'de', 'time' => 100],
|
|
250
|
+
['digraph' => 'ef', 'time' => 100]
|
|
251
|
+
]
|
|
252
|
+
];
|
|
253
|
+
$context = $this->createRequestContext([
|
|
254
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
255
|
+
]);
|
|
256
|
+
$score = RequestUtils::getBehaviorScore($context);
|
|
257
|
+
$this->assertGreaterThanOrEqual(70.0, $score['behaviorScore']);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public function testGetBehaviorScoreWithBotLikeTouchMovements(): void
|
|
261
|
+
{
|
|
262
|
+
$metrics = [
|
|
263
|
+
'honeypotInteraction' => false,
|
|
264
|
+
'touchMovementsHistory' => [
|
|
265
|
+
['x' => 50, 'y' => 50, 't' => 1, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
266
|
+
['x' => 55, 'y' => 55, 't' => 100, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
267
|
+
['x' => 60, 'y' => 60, 't' => 200, 'p' => 0.5, 'r' => 10, 'num' => 1],
|
|
268
|
+
['x' => 65, 'y' => 65, 't' => 300, 'p' => 0.5, 'r' => 10, 'num' => 1]
|
|
269
|
+
]
|
|
270
|
+
];
|
|
271
|
+
$context = $this->createRequestContext([
|
|
272
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
273
|
+
]);
|
|
274
|
+
$score = RequestUtils::getBehaviorScore($context);
|
|
275
|
+
$this->assertGreaterThan(60.0, $score['behaviorScore']);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public function testGetBehaviorScoreWithHumanLikeTouchMovements(): void
|
|
279
|
+
{
|
|
280
|
+
$metrics = [
|
|
281
|
+
'honeypotInteraction' => false,
|
|
282
|
+
'touchMovementsHistory' => [
|
|
283
|
+
['x' => 50, 'y' => 50, 't' => 1, 'p' => 0.45, 'r' => 8.5, 'num' => 1],
|
|
284
|
+
['x' => 60, 'y' => 52, 't' => 100, 'p' => 0.52, 'r' => 9.1, 'num' => 1],
|
|
285
|
+
['x' => 72, 'y' => 60, 't' => 200, 'p' => 0.49, 'r' => 8.8, 'num' => 1],
|
|
286
|
+
['x' => 80, 'y' => 80, 't' => 300, 'p' => 0.41, 'r' => 8.2, 'num' => 1]
|
|
287
|
+
]
|
|
288
|
+
];
|
|
289
|
+
$context = $this->createRequestContext([
|
|
290
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
291
|
+
]);
|
|
292
|
+
$score = RequestUtils::getBehaviorScore($context);
|
|
293
|
+
$this->assertLessThan(30.0, $score['behaviorScore']);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
public function testGetTimeInconsistencyScore(): void
|
|
297
|
+
{
|
|
298
|
+
$requestTimestamp = time() * 1000;
|
|
299
|
+
|
|
300
|
+
$metricsNormal = ['clientTimestamp' => $requestTimestamp - 100];
|
|
301
|
+
$contextNormal = $this->createRequestContext([
|
|
302
|
+
'headers' => ['x-behavior-metrics' => json_encode($metricsNormal)]
|
|
303
|
+
]);
|
|
304
|
+
$contextNormal->requestTimestamp = $requestTimestamp;
|
|
305
|
+
$scoreNormal = RequestUtils::getTimeInconsistencyScore($contextNormal);
|
|
306
|
+
$this->assertEquals(0.0, $scoreNormal['timeInconsistencyScore']);
|
|
307
|
+
|
|
308
|
+
$metricsReplay = ['clientTimestamp' => $requestTimestamp - 10000];
|
|
309
|
+
$contextReplay = $this->createRequestContext([
|
|
310
|
+
'headers' => ['x-behavior-metrics' => json_encode($metricsReplay)]
|
|
311
|
+
]);
|
|
312
|
+
$contextReplay->requestTimestamp = $requestTimestamp;
|
|
313
|
+
$scoreReplay = RequestUtils::getTimeInconsistencyScore($contextReplay);
|
|
314
|
+
$this->assertGreaterThan(0.0, $scoreReplay['timeInconsistencyScore']);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
public function testGetCrossLayerInconsistencyOSMismatch(): void
|
|
318
|
+
{
|
|
319
|
+
$windowsHash = FingerprintBuilder::cyrb53("Windows");
|
|
320
|
+
$context = $this->createRequestContext([
|
|
321
|
+
'headers' => [
|
|
322
|
+
'x-device-fingerprint' => "os:{$windowsHash}",
|
|
323
|
+
'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15'
|
|
324
|
+
]
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
328
|
+
$this->assertEquals(50.0, $score['crossLayerInconsistencyScore']);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public function testGetBotScoreDetections(): void
|
|
332
|
+
{
|
|
333
|
+
$contextBot = $this->createRequestContext([
|
|
334
|
+
'headers' => ['x-device-fingerprint' => 'ua:123|bot:true']
|
|
335
|
+
]);
|
|
336
|
+
$scoreBot = RequestUtils::getBotScore($contextBot);
|
|
337
|
+
$this->assertEquals(100.0, $scoreBot['botScore']);
|
|
338
|
+
|
|
339
|
+
$contextCdp = $this->createRequestContext([
|
|
340
|
+
'headers' => ['x-device-fingerprint' => 'ua:123|cdp:true']
|
|
341
|
+
]);
|
|
342
|
+
$scoreCdp = RequestUtils::getBotScore($contextCdp);
|
|
343
|
+
$this->assertEquals(100.0, $scoreCdp['botScore']);
|
|
344
|
+
|
|
345
|
+
$contextClean = $this->createRequestContext([
|
|
346
|
+
'headers' => ['x-device-fingerprint' => 'ua:123|os:456']
|
|
347
|
+
]);
|
|
348
|
+
$scoreClean = RequestUtils::getBotScore($contextClean);
|
|
349
|
+
$this->assertEquals(0.0, $scoreClean['botScore']);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public function testGetHeaderAnomaliesFirefoxTE(): void
|
|
353
|
+
{
|
|
354
|
+
$contextFirefoxNoTe = $this->createRequestContext([
|
|
355
|
+
'headers' => [
|
|
356
|
+
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
|
357
|
+
'accept-language' => 'en-US,en;q=0.9',
|
|
358
|
+
]
|
|
359
|
+
]);
|
|
360
|
+
$scoreFirefoxNoTe = RequestUtils::getHeaderAnomalies($contextFirefoxNoTe);
|
|
361
|
+
$this->assertEquals(30.0, $scoreFirefoxNoTe['headerAnomalyScore']);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public function testGetBehavioralIndicatorsRotationAndHistory(): void
|
|
365
|
+
{
|
|
366
|
+
$deviceData = [
|
|
367
|
+
'ips' => ['127.0.0.1'],
|
|
368
|
+
'lastFpHash' => 'cvs:original-canvas|gpu:original-gpu',
|
|
369
|
+
'lastChangeTimestamp' => (time() * 1000) - 500,
|
|
370
|
+
'rapidChangeCount' => 1
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
$context = $this->createRequestContext([
|
|
374
|
+
'headers' => [
|
|
375
|
+
'x-device-fingerprint' => 'cvs:new-canvas|gpu:new-gpu',
|
|
376
|
+
'user-agent' => 'Test UA'
|
|
377
|
+
]
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
$indicators = RequestUtils::getBehavioralIndicators($context, $deviceData);
|
|
381
|
+
$this->assertEquals(2, $deviceData['rapidChangeCount']);
|
|
382
|
+
$this->assertGreaterThan(0, $indicators['rotationScore']);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
public function testGetRequestPatternScoreWeightedSubscores(): void
|
|
386
|
+
{
|
|
387
|
+
$context = $this->createRequestContext(['path' => '/search']);
|
|
388
|
+
$deviceData = [
|
|
389
|
+
'requestHistory' => [],
|
|
390
|
+
'timingHistory' => [100, 100, 100, 100, 100, 100], // stdDev = 0
|
|
391
|
+
'lastPatternScore' => 0.0
|
|
392
|
+
];
|
|
393
|
+
$patternConfig = [
|
|
394
|
+
'minSamples' => 5,
|
|
395
|
+
'regularityThreshold' => 50,
|
|
396
|
+
'benfordThreshold' => 0.15,
|
|
397
|
+
'patternWeight' => 80,
|
|
398
|
+
'decayFactor' => 0.9,
|
|
399
|
+
'inactivityReset' => 5000,
|
|
400
|
+
'regularityRatio' => 0.4,
|
|
401
|
+
'benfordRatio' => 0.3,
|
|
402
|
+
'enumerationRatio' => 0.3
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
$result = RequestUtils::getRequestPatternScore($context, $deviceData, $patternConfig);
|
|
406
|
+
// regularityScore = 1.0. regularityRatio = 0.4.
|
|
407
|
+
// instantScore = 1.0 * 0.4 * 80 = 32.
|
|
408
|
+
$this->assertEquals(32.0, $result['requestPatternScore']);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
public function testGetBotnetClusterScoreCalculations(): void
|
|
412
|
+
{
|
|
413
|
+
$stableFpHash = 'test-stable-hash';
|
|
414
|
+
|
|
415
|
+
$context1 = $this->createRequestContext(['clientIp' => '192.168.1.1']);
|
|
416
|
+
$score = RequestUtils::getBotnetClusterScore($context1, $stableFpHash);
|
|
417
|
+
$this->assertEquals(0.0, $score['botnetClusterScore']);
|
|
418
|
+
|
|
419
|
+
RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.2']), $stableFpHash);
|
|
420
|
+
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.3']), $stableFpHash);
|
|
421
|
+
$this->assertEquals(30.2, $score['botnetClusterScore']);
|
|
422
|
+
|
|
423
|
+
RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.4']), $stableFpHash);
|
|
424
|
+
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.5']), $stableFpHash);
|
|
425
|
+
$this->assertEquals(45.2, $score['botnetClusterScore']);
|
|
426
|
+
|
|
427
|
+
for ($i = 6; $i <= 10; $i++) {
|
|
428
|
+
$score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => "192.168.1.{$i}"]), $stableFpHash);
|
|
429
|
+
}
|
|
430
|
+
$this->assertEquals(57.4, $score['botnetClusterScore']);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
public function testRealWorldConsoleBotnetClustering(): void
|
|
434
|
+
{
|
|
435
|
+
$ps4Headers = [
|
|
436
|
+
'user-agent' => 'Mozilla/5.0 (PlayStation 4 11.50) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.50 Safari/605.1.15',
|
|
437
|
+
'x-ja3-hash' => '76993ef93bf89104037599723ab9f201',
|
|
438
|
+
'x-ja4-hash' => 't13d1516h2_8daaf6152771_390237aa04be',
|
|
439
|
+
'x-http2-fingerprint' => '1:65536;3:1000;4:6291456;6:65536',
|
|
440
|
+
'x-tcp-fingerprint' => '64240:128:1:mss,nop,ws,nop,nop,sok:df:0'
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
for ($i = 1; $i <= 10; $i++) {
|
|
444
|
+
$context = $this->createRequestContext([
|
|
445
|
+
'clientIp' => "185.15.20.{$i}",
|
|
446
|
+
'headers' => array_merge($ps4Headers, [
|
|
447
|
+
'cookie_keys' => "session_id=fake_sess_{$i}"
|
|
448
|
+
])
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
$currentHash = RequestUtils::getCompositeDeviceHash($context);
|
|
452
|
+
$stableFp = RequestUtils::extractStablePart($currentHash);
|
|
453
|
+
$stableFpHash = FingerprintBuilder::cyrb53($stableFp);
|
|
454
|
+
|
|
455
|
+
$score = RequestUtils::getBotnetClusterScore($context, $stableFpHash);
|
|
456
|
+
|
|
457
|
+
if ($i === 1) {
|
|
458
|
+
$this->assertEquals(0.0, $score['botnetClusterScore'], "Expected score for 1 IP");
|
|
459
|
+
} else if ($i === 2) {
|
|
460
|
+
$this->assertEquals(17.7, $score['botnetClusterScore'], "Expected score for 2 IPs");
|
|
461
|
+
} else if ($i === 3) {
|
|
462
|
+
$this->assertEquals(30.2, $score['botnetClusterScore'], "Expected score for 3 IPs");
|
|
463
|
+
} else if ($i === 4) {
|
|
464
|
+
$this->assertEquals(39.0, $score['botnetClusterScore'], "Expected score for 4 IPs");
|
|
465
|
+
} else if ($i === 5) {
|
|
466
|
+
$this->assertEquals(45.2, $score['botnetClusterScore'], "Expected score for 5 IPs");
|
|
467
|
+
} else if ($i === 10) {
|
|
468
|
+
$this->assertEquals(57.4, $score['botnetClusterScore'], "Expected score for 10 IPs");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
472
|
}
|