@anonympins/fingerprint 0.7.5 → 0.8.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/anonympins-bot-mitigation-pow.zip +0 -0
  5. package/src/js/fingerprint.client.js +1784 -1772
  6. package/src/js/fingerprint.client.obfuscated.js +1 -1
  7. package/src/js/fingerprint.js +198 -28
  8. package/src/js/fingerprint.utils.js +252 -2
  9. package/src/js/pow.worker.js +2 -2
  10. package/src/js/tests/cross_parity.test.js +3 -3
  11. package/src/js/tests/dns-circuit-breaker.test.js +6 -6
  12. package/src/js/tests/fingerprint.builder.test.js +76 -78
  13. package/src/js/tests/fingerprint.client.test.js +16 -17
  14. package/src/js/tests/fingerprint.test.js +19 -12
  15. package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
  16. package/src/js/tests/library.test.js +94 -95
  17. package/src/js/tests/obfuscation.test.js +8 -8
  18. package/src/js/tests/quicFingerprint.test.js +47 -0
  19. package/src/js/tests/tcpFingerprint.test.js +4 -4
  20. package/src/js/upow-model-task.js +155 -112
  21. package/src/php/Challenge/ChallengeUtils.php +382 -86
  22. package/src/php/Config/SecurityProfiles.php +75 -62
  23. package/src/php/DirectFingerprint.php +55 -8
  24. package/src/php/FingerprintBuilder.php +13 -13
  25. package/src/php/FingerprintClient.php +6 -6
  26. package/src/php/FingerprintEngine.php +2120 -1837
  27. package/src/php/Ja3AnomalyDetector.php +22 -22
  28. package/src/php/Optimization/FunctionRegistry.php +4 -4
  29. package/src/php/Optimization/Optimization.php +6 -6
  30. package/src/php/RequestContext.php +11 -11
  31. package/src/php/Store/InMemoryStore.php +96 -66
  32. package/src/php/Store/MongoDbStore.php +2 -2
  33. package/src/php/Store/RedisStore.php +1 -1
  34. package/src/php/Store/StoreManager.php +48 -35
  35. package/src/php/Tests/ChallengeUtilsTest.php +31 -0
  36. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  37. package/src/php/Tests/FingerprintClientTest.php +70 -70
  38. package/src/php/Tests/PowTest.php +37 -39
  39. package/src/php/Tests/QuicFingerprintTest.php +115 -53
  40. package/src/php/Tests/ThreatIntelTest.php +2 -2
  41. package/src/php/Utils/BigInt.php +202 -202
  42. package/src/php/Utils/BlockList.php +13 -13
  43. package/src/php/Utils/Logger.php +19 -1
  44. package/src/php/Utils/MaliciousPatterns.php +6 -6
  45. package/src/php/Utils/MetricsManager.php +209 -209
  46. package/src/php/Utils/RequestUtils.php +327 -14
  47. package/src/php/Utils/TLSClientHelloParser.php +385 -369
  48. package/src/php/WordPress/WpDbStore.php +7 -7
  49. package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
  50. package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
  51. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
  52. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
  53. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  54. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
  55. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  56. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
  57. package/src/php/WordPress/package.php +80 -101
  58. package/src/php/WordPress/readme.txt +21 -5
  59. package/src/php/bin/auto-tune.php +1 -1
  60. package/public/fingerprint-anti-bot.zip +0 -0
@@ -7,12 +7,12 @@ import ClientLibrary from '../fingerprint.client.js';
7
7
 
