@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.
@@ -1,104 +1,158 @@
1
- import { describe, expect, it, vi, beforeEach } from 'vitest';
2
- import { handleMetricsRequest, __internal } from '../fingerprint.js';
3
-
4
- describe('Metrics & Authorization Callback Integration', () => {
5
- let req, res;
6
-
7
- beforeEach(() => {
8
- req = {
9
- ip: '127.0.0.1',
10
- path: '/metrics',
11
- headers: { 'user-agent': 'prometheus' },
12
- query: {},
13
- body: {},
14
- cookies: {},
15
- httpVersion: '1.1'
16
- };
17
- res = {
18
- status: vi.fn().mockReturnThis(),
19
- send: vi.fn().mockReturnThis(),
20
- set: vi.fn().mockReturnThis(),
21
- redirect: vi.fn().mockReturnThis()
22
- };
23
- });
24
- it('should return metrics with 200 Content-Type if authorized', async () => {
25
- const config = {
26
- metricsAuthorizationCallback: () => true
27
- };
28
- await handleMetricsRequest(req, res, config);
29
-
30
- expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
31
- expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_requests_total'));
32
- });
33
-
34
- it('should include current weights and thresholds in prometheus metrics output', async () => {
35
- const config = {
36
- metricsAuthorizationCallback: () => true,
37
- weights: { historyScore: 0.35, rotationScore: 0.65 },
38
- thresholds: { low: 22, block: 92 }
39
- };
40
- await handleMetricsRequest(req, res, config);
41
-
42
- expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
43
- expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_weight{indicator="historyScore"} 0.35'));
44
- expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_weight{indicator="rotationScore"} 0.65'));
45
- expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_threshold{level="low"} 22'));
46
- expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_threshold{level="block"} 92'));
47
- });
48
-
49
- it('should include auto-tuning performance metrics if a best solution is available', async () => {
50
- __internal.setLastBestSolution({
51
- solution: {},
52
- objectives: [0.0123, 0.0456]
53
- });
54
-
55
- const config = {
56
- metricsAuthorizationCallback: () => true
57
- };
58
- await handleMetricsRequest(req, res, config);
59
-
60
- expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
61
- expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_autotuning_false_positive_rate 0.0123'));
62
- expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_autotuning_false_negative_rate 0.0456'));
63
-
64
- // Cleanup
65
- __internal.setLastBestSolution(null);
66
- });
67
-
68
- it('should return 403 if authorization callback returns false', async () => {
69
- const config = {
70
- metricsAuthorizationCallback: () => false
71
- };
72
- await handleMetricsRequest(req, res, config);
73
-
74
- expect(res.status).toHaveBeenCalledWith(403);
75
- expect(res.send).toHaveBeenCalledWith('Access to metrics denied.');
76
- });
77
-
78
- it('should handle custom block action returned by authorization callback', async () => {
79
- const config = {
80
- metricsAuthorizationCallback: () => ({
81
- action: 'block',
82
- status: 401,
83
- body: 'Custom unauthorized message'
84
- })
85
- };
86
- await handleMetricsRequest(req, res, config);
87
-
88
- expect(res.status).toHaveBeenCalledWith(401);
89
- expect(res.send).toHaveBeenCalledWith('Custom unauthorized message');
90
- });
91
-
92
- it('should handle custom redirect action returned by authorization callback', async () => {
93
- const config = {
94
- metricsAuthorizationCallback: () => ({
95
- action: 'redirect',
96
- status: 302,
97
- path: '/forbidden'
98
- })
99
- };
100
- await handleMetricsRequest(req, res, config);
101
-
102
- expect(res.redirect).toHaveBeenCalledWith(302, '/forbidden');
103
- });
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { handleMetricsRequest, getMetric, incrementCounter, observeValue, __internal } from '../fingerprint.js';
3
+
4
+ describe('Metrics & Authorization Callback Integration', () => {
5
+ let req, res;
6
+
7
+ beforeEach(() => {
8
+ req = {
9
+ ip: '127.0.0.1',
10
+ path: '/metrics',
11
+ headers: { 'user-agent': 'prometheus' },
12
+ query: {},
13
+ body: {},
14
+ cookies: {},
15
+ httpVersion: '1.1'
16
+ };
17
+ res = {
18
+ status: vi.fn().mockReturnThis(),
19
+ send: vi.fn().mockReturnThis(),
20
+ set: vi.fn().mockReturnThis(),
21
+ redirect: vi.fn().mockReturnThis()
22
+ };
23
+ });
24
+ it('should return metrics with 200 Content-Type if authorized', async () => {
25
+ const config = {
26
+ metricsAuthorizationCallback: () => true
27
+ };
28
+ await handleMetricsRequest(req, res, config);
29
+
30
+ expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
31
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_requests_total'));
32
+ });
33
+
34
+ it('should include current weights and thresholds in prometheus metrics output', async () => {
35
+ const config = {
36
+ metricsAuthorizationCallback: () => true,
37
+ weights: { historyScore: 0.35, rotationScore: 0.65 },
38
+ thresholds: { low: 22, block: 92 }
39
+ };
40
+ await handleMetricsRequest(req, res, config);
41
+
42
+ expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
43
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_weight{indicator="historyScore"} 0.35'));
44
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_weight{indicator="rotationScore"} 0.65'));
45
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_threshold{level="low"} 22'));
46
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_threshold{level="block"} 92'));
47
+ });
48
+
49
+ it('should include auto-tuning performance metrics if a best solution is available', async () => {
50
+ __internal.setLastBestSolution({
51
+ solution: {},
52
+ objectives: [0.0123, 0.0456]
53
+ });
54
+
55
+ const config = {
56
+ metricsAuthorizationCallback: () => true
57
+ };
58
+ await handleMetricsRequest(req, res, config);
59
+
60
+ expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
61
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_autotuning_false_positive_rate 0.0123'));
62
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_autotuning_false_negative_rate 0.0456'));
63
+
64
+ // Cleanup
65
+ __internal.setLastBestSolution(null);
66
+ });
67
+
68
+ it('should return 403 if authorization callback returns false', async () => {
69
+ const config = {
70
+ metricsAuthorizationCallback: () => false
71
+ };
72
+ await handleMetricsRequest(req, res, config);
73
+
74
+ expect(res.status).toHaveBeenCalledWith(403);
75
+ expect(res.send).toHaveBeenCalledWith('Access to metrics denied.');
76
+ });
77
+
78
+ it('should handle custom block action returned by authorization callback', async () => {
79
+ const config = {
80
+ metricsAuthorizationCallback: () => ({
81
+ action: 'block',
82
+ status: 401,
83
+ body: 'Custom unauthorized message'
84
+ })
85
+ };
86
+ await handleMetricsRequest(req, res, config);
87
+
88
+ expect(res.status).toHaveBeenCalledWith(401);
89
+ expect(res.send).toHaveBeenCalledWith('Custom unauthorized message');
90
+ });
91
+
92
+ it('should handle custom redirect action returned by authorization callback', async () => {
93
+ const config = {
94
+ metricsAuthorizationCallback: () => ({
95
+ action: 'redirect',
96
+ status: 302,
97
+ path: '/forbidden'
98
+ })
99
+ };
100
+ await handleMetricsRequest(req, res, config);
101
+
102
+ expect(res.redirect).toHaveBeenCalledWith(302, '/forbidden');
103
+ });
104
+ });
105
+
106
+ describe('getMetric Unit Tests', () => {
107
+ it('should retrieve security weights from config', () => {
108
+ const config = {
109
+ weights: { historyScore: 0.5, rotationScore: 0.2, someInvalidWeight: 'not-a-number' }
110
+ };
111
+ const weights = getMetric('security_weight', config);
112
+ expect(weights.get('historyScore')).toBe(0.5);
113
+ expect(weights.get('rotationScore')).toBe(0.2);
114
+ expect(weights.has('someInvalidWeight')).toBe(false);
115
+ });
116
+
117
+ it('should retrieve security thresholds from config', () => {
118
+ const config = {
119
+ thresholds: { low: 10, medium: 30, high: 70, block: 90 }
120
+ };
121
+ const thresholds = getMetric('security_threshold', config);
122
+ expect(thresholds.get('low')).toBe(10);
123
+ expect(thresholds.get('medium')).toBe(30);
124
+ expect(thresholds.get('high')).toBe(70);
125
+ expect(thresholds.get('block')).toBe(90);
126
+ });
127
+
128
+ it('should retrieve dynamic counters', () => {
129
+ incrementCounter('requests_total', { status: 'passed' });
130
+ incrementCounter('requests_total', { status: 'blocked' });
131
+
132
+ const metric = getMetric('requests_total');
133
+ expect(metric.get('status="passed"')).toBeGreaterThanOrEqual(1);
134
+ expect(metric.get('status="blocked"')).toBeGreaterThanOrEqual(1);
135
+ });
136
+
137
+ it('should retrieve dynamic observations', () => {
138
+ observeValue('suspicion_score', 45.5, { action: 'passed' });
139
+
140
+ const metric = getMetric('suspicion_score');
141
+ expect(metric.get('action="passed"')).toBe(45.5);
142
+ });
143
+
144
+ it('should retrieve autotuning false positive and negative rates', () => {
145
+ __internal.setLastBestSolution({
146
+ solution: {},
147
+ objectives: [0.015, 0.045]
148
+ });
149
+
150
+ const fpr = getMetric('autotuning_false_positive_rate');
151
+ const fnr = getMetric('autotuning_false_negative_rate');
152
+
153
+ expect(fpr.get('fpr')).toBe(0.015);
154
+ expect(fnr.get('fnr')).toBe(0.045);
155
+
156
+ __internal.setLastBestSolution(null);
157
+ });
104
158
  });
@@ -8,6 +8,7 @@ use Anonympins\Fingerprint\Store\StoreManager;
8
8
  use Anonympins\Fingerprint\FingerprintBuilder;
9
9
  use Anonympins\Fingerprint\Utils\BigInt;
10
10
  use Anonympins\Fingerprint\Utils\RequestUtils;
11
+ use Anonympins\Fingerprint\Utils\Env;
11
12
 
12
13
  /**
13
14
  * Classe utilitaire pour la génération et la vérification des challenges Proof-of-Work.
@@ -397,8 +398,9 @@ class ChallengeUtils
397
398
  */
398
399
  public static function getPowSecret(): string
399
400
  {
400
- $secret = $_ENV['POW_SECRET'] ?? getenv('POW_SECRET');
401
- if (!$secret && ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production') {
401
+ $secret = Env::get('POW_SECRET');
402
+ $appEnv = Env::get('APP_ENV');
403
+ if (!$secret && $appEnv === 'production') {
402
404
  throw new \RuntimeException('POW_SECRET environment variable is not set. This is required for production.');
403
405
  }
404
406
  return $secret ?: "fallback-dev-secret-32-chars-minimum";
@@ -411,7 +413,7 @@ class ChallengeUtils
411
413
  */
412
414
  public static function generateStatelessTicket(array $payload): string
413
415
  {
414
- $ed25519Key = $_ENV['ED25519_PRIVATE_KEY'] ?? getenv('ED25519_PRIVATE_KEY');
416
+ $ed25519Key = Env::get('ED25519_PRIVATE_KEY');
415
417
  if ($ed25519Key) {
416
418
  try {
417
419
  $serialized = json_encode($payload);
@@ -457,7 +459,7 @@ class ChallengeUtils
457
459
  $payloadJson = $base64UrlDecode($parts[1]);
458
460
  $signature = $base64UrlDecode($parts[2]);
459
461
 
460
- $ed25519PubKey = $_ENV['ED25519_PUBLIC_KEY'] ?? getenv('ED25519_PUBLIC_KEY');
462
+ $ed25519PubKey = Env::get('ED25519_PUBLIC_KEY');
461
463
  if (!$ed25519PubKey) {
462
464
  error_log("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
463
465
  return null;
@@ -631,14 +633,14 @@ class ChallengeUtils
631
633
  /**
632
634
  * Crée le bloc de données de base pour le challenge CPU.
633
635
  */
634
- public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint): string
636
+ public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint, string $clientIp = '', string $tlsSessionId = ''): string
635
637
  {
636
638
  $parts = explode('|', $fingerprint);
637
639
  $filteredParts = array_filter($parts);
638
640
  sort($filteredParts);
639
641
  $sortedFingerprint = implode('|', $filteredParts);
640
642
 
641
- return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:";
643
+ return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:{$clientIp}:{$tlsSessionId}:";
642
644
  }
643
645
 
644
646
  /**
@@ -43,7 +43,7 @@ class SecurityProfiles
43
43
  'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
44
44
  'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
45
45
  'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
46
-
46
+ 'ipReputationScore' => 0.5, // NOUVEAU: Poids pour la réputation IP
47
47
  ],
48
48
  'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
49
49
  'patterns' => [
@@ -58,6 +58,7 @@ class SecurityProfiles
58
58
  'decayFactor' => 0.9,
59
59
  'inactivityReset' => 5000,
60
60
  ],
61
+ 'filterWhitelist' => 85.0, // Stratégie d'inspection modérée pour les IP/chemins en liste blanche
61
62
  'wasm' => true,
62
63
  ],
63
64
 
@@ -108,6 +109,7 @@ class SecurityProfiles
108
109
  ],
109
110
  'challengeNewDevices' => true, // Challenge all new devices
110
111
  'wasm' => true,
112
+ 'filterWhitelist' => true, // Tout comportement d'attaque certain bypass immédiatement la liste blanche
111
113
  ],
112
114
 
113
115
  /**
@@ -155,6 +157,7 @@ class SecurityProfiles
155
157
  ],
156
158
  // This would be a callable in PHP, but for now, we represent its intent.
157
159
  'isApiRequest' => 'req.path.startsWith("/api/") || req.headers.accept?.includes("application/json")',
160
+ 'filterWhitelist' => 75.0, // Seuil bas pour parer au vol de clés/tokens API légitimes
158
161
  'wasm' => true,
159
162
  ],
160
163
 
@@ -177,13 +180,14 @@ class SecurityProfiles
177
180
  'honeypotScore' => 1.0, // Crucial for comment spam
178
181
  'crossLayerInconsistencyScore' => 0.4,
179
182
  'timeInconsistencyScore' => 0.8,
180
- 'tlsSpoofingScore' => 0.6,
183
+ 'tlsSpoofingScore' => 0.6, // Moins critique pour les blogs
181
184
  'botScore' => 0.8,
182
185
  'cookieDroppingScore' => 0.7, // Moins critique, mais toujours un signal
183
186
  'threatIntelScore' => 0.3, // Lower priority for a blog
184
187
  'clientHintsInconsistencyScore' => 0.5,
185
188
  'clickVarianceScore' => 0.5, // Moderate weight for click variance
186
189
  'subnetScore' => 0.4, // Utile contre le spam de commentaires coordonné
190
+ 'ipReputationScore' => 0.3, // NOUVEAU: Poids pour la réputation IP
187
191
  'botnetClusterScore' => 0.5, // NOUVEAU: Poids pour le clustering botnet
188
192
  'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
189
193
  'quicAnomalyScore' => 0.5, // NEW: Anomalie QUIC
@@ -203,6 +207,7 @@ class SecurityProfiles
203
207
  'decayFactor' => 0.92,
204
208
  'inactivityReset' => 10000,
205
209
  ],
210
+ 'filterWhitelist' => 90.0, // Très tolérant, n'inspecte que si le score est presque au blocage
206
211
  'wasm' => true,
207
212
  ],
208
213
 
@@ -256,6 +261,7 @@ class SecurityProfiles
256
261
  // This would be a callable in PHP, but for now, we represent its intent.
257
262
  'isApiRequest' => 'req.path.startsWith("/api/cart") || req.path.startsWith("/api/stock") || req.path.startsWith("/api/checkout")',
258
263
  'wasm' => true,
264
+ 'filterWhitelist' => true, // Tolérance zéro pour le scraping / scalping distribué
259
265
  ],
260
266
  ];
261
267