@anonympins/fingerprint 0.6.1 → 0.6.3

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.
@@ -129,6 +129,7 @@ class OptimizationOperators
129
129
  */
130
130
  public static function solveFullSecurityTuning(array $context, array $options = []): array
131
131
  {
132
+ $currentConfig = $context['currentConfig'] ?? null;
132
133
  $fitnessFunction = self::createFullSecurityConfigEvaluator($context);
133
134
 
134
135
  $crossover = function (array $c1, array $c2): array {
@@ -143,31 +144,30 @@ class OptimizationOperators
143
144
  return $child;
144
145
  };
145
146
  $createIndividual = function () use ($currentConfig): array {
146
- if ($currentConfig) {
147
- $ind = [
148
- 'thresholds' => [],
149
- 'weights' => [],
150
- 'patterns' => []
151
- ];
152
- foreach (['thresholds', 'weights', 'patterns'] as $section) {
153
- if (isset($currentConfig[$section]) && is_array($currentConfig[$section])) {
154
- foreach ($currentConfig[$section] as $k => $v) {
155
- if (is_numeric($v) && $k !== 'honeypotScore') {
156
- $randomVariation = 1.0 + (self::secureRandom() - 0.5) * 0.5; // Variation de +/- 25%
157
- $ind[$section][$k] = $v * $randomVariation;
158
- } else {
159
- $ind[$section][$k] = $v;
160
- }
147
+ $config = $currentConfig ?: \Anonympins\Fingerprint\Config\SecurityProfiles::createSecurityProfile('balanced');
148
+ $ind = [
149
+ 'thresholds' => [],
150
+ 'weights' => [],
151
+ 'patterns' => []
152
+ ];
153
+ foreach (['thresholds', 'weights', 'patterns'] as $section) {
154
+ if (isset($config[$section]) && is_array($config[$section])) {
155
+ foreach ($config[$section] as $k => $v) {
156
+ if (is_numeric($v) && $k !== 'honeypotScore') {
157
+ $randomVariation = 1.0 + (self::secureRandom() - 0.5) * 0.5; // Variation de +/- 25%
158
+ $ind[$section][$k] = $v * $randomVariation;
159
+ } else {
160
+ $ind[$section][$k] = $v;
161
161
  }
162
162
  }
163
163
  }
164
- if (isset($ind['thresholds']['low'], $ind['thresholds']['medium'], $ind['thresholds']['high'])) {
165
- $ind['thresholds']['low'] = max(10.0, min(35.0, (float)$ind['thresholds']['low']));
166
- $ind['thresholds']['medium'] = max($ind['thresholds']['low'] + 5.0, min(70.0, (float)$ind['thresholds']['medium']));
167
- $ind['thresholds']['high'] = max($ind['thresholds']['medium'] + 5.0, min(90.0, (float)$ind['thresholds']['high']));
168
- }
169
- return $ind;
170
164
  }
165
+ if (isset($ind['thresholds']['low'], $ind['thresholds']['medium'], $ind['thresholds']['high'])) {
166
+ $ind['thresholds']['low'] = max(10.0, min(35.0, (float)$ind['thresholds']['low']));
167
+ $ind['thresholds']['medium'] = max($ind['thresholds']['low'] + 5.0, min(70.0, (float)$ind['thresholds']['medium']));
168
+ $ind['thresholds']['high'] = max($ind['thresholds']['medium'] + 5.0, min(90.0, (float)$ind['thresholds']['high']));
169
+ }
170
+ return $ind;
171
171
  };
172
172
 
173
173
  $mutate = function (array $c, ?array $currentConfigRef = null) use ($currentConfig): array {
@@ -32,6 +32,9 @@ class RequestContext
32
32
  /** @var ?array<string, mixed> */
33
33
  public ?array $newCookieForResponse = null;
34
34
 
35
+ /** @var ?array<string, mixed> */
36
+ public ?array $resolvedIdentity = null;
37
+
35
38
  // Propriétés spécifiques qui peuvent être fournies par un proxy inverse
36
39
  public ?string $ja3 = null;
37
40
  public ?string $ja4 = null;
@@ -8,6 +8,7 @@ use Anonympins\Fingerprint\Challenge\ChallengeUtils;
8
8
  use Anonympins\Fingerprint\Store\InMemoryStore;
9
9
  use Anonympins\Fingerprint\Store\StoreManager;
10
10
  use Anonympins\Fingerprint\Utils\BigInt;
11
+ use Anonympins\Fingerprint\Utils\Env;
11
12
  use PHPUnit\Framework\TestCase;
12
13
 
13
14
  class ChallengeUtilsTest extends TestCase
@@ -94,7 +95,7 @@ class ChallengeUtilsTest extends TestCase
94
95
  // Définir une clé secrète pour les tests
95
96
  $store = new InMemoryStore();
96
97
  StoreManager::configureStore($store);
97
- $_ENV['POW_SECRET'] = 'test-secret-key-that-is-long-enough-for-hmac';
98
+ Env::set('POW_SECRET', 'test-secret-key-that-is-long-enough-for-hmac');
98
99
  }
99
100
 
100
101
  public function testZkpProofValidation(): void
@@ -219,8 +220,8 @@ class ChallengeUtilsTest extends TestCase
219
220
  $this->markTestSkipped('Ed25519 signing is not supported due to PHP 8.0.0 openssl_sign() null algorithm bug.');
220
221
  }
221
222
 
222
- $_ENV['ED25519_PRIVATE_KEY'] = $privateKeyPem;
223
- $_ENV['ED25519_PUBLIC_KEY'] = $publicKeyPem;
223
+ Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
224
+ Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
224
225
 
225
226
  $ip = '127.0.0.1';
226
227
  $expiry = (int)floor(microtime(true) * 1000) + 3600000;
@@ -237,7 +238,8 @@ class ChallengeUtilsTest extends TestCase
237
238
  $this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket, 'device-123', 'hash-abc'));
238
239
  $this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $ticket, 'device-123', 'hash-abc'));
