@anonympins/fingerprint 0.5.1 → 0.5.2

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.
@@ -749,7 +749,7 @@ describe('Fingerprint & PoW Security Suite', () => {
749
749
  pow_type: 'cpu_mem',
750
750
  pow_nonce: nonce,
751
751
  pow_solution_cpu: String(cpuSolution),
752
- pow_solution_mem: String(memSolution),
752
+ pow_solution_mem: JSON.stringify(memSolution),
753
753
  pow_fp: solverFingerprint
754
754
  },
755
755
  headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint },
@@ -843,7 +843,7 @@ describe('Fingerprint & PoW Security Suite', () => {
843
843
  pow_type: 'cpu_mem',
844
844
  pow_nonce: nonce,
845
845
  pow_solution_cpu: String(cpuSolution),
846
- pow_solution_mem: String(memSolution),
846
+ pow_solution_mem: JSON.stringify(memSolution),
847
847
  pow_fp: solverFingerprint // The client submits its fingerprint.
848
848
  },
849
849
  headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint }, rawHeaders: ['User-Agent', userAgent], httpVersion: '1.1'
@@ -2122,7 +2122,7 @@ describe('Challenge Page Generation Security (XSS)', () => {
2122
2122
  });
2123
2123
 
2124
2124
  it('should escape the path parameter in generateCombinedPoWChallengePage to prevent XSS', () => {
2125
- const maliciousPath = `test.com";\nconsole.log("pwned");//`;
2125
+ const maliciousPath = `test.com";\nconsole.log("pwned");//`; // Original malicious path
2126
2126
  const challengeDetails = { nonce: 'test-nonce', target: '0000', path: maliciousPath };
2127
2127
 
2128
2128
  const html = generateCombinedPoWChallengePage(challengeDetails, 16, '127.0.0.1', 'secret', {}, '');
@@ -2721,3 +2721,87 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
2721
2721
  const isDiffIpValid = await fingerprint.isTicketValid('192.168.1.1', ticket, 'device-123', 'hash-abc', false);
2722
2722
  expect(isDiffIpValid).toBe(false);
2723
2723
  });
2724
+
2725
+ describe('IP Registration and Filtering', () => {
2726
+ const localInMemoryStore = {
2727
+ _map: new Map(),
2728
+ async get(key) { return this._map.get(key); },
2729
+ async set(key, value) { this._map.set(key, value); },
2730
+ async has(key) { return this._map.has(key); },
2731
+ async delete(key) { this._map.delete(key); },
2732
+ clear() { this._map.clear(); }
2733
+ };
2734
+
2735
+ beforeEach(() => {
2736
+ localInMemoryStore.clear();
2737
+ configureStore(localInMemoryStore);
2738
+ vi.restoreAllMocks();
2739
+ vi.spyOn(fingerprint.__internal, 'getTlsFingerprint').mockReturnValue({
2740
+ ja3: 'mock-ja3', ja4: 'mock-ja4'
2741
+ });
2742
+ });
2743
+
2744
+ it('should immediately allow requests from IPs in the static allowlist', async () => {
2745
+ const whitelistedIp = '198.51.100.42';
2746
+ const config = {
2747
+ weights: { historyScore: 1.0 },
2748
+ thresholds: { low: 20, block: 95 },
2749
+ whitelist: [
2750
+ { type: 'allowlist', entries: [whitelistedIp] }
2751
+ ]
2752
+ };
2753
+ const engine = new FingerprintEngine(config);
2754
+
2755
+ const req = {
2756
+ clientIp: whitelistedIp,
2757
+ path: '/',
2758
+ cookies: {},
2759
+ query: {},
2760
+ headers: { 'user-agent': 'test-ua' },
2761
+ rawHeaders: ['User-Agent', 'test-ua'],
2762
+ httpVersion: '1.1'
2763
+ };
2764
+
2765
+ const decision = await engine.processRequest(req);
2766
+ expect(decision.action).toBe('next');
2767
+ expect(decision.vector.whitelisted).toBe(100);
2768
+ expect(decision.vector.type).toBe('allowlist');
2769
+ });
2770
+
2771
+ it('should register distinct client IPs for a device in the data store', async () => {
2772
+ const ip1 = '192.168.1.100';
2773
+ const ip2 = '192.168.1.101';
2774
+ const deviceId = 'test-ip-reg-device';
2775
+
2776
+ await localInMemoryStore.set(`device:${deviceId}`, {
2777
+ initialDeviceHash: 'some-hash',
2778
+ ips: new Set([ip1]),
2779
+ lastUpdate: Date.now(),
2780
+ lastFpHash: 'some-hash',
2781
+ lastChangeTimestamp: 0,
2782
+ rapidChangeCount: 0,
2783
+ });
2784
+
2785
+ const req = {
2786
+ clientIp: ip2,
2787
+ path: '/',
2788
+ cookies: { device_id: deviceId },
2789
+ query: {},
2790
+ headers: { 'user-agent': 'test-ua' },
2791
+ rawHeaders: ['User-Agent', 'test-ua'],
2792
+ httpVersion: '1.1'
2793
+ };
2794
+
2795
+ const engine = new FingerprintEngine({
2796
+ weights: { historyScore: 1.0 },
2797
+ thresholds: { low: 20, block: 95 }
2798
+ });
2799
+
2800
+ await engine.processRequest(req);
2801
+
2802
+ const storedData = await localInMemoryStore.get(`device:${deviceId}`);
2803
+ expect(storedData.ips).toBeInstanceOf(Set);
2804
+ expect(storedData.ips.has(ip1)).toBe(true);
2805
+ expect(storedData.ips.has(ip2)).toBe(true);
2806
+ });
2807
+ });
@@ -115,4 +115,85 @@ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
115
115
  expect(result.solution.split(',').length).toBe(64);
