@anonympins/fingerprint 0.4.4 → 0.4.6
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 +40 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +180 -0
- package/src/js/fingerprint.client.js +21 -0
- package/src/js/fingerprint.js +763 -56
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +381 -285
- package/src/js/pow.solver.js +97 -1
- package/src/js/tests/dynamic-wasm.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +128 -104
- package/src/js/tests/fingerprint.test.js +79 -1
- package/src/js/tests/ip-reputation.test.js +141 -131
- package/src/js/tests/tcpFingerprint.test.js +78 -0
- package/src/php/AutoTuner.php +1 -1
- package/src/php/Challenge/ChallengeUtils.php +174 -12
- package/src/php/Config/SecurityProfiles.php +10 -0
- package/src/php/FingerprintEngine.php +140 -8
- package/src/php/Optimization/Optimization.php +257 -255
- package/src/php/Optimization/OptimizationOperators.php +41 -35
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/FingerprintEngineTest.php +132 -1
- package/src/php/Tests/IpReputationTest.php +175 -156
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/RequestUtils.php +319 -5
|
@@ -10,6 +10,7 @@ use Anonympins\Fingerprint\FingerprintEngine;
|
|
|
10
10
|
use Anonympins\Fingerprint\RequestContext;
|
|
11
11
|
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
12
12
|
use Anonympins\Fingerprint\Store\StoreManager;
|
|
13
|
+
use Anonympins\Fingerprint\Challenge\ChallengeUtils;
|
|
13
14
|
use PHPUnit\Framework\TestCase;
|
|
14
15
|
|
|
15
16
|
class FingerprintEngineTest extends TestCase
|
|
@@ -90,6 +91,35 @@ class FingerprintEngineTest extends TestCase
|
|
|
90
91
|
$this->assertLessThan($this->securityConfig['thresholds']['low'], $secondDecision['score']);
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
public function testTlsSessionResumptionCookielessTracking(): void
|
|
95
|
+
{
|
|
96
|
+
// 1. Première requête d'un client avec un ID de session TLS, sans cookie
|
|
97
|
+
$tlsSessionId = 'test-tls-1.3-session-resumption-id-12345';
|
|
98
|
+
$context1 = $this->createRequestContext([
|
|
99
|
+
'headers' => [
|
|
100
|
+
'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',
|
|
101
|
+
'x-tls-session-id' => $tlsSessionId,
|
|
102
|
+
]
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
$firstDecision = $this->engine->processRequest($context1);
|
|
106
|
+
$this->assertArrayHasKey('newCookieForResponse', $firstDecision, "Un cookie d'identité d'appareil doit être généré.");
|
|
107
|
+
$deviceId = $firstDecision['newCookieForResponse']['value'];
|
|
108
|
+
|
|
109
|
+
// 2. Deuxième requête : le client supprime ses cookies, mais garde la même session TLS (Session Resumption / 0-RTT)
|
|
110
|
+
$context2 = $this->createRequestContext([
|
|
111
|
+
'headers' => [
|
|
112
|
+
'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',
|
|
113
|
+
'x-tls-session-id' => $tlsSessionId,
|
|
114
|
+
],
|
|
115
|
+
'cookies' => [] // Cookies supprimés !
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
$secondDecision = $this->engine->processRequest($context2);
|
|
119
|
+
$this->assertEquals('next', $secondDecision['action']);
|
|
120
|
+
$this->assertArrayNotHasKey('newCookieForResponse', $secondDecision, "Aucun nouveau cookie d'identité ne doit être généré car la session TLS a permis de restaurer l'identité.");
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
public function testIssuesChallengeForSuspiciousRequest(): void
|
|
94
124
|
{
|
|
95
125
|
// 1. First, establish a device identity to avoid the cookie-dropping penalty
|
|
@@ -294,7 +324,7 @@ class FingerprintEngineTest extends TestCase
|
|
|
294
324
|
'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
325
|
'sec-ch-ua' => '"Not/A)Brand";v="99", "Google Chrome";v="120", "Chromium";v="120"',
|
|
296
326
|
]),
|
|
297
|
-
|
|
327
|
+
96.7,
|
|
298
328
|
'Should return 80 for a large version mismatch (>5).'
|
|
299
329
|
],
|
|
300
330
|
'small version mismatch' => [
|
|
@@ -328,4 +358,105 @@ class FingerprintEngineTest extends TestCase
|
|
|
328
358
|
],
|
|
329
359
|
];
|
|
330
360
|
}
|
|
361
|
+
|
|
362
|
+
public function testDetectsTcpAnomalyScore(): void
|
|
363
|
+
{
|
|
364
|
+
$firstContext = $this->createRequestContext();
|
|
365
|
+
$firstDecision = $this->engine->processRequest($firstContext);
|
|
366
|
+
$this->assertArrayHasKey('newCookieForResponse', $firstDecision);
|
|
367
|
+
$deviceIdCookie = $firstDecision['newCookieForResponse'];
|
|
368
|
+
|
|
369
|
+
// Trame binaire TCP SYN Linux simulée (TTL=64, WS=7)
|
|
370
|
+
$linuxSynHex = '4500003c1a2b400040063c1a7f0000017f0000011f9000500000000100000000a00272103c1a0000020405b4040201030307';
|
|
371
|
+
|
|
372
|
+
$context = $this->createRequestContext([
|
|
373
|
+
'cookies' => [$deviceIdCookie['name'] => $deviceIdCookie['value']],
|
|
374
|
+
'headers' => [
|
|
375
|
+
'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',
|
|
376
|
+
'accept-language' => 'en-US,en;q=0.9',
|
|
377
|
+
'x-raw-tcp-binary' => $linuxSynHex
|
|
378
|
+
]
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
$decision = $this->engine->processRequest($context);
|
|
382
|
+
|
|
383
|
+
$this->assertEquals(80.1, $decision['vector']['tcpAnomalyScore']);
|
|
384
|
+
// Le poids de tcpAnomalyScore dans le profil balanced est de 0.8 (80 * 0.8 = 64)
|
|
385
|
+
$this->assertGreaterThanOrEqual(64.0, $decision['score']);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
public function testDetectsAndVerifiesSpaceChallenge(): void
|
|
389
|
+
{
|
|
390
|
+
$store = StoreManager::getStore();
|
|
391
|
+
|
|
392
|
+
$config = SecurityProfiles::createSecurityProfile('balanced', [
|
|
393
|
+
'enableProofOfSpace' => true,
|
|
394
|
+
'pospace' => [
|
|
395
|
+
'sizeMb' => 1,
|
|
396
|
+
'numQueries' => 5
|
|
397
|
+
],
|
|
398
|
+
'challengeNewDevices' => false,
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
$engine = $this->getMockBuilder(FingerprintEngine::class)
|
|
402
|
+
->setConstructorArgs([$config])
|
|
403
|
+
->onlyMethods(['calculateFinalScore'])
|
|
404
|
+
->getMock();
|
|
405
|
+
$engine->method('calculateFinalScore')->willReturn(30.0);
|
|
406
|
+
|
|
407
|
+
$headers = [
|
|
408
|
+
'user-agent' => 'A normal browser',
|
|
409
|
+
'accept-language' => 'en-US,en;q=0.9',
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
// 1. Première requête provoquant un challenge
|
|
413
|
+
$context = $this->createRequestContext([
|
|
414
|
+
'headers' => $headers
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
$decision = $engine->processRequest($context);
|
|
418
|
+
$this->assertEquals('challenge', $decision['action']);
|
|
419
|
+
|
|
420
|
+
$html = $decision['body'];
|
|
421
|
+
$this->assertStringContainsString('initializeSpace', $html);
|
|
422
|
+
|
|
423
|
+
preg_match('/const nonce = "(.*?)";/', $html, $matchesNonce);
|
|
424
|
+
preg_match('/const clientSecret = "(.*?)";/', $html, $matchesSecret);
|
|
425
|
+
preg_match('/const queries = (\[.*?\]);/', $html, $matchesQueries);
|
|
426
|
+
|
|
427
|
+
$nonce = $matchesNonce[1];
|
|
428
|
+
$clientSecret = $matchesSecret[1];
|
|
429
|
+
$queries = json_decode($matchesQueries[1], true);
|
|
430
|
+
|
|
431
|
+
// 2. Résoudre le challenge
|
|
432
|
+
$combined = '';
|
|
433
|
+
foreach ($queries as $idx) {
|
|
434
|
+
$combined .= $this->callPrivateGenerateBlock($nonce . ":" . $clientSecret, (int)$idx);
|
|
435
|
+
}
|
|
436
|
+
$finalBlock = $combined . $nonce . ":" . $clientSecret;
|
|
437
|
+
$solution = hash('sha256', $finalBlock);
|
|
438
|
+
|
|
439
|
+
// 3. Soumettre le challenge
|
|
440
|
+
$submitContext = $this->createRequestContext([
|
|
441
|
+
'headers' => $headers,
|
|
442
|
+
'query' => [
|
|
443
|
+
'pow_type' => 'pospace',
|
|
444
|
+
'pow_nonce' => $nonce,
|
|
445
|
+
'pow_solution_space' => $solution
|
|
446
|
+
]
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
$engineReal = new FingerprintEngine($config);
|
|
450
|
+
$submitDecision = $engineReal->processRequest($submitContext);
|
|
451
|
+
$this->assertEquals('redirect', $submitDecision['action']);
|
|
452
|
+
$this->assertArrayHasKey('cookie', $submitDecision);
|
|
453
|
+
$this->assertEquals('pow_clearance', $submitDecision['cookie']['name']);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private function callPrivateGenerateBlock(string $seed, int $blockIndex): string
|
|
457
|
+
{
|
|
458
|
+
$ref = new \ReflectionClass(ChallengeUtils::class);
|
|
459
|
+
$method = $ref->getMethod('generateBlock');
|
|
460
|
+
return $method->invoke(null, $seed, $blockIndex);
|
|
461
|
+
}
|
|
331
462
|
}
|
|
@@ -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
|
}
|
|
@@ -35,6 +35,19 @@ class RequestUtilsTest extends TestCase
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
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
|
+
}
|
|
38
51
|
public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
|
|
39
52
|
{
|
|
40
53
|
$context = $this->createRequestContext([
|