@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
|
@@ -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 =
|
|
401
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|