@anonympins/fingerprint 0.4.3 → 0.4.5

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.
@@ -1,54 +1,54 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Store;
6
-
7
- /**
8
- * Adaptateur de stockage Redis pour le moteur Fingerprint.
9
- * Compatible avec phpredis et predis.
10
- */
11
- class RedisStore implements IStore
12
- {
13
- /**
14
- * @var mixed Une instance de \Redis ou de \Predis\Client
15
- */
16
- private $redis;
17
-
18
- /**
19
- * @param mixed $redis Client Redis déjà configuré et connecté
20
- */
21
- public function __construct($redis)
22
- {
23
- $this->redis = $redis;
24
- }
25
-
26
- public function get(string $key)
27
- {
28
- $value = $this->redis->get($key);
29
- if ($value === false || $value === null) {
30
- return null;
31
- }
32
- return json_decode($value, true);
33
- }
34
-
35
- public function set(string $key, $value, ?int $ttl = null): void
36
- {
37
- $stringValue = json_encode($value);
38
- if ($ttl !== null && $ttl > 0) {
39
- $this->redis->setex($key, $ttl, $stringValue);
40
- } else {
41
- $this->redis->set($key, $stringValue);
42
- }
43
- }
44
-
45
- public function has(string $key): bool
46
- {
47
- return (bool)$this->redis->exists($key);
48
- }
49
-
50
- public function delete(string $key): void
51
- {
52
- $this->redis->del($key);
53
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Store;
6
+
7
+ /**
8
+ * Adaptateur de stockage Redis pour le moteur Fingerprint.
9
+ * Compatible avec phpredis et predis.
10
+ */
11
+ class RedisStore implements IStore
12
+ {
13
+ /**
14
+ * @var mixed Une instance de \Redis ou de \Predis\Client
15
+ */
16
+ private $redis;
17
+
18
+ /**
19
+ * @param mixed $redis Client Redis déjà configuré et connecté
20
+ */
21
+ public function __construct($redis)
22
+ {
23
+ $this->redis = $redis;
24
+ }
25
+
26
+ public function get(string $key)
27
+ {
28
+ $value = $this->redis->get($key);
29
+ if ($value === false || $value === null) {
30
+ return null;
31
+ }
32
+ return json_decode($value, true);
33
+ }
34
+
35
+ public function set(string $key, $value, ?int $ttl = null): void
36
+ {
37
+ $stringValue = json_encode($value);
38
+ if ($ttl !== null && $ttl > 0) {
39
+ $this->redis->setex($key, $ttl, $stringValue);
40
+ } else {
41
+ $this->redis->set($key, $stringValue);
42
+ }
43
+ }
44
+
45
+ public function has(string $key): bool
46
+ {
47
+ return (bool)$this->redis->exists($key);
48
+ }
49
+
50
+ public function delete(string $key): void
51
+ {
52
+ $this->redis->del($key);
53
+ }
54
54
  }
@@ -294,7 +294,7 @@ class FingerprintEngineTest extends TestCase
294
294
  'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
295
295
  'sec-ch-ua' => '"Not/A)Brand";v="99", "Google Chrome";v="120", "Chromium";v="120"',
296
296
  ]),
297
- 80.0,
297
+ 96.7,
298
298
  'Should return 80 for a large version mismatch (>5).'
299
299
  ],
300
300
  'small version mismatch' => [
@@ -328,4 +328,30 @@ class FingerprintEngineTest extends TestCase
328
328
  ],
329
329
  ];
330
330
  }
