@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.
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +158 -0
- package/src/js/fingerprint.client.js +21 -0
- package/src/js/fingerprint.js +344 -20
- package/src/js/tests/dynamic-wasm.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +128 -104
- package/src/js/tests/fingerprint.test.js +20 -1
- 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 +77 -12
- package/src/php/Config/SecurityProfiles.php +10 -0
- package/src/php/FingerprintEngine.php +4 -0
- package/src/php/Tests/FingerprintEngineTest.php +27 -1
- package/src/php/Tests/IpReputationTest.php +175 -156
- package/src/php/Utils/RequestUtils.php +274 -5
|
@@ -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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
});
|