@anonympins/fingerprint 0.3.2 → 0.3.4

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 (51) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +1080 -834
  3. package/composer.json +39 -0
  4. package/index.js +5 -0
  5. package/package.json +31 -23
  6. package/phpunit.xml +21 -0
  7. package/public/fp.js +2 -0
  8. package/src/js/build-client.js +69 -0
  9. package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -175
  10. package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -538
  11. package/src/js/fingerprint.client.obfuscated.js +1 -0
  12. package/{fingerprint.js → src/js/fingerprint.js} +3733 -3294
  13. package/{library.js → src/js/library.js} +1729 -1729
  14. package/{problem-manager.js → src/js/problem-manager.js} +539 -522
  15. package/src/php/AutoTuner.php +155 -0
  16. package/src/php/Challenge/ChallengeUtils.php +306 -0
  17. package/src/php/Config/SecurityProfiles.php +267 -0
  18. package/src/php/DirectFingerprint.php +81 -0
  19. package/src/php/FingerprintBuilder.php +186 -0
  20. package/src/php/FingerprintClient.php +132 -0
  21. package/src/php/FingerprintEngine.php +863 -0
  22. package/src/php/Optimization/FunctionRegistry.php +63 -0
  23. package/src/php/Optimization/Optimization.php +256 -0
  24. package/src/php/Optimization/OptimizationOperators.php +305 -0
  25. package/src/php/Optimization/ProblemInitializers.php +53 -0
  26. package/src/php/ProblemManager.php +255 -0
  27. package/src/php/RequestContext.php +91 -0
  28. package/src/php/Store/IStore.php +42 -0
  29. package/src/php/Store/InMemoryStore.php +67 -0
  30. package/src/php/Store/StoreManager.php +36 -0
  31. package/src/php/Tests/ChallengeUtilsTest.php +82 -0
  32. package/src/php/Tests/FingerprintBuilderTest.php +58 -0
  33. package/src/php/Tests/FingerprintEngineTest.php +300 -0
  34. package/src/php/Tests/IpReputationTest.php +157 -0
  35. package/src/php/Tests/PowTest.php +40 -0
  36. package/src/php/Tests/ProblemManagerTest.php +295 -0
  37. package/src/php/Tests/RequestUtilsTest.php +81 -0
  38. package/src/php/Tests/problems.config.json +9 -0
  39. package/src/php/Utils/BigInt.php +145 -0
  40. package/src/php/Utils/BlockList.php +100 -0
  41. package/src/php/Utils/Logger.php +30 -0
  42. package/src/php/Utils/MaliciousPatterns.php +59 -0
  43. package/src/php/Utils/RequestUtils.php +962 -0
  44. package/fingerprint.client.obfuscated.js +0 -1
  45. /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
  46. /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
  47. /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
  48. /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
  49. /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
  50. /package/{redis-store.js → src/js/redis-store.js} +0 -0
  51. /package/{sql-store.js → src/js/sql-store.js} +0 -0
