@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.
- package/CHANGELOG.md +14 -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 +6802 -6601
- package/src/js/library.js +18 -11
- package/src/js/problem-manager.js +6 -2
- package/src/js/tests/fingerprint.test.js +47 -10
- package/src/js/tests/metrics.test.js +157 -103
- package/src/php/Challenge/ChallengeUtils.php +6 -4
- package/src/php/Config/SecurityProfiles.php +3 -2
- package/src/php/FingerprintEngine.php +56 -19
- package/src/php/Optimization/OptimizationOperators.php +21 -21
- package/src/php/RequestContext.php +3 -0
- package/src/php/Tests/ChallengeUtilsTest.php +6 -4
- package/src/php/Tests/FingerprintEngineTest.php +186 -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 +63 -13
|
@@ -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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|