@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.
- package/CHANGELOG.md +43 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +158 -0
- package/src/js/fingerprint.client.js +852 -635
- package/src/js/fingerprint.js +582 -48
- package/src/js/tests/dynamic-wasm.test.js +78 -0
- package/src/js/tests/fingerprint.client.init.test.js +140 -119
- package/src/js/tests/fingerprint.client.test.js +128 -104
- package/src/js/tests/fingerprint.test.js +130 -11
- package/src/js/tests/ip-reputation.test.js +141 -131
- package/src/js/tests/tcpFingerprint.test.js +78 -0
- package/src/php/Challenge/ChallengeUtils.php +480 -361
- package/src/php/Config/SecurityProfiles.php +286 -271
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +30 -0
- package/src/php/RequestContext.php +90 -90
- package/src/php/Store/IStore.php +41 -41
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/FingerprintEngineTest.php +27 -1
- package/src/php/Tests/IpReputationTest.php +175 -156
- package/src/php/Tests/RequestUtilsTest.php +130 -0
- package/src/php/Utils/RequestUtils.php +474 -21
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1967
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
2129
|
-
await __internal.updateSubnetMetrics(
|
|
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([
|
|
2146
|
+
expect(subnetData.deviceIds).toEqual([expectedId1]);
|
|
2135
2147
|
|
|
2136
2148
|
// Second update
|
|
2137
|
-
|
|
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([
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
});
|