@anonympins/fingerprint 0.7.0 → 0.7.2
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 +29 -0
- package/README.md +1 -1
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/build-client.js +8 -10
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +1531 -1463
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/src/js/fingerprint.js +190 -16
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +340 -376
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +154 -56
- package/src/js/pow.solver.js +180 -92
- package/src/js/pow.worker.js +48 -48
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +124 -10
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +57 -11
- package/src/php/Config/SecurityProfiles.php +20 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +147 -148
- package/src/php/FingerprintEngine.php +44 -17
- package/src/php/Ja3AnomalyDetector.php +33 -32
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +26 -25
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +38 -0
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/BlockList.php +99 -99
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +129 -7
- package/src/php/Utils/TLSClientHelloParser.php +369 -369
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +554 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
- package/src/php/WordPress/package.php +218 -0
- package/src/php/bin/auto-tune.php +118 -118
package/src/js/redis-store.js
CHANGED
|
@@ -35,7 +35,7 @@ export function createRedisStore(redisClient) {
|
|
|
35
35
|
reconnecting = false;
|
|
36
36
|
clearInterval(interval);
|
|
37
37
|
} catch (err) {
|
|
38
|
-
//
|
|
38
|
+
// Keep retrying connection every 5 seconds
|
|
39
39
|
}
|
|
40
40
|
}, 5000);
|
|
41
41
|
if (interval.unref) interval.unref();
|
|
@@ -1,144 +1,147 @@
|
|
|
1
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
-
import {JSDOM} from 'jsdom';
|
|
3
|
-
import ClientLibrary from '../fingerprint.client.js';
|
|
4
|
-
|
|
5
|
-
// --- Setup JSDOM Environment ---
|
|
6
|
-
// Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
|
|
7
|
-
// ici rend le test explicite et portable.
|
|
8
|
-
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
9
|
-
url: 'http://localhost',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// In recent Node.js versions, some global properties like 'navigator', 'performance',
|
|
13
|
-
// and 'screen' are read-only. To ensure our JSDOM environment works correctly across
|
|
14
|
-
// all versions, we use Object.defineProperty to make these properties writable.
|
|
15
|
-
Object.defineProperty(global, 'window', { value: dom.window, writable: true });
|
|
16
|
-
Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
|
|
17
|
-
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
|
-
Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
|
|
19
|
-
Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
|
|
20
|
-
|
|
21
|
-
global.fetch = vi.fn(); // Mock global fetch
|
|
22
|
-
global.Headers = dom.window.Headers;
|
|
23
|
-
global.Request = dom.window.Request;
|
|
24
|
-
global.URL = dom.window.URL;
|
|
25
|
-
global.TextEncoder = dom.window.TextEncoder;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
describe('ClientLibrary.initializeClient', () => {
|
|
29
|
-
|
|
30
|
-
// On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
|
|
31
|
-
let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
// Réinitialiser l'état du client avant chaque test
|
|
35
|
-
ClientLibrary._resetCache();
|
|
36
|
-
|
|
37
|
-
// Créer les espions sur les méthodes internes
|
|
38
|
-
startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
|
|
39
|
-
startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
|
|
40
|
-
startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
|
|
41
|
-
startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(
|
|
60
|
-
expect(
|
|
61
|
-
expect(
|
|
62
|
-
expect(
|
|
63
|
-
expect(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
expect(
|
|
72
|
-
expect(
|
|
73
|
-
expect(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
+
import {JSDOM} from 'jsdom';
|
|
3
|
+
import ClientLibrary from '../fingerprint.client.js';
|
|
4
|
+
|
|
5
|
+
// --- Setup JSDOM Environment ---
|
|
6
|
+
// Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
|
|
7
|
+
// ici rend le test explicite et portable.
|
|
8
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
9
|
+
url: 'http://localhost',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// In recent Node.js versions, some global properties like 'navigator', 'performance',
|
|
13
|
+
// and 'screen' are read-only. To ensure our JSDOM environment works correctly across
|
|
14
|
+
// all versions, we use Object.defineProperty to make these properties writable.
|
|
15
|
+
Object.defineProperty(global, 'window', { value: dom.window, writable: true });
|
|
16
|
+
Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
|
|
17
|
+
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
|
+
Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
|
|
19
|
+
Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
|
|
20
|
+
|
|
21
|
+
global.fetch = vi.fn(); // Mock global fetch
|
|
22
|
+
global.Headers = dom.window.Headers;
|
|
23
|
+
global.Request = dom.window.Request;
|
|
24
|
+
global.URL = dom.window.URL;
|
|
25
|
+
global.TextEncoder = dom.window.TextEncoder;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
describe('ClientLibrary.initializeClient', () => {
|
|
29
|
+
|
|
30
|
+
// On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
|
|
31
|
+
let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startMotionSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// Réinitialiser l'état du client avant chaque test
|
|
35
|
+
ClientLibrary._resetCache();
|
|
36
|
+
|
|
37
|
+
// Créer les espions sur les méthodes internes
|
|
38
|
+
startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
|
|
39
|
+
startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
|
|
40
|
+
startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
|
|
41
|
+
startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
|
|
42
|
+
startMotionSpy = vi.spyOn(ClientLibrary, 'startMotionTracker');
|
|
43
|
+
startRenderingSpy = vi.spyOn(ClientLibrary, 'startRenderingTracker');
|
|
44
|
+
initWasmSpy = vi.spyOn(ClientLibrary, 'initializeWasm').mockResolvedValue(undefined);
|
|
45
|
+
initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
|
|
46
|
+
injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
|
|
47
|
+
initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
|
|
48
|
+
injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
// Restaurer les espions après chaque test pour ne pas affecter les autres tests
|
|
53
|
+
vi.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should enable all trackers by default', () => {
|
|
57
|
+
ClientLibrary.initializeClient();
|
|
58
|
+
|
|
59
|
+
expect(startMouseSpy).toHaveBeenCalled();
|
|
60
|
+
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
61
|
+
expect(startClickSpy).toHaveBeenCalled();
|
|
62
|
+
expect(startTouchSpy).toHaveBeenCalled();
|
|
63
|
+
expect(startMotionSpy).toHaveBeenCalled();
|
|
64
|
+
expect(startRenderingSpy).toHaveBeenCalled();
|
|
65
|
+
expect(injectPhantomTrapsSpy).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should disable trackers when configured', () => {
|
|
69
|
+
ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false, motion: false, rendering: false });
|
|
70
|
+
|
|
71
|
+
expect(startMouseSpy).not.toHaveBeenCalled();
|
|
72
|
+
expect(startKeystrokeSpy).not.toHaveBeenCalled();
|
|
73
|
+
expect(startClickSpy).not.toHaveBeenCalled();
|
|
74
|
+
expect(startTouchSpy).not.toHaveBeenCalled();
|
|
75
|
+
expect(startMotionSpy).not.toHaveBeenCalled();
|
|
76
|
+
expect(startRenderingSpy).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should initialize WASM if wasmPath is configured', () => {
|
|
80
|
+
ClientLibrary.initializeClient({ wasmPath: '/fp.js' });
|
|
81
|
+
|
|
82
|
+
expect(initWasmSpy).toHaveBeenCalledWith('/fp.js');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should disable phantom traps when configured', () => {
|
|
86
|
+
ClientLibrary.initializeClient({ phantomTraps: false });
|
|
87
|
+
|
|
88
|
+
expect(injectPhantomTrapsSpy).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should initialize honeypots with the provided field names', () => {
|
|
92
|
+
const honeypotFields = ['email_confirm', 'user_nickname'];
|
|
93
|
+
ClientLibrary.initializeClient({ honeypots: honeypotFields });
|
|
94
|
+
|
|
95
|
+
expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should not initialize honeypots if the array is empty', () => {
|
|
99
|
+
ClientLibrary.initializeClient({ honeypots: [] });
|
|
100
|
+
|
|
101
|
+
expect(initHoneypotsSpy).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should inject trap URLs when provided', () => {
|
|
105
|
+
const urls = ['/trap1?sig=123', '/trap2?sig=456'];
|
|
106
|
+
ClientLibrary.initializeClient({ trapUrls: urls });
|
|
107
|
+
|
|
108
|
+
expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should not inject trap URLs if the array is empty', () => {
|
|
112
|
+
ClientLibrary.initializeClient({ trapUrls: [] });
|
|
113
|
+
|
|
114
|
+
expect(injectTrapsSpy).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should initialize fetch interception when fetch config is present', () => {
|
|
118
|
+
const targetDomains = ['api.example.com'];
|
|
119
|
+
ClientLibrary.initializeClient({ fetch: { targetDomains } });
|
|
120
|
+
|
|
121
|
+
expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should not initialize fetch interception if fetch config is absent', () => {
|
|
125
|
+
ClientLibrary.initializeClient(); // No fetch config
|
|
126
|
+
|
|
127
|
+
expect(initFetchSpy).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should call all initializers correctly when a full config is provided', () => {
|
|
131
|
+
const config = {
|
|
132
|
+
mouse: true,
|
|
133
|
+
keystrokes: true,
|
|
134
|
+
honeypots: ['field1'],
|
|
135
|
+
trapUrls: ['/trap1'],
|
|
136
|
+
fetch: { targetDomains: ['api.com'] }
|
|
137
|
+
};
|
|
138
|
+
ClientLibrary.initializeClient(config);
|
|
139
|
+
|
|
140
|
+
expect(startMouseSpy).toHaveBeenCalled();
|
|
141
|
+
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
142
|
+
expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
|
|
143
|
+
expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
|
|
144
|
+
expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
|
|
145
|
+
});
|
|
146
|
+
|
|
144
147
|
});
|
|
@@ -10,6 +10,8 @@ import * as fingerprint from '../fingerprint.js';
|
|
|
10
10
|
vi.mock('import-meta-env', () => ({
|
|
11
11
|
env: { NODE_ENV: 'test', POW_SECRET: 'fallback-dev-secret-32-chars-minimum' },
|
|
12
12
|
}));
|
|
13
|
+
// Mock the entire dns module
|
|
14
|
+
vi.mock('node:dns/promises');
|
|
13
15
|
// Mock readFileSync for custom challenge page tests
|
|
14
16
|
vi.mock('node:fs', async () => {
|
|
15
17
|
const actualFs = await vi.importActual('node:fs');
|
|
@@ -35,9 +37,8 @@ const {
|
|
|
35
37
|
} = fingerprint;
|
|
36
38
|
const { store, getRequestPatternScore, getDeviceHash } = __internal;
|
|
37
39
|
const { getRenderingAnomalyScore } = __internal;
|
|
38
|
-
let { getBehaviorScore, getClickVarianceScore } = __internal;
|
|
39
|
-
|
|
40
|
-
vi.mock('node:dns/promises');
|
|
40
|
+
let { getBehaviorScore, getClickVarianceScore } = __internal; // eslint-disable-line prefer-const
|
|
41
|
+
|
|
41
42
|
|
|
42
43
|
describe('Fingerprint & PoW Security Suite', () => {
|
|
43
44
|
test('cyrb53 should be deterministic', () => {
|
|
@@ -1764,6 +1765,92 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1764
1765
|
});
|
|
1765
1766
|
});
|
|
1766
1767
|
});
|
|
1768
|
+
describe('Googlebot Whitelisting (Node.js)', () => {
|
|
1769
|
+
const inMemoryStore = {
|
|
1770
|
+
_map: new Map(),
|
|
1771
|
+
async get(key) { return this._map.get(key); },
|
|
1772
|
+
async set(key, value) { this._map.set(key, value); },
|
|
1773
|
+
async has(key) { return this._map.has(key); },
|
|
1774
|
+
async delete(key) { this._map.delete(key); },
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
const securityConfig = {
|
|
1778
|
+
weights: { headerAnomalyScore: 1.0, tlsSpoofingScore: 1.0 }, // Simple config for testing
|
|
1779
|
+
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
1780
|
+
whitelist: [
|
|
1781
|
+
{ userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' }
|
|
1782
|
+
]
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
let engine;
|
|
1786
|
+
|
|
1787
|
+
beforeEach(() => {
|
|
1788
|
+
inMemoryStore._map.clear();
|
|
1789
|
+
configureStore(inMemoryStore);
|
|
1790
|
+
vi.clearAllMocks(); // Clear mocks before each test
|
|
1791
|
+
engine = new FingerprintEngine(securityConfig);
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
it('should correctly identify and allow a legitimate Googlebot request', async () => {
|
|
1795
|
+
const googleIp = '::ffff:66.249.66.39'; // An IP from the official list
|
|
1796
|
+
const googleHostname = 'crawl-66-249-66-39.googlebot.com';
|
|
1797
|
+
const googleUa = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
|
|
1798
|
+
|
|
1799
|
+
// Mock the DNS verification process
|
|
1800
|
+
vi.mocked(dns.reverse).mockResolvedValue([googleHostname]);
|
|
1801
|
+
vi.mocked(dns.resolve).mockResolvedValue([googleIp]);
|
|
1802
|
+
|
|
1803
|
+
const requestContext = {
|
|
1804
|
+
clientIp: googleIp,
|
|
1805
|
+
path: '/',
|
|
1806
|
+
headers: { 'user-agent': googleUa },
|
|
1807
|
+
isStatic: false,
|
|
1808
|
+
cookies: {},
|
|
1809
|
+
query: {},
|
|
1810
|
+
body: null,
|
|
1811
|
+
httpVersion: '1.1',
|
|
1812
|
+
rawHeaders: ['user-agent', googleUa]
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
const decision = await engine.processRequest(requestContext);
|
|
1816
|
+
|
|
1817
|
+
expect(decision.action).toBe('next');
|
|
1818
|
+
expect(decision.score).toBe(0);
|
|
1819
|
+
expect(decision.vector.whitelisted).toBe(100);
|
|
1820
|
+
expect(decision.vector.type).toBe('bot');
|
|
1821
|
+
|
|
1822
|
+
expect(dns.reverse).toHaveBeenCalledWith(googleIp);
|
|
1823
|
+
expect(dns.resolve).toHaveBeenCalledWith(googleHostname);
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
it('should challenge or block a request spoofing a Googlebot User-Agent from a non-Google IP', async () => {
|
|
1827
|
+
const fakeGoogleIp = '1.2.3.4';
|
|
1828
|
+
const googleUa = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
|
|
1829
|
+
|
|
1830
|
+
vi.mocked(dns.reverse).mockRejectedValue(new Error('Host not found'));
|
|
1831
|
+
|
|
1832
|
+
const requestContext = {
|
|
1833
|
+
clientIp: fakeGoogleIp,
|
|
1834
|
+
path: '/',
|
|
1835
|
+
headers: { 'user-agent': googleUa, 'accept-language': 'en-US' },
|
|
1836
|
+
isStatic: false,
|
|
1837
|
+
cookies: {},
|
|
1838
|
+
query: {},
|
|
1839
|
+
body: null,
|
|
1840
|
+
httpVersion: '1.1',
|
|
1841
|
+
rawHeaders: ['user-agent', googleUa, 'accept-language', 'en-US']
|
|
1842
|
+
};
|
|
1843
|
+
|
|
1844
|
+
const decision = await engine.processRequest(requestContext);
|
|
1845
|
+
|
|
1846
|
+
expect(decision.action).toBe('block');
|
|
1847
|
+
expect(decision.score).toBeGreaterThan(0);
|
|
1848
|
+
expect(decision.vector.whitelisted).toBeUndefined();
|
|
1849
|
+
|
|
1850
|
+
expect(dns.reverse).toHaveBeenCalledWith(fakeGoogleIp);
|
|
1851
|
+
expect(dns.resolve).not.toHaveBeenCalled();
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1767
1854
|
|
|
1768
1855
|
describe('TLS Session Resumption (Cookieless Identity)', () => {
|
|
1769
1856
|
const inMemoryStore = {
|
|
@@ -2369,20 +2456,47 @@ describe('Subnet Scoring (Node.js)', () => {
|
|
|
2369
2456
|
// 2. Some high scores, few devices. The subnet is calculated from the context.
|
|
2370
2457
|
// The key is hardcoded here to match what the implementation would store.
|
|
2371
2458
|
await inMemoryStore.set('subnet:10.0.0.0/24', {
|
|
2372
|
-
highScoreCount: 5,
|
|
2373
|
-
deviceIds: ['d1', 'd2'],
|
|
2459
|
+
highScoreCount: 5,
|
|
2460
|
+
deviceIds: ['d1', 'd2'],
|
|
2374
2461
|
});
|
|
2375
2462
|
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2376
|
-
expect(subnetScore).
|
|
2463
|
+
expect(subnetScore).toBeGreaterThan(0);
|
|
2464
|
+
expect(subnetScore).toBeLessThanOrEqual(45.0); // Ambient score capped in Medium zone
|
|
2377
2465
|
|
|
2378
|
-
// 3. Many devices and high scores
|
|
2466
|
+
// 3. Many devices and high scores, ambient score still capped at 45.0 (Medium asymptote)
|
|
2379
2467
|
const deviceIds = Array.from({ length: 20 }, (_, i) => `d${i}`);
|
|
2380
2468
|
await inMemoryStore.set('subnet:10.0.0.0/24', {
|
|
2381
|
-
highScoreCount: 30,
|
|
2382
|
-
deviceIds: deviceIds,
|
|
2469
|
+
highScoreCount: 30,
|
|
2470
|
+
deviceIds: deviceIds,
|
|
2383
2471
|
});
|
|
2384
2472
|
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2385
|
-
|
|
2473
|
+
expect(subnetScore).toBeLessThanOrEqual(45.0);
|
|
2474
|
+
expect(subnetScore).toBeGreaterThanOrEqual(30.0);
|
|
2475
|
+
|
|
2476
|
+
// 4. Nearby attacker in /30 (+45 boost), escalates score into High territory
|
|
2477
|
+
await inMemoryStore.set('subnet:10.0.0.0/24', {
|
|
2478
|
+
highScoreCount: 30,
|
|
2479
|
+
deviceIds: deviceIds,
|
|
2480
|
+
attackerIps: [{ ip: '10.0.0.26', lastSeen: Date.now(), score: 85 }]
|
|
2481
|
+
});
|
|
2482
|
+
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2483
|
+
expect(subnetScore).toBeGreaterThanOrEqual(80.0);
|
|
2484
|
+
|
|
2485
|
+
// 5. Exact IP match (+55 boost), reaches 90-100
|
|
2486
|
+
await inMemoryStore.set('subnet:10.0.0.0/24', {
|
|
2487
|
+
highScoreCount: 30,
|
|
2488
|
+
deviceIds: deviceIds,
|
|
2489
|
+
attackerIps: [{ ip: '10.0.0.25', lastSeen: Date.now(), score: 95 }]
|
|
2490
|
+
});
|
|
2491
|
+
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2492
|
+
expect(subnetScore).toBeGreaterThanOrEqual(90.0);
|
|
2493
|
+
});
|
|
2494
|
+
|
|
2495
|
+
it('getIpCommonPrefixLength should accurately calculate common prefix bits', () => {
|
|
2496
|
+
const { getIpCommonPrefixLength } = __internal;
|
|
2497
|
+
expect(getIpCommonPrefixLength('10.0.0.25', '10.0.0.25')).toBe(32);
|
|
2498
|
+
expect(getIpCommonPrefixLength('10.0.0.24', '10.0.0.25')).toBe(31);
|
|
2499
|
+
expect(getIpCommonPrefixLength('192.168.1.10', '192.168.2.10')).toBe(22);
|
|
2386
2500
|
});
|
|
2387
2501
|
});
|
|
2388
2502
|
|
|
@@ -138,5 +138,6 @@ describe('IP Reputation Local System (Node.js)', () => {
|
|
|
138
138
|
}
|
|
139
139
|
score = await __internal.getSubnetScore(mockContext);
|
|
140
140
|
expect(score.subnetScore).toBeGreaterThan(0);
|
|
141
|
+
expect(score.subnetScore).toBeLessThanOrEqual(45.0); // Ambient cap
|
|
141
142
|
});
|
|
142
143
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Abstract base class defining useful Proof-of-Work (uPoW) tasks based on ML training
|
|
5
|
+
* or inference validation (e.g. PyTorch models exported to ONNX format).
|
|
6
6
|
*/
|
|
7
7
|
export class UpowModelTask {
|
|
8
8
|
constructor(problemId, modelPath, config = {}) {
|
|
@@ -20,7 +20,7 @@ export class UpowModelTask {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Returns the unique problem identifier.
|
|
24
24
|
* @returns {string}
|
|
25
25
|
*/
|
|
26
26
|
getProblemId() {
|
|
@@ -28,7 +28,7 @@ export class UpowModelTask {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Returns the network asset path of the model.
|
|
32
32
|
* @returns {string}
|
|
33
33
|
*/
|
|
34
34
|
getModelAssetPath() {
|
|
@@ -36,18 +36,18 @@ export class UpowModelTask {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* Dispatches task workload to the client.
|
|
40
|
+
* Transmits current model weights and mini-batch sample data.
|
|
41
41
|
* @param {number} suspicionFactor
|
|
42
|
-
* @returns {object}
|
|
42
|
+
* @returns {object} Task payload dispatched to client
|
|
43
43
|
*/
|
|
44
44
|
dispatchTask(suspicionFactor) {
|
|
45
45
|
throw new Error("Method 'dispatchTask(suspicionFactor)' must be implemented.");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* Validates solution returned by the client.
|
|
50
|
+
* Implements anti-poisoning guards and gradient bounds checks.
|
|
51
51
|
* @param {object} taskContext
|
|
52
52
|
* @param {object} solution
|
|
53
53
|
* @returns {boolean}
|
|
@@ -57,8 +57,7 @@ export class UpowModelTask {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
61
|
-
* Par exemple : algorithme FedAvg (Federated Averaging).
|
|
60
|
+
* Integrates validated gradients into global model weights (e.g. FedAvg).
|
|
62
61
|
* @param {object} solution
|
|
63
62
|
*/
|
|
64
63
|
integrateSolution(solution) {
|
|
@@ -67,17 +66,17 @@ export class UpowModelTask {
|
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
/**
|
|
70
|
-
*
|
|
69
|
+
* Example training task implementation (suspicious request classifier).
|
|
71
70
|
*/
|
|
72
71
|
export class RequestClassifierTask extends UpowModelTask {
|
|
73
72
|
constructor(modelPath, config) {
|
|
74
73
|
super("request_classifier_nn", modelPath, config);
|
|
75
|
-
//
|
|
74
|
+
// Initial model weights (linear neural network)
|
|
76
75
|
this.weights = [0.1, -0.2, 0.8, 0.5];
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
dispatchTask(suspicionFactor) {
|
|
80
|
-
//
|
|
79
|
+
// Anonymized sample training batch dispatched to client
|
|
81
80
|
const inputs = [
|
|
82
81
|
[1.0, 0.5, 0.0, 1.2],
|
|
83
82
|
[0.0, 1.0, -0.5, 0.8]
|
|
@@ -95,7 +94,7 @@ export class RequestClassifierTask extends UpowModelTask {
|
|
|
95
94
|
if (!Array.isArray(gradients) || gradients.length !== this.weights.length) {
|
|
96
95
|
return false;
|
|
97
96
|
}
|
|
98
|
-
// Anti-poisoning :
|
|
97
|
+
// Anti-poisoning guard: ensure gradients do not exceed threshold norms (gradient clipping)
|
|
99
98
|
const maxGradientNorm = 10.0;
|
|
100
99
|
for (const grad of gradients) {
|
|
101
100
|
if (typeof grad !== 'number' || isNaN(grad) || Math.abs(grad) > maxGradientNorm) {
|