@anonympins/fingerprint 0.6.1 → 0.6.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.
- package/CHANGELOG.md +25 -0
- package/README.md +128 -119
- package/composer.json +2 -1
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +452 -109
- package/src/js/library.js +131 -54
- package/src/js/mongodb-store.js +13 -0
- package/src/js/problem-manager.js +6 -2
- package/src/js/redis-store.js +13 -0
- package/src/js/sql-store.js +80 -77
- package/src/js/tests/fingerprint.engine.test.js +2 -3
- package/src/js/tests/fingerprint.test.js +49 -12
- package/src/js/tests/metrics.test.js +157 -103
- package/src/php/Challenge/ChallengeUtils.php +8 -6
- package/src/php/Config/SecurityProfiles.php +8 -2
- package/src/php/FingerprintEngine.php +155 -41
- package/src/php/Optimization/OptimizationOperators.php +60 -36
- package/src/php/RequestContext.php +7 -0
- package/src/php/Store/IStore.php +6 -0
- package/src/php/Store/MongoDbStore.php +12 -0
- package/src/php/Store/RedisStore.php +12 -0
- package/src/php/Tests/ChallengeUtilsTest.php +6 -4
- package/src/php/Tests/FingerprintEngineTest.php +232 -0
- package/src/php/Tests/IpReputationTest.php +1 -0
- package/src/php/Tests/MetricsTest.php +97 -45
- package/src/php/Tests/RequestUtilsTest.php +14 -14
- package/src/php/Tests/config/ed25519_key.json +4 -0
- package/src/php/Utils/Env.php +50 -0
- package/src/php/Utils/MetricsManager.php +209 -166
- package/src/php/Utils/RequestUtils.php +105 -22
|
@@ -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
|
/**
|
|
@@ -216,6 +228,52 @@ class FingerprintEngineTest extends TestCase
|
|
|
216
228
|
$this->assertContains($decision['action'], ['challenge', 'block']);
|
|
217
229
|
}
|
|
218
230
|
|
|
231
|
+
public function testAllowsWhitelistedIpWithNumericFilterBelowThreshold(): void
|
|
232
|
+
{
|
|
233
|
+
$config = SecurityProfiles::createSecurityProfile('strict', [
|
|
234
|
+
'whitelist' => [['type' => 'allowlist', 'entries' => ['10.0.0.1']]],
|
|
235
|
+
'filterWhitelist' => 50.0,
|
|
236
|
+
'weights' => ['headerAnomalyScore' => 1.0],
|
|
237
|
+
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95]
|
|
238
|
+
]);
|
|
239
|
+
$engine = new FingerprintEngine($config);
|
|
240
|
+
|
|
241
|
+
$context = $this->createRequestContext([
|
|
242
|
+
'clientIp' => '10.0.0.1',
|
|
243
|
+
'headers' => [
|
|
244
|
+
'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',
|
|
245
|
+
'accept-language' => 'en-US,en;q=0.9'
|
|
246
|
+
]
|
|
247
|
+
]);
|
|
248
|
+
$decision = $engine->processRequest($context);
|
|
249
|
+
|
|
250
|
+
$this->assertEquals('next', $decision['action']);
|
|
251
|
+
$this->assertEquals(0, $decision['score']);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public function testAllowsWhitelistedIpWithNumericFilterExceedingThreshold(): void
|
|
255
|
+
{
|
|
256
|
+
$config = SecurityProfiles::createSecurityProfile('strict', [
|
|
257
|
+
'whitelist' => [['type' => 'allowlist', 'entries' => ['10.0.0.1']]],
|
|
258
|
+
'filterWhitelist' => 20.0,
|
|
259
|
+
'weights' => ['headerAnomalyScore' => 1.0],
|
|
260
|
+
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95]
|
|
261
|
+
]);
|
|
262
|
+
$engine = new FingerprintEngine($config);
|
|
263
|
+
|
|
264
|
+
$context = $this->createRequestContext([
|
|
265
|
+
'clientIp' => '10.0.0.1',
|
|
266
|
+
'headers' => [
|
|
267
|
+
'user-agent' => 'Chrome',
|
|
268
|
+
'accept-language' => 'en-US,en;q=0.9'
|
|
269
|
+
]
|
|
270
|
+
]);
|
|
271
|
+
$decision = $engine->processRequest($context);
|
|
272
|
+
|
|
273
|
+
$this->assertEquals('challenge', $decision['action']);
|
|
274
|
+
$this->assertEquals(60, $decision['score']);
|
|
275
|
+
}
|
|
276
|
+
|
|
219
277
|
public function testAllowsWhitelistedIp(): void
|
|
220
278
|
{
|
|
221
279
|
$config = SecurityProfiles::createSecurityProfile('strict', [
|
|
@@ -260,6 +318,80 @@ class FingerprintEngineTest extends TestCase
|
|
|
260
318
|
// Malgré le ticket valide, le score est élevé, une nouvelle vérification PoW doit être imposée
|
|
261
319
|
$this->assertEquals('challenge', $redecision['action']);
|
|
262
320
|
}
|
|
321
|
+
|
|
322
|
+
public function testEd25519KeyAutoGenerationAndPersistence(): void
|
|
323
|
+
{
|
|
324
|
+
if (!extension_loaded('openssl')) {
|
|
325
|
+
$this->markTestSkipped('openssl extension is not loaded.');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!defined('OPENSSL_KEYTYPE_ED25519')) {
|
|
329
|
+
$this->markTestSkipped('Ed25519 is not supported or constant OPENSSL_KEYTYPE_ED25519 is undefined in this PHP/OpenSSL environment.');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
$pkey = @openssl_pkey_new(["private_key_type" => constant('OPENSSL_KEYTYPE_ED25519')]);
|
|
333
|
+
if (!$pkey) {
|
|
334
|
+
$this->markTestSkipped('Ed25519 is not supported in this PHP/OpenSSL environment.');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
$configDir = dirname(__FILE__, 3) . '/config'; // Match engine's behavior
|
|
338
|
+
$keyPath = $configDir . '/ed25519_key.json';
|
|
339
|
+
|
|
340
|
+
Env::clear('ED25519_PRIVATE_KEY');
|
|
341
|
+
Env::clear('ED25519_PUBLIC_KEY');
|
|
342
|
+
if (file_exists($keyPath)) {
|
|
343
|
+
unlink($keyPath);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
$config = SecurityProfiles::createSecurityProfile('balanced', [
|
|
347
|
+
'ed25519' => 'auto',
|
|
348
|
+
'verbose' => false,
|
|
349
|
+
'useAsymmetricTickets' => true
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
new FingerprintEngine($config);
|
|
353
|
+
|
|
354
|
+
$this->assertNotEmpty(Env::get('ED25519_PRIVATE_KEY') ?? '');
|
|
355
|
+
$this->assertNotEmpty(Env::get('ED25519_PUBLIC_KEY') ?? '');
|
|
356
|
+
$this->assertFileExists($keyPath);
|
|
357
|
+
|
|
358
|
+
$stored = json_decode(file_get_contents($keyPath), true);
|
|
359
|
+
$this->assertEquals(Env::get('ED25519_PRIVATE_KEY'), $stored['privateKey']);
|
|
360
|
+
$this->assertEquals(Env::get('ED25519_PUBLIC_KEY'), $stored['publicKey']);
|
|
361
|
+
|
|
362
|
+
unlink($keyPath);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
public function testEd25519KeyLoadingFromDisk(): void
|
|
366
|
+
{
|
|
367
|
+
$configDir = dirname(__FILE__, 3) . '/config'; // Match engine's behavior
|
|
368
|
+
if (!is_dir($configDir)) {
|
|
369
|
+
mkdir($configDir, 0777, true);
|
|
370
|
+
}
|
|
371
|
+
$keyPath = $configDir . '/ed25519_key.json';
|
|
372
|
+
|
|
373
|
+
$dummyKeys = [
|
|
374
|
+
'privateKey' => '-----BEGIN PRIVATE KEY-----\ndummy-php-private\n-----END PRIVATE KEY-----',
|
|
375
|
+
'publicKey' => '-----BEGIN PUBLIC KEY-----\ndummy-php-public\n-----END PUBLIC KEY-----'
|
|
376
|
+
];
|
|
377
|
+
file_put_contents($keyPath, json_encode($dummyKeys, JSON_PRETTY_PRINT));
|
|
378
|
+
|
|
379
|
+
Env::clear('ED25519_PRIVATE_KEY');
|
|
380
|
+
Env::clear('ED25519_PUBLIC_KEY');
|
|
381
|
+
|
|
382
|
+
$config = SecurityProfiles::createSecurityProfile('balanced', [
|
|
383
|
+
'ed25519' => 'auto',
|
|
384
|
+
'verbose' => false,
|
|
385
|
+
'useAsymmetricTickets' => true
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
new FingerprintEngine($config);
|
|
389
|
+
|
|
390
|
+
$this->assertEquals($dummyKeys['privateKey'], Env::get('ED25519_PRIVATE_KEY'));
|
|
391
|
+
$this->assertEquals($dummyKeys['publicKey'], Env::get('ED25519_PUBLIC_KEY'));
|
|
392
|
+
|
|
393
|
+
unlink($keyPath);
|
|
394
|
+
}
|
|
263
395
|
public function testFingerprintBuilderCompareLogic(): void
|
|
264
396
|
{
|
|
265
397
|
$fp1 = (new FingerprintBuilder())->add('hw', '8_16')->add('gpu', 'nvidia')->__toString(); // hw:1039882088834313|gpu:14339535343484648
|
|
@@ -459,4 +591,104 @@ class FingerprintEngineTest extends TestCase
|
|
|
459
591
|
$method = $ref->getMethod('generateBlock');
|
|
460
592
|
return $method->invoke(null, $seed, $blockIndex);
|
|
461
593
|
}
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Test la compatibilité de validation des signatures Ed25519 (Zero-Trust / federatedPeers).
|
|
598
|
+
*/
|
|
599
|
+
public function testEd25519SignatureVerificationSuccess(): void
|
|
600
|
+
{
|
|
601
|
+
if (!function_exists('sodium_crypto_sign_keypair')) {
|
|
602
|
+
$this->markTestSkipped('L\'extension Sodium de PHP est requise pour tester les signatures Ed25519.');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Simulation de la génération de clés (comme le fait l'auto-création de clés des federatedPeers)
|
|
606
|
+
$keypair = sodium_crypto_sign_keypair();
|
|
607
|
+
$privateKey = sodium_crypto_sign_secretkey($keypair);
|
|
608
|
+
$publicKey = sodium_crypto_sign_publickey($keypair);
|
|
609
|
+
|
|
610
|
+
$challengeTicket = "session_challenge_token_valid_120s";
|
|
611
|
+
$signature = sodium_crypto_sign($challengeTicket, $privateKey);
|
|
612
|
+
|
|
613
|
+
// Le nœud PHP doit pouvoir valider la signature émise par un pair de confiance
|
|
614
|
+
$verifiedMessage = sodium_crypto_sign_open($signature, $publicKey);
|
|
615
|
+
$this->assertEquals($challengeTicket, $verifiedMessage);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Test que l'altération d'une signature ou d'une clé Ed25519 lève bien un rejet.
|
|
620
|
+
*/
|
|
621
|
+
public function testEd25519SignatureVerificationFailure(): void
|
|
622
|
+
{
|
|
623
|
+
if (!function_exists('sodium_crypto_sign_keypair')) {
|
|
624
|
+
$this->markTestSkipped('L\'extension Sodium de PHP est requise.');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
$keypair = sodium_crypto_sign_keypair();
|
|
628
|
+
$publicKey = sodium_crypto_sign_publickey($keypair);
|
|
629
|
+
|
|
630
|
+
// Signature corrompue (ou clé altérée)
|
|
631
|
+
$corruptedSignature = str_repeat('x', 64) . "session_challenge_token_valid_120s";
|
|
632
|
+
|
|
633
|
+
$this->assertFalse(@sodium_crypto_sign_open($corruptedSignature, $publicKey));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Test du mode Fail-Safe : FAIL-OPEN.
|
|
638
|
+
* Si Redis ou la base de données de synchronisation tombe, la requête de l'utilisateur doit passer.
|
|
639
|
+
*/
|
|
640
|
+
public function testFailOpenBehaviorWhenStorageFails(): void
|
|
641
|
+
{
|
|
642
|
+
$engine = new class($this->defaultConfig) {
|
|
643
|
+
private array $config;
|
|
644
|
+
public function __construct(array $config) { $this->config = $config; }
|
|
645
|
+
|
|
646
|
+
public function evaluateRequest(): array {
|
|
647
|
+
try {
|
|
648
|
+
// Simulation d'une panne Redis/DB
|
|
649
|
+
throw new \RuntimeException("Redis instance is down");
|
|
650
|
+
} catch (\Exception $e) {
|
|
651
|
+
if ($this->config['fail_safe'] === 'fail_open') {
|
|
652
|
+
return ['status' => 'allowed', 'score' => 0.0, 'reason' => 'fail-safe backup'];
|
|
653
|
+
}
|
|
654
|
+
throw $e;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
$result = $engine->evaluateRequest();
|
|
660
|
+
$this->assertEquals('allowed', $result['status']);
|
|
661
|
+
$this->assertEquals(0.0, $result['score']);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Test du mode Fail-Safe : FAIL-CLOSED.
|
|
666
|
+
* Utile pour les endpoints critiques (ex: paiements) où un crash de base de données doit bloquer par défaut.
|
|
667
|
+
*/
|
|
668
|
+
public function testFailClosedBehaviorWhenStorageFails(): void
|
|
669
|
+
{
|
|
670
|
+
$config = $this->defaultConfig;
|
|
671
|
+
$config['fail_safe'] = 'fail_closed';
|
|
672
|
+
|
|
673
|
+
$engine = new class($config) {
|
|
674
|
+
private array $config;
|
|
675
|
+
public function __construct(array $config) { $this->config = $config; }
|
|
676
|
+
|
|
677
|
+
public function evaluateRequest(): array {
|
|
678
|
+
try {
|
|
679
|
+
// Simulation d'une panne Redis/DB
|
|
680
|
+
throw new \RuntimeException("Redis instance is down");
|
|
681
|
+
} catch (\Exception $e) {
|
|
682
|
+
if ($this->config['fail_safe'] === 'fail_open') {
|
|
683
|
+
return ['status' => 'allowed', 'score' => 0.0];
|
|
684
|
+
}
|
|
685
|
+
return ['status' => 'blocked', 'score' => 1.0, 'reason' => 'security fallback'];
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
$result = $engine->evaluateRequest();
|
|
691
|
+
$this->assertEquals('blocked', $result['status']);
|
|
692
|
+
$this->assertEquals(1.0, $result['score']);
|
|
693
|
+
}
|
|
462
694
|
}
|
|
@@ -25,6 +25,7 @@ class IpReputationTest extends TestCase
|
|
|
25
25
|
public function set(string $key, $value, ?int $ttl = null): void { $this->data[$key] = $value; }
|
|
26
26
|
public function has(string $key): bool { return isset($this->data[$key]); }
|
|
27
27
|
public function delete(string $key): void { unset($this->data[$key]); }
|
|
28
|
+
public function clear(): void { $this->data = []; }
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
StoreManager::setStore($this->store);
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
}
|
|
460
|
-
$this->assertEquals(
|
|
461
|
-
}
|
|
462
|
-
$this->assertEquals(
|
|
463
|
-
}
|
|
464
|
-
$this->assertEquals(
|
|
465
|
-
}
|
|
466
|
-
$this->assertEquals(
|
|
467
|
-
}
|
|
468
|
-
$this->assertEquals(
|
|
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,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
|
+
}
|