@anonympins/fingerprint 0.4.4 → 0.4.5

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,105 +1,129 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
-
5
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
6
- import ClientLibrary from '../fingerprint.client.js';
7
-
8
- describe('ClientLibrary WASM Integration', () => {
9
- beforeEach(() => {
10
- // Réinitialise le cache et les mocks avant chaque test
11
- ClientLibrary._resetCache();
12
- vi.spyOn(console, 'log').mockImplementation(() => {});
13
- vi.spyOn(console, 'warn').mockImplementation(() => {});
14
-
15
- // Supprime les mocks globaux pour éviter les fuites entre les tests
16
- delete window.createFingerprintModule;
17
- });
18
-
19
- afterEach(() => {
20
- vi.restoreAllMocks();
21
- });
22
-
23
- it('should successfully load WASM module and switch hasher', async () => {
24
- // 1. Simuler un module WASM fonctionnel
25
- const mockWasmModule = {
26
- _hash_string: vi.fn((str) => 99999), // Un mock qui retourne une valeur distincte
27
- };
28
-
29
- // 2. Simuler le chargement du script et l'initialisation du module
30
- // On attache les fonctions de simulation à `window` car c'est ce que le code cherche
31
- window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
32
-
33
- // On simule l'injection du script en appelant directement `onload`
34
- vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
35
- // Simule le chargement réussi du script
36
- script.onload();
37
- return script;
38
- });
39
-
40
- // 3. Appeler la fonction d'initialisation
41
- await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
42
-
43
- // 4. Vérifier que le hasher a été remplacé
44
- const wasmHash = ClientLibrary._hasher("test");
45
- expect(wasmHash).toBe(99999);
46
- expect(mockWasmModule._hash_string).toHaveBeenCalledWith("test");
47
- expect(console.log).toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
48
- });
49
-
50
- it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
51
- // 1. Simuler un échec de chargement du script
52
- vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
53
- script.onerror(new Error('Script loading failed'));
54
- return script;
55
- });
56
-
57
- // 2. Appeler la fonction d'initialisation
58
- await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
59
-
60
- // 3. Vérifier que le hasher est toujours l'implémentation JS
61
- const jsHash = ClientLibrary._hasher("test");
62
- const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
63
-
64
- expect(jsHash).toBe(originalJsHash);
65
- expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'), expect.any(Error));
66
- });
67
-
68
- it('should gracefully fall back if WASM module does not export _hash_string', async () => {
69
- // 1. Simuler un module WASM malformé (sans la fonction attendue)
70
- const mockWasmModule = {};
71
- window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
72
-
73
- vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
74
- script.onload();
75
- return script;
76
- });
77
-
78
- // 2. Appeler la fonction d'initialisation
79
- await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
80
-
81
- // 3. Vérifier le fallback
82
- const jsHash = ClientLibrary._hasher("test");
83
- const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
84
- expect(jsHash).toBe(originalJsHash);
85
- // L'assertion est maintenant plus précise : elle vérifie le message générique ET le message d'erreur spécifique.
86
- expect(console.warn).toHaveBeenCalledWith(
87
- expect.stringContaining('WASM module failed to load'),
88
- expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
89
- );
90
- });
91
-
92
- it('should use the default JS hasher if WASM is not configured', async () => {
93
- // Ensure no WASM path is provided in the config
94
- ClientLibrary.initializeClient({});
95
-
96
- // Verify that the active hasher is the original JS implementation
97
- const jsHash = ClientLibrary._hasher("another test string");
98
- const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("another test string");
99
-
100
- expect(jsHash).toBe(originalJsHash);
101
- // Ensure no WASM-related console logs or warnings were made
102
- expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
103
- expect(console.warn).not.toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'));
104
- });
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
6
+ import ClientLibrary from '../fingerprint.client.js';
7
+
8
+ describe('ClientLibrary WASM Integration', () => {
9
+ beforeEach(() => {
10
+ // Réinitialise le cache et les mocks avant chaque test
11
+ ClientLibrary._resetCache();
12
+ vi.spyOn(console, 'log').mockImplementation(() => {});
13
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
14
+
15
+ // Supprime les mocks globaux pour éviter les fuites entre les tests
16
+ delete window.createFingerprintModule;
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ it('should successfully load WASM module and switch hasher', async () => {
24
+ // 1. Simuler un module WASM fonctionnel
25
+ const mockWasmModule = {
26
+ _hash_string: vi.fn((str) => 99999), // Un mock qui retourne une valeur distincte
27
+ };
28
+
29
+ // 2. Simuler le chargement du script et l'initialisation du module
30
+ // On attache les fonctions de simulation à `window` car c'est ce que le code cherche
31
+ window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
32
+
33
+ // On simule l'injection du script en appelant directement `onload`
34
+ vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
35
+ // Simule le chargement réussi du script
36
+ script.onload();
37
+ return script;
38
+ });
39
+
40
+ // 3. Appeler la fonction d'initialisation
41
+ await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
42
+
43
+ // 4. Vérifier que le hasher a été remplacé
44
+ const wasmHash = ClientLibrary._hasher("test");
45
+ expect(wasmHash).toBe(99999);
46
+ expect(mockWasmModule._hash_string).toHaveBeenCalledWith("test");
47
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
48
+ });
49
+
50
+ it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
51
+ // 1. Simuler un échec de chargement du script
52
+ vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
53
+ script.onerror(new Error('Script loading failed'));
54
+ return script;
55
+ });
56
+
57
+ // 2. Appeler la fonction d'initialisation
58
+ await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
59
+
60
+ // 3. Vérifier que le hasher est toujours l'implémentation JS
61
+ const jsHash = ClientLibrary._hasher("test");
62
+ const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
63
+
64
+ expect(jsHash).toBe(originalJsHash);
65
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'), expect.any(Error));
66
+ });
67
+
68
+ it('should gracefully fall back if WASM module does not export _hash_string', async () => {
69
+ // 1. Simuler un module WASM malformé (sans la fonction attendue)
70
+ const mockWasmModule = {};
71
+ window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
72
+
73
+ vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
74
+ script.onload();
75
+ return script;
76
+ });
77
+
78
+ // 2. Appeler la fonction d'initialisation
79
+ await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
80
+
81
+ // 3. Vérifier le fallback
82
+ const jsHash = ClientLibrary._hasher("test");
83
+ const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
84
+ expect(jsHash).toBe(originalJsHash);
85
+ // L'assertion est maintenant plus précise : elle vérifie le message générique ET le message d'erreur spécifique.
86
+ expect(console.warn).toHaveBeenCalledWith(
87
+ expect.stringContaining('WASM module failed to load'),
88
+ expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
89
+ );
90
+ });
91
+
92
+ it('should successfully load raw WASM and use standalone polymorphic hasher', async () => {
93
+ const mockWasmInstance = {
94
+ exports: {
95
+ memory: { buffer: new ArrayBuffer(65536) },
96
+ hash: vi.fn().mockReturnValue(123456)
97
+ }
98
+ };
99
+
100
+ global.WebAssembly = {
101
+ compile: vi.fn().mockResolvedValue({}),
102
+ instantiate: vi.fn().mockResolvedValue(mockWasmInstance)
103
+ };
104
+
105
+ global.fetch = vi.fn().mockResolvedValue({
106
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(100))
107
+ });
108
+
109
+ await ClientLibrary.initializeWasm('/fp.wasm');
110
+
111
+ const hash = ClientLibrary._hasher("test");
112
+ expect(hash).toBe(123456);
113
+ expect(mockWasmInstance.exports.hash).toHaveBeenCalledWith(0, 4);
114
+ });
115
+
116
+ it('should use the default JS hasher if WASM is not configured', async () => {
117
+ // Ensure no WASM path is provided in the config
118
+ ClientLibrary.initializeClient({});
119
+
120
+ // Verify that the active hasher is the original JS implementation
121
+ const jsHash = ClientLibrary._hasher("another test string");
122
+ const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("another test string");
123
+
124
+ expect(jsHash).toBe(originalJsHash);
125
+ // Ensure no WASM-related console logs or warnings were made
126
+ expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
127
+ expect(console.warn).not.toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'));
128
+ });
105
129
  });