331
+
332
+ public function testDetectsTcpAnomalyScore(): void
333
+ {
334
+ $firstContext = $this->createRequestContext();
335
+ $firstDecision = $this->engine->processRequest($firstContext);
336
+ $this->assertArrayHasKey('newCookieForResponse', $firstDecision);
337
+ $deviceIdCookie = $firstDecision['newCookieForResponse'];
338
+
339
+ // Trame binaire TCP SYN Linux simulée (TTL=64, WS=7)
340
+ $linuxSynHex = '4500003c1a2b400040063c1a7f0000017f0000011f9000500000000100000000a00272103c1a0000020405b4040201030307';
341
+
342
+ $context = $this->createRequestContext([
343
+ 'cookies' => [$deviceIdCookie['name'] => $deviceIdCookie['value']],
344
+ 'headers' => [
345
+ 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
346
+ 'accept-language' => 'en-US,en;q=0.9',
347
+ 'x-raw-tcp-binary' => $linuxSynHex
348
+ ]
349
+ ]);
350
+
351
+ $decision = $this->engine->processRequest($context);
352
+
353
+ $this->assertEquals(80.1, $decision['vector']['tcpAnomalyScore']);
354
+ // Le poids de tcpAnomalyScore dans le profil balanced est de 0.8 (80 * 0.8 = 64)
355
+ $this->assertGreaterThanOrEqual(64.0, $decision['score']);
356
+ }
331
357
  }
