@anonympins/fingerprint 0.7.4 → 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 +59 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +1784 -1738
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +489 -99
- 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/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.init.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +37 -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/tests/threatIntel.test.js +40 -1
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +392 -86
- package/src/php/Config/SecurityProfiles.php +79 -61
- 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 +2122 -1690
- 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 +34 -0
- 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 +362 -1
- 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
|
@@ -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
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { __internal } from '../fingerprint.js';
|
|
3
3
|
|
|
4
|
-
describe('
|
|
4
|
+
describe('Polymorphic System and Obfuscation Compiler', () => {
|
|
5
5
|
|
|
6
6
|
it('should generate a valid session mapping with randomized headers and globals', () => {
|
|
7
7
|
const mapping1 = __internal.generateSessionMapping();
|
|
8
8
|
const mapping2 = __internal.generateSessionMapping();
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// Verify expected mapping structure
|
|
11
11
|
expect(mapping1).toBeDefined();
|
|
12
12
|
expect(mapping1.headers).toHaveProperty('x-device-fingerprint');
|
|
13
13
|
expect(mapping1.headers).toHaveProperty('x-behavior-metrics');
|
|
@@ -17,7 +17,7 @@ describe('Système de Polymorphisme et Compilateur d\'Obfuscation', () => {
|
|
|
17
17
|
expect(mapping1.keys).toHaveProperty('ua');
|
|
18
18
|
expect(mapping1.wasmConstants).toHaveProperty('seed');
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// Verify randomness: two consecutive sessions must yield unique mappings
|
|
21
21
|
expect(mapping1.headers['x-device-fingerprint']).not.toBe(mapping2.headers['x-device-fingerprint']);
|
|
22
22
|
expect(mapping1.globals['ClientLibrary']).not.toBe(mapping2.globals['ClientLibrary']);
|
|
23
23
|
expect(mapping1.keys['ua']).not.toBe(mapping2.keys['ua']);
|
|
@@ -27,22 +27,22 @@ describe('Système de Polymorphisme et Compilateur d\'Obfuscation', () => {
|
|
|
27
27
|
it('should compile and obfuscate client script asynchronously using the obfuscation worker', async () => {
|
|
28
28
|
const mapping = __internal.generateSessionMapping();
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// Run background worker obfuscation task
|
|
31
31
|
const obfuscatedCode = await __internal.compilePolymorphicJs(mapping);
|
|
32
32
|
|
|
33
33
|
expect(obfuscatedCode).toBeDefined();
|
|
34
34
|
expect(typeof obfuscatedCode).toBe('string');
|
|
35
35
|
expect(obfuscatedCode.length).toBeGreaterThan(0);
|
|
36
36
|
|
|
37
|
-
//
|
|
37
|
+
// Verify original identifiers were rewritten in compiled client script
|
|
38
38
|
expect(obfuscatedCode).not.toContain('X-Device-Fingerprint');
|
|
39
39
|
expect(obfuscatedCode).not.toContain('X-Behavior-Metrics');
|
|
40
40
|
expect(obfuscatedCode).not.toContain('ClientLibrary');
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Verify code is obfuscated without plain dev comments
|
|
43
43
|
expect(obfuscatedCode).not.toContain('// Nom trompeur aléatoire pour attirer les analyseurs');
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// Obfuscator emits string array wrapper prefix
|
|
46
46
|
expect(obfuscatedCode).toMatch(/_0x/);
|
|
47
|
-
}, 15000); //
|
|
47
|
+
}, 15000); // 15s timeout for CPU-intensive worker execution
|
|
48
48
|
});
|
|
@@ -43,4 +43,51 @@ describe('Protocol Anomaly Flow Profiling', () => {
|
|
|
43
43
|
const score = getProtocolAnomalyScore(context);
|
|
44
44
|
expect(score.protocolAnomalyScore).toBe(100.0);
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
it('should detect RFC 9218 invalid urgency value out of range (u=9)', () => {
|
|
48
|
+
const context = {
|
|
49
|
+
headers: {
|
|
50
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
51
|
+
'x-quic-fp': '1;1=1572864,4=100;u=9,i'
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const score = getProtocolAnomalyScore(context);
|
|
55
|
+
expect(score.quicAnomalyScore).toBeGreaterThanOrEqual(50.0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should detect HTTP/2 flat dependency tree from Go net/http2 scraper', () => {
|
|
59
|
+
const context = {
|
|
60
|
+
headers: {
|
|
61
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
62
|
+
'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:16|m,a,s,p|p:3,w:4,c:1'
|
|
63
|
+
},
|
|
64
|
+
httpVersion: '2.0'
|
|
65
|
+
};
|
|
66
|
+
const score = getProtocolAnomalyScore(context);
|
|
67
|
+
expect(score.http2AnomalyScore).toBeGreaterThanOrEqual(55.0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should detect HTTP/2 self-dependency violation', () => {
|
|
71
|
+
const context = {
|
|
72
|
+
headers: {
|
|
73
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
74
|
+
'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|3:0:3:256|m,a,s,p|p:3,w:4,c:1'
|
|
75
|
+
},
|
|
76
|
+
httpVersion: '2.0'
|
|
77
|
+
};
|
|
78
|
+
const score = getProtocolAnomalyScore(context);
|
|
79
|
+
expect(score.http2AnomalyScore).toBeGreaterThanOrEqual(60.0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should detect scrapers with 0 dynamic table entries on repeated requests', () => {
|
|
83
|
+
const context = {
|
|
84
|
+
headers: {
|
|
85
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
86
|
+
'x-compression-info': 'req_count:5,dynamic_table_entries:0'
|
|
87
|
+
},
|
|
88
|
+
httpVersion: '2.0'
|
|
89
|
+
};
|
|
90
|
+
const score = getProtocolAnomalyScore(context);
|
|
91
|
+
expect(score.http2AnomalyScore).toBeGreaterThanOrEqual(45.0);
|
|
92
|
+
});
|
|
46
93
|
});
|
|
@@ -5,7 +5,7 @@ const { parseTcpSyn, classifyTcpOs, getTcpAnomalyScore } = __internal;
|
|
|
5
5
|
|
|
6
6
|
describe('Passive TCP/IP Fingerprinting (Type p0f)', () => {
|
|
7
7
|
|
|
8
|
-
// Mock
|
|
8
|
+
// Mock Linux IP + TCP SYN packet (TTL=64, WS=7)
|
|
9
9
|
const mockLinuxSynPacket = Buffer.from([
|
|
10
10
|
0x45, 0x00, 0x00, 0x3c, 0x1a, 0x2b, 0x40, 0x00,
|
|
11
11
|
0x40, 0x06, 0x3c, 0x1a, 0x7f, 0x00, 0x00, 0x01, // TTL=0x40 (64)
|
|
@@ -19,7 +19,7 @@ describe('Passive TCP/IP Fingerprinting (Type p0f)', () => {
|
|
|
19
19
|
0x03, 0x03, 0x07 // Option WS: 7
|
|
20
20
|
]);
|
|
21
21
|
|
|
22
|
-
// Mock
|
|
22
|
+
// Mock Windows IP + TCP SYN packet (TTL=128, WS=8)
|
|
23
23
|
const mockWindowsSynPacket = Buffer.from([
|
|
24
24
|
0x45, 0x00, 0x00, 0x3c, 0x1a, 0x2b, 0x40, 0x00,
|
|
25
25
|
0x80, 0x06, 0x3c, 0x1a, 0x7f, 0x00, 0x00, 0x01, // TTL=0x80 (128)
|
|
@@ -57,7 +57,7 @@ describe('Passive TCP/IP Fingerprinting (Type p0f)', () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it('should calculate correct TCP/IP anomaly scores', () => {
|
|
60
|
-
//
|
|
60
|
+
// Case 1: User-Agent claims Windows, but TCP/IP stack signature is Linux
|
|
61
61
|
const contextAnomaly = {
|
|
62
62
|
headers: {
|
|
63
63
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0',
|
|
@@ -66,7 +66,7 @@ describe('Passive TCP/IP Fingerprinting (Type p0f)', () => {
|
|
|
66
66
|
};
|
|
67
67
|
expect(getTcpAnomalyScore(contextAnomaly).tcpAnomalyScore).toBe(80.1);
|
|
68
68
|
|
|
69
|
-
//
|
|
69
|
+
// Case 2: Consistent stack (Windows UA and Windows TCP/IP stack)
|
|
70
70
|
const contextCoherent = {
|
|
71
71
|
headers: {
|
|
72
72
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0',
|