116
116
  vi.restoreAllMocks();
117
117
  });
118
+
119
+ it('should fallback to WebGL1 if both WebGPU and WebGL2 are not supported', async () => {
120
+ // Mock WebGPU as undefined
121
+ Object.defineProperty(global.navigator, 'gpu', {
122
+ value: undefined,
123
+ writable: true,
124
+ configurable: true
125
+ });
126
+
127
+ // Mock WebGL1 (webgl) context with float extensions enabled
128
+ const mockGl1 = {
129
+ FRAMEBUFFER: 36160,
130
+ FRAMEBUFFER_COMPLETE: 36053,
131
+ COLOR_ATTACHMENT0: 36064,
132
+ TEXTURE_2D: 3553,
133
+ RGBA: 6408,
134
+ FLOAT: 5126,
135
+ TRIANGLES: 4,
136
+ CLAMP_TO_EDGE: 33071,
137
+ TEXTURE_MAG_FILTER: 10240,
138
+ TEXTURE_MIN_FILTER: 10241,
139
+ NEAREST: 9728,
140
+ TEXTURE_WRAP_S: 10242,
141
+ TEXTURE_WRAP_T: 10243,
142
+ useProgram: vi.fn(),
143
+ createBuffer: vi.fn(),
144
+ bindBuffer: vi.fn(),
145
+ bufferData: vi.fn(),
146
+ enableVertexAttribArray: vi.fn(),
147
+ vertexAttribPointer: vi.fn(),
148
+ uniform1f: vi.fn(),
149
+ uniform1i: vi.fn(),
150
+ getUniformLocation: vi.fn(),
151
+ getAttribLocation: vi.fn(),
152
+ drawArrays: vi.fn(),
153
+ getExtension: vi.fn().mockImplementation((ext) => {
154
+ if (ext === 'OES_texture_float' || ext === 'WEBGL_color_buffer_float') {
155
+ return {};
156
+ }
157
+ return null;
158
+ }),
159
+ createTexture: vi.fn(),
160
+ bindTexture: vi.fn(),
161
+ texImage2D: vi.fn(),
162
+ texParameteri: vi.fn(),
163
+ createFramebuffer: vi.fn(),
164
+ bindFramebuffer: vi.fn(),
165
+ framebufferTexture2D: vi.fn(),
166
+ checkFramebufferStatus: vi.fn().mockReturnValue(36053), // gl.FRAMEBUFFER_COMPLETE
167
+ viewport: vi.fn(),
168
+ readPixels: vi.fn((x, y, w, h, format, type, pixels) => {
169
+ for (let i = 0; i < pixels.length; i++) {
170
+ pixels[i] = 0.75;
171
+ }
172
+ })
173
+ };
174
+
175
+ const mockCanvas = {
176
+ width: 8,
177
+ height: 8,
178
+ getContext: vi.fn().mockImplementation((contextId) => {
179
+ if (contextId === 'webgl2') return null;
180
+ if (contextId === 'webgl' || contextId === 'experimental-webgl') return mockGl1;
181
+ return null;
182
+ })
183
+ };
184
+
185
+ vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
186
+ if (tagName === 'canvas') return mockCanvas;
187
+ return {};
188
+ });
189
+
190
+ vi.spyOn(GpuPowSolver, '_createProgram').mockReturnValue({});
191
+
192
+ const result = await GpuPowSolver.solve(seed, 100);
193
+
194
+ expect(result.platform).toBe('webgl1');
195
+ expect(result.solution.split(',').length).toBe(64);
196
+ expect(result.solution.split(',')[0]).toBe('0.750000');
197
+ vi.restoreAllMocks();
198
+ });
118
199
  });