8
8
  describe('ClientLibrary WASM Integration', () => {
9
9
  beforeEach(() => {
10
- // Réinitialise le cache et les mocks avant chaque test
10
+ // Reset cache and spies before each test
11
11
  ClientLibrary._resetCache();
12
12
  vi.spyOn(console, 'log').mockImplementation(() => {});
13
13
  vi.spyOn(console, 'warn').mockImplementation(() => {});
14
14
 
15
- // Supprime les mocks globaux pour éviter les fuites entre les tests
15
+ // Remove global mocks to prevent leakage across tests
16
16
  delete window.createFingerprintModule;
17
17
  });
18
18
 
@@ -21,28 +21,27 @@ describe('ClientLibrary WASM Integration', () => {
21
21
  });
22
22
 
23
23
  it('should successfully load WASM module and switch hasher', async () => {
24
- // 1. Simuler un module WASM fonctionnel
24
+ // 1. Mock functional WASM module
25
25
  const mockWasmModule = {
26
- _hash_string: vi.fn((str) => 99999), // Un mock qui retourne une valeur distincte
26
+ _hash_string: vi.fn((str) => 99999), // Distinct mock hash value
27
27
  _malloc: vi.fn().mockReturnValue(0),
28
28
  HEAPU8: new Uint8Array(4096)
29
29
  };
30
30
 
31
- // 2. Simuler le chargement du script et l'initialisation du module
32
- // On attache les fonctions de simulation à `window` car c'est ce que le code cherche
31
+ // 2. Mock script loading and module factory
33
32
  window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
34
33
 
35
- // On simule l'injection du script en appelant directement `onload`
34
+ // Mock script element injection with direct onload callback
36
35
  vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
37
- // Simule le chargement réussi du script
36
+ // Simulate successful script load
38
37
  script.onload();
39
38
  return script;
40
39
  });
41
40
 
42
- // 3. Appeler la fonction d'initialisation
41
+ // 3. Initialize WASM module
43
42
  await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
44
43
 
45
- // 4. Vérifier que le hasher a été remplacé
44
+ // 4. Verify hasher was switched
46
45
  const wasmHash = ClientLibrary._hasher("test");
47
46
  expect(wasmHash).toBe(99999);
48
47
  expect(mockWasmModule._hash_string).toHaveBeenCalledWith(0);
@@ -50,16 +49,16 @@ describe('ClientLibrary WASM Integration', () => {
50
49
  });
51
50
 
52
51
  it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
53
- // 1. Simuler un échec de chargement du script
52
+ // 1. Simulate script loading failure
54
53
  vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
55
54
  script.onerror(new Error('Script loading failed'));
56
55
  return script;
57
56
  });
58
57
 
59
- // 2. Appeler la fonction d'initialisation
58
+ // 2. Initialize WASM module
60
59
  await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
61
60
 
62
- // 3. Vérifier que le hasher est toujours l'implémentation JS
61
+ // 3. Verify hasher falls back to JS implementation
63
62
  const jsHash = ClientLibrary._hasher("test");
64
63
  const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
65
64
 
@@ -68,7 +67,7 @@ describe('ClientLibrary WASM Integration', () => {
68
67
  });
69
68
 