@@ -2069,7 +2069,7 @@ describe('getClientHintsInconsistencyScore', () => {
2069
2069
  }
2070
2070
  };
2071
2071
  const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
2072
- expect(clientHintsInconsistencyScore).toBe(80);
2072
+ expect(clientHintsInconsistencyScore).toBe(97);
2073
2073
  });
2074
2074
 
2075
2075
  it('should return 40 for a small version mismatch', () => {
@@ -2449,3 +2449,22 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
2449
2449
  }
2450
2450
  });
2451
2451
  });
2452
+
2453
+ test('Stateless Ticket Generation and Validation', async () => {
2454
+ const payload = {
2455
+ expiry: Date.now() + 3600000,
2456
+ originalIp: '127.0.0.1',
2457
+ deviceId: 'device-123',
2458
+ deviceHash: 'hash-abc'
2459
+ };
2460
+
2461
+ const ticket = fingerprint.generateStatelessTicket(payload);
2462
+ expect(ticket).toBeTruthy();
2463
+ expect(ticket.split('.').length).toBe(3);
2464
+
2465
+ const isValid = await fingerprint.isTicketValid('127.0.0.1', ticket, 'device-123', 'hash-abc');
2466
+ expect(isValid).toBe(true);
2467
+
2468
+ const isDiffIpValid = await fingerprint.isTicketValid('192.168.1.1', ticket, 'device-123', 'hash-abc', false);
2469
+ expect(isDiffIpValid).toBe(false);
2470
+ });
@@ -1,132 +1,142 @@
1
- import {beforeEach, describe, expect, it} from 'vitest';
2
- import {__internal, configureStore, FingerprintEngine} from '../fingerprint.js';
3
-
4
- describe('IP Reputation Local System (Node.js)', () => {
5
- let mockStore;
6
-
7
- beforeEach(() => {
8
- mockStore = {
9
- _map: new Map(),
10
- async get(key) { return this._map.get(key); },
11
- async set(key, value) { this._map.set(key, value); },
12
- async has(key) { return this._map.has(key); },
13
- async delete(key) { this._map.delete(key); }
14
- };
15
- configureStore(mockStore);
16
- });
17
-
18
- it('should return a default score of 0 for unknown IPs', async () => {
19
- const score = await __internal.getIpReputationScore('1.1.1.1');
20
- expect(score).toBe(0);
21
- });
22
-
23
- it('should correctly increment and decrement score values', async () => {
24
- const ip = '192.168.1.1';
25
- await __internal.updateIpReputationScore(ip, 30);
26
- let score = await __internal.getIpReputationScore(ip);
27
- expect(score).toBe(30);
28
-
29
- await __internal.updateIpReputationScore(ip, -10);
30
- score = await __internal.getIpReputationScore(ip);
31
- expect(score).toBe(20);
32
- });
33
-
34
- it('should clamp the reputation score within bounds [0, 100]', async () => {
35
- const ip = '10.0.0.1';
36
- await __internal.updateIpReputationScore(ip, 150);
37
- let score = await __internal.getIpReputationScore(ip);
38
- expect(score).toBe(100);
39
-
40
- await __internal.updateIpReputationScore(ip, -200);
41
- score = await __internal.getIpReputationScore(ip);
42
- expect(score).toBe(0);
43
- });
44
-
45
- it('should apply passive decay of 2 points per hour of inactivity', async () => {
46
- const ip = '172.16.0.1';
47
- const now = Date.now();
48
-
49
- // Simulation d'un score de 50 mis à jour il y a 3 heures (3h * 2 points/heure = 6 points de perte)
50
- await mockStore.set(`ip-reputation:${ip}`, {
51
- score: 50,
52
- lastUpdate: now - (3 * 60 * 60 * 1000)
53
- });
54
-
55
- const score = await __internal.getIpReputationScore(ip);
56
- expect(score).toBe(44);
57
- });
58
-
59
- it('should integrate ipReputationScore into the final score calculation', async () => {
60
- const ip = '1.2.3.4';
61
- await __internal.updateIpReputationScore(ip, 60); // Set IP reputation to 60
62
-
63
- const mockSecurityConfig = {
64
- weights: {
65
- ipReputationScore: 0.5, // Give it a weight
66
- historyScore: 0.0, // Other weights to 0 for isolation
67
- rotationScore: 0.0,
68
- headerAnomalyScore: 0.0,
69
- requestPatternScore: 0.0,
70
- inconsistencyScore: 0.0,
71
- honeypotScore: 0.0,
72
- behaviorScore: 0.0,
73
- botScore: 0.0,
74
- crossLayerInconsistencyScore: 0.0,
75
- tlsSpoofingScore: 0.0,
76
- timeInconsistencyScore: 0.0,
77
- clickVarianceScore: 0.0,
78
- clientHintsInconsistencyScore: 0.0,
79
- subnetScore: 0.0,
80
- },
81
- thresholds: { low: 0, medium: 0, high: 0, block: 100 }, // Irrelevant for this test
82
- };
83
-
84
- const ipRepScore = await __internal.getIpReputationScore(ip);
85
- const suspicionVector = { ipReputationScore: ipRepScore };
86
-
87
- const engine = new FingerprintEngine(mockSecurityConfig);
88
- const finalScore = engine.calculateFinalScore(suspicionVector);
89
-
90
- // Expected score: 60 (ipRepScore) * 0.5 (weight) = 30
91
- expect(finalScore).toBe(30.0);
92
- });
93
-
94
- it('should return correct subnet for IPv4 and IPv6', async () => {
95
- const ipv4Subnet = __internal.getIpSubnet('192.168.1.50', 24, 48);
96
- expect(ipv4Subnet).toBe('192.168.1.0/24');
97
-
98
- const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
99
- expect(ipv6Subnet).toBe('2001:db8:abcd:0:0:0:0:0/48');
100
- });
101
-
102
- it('should calculate client hints inconsistency score correctly', async () => {
103
- const mockContextMismatch = {
104
- headers: {
105
- 'user-agent': 'Mozilla/5.0 Firefox/117.0',
106
- 'sec-ch-ua': '"Google Chrome";v="117"'
107
- }
108
- };
109
- const scoreMismatch = __internal.getClientHintsInconsistencyScore(mockContextMismatch);
110
- expect(scoreMismatch.clientHintsInconsistencyScore).toBe(90);
111
-
112
- const mockContextVersionDrift = {
113
- headers: {
114
- 'user-agent': 'Mozilla/5.0 Chrome/110.0',
115
- 'sec-ch-ua': '"Google Chrome";v="117"'
116
- }
117
- };
118
- const scoreVersionDrift = __internal.getClientHintsInconsistencyScore(mockContextVersionDrift);
119
- expect(scoreVersionDrift.clientHintsInconsistencyScore).toBe(80);
120
- });
121
-
122
- it('should integrate subnet history into subnet score', async () => {
123
- const mockContext = { clientIp: '192.168.1.50' };
124
- let score = await __internal.getSubnetScore(mockContext);
125
- expect(score.subnetScore).toBe(0);
126
- for (let i = 1; i <= 12; i++) {
127
- await __internal.updateSubnetMetrics(mockContext, `dev-${i}`, 40);
128
- }
129
- score = await __internal.getSubnetScore(mockContext);
130
- expect(score.subnetScore).toBeGreaterThan(0);
131
- });
1
+ import {beforeEach, describe, expect, it} from 'vitest';
2
+ import {__internal, configureStore, FingerprintEngine} from '../fingerprint.js';
3
+
4
+ describe('IP Reputation Local System (Node.js)', () => {
5
+ let mockStore;
6
+
7
+ beforeEach(() => {
8
+ mockStore = {
9
+ _map: new Map(),
10
+ async get(key) { return this._map.get(key); },
11
+ async set(key, value) { this._map.set(key, value); },
12
+ async has(key) { return this._map.has(key); },
13
+ async delete(key) { this._map.delete(key); }
14
+ };
15
+ configureStore(mockStore);
16
+ });
17
+
18
+ it('should return a default score of 0 for unknown IPs', async () => {
19
+ const score = await __internal.getIpReputationScore('1.1.1.1');
20
+ expect(score).toBe(0);
21
+ });
22
+
23
+ it('should correctly increment and decrement score values', async () => {
24
+ const ip = '192.168.1.1';
25
+ await __internal.updateIpReputationScore(ip, 30);
26
+ let score = await __internal.getIpReputationScore(ip);
27
+ expect(score).toBe(30);
28
+
29
+ await __internal.updateIpReputationScore(ip, -10);
30
+ score = await __internal.getIpReputationScore(ip);
31
+ expect(score).toBe(20);
32
+ });
33
+
34
+ it('should clamp the reputation score within bounds [0, 100]', async () => {
35
+ const ip = '10.0.0.1';
36
+ await __internal.updateIpReputationScore(ip, 150);
37
+ let score = await __internal.getIpReputationScore(ip);
38
+ expect(score).toBe(100);
39
+
40
+ await __internal.updateIpReputationScore(ip, -200);
41
+ score = await __internal.getIpReputationScore(ip);
42
+ expect(score).toBe(0);
43
+ });
44
+
45
+ it('should apply passive decay of 2 points per hour of inactivity', async () => {
46
+ const ip = '172.16.0.1';
47
+ const now = Date.now();
48
+
49
+ // Simulation d'un score de 50 mis à jour il y a 3 heures (3h * 2 points/heure = 6 points de perte)
50
+ await mockStore.set(`ip-reputation:${ip}`, {
51
+ score: 50,
52
+ lastUpdate: now - (3 * 60 * 60 * 1000)
53
+ });
54
+
55
+ const score = await __internal.getIpReputationScore(ip);
56
+ expect(score).toBe(44);
57
+ });
58
+
59
+ it('should integrate ipReputationScore into the final score calculation', async () => {
60
+ const ip = '1.2.3.4';
61
+ await __internal.updateIpReputationScore(ip, 60); // Set IP reputation to 60
62
+
63
+ const mockSecurityConfig = {
64
+ weights: {
65
+ ipReputationScore: 0.5, // Give it a weight
66
+ historyScore: 0.0, // Other weights to 0 for isolation
67
+ rotationScore: 0.0,
68
+ headerAnomalyScore: 0.0,
69
+ requestPatternScore: 0.0,
70
+ inconsistencyScore: 0.0,
71
+ honeypotScore: 0.0,
72
+ behaviorScore: 0.0,
73
+ botScore: 0.0,
74
+ crossLayerInconsistencyScore: 0.0,
75
+ tlsSpoofingScore: 0.0,
76
+ timeInconsistencyScore: 0.0,
77
+ clickVarianceScore: 0.0,
78
+ clientHintsInconsistencyScore: 0.0,
79
+ subnetScore: 0.0,
80
+ },
81
+ thresholds: { low: 0, medium: 0, high: 0, block: 100 }, // Irrelevant for this test
82
+ };
83
+
84
+ const ipRepScore = await __internal.getIpReputationScore(ip);
85
+ const suspicionVector = { ipReputationScore: ipRepScore };
86
+
87
+ const engine = new FingerprintEngine(mockSecurityConfig);
88
+ const finalScore = engine.calculateFinalScore(suspicionVector);
89
+
90
+ // Expected score: 60 (ipRepScore) * 0.5 (weight) = 30
91
+ expect(finalScore).toBe(30.0);
92
+ });
93
+
94
+ it('should return correct subnet for IPv4 and IPv6', async () => {
95
+ const ipv4Subnet = __internal.getIpSubnet('192.168.1.50', 24, 48);
96
+ expect(ipv4Subnet).toBe('192.168.1.0/24');
97
+
98
+ const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
99
+ expect(ipv6Subnet).toBe('2001:db8:abcd:0:0:0:0:0/48');
100
+ });
101
+
102
+ it('should calculate client hints inconsistency score correctly', async () => {
103
+ const mockContextMismatch = {
104
+ headers: {
105
+ 'user-agent': 'Mozilla/5.0 Firefox/117.0',
106
+ 'sec-ch-ua': '"Google Chrome";v="117"'
107
+ }
108
+ };
109
+ const scoreMismatch = __internal.getClientHintsInconsistencyScore(mockContextMismatch);
110
+ expect(scoreMismatch.clientHintsInconsistencyScore).toBe(90);
111
+
112
+ const mockContextVersionDrift = {
113
+ headers: {
114
+ 'user-agent': 'Mozilla/5.0 Chrome/110.0',
115
+ 'sec-ch-ua': '"Google Chrome";v="117"'
116
+ }
117
+ };
118
+ const scoreVersionDrift = __internal.getClientHintsInconsistencyScore(mockContextVersionDrift);
119
+ expect(scoreVersionDrift.clientHintsInconsistencyScore).toBe(80);
120
+
121
+ const mockContextFullVersionMismatch = {
122
+ headers: {
123
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.1.2 Safari/537.36',
124
+ 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
125
+ 'sec-ch-ua-full-version-list': '"Not_A Brand";v="8.0.0.0", "Chromium";v="120.0.1.3", "Google Chrome";v="120.0.1.3"'
126
+ }
127
+ };
128
+ const scoreFullVersionMismatch = __internal.getClientHintsInconsistencyScore(mockContextFullVersionMismatch);
129
+ expect(scoreFullVersionMismatch.clientHintsInconsistencyScore).toBe(85);
130
+ });
131
+
132
+ it('should integrate subnet history into subnet score', async () => {
133
+ const mockContext = { clientIp: '192.168.1.50' };
134
+ let score = await __internal.getSubnetScore(mockContext);
135
+ expect(score.subnetScore).toBe(0);
136
+ for (let i = 1; i <= 12; i++) {
137
+ await __internal.updateSubnetMetrics(mockContext, `dev-${i}`, 40);
138
+ }
139
+ score = await __internal.getSubnetScore(mockContext);
140
+ expect(score.subnetScore).toBeGreaterThan(0);
141
+ });
132
142
  });
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __internal } from '../fingerprint.js';
3
+
4
+ const { parseTcpSyn, classifyTcpOs, getTcpAnomalyScore } = __internal;
5
+
6
+ describe('Passive TCP/IP Fingerprinting (Type p0f)', () => {
7
+
8
+ // Mock d'un paquet IP + TCP SYN d'un système Linux (TTL=64, WS=7)
9
+ const mockLinuxSynPacket = Buffer.from([
10
+ 0x45, 0x00, 0x00, 0x3c, 0x1a, 0x2b, 0x40, 0x00,
11
+ 0x40, 0x06, 0x3c, 0x1a, 0x7f, 0x00, 0x00, 0x01, // TTL=0x40 (64)
12
+ 0x7f, 0x00, 0x00, 0x01,
13
+ 0x1f, 0x90, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01,
14
+ 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0x72, 0x10, // Window=0x7210 (29200), TCP header len = 40 (0xa0)
15
+ 0x3c, 0x1a, 0x00, 0x00,
16
+ 0x02, 0x04, 0x05, 0xb4, // Option MSS: 1460 (0x05b4)
17
+ 0x04, 0x02, // Option SACK Permitted
18
+ 0x01, // NOP
19
+ 0x03, 0x03, 0x07 // Option WS: 7
20
+ ]);
21
+
22
+ // Mock d'un paquet IP + TCP SYN d'un système Windows (TTL=128, WS=8)
23
+ const mockWindowsSynPacket = Buffer.from([
24
+ 0x45, 0x00, 0x00, 0x3c, 0x1a, 0x2b, 0x40, 0x00,
25
+ 0x80, 0x06, 0x3c, 0x1a, 0x7f, 0x00, 0x00, 0x01, // TTL=0x80 (128)
26
+ 0x7f, 0x00, 0x00, 0x01,
27
+ 0x1f, 0x90, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01,
28
+ 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfa, 0xf0, // Window=64240 (0xfaf0)
29
+ 0x3c, 0x1a, 0x00, 0x00,
30
+ 0x02, 0x04, 0x05, 0xb4, // Option MSS: 1460 (0x05b4)
31
+ 0x04, 0x02, // Option SACK Permitted
32
+ 0x01, // NOP
33
+ 0x03, 0x03, 0x08 // Option WS: 8
34
+ ]);
35
+
36
+ it('should correctly parse raw TCP SYN binary packets', () => {
37
+ const linuxFp = parseTcpSyn(mockLinuxSynPacket);
38
+ expect(linuxFp).not.toBeNull();
39
+ expect(linuxFp.ttl).toBe(64);
40
+ expect(linuxFp.windowSize).toBe(29200);
41
+ expect(linuxFp.mss).toBe(1460);
42
+ expect(linuxFp.ws).toBe(7);
43
+ expect(linuxFp.sack).toBe(true);
44
+
45
+ const windowsFp = parseTcpSyn(mockWindowsSynPacket);
46
+ expect(windowsFp.ttl).toBe(128);
47
+ expect(windowsFp.windowSize).toBe(64240);
48
+ expect(windowsFp.ws).toBe(8);
49
+ });
50
+
51
+ it('should classify OS correctly based on parsed parameters', () => {
52
+ const linuxFp = parseTcpSyn(mockLinuxSynPacket);
53
+ expect(classifyTcpOs(linuxFp)).toBe('Linux');
54
+
55
+ const windowsFp = parseTcpSyn(mockWindowsSynPacket);
56
+ expect(classifyTcpOs(windowsFp)).toBe('Windows');
57
+ });
58
+
59
+ it('should calculate correct TCP/IP anomaly scores', () => {
60
+ // Cas 1 : Le User-Agent prétend être Windows, mais la pile TCP/IP est Linux
61
+ const contextAnomaly = {
62
+ headers: {
63
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0',
64
+ },
65
+ rawTcpBinary: mockLinuxSynPacket
66
+ };
67
+ expect(getTcpAnomalyScore(contextAnomaly).tcpAnomalyScore).toBe(80.1);
68
+
69
+ // Cas 2 : Cohérence complète (UA Windows et pile TCP/IP Windows)
70
+ const contextCoherent = {
71
+ headers: {
72
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0',
73
+ },
74
+ rawTcpBinary: mockWindowsSynPacket
75
+ };
76
+ expect(getTcpAnomalyScore(contextCoherent).tcpAnomalyScore).toBe(0);
77
+ });
78
+ });