@anonympins/fingerprint 0.4.3 → 0.4.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,120 +1,141 @@
1
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
2
- import {JSDOM} from 'jsdom';
3
- import ClientLibrary from '../fingerprint.client.js';
4
-
5
- // --- Setup JSDOM Environment ---
6
- // Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
7
- // ici rend le test explicite et portable.
8
- const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
9
- url: 'http://localhost',
10
- });
11
-
12
- // In recent Node.js versions, some global properties like 'navigator', 'performance',
13
- // and 'screen' are read-only. To ensure our JSDOM environment works correctly across
14
- // all versions, we use Object.defineProperty to make these properties writable.
15
- Object.defineProperty(global, 'window', { value: dom.window, writable: true });
16
- Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
17
- Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
18
- Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
19
- Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
20
-
21
- global.fetch = vi.fn(); // Mock global fetch
22
- global.Headers = dom.window.Headers;
23
- global.Request = dom.window.Request;
24
- global.URL = dom.window.URL;
25
- global.TextEncoder = dom.window.TextEncoder;
26
-
27
-
28
- describe('ClientLibrary.initializeClient', () => {
29
-
30
- // On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
31
- let startMouseSpy, startKeystrokeSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy;
32
-
33
- beforeEach(() => {
34
- // Réinitialiser l'état du client avant chaque test
35
- ClientLibrary._resetCache();
36
-
37
- // Créer les espions sur les méthodes internes
38
- startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
39
- startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
40
- initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
41
- injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
42
- initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
43
- });
44
-
45
- afterEach(() => {
46
- // Restaurer les espions après chaque test pour ne pas affecter les autres tests
47
- vi.restoreAllMocks();
48
- });
49
-
50
- it('should enable all trackers by default', () => {
51
- ClientLibrary.initializeClient();
52
-
53
- expect(startMouseSpy).toHaveBeenCalled();
54
- expect(startKeystrokeSpy).toHaveBeenCalled();
55
- });
56
-
57
- it('should disable mouse and keystroke trackers when configured', () => {
58
- ClientLibrary.initializeClient({ mouse: false, keystrokes: false });
59
-
60
- expect(startMouseSpy).not.toHaveBeenCalled();
61
- expect(startKeystrokeSpy).not.toHaveBeenCalled();
62
- });
63
-
64
- it('should initialize honeypots with the provided field names', () => {
65
- const honeypotFields = ['email_confirm', 'user_nickname'];
66
- ClientLibrary.initializeClient({ honeypots: honeypotFields });
67
-
68
- expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
69
- });
70
-
71
- it('should not initialize honeypots if the array is empty', () => {
72
- ClientLibrary.initializeClient({ honeypots: [] });
73
-
74
- expect(initHoneypotsSpy).not.toHaveBeenCalled();
75
- });
76
-
77
- it('should inject trap URLs when provided', () => {
78
- const urls = ['/trap1?sig=123', '/trap2?sig=456'];
79
- ClientLibrary.initializeClient({ trapUrls: urls });
80
-
81
- expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
82
- });
83
-
84
- it('should not inject trap URLs if the array is empty', () => {
85
- ClientLibrary.initializeClient({ trapUrls: [] });
86
-
87
- expect(injectTrapsSpy).not.toHaveBeenCalled();
88
- });
89
-
90
- it('should initialize fetch interception when fetch config is present', () => {
91
- const targetDomains = ['api.example.com'];
92
- ClientLibrary.initializeClient({ fetch: { targetDomains } });
93
-
94
- expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
95
- });
96
-
97
- it('should not initialize fetch interception if fetch config is absent', () => {
98
- ClientLibrary.initializeClient(); // No fetch config
99
-
100
- expect(initFetchSpy).not.toHaveBeenCalled();
101
- });
102
-
103
- it('should call all initializers correctly when a full config is provided', () => {
104
- const config = {
105
- mouse: true,
106
- keystrokes: true,
107
- honeypots: ['field1'],
108
- trapUrls: ['/trap1'],
109
- fetch: { targetDomains: ['api.com'] }
110
- };
111
- ClientLibrary.initializeClient(config);
112
-
113
- expect(startMouseSpy).toHaveBeenCalled();
114
- expect(startKeystrokeSpy).toHaveBeenCalled();
115
- expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
116
- expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
117
- expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
118
- });
119
-
1
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
2
+ import {JSDOM} from 'jsdom';
3
+ import ClientLibrary from '../fingerprint.client.js';
4
+
5
+ // --- Setup JSDOM Environment ---
6
+ // Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
7
+ // ici rend le test explicite et portable.
8
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
9
+ url: 'http://localhost',
10
+ });
11
+
12
+ // In recent Node.js versions, some global properties like 'navigator', 'performance',
13
+ // and 'screen' are read-only. To ensure our JSDOM environment works correctly across
14
+ // all versions, we use Object.defineProperty to make these properties writable.
15
+ Object.defineProperty(global, 'window', { value: dom.window, writable: true });
16
+ Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
17
+ Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
18
+ Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
19
+ Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
20
+
21
+ global.fetch = vi.fn(); // Mock global fetch
22
+ global.Headers = dom.window.Headers;
23
+ global.Request = dom.window.Request;
24
+ global.URL = dom.window.URL;
25
+ global.TextEncoder = dom.window.TextEncoder;
26
+
27
+
28
+ describe('ClientLibrary.initializeClient', () => {
29
+
30
+ // On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
31
+ let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
32
+
33
+ beforeEach(() => {
34
+ // Réinitialiser l'état du client avant chaque test
35
+ ClientLibrary._resetCache();
36
+
37
+ // Créer les espions sur les méthodes internes
38
+ startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
39
+ startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
40
+ startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
41
+ startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
42
+ initWasmSpy = vi.spyOn(ClientLibrary, 'initializeWasm').mockResolvedValue(undefined);
43
+ initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
44
+ injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
45
+ initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
46
+ injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
47
+ });
48
+
49
+ afterEach(() => {
50
+ // Restaurer les espions après chaque test pour ne pas affecter les autres tests
51
+ vi.restoreAllMocks();
52
+ });
53
+
54
+ it('should enable all trackers by default', () => {
55
+ ClientLibrary.initializeClient();
56
+
57
+ expect(startMouseSpy).toHaveBeenCalled();
58
+ expect(startKeystrokeSpy).toHaveBeenCalled();
59
+ expect(startClickSpy).toHaveBeenCalled();
60
+ expect(startTouchSpy).toHaveBeenCalled();
61
+ expect(injectPhantomTrapsSpy).toHaveBeenCalled();
62
+ });
63
+
64
+ it('should disable trackers when configured', () => {
65
+ ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false });
66
+
67
+ expect(startMouseSpy).not.toHaveBeenCalled();
68
+ expect(startKeystrokeSpy).not.toHaveBeenCalled();
69
+ expect(startClickSpy).not.toHaveBeenCalled();
70
+ expect(startTouchSpy).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it('should initialize WASM if wasmPath is configured', () => {
74
+ ClientLibrary.initializeClient({ wasmPath: '/fp.js' });
75
+
76
+ expect(initWasmSpy).toHaveBeenCalledWith('/fp.js');
77
+ });
78
+
79
+ it('should disable phantom traps when configured', () => {
80
+ ClientLibrary.initializeClient({ phantomTraps: false });
81
+
82
+ expect(injectPhantomTrapsSpy).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('should initialize honeypots with the provided field names', () => {
86
+ const honeypotFields = ['email_confirm', 'user_nickname'];
87
+ ClientLibrary.initializeClient({ honeypots: honeypotFields });
88
+
89
+ expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
90
+ });
91
+
92
+ it('should not initialize honeypots if the array is empty', () => {
93
+ ClientLibrary.initializeClient({ honeypots: [] });
94
+
95
+ expect(initHoneypotsSpy).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it('should inject trap URLs when provided', () => {
99
+ const urls = ['/trap1?sig=123', '/trap2?sig=456'];
100
+ ClientLibrary.initializeClient({ trapUrls: urls });
101
+
102
+ expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
103
+ });
104
+
105
+ it('should not inject trap URLs if the array is empty', () => {
106
+ ClientLibrary.initializeClient({ trapUrls: [] });
107
+
108
+ expect(injectTrapsSpy).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it('should initialize fetch interception when fetch config is present', () => {
112
+ const targetDomains = ['api.example.com'];
113
+ ClientLibrary.initializeClient({ fetch: { targetDomains } });
114
+
115
+ expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
116
+ });
117
+
118
+ it('should not initialize fetch interception if fetch config is absent', () => {
119
+ ClientLibrary.initializeClient(); // No fetch config
120
+
121
+ expect(initFetchSpy).not.toHaveBeenCalled();
122
+ });
123
+
124
+ it('should call all initializers correctly when a full config is provided', () => {
125
+ const config = {
126
+ mouse: true,
127
+ keystrokes: true,
128
+ honeypots: ['field1'],
129
+ trapUrls: ['/trap1'],
130
+ fetch: { targetDomains: ['api.com'] }
131
+ };
132
+ ClientLibrary.initializeClient(config);
133
+
134
+ expect(startMouseSpy).toHaveBeenCalled();
135
+ expect(startKeystrokeSpy).toHaveBeenCalled();
136
+ expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
137
+ expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
138
+ expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
139
+ });
140
+
120
141
  });
@@ -1747,7 +1747,9 @@ describe('getRequestPatternScore', () => {
1747
1747
  dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(10000);
1748
1748
 
1749
1749
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, patternConfig);
1750
- expect(requestPatternScore).toBe(patternConfig.patternWeight); // 80
1750
+ // regularityScore = 1.0 (stdDev = 0). regularityRatio = 0.4.
1751
+ // instantScore = 1.0 * 0.4 * 80 = 32.
1752
+ expect(requestPatternScore).toBe(32);
1751
1753
  });
1752
1754
 
1753
1755
  test('should assign a high pattern score for non-natural (Benford-violating) timings', () => {
@@ -1760,7 +1762,11 @@ describe('getRequestPatternScore', () => {
1760
1762
  dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(10000);
1761
1763
 
1762
1764
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, patternConfig);
1763
- expect(requestPatternScore).toBe(patternConfig.patternWeight); // 80
1765
+ // stdDev ~ 31.29. regularityScore = 1 - (31.29 / 50) = ~0.374.
1766
+ // benfordDeviation ~ 2.07. benfordScore = 1.0 (capped).
1767
+ // weightedScore = (0.374 * 0.4) + (1.0 * 0.3) = ~0.4496.
1768
+ // instantScore = 0.4496 * 80 = ~35.97.
1769
+ expect(requestPatternScore).toBeCloseTo(35.97, 1);
1764
1770
  });
1765
1771
 
1766
1772
  test('should apply decay factor to the score over time', () => {
@@ -1963,8 +1969,9 @@ describe('Regularity Detection (Standard Deviation)', () => {
1963
1969
 
1964
1970
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, regularityConfig);
1965
1971
 
1966
- // stdDev is 0, which is < regularityThreshold, so the full patternWeight is applied.
1967
- expect(requestPatternScore).toBe(regularityConfig.patternWeight);
1972
+ // stdDev is 0 (regularityScore = 1.0). regularityRatio = 0.4.
1973
+ // Expected score: 1.0 * 0.4 * 60 = 24.
1974
+ expect(requestPatternScore).toBe(24);
1968
1975
  });
1969
1976
 
1970
1977
  it('should apply a low penalty for slightly irregular requests', () => {
@@ -1975,7 +1982,9 @@ describe('Regularity Detection (Standard Deviation)', () => {
1975
1982
 
1976
1983
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, localConfig);
1977
1984
 
1978
- expect(requestPatternScore).toBe(localConfig.patternWeight);
1985
+ // stdDev is ~7.07 (regularityScore = 1 - 7.07/10 = 0.293).
1986
+ // Expected score: 0.293 * 0.4 * 60 = 7.03.
1987
+ expect(requestPatternScore).toBeCloseTo(7.03, 1);
1979
1988
  });
1980
1989
 
1981
1990
  it('should apply no penalty for highly irregular (human-like) requests', () => {
@@ -2125,19 +2134,27 @@ describe('Subnet Scoring (Node.js)', () => {
2125
2134
  });
2126
2135
 
2127
2136
  it('updateSubnetMetrics should create and update subnet data in the store', async () => {
2128
- const context = { clientIp: '10.0.0.25' };
2129
- await __internal.updateSubnetMetrics(context, 'device-1', 50);
2137
+ const context1 = { clientIp: '10.0.0.25', headers: { 'user-agent': 'device-1' } };
2138
+ await __internal.updateSubnetMetrics(context1, 'device-1', 50);
2139
+
2140
+ const fp1 = new FingerprintBuilder().add('ua', 'device-1').toString();
2141
+ const expectedId1 = cyrb53(fp1).toString();
2130
2142
 
2131
2143
  const subnetData = await inMemoryStore.get('subnet:10.0.0.0/24');
2132
2144
  expect(subnetData).toBeDefined();
2133
2145
  expect(subnetData.highScoreCount).toBe(1);
2134
- expect(subnetData.deviceIds).toEqual(['device-1']);
2146
+ expect(subnetData.deviceIds).toEqual([expectedId1]);
2135
2147
 
2136
2148
  // Second update
2137
- await __internal.updateSubnetMetrics(context, 'device-2', 60);
2149
+ const context2 = { clientIp: '10.0.0.25', headers: { 'user-agent': 'device-2' } };
2150
+ await __internal.updateSubnetMetrics(context2, 'device-2', 60);
2151
+
2152
+ const fp2 = new FingerprintBuilder().add('ua', 'device-2').toString();
2153
+ const expectedId2 = cyrb53(fp2).toString();
2154
+
2138
2155
  const updatedSubnetData = await inMemoryStore.get('subnet:10.0.0.0/24');
2139
2156
  expect(updatedSubnetData.highScoreCount).toBe(2);
2140
- expect(updatedSubnetData.deviceIds).toEqual(['device-1', 'device-2']);
2157
+ expect(updatedSubnetData.deviceIds).toEqual([expectedId1, expectedId2]);
2141
2158
  });
2142
2159
 
2143
2160
  it('getSubnetScore should calculate score based on stored metrics', async () => {
@@ -2349,3 +2366,86 @@ describe('Additional Suspicion Vectors Coverage', () => {
2349
2366
  expect(crossLayerInconsistencyScore).toBe(50);
2350
2367
  });
2351
2368
  });
2369
+
2370
+ describe('Botnet Cluster Scoring (Node.js)', () => {
2371
+ const inMemoryStore = {
2372
+ _map: new Map(),
2373
+ async get(key) { return this._map.get(key); },
2374
+ async set(key, value) { this._map.set(key, value); },
2375
+ clear() { this._map.clear(); }
2376
+ };
2377
+
2378
+ beforeEach(async () => {
2379
+ inMemoryStore.clear();
2380
+ await configureStore(inMemoryStore);
2381
+ });
2382
+
2383
+ it('should calculate botnetClusterScore based on unique IPs within 10 minutes', async () => {
2384
+ const { getBotnetClusterScore } = __internal;
2385
+ const stableFpHash = 'test-stable-hash';
2386
+
2387
+ // 1. Première IP
2388
+ let scoreData = await getBotnetClusterScore({ clientIp: '192.168.1.1' }, stableFpHash);
2389
+ expect(scoreData.botnetClusterScore).toBe(0);
2390
+
2391
+ // 2. Ajout de 2 IPs uniques (total 3)
2392
+ await getBotnetClusterScore({ clientIp: '192.168.1.2' }, stableFpHash);
2393
+ scoreData = await getBotnetClusterScore({ clientIp: '192.168.1.3' }, stableFpHash);
2394
+ expect(scoreData.botnetClusterScore).toBe(50.3);
2395
+
2396
+ // 3. Ajout de 2 IPs uniques (total 5)
2397
+ await getBotnetClusterScore({ clientIp: '192.168.1.4' }, stableFpHash);
2398
+ scoreData = await getBotnetClusterScore({ clientIp: '192.168.1.5' }, stableFpHash);
2399
+ expect(scoreData.botnetClusterScore).toBe(75.3);
2400
+
2401
+ // 4. Ajout de 5 IPs uniques (total 10)
2402
+ for (let i = 6; i <= 10; i++) {
2403
+ scoreData = await getBotnetClusterScore({ clientIp: `192.168.1.${i}` }, stableFpHash);
2404
+ }
2405
+ expect(scoreData.botnetClusterScore).toBe(95.7);
2406
+ });
2407
+
2408
+ it('should realistically group PS4 consoles with volatile differences (different IPs/cookies) under the same cluster score', async () => {
2409
+ const { getSuspicionVector } = __internal;
2410
+
2411
+ const ps4Headers = {
2412
+ 'user-agent': 'Mozilla/5.0 (PlayStation 4 11.50) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.50 Safari/605.1.15',
2413
+ 'x-ja3-hash': '76993ef93bf89104037599723ab9f201',
2414
+ 'x-ja4-hash': 't13d1516h2_8daaf6152771_390237aa04be',
2415
+ 'x-http2-fingerprint': '1:65536;3:1000;4:6291456;6:65536',
2416
+ 'x-tcp-fingerprint': '64240:128:1:mss,nop,ws,nop,nop,sok:df:0'
2417
+ };
2418
+
2419
+ // Simule des requêtes provenant de 10 consoles PlayStation 4 infectées différentes (IPs différentes, pas de cookies communs)
2420
+ for (let i = 1; i <= 10; i++) {
2421
+ const context = {
2422
+ clientIp: `185.15.20.${i}`,
2423
+ path: '/api/login',
2424
+ headers: {
2425
+ ...ps4Headers,
2426
+ 'cookie_keys': `session_id=fake_sess_${i}` // Élément volatil
2427
+ },
2428
+ cookies: {}, // Pas de cookie device_id partagé pour simuler des terminaux distincts
2429
+ query: {},
2430
+ httpVersion: '2.0',
2431
+ requestTimestamp: Date.now()
2432
+ };
2433
+
2434
+ const vector = await getSuspicionVector(context, { honeypot: {}, patterns: {} });
2435
+
2436
+ if (i === 1) {
2437
+ expect(vector.botnetClusterScore).toBe(0);
2438
+ } else if (i === 2) {
2439
+ expect(vector.botnetClusterScore).toBe(29.5);
2440
+ } else if (i === 3) {
2441
+ expect(vector.botnetClusterScore).toBe(50.3);
2442
+ } else if (i === 4) {
2443
+ expect(vector.botnetClusterScore).toBe(65);
2444
+ } else if (i === 5) {
2445
+ expect(vector.botnetClusterScore).toBe(75.3);
2446
+ } else if (i === 10) {
2447
+ expect(vector.botnetClusterScore).toBe(95.7);
2448
+ }
2449
+ }
2450
+ });
2451
+ });