@anonympins/fingerprint 0.7.5 → 0.8.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 +32 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/fingerprint.client.js +1784 -1772
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +198 -28
- package/src/js/fingerprint.utils.js +252 -2
- package/src/js/pow.worker.js +2 -2
- package/src/js/tests/cross_parity.test.js +3 -3
- package/src/js/tests/dns-circuit-breaker.test.js +6 -6
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +19 -12
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
- package/src/js/tests/library.test.js +94 -95
- package/src/js/tests/obfuscation.test.js +8 -8
- package/src/js/tests/quicFingerprint.test.js +47 -0
- package/src/js/tests/tcpFingerprint.test.js +4 -4
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +382 -86
- package/src/php/Config/SecurityProfiles.php +75 -62
- package/src/php/DirectFingerprint.php +55 -8
- package/src/php/FingerprintBuilder.php +13 -13
- package/src/php/FingerprintClient.php +6 -6
- package/src/php/FingerprintEngine.php +2120 -1837
- package/src/php/Ja3AnomalyDetector.php +22 -22
- package/src/php/Optimization/FunctionRegistry.php +4 -4
- package/src/php/Optimization/Optimization.php +6 -6
- package/src/php/RequestContext.php +11 -11
- package/src/php/Store/InMemoryStore.php +96 -66
- package/src/php/Store/MongoDbStore.php +2 -2
- package/src/php/Store/RedisStore.php +1 -1
- package/src/php/Store/StoreManager.php +48 -35
- package/src/php/Tests/ChallengeUtilsTest.php +31 -0
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintClientTest.php +70 -70
- package/src/php/Tests/PowTest.php +37 -39
- package/src/php/Tests/QuicFingerprintTest.php +115 -53
- package/src/php/Tests/ThreatIntelTest.php +2 -2
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/BlockList.php +13 -13
- package/src/php/Utils/Logger.php +19 -1
- package/src/php/Utils/MaliciousPatterns.php +6 -6
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +327 -14
- package/src/php/Utils/TLSClientHelloParser.php +385 -369
- package/src/php/WordPress/WpDbStore.php +7 -7
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
- package/src/php/WordPress/package.php +80 -101
- package/src/php/WordPress/readme.txt +21 -5
- package/src/php/bin/auto-tune.php +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
|
@@ -7,12 +7,12 @@ import ClientLibrary from '../fingerprint.client.js';
|
|
|
7
7
|
|
|
8
8
|
describe('ClientLibrary WASM Integration', () => {
|
|
9
9
|
beforeEach(() => {
|
|
10
|
-
//
|
|
10
|
+
// Reset cache and spies before each test
|
|
11
11
|
ClientLibrary._resetCache();
|
|
12
12
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
13
13
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// Remove global mocks to prevent leakage across tests
|
|
16
16
|
delete window.createFingerprintModule;
|
|
17
17
|
});
|
|
18
18
|
|
|
@@ -21,28 +21,27 @@ describe('ClientLibrary WASM Integration', () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it('should successfully load WASM module and switch hasher', async () => {
|
|
24
|
-
// 1.
|
|
24
|
+
// 1. Mock functional WASM module
|
|
25
25
|
const mockWasmModule = {
|
|
26
|
-
_hash_string: vi.fn((str) => 99999), //
|
|
26
|
+
_hash_string: vi.fn((str) => 99999), // Distinct mock hash value
|
|
27
27
|
_malloc: vi.fn().mockReturnValue(0),
|
|
28
28
|
HEAPU8: new Uint8Array(4096)
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
// 2.
|
|
32
|
-
// On attache les fonctions de simulation à `window` car c'est ce que le code cherche
|
|
31
|
+
// 2. Mock script loading and module factory
|
|
33
32
|
window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
|
|
34
33
|
|
|
35
|
-
//
|
|
34
|
+
// Mock script element injection with direct onload callback
|
|
36
35
|
vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
|
|
37
|
-
//
|
|
36
|
+
// Simulate successful script load
|
|
38
37
|
script.onload();
|
|
39
38
|
return script;
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
// 3.
|
|
41
|
+
// 3. Initialize WASM module
|
|
43
42
|
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
44
43
|
|
|
45
|
-
// 4.
|
|
44
|
+
// 4. Verify hasher was switched
|
|
46
45
|
const wasmHash = ClientLibrary._hasher("test");
|
|
47
46
|
expect(wasmHash).toBe(99999);
|
|
48
47
|
expect(mockWasmModule._hash_string).toHaveBeenCalledWith(0);
|
|
@@ -50,16 +49,16 @@ describe('ClientLibrary WASM Integration', () => {
|
|
|
50
49
|
});
|
|
51
50
|
|
|
52
51
|
it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
|
|
53
|
-
// 1.
|
|
52
|
+
// 1. Simulate script loading failure
|
|
54
53
|
vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
|
|
55
54
|
script.onerror(new Error('Script loading failed'));
|
|
56
55
|
return script;
|
|
57
56
|
});
|
|
58
57
|
|
|
59
|
-
// 2.
|
|
58
|
+
// 2. Initialize WASM module
|
|
60
59
|
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
61
60
|
|
|
62
|
-
// 3.
|
|
61
|
+
// 3. Verify hasher falls back to JS implementation
|
|
63
62
|
const jsHash = ClientLibrary._hasher("test");
|
|
64
63
|
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
|
|
65
64
|
|
|
@@ -68,7 +67,7 @@ describe('ClientLibrary WASM Integration', () => {
|
|
|
68
67
|
});
|
|
69
68
|
|
|
70
69
|
it('should gracefully fall back if WASM module does not export _hash_string', async () => {
|
|
71
|
-
// 1.
|
|
70
|
+
// 1. Simulate malformed WASM module missing required export
|
|
72
71
|
const mockWasmModule = {};
|
|
73
72
|
window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
|
|
74
73
|
|
|
@@ -77,14 +76,14 @@ describe('ClientLibrary WASM Integration', () => {
|
|
|
77
76
|
return script;
|
|
78
77
|
});
|
|
79
78
|
|
|
80
|
-
// 2.
|
|
79
|
+
// 2. Initialize WASM module
|
|
81
80
|
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
82
81
|
|
|
83
|
-
// 3.
|
|
82
|
+
// 3. Verify fallback
|
|
84
83
|
const jsHash = ClientLibrary._hasher("test");
|
|
85
84
|
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
|
|
86
85
|
expect(jsHash).toBe(originalJsHash);
|
|
87
|
-
//
|
|
86
|
+
// Verify both generic error log and specific missing export cause
|
|
88
87
|
expect(console.warn).toHaveBeenCalledWith(
|
|
89
88
|
expect.stringContaining('WASM module failed to load'),
|
|
90
89
|
expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
|
|
@@ -1621,18 +1621,6 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1621
1621
|
expect(behaviorScore).toBe(0);
|
|
1622
1622
|
});
|
|
1623
1623
|
|
|
1624
|
-
it('should return a score of 0 when x-behavior-metrics is 1 (human verified client-side)', () => {
|
|
1625
|
-
const context = { headers: { 'x-behavior-metrics': '1' } };
|
|
1626
|
-
const { behaviorScore } = getBehaviorScore(context);
|
|
1627
|
-
expect(behaviorScore).toBe(0);
|
|
1628
|
-
});
|
|
1629
|
-
|
|
1630
|
-
it('should return a score of 100 when x-behavior-metrics is 0 (bot detected client-side)', () => {
|
|
1631
|
-
const context = { headers: { 'x-behavior-metrics': '0' } };
|
|
1632
|
-
const { behaviorScore } = getBehaviorScore(context);
|
|
1633
|
-
expect(behaviorScore).toBe(100);
|
|
1634
|
-
});
|
|
1635
|
-
|
|
1636
1624
|
it('should return a score of 100 for honeypot interaction', () => {
|
|
1637
1625
|
const metrics = { honeypotInteraction: true, mouseEntropy: 50, keystrokeLatency: 120 };
|
|
1638
1626
|
const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
|
|
@@ -3199,6 +3187,25 @@ describe('IP Registration and Filtering', () => {
|
|
|
3199
3187
|
expect(process.env.ED25519_PRIVATE_KEY).toBe(dummyKeys.privateKey);
|
|
3200
3188
|
expect(process.env.ED25519_PUBLIC_KEY).toBe(dummyKeys.publicKey);
|
|
3201
3189
|
});
|
|
3190
|
+
|
|
3191
|
+
it('should generate issuer PEM files directly via generateIssuerPemKeys', async () => {
|
|
3192
|
+
const fs = await import('node:fs');
|
|
3193
|
+
const os = await import('node:os');
|
|
3194
|
+
const path = await import('node:path');
|
|
3195
|
+
const tempDir = path.join(os.tmpdir(), `fp-issuer-keys-${Date.now()}`);
|
|
3196
|
+
|
|
3197
|
+
try {
|
|
3198
|
+
const keys = fingerprint.generateIssuerPemKeys(tempDir);
|
|
3199
|
+
expect(keys.privateKeyPath).toContain('issuer-private.pem');
|
|
3200
|
+
expect(keys.publicKeyPath).toContain('issuer-public.pem');
|
|
3201
|
+
expect(keys.privateKey).toContain('-----BEGIN PRIVATE KEY-----');
|
|
3202
|
+
expect(keys.publicKey).toContain('-----BEGIN PUBLIC KEY-----');
|
|
3203
|
+
expect(fs.existsSync(keys.privateKeyPath)).toBe(true);
|
|
3204
|
+
expect(fs.existsSync(keys.publicKeyPath)).toBe(true);
|
|
3205
|
+
} finally {
|
|
3206
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3202
3209
|
});
|
|
3203
3210
|
|
|
3204
3211
|
describe('Closed-Shadow DOM Honeypot Traps (Feature 19)', () => {
|
|
@@ -1,136 +1,135 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
|
-
import {getTlsSpoofingScore, parseJa3} from '../fingerprint.js';
|
|
3
|
-
import {vi} from 'vitest';
|
|
4
|
-
import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "../../js/fingerprint.utils.js";
|
|
5
|
-
|
|
6
|
-
describe('JA3 Anomaly Detector (Node.js)', () => {
|
|
7
|
-
|
|
8
|
-
// Mock
|
|
9
|
-
const createMockStore = () => {
|
|
10
|
-
const storage = {};
|
|
11
|
-
return {
|
|
12
|
-
get: vi.fn(async (key) => storage[key] || null),
|
|
13
|
-
set: vi.fn(async (key, val) => { storage[key] = val; })
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
describe('parseJa3', () => {
|
|
18
|
-
it('
|
|
19
|
-
const rawJa3 = '771,4865-4866-4867,0-23-65281-10-11,29-23-24,0';
|
|
20
|
-
const parsed = parseJa3(rawJa3);
|
|
21
|
-
|
|
22
|
-
expect(parsed).not.toBeNull();
|
|
23
|
-
expect(parsed.tlsVersion).toBe(771);
|
|
24
|
-
expect(parsed.ciphers).toEqual([4865, 4866, 4867]);
|
|
25
|
-
expect(parsed.extensions).toEqual([0, 23, 65281, 10, 11]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('
|
|
29
|
-
expect(parseJa3('')).toBeNull();
|
|
30
|
-
expect(parseJa3('771,4865')).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('getTlsSpoofingScore (Anomalies JA3 hachées)', () => {
|
|
35
|
-
it('
|
|
36
|
-
const pythonJa3Hash = '47344a349b75c4e82333475553b5f358';
|
|
37
|
-
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
38
|
-
|
|
39
|
-
const context = {
|
|
40
|
-
headers: {
|
|
41
|
-
'user-agent': chromeUa,
|
|
42
|
-
'x-ja3-hash': pythonJa3Hash
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const result = await getTlsSpoofingScore(context);
|
|
47
|
-
expect(result.tlsSpoofingScore).toBe(90);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('
|
|
51
|
-
const mockStore = createMockStore();
|
|
52
|
-
const unknownJa3 = '00000000000000000000000000000000';
|
|
53
|
-
|
|
54
|
-
const contextChrome = {
|
|
55
|
-
headers: {
|
|
56
|
-
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
|
|
57
|
-
'x-ja3-hash': unknownJa3
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const contextFirefox = {
|
|
62
|
-
headers: {
|
|
63
|
-
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/120.0',
|
|
64
|
-
'x-ja3-hash': unknownJa3
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
'x-ja3-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
'
|
|
107
|
-
'x-ja3-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
'
|
|
126
|
-
'x-ja3-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import {getTlsSpoofingScore, parseJa3} from '../fingerprint.js';
|
|
3
|
+
import {vi} from 'vitest';
|
|
4
|
+
import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "../../js/fingerprint.utils.js";
|
|
5
|
+
|
|
6
|
+
describe('JA3 Anomaly Detector (Node.js)', () => {
|
|
7
|
+
|
|
8
|
+
// Mock async in-memory cache store
|
|
9
|
+
const createMockStore = () => {
|
|
10
|
+
const storage = {};
|
|
11
|
+
return {
|
|
12
|
+
get: vi.fn(async (key) => storage[key] || null),
|
|
13
|
+
set: vi.fn(async (key, val) => { storage[key] = val; })
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('parseJa3', () => {
|
|
18
|
+
it('should correctly parse raw JA3 string', () => {
|
|
19
|
+
const rawJa3 = '771,4865-4866-4867,0-23-65281-10-11,29-23-24,0';
|
|
20
|
+
const parsed = parseJa3(rawJa3);
|
|
21
|
+
|
|
22
|
+
expect(parsed).not.toBeNull();
|
|
23
|
+
expect(parsed.tlsVersion).toBe(771);
|
|
24
|
+
expect(parsed.ciphers).toEqual([4865, 4866, 4867]);
|
|
25
|
+
expect(parsed.extensions).toEqual([0, 23, 65281, 10, 11]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return null for invalid strings', () => {
|
|
29
|
+
expect(parseJa3('')).toBeNull();
|
|
30
|
+
expect(parseJa3('771,4865')).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('getTlsSpoofingScore (Anomalies JA3 hachées)', () => {
|
|
35
|
+
it('should detect library spoofing (Python)', async () => {
|
|
36
|
+
const pythonJa3Hash = '47344a349b75c4e82333475553b5f358';
|
|
37
|
+
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
38
|
+
|
|
39
|
+
const context = {
|
|
40
|
+
headers: {
|
|
41
|
+
'user-agent': chromeUa,
|
|
42
|
+
'x-ja3-hash': pythonJa3Hash
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = await getTlsSpoofingScore(context);
|
|
47
|
+
expect(result.tlsSpoofingScore).toBe(90);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should detect User-Agent rotation on identical JA3 hash (Stagnation)', async () => {
|
|
51
|
+
const mockStore = createMockStore();
|
|
52
|
+
const unknownJa3 = '00000000000000000000000000000000';
|
|
53
|
+
|
|
54
|
+
const contextChrome = {
|
|
55
|
+
headers: {
|
|
56
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
|
|
57
|
+
'x-ja3-hash': unknownJa3
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const contextFirefox = {
|
|
62
|
+
headers: {
|
|
63
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/120.0',
|
|
64
|
+
'x-ja3-hash': unknownJa3
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// First call: Chrome
|
|
69
|
+
const res1 = await getTlsSpoofingScore(contextChrome, mockStore);
|
|
70
|
+
|
|
71
|
+
// Second call: Firefox (Detects UA rotation)
|
|
72
|
+
const res2 = await getTlsSpoofingScore(contextFirefox, mockStore);
|
|
73
|
+
expect(res2.tlsSpoofingScore).toBe(85);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('getTlsSpoofingScore (Anomalies JA3 brutes)', () => {
|
|
78
|
+
it('should flag fake Chrome lacking GREASE values', async () => {
|
|
79
|
+
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
80
|
+
// No GREASE values in ciphers/extensions
|
|
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('should validate legitimate Chrome using GREASE values', async () => {
|
|
98
|
+
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
99
|
+
// 2570 is a valid GREASE value
|
|
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('should detect missing ALPN extension on HTTP/2 connections', async () => {
|
|
117
|
+
const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
|
|
118
|
+
// Missing 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
|
+
});
|
|
@@ -1,96 +1,95 @@
|
|
|
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
|
-
//
|
|
30
|
-
const benfordSample = [
|
|
31
|
-
...Array(301).fill(100), // 301
|
|
32
|
-
...Array(176).fill(200), // 176
|
|
33
|
-
...Array(125).fill(300), // 125
|
|
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
|
-
//
|
|
44
|
-
expect(score).toBeLessThan(1e-9);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should return a high (suspect) score for a uniform distribution', () => {
|
|
48
|
-
//
|
|
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
|
-
//
|
|
58
|
-
expect(score).toBeGreaterThan(0.15);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should return a high (suspect) score for a distribution skewed towards high digits', () => {
|
|
62
|
-
//
|
|
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); //
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should handle real-world-like data (request timings)', () => {
|
|
80
|
-
//
|
|
81
|
-
const botTimings = Array.from({ length: 100 }, () => 500 + Math.random() * 1000);
|
|
82
|
-
const botScore = Optimization.Operators.benfordTest(botTimings);
|
|
83
|
-
|
|
84
|
-
//
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
expect(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
// Sample of 1000 numbers matching Benford's Law distribution
|
|
30
|
+
const benfordSample = [
|
|
31
|
+
...Array(301).fill(100), // 301 numbers leading with 1
|
|
32
|
+
...Array(176).fill(200), // 176 numbers leading with 2
|
|
33
|
+
...Array(125).fill(300), // 125 numbers leading with 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
|
+
// Score should be close to 0; tolerance used for floating point variance
|
|
44
|
+
expect(score).toBeLessThan(1e-9);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return a high (suspect) score for a uniform distribution', () => {
|
|
48
|
+
// Uniform distribution is unnatural for this category of metrics
|
|
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
|
+
// A score > 0.15 is considered suspicious
|
|
58
|
+
expect(score).toBeGreaterThan(0.15);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return a high (suspect) score for a distribution skewed towards high digits', () => {
|
|
62
|
+
// Inverted Benford distribution: highly unnatural
|
|
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); // Significantly elevated score expected
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle real-world-like data (request timings)', () => {
|
|
80
|
+
// Simulates bot request intervals (uniform random between 500 and 1500ms)
|
|
81
|
+
const botTimings = Array.from({ length: 100 }, () => 500 + Math.random() * 1000);
|
|
82
|
+
const botScore = Optimization.Operators.benfordTest(botTimings);
|
|
83
|
+
|
|
84
|
+
// Simulates human timings (more short delays, few long pauses)
|
|
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
|
+
// Bot score should be significantly higher than human score
|
|
91
|
+
expect(botScore).toBeGreaterThan(0.1);
|
|
92
|
+
expect(humanScore).toBeLessThan(botScore);
|
|
93
|
+
});
|
|
94
|
+
|
|
96
95
|
});
|