@anonympins/fingerprint 0.6.0 → 0.6.3
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 +28 -0
- package/README.md +128 -119
- package/composer.json +2 -1
- package/package.json +1 -1
- 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.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +6802 -6411
- package/src/js/library.js +18 -11
- package/src/js/problem-manager.js +6 -2
- package/src/js/tests/cross_parity.test.js +17 -11
- package/src/js/tests/fingerprint.test.js +47 -10
- package/src/js/tests/metrics.test.js +157 -103
- package/src/php/Challenge/ChallengeUtils.php +69 -6
- package/src/php/Config/SecurityProfiles.php +3 -2
- package/src/php/FingerprintEngine.php +154 -21
- package/src/php/Optimization/OptimizationOperators.php +21 -21
- package/src/php/RequestContext.php +5 -0
- package/src/php/Tests/ChallengeUtilsTest.php +6 -4
- package/src/php/Tests/FingerprintEngineTest.php +186 -0
- package/src/php/Tests/MetricsTest.php +97 -45
- package/src/php/Tests/RequestUtilsTest.php +14 -14
- package/src/php/Tests/config/ed25519_key.json +4 -0
- package/src/php/Utils/Env.php +50 -0
- package/src/php/Utils/MetricsManager.php +209 -166
- package/src/php/Utils/RequestUtils.php +70 -14
package/src/js/library.js
CHANGED
|
@@ -1646,17 +1646,24 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1646
1646
|
high: 70 + secureRandom() * 20,
|
|
1647
1647
|
},
|
|
1648
1648
|
weights: {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1649
|
+
historyScore: secureRandom(),
|
|
1650
|
+
rotationScore: secureRandom(),
|
|
1651
|
+
headerAnomalyScore: secureRandom(),
|
|
1652
|
+
requestPatternScore: 0.5 + secureRandom(),
|
|
1653
|
+
inconsistencyScore: secureRandom(),
|
|
1654
|
+
honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
|
|
1655
|
+
behaviorScore: secureRandom(),
|
|
1656
|
+
crossLayerInconsistencyScore: secureRandom(),
|
|
1657
|
+
timeInconsistencyScore: secureRandom(),
|
|
1658
|
+
tlsSpoofingScore: secureRandom(),
|
|
1659
|
+
botScore: secureRandom(),
|
|
1660
|
+
subnetScore: secureRandom(),
|
|
1661
|
+
ipReputationScore: secureRandom(),
|
|
1662
|
+
botnetClusterScore: secureRandom(),
|
|
1663
|
+
tcpAnomalyScore: secureRandom(),
|
|
1664
|
+
quicAnomalyScore: secureRandom(),
|
|
1665
|
+
clickVarianceScore: secureRandom(),
|
|
1666
|
+
clientHintsInconsistencyScore: secureRandom()
|
|
1660
1667
|
},
|
|
1661
1668
|
patterns: {
|
|
1662
1669
|
velocityThreshold: 100 + secureRandom() * 400,
|
|
@@ -48,8 +48,12 @@ FunctionRegistry['facility.calculateEnergy'] = (facilities, payload) => {
|
|
|
48
48
|
* @param {Array<{x: number, y: number}>} path - Un tableau de points représentant le chemin.
|
|
49
49
|
* @returns {number} La distance totale du chemin.
|
|
50
50
|
*/
|
|
51
|
-
FunctionRegistry['tsp.calculateEnergy'] = (path) => {
|
|
52
|
-
|
|
51
|
+
FunctionRegistry['tsp.calculateEnergy'] = (path, payload) => {
|
|
52
|
+
const cities = payload?.cities || payload?.points || [];
|
|
53
|
+
if (path && typeof path[0] === 'number') {
|
|
54
|
+
return Optimization.Utils.evaluatePathDistance(cities, path);
|
|
55
|
+
}
|
|
56
|
+
// Fallback: Crée un tableau d'indices [0, 1, 2, ...] pour la fonction evaluatePathDistance.
|
|
53
57
|
const indices = Array.from({ length: path.length }, (_, i) => i);
|
|
54
58
|
return Optimization.Utils.evaluatePathDistance(path, indices);
|
|
55
59
|
};
|
|
@@ -140,21 +140,27 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
140
140
|
it('should verify bit-to-bit convergence across 1,000 different seeds', () => {
|
|
141
141
|
const iterations = 100;
|
|
142
142
|
for (let i = 0; i < 50; i++) {
|
|
143
|
-
const seed = `seed_parity_val_${i}`;
|
|
144
|
-
const jsTrajectory = jsGenerateTrajectory(seed, iterations);
|
|
145
|
-
const phpTrajectory = runPhpTrajectory(seed, iterations);
|
|
143
|
+
const seed = `seed_parity_val_${i}`; // Use a unique seed for each iteration
|
|
144
|
+
const jsTrajectory = jsGenerateTrajectory(seed, iterations); // Generate trajectory using JS
|
|
145
|
+
const phpTrajectory = runPhpTrajectory(seed, iterations); // Generate trajectory using PHP
|
|
146
146
|
|
|
147
147
|
for (let idx = 0; idx < 64; idx++) {
|
|
148
148
|
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
149
149
|
const phpVal = Math.fround(phpTrajectory[idx]);
|
|
150
|
-
expect(jsVal).toBe(phpVal);
|
|
150
|
+
expect(jsVal).toBe(phpVal, `PHP and JS trajectories mismatch at index ${idx} for seed "${seed}"`);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
if (wasmModule && typeof wasmModule._generate_gpu_pow_trajectory === 'function') {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
// Ensure _malloc and HEAP8 are available on the wasmModule
|
|
155
|
+
if (typeof wasmModule._malloc !== 'function' || wasmModule.HEAP8 == null) {
|
|
156
|
+
console.warn("WASM module is missing _malloc or HEAP8. Skipping WASM parity test.");
|
|
157
|
+
continue; // Skip WASM part of the test if prerequisites are not met
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const outputPtr = wasmModule._malloc(256); // Allocate memory for 64 floats (64 * 4 bytes)
|
|
161
|
+
const seedPtr = wasmModule._malloc(seed.length + 1); // Allocate memory for seed string + null terminator
|
|
162
|
+
for (let s = 0; s < seed.length; s++) { // Copy seed string to WASM memory
|
|
163
|
+
wasmModule.HEAP8[seedPtr + s] = seed.charCodeAt(s); // HEAP8 is Int8Array
|
|
158
164
|
}
|
|
159
165
|
wasmModule.HEAP8[seedPtr + seed.length] = 0;
|
|
160
166
|
|
|
@@ -163,13 +169,13 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
163
169
|
const wasmOutput = new Float32Array(wasmModule.HEAPF32.buffer, outputPtr, 64);
|
|
164
170
|
for (let idx = 0; idx < 64; idx++) {
|
|
165
171
|
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
166
|
-
const wasmVal = Math.fround(wasmOutput[idx]);
|
|
167
|
-
expect(jsVal).toBe(wasmVal);
|
|
172
|
+
const wasmVal = Math.fround(wasmOutput[idx]); // Ensure float32 precision for comparison
|
|
173
|
+
expect(jsVal).toBe(wasmVal, `WASM and JS trajectories mismatch at index ${idx} for seed "${seed}"`);
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
wasmModule._free(outputPtr);
|
|
171
177
|
wasmModule._free(seedPtr);
|
|
172
178
|
}
|
|
173
179
|
}
|
|
174
|
-
},
|
|
180
|
+
}, 30000); // Increased timeout for potentially longer WASM compilation/execution
|
|
175
181
|
});
|
|
@@ -2405,7 +2405,7 @@ describe('Subnet Scoring (Node.js)', () => {
|
|
|
2405
2405
|
deviceIds: ['d1', 'd2'], // count < 10, score += 0
|
|
2406
2406
|
});
|
|
2407
2407
|
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2408
|
-
expect(subnetScore).toBe(
|
|
2408
|
+
expect(subnetScore).toBe(100);
|
|
2409
2409
|
|
|
2410
2410
|
// 3. Many devices and high scores
|
|
2411
2411
|
const deviceIds = Array.from({ length: 20 }, (_, i) => `d${i}`);
|
|
@@ -2414,7 +2414,7 @@ describe('Subnet Scoring (Node.js)', () => {
|
|
|
2414
2414
|
deviceIds: deviceIds, // count = 20. score += (20-10)*5 = 50
|
|
2415
2415
|
});
|
|
2416
2416
|
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2417
|
-
expect(subnetScore).toBe(
|
|
2417
|
+
expect(subnetScore).toBe(100);
|
|
2418
2418
|
});
|
|
2419
2419
|
});
|
|
2420
2420
|
|
|
@@ -2714,18 +2714,18 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
|
|
|
2714
2714
|
// 2. Ajout de 2 IPs uniques (total 3)
|
|
2715
2715
|
await getBotnetClusterScore({ clientIp: '192.168.1.2' }, stableFpHash);
|
|
2716
2716
|
scoreData = await getBotnetClusterScore({ clientIp: '192.168.1.3' }, stableFpHash);
|
|
2717
|
-
expect(scoreData.botnetClusterScore).toBe(
|
|
2717
|
+
expect(scoreData.botnetClusterScore).toBe(30.2);
|
|
2718
2718
|
|
|
2719
2719
|
// 3. Ajout de 2 IPs uniques (total 5)
|
|
2720
2720
|
await getBotnetClusterScore({ clientIp: '192.168.1.4' }, stableFpHash);
|
|
2721
2721
|
scoreData = await getBotnetClusterScore({ clientIp: '192.168.1.5' }, stableFpHash);
|
|
2722
|
-
expect(scoreData.botnetClusterScore).toBe(
|
|
2722
|
+
expect(scoreData.botnetClusterScore).toBe(45.2);
|
|
2723
2723
|
|
|
2724
2724
|
// 4. Ajout de 5 IPs uniques (total 10)
|
|
2725
2725
|
for (let i = 6; i <= 10; i++) {
|
|
2726
2726
|
scoreData = await getBotnetClusterScore({ clientIp: `192.168.1.${i}` }, stableFpHash);
|
|
2727
2727
|
}
|
|
2728
|
-
expect(scoreData.botnetClusterScore).toBe(
|
|
2728
|
+
expect(scoreData.botnetClusterScore).toBe(57.4);
|
|
2729
2729
|
});
|
|
2730
2730
|
|
|
2731
2731
|
it('should realistically group PS4 consoles with volatile differences (different IPs/cookies) under the same cluster score', async () => {
|
|
@@ -2759,15 +2759,15 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
|
|
|
2759
2759
|
if (i === 1) {
|
|
2760
2760
|
expect(vector.botnetClusterScore).toBe(0);
|
|
2761
2761
|
} else if (i === 2) {
|
|
2762
|
-
expect(vector.botnetClusterScore).toBe(
|
|
2762
|
+
expect(vector.botnetClusterScore).toBe(17.7);
|
|
2763
2763
|
} else if (i === 3) {
|
|
2764
|
-
expect(vector.botnetClusterScore).toBe(
|
|
2764
|
+
expect(vector.botnetClusterScore).toBe(30.2);
|
|
2765
2765
|
} else if (i === 4) {
|
|
2766
|
-
expect(vector.botnetClusterScore).toBe(
|
|
2766
|
+
expect(vector.botnetClusterScore).toBe(39.0);
|
|
2767
2767
|
} else if (i === 5) {
|
|
2768
|
-
expect(vector.botnetClusterScore).toBe(
|
|
2768
|
+
expect(vector.botnetClusterScore).toBe(45.2);
|
|
2769
2769
|
} else if (i === 10) {
|
|
2770
|
-
expect(vector.botnetClusterScore).toBe(
|
|
2770
|
+
expect(vector.botnetClusterScore).toBe(57.4);
|
|
2771
2771
|
}
|
|
2772
2772
|
}
|
|
2773
2773
|
});
|
|
@@ -2874,4 +2874,41 @@ describe('IP Registration and Filtering', () => {
|
|
|
2874
2874
|
expect(storedData.ips.has(ip1)).toBe(true);
|
|
2875
2875
|
expect(storedData.ips.has(ip2)).toBe(true);
|
|
2876
2876
|
});
|
|
2877
|
+
|
|
2878
|
+
describe('Ed25519 Key Generation & Reopening', () => {
|
|
2879
|
+
beforeEach(() => {
|
|
2880
|
+
delete process.env.ED25519_PRIVATE_KEY;
|
|
2881
|
+
delete process.env.ED25519_PUBLIC_KEY;
|
|
2882
|
+
vi.restoreAllMocks();
|
|
2883
|
+
});
|
|
2884
|
+
|
|
2885
|
+
it('should generate new keys and save to disk if not present', async () => {
|
|
2886
|
+
const fs = await import('node:fs');
|
|
2887
|
+
const existsSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(false);
|
|
2888
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
|
2889
|
+
|
|
2890
|
+
const config = { ed25519: 'auto', weights: {}, thresholds: {} };
|
|
2891
|
+
new FingerprintEngine(config);
|
|
2892
|
+
|
|
2893
|
+
expect(process.env.ED25519_PRIVATE_KEY).toBeDefined();
|
|
2894
|
+
expect(process.env.ED25519_PUBLIC_KEY).toBeDefined();
|
|
2895
|
+
expect(writeSpy).toHaveBeenCalled();
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
it('should load keys from disk if they already exist', async () => {
|
|
2899
|
+
const fs = await import('node:fs');
|
|
2900
|
+
const dummyKeys = {
|
|
2901
|
+
privateKey: '-----BEGIN PRIVATE KEY-----\ndummy-private-key\n-----END PRIVATE KEY-----',
|
|
2902
|
+
publicKey: '-----BEGIN PUBLIC KEY-----\ndummy-public-key\n-----END PUBLIC KEY-----'
|
|
2903
|
+
};
|
|
2904
|
+
const existsSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
2905
|
+
const readSpy = vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(dummyKeys));
|
|
2906
|
+
|
|
2907
|
+
const config = { ed25519: 'auto', weights: {}, thresholds: {} };
|
|
2908
|
+
new FingerprintEngine(config);
|
|
2909
|
+
|
|
2910
|
+
expect(process.env.ED25519_PRIVATE_KEY).toBe(dummyKeys.privateKey);
|
|
2911
|
+
expect(process.env.ED25519_PUBLIC_KEY).toBe(dummyKeys.publicKey);
|
|
2912
|
+
});
|
|
2913
|
+
});
|
|
2877
2914
|
});
|
|
@@ -1,104 +1,158 @@
|
|
|
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
|
-
});
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { handleMetricsRequest, getMetric, incrementCounter, observeValue, __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
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('getMetric Unit Tests', () => {
|
|
107
|
+
it('should retrieve security weights from config', () => {
|
|
108
|
+
const config = {
|
|
109
|
+
weights: { historyScore: 0.5, rotationScore: 0.2, someInvalidWeight: 'not-a-number' }
|
|
110
|
+
};
|
|
111
|
+
const weights = getMetric('security_weight', config);
|
|
112
|
+
expect(weights.get('historyScore')).toBe(0.5);
|
|
113
|
+
expect(weights.get('rotationScore')).toBe(0.2);
|
|
114
|
+
expect(weights.has('someInvalidWeight')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should retrieve security thresholds from config', () => {
|
|
118
|
+
const config = {
|
|
119
|
+
thresholds: { low: 10, medium: 30, high: 70, block: 90 }
|
|
120
|
+
};
|
|
121
|
+
const thresholds = getMetric('security_threshold', config);
|
|
122
|
+
expect(thresholds.get('low')).toBe(10);
|
|
123
|
+
expect(thresholds.get('medium')).toBe(30);
|
|
124
|
+
expect(thresholds.get('high')).toBe(70);
|
|
125
|
+
expect(thresholds.get('block')).toBe(90);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should retrieve dynamic counters', () => {
|
|
129
|
+
incrementCounter('requests_total', { status: 'passed' });
|
|
130
|
+
incrementCounter('requests_total', { status: 'blocked' });
|
|
131
|
+
|
|
132
|
+
const metric = getMetric('requests_total');
|
|
133
|
+
expect(metric.get('status="passed"')).toBeGreaterThanOrEqual(1);
|
|
134
|
+
expect(metric.get('status="blocked"')).toBeGreaterThanOrEqual(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should retrieve dynamic observations', () => {
|
|
138
|
+
observeValue('suspicion_score', 45.5, { action: 'passed' });
|
|
139
|
+
|
|
140
|
+
const metric = getMetric('suspicion_score');
|
|
141
|
+
expect(metric.get('action="passed"')).toBe(45.5);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should retrieve autotuning false positive and negative rates', () => {
|
|
145
|
+
__internal.setLastBestSolution({
|
|
146
|
+
solution: {},
|
|
147
|
+
objectives: [0.015, 0.045]
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const fpr = getMetric('autotuning_false_positive_rate');
|
|
151
|
+
const fnr = getMetric('autotuning_false_negative_rate');
|
|
152
|
+
|
|
153
|
+
expect(fpr.get('fpr')).toBe(0.015);
|
|
154
|
+
expect(fnr.get('fnr')).toBe(0.045);
|
|
155
|
+
|
|
156
|
+
__internal.setLastBestSolution(null);
|
|
157
|
+
});
|
|
104
158
|
});
|
|
@@ -8,6 +8,7 @@ use Anonympins\Fingerprint\Store\StoreManager;
|
|
|
8
8
|
use Anonympins\Fingerprint\FingerprintBuilder;
|
|
9
9
|
use Anonympins\Fingerprint\Utils\BigInt;
|
|
10
10
|
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
11
|
+
use Anonympins\Fingerprint\Utils\Env;
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Classe utilitaire pour la génération et la vérification des challenges Proof-of-Work.
|
|
@@ -138,7 +139,7 @@ class ChallengeUtils
|
|
|
138
139
|
return $activePeers[array_rand($activePeers)];
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
public static function handleCooperativeRequest(array $params): ?array
|
|
142
|
+
public static function handleCooperativeRequest(array $params, string $clientIp = '127.0.0.1', array $config = []): ?array
|
|
142
143
|
{
|
|
143
144
|
$op = $params['coop_op'] ?? null;
|
|
144
145
|
if (!$op) {
|
|
@@ -146,6 +147,67 @@ class ChallengeUtils
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
$store = StoreManager::getStore();
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if ($op === 'share_threat_intel') {
|
|
153
|
+
$peers = $config['federatedPeers'] ?? [];
|
|
154
|
+
if (!empty($peers)) {
|
|
155
|
+
$allowedHosts = array_map(function ($url) {
|
|
156
|
+
$host = parse_url($url, PHP_URL_HOST);
|
|
157
|
+
return !empty($host) ? $host : $url;
|
|
158
|
+
}, $peers);
|
|
159
|
+
|
|
160
|
+
if (!in_array($clientIp, $allowedHosts, true)) {
|
|
161
|
+
return ['error' => 'Unauthorized federation sender IP'];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
$zkpY = $params['zkpY'] ?? '';
|
|
165
|
+
$headers = function_exists('getallheaders') ? array_change_key_case(getallheaders(), CASE_LOWER) : [];
|
|
166
|
+
$sigEd25519 = $params['signature_ed25519'] ?? $headers['x-federation-signature-ed25519'] ?? $_SERVER['HTTP_X_FEDERATION_SIGNATURE_ED25519'] ?? '';
|
|
167
|
+
$sigHmac = $params['signature'] ?? $headers['x-federation-signature'] ?? $_SERVER['HTTP_X_FEDERATION_SIGNATURE'] ?? '';
|
|
168
|
+
$timestamp = (int)($params['timestamp'] ?? $headers['x-federation-timestamp'] ?? $_SERVER['HTTP_X_FEDERATION_TIMESTAMP'] ?? 0);
|
|
169
|
+
|
|
170
|
+
if (empty($zkpY) || empty($timestamp)) {
|
|
171
|
+
return ['error' => 'Missing threat intel parameters'];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Time window check (5 minutes anti-replay)
|
|
175
|
+
$now = (int)(microtime(true) * 1000);
|
|
176
|
+
if (abs($now - $timestamp) > 300000) {
|
|
177
|
+
return ['error' => 'Message expired or clock skew too high'];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
$msg = "{$timestamp}:{$zkpY}";
|
|
181
|
+
|
|
182
|
+
if (!empty($sigEd25519)) {
|
|
183
|
+
$publicKey = $config['ed25519_public_key'] ?? $_ENV['ED25519_PUBLIC_KEY'] ?? getenv('ED25519_PUBLIC_KEY');
|
|
184
|
+
if (!$publicKey) {
|
|
185
|
+
return ['error' => 'Missing public key for asymmetric verification'];
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
$cleanKey = str_replace('\n', "\n", $publicKey);
|
|
189
|
+
$pubKeyObj = openssl_pkey_get_public($cleanKey);
|
|
190
|
+
if (!$pubKeyObj || openssl_verify($msg, hex2bin($sigEd25519), $pubKeyObj, null) !== 1) {
|
|
191
|
+
return ['error' => 'Invalid asymmetric federation signature'];
|
|
192
|
+
}
|
|
193
|
+
} catch (\Throwable $e) {
|
|
194
|
+
return ['error' => 'Asymmetric signature verification failed'];
|
|
195
|
+
}
|
|
196
|
+
} elseif (!empty($sigHmac)) {
|
|
197
|
+
$secret = $params['federationSecret'] ?? $config['federationSecret'] ?? self::getPowSecret();
|
|
198
|
+
$expectedSig = hash_hmac('sha256', $msg, $secret);
|
|
199
|
+
if (!hash_equals($expectedSig, $sigHmac)) {
|
|
200
|
+
return ['error' => 'Invalid federation signature'];
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
return ['error' => 'Missing signature'];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Ban the ZKP public key for 30 days
|
|
207
|
+
$store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
|
|
208
|
+
return ['status' => 'synchronized'];
|
|
209
|
+
}
|
|
210
|
+
|
|
149
211
|
$nodeId = $params['node_id'] ?? '';
|
|
150
212
|
if (empty($nodeId)) {
|
|
151
213
|
return ['error' => 'Missing node_id'];
|
|
@@ -334,10 +396,11 @@ class ChallengeUtils
|
|
|
334
396
|
/**
|
|
335
397
|
* Récupère la clé secrète pour les PoW depuis les variables d'environnement.
|
|
336
398
|
*/
|
|
337
|
-
|
|
399
|
+
public static function getPowSecret(): string
|
|
338
400
|
{
|
|
339
|
-
$secret =
|
|
340
|
-
|
|
401
|
+
$secret = Env::get('POW_SECRET');
|
|
402
|
+
$appEnv = Env::get('APP_ENV');
|
|
403
|
+
if (!$secret && $appEnv === 'production') {
|
|
341
404
|
throw new \RuntimeException('POW_SECRET environment variable is not set. This is required for production.');
|
|
342
405
|
}
|
|
343
406
|
return $secret ?: "fallback-dev-secret-32-chars-minimum";
|
|
@@ -350,7 +413,7 @@ class ChallengeUtils
|
|
|
350
413
|
*/
|
|
351
414
|
public static function generateStatelessTicket(array $payload): string
|
|
352
415
|
{
|
|
353
|
-
$ed25519Key =
|
|
416
|
+
$ed25519Key = Env::get('ED25519_PRIVATE_KEY');
|
|
354
417
|
if ($ed25519Key) {
|
|
355
418
|
try {
|
|
356
419
|
$serialized = json_encode($payload);
|
|
@@ -396,7 +459,7 @@ class ChallengeUtils
|
|
|
396
459
|
$payloadJson = $base64UrlDecode($parts[1]);
|
|
397
460
|
$signature = $base64UrlDecode($parts[2]);
|
|
398
461
|
|
|
399
|
-
$ed25519PubKey =
|
|
462
|
+
$ed25519PubKey = Env::get('ED25519_PUBLIC_KEY');
|
|
400
463
|
if (!$ed25519PubKey) {
|
|
401
464
|
error_log("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
|
|
402
465
|
return null;
|
|
@@ -43,7 +43,7 @@ class SecurityProfiles
|
|
|
43
43
|
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
44
44
|
'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
|
|
45
45
|
'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
|
|
46
|
-
|
|
46
|
+
'ipReputationScore' => 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
47
47
|
],
|
|
48
48
|
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
|
|
49
49
|
'patterns' => [
|
|
@@ -177,13 +177,14 @@ class SecurityProfiles
|
|
|
177
177
|
'honeypotScore' => 1.0, // Crucial for comment spam
|
|
178
178
|
'crossLayerInconsistencyScore' => 0.4,
|
|
179
179
|
'timeInconsistencyScore' => 0.8,
|
|
180
|
-
'tlsSpoofingScore' => 0.6,
|
|
180
|
+
'tlsSpoofingScore' => 0.6, // Moins critique pour les blogs
|
|
181
181
|
'botScore' => 0.8,
|
|
182
182
|
'cookieDroppingScore' => 0.7, // Moins critique, mais toujours un signal
|
|
183
183
|
'threatIntelScore' => 0.3, // Lower priority for a blog
|
|
184
184
|
'clientHintsInconsistencyScore' => 0.5,
|
|
185
185
|
'clickVarianceScore' => 0.5, // Moderate weight for click variance
|
|
186
186
|
'subnetScore' => 0.4, // Utile contre le spam de commentaires coordonné
|
|
187
|
+
'ipReputationScore' => 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
187
188
|
'botnetClusterScore' => 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
188
189
|
'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
|
|
189
190
|
'quicAnomalyScore' => 0.5, // NEW: Anomalie QUIC
|