@@ -0,0 +1,300 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use PHPUnit\Framework\TestCase;
8
+ use Anonympins\Fingerprint\FingerprintEngine;
9
+ use Anonympins\Fingerprint\FingerprintBuilder;
10
+ use Anonympins\Fingerprint\RequestContext;
11
+ use Anonympins\Fingerprint\Store\StoreManager;
12
+ use Anonympins\Fingerprint\Store\InMemoryStore;
13
+ use Anonympins\Fingerprint\Config\SecurityProfiles;
14
+
15
+ class FingerprintEngineTest extends TestCase
16
+ {
17
+ private FingerprintEngine $engine;
18
+ private array $securityConfig;
19
+
20
+ /**
21
+ * Cette méthode est appelée avant chaque test.
22
+ * Elle garantit que chaque test s'exécute dans un environnement propre.
23
+ */
24
+ protected function setUp(): void
25
+ {
26
+ // 1. Utiliser un store en mémoire propre pour chaque test
27
+ $store = new InMemoryStore();
28
+ StoreManager::configureStore($store);
29
+
30
+ // 2. Charger une configuration de sécurité de base pour les tests
31
+ $this->securityConfig = SecurityProfiles::createSecurityProfile('balanced', [
32
+ 'verbose' => false, // Désactiver les logs pour ne pas polluer la sortie des tests
33
+ 'challengeNewDevices' => false, // Simplifie les tests de base
34
+ ]);
35
+
36
+ // 3. Créer une nouvelle instance du moteur pour chaque test
37
+ $this->engine = new FingerprintEngine($this->securityConfig);
38
+ }
39
+
40
+ /**
41
+ * Crée un contexte de requête de base pour les tests.
42
+ *
43
+ * @param array<string, mixed> $overrides
44
+ * @return RequestContext
45
+ */
46
+ private function createRequestContext(array $overrides = []): RequestContext
47
+ {
48
+ $defaults = [
49
+ 'clientIp' => '192.168.1.10',
50
+ 'path' => '/',
51
+ 'headers' => [
52
+ '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',
53
+ 'accept-language' => 'en-US,en;q=0.9',
54
+ ],
55
+ 'query' => [],
56
+ 'body' => null,
57
+ 'cookies' => [],
58
+ 'httpVersion' => '1.1',
59
+ ];
60
+
61
+ $params = array_merge($defaults, $overrides);
62
+
63
+ return new RequestContext(
64
+ $params['clientIp'],
65
+ $params['path'],
66
+ $params['headers'],
67
+ $params['query'],
68
+ $params['body'],
69
+ $params['cookies'],
70
+ $params['httpVersion']
71
+ );
72
+ }
73
+
74
+ public function testAllowsLegitimateRequest(): void
75
+ {
76
+ $context = $this->createRequestContext();
77
+
78
+ // Simule une première visite
79
+ $firstDecision = $this->engine->processRequest($context);
80
+ $this->assertArrayHasKey('newCookieForResponse', $firstDecision, "Un nouveau cookie aurait dû être généré.");
81
+ $deviceIdCookie = $firstDecision['newCookieForResponse'];
82
+
83
+ // Simule une deuxième visite avec le cookie reçu
84
+ $nextContext = $this->createRequestContext([
85
+ 'cookies' => [$deviceIdCookie['name'] => $deviceIdCookie['value']]
86
+ ]);
87
+ $secondDecision = $this->engine->processRequest($nextContext);
88
+
89
+ $this->assertEquals('next', $secondDecision['action']);
90
+ $this->assertLessThan($this->securityConfig['thresholds']['low'], $secondDecision['score']);
91
+ }
92
+
93
+ public function testIssuesChallengeForSuspiciousRequest(): void
94
+ {
95
+ // 1. First, establish a device identity to avoid the cookie-dropping penalty
96
+ $firstContext = $this->createRequestContext();
97
+ $firstDecision = $this->engine->processRequest($firstContext);
98
+ $this->assertArrayHasKey('newCookieForResponse', $firstDecision, "A device ID cookie should have been generated.");
99
+ $deviceIdCookie = $firstDecision['newCookieForResponse'];
100
+
101
+ // 2. Now, simulate a suspicious request from that same device (e.g., missing user-agent)
102
+ $suspiciousContext = $this->createRequestContext([
103
+ 'cookies' => [$deviceIdCookie['name'] => $deviceIdCookie['value']],
104
+ 'headers' => [
105
+ // No user-agent, which is anomalous
106
+ 'accept-language' => 'en-US,en;q=0.9',
107
+ ]
108
+ ]);
109
+
110
+ $decision = $this->engine->processRequest($suspiciousContext);
111
+
112
+ $this->assertEquals('challenge', $decision['action'], "The action should be to issue a challenge.");
113
+ $this->assertGreaterThanOrEqual($this->securityConfig['thresholds']['low'], $decision['score'], "The score should be above the 'low' threshold.");
114
+ $this->assertLessThan($this->securityConfig['thresholds']['block'], $decision['score']);
115
+ }
116
+
117
+ public function testBlocksHighlySuspiciousRequest(): void
118
+ {
119
+ // Configure the engine to recognize the 'email_confirm' honeypot field.
120
+ // We need to create a new engine instance for this test with a custom config.
121
+ $config = SecurityProfiles::createSecurityProfile('balanced', [
122
+ 'honeypot' => ['fields' => ['email_confirm']],
123
+ 'verbose' => true,
124
+ 'weights'=> ['honeypotScore' => 1]
125
+ ]);
126
+ $engine = new FingerprintEngine($config);
127
+
128
+ // 1. First, establish a device identity to avoid other scores interfering.
129
+ $firstContext = $this->createRequestContext();
130
+ $firstDecision = $engine->processRequest($firstContext);
131
+ $this->assertArrayHasKey('newCookieForResponse', $firstDecision);
132
+ $deviceIdCookie = $firstDecision['newCookieForResponse'];
133
+
134
+ // 2. Now, simulate a request from that device that falls into a honeypot trap.
135
+ $honeypotContext = $this->createRequestContext([
136
+ 'cookies' => [$deviceIdCookie['name'] => $deviceIdCookie['value']],
137
+ // The bot filled in a hidden field, triggering the honeypot.
138
+ 'body' => ['email_confirm' => 'bot@example.com']
139
+ ]);
140
+
141
+ $decision = $engine->processRequest($honeypotContext);
142
+
143
+ $this->assertEquals('block', $decision['action']);
144
+ $this->assertGreaterThanOrEqual($config['thresholds']['block'], $decision['score']);
145
+ }
146
+
147
+ public function testDetectsCookieDropping(): void
148
+ {
149
+ $context = $this->createRequestContext(['clientIp' => '1.2.3.4']);
150
+
151
+ // 1. Première requête, un "pending_cookie" est défini pour cette IP
152
+ $this->engine->processRequest($context);
153
+
154
+ // 2. Deuxième requête de la même IP, mais sans le cookie attendu
155
+ $contextWithoutCookie = $this->createRequestContext(['clientIp' => '1.2.3.4']);
156
+ $decision = $this->engine->processRequest($contextWithoutCookie);
157
+
158
+ // Le score de "cookieDropping" devrait être de 100
159
+ $this->assertEquals(100, $decision['vector']['cookieDroppingScore']);
160
+ // L'action devrait être un challenge car le score final sera élevé
161
+ $this->assertEquals('challenge', $decision['action']);
162
+ }
163
+
164
+ public function testDetectsFingerprintInconsistency(): void
165
+ {
166
+ $context = $this->createRequestContext();
167
+
168
+ // 1. Première visite, le fingerprint initial est stocké
169
+ $firstDecision = $this->engine->processRequest($context);
170
+ $deviceIdCookie = $firstDecision['newCookieForResponse'];
171
+
172
+ // 2. Deuxième visite avec le même cookie, mais un User-Agent complètement différent (incohérence)
173
+ $nextContext = $this->createRequestContext([
174
+ 'cookies' => [$deviceIdCookie['name'] => $deviceIdCookie['value']],
175
+ 'headers' => [
176
+ 'user-agent' => 'DefinitelyNotTheSameBrowser/1.0',
177
+ 'accept-language' => 'fr-FR',
178
+ ]
179
+ ]);
180
+
181
+ $decision = $this->engine->processRequest($nextContext);
182
+
183
+ // Le score d'incohérence devrait être de 100
184
+ $this->assertEquals(100, $decision['vector']['inconsistencyScore']);
185
+ // L'action devrait être un challenge ou un blocage
186
+ $this->assertContains($decision['action'], ['challenge', 'block']);
187
+ }
188
+
189
+ public function testAllowsWhitelistedIp(): void
190
+ {
191
+ $config = SecurityProfiles::createSecurityProfile('strict', [
192
+ 'whitelist' => [['type' => 'allowlist', 'entries' => ['10.0.0.1']]]
193
+ ]);
194
+ $engine = new FingerprintEngine($config);
195
+
196
+ $context = $this->createRequestContext(['clientIp' => '10.0.0.1']);
197
+ $decision = $engine->processRequest($context);
198
+
199
+ $this->assertEquals('next', $decision['action']);
200
+ $this->assertEquals(0, $decision['score']);
201
+ }
202
+ public function testFingerprintBuilderCompareLogic(): void
203
+ {
204
+ $fp1 = (new FingerprintBuilder())->add('hw', '8_16')->add('gpu', 'nvidia')->__toString(); // hw:1039882088834313|gpu:14339535343484648
205
+ $fp2 = (new FingerprintBuilder())->add('hw', '8_16')->add('gpu', 'nvidia')->__toString(); // hw:1039882088834313|gpu:14339535343484648
206
+ $fp3 = (new FingerprintBuilder())->add('hw', '4_8')->add('gpu', 'amd')->__toString(); // hw:1039882088834313|gpu:14339535343484648
207
+ $fp4 = (new FingerprintBuilder())->add('hw', '8_16')->add('os', 'win32')->__toString(); // hw:1039882088834313|os:14339535343484648
208
+
209
+ $this->assertEquals(1.0, FingerprintBuilder::compare($fp1, $fp2), "Identical FPs should return 1.0");
210
+ $this->assertLessThan(0.5, FingerprintBuilder::compare($fp1, $fp3), "Different FPs should have low similarity");
211
+
212
+ // The partial match score calculation reflects the current weights in FingerprintBuilder.
213
+ // Matching keys: 'hw' (weight 1.5).
214
+ // All relevant keys considered from both fingerprints: 'hw' (1.5), 'gpu' (4.0), 'os' (0.8).
215
+ // Total weight = 1.5 + 4.0 + 0.8 = 6.3.
216
+ // Score = 1.5 / 6.3 = ~0.238095... (This logic is correct, the test is fine)
217
+ $this->assertEqualsWithDelta(0.238, FingerprintBuilder::compare($fp1, $fp4), 0.001, "Partial match score should reflect current weights"); // This test is correct, no change needed.
218
+ }
219
+
220
+ /**
221
+ * @dataProvider clientHintsInconsistencyProvider
222
+ * @param array<string, string> $headers Les en-têtes à tester.
223
+ * @param float $expectedScore Le score attendu.
224
+ * @param string $message Le message de test.
225
+ */
226
+ public function testDetectsClientHintsInconsistency(array $headers, float $expectedScore, string $message): void
227
+ {
228
+ // Le test nécessite un deviceId, donc on simule une première visite.
229
+ // Les en-têtes de la première visite n'ont pas d'importance ici.
230
+ $firstDecision = $this->engine->processRequest($this->createRequestContext());
231
+ $deviceIdCookie = $firstDecision['newCookieForResponse'] ?? null;
232
+ $this->assertNotNull($deviceIdCookie, "Un cookie deviceId aurait dû être créé.");
233
+
234
+ // On simule la visite suivante avec les en-têtes à tester.
235
+ $testContext = $this->createRequestContext([
236
+ 'cookies' => [$deviceIdCookie['name'] => $deviceIdCookie['value']],
237
+ 'headers' => $headers,
238
+ ]);
239
+
240
+ $decision = $this->engine->processRequest($testContext);
241
+ $this->assertEquals($expectedScore, $decision['vector']['clientHintsInconsistencyScore'], $message);
242
+ }
243
+
244
+ /**
245
+ * Fournisseur de données pour le test d'incohérence des Client Hints.
246
+ * @return array<string, array{0: array<string, string>, 1: float, 2: string}>
247
+ */
248
+ public function clientHintsInconsistencyProvider(): array
249
+ {
250
+ $baseHeaders = ['accept-language' => 'en-US,en;q=0.9'];
251
+
252
+ return [
253
+ 'no inconsistency' => [
254
+ array_merge($baseHeaders, [
255
+ 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
256
+ 'sec-ch-ua' => '"Not/A)Brand";v="99", "Google Chrome";v="120", "Chromium";v="120"',
257
+ ]),
258
+ 0.0,
259
+ 'Should return 0 for consistent headers.'
260
+ ],
261
+ 'large version mismatch' => [
262
+ array_merge($baseHeaders, [
263
+ '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',
264
+ 'sec-ch-ua' => '"Not/A)Brand";v="99", "Google Chrome";v="120", "Chromium";v="120"',
265
+ ]),
266
+ 80.0,
267
+ 'Should return 80 for a large version mismatch (>5).'
268
+ ],
269
+ 'small version mismatch' => [
270
+ array_merge($baseHeaders, [
271
+ 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36',
272
+ 'sec-ch-ua' => '"Not/A)Brand";v="99", "Google Chrome";v="120", "Chromium";v="120"',
273
+ ]),
274
+ 40.0,
275
+ 'Should return 40 for a small version mismatch (>1).'
276
+ ],
277
+ 'browser mismatch' => [
278
+ array_merge($baseHeaders, [
279
+ 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
280
+ 'sec-ch-ua' => '"Not/A)Brand";v="99", "Google Chrome";v="120", "Chromium";v="120"',
281
+ ]),
282
+ 90.0,
283
+ 'Should return 90 for a browser mismatch (Firefox vs Chrome).'
284
+ ],
285
+ 'missing client hints header' => [
286
+ ['user-agent' => 'Mozilla/5.0 Chrome/120.0.0.0'],
287
+ 0.0,
288
+ 'Should return 0 if sec-ch-ua header is missing.'
289
+ ],
290
+ 'malformed client hints' => [
291
+ array_merge($baseHeaders, [
292
+ 'user-agent' => 'Mozilla/5.0 Chrome/120.0.0.0',
293
+ 'sec-ch-ua' => '"SomeOtherBrowser";v="abc"',
294
+ ]),
295
+ 0.0,
296
+ 'Should return 0 if headers are malformed or unparsable.'
297
+ ],
298
+ ];
299
+ }
300
+ }
@@ -0,0 +1,157 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use PHPUnit\Framework\TestCase;
8
+ use Anonympins\Fingerprint\Utils\RequestUtils;
9
+ use Anonympins\Fingerprint\Store\StoreManager;
10
+ use Anonympins\Fingerprint\Store\IStore;
11
+ use Anonympins\Fingerprint\RequestContext;
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
+ }
157
+ }
@@ -0,0 +1,40 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use PHPUnit\Framework\TestCase;
8
+ use Anonympins\Fingerprint\FingerprintEngine;
9
+ use Anonympins\Fingerprint\ProblemManager;
10
+ use Anonympins\Fingerprint\Store\InMemoryStore;
11
+ use Anonympins\Fingerprint\Store\StoreManager;
12
+ use Anonympins\Fingerprint\Config\SecurityProfiles;
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__) . '/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
+ }
40
+ }