70
69
  it('should gracefully fall back if WASM module does not export _hash_string', async () => {
71
- // 1. Simuler un module WASM malformé (sans la fonction attendue)
70
+ // 1. Simulate malformed WASM module missing required export
72
71
  const mockWasmModule = {};
73
72
  window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
74
73
 
@@ -77,14 +76,14 @@ describe('ClientLibrary WASM Integration', () => {
77
76
  return script;
78
77
  });
79
78
 
80
- // 2. Appeler la fonction d'initialisation
79
+ // 2. Initialize WASM module
81
80
  await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
82
81
 
83
- // 3. Vérifier le fallback
82
+ // 3. Verify fallback
84
83
  const jsHash = ClientLibrary._hasher("test");
85
84
  const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
86
85
  expect(jsHash).toBe(originalJsHash);
87
- // L'assertion est maintenant plus précise : elle vérifie le message générique ET le message d'erreur spécifique.
86
+ // Verify both generic error log and specific missing export cause
88
87
  expect(console.warn).toHaveBeenCalledWith(
89
88
  expect.stringContaining('WASM module failed to load'),
90
89
  expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
@@ -1621,18 +1621,6 @@ describe('Fingerprint & PoW Security Suite', () => {
1621
1621
  expect(behaviorScore).toBe(0);
1622
1622
  });
1623
1623
 
1624
- it('should return a score of 0 when x-behavior-metrics is 1 (human verified client-side)', () => {
1625
- const context = { headers: { 'x-behavior-metrics': '1' } };
1626
- const { behaviorScore } = getBehaviorScore(context);
1627
- expect(behaviorScore).toBe(0);
1628
- });
1629
-
1630
- it('should return a score of 100 when x-behavior-metrics is 0 (bot detected client-side)', () => {
1631
- const context = { headers: { 'x-behavior-metrics': '0' } };
1632
- const { behaviorScore } = getBehaviorScore(context);
1633
- expect(behaviorScore).toBe(100);
1634
- });
1635
-
1636
1624
  it('should return a score of 100 for honeypot interaction', () => {
1637
1625
  const metrics = { honeypotInteraction: true, mouseEntropy: 50, keystrokeLatency: 120 };
1638
1626
  const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
@@ -3199,6 +3187,25 @@ describe('IP Registration and Filtering', () => {
3199
3187
  expect(process.env.ED25519_PRIVATE_KEY).toBe(dummyKeys.privateKey);
3200
3188
  expect(process.env.ED25519_PUBLIC_KEY).toBe(dummyKeys.publicKey);
3201
3189
  });
3190
+
3191
+ it('should generate issuer PEM files directly via generateIssuerPemKeys', async () => {
3192
+ const fs = await import('node:fs');
3193
+ const os = await import('node:os');
3194
+ const path = await import('node:path');
3195
+ const tempDir = path.join(os.tmpdir(), `fp-issuer-keys-${Date.now()}`);
3196
+
3197
+ try {
3198
+ const keys = fingerprint.generateIssuerPemKeys(tempDir);
3199
+ expect(keys.privateKeyPath).toContain('issuer-private.pem');
3200
+ expect(keys.publicKeyPath).toContain('issuer-public.pem');
3201
+ expect(keys.privateKey).toContain('-----BEGIN PRIVATE KEY-----');
3202
+ expect(keys.publicKey).toContain('-----BEGIN PUBLIC KEY-----');
3203
+ expect(fs.existsSync(keys.privateKeyPath)).toBe(true);
3204
+ expect(fs.existsSync(keys.publicKeyPath)).toBe(true);
3205
+ } finally {
3206
+ fs.rmSync(tempDir, { recursive: true, force: true });
3207
+ }
3208
+ });
3202
3209
  });
3203
3210
 
3204
3211
  describe('Closed-Shadow DOM Honeypot Traps (Feature 19)', () => {
@@ -1,136 +1,135 @@
1
- import crypto from 'crypto';
2
- import {getTlsSpoofingScore, parseJa3} from '../fingerprint.js';
3
- import {vi} from 'vitest';
4
- import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "../../js/fingerprint.utils.js";
5
-
6
- describe('JA3 Anomaly Detector (Node.js)', () => {
7
-
8
- // Mock d'un store de cache asynchrone en mémoire
9
- const createMockStore = () => {
10
- const storage = {};
11
- return {
12
- get: vi.fn(async (key) => storage[key] || null),
13
- set: vi.fn(async (key, val) => { storage[key] = val; })
14
- };
15
- };
16
-
17
- describe('parseJa3', () => {
18
- it('devrait parser correctement une chaine JA3 brute', () => {
19
- const rawJa3 = '771,4865-4866-4867,0-23-65281-10-11,29-23-24,0';
20
- const parsed = parseJa3(rawJa3);
21
-
22
- expect(parsed).not.toBeNull();
23
- expect(parsed.tlsVersion).toBe(771);
24
- expect(parsed.ciphers).toEqual([4865, 4866, 4867]);
25
- expect(parsed.extensions).toEqual([0, 23, 65281, 10, 11]);
26
- });
27
-
28
- it('devrait retourner null pour des chaines invalides', () => {
29
- expect(parseJa3('')).toBeNull();
30
- expect(parseJa3('771,4865')).toBeNull();
31
- });
32
- });
33
-
34
- describe('getTlsSpoofingScore (Anomalies JA3 hachées)', () => {
35
- it('devrait détecter l\'usurpation d\'identité d\'une bibliothèque (Python)', async () => {
36
- const pythonJa3Hash = '47344a349b75c4e82333475553b5f358';
37
- const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
38
-
39
- const context = {
40
- headers: {
41
- 'user-agent': chromeUa,
42
- 'x-ja3-hash': pythonJa3Hash
43
- }
44
- };
45
-
46
- const result = await getTlsSpoofingScore(context);
47
- expect(result.tlsSpoofingScore).toBe(90);
48
- });
49
-
50
- it('devrait détecter la rotation d\'User-Agent sur un même hash JA3 (Stagnation)', async () => {
51
- const mockStore = createMockStore();
52
- const unknownJa3 = '00000000000000000000000000000000';
53
-
54
- const contextChrome = {
55
- headers: {
56
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
57
- 'x-ja3-hash': unknownJa3
58
- }
59
- };
60
-
61
- const contextFirefox = {
62
- headers: {
63
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/120.0',
64
- 'x-ja3-hash': unknownJa3
65
- }
66
- };
67
-
68
- // Injecter le mockStore dans le module de test si nécessaire ou mocker l'importation du store global
69
- // Premier passage : Chrome
70
- const res1 = await getTlsSpoofingScore(contextChrome, mockStore);
71
-
72
- // Deuxième passage : Firefox (Détection de la rotation de UA)
73
- const res2 = await getTlsSpoofingScore(contextFirefox, mockStore);
74
- expect(res2.tlsSpoofingScore).toBe(85);
75
- });
76
- });
77
-
78
- describe('getTlsSpoofingScore (Anomalies JA3 brutes)', () => {
79
- it('devrait suspecter un faux Chrome n\'utilisant pas le mécanisme GREASE', async () => {
80
- const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
81
- // Pas de valeurs GREASE dans la suite
82
- const ja3RawNoGrease = '771,4865-4866,0-23-10,29,0';
83
- const ja3Hash = crypto.createHash('md5').update(ja3RawNoGrease).digest('hex');
84
-
85
- const context = {
86
- headers: {
87
- 'user-agent': chromeUa,
88
- 'x-ja3-raw': ja3RawNoGrease,
89
- 'x-ja3-hash': ja3Hash
90
- },
91
- httpVersion: '2.0'
92
- };
93
-
94
- const result = await getTlsSpoofingScore(context);
95
- expect(result.tlsSpoofingScore).toBe(75);
96
- });
97
-
98
- it('devrait valider un Chrome légitime utilisant des valeurs GREASE', async () => {
99
- const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
100
- // 2570 est une suite GREASE valide
101
- const ja3RawWithGrease = '771,4865-2570,0-23-10-16,29,0';
102
- const ja3Hash = crypto.createHash('md5').update(ja3RawWithGrease).digest('hex');
103
-
104
- const context = {
105
- headers: {
106
- 'user-agent': chromeUa,
107
- 'x-ja3-raw': ja3RawWithGrease,
108
- 'x-ja3-hash': ja3Hash
109
- },
110
- httpVersion: '2.0'
111
- };
112
-
113
- const result = await getTlsSpoofingScore(context);
114
- expect(result.tlsSpoofingScore).toBeLessThan(70);
115
- });
116
-
117
- it('devrait détecter l\'absence d\'extension ALPN pour une connexion HTTP/2', async () => {
118
- const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
119
- // Pas d'extension 16 (ALPN)
120
- const ja3RawNoAlpn = '771,4865-2570,0-23-10,29,0';
121
- const ja3Hash = crypto.createHash('md5').update(ja3RawNoAlpn).digest('hex');
122
-
123
- const context = {
124
- headers: {
125
- 'user-agent': chromeUa,
126
- 'x-ja3-raw': ja3RawNoAlpn,
127
- 'x-ja3-hash': ja3Hash
128
- },
129
- httpVersion: '2.0'
130
- };
131
-
132
- const result = await getTlsSpoofingScore(context);
133
- expect(result.tlsSpoofingScore).toBe(70);
134
- });
135
- });
136
- });
1
+ import crypto from 'crypto';
2
+ import {getTlsSpoofingScore, parseJa3} from '../fingerprint.js';
3
+ import {vi} from 'vitest';
4
+ import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "../../js/fingerprint.utils.js";
5
+
6
+ describe('JA3 Anomaly Detector (Node.js)', () => {
7
+
8
+ // Mock async in-memory cache store
9
+ const createMockStore = () => {
10
+ const storage = {};
11
+ return {
12
+ get: vi.fn(async (key) => storage[key] || null),
13
+ set: vi.fn(async (key, val) => { storage[key] = val; })
14
+ };
15
+ };
16
+
17
+ describe('parseJa3', () => {
18
+ it('should correctly parse raw JA3 string', () => {
19
+ const rawJa3 = '771,4865-4866-4867,0-23-65281-10-11,29-23-24,0';
20
+ const parsed = parseJa3(rawJa3);
21
+
22
+ expect(parsed).not.toBeNull();
23
+ expect(parsed.tlsVersion).toBe(771);
24
+ expect(parsed.ciphers).toEqual([4865, 4866, 4867]);
25
+ expect(parsed.extensions).toEqual([0, 23, 65281, 10, 11]);
26
+ });
27
+
28
+ it('should return null for invalid strings', () => {
29
+ expect(parseJa3('')).toBeNull();
30
+ expect(parseJa3('771,4865')).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe('getTlsSpoofingScore (Anomalies JA3 hachées)', () => {
35
+ it('should detect library spoofing (Python)', async () => {
36
+ const pythonJa3Hash = '47344a349b75c4e82333475553b5f358';
37
+ const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
38
+
39
+ const context = {
40
+ headers: {
41
+ 'user-agent': chromeUa,
42
+ 'x-ja3-hash': pythonJa3Hash
43
+ }
44
+ };
45
+
46
+ const result = await getTlsSpoofingScore(context);
47
+ expect(result.tlsSpoofingScore).toBe(90);
48
+ });
49
+
50
+ it('should detect User-Agent rotation on identical JA3 hash (Stagnation)', async () => {
51
+ const mockStore = createMockStore();
52
+ const unknownJa3 = '00000000000000000000000000000000';
53
+
54
+ const contextChrome = {
55
+ headers: {
56
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
57
+ 'x-ja3-hash': unknownJa3
58
+ }
59
+ };
60
+
61
+ const contextFirefox = {
62
+ headers: {
63
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/120.0',
64
+ 'x-ja3-hash': unknownJa3
65
+ }
66
+ };
67
+
68
+ // First call: Chrome
69
+ const res1 = await getTlsSpoofingScore(contextChrome, mockStore);
70
+
71
+ // Second call: Firefox (Detects UA rotation)
72
+ const res2 = await getTlsSpoofingScore(contextFirefox, mockStore);
73
+ expect(res2.tlsSpoofingScore).toBe(85);
74
+ });
75
+ });
76
+
77
+ describe('getTlsSpoofingScore (Anomalies JA3 brutes)', () => {
78
+ it('should flag fake Chrome lacking GREASE values', async () => {
79
+ const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
80
+ // No GREASE values in ciphers/extensions
81
+ const ja3RawNoGrease = '771,4865-4866,0-23-10,29,0';
82
+ const ja3Hash = crypto.createHash('md5').update(ja3RawNoGrease).digest('hex');
83
+
84
+ const context = {
85
+ headers: {
86
+ 'user-agent': chromeUa,
87
+ 'x-ja3-raw': ja3RawNoGrease,
88
+ 'x-ja3-hash': ja3Hash
89
+ },
90
+ httpVersion: '2.0'
91
+ };
92
+
93
+ const result = await getTlsSpoofingScore(context);
94
+ expect(result.tlsSpoofingScore).toBe(75);
95
+ });
96
+
97
+ it('should validate legitimate Chrome using GREASE values', async () => {
98
+ const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
99
+ // 2570 is a valid GREASE value
100
+ const ja3RawWithGrease = '771,4865-2570,0-23-10-16,29,0';
101
+ const ja3Hash = crypto.createHash('md5').update(ja3RawWithGrease).digest('hex');
102
+
103
+ const context = {
104
+ headers: {
105
+ 'user-agent': chromeUa,
106
+ 'x-ja3-raw': ja3RawWithGrease,
107
+ 'x-ja3-hash': ja3Hash
108
+ },
109
+ httpVersion: '2.0'
110
+ };
111
+
112
+ const result = await getTlsSpoofingScore(context);
113
+ expect(result.tlsSpoofingScore).toBeLessThan(70);
114
+ });
115
+
116
+ it('should detect missing ALPN extension on HTTP/2 connections', async () => {
117
+ const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
118
+ // Missing extension 16 (ALPN)
119
+ const ja3RawNoAlpn = '771,4865-2570,0-23-10,29,0';
120
+ const ja3Hash = crypto.createHash('md5').update(ja3RawNoAlpn).digest('hex');
121
+
122
+ const context = {
123
+ headers: {
124
+ 'user-agent': chromeUa,
125
+ 'x-ja3-raw': ja3RawNoAlpn,
126
+ 'x-ja3-hash': ja3Hash
127
+ },
128
+ httpVersion: '2.0'
129
+ };
130
+
131
+ const result = await getTlsSpoofingScore(context);
132
+ expect(result.tlsSpoofingScore).toBe(70);
133
+ });
134
+ });
135
+ });
@@ -1,96 +1,95 @@
1
- import {describe, expect, it} from 'vitest';
2
- import {Optimization} from '../library.js';
3
-
4
- describe('Optimization.Operators.benfordTest', () => {
5
-
6
- it('should return 0 for non-array inputs', () => {
7
- expect(Optimization.Operators.benfordTest("12345")).toBe(0);
8
- expect(Optimization.Operators.benfordTest(null)).toBe(0);
9
- expect(Optimization.Operators.benfordTest(undefined)).toBe(0);
10
- expect(Optimization.Operators.benfordTest({ a: 1 })).toBe(0);
11
- });
12
-
13
- it('should return 0 for arrays with less than 10 valid numbers', () => {
14
- const smallArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
15
- expect(Optimization.Operators.benfordTest(smallArray)).toBe(0);
16
- });
17
-
18
- it('should return 0 for an empty array', () => {
19
- expect(Optimization.Operators.benfordTest([])).toBe(0);
20
- });
21
-
22
- it('should ignore zeros, non-numeric strings, and leading spaces', () => {
23
- // This array contains only 9 valid leading digits.
24
- const dirtyArray = [0, " 123", "abc", 2, 3, 4, 5, 6, 7, 8, 9, null, undefined];
25
- expect(Optimization.Operators.benfordTest(dirtyArray)).toBe(0);
26
- });
27
-
28
- it('should return a very low score for a distribution that perfectly matches Benford\'s law', () => {
29
- // Création d'un échantillon de 1000 nombres qui suit la loi de Benford
30
- const benfordSample = [
31
- ...Array(301).fill(100), // 301 nombres commençant par 1
32
- ...Array(176).fill(200), // 176 nombres commençant par 2
33
- ...Array(125).fill(300), // 125 nombres commençant par 3
34
- ...Array(97).fill(400), // etc.
35
- ...Array(79).fill(500),
36
- ...Array(67).fill(600),
37
- ...Array(58).fill(700),
38
- ...Array(51).fill(800),
39
- ...Array(46).fill(900),
40
- ];
41
-
42
- const score = Optimization.Operators.benfordTest(benfordSample);
43
- // Le score devrait être très proche de 0. On utilise toBeLessThan pour tolérer les imprécisions de calcul.
44
- expect(score).toBeLessThan(1e-9);
45
- });
46
-
47
- it('should return a high (suspect) score for a uniform distribution', () => {
48
- // Une distribution uniforme est très peu naturelle pour ce genre de données.
49
- const uniformSample = [];
50
- for (let i = 1; i <= 9; i++) {
51
- for (let j = 0; j < 100; j++) {
52
- uniformSample.push(i * 100 + j);
53
- }
54
- }
55
-
56
- const score = Optimization.Operators.benfordTest(uniformSample);
57
- // Un score > 0.15 est considéré comme suspect.
58
- expect(score).toBeGreaterThan(0.15);
59
- });
60
-
61
- it('should return a high (suspect) score for a distribution skewed towards high digits', () => {
62
- // L'inverse de la loi de Benford, très suspect.
63
- const inverseBenfordSample = [
64
- ...Array(46).fill(100),
65
- ...Array(51).fill(200),
66
- ...Array(58).fill(300),
67
- ...Array(67).fill(400),
68
- ...Array(79).fill(500),
69
- ...Array(97).fill(600),
70
- ...Array(125).fill(700),
71
- ...Array(176).fill(800),
72
- ...Array(301).fill(900),
73
- ];
74
-
75
- const score = Optimization.Operators.benfordTest(inverseBenfordSample);
76
- expect(score).toBeGreaterThan(0.3); // Score attendu encore plus élevé
77
- });
78
-
79
- it('should handle real-world-like data (request timings)', () => {
80
- // Simule des délais de requêtes générés par un bot (ex: aléatoire uniforme entre 500 et 1500ms)
81
- const botTimings = Array.from({ length: 100 }, () => 500 + Math.random() * 1000);
82
- const botScore = Optimization.Operators.benfordTest(botTimings);
83
-
84
- // Simule des délais humains (plus de petits délais, quelques longs délais)
85
- const humanTimings = [
86
- 123, 234, 180, 345, 150, 456, 110, 190, 210, 280, 567, 130, 890, 1200, 310, 160
87
- ];
88
- const humanScore = Optimization.Operators.benfordTest(humanTimings);
89
-
90
- // Le score du bot devrait être significativement plus élevé que celui de l'humain.
91
- // Les valeurs exactes peuvent varier, mais la tendance doit être claire.
92
- expect(botScore).toBeGreaterThan(0.1);
93
- expect(humanScore).toBeLessThan(botScore);
94
- });
95
-
1
+ import {describe, expect, it} from 'vitest';
2
+ import {Optimization} from '../library.js';
3
+
4
+ describe('Optimization.Operators.benfordTest', () => {
5
+
6
+ it('should return 0 for non-array inputs', () => {
7
+ expect(Optimization.Operators.benfordTest("12345")).toBe(0);
8
+ expect(Optimization.Operators.benfordTest(null)).toBe(0);
9
+ expect(Optimization.Operators.benfordTest(undefined)).toBe(0);
10
+ expect(Optimization.Operators.benfordTest({ a: 1 })).toBe(0);
11
+ });
12
+
13
+ it('should return 0 for arrays with less than 10 valid numbers', () => {
14
+ const smallArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
15
+ expect(Optimization.Operators.benfordTest(smallArray)).toBe(0);
16
+ });
17
+
18
+ it('should return 0 for an empty array', () => {
19
+ expect(Optimization.Operators.benfordTest([])).toBe(0);
20
+ });
21
+
22
+ it('should ignore zeros, non-numeric strings, and leading spaces', () => {
23
+ // This array contains only 9 valid leading digits.
24
+ const dirtyArray = [0, " 123", "abc", 2, 3, 4, 5, 6, 7, 8, 9, null, undefined];
25
+ expect(Optimization.Operators.benfordTest(dirtyArray)).toBe(0);
26
+ });
27
+
28
+ it('should return a very low score for a distribution that perfectly matches Benford\'s law', () => {
29
+ // Sample of 1000 numbers matching Benford's Law distribution
30
+ const benfordSample = [
31
+ ...Array(301).fill(100), // 301 numbers leading with 1
32
+ ...Array(176).fill(200), // 176 numbers leading with 2
33
+ ...Array(125).fill(300), // 125 numbers leading with 3
34
+ ...Array(97).fill(400), // etc.
35
+ ...Array(79).fill(500),
36
+ ...Array(67).fill(600),
37
+ ...Array(58).fill(700),
38
+ ...Array(51).fill(800),
39
+ ...Array(46).fill(900),
40
+ ];
41
+
42
+ const score = Optimization.Operators.benfordTest(benfordSample);
43
+ // Score should be close to 0; tolerance used for floating point variance
44
+ expect(score).toBeLessThan(1e-9);
45
+ });
46
+
47
+ it('should return a high (suspect) score for a uniform distribution', () => {
48
+ // Uniform distribution is unnatural for this category of metrics
49
+ const uniformSample = [];
50
+ for (let i = 1; i <= 9; i++) {
51
+ for (let j = 0; j < 100; j++) {
52
+ uniformSample.push(i * 100 + j);
53
+ }
54
+ }
55
+
56
+ const score = Optimization.Operators.benfordTest(uniformSample);
57
+ // A score > 0.15 is considered suspicious
58
+ expect(score).toBeGreaterThan(0.15);
59
+ });
60
+
61
+ it('should return a high (suspect) score for a distribution skewed towards high digits', () => {
62
+ // Inverted Benford distribution: highly unnatural
63
+ const inverseBenfordSample = [
64
+ ...Array(46).fill(100),
65
+ ...Array(51).fill(200),
66
+ ...Array(58).fill(300),
67
+ ...Array(67).fill(400),
68
+ ...Array(79).fill(500),
69
+ ...Array(97).fill(600),
70
+ ...Array(125).fill(700),
71
+ ...Array(176).fill(800),
72
+ ...Array(301).fill(900),
73
+ ];
74
+
75
+ const score = Optimization.Operators.benfordTest(inverseBenfordSample);
76
+ expect(score).toBeGreaterThan(0.3); // Significantly elevated score expected
77
+ });
78
+
79
+ it('should handle real-world-like data (request timings)', () => {
80
+ // Simulates bot request intervals (uniform random between 500 and 1500ms)
81
+ const botTimings = Array.from({ length: 100 }, () => 500 + Math.random() * 1000);
82
+ const botScore = Optimization.Operators.benfordTest(botTimings);
83
+
84
+ // Simulates human timings (more short delays, few long pauses)
85
+ const humanTimings = [
86
+ 123, 234, 180, 345, 150, 456, 110, 190, 210, 280, 567, 130, 890, 1200, 310, 160
87
+ ];
88
+ const humanScore = Optimization.Operators.benfordTest(humanTimings);
89
+
90
+ // Bot score should be significantly higher than human score
91
+ expect(botScore).toBeGreaterThan(0.1);
92
+ expect(humanScore).toBeLessThan(botScore);
93
+ });
94
+
96
95
  });