@anonympins/fingerprint 0.7.4 → 0.8.0

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/anonympins-bot-mitigation-pow.zip +0 -0
  5. package/src/js/dynamic-wasm.js +218 -218
  6. package/src/js/fingerprint.client.js +1784 -1738
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +489 -99
  9. package/src/js/fingerprint.utils.js +252 -2
  10. package/src/js/pow.worker.js +2 -2
  11. package/src/js/tests/cross_parity.test.js +3 -3
  12. package/src/js/tests/dns-circuit-breaker.test.js +6 -6
  13. package/src/js/tests/dynamic-wasm.test.js +77 -77
  14. package/src/js/tests/fingerprint.builder.test.js +76 -78
  15. package/src/js/tests/fingerprint.client.init.test.js +78 -0
  16. package/src/js/tests/fingerprint.client.test.js +16 -17
  17. package/src/js/tests/fingerprint.test.js +37 -12
  18. package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
  19. package/src/js/tests/library.test.js +94 -95
  20. package/src/js/tests/obfuscation.test.js +8 -8
  21. package/src/js/tests/quicFingerprint.test.js +47 -0
  22. package/src/js/tests/tcpFingerprint.test.js +4 -4
  23. package/src/js/tests/threatIntel.test.js +40 -1
  24. package/src/js/upow-model-task.js +155 -112
  25. package/src/php/Challenge/ChallengeUtils.php +392 -86
  26. package/src/php/Config/SecurityProfiles.php +79 -61
  27. package/src/php/DirectFingerprint.php +55 -8
  28. package/src/php/FingerprintBuilder.php +13 -13
  29. package/src/php/FingerprintClient.php +6 -6
  30. package/src/php/FingerprintEngine.php +2122 -1690
  31. package/src/php/Ja3AnomalyDetector.php +22 -22
  32. package/src/php/Optimization/FunctionRegistry.php +4 -4
  33. package/src/php/Optimization/Optimization.php +6 -6
  34. package/src/php/RequestContext.php +11 -11
  35. package/src/php/Store/InMemoryStore.php +96 -66
  36. package/src/php/Store/MongoDbStore.php +2 -2
  37. package/src/php/Store/RedisStore.php +1 -1
  38. package/src/php/Store/StoreManager.php +48 -35
  39. package/src/php/Tests/ChallengeUtilsTest.php +31 -0
  40. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  41. package/src/php/Tests/FingerprintClientTest.php +70 -70
  42. package/src/php/Tests/PowTest.php +37 -39
  43. package/src/php/Tests/QuicFingerprintTest.php +115 -53
  44. package/src/php/Tests/ThreatIntelTest.php +34 -0
  45. package/src/php/Utils/BigInt.php +202 -202
  46. package/src/php/Utils/BlockList.php +13 -13
  47. package/src/php/Utils/Logger.php +19 -1
  48. package/src/php/Utils/MaliciousPatterns.php +6 -6
  49. package/src/php/Utils/MetricsManager.php +209 -209
  50. package/src/php/Utils/RequestUtils.php +362 -1
  51. package/src/php/Utils/TLSClientHelloParser.php +385 -369
  52. package/src/php/WordPress/WpDbStore.php +7 -7
  53. package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
  54. package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
  55. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
  56. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
  57. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  58. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
  59. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  60. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
  61. package/src/php/WordPress/package.php +80 -101
  62. package/src/php/WordPress/readme.txt +21 -5
  63. package/src/php/bin/auto-tune.php +1 -1
  64. package/public/fingerprint-anti-bot.zip +0 -0