@@ -1,157 +1,176 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\RequestContext;
8
- use Anonympins\Fingerprint\Store\IStore;
9
- use Anonympins\Fingerprint\Store\StoreManager;
10
- use Anonympins\Fingerprint\Utils\RequestUtils;
11
- use PHPUnit\Framework\TestCase;
12
-
13
- class IpReputationTest extends TestCase
14
- {
15
- private $store;
16
-
17
- protected function setUp(): void
18
- {
19
- parent::setUp();
20
-
21
- // Un mock anonyme simple et en mémoire du Store pour isoler les tests
22
- $this->store = new class implements IStore {
23
- private array $data = [];
24
- public function get(string $key) { return $this->data[$key] ?? null; }
25
- public function set(string $key, $value, ?int $ttl = null): void { $this->data[$key] = $value; }
26
- public function has(string $key): bool { return isset($this->data[$key]); }
27
- public function delete(string $key): void { unset($this->data[$key]); }
28
- };
29
-
30
- StoreManager::setStore($this->store);
31
- }
32
-
33
- public function testGetScoreReturnsZeroForUnknownIp(): void
34
- {
35
- $score = RequestUtils::getIpReputationScore('8.8.8.8');
36
- $this->assertEquals(0.0, $score);
37
- }
38
-
39
- public function testUpdateScoreChangesValueCorrectly(): void
40
- {
41
- $ip = '8.8.4.4';
42
- RequestUtils::updateIpReputationScore($ip, 45.0);
43
- $this->assertEquals(45.0, RequestUtils::getIpReputationScore($ip));
44
-
45
- RequestUtils::updateIpReputationScore($ip, -15.0);
46
- $this->assertEquals(30.0, RequestUtils::getIpReputationScore($ip));
47
- }
48
-
49
- public function testBoundsAreClampedBetween0And100(): void
50
- {
51
- $ip = '1.1.1.1';
52
- RequestUtils::updateIpReputationScore($ip, 120.0);
53
- $this->assertEquals(100.0, RequestUtils::getIpReputationScore($ip));
54
-
55
- RequestUtils::updateIpReputationScore($ip, -150.0);
56
- $this->assertEquals(0.0, RequestUtils::getIpReputationScore($ip));
57
- }
58
-
59
- public function testTimeDecayCalculatesLossOfTwoPointsPerHour(): void
60
- {
61
- $ip = '2.2.2.2';
62
- $now = time();
63
-
64
- // Simulation d'un score de 80 vieux de 4 heures (4 * 2 = 8 points de perte)
65
- $this->store->set("ip-reputation:{$ip}", [
66
- 'score' => 80.0,
67
- 'lastUpdate' => $now - 14400
68
- ]);
69
-
70
- $score = RequestUtils::getIpReputationScore($ip);
71
- $this->assertEquals(72.0, $score);
72
- }
73
-
74
- public function testIpReputationScoreIsIntegratedIntoFinalScore(): void
75
- {
76
- $ip = '1.2.3.4';
77
- RequestUtils::updateIpReputationScore($ip, 60.0);
78
-
79
- $weights = [
80
- 'ipReputationScore' => 0.5,
81
- 'historyScore' => 0.0,
82
- 'rotationScore' => 0.0,
83
- 'headerAnomalyScore' => 0.0,
84
- 'requestPatternScore' => 0.0,
85
- 'inconsistencyScore' => 0.0,
86
- 'honeypotScore' => 0.0,
87
- 'behaviorScore' => 0.0,
88
- 'botScore' => 0.0,
89
- 'crossLayerInconsistencyScore' => 0.0,
90
- 'tlsSpoofingScore' => 0.0,
91
- 'timeInconsistencyScore' => 0.0,
92
- 'clickVarianceScore' => 0.0,
93
- 'clientHintsInconsistencyScore' => 0.0,
94
- 'subnetScore' => 0.0,
95
- ];
96
-
97
- $ipRepScore = RequestUtils::getIpReputationScore($ip);
98
- $this->assertEquals(60.0, $ipRepScore);
99
- $score = $ipRepScore * $weights['ipReputationScore'];
100
- $this->assertEquals(30.0, $score);
101
- }
102
-
103
- public function testGetIpSubnetIpv4AndIpv6(): void
104
- {
105
- $ipv4Subnet = RequestUtils::getIpSubnet('192.168.1.50', 24, 48);
106
- $this->assertEquals('192.168.1.0/24', $ipv4Subnet);
107
-
108
- $ipv6Subnet = RequestUtils::getIpSubnet('2001:db8:abcd:0012::1', 24, 48);
109
- $this->assertEquals('2001:db8:abcd::/48', $ipv6Subnet);
110
- }
111
-
112
- public function testGetClientHintsInconsistencyScoreMismatch(): void
113
- {
114
- $context = $this->createMock(RequestContext::class);
115
- $context->method('getHeader')->willReturnCallback(function($name) {
116
- if ($name === 'user-agent') {
117
- return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/117.0';
118
- }
119
- if ($name === 'sec-ch-ua') {
120
- return '"Google Chrome";v="117"';
121
- }
122
- return null;
123
- });
124
-
125
- $score = RequestUtils::getClientHintsInconsistencyScore($context);
126
- $this->assertEquals(90.0, $score['clientHintsInconsistencyScore']);
127
- }
128
-
129
- public function testGetSubnetScoreCalculations(): void
130
- {
131
- $context = $this->createMock(RequestContext::class);
132
- $context->clientIp = '192.168.1.50';
133
-
134
- $score = RequestUtils::getSubnetScore($context, 'device-1');
135
- $this->assertEquals(0.0, $score['subnetScore']);
136
-
137
- for ($i = 1; $i <= 12; $i++) {
138
- RequestUtils::updateSubnetMetrics($context, "device-{$i}", 30.0);
139
- }
140
-
141
- $scoreWithHistory = RequestUtils::getSubnetScore($context, 'device-1');
142
- $this->assertGreaterThan(0.0, $scoreWithHistory['subnetScore']);
143
- }
144
-
145
- public function testTlsSpoofingScoreWithJa4(): void
146
- {
147
- $context = $this->createMock(RequestContext::class);
148
- $context->ja4 = 't13d1517h2_8daaf61527d5';
149
- $context->ja3 = null;
150
- $context->method('getHeader')->willReturnCallback(function($name) {
151
- return $name === 'user-agent' ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/117.0' : null;
152
- });
153
-
154
- $score = RequestUtils::getTlsSpoofingScore($context);
155
- $this->assertEquals(90.0, $score['tlsSpoofingScore']);
156
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\RequestContext;
8
+ use Anonympins\Fingerprint\Store\IStore;
9
+ use Anonympins\Fingerprint\Store\StoreManager;
10
+ use Anonympins\Fingerprint\Utils\RequestUtils;
11
+ use PHPUnit\Framework\TestCase;
12
+
13
+ class IpReputationTest extends TestCase
14
+ {
15
+ private $store;
16
+
17
+ protected function setUp(): void
18
+ {
19
+ parent::setUp();
20
+
21
+ // Un mock anonyme simple et en mémoire du Store pour isoler les tests
22
+ $this->store = new class implements IStore {
23
+ private array $data = [];
24
+ public function get(string $key) { return $this->data[$key] ?? null; }
25
+ public function set(string $key, $value, ?int $ttl = null): void { $this->data[$key] = $value; }
26
+ public function has(string $key): bool { return isset($this->data[$key]); }
27
+ public function delete(string $key): void { unset($this->data[$key]); }
28
+ };
29
+
30
+ StoreManager::setStore($this->store);
31
+ }
32
+
33
+ public function testGetScoreReturnsZeroForUnknownIp(): void
34
+ {
35
+ $score = RequestUtils::getIpReputationScore('8.8.8.8');
36
+ $this->assertEquals(0.0, $score);
37
+ }
38
+
39
+ public function testUpdateScoreChangesValueCorrectly(): void
40
+ {
41
+ $ip = '8.8.4.4';
42
+ RequestUtils::updateIpReputationScore($ip, 45.0);
43
+ $this->assertEquals(45.0, RequestUtils::getIpReputationScore($ip));
44
+
45
+ RequestUtils::updateIpReputationScore($ip, -15.0);
46
+ $this->assertEquals(30.0, RequestUtils::getIpReputationScore($ip));
47
+ }
48
+
49
+ public function testBoundsAreClampedBetween0And100(): void
50
+ {
51
+ $ip = '1.1.1.1';
52
+ RequestUtils::updateIpReputationScore($ip, 120.0);
53
+ $this->assertEquals(100.0, RequestUtils::getIpReputationScore($ip));
54
+
55
+ RequestUtils::updateIpReputationScore($ip, -150.0);
56
+ $this->assertEquals(0.0, RequestUtils::getIpReputationScore($ip));
57
+ }
58
+
59
+ public function testTimeDecayCalculatesLossOfTwoPointsPerHour(): void
60
+ {
61
+ $ip = '2.2.2.2';
62
+ $now = time();
63
+
64
+ // Simulation d'un score de 80 vieux de 4 heures (4 * 2 = 8 points de perte)
65
+ $this->store->set("ip-reputation:{$ip}", [
66
+ 'score' => 80.0,
67
+ 'lastUpdate' => $now - 14400
68
+ ]);
69
+
70
+ $score = RequestUtils::getIpReputationScore($ip);
71
+ $this->assertEquals(72.0, $score);
72
+ }
73
+
74
+ public function testIpReputationScoreIsIntegratedIntoFinalScore(): void
75
+ {
76
+ $ip = '1.2.3.4';
77
+ RequestUtils::updateIpReputationScore($ip, 60.0);
78
+
79
+ $weights = [
80
+ 'ipReputationScore' => 0.5,
81
+ 'historyScore' => 0.0,
82
+ 'rotationScore' => 0.0,
83
+ 'headerAnomalyScore' => 0.0,
84
+ 'requestPatternScore' => 0.0,
85
+ 'inconsistencyScore' => 0.0,
86
+ 'honeypotScore' => 0.0,
87
+ 'behaviorScore' => 0.0,
88
+ 'botScore' => 0.0,
89
+ 'crossLayerInconsistencyScore' => 0.0,
90
+ 'tlsSpoofingScore' => 0.0,
91
+ 'timeInconsistencyScore' => 0.0,
92
+ 'clickVarianceScore' => 0.0,
93
+ 'clientHintsInconsistencyScore' => 0.0,
94
+ 'subnetScore' => 0.0,
95
+ ];
96
+
97
+ $ipRepScore = RequestUtils::getIpReputationScore($ip);
98
+ $this->assertEquals(60.0, $ipRepScore);
99
+ $score = $ipRepScore * $weights['ipReputationScore'];
100
+ $this->assertEquals(30.0, $score);
101
+ }
102
+
103
+ public function testGetIpSubnetIpv4AndIpv6(): void
104
+ {
105
+ $ipv4Subnet = RequestUtils::getIpSubnet('192.168.1.50', 24, 48);
106
+ $this->assertEquals('192.168.1.0/24', $ipv4Subnet);
107
+
108
+ $ipv6Subnet = RequestUtils::getIpSubnet('2001:db8:abcd:0012::1', 24, 48);
109
+ $this->assertEquals('2001:db8:abcd::/48', $ipv6Subnet);
110
+ }
111
+
112
+ public function testGetClientHintsInconsistencyScoreMismatch(): void
113
+ {
114
+ $context = $this->createMock(RequestContext::class);
115
+ $context->method('getHeader')->willReturnCallback(function($name) {
116
+ if ($name === 'user-agent') {
117
+ return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/117.0';
118
+ }
119
+ if ($name === 'sec-ch-ua') {
120
+ return '"Google Chrome";v="117"';
121
+ }
122
+ return null;
123
+ });
124
+
125
+ $score = RequestUtils::getClientHintsInconsistencyScore($context);
126
+ $this->assertEquals(90.0, $score['clientHintsInconsistencyScore']);
127
+ }
128
+
129
+ public function testGetClientHintsInconsistencyFullVersionMismatch(): void
130
+ {
131
+ $context = $this->createMock(RequestContext::class);
132
+ $context->method('getHeader')->willReturnCallback(function($name) {
133
+ if ($name === 'user-agent') {
134
+ return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.1.2 Safari/537.36';
135
+ }
136
+ if ($name === 'sec-ch-ua') {
137
+ return '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"';
138
+ }
139
+ if ($name === 'sec-ch-ua-full-version-list') {
140
+ return '"Not_A Brand";v="8.0.0.0", "Chromium";v="120.0.1.3", "Google Chrome";v="120.0.1.3"';
141
+ }
142
+ return null;
143
+ });
144
+ $score = RequestUtils::getClientHintsInconsistencyScore($context);
145
+ $this->assertEquals(85.0, $score['clientHintsInconsistencyScore']);
146
+ }
147
+
148
+ public function testGetSubnetScoreCalculations(): void
149
+ {
150
+ $context = $this->createMock(RequestContext::class);
151
+ $context->clientIp = '192.168.1.50';
152
+
153
+ $score = RequestUtils::getSubnetScore($context, 'device-1');
154
+ $this->assertEquals(0.0, $score['subnetScore']);
155
+
156
+ for ($i = 1; $i <= 12; $i++) {
157
+ RequestUtils::updateSubnetMetrics($context, "device-{$i}", 30.0);
158
+ }
159
+
160
+ $scoreWithHistory = RequestUtils::getSubnetScore($context, 'device-1');
161
+ $this->assertGreaterThan(0.0, $scoreWithHistory['subnetScore']);
162
+ }
163
+
164
+ public function testTlsSpoofingScoreWithJa4(): void
165
+ {
166
+ $context = $this->createMock(RequestContext::class);
167
+ $context->ja4 = 't13d1517h2_8daaf61527d5';
168
+ $context->ja3 = null;
169
+ $context->method('getHeader')->willReturnCallback(function($name) {
170
+ return $name === 'user-agent' ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/117.0' : null;
171
+ });
172
+
173
+ $score = RequestUtils::getTlsSpoofingScore($context);
174
+ $this->assertEquals(90.0, $score['tlsSpoofingScore']);
175
+ }
157
176
  }
@@ -11,6 +11,12 @@ use PHPUnit\Framework\TestCase;
11
11
 
12
12
  class RequestUtilsTest extends TestCase
13
13
  {
14
+ protected function setUp(): void
15
+ {
16
+ parent::setUp();
17
+ \Anonympins\Fingerprint\Store\StoreManager::configureStore(new \Anonympins\Fingerprint\Store\InMemoryStore());
18
+ }
19
+
14
20
  private function createRequestContext(array $overrides = []): RequestContext
15
21
  {
16
22
  $defaults = [
@@ -166,6 +172,42 @@ class RequestUtilsTest extends TestCase
166
172
  $this->assertEquals(40.0, $scoreNoActivity['behaviorScore']);
167
173
  }
168
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
+
169
211
  public function testGetTimeInconsistencyScore(): void
170
212
  {
171
213
  $requestTimestamp = time() * 1000;
@@ -254,4 +296,92 @@ class RequestUtilsTest extends TestCase
254
296
  $this->assertEquals(2, $deviceData['rapidChangeCount']);
255
297
  $this->assertGreaterThan(0, $indicators['rotationScore']);
256
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
+ }
257
387
  }