@anonympins/fingerprint 0.6.1 → 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 +14 -0
- package/README.md +128 -119
- package/composer.json +2 -1
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +6802 -6601
- package/src/js/library.js +18 -11
- package/src/js/problem-manager.js +6 -2
- package/src/js/tests/fingerprint.test.js +47 -10
- package/src/js/tests/metrics.test.js +157 -103
- package/src/php/Challenge/ChallengeUtils.php +6 -4
- package/src/php/Config/SecurityProfiles.php +3 -2
- package/src/php/FingerprintEngine.php +56 -19
- package/src/php/Optimization/OptimizationOperators.php +21 -21
- package/src/php/RequestContext.php +3 -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 +63 -13
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
|
};
|
|
@@ -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.
|
|
@@ -397,8 +398,9 @@ class ChallengeUtils
|
|
|
397
398
|
*/
|
|
398
399
|
public static function getPowSecret(): string
|
|
399
400
|
{
|
|
400
|
-
$secret =
|
|
401
|
-
|
|
401
|
+
$secret = Env::get('POW_SECRET');
|
|
402
|
+
$appEnv = Env::get('APP_ENV');
|
|
403
|
+
if (!$secret && $appEnv === 'production') {
|
|
402
404
|
throw new \RuntimeException('POW_SECRET environment variable is not set. This is required for production.');
|
|
403
405
|
}
|
|
404
406
|
return $secret ?: "fallback-dev-secret-32-chars-minimum";
|
|
@@ -411,7 +413,7 @@ class ChallengeUtils
|
|
|
411
413
|
*/
|
|
412
414
|
public static function generateStatelessTicket(array $payload): string
|
|
413
415
|
{
|
|
414
|
-
$ed25519Key =
|
|
416
|
+
$ed25519Key = Env::get('ED25519_PRIVATE_KEY');
|
|
415
417
|
if ($ed25519Key) {
|
|
416
418
|
try {
|
|
417
419
|
$serialized = json_encode($payload);
|
|
@@ -457,7 +459,7 @@ class ChallengeUtils
|
|
|
457
459
|
$payloadJson = $base64UrlDecode($parts[1]);
|
|
458
460
|
$signature = $base64UrlDecode($parts[2]);
|
|
459
461
|
|
|
460
|
-
$ed25519PubKey =
|
|
462
|
+
$ed25519PubKey = Env::get('ED25519_PUBLIC_KEY');
|
|
461
463
|
if (!$ed25519PubKey) {
|
|
462
464
|
error_log("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
|
|
463
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
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
use Anonympins\Fingerprint\Utils\Logger;
|
|
12
12
|
use Anonympins\Fingerprint\Utils\MetricsManager;
|
|
13
13
|
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
14
|
+
use Anonympins\Fingerprint\Utils\Env;
|
|
14
15
|
|
|
15
16
|
// Correction de l'import
|
|
16
17
|
|
|
@@ -164,14 +165,56 @@
|
|
|
164
165
|
|
|
165
166
|
public function __construct(array $securityConfig)
|
|
166
167
|
{
|
|
167
|
-
$this->isProduction = (
|
|
168
|
+
$this->isProduction = Env::get('APP_ENV') === 'production';
|
|
168
169
|
|
|
169
170
|
// Dynamically bind Ed25519 keys if passed via config
|
|
170
171
|
if (isset($securityConfig['ed25519_private_key'])) {
|
|
171
|
-
|
|
172
|
+
Env::set('ED25519_PRIVATE_KEY', $securityConfig['ed25519_private_key']);
|
|
172
173
|
}
|
|
173
174
|
if (isset($securityConfig['ed25519_public_key'])) {
|
|
174
|
-
|
|
175
|
+
Env::set('ED25519_PUBLIC_KEY', $securityConfig['ed25519_public_key']);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Génération et persistance automatique de la paire de clés Ed25519
|
|
179
|
+
$useAsymmetric = ($securityConfig['useAsymmetricTickets'] ?? false) === true || ($securityConfig['ed25519'] ?? '') === 'auto';
|
|
180
|
+
$envPrivate = Env::get('ED25519_PRIVATE_KEY');
|
|
181
|
+
if ($useAsymmetric && ($envPrivate === null || $envPrivate === '')) {
|
|
182
|
+
$configDir = dirname(__DIR__, 1) . '/config';
|
|
183
|
+
$persistentKeyPath = $configDir . '/ed25519_key.json';
|
|
184
|
+
|
|
185
|
+
if (file_exists($persistentKeyPath)) {
|
|
186
|
+
try {
|
|
187
|
+
$keys = json_decode(file_get_contents($persistentKeyPath), true);
|
|
188
|
+
if (isset($keys['privateKey'], $keys['publicKey'])) {
|
|
189
|
+
Env::set('ED25519_PRIVATE_KEY', $keys['privateKey']);
|
|
190
|
+
Env::set('ED25519_PUBLIC_KEY', $keys['publicKey']);
|
|
191
|
+
}
|
|
192
|
+
} catch (\Throwable $e) {
|
|
193
|
+
error_log('[Fingerprint] Failed to load persistent Ed25519 keys: ' . $e->getMessage());
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
try {
|
|
197
|
+
if (defined('OPENSSL_KEYTYPE_ED25519')) {
|
|
198
|
+
$pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
|
|
199
|
+
if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
|
|
200
|
+
$details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
|
|
201
|
+
$publicKeyPem = $details['key']; // @phpstan-ignore-line
|
|
202
|
+
Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
|
|
203
|
+
Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
|
|
204
|
+
// Also update $_ENV and $_SERVER for consistency
|
|
205
|
+
if (!is_dir($configDir)) {
|
|
206
|
+
mkdir($configDir, 0777, true);
|
|
207
|
+
}
|
|
208
|
+
file_put_contents($persistentKeyPath, json_encode([
|
|
209
|
+
'privateKey' => $privateKeyPem,
|
|
210
|
+
'publicKey' => $publicKeyPem
|
|
211
|
+
], JSON_PRETTY_PRINT));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (\Throwable $e) {
|
|
215
|
+
error_log('[Fingerprint] Native Ed25519 key generation failed: ' . $e->getMessage());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
175
218
|
}
|
|
176
219
|
|
|
177
220
|
// Auto-load optimized config if autotuning savePath is specified
|
|
@@ -222,7 +265,8 @@
|
|
|
222
265
|
'honeypot', 'threatIntel', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger', 'probationaryTtl',
|
|
223
266
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
224
267
|
'similarityThreshold', 'summary', 'description',
|
|
225
|
-
'ed25519_private_key', 'ed25519_public_key'
|
|
268
|
+
'ed25519_private_key', 'ed25519_public_key',
|
|
269
|
+
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret'
|
|
226
270
|
];
|
|
227
271
|
|
|
228
272
|
if (empty($config['weights'])) {
|
|
@@ -483,6 +527,9 @@
|
|
|
483
527
|
*/
|
|
484
528
|
private function resolveRequestIdentity(RequestContext $context, array &$suspicionVector): array
|
|
485
529
|
{
|
|
530
|
+
if ($context->resolvedIdentity !== null) {
|
|
531
|
+
return $context->resolvedIdentity;
|
|
532
|
+
}
|
|
486
533
|
$this->log('Resolving request identity', ['clientIp' => $context->clientIp, 'cookies' => $context->cookies]);
|
|
487
534
|
$store = StoreManager::getStore();
|
|
488
535
|
$existingDeviceId = $context->cookies['device_id'] ?? null; // @phpstan-ignore-line
|
|
@@ -564,11 +611,12 @@
|
|
|
564
611
|
$store->set("tls-session:{$tlsSessionId}", $deviceId, 3600); // 1h cache duration
|
|
565
612
|
}
|
|
566
613
|
|
|
567
|
-
|
|
614
|
+
$context->resolvedIdentity = [
|
|
568
615
|
'deviceId' => $deviceId,
|
|
569
616
|
'deviceData' => $deviceData,
|
|
570
617
|
'newCookie' => $newCookie,
|
|
571
618
|
];
|
|
619
|
+
return $context->resolvedIdentity;
|
|
572
620
|
}
|
|
573
621
|
|
|
574
622
|
/**
|
|
@@ -891,22 +939,11 @@
|
|
|
891
939
|
$context->graphqlOperation = $gqlInfo;
|
|
892
940
|
}
|
|
893
941
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
$this->log('Processing request', ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
897
|
-
|
|
898
|
-
// Parse GraphQL query if applicable
|
|
899
|
-
if ($context->path === '/graphql' && !empty($context->body)) {
|
|
900
|
-
$gqlInfo = RequestUtils::parseGraphQLQuery(is_array($context->body) ? $context->body : []);
|
|
901
|
-
if ($gqlInfo) {
|
|
902
|
-
$context->graphqlOperation = $gqlInfo;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
942
|
|
|
906
943
|
// 1. Vérifier les listes blanches
|
|
907
944
|
if ($this->checkAllowlists($context)) {
|
|
945
|
+
MetricsManager::incrementCounter('requests_total', ['status' => 'whitelisted']);
|
|
908
946
|
return ['action' => 'next', 'score' => 0.0, 'vector' => ['whitelisted' => 100.0]];
|
|
909
|
-
MetricsManager::incrementCounter('requests_total', ['status' => 'whitelisted']);
|
|
910
947
|
}
|
|
911
948
|
|
|
912
949
|
// Initialiser le vecteur de suspicion
|
|
@@ -1234,7 +1271,7 @@
|
|
|
1234
1271
|
|
|
1235
1272
|
if ($shouldUseUsefulWork) {
|
|
1236
1273
|
$this->log('Issuing a useful work challenge', ['finalScore' => $finalScore]);
|
|
1237
|
-
|
|
1274
|
+
$defaultPath = dirname(__DIR__, 1) . '/config/problems.config.json';
|
|
1238
1275
|
$configPath = $this->securityConfig['usefulWorkConfigPath'] ?? (file_exists($defaultPath) ? $defaultPath : null);
|
|
1239
1276
|
$problemManager = ProblemManager::getInstance($configPath, $store);
|
|
1240
1277
|
$work = $problemManager->dispatchWork($finalScore);
|
|
@@ -1437,7 +1474,7 @@
|
|
|
1437
1474
|
$signature = '';
|
|
1438
1475
|
$isAsymmetric = false;
|
|
1439
1476
|
|
|
1440
|
-
$privateKey =
|
|
1477
|
+
$privateKey = Env::get('ED25519_PRIVATE_KEY');
|
|
1441
1478
|
if ($privateKey) {
|
|
1442
1479
|
try {
|
|
1443
1480
|
$cleanKey = str_replace('\n', "\n", $privateKey);
|