@@ -1,106 +1,106 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\FingerprintEngine;
8
- use PHPUnit\Framework\TestCase;
9
- use ReflectionClass;
10
-
11
- class DnsCircuitBreakerTest extends TestCase
12
- {
13
- private ReflectionClass $reflection;
14
-
15
- protected function setUp(): void
16
- {
17
- parent::setUp();
18
- $this->reflection = new ReflectionClass(FingerprintEngine::class);
19
-
20
- // Réinitialise l'état statique du disjoncteur avant chaque test
21
- $this->setCircuitBreakerState([
22
- 'state' => 'CLOSED',
23
- 'failureCount' => 0,
24
- 'lastStateChange' => 0,
25
- 'threshold' => 5,
26
- 'cooldown' => 1, // 1 seconde de cooldown pour accélérer le test
27
- ]);
28
- }
29
-
30
- private function getCircuitBreakerState(): array
31
- {
32
- $property = $this->reflection->getProperty('dnsCircuitBreaker');
33
- if (PHP_VERSION_ID < 80100) {
34
- $property->setAccessible(true);
35
- }
36
- return $property->getValue();
37
- }
38
-
39
- private function setCircuitBreakerState(array $state): void
40
- {
41
- $property = $this->reflection->getProperty('dnsCircuitBreaker');
42
- if (PHP_VERSION_ID < 80100) {
43
- $property->setAccessible(true);
44
- }
45
- $property->setValue(null, $state);
46
- }
47
-
48
- private function invokeMethod(string $methodName, array $args = [])
49
- {
50
- $method = $this->reflection->getMethod($methodName);
51
- if (PHP_VERSION_ID < 80100) { // PHP 8.1.0
52
- $method->setAccessible(true);
53
- }
54
- return $method->invokeArgs(null, $args);
55
- }
56
-
57
- public function testInitialStateIsClosed(): void
58
- {
59
- $this->assertTrue($this->invokeMethod('canAttemptDns'));
60
- $state = $this->getCircuitBreakerState();
61
- $this->assertSame('CLOSED', $state['state']);
62
- }
63
-
64
- public function testFailureThresholdTripsBreaker(): void
65
- {
66
- for ($i = 0; $i < 4; $i++) {
67
- $this->invokeMethod('recordDnsFailure');
68
- $this->assertTrue($this->invokeMethod('canAttemptDns'));
69
- $state = $this->getCircuitBreakerState();
70
- $this->assertSame('CLOSED', $state['state']);
71
- }
72
-
73
- $this->invokeMethod('recordDnsFailure');
74
- $this->assertFalse($this->invokeMethod('canAttemptDns'));
75
- $state = $this->getCircuitBreakerState();
76
- $this->assertSame('OPEN', $state['state']);
77
- }
78
-
79
- public function testCooldownTransitionsToHalfOpen(): void
80
- {
81
- for ($i = 0; $i < 5; $i++) {
82
- $this->invokeMethod('recordDnsFailure');
83
- }
84
- $this->assertFalse($this->invokeMethod('canAttemptDns'));
85
-
86
- usleep(1100000); // 1.1s
87
-
88
- $this->assertTrue($this->invokeMethod('canAttemptDns'));
89
- $state = $this->getCircuitBreakerState();
90
- $this->assertSame('HALF-OPEN', $state['state']);
91
- }
92
-
93
- public function testSuccessOnHalfOpenResetsBreaker(): void
94
- {
95
- for ($i = 0; $i < 5; $i++) {
96
- $this->invokeMethod('recordDnsFailure');
97
- }
98
- usleep(1100000);
99
- $this->assertTrue($this->invokeMethod('canAttemptDns'));
100
-
101
- $this->invokeMethod('recordDnsSuccess');
102
- $state = $this->getCircuitBreakerState();
103
- $this->assertSame('CLOSED', $state['state']);
104
- $this->assertSame(0, $state['failureCount']);
105
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\FingerprintEngine;
8
+ use PHPUnit\Framework\TestCase;
9
+ use ReflectionClass;
10
+
11
+ class DnsCircuitBreakerTest extends TestCase
12
+ {
13
+ private ReflectionClass $reflection;
14
+
15
+ protected function setUp(): void
16
+ {
17
+ parent::setUp();
18
+ $this->reflection = new ReflectionClass(FingerprintEngine::class);
19
+
20
+ // Reset static circuit breaker state before each test
21
+ $this->setCircuitBreakerState([
22
+ 'state' => 'CLOSED',
23
+ 'failureCount' => 0,
24
+ 'lastStateChange' => 0,
25
+ 'threshold' => 5,
26
+ 'cooldown' => 1, // 1 second cooldown for faster test execution
27
+ ]);
28
+ }
29
+
30
+ private function getCircuitBreakerState(): array
31
+ {
32
+ $property = $this->reflection->getProperty('dnsCircuitBreaker');
33
+ if (PHP_VERSION_ID < 80100) {
34
+ $property->setAccessible(true);
35
+ }
36
+ return $property->getValue();
37
+ }
38
+
39
+ private function setCircuitBreakerState(array $state): void
40
+ {
41
+ $property = $this->reflection->getProperty('dnsCircuitBreaker');
42
+ if (PHP_VERSION_ID < 80100) {
43
+ $property->setAccessible(true);
44
+ }
45
+ $property->setValue(null, $state);
46
+ }
47
+
48
+ private function invokeMethod(string $methodName, array $args = [])
49
+ {
50
+ $method = $this->reflection->getMethod($methodName);
51
+ if (PHP_VERSION_ID < 80100) { // PHP 8.1.0
52
+ $method->setAccessible(true);
53
+ }
54
+ return $method->invokeArgs(null, $args);
55
+ }
56
+
57
+ public function testInitialStateIsClosed(): void
58
+ {
59
+ $this->assertTrue($this->invokeMethod('canAttemptDns'));
60
+ $state = $this->getCircuitBreakerState();
61
+ $this->assertSame('CLOSED', $state['state']);
62
+ }
63
+
64
+ public function testFailureThresholdTripsBreaker(): void
65
+ {
66
+ for ($i = 0; $i < 4; $i++) {
67
+ $this->invokeMethod('recordDnsFailure');
68
+ $this->assertTrue($this->invokeMethod('canAttemptDns'));
69
+ $state = $this->getCircuitBreakerState();
70
+ $this->assertSame('CLOSED', $state['state']);
71
+ }
72
+
73
+ $this->invokeMethod('recordDnsFailure');
74
+ $this->assertFalse($this->invokeMethod('canAttemptDns'));
75
+ $state = $this->getCircuitBreakerState();
76
+ $this->assertSame('OPEN', $state['state']);
77
+ }
78
+
79
+ public function testCooldownTransitionsToHalfOpen(): void
80
+ {
81
+ for ($i = 0; $i < 5; $i++) {
82
+ $this->invokeMethod('recordDnsFailure');
83
+ }
84
+ $this->assertFalse($this->invokeMethod('canAttemptDns'));
85
+
86
+ usleep(1100000); // 1.1s
87
+
88
+ $this->assertTrue($this->invokeMethod('canAttemptDns'));
89
+ $state = $this->getCircuitBreakerState();
90
+ $this->assertSame('HALF-OPEN', $state['state']);
91
+ }
92
+
93
+ public function testSuccessOnHalfOpenResetsBreaker(): void
94
+ {
95
+ for ($i = 0; $i < 5; $i++) {
96
+ $this->invokeMethod('recordDnsFailure');
97
+ }
98
+ usleep(1100000);
99
+ $this->assertTrue($this->invokeMethod('canAttemptDns'));
100
+
101
+ $this->invokeMethod('recordDnsSuccess');
102
+ $state = $this->getCircuitBreakerState();
103
+ $this->assertSame('CLOSED', $state['state']);
104
+ $this->assertSame(0, $state['failureCount']);
105
+ }
106
106
  }
@@ -1,71 +1,71 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\FingerprintClient;
8
- use PHPUnit\Framework\TestCase;
9
-
10
- class FingerprintClientTest extends TestCase
11
- {
12
- public function testDefaultConfigurationIncludesWasmAndDefaultPath(): void
13
- {
14
- $client = new FingerprintClient('/js/fingerprint.client.js');
15
- $scriptTag = $client->getScriptTag();
16
-
17
- // Vérifie que le script principal est présent
18
- $this->assertStringContainsString('src="/js/fingerprint.client.js"', $scriptTag);
19
-
20
- // Vérifie les configurations WASM par défaut dans l'objet sérialisé pour le JS
21
- $this->assertStringContainsString('"wasm":true', $scriptTag);
22
- $this->assertStringContainsString('"wasmPath":"\/fp.js"', $scriptTag);
23
- $this->assertStringContainsString('wasmScript.src = config.wasmPath;', $scriptTag);
24
- }
25
-
26
- public function testWasmCanBeOverriddenOrDisabled(): void
27
- {
28
- $client = new FingerprintClient('/js/fingerprint.client.js', [
29
- 'wasm' => false,
30
- 'wasmPath' => '/custom/path/to/wasm.js'
31
- ]);
32
- $scriptTag = $client->getScriptTag();
33
-
34
- // Vérifie que les surcharges sont correctement prises en compte
35
- $this->assertStringContainsString('"wasm":false', $scriptTag);
36
- $this->assertStringContainsString('"wasmPath":"\/custom\/path\/to\/wasm.js"', $scriptTag);
37
- }
38
-
39
- public function testGenerateHoneypotFieldAddsFieldToConfig(): void
40
- {
41
- $client = new FingerprintClient('/js/fingerprint.client.js');
42
-
43
- $honeypotFieldHtml = $client->generateHoneypotField('confirm_email_trap');
44
-
45
- // Vérifie le code HTML du champ Honeypot
46
- $this->assertStringContainsString('name="confirm_email_trap"', $honeypotFieldHtml);
47
- $this->assertStringContainsString('tabindex="-1"', $honeypotFieldHtml);
48
-
49
- // Vérifie que le champ s'enregistre dynamiquement dans la configuration JS finale
50
- $scriptTag = $client->getScriptTag();
51
- $this->assertStringContainsString('"honeypots":["confirm_email_trap"]', $scriptTag);
52
- }
53
-
54
- public function testNonceIsGeneratedAndAppliedToScripts(): void
55
- {
56
- $client = new FingerprintClient('/js/fingerprint.client.js');
57
- $nonce = $client->getNonce();
58
-
59
- // Vérifie que le nonce est généré correctement (32 caractères hexa)
60
- $this->assertNotNull($nonce);
61
- $this->assertEquals(32, strlen($nonce));
62
-
63
- $scriptTag = $client->getScriptTag();
64
-
65
- // Le nonce doit être présent sur la balise script HTML
66
- $this->assertStringContainsString('nonce="' . $nonce . '"', $scriptTag);
67
-
68
- // Et configuré dynamiquement lors de l'injection du script de chargement WASM
69
- $this->assertStringContainsString('wasmScript.nonce = \'' . $nonce . '\'', $scriptTag);
70
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\FingerprintClient;
8
+ use PHPUnit\Framework\TestCase;
9
+
10
+ class FingerprintClientTest extends TestCase
11
+ {
12
+ public function testDefaultConfigurationIncludesWasmAndDefaultPath(): void
13
+ {
14
+ $client = new FingerprintClient('/js/fingerprint.client.js');
15
+ $scriptTag = $client->getScriptTag();
16
+
17
+ // Verify main script tag is present
18
+ $this->assertStringContainsString('src="/js/fingerprint.client.js"', $scriptTag);
19
+
20
+ // Verify default WASM configuration serialized for JS runtime
21
+ $this->assertStringContainsString('"wasm":true', $scriptTag);
22
+ $this->assertStringContainsString('"wasmPath":"\/fp.js"', $scriptTag);
23
+ $this->assertStringContainsString('wasmScript.src = config.wasmPath;', $scriptTag);
24
+ }
25
+
26
+ public function testWasmCanBeOverriddenOrDisabled(): void
27
+ {
28
+ $client = new FingerprintClient('/js/fingerprint.client.js', [
29
+ 'wasm' => false,
30
+ 'wasmPath' => '/custom/path/to/wasm.js'
31
+ ]);
32
+ $scriptTag = $client->getScriptTag();
33
+
34
+ // Verify configuration overrides are properly applied
35
+ $this->assertStringContainsString('"wasm":false', $scriptTag);
36
+ $this->assertStringContainsString('"wasmPath":"\/custom\/path\/to\/wasm.js"', $scriptTag);
37
+ }
38
+
39
+ public function testGenerateHoneypotFieldAddsFieldToConfig(): void
40
+ {
41
+ $client = new FingerprintClient('/js/fingerprint.client.js');
42
+
43
+ $honeypotFieldHtml = $client->generateHoneypotField('confirm_email_trap');
44
+
45
+ // Verify HTML markup of the honeypot field
46
+ $this->assertStringContainsString('name="confirm_email_trap"', $honeypotFieldHtml);
47
+ $this->assertStringContainsString('tabindex="-1"', $honeypotFieldHtml);
48
+
49
+ // Verify field dynamically registers with client-side configuration
50
+ $scriptTag = $client->getScriptTag();
51
+ $this->assertStringContainsString('"honeypots":["confirm_email_trap"]', $scriptTag);
52
+ }
53
+
54
+ public function testNonceIsGeneratedAndAppliedToScripts(): void
55
+ {
56
+ $client = new FingerprintClient('/js/fingerprint.client.js');
57
+ $nonce = $client->getNonce();
58
+
59
+ // Verify nonce generation format (32 hex characters)
60
+ $this->assertNotNull($nonce);
61
+ $this->assertEquals(32, strlen($nonce));
62
+
63
+ $scriptTag = $client->getScriptTag();
64
+
65
+ // Nonce must be attached to HTML script tag
66
+ $this->assertStringContainsString('nonce="' . $nonce . '"', $scriptTag);
67
+
68
+ // And dynamically set during WASM loader script injection
69
+ $this->assertStringContainsString('wasmScript.nonce = \'' . $nonce . '\'', $scriptTag);
70
+ }
71
71
  }
@@ -1,40 +1,38 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\Config\SecurityProfiles;
8
- use Anonympins\Fingerprint\FingerprintEngine;
9
- use Anonympins\Fingerprint\ProblemManager;
10
- use Anonympins\Fingerprint\Store\InMemoryStore;
11
- use Anonympins\Fingerprint\Store\StoreManager;
12
- use PHPUnit\Framework\TestCase;
13
-
14
- class PowTest extends TestCase
15
- {
16
- private FingerprintEngine $engine;
17
-
18
- protected function setUp(): void
19
- {
20
- // 1. Configurer un store en mémoire pour l'isolation des tests.
21
- $store = new InMemoryStore();
22
- StoreManager::configureStore($store);
23
-
24
- // 2. Initialiser le ProblemManager avec une configuration et un store valides.
25
- // C'est l'étape cruciale qui manquait.
26
- $configPath = dirname(__FILE__) . '/config/problems.config.json';
27
- ProblemManager::getInstance($configPath, $store);
28
-
29
- // 3. Créer l'instance du moteur.
30
- $this->engine = new FingerprintEngine(SecurityProfiles::createSecurityProfile('balanced'));
31
- }
32
-
33
- public function testGetProblemsIsExposedForTesting(): void
34
- {
35
- // Cette méthode appelle ProblemManager::getInstance() en interne.
36
- // Grâce au setUp(), l'instance est déjà initialisée et le test passe.
37
- $problems = $this->engine->getProblems();
38
- $this->assertIsArray($problems);
39
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\Config\SecurityProfiles;
8
+ use Anonympins\Fingerprint\FingerprintEngine;
9
+ use Anonympins\Fingerprint\ProblemManager;
10
+ use Anonympins\Fingerprint\Store\InMemoryStore;
11
+ use Anonympins\Fingerprint\Store\StoreManager;
12
+ use PHPUnit\Framework\TestCase;
13
+
14
+ class PowTest extends TestCase
15
+ {
16
+ private FingerprintEngine $engine;
17
+
18
+ protected function setUp(): void
19
+ {
20
+ // 1. Configure an in-memory store for test isolation.
21
+ $store = new InMemoryStore();
22
+ StoreManager::configureStore($store);
23
+
24
+ // 2. Initialize ProblemManager with a valid configuration and store.
25
+ $configPath = dirname(__FILE__) . '/config/problems.config.json';
26
+ ProblemManager::getInstance($configPath, $store);
27
+
28
+ // 3. Create engine instance.
29
+ $this->engine = new FingerprintEngine(SecurityProfiles::createSecurityProfile('balanced'));
30
+ }
31
+
32
+ public function testGetProblemsIsExposedForTesting(): void
33
+ {
34
+ // Calls ProblemManager::getInstance() internally; setUp() ensures valid initialization.
35
+ $problems = $this->engine->getProblems();
36
+ $this->assertIsArray($problems);
37
+ }
40
38
  }
@@ -1,54 +1,116 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\RequestContext;
8
- use Anonympins\Fingerprint\Utils\RequestUtils;
9
- use PHPUnit\Framework\TestCase;
10
-
11
- class QuicFingerprintTest extends TestCase
12
- {
13
- public function testGetProtocolAnomalyScoreReturnsZeroIfNoFp(): void
14
- {
15
- $context = $this->createMock(RequestContext::class);
16
- $context->method('getHeader')->willReturn(null);
17
- $res = RequestUtils::getProtocolAnomalyScore($context);
18
- $this->assertEquals(0.0, $res['protocolAnomalyScore']);
19
- }
20
-
21
- public function testGetProtocolAnomalyScoreDetectsSpoofedChrome(): void
22
- {
23
- $context = $this->createMock(RequestContext::class);
24
- $context->method('getHeader')->willReturnCallback(function($name) {
25
- if ($name === 'user-agent') {
26
- return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
27
- }
28
- if ($name === 'x-quic-fp') {
29
- return '1;1=65536,4=50;i=0';
30
- }
31
- return null;
32
- });
33
-
34
- $res = RequestUtils::getProtocolAnomalyScore($context);
35
- $this->assertEquals(100.0, $res['protocolAnomalyScore']);
36
- }
37
-
38
- public function testGetProtocolAnomalyScoreAllowsLegitChrome(): void
39
- {
40
- $context = $this->createMock(RequestContext::class);
41
- $context->method('getHeader')->willReturnCallback(function($name) {
42
- if ($name === 'user-agent') {
43
- return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
44
- }
45
- if ($name === 'x-quic-fp') {
46
- return '1;1=1572864,4=100;u=2,i';
47
- }
48
- return null;
49
- });
50
-
51
- $res = RequestUtils::getProtocolAnomalyScore($context);
52
- $this->assertEquals(0.0, $res['protocolAnomalyScore']);
53
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\RequestContext;
8
+ use Anonympins\Fingerprint\Utils\RequestUtils;
9
+ use PHPUnit\Framework\TestCase;
10
+
11
+ class QuicFingerprintTest extends TestCase
12
+ {
13
+ public function testGetProtocolAnomalyScoreReturnsZeroIfNoFp(): void
14
+ {
15
+ $context = $this->createMock(RequestContext::class);
16
+ $context->method('getHeader')->willReturn(null);
17
+ $res = RequestUtils::getProtocolAnomalyScore($context);
18
+ $this->assertEquals(0.0, $res['protocolAnomalyScore']);
19
+ }
20
+
21
+ public function testGetProtocolAnomalyScoreDetectsSpoofedChrome(): void
22
+ {
23
+ $context = $this->createMock(RequestContext::class);
24
+ $context->method('getHeader')->willReturnCallback(function($name) {
25
+ if ($name === 'user-agent') {
26
+ return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
27
+ }
28
+ if ($name === 'x-quic-fp') {
29
+ return '1;1=65536,4=50;i=0';
30
+ }
31
+ return null;
32
+ });
33
+
34
+ $res = RequestUtils::getProtocolAnomalyScore($context);
35
+ $this->assertEquals(100.0, $res['protocolAnomalyScore']);
36
+ }
37
+
38
+ public function testGetProtocolAnomalyScoreAllowsLegitChrome(): void
39
+ {
40
+ $context = $this->createMock(RequestContext::class);
41
+ $context->method('getHeader')->willReturnCallback(function($name) {
42
+ if ($name === 'user-agent') {
43
+ return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
44
+ }
45
+ if ($name === 'x-quic-fp') {
46
+ return '1;1=1572864,4=100;u=2,i';
47
+ }
48
+ return null;
49
+ });
50
+
51
+ $res = RequestUtils::getProtocolAnomalyScore($context);
52
+ $this->assertEquals(0.0, $res['protocolAnomalyScore']);
53
+ }
54
+
55
+ public function testRfc9218PriorityWithoutIncrementalFlag(): void
56
+ {
57
+ $context = $this->createMock(RequestContext::class);
58
+ $context->method('getHeader')->willReturnCallback(function($name) {
59
+ if ($name === 'user-agent') return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
60
+ if ($name === 'x-quic-fp') return '1;1=1572864,4=100;u=2'; // Manque le drapeau 'i'
61
+ return null;
62
+ });
63
+ $res = RequestUtils::getProtocolAnomalyScore($context);
64
+ $this->assertEquals(40.0, $res['quicAnomalyScore']);
65
+ }
66
+
67
+ public function testCompressionRatioPenalty(): void
68
+ {
69
+ $context = $this->createMock(RequestContext::class);
70
+ $context->method('getHeader')->willReturnCallback(function($name) {
71
+ if ($name === 'user-agent') return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
72
+ if ($name === 'x-compression-info') return 'req_count:5,hpack_ratio:0.2';
73
+ return null;
74
+ });
75
+ $res = RequestUtils::getProtocolAnomalyScore($context);
76
+ $this->assertEquals(40.0, $res['http2AnomalyScore']);
77
+ }
78
+
79
+ public function testRfc9218UrgencyOutOfRange(): void
80
+ {
81
+ $context = $this->createMock(RequestContext::class);
82
+ $context->method('getHeader')->willReturnCallback(function($name) {
83
+ if ($name === 'user-agent') return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
84
+ if ($name === 'x-quic-fp') return '1;1=1572864,4=100;u=9,i';
85
+ return null;
86
+ });
87
+ $res = RequestUtils::getProtocolAnomalyScore($context);
88
+ $this->assertGreaterThanOrEqual(50.0, $res['quicAnomalyScore']);
89
+ }
90
+
91
+ public function testHttp2DefaultGoScraperDependencyTree(): void
92
+ {
93
+ $context = $this->createMock(RequestContext::class);
94
+ $context->method('getHeader')->willReturnCallback(function($name) {
95
+ if ($name === 'user-agent') return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
96
+ if ($name === 'x-http2-fingerprint') return 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:16|m,a,s,p|p:3,w:4,c:1';
97
+ return null;
98
+ });
99
+ $context->httpVersion = '2.0';
100
+ $res = RequestUtils::getProtocolAnomalyScore($context);
101
+ $this->assertGreaterThanOrEqual(55.0, $res['http2AnomalyScore']);
102
+ }
103
+
104
+ public function testScraperWithZeroDynamicTableEntries(): void
105
+ {
106
+ $context = $this->createMock(RequestContext::class);
107
+ $context->method('getHeader')->willReturnCallback(function($name) {
108
+ if ($name === 'user-agent') return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
109
+ if ($name === 'x-compression-info') return 'req_count:5,dynamic_table_entries:0';
110
+ return null;
111
+ });
112
+ $context->httpVersion = '2.0';
113
+ $res = RequestUtils::getProtocolAnomalyScore($context);
114
+ $this->assertGreaterThanOrEqual(45.0, $res['http2AnomalyScore']);
115
+ }
54
116
  }
@@ -61,4 +61,38 @@ class ThreatIntelTest extends TestCase
61
61
  $this->assertGreaterThan(75.0, $score);
62
62
  $this->assertLessThanOrEqual(95.0, $score);
63
63
  }
64
+
65
+ public function testDifferentialPrivacyDecoyInjectionAndTimestampNoise(): void
66
+ {
67
+ $config = [
68
+ 'federatedPeers' => ['https://peer1.example.com'],
69
+ 'federationSecret' => 'test-secret-key-32-chars-long!!',
70
+ 'differentialPrivacy' => [
71
+ 'enabled' => true,
72
+ 'epsilon' => 1.0,
73
+ 'dummyRate' => 1.0 // Force decoy injection for test
74
+ ]
75
+ ];
76
+
77
+ $postedCalls = [];
78
+ $engineSub = new class($config, $postedCalls) extends \Anonympins\Fingerprint\FingerprintEngine {
79
+ public array $calls = [];
80
+ public function __construct(array $cfg, array &$callsRef) {
81
+ $this->calls = &$callsRef;
82
+ parent::__construct($cfg);
83
+ }
84
+ protected function asyncPost(string $url, array $params): void {
85
+ $this->calls[] = ['url' => $url, 'params' => $params];
86
+ }
87
+ };
88
+
89
+ $refSub = new \ReflectionClass($engineSub);
90
+ $broadcastMethod = $refSub->getMethod('broadcastBannedZkp');
91
+ $broadcastMethod->setAccessible(true);
92
+
93
+ $realZkpY = 'realzkpykey12345';
94
+ $broadcastMethod->invoke($engineSub, $realZkpY);
95
+
96
+ $this->assertCount(2, $postedCalls, 'Should broadcast both real and decoy keys');
97
+ }
64
98
  }