@@ -63,8 +63,8 @@ describe('Proof-of-Work Solvers', () => {
63
63
  const solution1 = await solveMemory(seed, difficulty);
64
64
  const solution2 = await solveMemory(seed, difficulty);
65
65
 
66
- expect(solution1).toBe(solution2);
67
- expect(solution1).not.toBe(await solveMemory('different-seed', difficulty));
66
+ expect(solution1).toStrictEqual(solution2);
67
+ expect(solution1).not.toStrictEqual(await solveMemory('different-seed', difficulty));
68
68
  });
69
69
  });
70
70
 
@@ -106,7 +106,7 @@ describe('Proof-of-Work Solvers', () => {
106
106
 
107
107
  const solutions = await solveChallenge(challenge, ''); // Le fingerprint est passé en 2e arg
108
108
  expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
109
- expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Number));
109
+ expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Object));
110
110
  }, 20000);
111
111
 
112
112
  it('should solve a "cpu_mem_inline" challenge for a browser', async () => {
@@ -122,7 +122,7 @@ describe('Proof-of-Work Solvers', () => {
122
122
 
123
123
  const solutions = await solveChallenge(challenge, '');
124
124
  expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
125
- expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Number));
125
+ expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Object));
126
126
  }, 20000);
127
127
 
128
128
  it('should solve a "tsp" challenge', async () => {
@@ -638,6 +638,55 @@ class ChallengeUtils
638
638
  /**
639
639
  * Vérifie une solution de PoW mémoire.
640
640
  */
641
+ private static function getChallengedIndices(string $seed, int $solution, int $numBlocks, int $k = 4): array
642
+ {
643
+ $indices = [];
644
+ $h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $solution), '4294967296');
645
+ for ($i = 0; $i < $k; $i++) {
646
+ $h = self::gmp_imul($h ^ $i, 1597334677);
647
+ $indices[] = abs($h) % $numBlocks;
648
+ }
649
+ return $indices;
650
+ }
651
+
652
+ private static function verifyMerkleProof(string $leafHash, int $index, array $proof, string $root): bool
653
+ {
654
+ $currentHash = $leafHash;
655
+ $idx = $index;
656
+ foreach ($proof as $sibling) {
657
+ $combined = ($idx % 2 === 0) ? $currentHash . $sibling : $sibling . $currentHash;
658
+ $currentHash = hash('sha256', hex2bin($combined));
659
+ $idx = (int)floor($idx / 2);
660
+ }
661
+ return $currentHash === $root;
662
+ }
663
+
664
+ private static function verifyMemoryPoWLegacy(string $nonce, int $solution, int $difficulty, string $clientSecret): bool
665
+ {
666
+ $size = $difficulty * 1024 * 1024;
667
+ $iterations = (int)floor($size / 16);
668
+ $buffer = new \SplFixedArray((int)floor($size / 4));
669
+
670
+ $seed = ":{$nonce}:{$clientSecret}";
671
+ $h = 0;
672
+ foreach (unpack('C*', $seed) as $byte) {
673
+ $h += $byte;
674
+ }
675
+
676
+ for ($i = 0; $i < count($buffer); $i++) {
677
+ $buffer[$i] = $h = self::gmp_imul($h ^ $i, 1597334677);
678
+ }
679
+
680
+ $finalHash = 0;
681
+ $addr = count($buffer) > 0 ? $buffer[0] % count($buffer) : 0;
682
+ for ($i = 0; $i < $iterations; $i++) {
683
+ $addr = $buffer[$addr] % count($buffer);
684
+ $finalHash ^= $addr;
685
+ }
686
+
687
+ return $finalHash === $solution;
688
+ }
689
+
641
690
  public static function verifyMemoryPoW(
642
691
  string $nonce,
643
692
  string $solution,
@@ -654,37 +703,69 @@ class ChallengeUtils
654
703
  return false;
655
704
  }