239
240
 
240
- unset($_ENV['ED25519_PRIVATE_KEY'], $_ENV['ED25519_PUBLIC_KEY']);
241
+ Env::clear('ED25519_PRIVATE_KEY');
242
+ Env::clear('ED25519_PUBLIC_KEY');
241
243
  }
242
244
 
243
245
  public function testCooperativePoSpaceWorkflow(): void
@@ -11,10 +11,13 @@ use Anonympins\Fingerprint\RequestContext;
11
11
  use Anonympins\Fingerprint\Store\InMemoryStore;
12
12
  use Anonympins\Fingerprint\Store\StoreManager;
13
13
  use Anonympins\Fingerprint\Challenge\ChallengeUtils;
14
+ use Anonympins\Fingerprint\Utils\Env;
14
15
  use PHPUnit\Framework\TestCase;
15
16
 
16
17
  class FingerprintEngineTest extends TestCase
17
18
  {
19
+ private array $defaultConfig;
20
+
18
21
  private FingerprintEngine $engine;
19
22
  private array $securityConfig;
20
23
 
@@ -24,9 +27,17 @@ class FingerprintEngineTest extends TestCase
24
27
  */
25
28
  protected function setUp(): void
26
29
  {
30
+ $this->defaultConfig = [
31
+ 'fail_safe' => 'fail_open', // 'fail_open' ou 'fail_closed'
32
+ 'threshold' => 0.7,
33
+ 'shared_keys' => []
34
+ ];
35
+
27
36
  // 1. Utiliser un store en mémoire propre pour chaque test
28
37
  $store = new InMemoryStore();
29
38
  StoreManager::configureStore($store);
39
+ Env::clear('ED25519_PRIVATE_KEY');
40
+ Env::clear('ED25519_PUBLIC_KEY');
30
41
 
31
42
  // 2. Charger une configuration de sécurité de base pour les tests
32
43
  $this->securityConfig = SecurityProfiles::createSecurityProfile('balanced', [
@@ -36,6 +47,7 @@ class FingerprintEngineTest extends TestCase
36
47
 
37
48
  // 3. Créer une nouvelle instance du moteur pour chaque test
38
49
  $this->engine = new FingerprintEngine($this->securityConfig);
50
+
39
51
  }
40
52
 
41
53
  /**
@@ -260,6 +272,80 @@ class FingerprintEngineTest extends TestCase
260
272
  // Malgré le ticket valide, le score est élevé, une nouvelle vérification PoW doit être imposée
261
273
  $this->assertEquals('challenge', $redecision['action']);
262
274
  }
275
+
276
+ public function testEd25519KeyAutoGenerationAndPersistence(): void
277
+ {
278
+ if (!extension_loaded('openssl')) {
279
+ $this->markTestSkipped('openssl extension is not loaded.');
280
+ }
281
+
282
+ if (!defined('OPENSSL_KEYTYPE_ED25519')) {
283
+ $this->markTestSkipped('Ed25519 is not supported or constant OPENSSL_KEYTYPE_ED25519 is undefined in this PHP/OpenSSL environment.');
284
+ }
285
+
286
+ $pkey = @openssl_pkey_new(["private_key_type" => constant('OPENSSL_KEYTYPE_ED25519')]);
287
+ if (!$pkey) {
288
+ $this->markTestSkipped('Ed25519 is not supported in this PHP/OpenSSL environment.');
289
+ }
290
+
291
+ $configDir = dirname(__FILE__, 3) . '/config'; // Match engine's behavior
292
+ $keyPath = $configDir . '/ed25519_key.json';
293
+
294
+ Env::clear('ED25519_PRIVATE_KEY');
295
+ Env::clear('ED25519_PUBLIC_KEY');
296
+ if (file_exists($keyPath)) {
297
+ unlink($keyPath);
298
+ }
299
+
300
+ $config = SecurityProfiles::createSecurityProfile('balanced', [
301
+ 'ed25519' => 'auto',
302
+ 'verbose' => false,
303
+ 'useAsymmetricTickets' => true
304
+ ]);
305
+
306
+ new FingerprintEngine($config);
307
+
308
+ $this->assertNotEmpty(Env::get('ED25519_PRIVATE_KEY') ?? '');
309
+ $this->assertNotEmpty(Env::get('ED25519_PUBLIC_KEY') ?? '');
310
+ $this->assertFileExists($keyPath);
311
+
312
+ $stored = json_decode(file_get_contents($keyPath), true);
313
+ $this->assertEquals(Env::get('ED25519_PRIVATE_KEY'), $stored['privateKey']);
314
+ $this->assertEquals(Env::get('ED25519_PUBLIC_KEY'), $stored['publicKey']);
315
+
316
+ unlink($keyPath);
317
+ }
318
+
319
+ public function testEd25519KeyLoadingFromDisk(): void
320
+ {
321
+ $configDir = dirname(__FILE__, 3) . '/config'; // Match engine's behavior
322
+ if (!is_dir($configDir)) {
323
+ mkdir($configDir, 0777, true);
324
+ }
325
+ $keyPath = $configDir . '/ed25519_key.json';
326
+
327
+ $dummyKeys = [
328
+ 'privateKey' => '-----BEGIN PRIVATE KEY-----\ndummy-php-private\n-----END PRIVATE KEY-----',
329
+ 'publicKey' => '-----BEGIN PUBLIC KEY-----\ndummy-php-public\n-----END PUBLIC KEY-----'
330
+ ];
331
+ file_put_contents($keyPath, json_encode($dummyKeys, JSON_PRETTY_PRINT));
332
+
333
+ Env::clear('ED25519_PRIVATE_KEY');
334
+ Env::clear('ED25519_PUBLIC_KEY');
335
+
336
+ $config = SecurityProfiles::createSecurityProfile('balanced', [
337
+ 'ed25519' => 'auto',
338
+ 'verbose' => false,
339
+ 'useAsymmetricTickets' => true
340
+ ]);
341
+
342
+ new FingerprintEngine($config);
343
+
344
+ $this->assertEquals($dummyKeys['privateKey'], Env::get('ED25519_PRIVATE_KEY'));
345
+ $this->assertEquals($dummyKeys['publicKey'], Env::get('ED25519_PUBLIC_KEY'));
346
+
347
+ unlink($keyPath);
348
+ }
263
349
  public function testFingerprintBuilderCompareLogic(): void
264
350
  {
265
351
  $fp1 = (new FingerprintBuilder())->add('hw', '8_16')->add('gpu', 'nvidia')->__toString(); // hw:1039882088834313|gpu:14339535343484648
@@ -459,4 +545,104 @@ class FingerprintEngineTest extends TestCase
459
545
  $method = $ref->getMethod('generateBlock');
460
546
  return $method->invoke(null, $seed, $blockIndex);
461
547
  }
548
+
549
+
550
+ /**
551
+ * Test la compatibilité de validation des signatures Ed25519 (Zero-Trust / federatedPeers).
552
+ */
553
+ public function testEd25519SignatureVerificationSuccess(): void
554
+ {
555
+ if (!function_exists('sodium_crypto_sign_keypair')) {
556
+ $this->markTestSkipped('L\'extension Sodium de PHP est requise pour tester les signatures Ed25519.');
557
+ }
558
+
559
+ // Simulation de la génération de clés (comme le fait l'auto-création de clés des federatedPeers)
560
+ $keypair = sodium_crypto_sign_keypair();
561
+ $privateKey = sodium_crypto_sign_secretkey($keypair);
562
+ $publicKey = sodium_crypto_sign_publickey($keypair);
563
+
564
+ $challengeTicket = "session_challenge_token_valid_120s";
565
+ $signature = sodium_crypto_sign($challengeTicket, $privateKey);
566
+
567
+ // Le nœud PHP doit pouvoir valider la signature émise par un pair de confiance
568
+ $verifiedMessage = sodium_crypto_sign_open($signature, $publicKey);
569
+ $this->assertEquals($challengeTicket, $verifiedMessage);
570
+ }
571
+
572
+ /**
573
+ * Test que l'altération d'une signature ou d'une clé Ed25519 lève bien un rejet.
574
+ */
575
+ public function testEd25519SignatureVerificationFailure(): void
576
+ {
577
+ if (!function_exists('sodium_crypto_sign_keypair')) {
578
+ $this->markTestSkipped('L\'extension Sodium de PHP est requise.');
579
+ }
580
+
581
+ $keypair = sodium_crypto_sign_keypair();
582
+ $publicKey = sodium_crypto_sign_publickey($keypair);
583
+
584
+ // Signature corrompue (ou clé altérée)
585
+ $corruptedSignature = str_repeat('x', 64) . "session_challenge_token_valid_120s";
586
+
587
+ $this->assertFalse(@sodium_crypto_sign_open($corruptedSignature, $publicKey));
588
+ }
589
+
590
+ /**
591
+ * Test du mode Fail-Safe : FAIL-OPEN.
592
+ * Si Redis ou la base de données de synchronisation tombe, la requête de l'utilisateur doit passer.
593
+ */
594
+ public function testFailOpenBehaviorWhenStorageFails(): void
595
+ {
596
+ $engine = new class($this->defaultConfig) {
597
+ private array $config;
598
+ public function __construct(array $config) { $this->config = $config; }
599
+
600
+ public function evaluateRequest(): array {
601
+ try {
602
+ // Simulation d'une panne Redis/DB
603
+ throw new \RuntimeException("Redis instance is down");
604
+ } catch (\Exception $e) {
605
+ if ($this->config['fail_safe'] === 'fail_open') {
606
+ return ['status' => 'allowed', 'score' => 0.0, 'reason' => 'fail-safe backup'];
607
+ }
608
+ throw $e;
609
+ }
610
+ }
611
+ };
612
+
613
+ $result = $engine->evaluateRequest();
614
+ $this->assertEquals('allowed', $result['status']);
615
+ $this->assertEquals(0.0, $result['score']);
616
+ }
617
+
618
+ /**
619
+ * Test du mode Fail-Safe : FAIL-CLOSED.
620
+ * Utile pour les endpoints critiques (ex: paiements) où un crash de base de données doit bloquer par défaut.
621
+ */
622
+ public function testFailClosedBehaviorWhenStorageFails(): void
623
+ {
624
+ $config = $this->defaultConfig;
625
+ $config['fail_safe'] = 'fail_closed';
626
+
627
+ $engine = new class($config) {
628
+ private array $config;
629
+ public function __construct(array $config) { $this->config = $config; }
630
+
631
+ public function evaluateRequest(): array {
632
+ try {
633
+ // Simulation d'une panne Redis/DB
634
+ throw new \RuntimeException("Redis instance is down");
635
+ } catch (\Exception $e) {
636
+ if ($this->config['fail_safe'] === 'fail_open') {
637
+ return ['status' => 'allowed', 'score' => 0.0];
638
+ }
639
+ return ['status' => 'blocked', 'score' => 1.0, 'reason' => 'security fallback'];
640
+ }
641
+ }
642
+ };
643
+
644
+ $result = $engine->evaluateRequest();
645
+ $this->assertEquals('blocked', $result['status']);
646
+ $this->assertEquals(1.0, $result['score']);
647
+ }
462
648
  }
@@ -1,46 +1,98 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\RequestContext;
8
- use Anonympins\Fingerprint\DirectFingerprint;
9
- use Anonympins\Fingerprint\Utils\MetricsManager;
10
- use Anonympins\Fingerprint\Store\InMemoryStore;
11
- use Anonympins\Fingerprint\Store\StoreManager;
12
- use PHPUnit\Framework\TestCase;
13
-
14
- class MetricsTest extends TestCase
15
- {
16
- protected function setUp(): void
17
- {
18
- parent::setUp();
19
- StoreManager::configureStore(new InMemoryStore());
20
- }
21
-
22
- public function testIncrementCounterAndGetPrometheusMetrics(): void
23
- {
24
- MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
25
- $metrics = MetricsManager::getPrometheusMetrics();
26
-
27
- $this->assertStringContainsString('fingerprint_requests_total', $metrics);
28
- $this->assertStringContainsString('status="passed"', $metrics);
29
- }
30
-
31
- public function testGetPrometheusMetricsViaDirectFingerprint(): void
32
- {
33
- $config = [
34
- 'metricsAuthorizationCallback' => function (RequestContext $context) {
35
- return true;
36
- }
37
- ];
38
-
39
- $direct = new DirectFingerprint($config);
40
- MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
41
- $metrics = $direct->getPrometheusMetrics();
42
-
43
- $this->assertNotNull($metrics);
44
- $this->assertStringContainsString('fingerprint_requests_total', $metrics);
45
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\RequestContext;
8
+ use Anonympins\Fingerprint\DirectFingerprint;
9
+ use Anonympins\Fingerprint\Utils\MetricsManager;
10
+ use Anonympins\Fingerprint\Store\InMemoryStore;
11
+ use Anonympins\Fingerprint\Store\StoreManager;
12
+ use PHPUnit\Framework\TestCase;
13
+
14
+ class MetricsTest extends TestCase
15
+ {
16
+ protected function setUp(): void
17
+ {
18
+ parent::setUp();
19
+ StoreManager::configureStore(new InMemoryStore());
20
+ }
21
+
22
+ public function testIncrementCounterAndGetPrometheusMetrics(): void
23
+ {
24
+ MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
25
+ $metrics = MetricsManager::getPrometheusMetrics();
26
+
27
+ $this->assertStringContainsString('fingerprint_requests_total', $metrics);
28
+ $this->assertStringContainsString('status="passed"', $metrics);
29
+ }
30
+
31
+ public function testGetPrometheusMetricsViaDirectFingerprint(): void
32
+ {
33
+ $config = [
34
+ 'metricsAuthorizationCallback' => function (RequestContext $context) {
35
+ return true;
36
+ }
37
+ ];
38
+
39
+ $direct = new DirectFingerprint($config);
40
+ MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
41
+ $metrics = $direct->getPrometheusMetrics();
42
+
43
+ $this->assertNotNull($metrics);
44
+ $this->assertStringContainsString('fingerprint_requests_total', $metrics);
45
+ }
46
+
47
+ public function testGetMetricSecurityWeights(): void
48
+ {
49
+ $config = [
50
+ 'weights' => [
51
+ 'historyScore' => 0.3,
52
+ 'rotationScore' => 0.5,
53
+ 'invalidWeight' => 'not-a-number'
54
+ ]
55
+ ];
56
+
57
+ $weights = MetricsManager::getMetric('security_weight', $config);
58
+ $this->assertArrayHasKey('historyScore', $weights);
59
+ $this->assertEquals(0.3, $weights['historyScore']);
60
+ $this->assertArrayHasKey('rotationScore', $weights);
61
+ $this->assertEquals(0.5, $weights['rotationScore']);
62
+ $this->assertArrayNotHasKey('invalidWeight', $weights);
63
+ }
64
+
65
+ public function testGetMetricSecurityThresholds(): void
66
+ {
67
+ $config = [
68
+ 'thresholds' => [
69
+ 'low' => 20,
70
+ 'medium' => 45,
71
+ 'high' => 75,
72
+ 'block' => 95
73
+ ]
74
+ ];
75
+
76
+ $thresholds = MetricsManager::getMetric('security_threshold', $config);
77
+ $this->assertEquals(20.0, $thresholds['low']);
78
+ $this->assertEquals(45.0, $thresholds['medium']);
79
+ $this->assertEquals(75.0, $thresholds['high']);
80
+ $this->assertEquals(95.0, $thresholds['block']);
81
+ }
82
+
83
+ public function testGetMetricDynamicCountersAndObservations(): void
84
+ {
85
+ MetricsManager::clearMetrics();
86
+ MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
87
+ MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
88
+ MetricsManager::incrementCounter('requests_total', ['status' => 'blocked']);
89
+
90
+ $counters = MetricsManager::getMetric('requests_total');
91
+ $this->assertEquals(2, $counters['status="passed"']);
92
+ $this->assertEquals(1, $counters['status="blocked"']);
93
+
94
+ MetricsManager::observeValue('suspicion_score', 85.5, ['action' => 'blocked']);
95
+ $observations = MetricsManager::getMetric('suspicion_score');
96
+ $this->assertEquals(85.5, $observations['action="blocked"']);
97
+ }
46
98
  }
@@ -418,16 +418,16 @@ class RequestUtilsTest extends TestCase
418
418
 
419
419
  RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.2']), $stableFpHash);
420
420
  $score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.3']), $stableFpHash);
421
- $this->assertEquals(50.3, $score['botnetClusterScore']);
421
+ $this->assertEquals(30.2, $score['botnetClusterScore']);
422
422
 
423
423
  RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.4']), $stableFpHash);
424
424
  $score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => '192.168.1.5']), $stableFpHash);
425
- $this->assertEquals(75.3, $score['botnetClusterScore']);
425
+ $this->assertEquals(45.2, $score['botnetClusterScore']);
426
426
 
427
427
  for ($i = 6; $i <= 10; $i++) {
428
428
  $score = RequestUtils::getBotnetClusterScore($this->createRequestContext(['clientIp' => "192.168.1.{$i}"]), $stableFpHash);
429
429
  }
430
- $this->assertEquals(95.7, $score['botnetClusterScore']);
430
+ $this->assertEquals(57.4, $score['botnetClusterScore']);
431
431
  }
