@anonympins/fingerprint 0.3.8 → 0.4.0
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 +331 -256
- package/README.md +62 -53
- package/composer.json +38 -38
- package/index.js +4 -4
- package/package.json +103 -103
- package/phpunit.xml +20 -20
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/fingerprint.client.js +2 -0
- package/src/js/fingerprint.js +4429 -4381
- package/src/js/mongodb-store.js +79 -79
- package/src/js/pow.solver.inline.js +31 -0
- package/src/js/pow.solver.js +31 -0
- package/src/js/tests/fingerprint.builder.test.js +79 -0
- package/src/js/tests/fingerprint.client.init.test.js +120 -0
- package/src/js/tests/fingerprint.client.test.js +105 -0
- package/src/js/tests/fingerprint.engine.test.js +371 -0
- package/src/js/tests/fingerprint.isMalicious.test.js +117 -0
- package/src/js/tests/fingerprint.test.js +2319 -0
- package/src/js/tests/ip-reputation.test.js +132 -0
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -0
- package/src/js/tests/library.test.js +96 -0
- package/src/js/tests/metrics.test.js +104 -0
- package/src/js/tests/pow.solver.test.js +198 -0
- package/src/js/tests/problem-manager.test.js +323 -0
- package/src/js/tests/stores.test.js +118 -0
- package/src/php/Challenge/ChallengeUtils.php +361 -361
- package/src/php/Config/SecurityProfiles.php +271 -266
- package/src/php/FingerprintBuilder.php +185 -185
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +1006 -1006
- package/src/php/Ja3AnomalyDetector.php +227 -227
- package/src/php/Optimization/FunctionRegistry.php +62 -62
- package/src/php/Optimization/Optimization.php +255 -255
- package/src/php/Optimization/OptimizationOperators.php +304 -304
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +104 -104
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/ChallengeUtilsTest.php +81 -81
- package/src/php/Tests/FingerprintBuilderTest.php +57 -57
- package/src/php/Tests/FingerprintClientTest.php +71 -0
- package/src/php/Tests/FingerprintEngineTest.php +299 -299
- package/src/php/Tests/IpReputationTest.php +156 -156
- package/src/php/Tests/Ja3AnomalyDetectorTest.php +179 -179
- package/src/php/Tests/MetricsTest.php +45 -45
- package/src/php/Tests/PowTest.php +39 -39
- package/src/php/Tests/ProblemManagerTest.php +296 -296
- package/src/php/Tests/RequestUtilsTest.php +253 -145
- package/src/php/Tests/TLSClientHelloParserTest.php +118 -0
- package/src/php/Tests/problems.config.json +8 -8
- package/src/php/Utils/BigInt.php +144 -144
- package/src/php/Utils/Logger.php +29 -29
- package/src/php/Utils/MaliciousPatterns.php +58 -58
- package/src/php/Utils/MetricsManager.php +166 -166
- package/src/php/Utils/RequestUtils.php +1169 -1169
- package/src/php/Utils/TLSClientHelloParser.php +117 -0
- package/src/php/bin/auto-tune.php +117 -117
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import {getTlsSpoofingScore, parseJa3} from '../fingerprint.js';
|
|
3
|
+
import {vi} from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('JA3 Anomaly Detector (Node.js)', () => {
|
|
6
|
+
|
|
7
|
+
// Mock d'un store de cache asynchrone en mémoire
|
|
8
|
+
const createMockStore = () => {
|
|
9
|
+
const storage = {};
|
|
10
|
+
return {
|
|
11
|
+
get: vi.fn(async (key) => storage[key] || null),
|
|
12
|
+
set: vi.fn(async (key, val) => { storage[key] = val; })
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('parseJa3', () => {
|
|
17
|
+
it('devrait parser correctement une chaine JA3 brute', () => {
|
|
18
|
+
const rawJa3 = '771,4865-4866-4867,0-23-65281-10-11,29-23-24,0';
|
|
19
|
+
const parsed = parseJa3(rawJa3);
|
|
20
|
+
|
|
21
|
+
expect(parsed).not.toBeNull();
|
|
22
|
+
expect(parsed.tlsVersion).toBe(771);
|
|
23
|
+
expect(parsed.ciphers).toEqual([4865, 4866, 4867]);
|
|
24
|
+
expect(parsed.extensions).toEqual([0, 23, 65281, 10, 11]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('devrait retourner null pour des chaines invalides', () => {
|
|
28
|
+
expect(parseJa3('')).toBeNull();
|
|
29
|
+
expect(parseJa3('771,4865')).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('getTlsSpoofingScore (Anomalies JA3 hachées)', () => {
|
|
34
|
+
it('devrait détecter l\'usurpation d\'identité d\'une bibliothèque (Python)', async () => {
|
|
35
|
+
const pythonJa3Hash = '47344a349b75c4e82333475553b5f358';
|
|
36
|
+
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
37
|
+
|
|
38
|
+
const context = {
|
|
39
|
+
headers: {
|
|
40
|
+
'user-agent': chromeUa,
|
|
41
|
+
'x-ja3-hash': pythonJa3Hash
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = await getTlsSpoofingScore(context);
|
|
46
|
+
expect(result.tlsSpoofingScore).toBe(90);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('devrait détecter la rotation d\'User-Agent sur un même hash JA3 (Stagnation)', async () => {
|
|
50
|
+
const mockStore = createMockStore();
|
|
51
|
+
const unknownJa3 = '00000000000000000000000000000000';
|
|
52
|
+
|
|
53
|
+
const contextChrome = {
|
|
54
|
+
headers: {
|
|
55
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
|
|
56
|
+
'x-ja3-hash': unknownJa3
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const contextFirefox = {
|
|
61
|
+
headers: {
|
|
62
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/120.0',
|
|
63
|
+
'x-ja3-hash': unknownJa3
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Injecter le mockStore dans le module de test si nécessaire ou mocker l'importation du store global
|
|
68
|
+
// Premier passage : Chrome
|
|
69
|
+
const res1 = await getTlsSpoofingScore(contextChrome, mockStore);
|
|
70
|
+
|
|
71
|
+
// Deuxième passage : Firefox (Détection de la rotation de UA)
|
|
72
|
+
const res2 = await getTlsSpoofingScore(contextFirefox, mockStore);
|
|
73
|
+
expect(res2.tlsSpoofingScore).toBe(85);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('getTlsSpoofingScore (Anomalies JA3 brutes)', () => {
|
|
78
|
+
it('devrait suspecter un faux Chrome n\'utilisant pas le mécanisme GREASE', async () => {
|
|
79
|
+
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
80
|
+
// Pas de valeurs GREASE dans la suite
|
|
81
|
+
const ja3RawNoGrease = '771,4865-4866,0-23-10,29,0';
|
|
82
|
+
const ja3Hash = crypto.createHash('md5').update(ja3RawNoGrease).digest('hex');
|
|
83
|
+
|
|
84
|
+
const context = {
|
|
85
|
+
headers: {
|
|
86
|
+
'user-agent': chromeUa,
|
|
87
|
+
'x-ja3-raw': ja3RawNoGrease,
|
|
88
|
+
'x-ja3-hash': ja3Hash
|
|
89
|
+
},
|
|
90
|
+
httpVersion: '2.0'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const result = await getTlsSpoofingScore(context);
|
|
94
|
+
expect(result.tlsSpoofingScore).toBe(75);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('devrait valider un Chrome légitime utilisant des valeurs GREASE', async () => {
|
|
98
|
+
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
99
|
+
// 2570 est une suite GREASE valide
|
|
100
|
+
const ja3RawWithGrease = '771,4865-2570,0-23-10-16,29,0';
|
|
101
|
+
const ja3Hash = crypto.createHash('md5').update(ja3RawWithGrease).digest('hex');
|
|
102
|
+
|
|
103
|
+
const context = {
|
|
104
|
+
headers: {
|
|
105
|
+
'user-agent': chromeUa,
|
|
106
|
+
'x-ja3-raw': ja3RawWithGrease,
|
|
107
|
+
'x-ja3-hash': ja3Hash
|
|
108
|
+
},
|
|
109
|
+
httpVersion: '2.0'
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const result = await getTlsSpoofingScore(context);
|
|
113
|
+
expect(result.tlsSpoofingScore).toBeLessThan(70);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('devrait détecter l\'absence d\'extension ALPN pour une connexion HTTP/2', async () => {
|
|
117
|
+
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
118
|
+
// Pas d'extension 16 (ALPN)
|
|
119
|
+
const ja3RawNoAlpn = '771,4865-2570,0-23-10,29,0';
|
|
120
|
+
const ja3Hash = crypto.createHash('md5').update(ja3RawNoAlpn).digest('hex');
|
|
121
|
+
|
|
122
|
+
const context = {
|
|
123
|
+
headers: {
|
|
124
|
+
'user-agent': chromeUa,
|
|
125
|
+
'x-ja3-raw': ja3RawNoAlpn,
|
|
126
|
+
'x-ja3-hash': ja3Hash
|
|
127
|
+
},
|
|
128
|
+
httpVersion: '2.0'
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = await getTlsSpoofingScore(context);
|
|
132
|
+
expect(result.tlsSpoofingScore).toBe(70);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest';
|
|
2
|
+
import {Optimization} from '../library.js';
|
|
3
|
+
|
|
4
|
+
describe('Optimization.Operators.benfordTest', () => {
|
|
5
|
+
|
|
6
|
+
it('should return 0 for non-array inputs', () => {
|
|
7
|
+
expect(Optimization.Operators.benfordTest("12345")).toBe(0);
|
|
8
|
+
expect(Optimization.Operators.benfordTest(null)).toBe(0);
|
|
9
|
+
expect(Optimization.Operators.benfordTest(undefined)).toBe(0);
|
|
10
|
+
expect(Optimization.Operators.benfordTest({ a: 1 })).toBe(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return 0 for arrays with less than 10 valid numbers', () => {
|
|
14
|
+
const smallArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
15
|
+
expect(Optimization.Operators.benfordTest(smallArray)).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should return 0 for an empty array', () => {
|
|
19
|
+
expect(Optimization.Operators.benfordTest([])).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should ignore zeros, non-numeric strings, and leading spaces', () => {
|
|
23
|
+
// This array contains only 9 valid leading digits.
|
|
24
|
+
const dirtyArray = [0, " 123", "abc", 2, 3, 4, 5, 6, 7, 8, 9, null, undefined];
|
|
25
|
+
expect(Optimization.Operators.benfordTest(dirtyArray)).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return a very low score for a distribution that perfectly matches Benford\'s law', () => {
|
|
29
|
+
// Création d'un échantillon de 1000 nombres qui suit la loi de Benford
|
|
30
|
+
const benfordSample = [
|
|
31
|
+
...Array(301).fill(100), // 301 nombres commençant par 1
|
|
32
|
+
...Array(176).fill(200), // 176 nombres commençant par 2
|
|
33
|
+
...Array(125).fill(300), // 125 nombres commençant par 3
|
|
34
|
+
...Array(97).fill(400), // etc.
|
|
35
|
+
...Array(79).fill(500),
|
|
36
|
+
...Array(67).fill(600),
|
|
37
|
+
...Array(58).fill(700),
|
|
38
|
+
...Array(51).fill(800),
|
|
39
|
+
...Array(46).fill(900),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const score = Optimization.Operators.benfordTest(benfordSample);
|
|
43
|
+
// Le score devrait être très proche de 0. On utilise toBeLessThan pour tolérer les imprécisions de calcul.
|
|
44
|
+
expect(score).toBeLessThan(1e-9);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return a high (suspect) score for a uniform distribution', () => {
|
|
48
|
+
// Une distribution uniforme est très peu naturelle pour ce genre de données.
|
|
49
|
+
const uniformSample = [];
|
|
50
|
+
for (let i = 1; i <= 9; i++) {
|
|
51
|
+
for (let j = 0; j < 100; j++) {
|
|
52
|
+
uniformSample.push(i * 100 + j);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const score = Optimization.Operators.benfordTest(uniformSample);
|
|
57
|
+
// Un score > 0.15 est considéré comme suspect.
|
|
58
|
+
expect(score).toBeGreaterThan(0.15);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return a high (suspect) score for a distribution skewed towards high digits', () => {
|
|
62
|
+
// L'inverse de la loi de Benford, très suspect.
|
|
63
|
+
const inverseBenfordSample = [
|
|
64
|
+
...Array(46).fill(100),
|
|
65
|
+
...Array(51).fill(200),
|
|
66
|
+
...Array(58).fill(300),
|
|
67
|
+
...Array(67).fill(400),
|
|
68
|
+
...Array(79).fill(500),
|
|
69
|
+
...Array(97).fill(600),
|
|
70
|
+
...Array(125).fill(700),
|
|
71
|
+
...Array(176).fill(800),
|
|
72
|
+
...Array(301).fill(900),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const score = Optimization.Operators.benfordTest(inverseBenfordSample);
|
|
76
|
+
expect(score).toBeGreaterThan(0.3); // Score attendu encore plus élevé
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle real-world-like data (request timings)', () => {
|
|
80
|
+
// Simule des délais de requêtes générés par un bot (ex: aléatoire uniforme entre 500 et 1500ms)
|
|
81
|
+
const botTimings = Array.from({ length: 100 }, () => 500 + Math.random() * 1000);
|
|
82
|
+
const botScore = Optimization.Operators.benfordTest(botTimings);
|
|
83
|
+
|
|
84
|
+
// Simule des délais humains (plus de petits délais, quelques longs délais)
|
|
85
|
+
const humanTimings = [
|
|
86
|
+
123, 234, 180, 345, 150, 456, 110, 190, 210, 280, 567, 130, 890, 1200, 310, 160
|
|
87
|
+
];
|
|
88
|
+
const humanScore = Optimization.Operators.benfordTest(humanTimings);
|
|
89
|
+
|
|
90
|
+
// Le score du bot devrait être significativement plus élevé que celui de l'humain.
|
|
91
|
+
// Les valeurs exactes peuvent varier, mais la tendance doit être claire.
|
|
92
|
+
expect(botScore).toBeGreaterThan(0.1);
|
|
93
|
+
expect(humanScore).toBeLessThan(botScore);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { handleMetricsRequest, __internal } from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
describe('Metrics & Authorization Callback Integration', () => {
|
|
5
|
+
let req, res;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
req = {
|
|
9
|
+
ip: '127.0.0.1',
|
|
10
|
+
path: '/metrics',
|
|
11
|
+
headers: { 'user-agent': 'prometheus' },
|
|
12
|
+
query: {},
|
|
13
|
+
body: {},
|
|
14
|
+
cookies: {},
|
|
15
|
+
httpVersion: '1.1'
|
|
16
|
+
};
|
|
17
|
+
res = {
|
|
18
|
+
status: vi.fn().mockReturnThis(),
|
|
19
|
+
send: vi.fn().mockReturnThis(),
|
|
20
|
+
set: vi.fn().mockReturnThis(),
|
|
21
|
+
redirect: vi.fn().mockReturnThis()
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
it('should return metrics with 200 Content-Type if authorized', async () => {
|
|
25
|
+
const config = {
|
|
26
|
+
metricsAuthorizationCallback: () => true
|
|
27
|
+
};
|
|
28
|
+
await handleMetricsRequest(req, res, config);
|
|
29
|
+
|
|
30
|
+
expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
|
|
31
|
+
expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_requests_total'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should include current weights and thresholds in prometheus metrics output', async () => {
|
|
35
|
+
const config = {
|
|
36
|
+
metricsAuthorizationCallback: () => true,
|
|
37
|
+
weights: { historyScore: 0.35, rotationScore: 0.65 },
|
|
38
|
+
thresholds: { low: 22, block: 92 }
|
|
39
|
+
};
|
|
40
|
+
await handleMetricsRequest(req, res, config);
|
|
41
|
+
|
|
42
|
+
expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
|
|
43
|
+
expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_weight{indicator="historyScore"} 0.35'));
|
|
44
|
+
expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_weight{indicator="rotationScore"} 0.65'));
|
|
45
|
+
expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_threshold{level="low"} 22'));
|
|
46
|
+
expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_threshold{level="block"} 92'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should include auto-tuning performance metrics if a best solution is available', async () => {
|
|
50
|
+
__internal.setLastBestSolution({
|
|
51
|
+
solution: {},
|
|
52
|
+
objectives: [0.0123, 0.0456]
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const config = {
|
|
56
|
+
metricsAuthorizationCallback: () => true
|
|
57
|
+
};
|
|
58
|
+
await handleMetricsRequest(req, res, config);
|
|
59
|
+
|
|
60
|
+
expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
|
|
61
|
+
expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_autotuning_false_positive_rate 0.0123'));
|
|
62
|
+
expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_autotuning_false_negative_rate 0.0456'));
|
|
63
|
+
|
|
64
|
+
// Cleanup
|
|
65
|
+
__internal.setLastBestSolution(null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return 403 if authorization callback returns false', async () => {
|
|
69
|
+
const config = {
|
|
70
|
+
metricsAuthorizationCallback: () => false
|
|
71
|
+
};
|
|
72
|
+
await handleMetricsRequest(req, res, config);
|
|
73
|
+
|
|
74
|
+
expect(res.status).toHaveBeenCalledWith(403);
|
|
75
|
+
expect(res.send).toHaveBeenCalledWith('Access to metrics denied.');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle custom block action returned by authorization callback', async () => {
|
|
79
|
+
const config = {
|
|
80
|
+
metricsAuthorizationCallback: () => ({
|
|
81
|
+
action: 'block',
|
|
82
|
+
status: 401,
|
|
83
|
+
body: 'Custom unauthorized message'
|
|
84
|
+
})
|
|
85
|
+
};
|
|
86
|
+
await handleMetricsRequest(req, res, config);
|
|
87
|
+
|
|
88
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
89
|
+
expect(res.send).toHaveBeenCalledWith('Custom unauthorized message');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle custom redirect action returned by authorization callback', async () => {
|
|
93
|
+
const config = {
|
|
94
|
+
metricsAuthorizationCallback: () => ({
|
|
95
|
+
action: 'redirect',
|
|
96
|
+
status: 302,
|
|
97
|
+
path: '/forbidden'
|
|
98
|
+
})
|
|
99
|
+
};
|
|
100
|
+
await handleMetricsRequest(req, res, config);
|
|
101
|
+
|
|
102
|
+
expect(res.redirect).toHaveBeenCalledWith(302, '/forbidden');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
// Import the functions to be tested.
|
|
4
|
+
// Since the inline file doesn't use exports, we can't directly import.
|
|
5
|
+
// Instead, we'll test the ES module version which shares the same logic.
|
|
6
|
+
import {solveChallenge, solveCpuTargetInline, solveMemory, solveTsp} from '../pow.solver.js';
|
|
7
|
+
|
|
8
|
+
describe('Proof-of-Work Solvers', () => {
|
|
9
|
+
|
|
10
|
+
describe('solveCpuTargetInline', () => {
|
|
11
|
+
const nonce = 'test-nonce';
|
|
12
|
+
// A relatively easy target for quick tests (first 16 bits must be zero)
|
|
13
|
+
const targetHex = '0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
|
|
14
|
+
const targetBigInt = BigInt('0x' + targetHex);
|
|
15
|
+
|
|
16
|
+
it('should find a valid CPU solution with a simple base block', async () => {
|
|
17
|
+
// Le `baseBlock` est maintenant un Uint8Array.
|
|
18
|
+
// Le client et le serveur doivent s'accorder sur sa construction.
|
|
19
|
+
// Ici, on simule un challenge simple avec juste le nonce.
|
|
20
|
+
const baseBlock = new TextEncoder().encode(`${nonce}:`);
|
|
21
|
+
const solution = await solveCpuTargetInline(baseBlock, targetHex);
|
|
22
|
+
|
|
23
|
+
// Verification: re-hash the solution and check against the target
|
|
24
|
+
const finalBlock = new Uint8Array([...baseBlock, ...new TextEncoder().encode(String(solution))]);
|
|
25
|
+
const hash = createHash('sha256').update(finalBlock).digest('hex');
|
|
26
|
+
const hashBigInt = BigInt('0x' + hash);
|
|
27
|
+
|
|
28
|
+
expect(hashBigInt).toBeLessThan(targetBigInt);
|
|
29
|
+
}, 20000);
|
|
30
|
+
|
|
31
|
+
it('should find a valid CPU solution with a client secret', async () => {
|
|
32
|
+
const clientSecret = 'my-secret';
|
|
33
|
+
const fingerprint = 'test-fp-string';
|
|
34
|
+
// Le `baseBlock` est maintenant un Uint8Array qui inclut toutes les informations.
|
|
35
|
+
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${fingerprint}:`);
|
|
36
|
+
const solution = await solveCpuTargetInline(baseBlock, targetHex);
|
|
37
|
+
|
|
38
|
+
// Verification: re-hash the solution and check against the target
|
|
39
|
+
const finalBlock = new Uint8Array([...baseBlock, ...new TextEncoder().encode(String(solution))]);
|
|
40
|
+
const hash = createHash('sha256').update(finalBlock).digest('hex');
|
|
41
|
+
const hashBigInt = BigInt('0x' + hash);
|
|
42
|
+
expect(hashBigInt).toBeLessThan(targetBigInt);
|
|
43
|
+
}, 20000);
|
|
44
|
+
|
|
45
|
+
it('should call the progress callback', async () => {
|
|
46
|
+
const progressCallback = vi.fn();
|
|
47
|
+
const baseBlock = new TextEncoder().encode(`${nonce}:`);
|
|
48
|
+
// Use a much harder target to ensure the loop runs long enough
|
|
49
|
+
const hardTarget = '0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
|
|
50
|
+
await solveCpuTargetInline(baseBlock, hardTarget, progressCallback);
|
|
51
|
+
|
|
52
|
+
// Check if the callback was called with a number
|
|
53
|
+
expect(progressCallback).toHaveBeenCalled();
|
|
54
|
+
expect(progressCallback).toHaveBeenCalledWith(expect.any(Number));
|
|
55
|
+
}, 40000); // Increase timeout for harder challenge
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('solveMemory', () => {
|
|
59
|
+
it('should produce a deterministic result for a given seed and difficulty', async () => {
|
|
60
|
+
const seed = 'test-seed';
|
|
61
|
+
const difficulty = 1; // 1MB
|
|
62
|
+
|
|
63
|
+
const solution1 = await solveMemory(seed, difficulty);
|
|
64
|
+
const solution2 = await solveMemory(seed, difficulty);
|
|
65
|
+
|
|
66
|
+
expect(solution1).toBe(solution2);
|
|
67
|
+
expect(solution1).not.toBe(await solveMemory('different-seed', difficulty));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('solveTsp', () => {
|
|
72
|
+
it('should solve a simple TSP problem', async () => {
|
|
73
|
+
const cities = [{ x: 10, y: 10 }, { x: 90, y: 90 }, { x: 10, y: 90 }, { x: 90, y: 10 }];
|
|
74
|
+
const result = await solveTsp(cities, 400);
|
|
75
|
+
|
|
76
|
+
expect(result.path).toBeInstanceOf(Array);
|
|
77
|
+
expect(result.path.length).toBe(4);
|
|
78
|
+
// The optimal path for a square is ~324. The nearest neighbor should find this.
|
|
79
|
+
expect(result.distance).toBeLessThan(330);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('solveChallenge', () => {
|
|
84
|
+
const nonce = 'challenge-nonce';
|
|
85
|
+
const clientSecret = 'challenge-secret';
|
|
86
|
+
const cpuTarget = '0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
|
|
87
|
+
|
|
88
|
+
it('should solve a "cpu_target" challenge', async () => {
|
|
89
|
+
// The 'cpu_target' case in solveChallenge uses Web Workers, which are not available in Node.js test env.
|
|
90
|
+
// We will test the 'cpu_mem' case which uses the same underlying `solveCpuTargetInline` function.
|
|
91
|
+
// This test is effectively covered by the 'cpu_mem' test below.
|
|
92
|
+
expect(true).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should solve a "cpu_mem" challenge for an API client', async () => {
|
|
96
|
+
const challenge = {
|
|
97
|
+
type: 'cpu_mem',
|
|
98
|
+
nonce,
|
|
99
|
+
clientSecret,
|
|
100
|
+
cpuTarget,
|
|
101
|
+
memDifficulty: 1,
|
|
102
|
+
// Pour les appels API, le serveur pré-calcule le baseBlock
|
|
103
|
+
// sans l'IP du client, mais avec l'empreinte digitale.
|
|
104
|
+
baseBlock: [...new TextEncoder().encode(`${nonce}:${clientSecret}:`)]
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const solutions = await solveChallenge(challenge, ''); // Le fingerprint est passé en 2e arg
|
|
108
|
+
expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
|
|
109
|
+
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Number));
|
|
110
|
+
}, 20000);
|
|
111
|
+
|
|
112
|
+
it('should solve a "cpu_mem_inline" challenge for a browser', async () => {
|
|
113
|
+
const challenge = {
|
|
114
|
+
type: 'cpu_mem_inline',
|
|
115
|
+
nonce,
|
|
116
|
+
clientSecret,
|
|
117
|
+
cpuTarget,
|
|
118
|
+
memDifficulty: 1,
|
|
119
|
+
// Pour le challenge inline, le serveur inclut l'IP dans le baseBlock.
|
|
120
|
+
baseBlock: [...new TextEncoder().encode(`${nonce}:${clientSecret}:`)]
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const solutions = await solveChallenge(challenge, '');
|
|
124
|
+
expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
|
|
125
|
+
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Number));
|
|
126
|
+
}, 20000);
|
|
127
|
+
|
|
128
|
+
it('should solve a "tsp" challenge', async () => {
|
|
129
|
+
const challenge = {
|
|
130
|
+
type: 'tsp',
|
|
131
|
+
cities: [{ x: 0, y: 0 }, { x: 100, y: 100 }],
|
|
132
|
+
targetMaxDistance: 300
|
|
133
|
+
};
|
|
134
|
+
const solutions = await solveChallenge(challenge);
|
|
135
|
+
// Pour le TSP, la solution brute est directement le chemin
|
|
136
|
+
expect(solutions.rawSolution).toEqual([0, 1]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should throw an error for an unknown challenge type', async () => {
|
|
140
|
+
const challenge = { type: 'unknown' };
|
|
141
|
+
await expect(solveChallenge(challenge)).rejects.toThrow('Unknown challenge type: unknown');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('WASM Solver Path Integration (Mocked)', () => {
|
|
146
|
+
let mockWasmModule;
|
|
147
|
+
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
mockWasmModule = {
|
|
150
|
+
_malloc: vi.fn().mockImplementation(() => 1234), // Simule un pointeur mémoire
|
|
151
|
+
_free: vi.fn(),
|
|
152
|
+
HEAPU8: new Uint8Array(10000),
|
|
153
|
+
HEAP8: new Int8Array(10000),
|
|
154
|
+
_solve_cpu_target: vi.fn().mockReturnValue(1337),
|
|
155
|
+
_solve_memory_challenge: vi.fn().mockReturnValue(777)
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Injecter le module WASM globalement pour simuler le comportement du navigateur
|
|
159
|
+
global.window = {
|
|
160
|
+
wasmModule: mockWasmModule
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
// Nettoyer l'environnement global après chaque test
|
|
166
|
+
delete global.window;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should redirect solveCpuTargetInline to WASM and manage heap memory safely', async () => {
|
|
170
|
+
const baseBlock = new Uint8Array([1, 2, 3, 4]);
|
|
171
|
+
const target = '0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
|
|
172
|
+
|
|
173
|
+
const solution = await solveCpuTargetInline(baseBlock, target);
|
|
174
|
+
|
|
175
|
+
// 1. Vérification du résultat natif retourné par le mock
|
|
176
|
+
expect(solution).toBe(1337);
|
|
177
|
+
|
|
178
|
+
// 2. Vérification des allocations et libérations mémoire pour le bloc de base et la cible hexadécimale
|
|
179
|
+
expect(mockWasmModule._malloc).toHaveBeenCalledTimes(2);
|
|
180
|
+
expect(mockWasmModule._free).toHaveBeenCalledTimes(2);
|
|
181
|
+
|
|
182
|
+
// 3. Vérification de l'appel à la fonction C++ exportée
|
|
183
|
+
expect(mockWasmModule._solve_cpu_target).toHaveBeenCalledWith(1234, baseBlock.length, 1234);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should redirect solveMemory to WASM and manage heap memory safely', async () => {
|
|
187
|
+
const seed = 'test-wasm-seed';
|
|
188
|
+
const difficulty = 4; // 4 MB
|
|
189
|
+
|
|
190
|
+
const solution = await solveMemory(seed, difficulty);
|
|
191
|
+
|
|
192
|
+
expect(solution).toBe(777);
|
|
193
|
+
expect(mockWasmModule._malloc).toHaveBeenCalledTimes(1);
|
|
194
|
+
expect(mockWasmModule._free).toHaveBeenCalledTimes(1);
|
|
195
|
+
expect(mockWasmModule._solve_memory_challenge).toHaveBeenCalledWith(1234, difficulty);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|