656
705
 
657
- // If difficulty is high (production workloads), we treat memory PoW purely as a client-side cost.
658
- // Cryptographic integrity is already fully enforced by the chained CPU PoW verification.
659
- if ($difficulty > 4) {
660
- return true;
706
+ $data = json_decode($solution, true);
707
+ if (json_last_error() !== JSON_ERROR_NONE || !isset($data['solution']) || !isset($data['merkleRoot']) || !isset($data['proofs'])) {
708
+ if ($difficulty <= 4 && preg_match('/^\d+$/', $solution)) {
709
+ return self::verifyMemoryPoWLegacy($nonce, (int)$solution, $difficulty, $clientSecret);
710
+ }
711
+ return false;
661
712
  }
662
713
 
663
- $size = $difficulty * 1024 * 1024;
664
- if ($size <= 0) {
665
- return true; // Pas de challenge mémoire si la difficulté est nulle ou négative.
666
- }
667
- $iterations = (int)floor($size / 16);
668
- $buffer = new \SplFixedArray((int)floor($size / 4));
714
+ $sol = $data['solution'];
715
+ $merkleRoot = $data['merkleRoot'];
716
+ $proofs = $data['proofs'];
669
717
 
718
+ $numBlocks = $difficulty * 256;
670
719
  $seed = ":{$nonce}:{$clientSecret}";
671
- $h = 0;
672
- foreach (unpack('C*', $seed) as $byte) {
673
- $h += $byte;
674
- }
675
720
 
676
- for ($i = 0; $i < count($buffer); $i++) {
677
- $buffer[$i] = $h = self::gmp_imul($h ^ $i, 1597334677);
721
+ $challengedIndices = self::getChallengedIndices($seed, (int)$sol, $numBlocks, 4);
722
+
723
+ foreach ($challengedIndices as $b) {
724
+ $proof = $proofs[$b] ?? $proofs[(string)$b] ?? null;
725
+ if ($proof === null) return false;
726
+
727
+ $blockBytes = '';
728
+ $h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $b), '4294967296');
729
+ for ($i = 0; $i < 1024; $i++) {
730
+ $h = self::gmp_imul($h ^ $i, 1597334677);
731
+ $blockBytes .= pack('V', $h);
732
+ }
733
+
734
+ $expectedLeaf = hash('sha256', $blockBytes);
735
+
736
+ if (!self::verifyMerkleProof($expectedLeaf, $b, $proof, $merkleRoot)) {
737
+ return false;
738
+ }
678
739
  }
679
740
 
680
- $finalHash = 0;
681
- $addr = count($buffer) > 0 ? $buffer[0] % count($buffer) : 0;
741
+ $blockCache = [];
742
+ $getBlockElement = function (int $blockIdx, int $elementIdx) use (&$blockCache, $seed): int {
743
+ if (!isset($blockCache[$blockIdx])) {
744
+ $block = [];
745
+ $h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $blockIdx), '4294967296');
746
+ for ($i = 0; $i < 1024; $i++) {
747
+ $h = self::gmp_imul($h ^ $i, 1597334677);
748
+ $block[$i] = $h;
749
+ }
750
+ $blockCache[$blockIdx] = $block;
751
+ }
752
+ return $blockCache[$blockIdx][$elementIdx];
753
+ };
754
+
755
+ $totalElements = $numBlocks * 1024;
756
+ $addr = $totalElements > 0 ? $getBlockElement(0, 0) % $totalElements : 0;
757
+ $addr = $addr & 0xffffffff;
758
+ $expectedSolution = 0;
759
+ $iterations = 1024;
682
760
  for ($i = 0; $i < $iterations; $i++) {
683
- $addr = $buffer[$addr] % count($buffer);
684
- $finalHash ^= $addr;
761
+ $blockIdx = (int)floor($addr / 1024);
762
+ $elementIdx = $addr % 1024;
763
+ $addr = $getBlockElement($blockIdx, $elementIdx) % $totalElements;
764
+ $addr = $addr & 0xffffffff;
765
+ $expectedSolution ^= $addr;
685
766
  }