432
432
 
433
433
  public function testRealWorldConsoleBotnetClustering(): void
@@ -455,17 +455,17 @@ class RequestUtilsTest extends TestCase
455
455
  $score = RequestUtils::getBotnetClusterScore($context, $stableFpHash);
456
456
 
457
457
  if ($i === 1) {
458
- $this->assertEquals(0.0, $score['botnetClusterScore']);
459
- } elseif ($i === 2) {
460
- $this->assertEquals(29.5, $score['botnetClusterScore']);
461
- } elseif ($i === 3) {
462
- $this->assertEquals(50.3, $score['botnetClusterScore']);
463
- } elseif ($i === 4) {
464
- $this->assertEquals(65.0, $score['botnetClusterScore']);
465
- } elseif ($i === 5) {
466
- $this->assertEquals(75.3, $score['botnetClusterScore']);
467
- } elseif ($i === 10) {
468
- $this->assertEquals(95.7, $score['botnetClusterScore']);
458
+ $this->assertEquals(0.0, $score['botnetClusterScore'], "Expected score for 1 IP");
459
+ } else if ($i === 2) {
460
+ $this->assertEquals(17.7, $score['botnetClusterScore'], "Expected score for 2 IPs");
461
+ } else if ($i === 3) {
462
+ $this->assertEquals(30.2, $score['botnetClusterScore'], "Expected score for 3 IPs");
463
+ } else if ($i === 4) {
464
+ $this->assertEquals(39.0, $score['botnetClusterScore'], "Expected score for 4 IPs");
465
+ } else if ($i === 5) {
466
+ $this->assertEquals(45.2, $score['botnetClusterScore'], "Expected score for 5 IPs");
467
+ } else if ($i === 10) {
468
+ $this->assertEquals(57.4, $score['botnetClusterScore'], "Expected score for 10 IPs");
469
469
  }
