@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.
- package/CHANGELOG.md +43 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +158 -0
- package/src/js/fingerprint.client.js +852 -635
- package/src/js/fingerprint.js +582 -48
- package/src/js/tests/dynamic-wasm.test.js +78 -0
- package/src/js/tests/fingerprint.client.init.test.js +140 -119
- package/src/js/tests/fingerprint.client.test.js +128 -104
- package/src/js/tests/fingerprint.test.js +130 -11
- package/src/js/tests/ip-reputation.test.js +141 -131
- package/src/js/tests/tcpFingerprint.test.js +78 -0
- package/src/php/Challenge/ChallengeUtils.php +480 -361
- package/src/php/Config/SecurityProfiles.php +286 -271
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +30 -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 +27 -1
- package/src/php/Tests/IpReputationTest.php +175 -156
- package/src/php/Tests/RequestUtilsTest.php +130 -0
- package/src/php/Utils/RequestUtils.php +474 -21
|
@@ -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
|
-
|
|
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
|
|
130
|
-
{
|
|
131
|
-
$context = $this->createMock(RequestContext::class);
|
|
132
|
-
$context->
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
$context->
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
$
|
|
155
|
-
|
|
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
|
}
|