686
767
 
687
- return $finalHash === (int)$solution;
768
+ return $expectedSolution === (int)$sol;
688
769
  }
689
770
 
690
771
  /**
@@ -759,11 +840,15 @@ class ChallengeUtils
759
840
  $solverCode = self::getPowSolverCode();
760
841
  $queriesJson = json_encode($queries);
761
842
 
843
+ $safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
844
+ $safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
845
+ $safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
846
+
762
847
  $challengeScript = <<<JS
763
848
  async function solve() {
764
- const nonce = "{$nonce}";
765
- const path = "{$path}";
766
- const clientSecret = "{$clientSecret}";
849
+ const nonce = {$safeNonce};
850
+ const path = {$safePath};
851
+ const clientSecret = {$safeClientSecret};
767
852
  const queries = {$queriesJson};
768
853
  const sizeMb = {$sizeMb};
769
854
  const nodeId = "{$nodeId}";
@@ -866,11 +951,15 @@ JS;
866
951
  ));
867
952
  $trapContainerHtml = "<div style=\"position:absolute;left:-9999px;top:-9999px;transform:scale(0);pointer-events:none;\" aria-hidden=\"true\">{$trapLinksHtml}</div>";
868
953
 
954
+ $safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
955
+ $safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
956
+ $safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
957
+
869
958
  $challengeScript = <<<JS
870
959
  async function solve() {
871
- const nonce = "{$nonce}";
872
- const path = "{$path}";
873
- const clientSecret = "{$clientSecret}";
960
+ const nonce = {$safeNonce};
961
+ const path = {$safePath};
962
+ const clientSecret = {$safeClientSecret};
874
963
  const cpuTarget = BigInt("0x" + "{$target}");
875
964
  const memDifficulty = {$memoryDifficulty};
876
965
  const baseBlock = new Uint8Array({$baseBlockBytes});
@@ -27,6 +27,10 @@
27
27
  private ?Logger $logger = null;
28
28
  private bool $dryRun;
29
29
 
30
+ private static ?array $googlebotEntries = null;
31
+ private static ?array $yandexEntries = null;
32
+ private static ?array $bingbotEntries = null;
33
+
30
34
  public function __construct(array $securityConfig)
31
35
  {
32
36
  $this->isProduction = ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production';
@@ -103,7 +107,103 @@
103
107
  }
104
108
  }
105
109
  }
110
+ private static function loadBotWhitelist(string $filename, array $fallbackEntries): array
111
+ {
112
+ $configDir = dirname(__DIR__, 2) . '/config';
113
+ $filePath = $configDir . '/' . $filename;
114
+ if (file_exists($filePath)) {
115
+ try {
116
+ $content = file_get_contents($filePath);
117
+ if ($content !== false) {
118
+ $decoded = json_decode($content, true);
119
+ if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
120
+ return $decoded;
121
+ }
122
+ }
123
+ } catch (\Throwable $e) {
124
+ error_log("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
125
+ }
126
+ }
127
+ return $fallbackEntries;
128
+ }
129
+
130
+ public static function googlebot_whitelist(): array
131
+ {
132
+ if (self::$googlebotEntries === null) {
133
+ self::$googlebotEntries = self::loadBotWhitelist('googlebot.json', [
134
+ "2001:4860:4801:10::/64",
135
+ "2001:4860:4801:11::/64",
136
+ "2001:4860:4801:12::/64",
137
+ "66.249.79.64"
138
+ ]);
139
+ }
140
+ return [
141
+ 'type' => 'allowlist',
142
+ 'entries' => self::$googlebotEntries
143
+ ];
144
+ }
145
+
146
+ public static function yandex_whitelist(): array
147
+ {
148
+ if (self::$yandexEntries === null) {
149
+ self::$yandexEntries = self::loadBotWhitelist('yandex.json', [
150
+ "2a02:6b8::/29",
151
+ "5.45.192.0/18",
152
+ "213.180.192.0/19"
153
+ ]);
154
+ }
155
+ return [
156
+ 'type' => 'allowlist',
157
+ 'entries' => self::$yandexEntries
158
+ ];
159
+ }
106
160
 
161
+ public static function bingbot_whitelist(): array
162
+ {
163
+ if (self::$bingbotEntries === null) {
164
+ self::$bingbotEntries = self::loadBotWhitelist('bingbot.json', [
165
+ "157.55.39.0/24",
166
+ "207.46.13.0/24",
167
+ "40.77.178.0/23"
168
+ ]);
169
+ }
170
+ return [
171
+ 'type' => 'allowlist',
172
+ 'entries' => self::$bingbotEntries
173
+ ];
174
+ }
175
+
176
+ public static function default_whitelist(): array
177
+ {
178
+ return [
179
+ self::googlebot_whitelist(),
180
+ self::bingbot_whitelist(),
181
+ self::yandex_whitelist(),
182
+ ['userAgent' => 'Googlebot', 'hostnameSuffix' => '.googlebot.com'],
183
+ ['userAgent' => 'Google-Extended', 'hostnameSuffix' => '.google.com'],
184
+ ['userAgent' => 'AdsBot-Google', 'hostnameSuffix' => '.googlebot.com'],
185
+ ['userAgent' => 'Mediapartners-Google', 'hostnameSuffix' => '.google.com'],
186
+ ['userAgent' => 'Google-InspectionTool', 'hostnameSuffix' => '.google.com'],
187
+ ['userAgent' => '(bingbot|adidxbot)', 'hostnameSuffix' => '.search.msn.com'],
188
+ ['userAgent' => 'DuckDuckBot', 'hostnameSuffix' => '.duckduckgo.com'],
189
+ ['userAgent' => 'YandexBot', 'hostnameSuffix' => '.yandex.com'],
190
+ ['userAgent' => 'YandexImages', 'hostnameSuffix' => '.yandex.com'],
191
+ ['userAgent' => 'Baiduspider', 'hostnameSuffix' => '.crawl.baidu.com'],
192
+ ['userAgent' => 'Slurp', 'hostnameSuffix' => '.crawl.yahoo.net'],
193
+ ['userAgent' => 'Sogou web spider', 'hostnameSuffix' => '.sogou.com'],
194
+ ['userAgent' => 'Exabot', 'hostnameSuffix' => '.exabot.com'],
195
+ ['userAgent' => 'ia_archiver', 'hostnameSuffix' => '.alexa.com'],
196
+ ['userAgent' => 'SeznamBot', 'hostnameSuffix' => '.seznam.cz'],
197
+ ['userAgent' => 'Mail.RU_Bot', 'hostnameSuffix' => '.mail.ru'],
198
+ ['userAgent' => 'Yeti', 'hostnameSuffix' => '.naver.com'],
199
+ ['userAgent' => 'AhrefsBot', 'hostnameSuffix' => '.ahrefs.com'],
200
+ ['userAgent' => 'SemrushBot', 'hostnameSuffix' => '.semrush.com'],
201
+ ['userAgent' => 'MJ12bot', 'hostnameSuffix' => '.mj12bot.com'],
202
+ ['userAgent' => 'rogerbot', 'hostnameSuffix' => '.moz.com'],
203
+ ['userAgent' => 'DotBot', 'hostnameSuffix' => '.moz.com'],
204
+ ['userAgent' => 'Screaming Frog SEO Spider', 'hostnameSuffix' => '.screamingfrog.co.uk'],
205
+ ];
206
+ }
107
207
  private function log(string $message, array $data = [], string $level = 'info'): void
108
208
  {
109
209
  if ($this->verbose) {
@@ -1039,8 +1139,9 @@
1039
1139
  'target' => ChallengeUtils::calculateCpuTarget($suspicionFactor, $this->securityConfig),
1040
1140
  'path' => $context->path,
1041
1141
  ];
1042
-
1043
- $memActivationFactor = max(0, ($suspicionFactor - 0.25) / 0.75);
1142
+
1143
+ // Alignement linéaire parfait du ratio d'effort CPU/Mémoire
1144
+ $memActivationFactor = $suspicionFactor;
1044
1145
  $memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
1045
1146
 
1046
1147
  $originalFingerprint = RequestUtils::getCompositeDeviceHash($context);