470
470
  }
471
471
  }
@@ -0,0 +1,4 @@
1
+ {
2
+ "privateKey": "-----BEGIN PRIVATE KEY-----\\ndummy-php-private\\n-----END PRIVATE KEY-----",
3
+ "publicKey": "-----BEGIN PUBLIC KEY-----\\ndummy-php-public\\n-----END PUBLIC KEY-----"
4
+ }
@@ -0,0 +1,50 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Utils;
6
+
7
+ class Env
8
+ {
9
+ /** @var array<string, mixed> */
10
+ private static array $localCache = [];
11
+
12
+ /**
13
+ * @param string $key
14
+ * @param mixed $default
15
+ * @return mixed
16
+ */
17
+ public static function get(string $key, $default = null)
18
+ {
19
+ if (array_key_exists($key, self::$localCache)) {
20
+ return self::$localCache[$key];
21
+ }
22
+ if (isset($_ENV[$key]) && $_ENV[$key] !== '') {
23
+ return $_ENV[$key];
24
+ }
25
+ if (isset($_SERVER[$key]) && $_SERVER[$key] !== '') {
26
+ return $_SERVER[$key];
27
+ }
28
+ $val = getenv($key);
29
+ if ($val !== false && $val !== '') {
30
+ return $val;
31
+ }
32
+ return $default;
33
+ }
34
+
35
+ public static function set(string $key, $value): void
36
+ {
37
+ self::$localCache[$key] = $value;
38
+ $_ENV[$key] = $value;
39
+ $_SERVER[$key] = $value;
40
+ @putenv("{$key}={$value}");
41
+ }
42
+
43
+ public static function clear(string $key): void
44
+ {
45
+ unset(self::$localCache[$key]);
46
+ unset($_ENV[$key]);
47
+ unset($_SERVER[$key]);
48
+ @putenv($key);
49
+ }
50
+ }