@anonympins/fingerprint 0.4.3 → 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.
@@ -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', () => {
@@ -2060,7 +2069,7 @@ describe('getClientHintsInconsistencyScore', () => {
2060
2069
  }
2061
2070
  };
2062
2071
  const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
2063
- expect(clientHintsInconsistencyScore).toBe(80);
2072
+ expect(clientHintsInconsistencyScore).toBe(97);
2064
2073
  });
2065
2074
 
2066
2075
  it('should return 40 for a small version mismatch', () => {
@@ -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,105 @@ 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
+ });
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
+ });