@anonympins/fingerprint 0.7.2 → 0.7.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 +22 -9
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/fingerprint.js +68 -23
- package/src/js/fingerprint.utils.js +32 -5
- package/src/js/tests/fingerprint.test.js +1 -1
- package/src/js/tests/threatIntel.test.js +76 -0
- package/src/php/AutoTuner.php +36 -18
- package/src/php/Challenge/ChallengeUtils.php +106 -224
- package/src/php/DirectFingerprint.php +16 -4
- package/src/php/FingerprintClient.php +17 -19
- package/src/php/FingerprintEngine.php +77 -39
- package/src/php/Ja3AnomalyDetector.php +20 -12
- package/src/php/Optimization/Optimization.php +5 -1
- package/src/php/Optimization/OptimizationOperators.php +5 -1
- package/src/php/Optimization/ProblemInitializers.php +16 -4
- package/src/php/ProblemManager.php +20 -14
- package/src/php/Tests/FingerprintEngineTest.php +6 -1
- package/src/php/Tests/ThreatIntelTest.php +64 -0
- package/src/php/Utils/Env.php +51 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/RequestUtils.php +22 -5
- package/src/php/WordPress/WpDbStore.php +4 -0
- package/src/php/WordPress/{fingerprint-wordpress.php → fingerprint-anti-bot.php} +131 -126
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
- package/src/php/WordPress/package.php +6 -3
- package/src/php/WordPress/readme.txt +36 -0
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/fingerprint.client.obfuscated.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
|
+
## Version 0.7.3
|
|
2
|
+
|
|
3
|
+
- 🛡️ **Scoring Corrections & Anti-Spoofing Fixes**:
|
|
4
|
+
- **TLS Spoofing Score Fix**: Resolved false positive detections where legitimate Chrome browsers were erroneously flagged as spoofed (`tlsSpoofingScore`), improving GREASE cipher/extension inspection and ALPN protocol validation.
|
|
5
|
+
- **Threat Intelligence Rework (`threatIntelScore`)**: Overhauled the threat intelligence scoring mechanism, refining the correlation between low-level TCP RTT and application-layer transit discrepancies while streamlining federated ZKP blacklist checks.
|
|
6
|
+
- 🌐 **Cross-Language Score Parity**:
|
|
7
|
+
- Resolved calculation discrepancies and harmonized scoring formulas across Node.js, PHP, and Java implementations to guarantee deterministic and identical results across all runtimes.
|
|
8
|
+
- Fixed all known scoring edge cases primarily affecting the Node.js module.
|
|
9
|
+
- 🔌 **WordPress Plugin Hardening & Compliance**:
|
|
10
|
+
- **Full WordPress Plugin Check Compliance**: Resolved all errors, warnings, and notices reported by the official WordPress Plugin Check tool, ensuring strict adherence to WordPress coding and security standards.
|
|
11
|
+
- 👥 **Contributors**:
|
|
12
|
+
- Contributed by @anonympins.
|
|
13
|
+
|
|
1
14
|
## Version 0.7.2
|
|
2
15
|
|
|
3
16
|
- 🔌 **WordPress Plugin Enhancements**:
|
|
4
|
-
-
|
|
5
|
-
-
|
|
17
|
+
- 🌐 **Full Internationalization (i18n)**: The WordPress plugin is now fully translated into French (fr_FR) and German (de_DE), providing a native experience for administrators in those languages.
|
|
18
|
+
- 🐛 **HTTP Fallback Fix**: Resolved a critical issue where challenges would fail on non-HTTPS (HTTP) sites. The system now transparently falls back to a secure server-side validation mechanism when the browser's `crypto.subtle` API is unavailable, ensuring functionality in all environments.
|
|
6
19
|
- 🛡️ **Security & Bot Detection Hardening**:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
20
|
+
- 🔐 **Obfuscated Client Script (`fingerprint.client.obfuscated.js`)**: Introduced a new build step that generates a heavily obfuscated version of the client-side library. This makes reverse-engineering of the fingerprinting and behavioral analysis logic significantly more difficult for attackers.
|
|
21
|
+
- 🤖 **Default Bot Whitelist**: The engine now applies a comprehensive default whitelist for major search engine crawlers (Googlebot, Bingbot, Yandex, etc.) out-of-the-box. This ensures zero-configuration SEO compatibility and prevents legitimate bots from being challenged.
|
|
22
|
+
- 📡 **Updated Googlebot IPs**: The list of official Googlebot IP ranges has been updated to ensure the latest crawlers are correctly identified and allowed.
|
|
10
23
|
- 🔧 **Developer Experience & Debugging**:
|
|
11
|
-
-
|
|
24
|
+
- 📝 **Reverse DNS Logging**: Added detailed logging to the bot verification workflow. This provides clear insights into the reverse and forward DNS lookup process, making it easier to debug whitelisting and identify spoofing attempts.
|
|
12
25
|
|
|
13
26
|
## Version 0.7.1
|
|
14
27
|
|
|
15
|
-
- 🔌 **Native WordPress Plugin Integration (`fingerprint-
|
|
16
|
-
- Full official WordPress plugin integration (`fingerprint-
|
|
28
|
+
- 🔌 **Native WordPress Plugin Integration (`fingerprint-anti-bot`)**:
|
|
29
|
+
- Full official WordPress plugin integration (`fingerprint-anti-bot.php`) without external runtime dependencies, leveraging `$wpdb` through a dedicated `WpDbStore` adapter.
|
|
17
30
|
- Automatic SQL cache table provisioning and periodic cleanup of expired tickets via WP-Cron (`fingerprint_prune_expired_entries`).
|
|
18
|
-
- Automated packaging script (`package.php`) generating a ready-to-deploy ZIP archive (`fingerprint-
|
|
31
|
+
- Automated packaging script (`package.php`) generating a ready-to-deploy ZIP archive (`fingerprint-anti-bot.zip`).
|
|
19
32
|
- Cross-environment honeypot tripwires: immediately detects and drops automated scanners probing for non-WordPress entrypoints (Laravel, Symfony, Django, Spring Boot `/actuator`, phpMyAdmin, `.env`, `.git`).
|
|
20
33
|
- Admin dashboard security notice if running over unencrypted HTTP (warning about modern browsers disabling native `crypto.subtle` APIs in insecure contexts).
|
|
21
34
|
- 🔬 **JavaScript Prototype Tampering Detection (`detectTamperedPrototypes`)**:
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Advanced anti-bot library for Node.js using multi-layer fingerprinting (JA3, client-side, headers), behavioral analysis, and adaptive Proof-of-Work (PoW) challenges to mitigate scraping, scalping, and automated threats.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
Binary file
|
package/src/js/fingerprint.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
modPow,
|
|
21
21
|
hashNetwork,
|
|
22
22
|
normalizeReferer,
|
|
23
|
+
isLoopbackIp,
|
|
23
24
|
isPrivateIp,
|
|
24
25
|
parseUserAgent,
|
|
25
26
|
safeJsonStringify
|
|
@@ -2666,9 +2667,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2666
2667
|
|
|
2667
2668
|
// Check for known spoofed/suspicious JA4 fingerprints
|
|
2668
2669
|
const spoofedJa4s = [
|
|
2669
|
-
't13d1516h2_8daaf6152771_390237aa04be', // Chrome classique (curl-impersonate / tls-client)
|
|
2670
|
-
't13d1413h2_bc66258908f0_bc2531da1615', // Firefox statique (curl-impersonate-ff / curl_cffi)
|
|
2671
|
-
't13d1515h2_8daaf6152771_a729e2f67de4', // Safari statique (curl-impersonate-safari / tls-client)
|
|
2672
2670
|
't13d1516h2_8daaf6152771_4be0df930c2c', // Alternatif Chrome (tls-client Go)
|
|
2673
2671
|
't12d1516h2_8daaf6152771_390237aa04be', // Chrome usurpé dégradé en TLS 1.2
|
|
2674
2672
|
't13d1516h2_e822d36d892d_93ec3f0b2f5b' // Scraping bot OpenSSL customisé
|
|
@@ -3162,6 +3160,9 @@ function decaySubnetData(subnetData, now) {
|
|
|
3162
3160
|
* @param {number} finalScore The final suspicion score.
|
|
3163
3161
|
*/
|
|
3164
3162
|
async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
3163
|
+
if (!context.clientIp || isLoopbackIp(context.clientIp)) {
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3165
3166
|
const subnet = getIpSubnet(context.clientIp);
|
|
3166
3167
|
if (!subnet) return;
|
|
3167
3168
|
|
|
@@ -3194,7 +3195,8 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
3194
3195
|
|
|
3195
3196
|
// Utilisation d'un identifiant d'appareil stable (fingerprint matériel) plutôt que l'ID de cookie volatil
|
|
3196
3197
|
const currentDeviceHash = getCompositeDeviceHash(context);
|
|
3197
|
-
const
|
|
3198
|
+
const stablePart = extractStablePart(currentDeviceHash);
|
|
3199
|
+
const stableFpId = stablePart ? cyrb53(stablePart).toString() : (deviceId || cyrb53(currentDeviceHash).toString());
|
|
3198
3200
|
|
|
3199
3201
|
const currentDeviceContributions = subnetData.highScoreDevices[stableFpId] || 0;
|
|
3200
3202
|
if (currentDeviceContributions < 1) {
|
|
@@ -3212,7 +3214,9 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
3212
3214
|
|
|
3213
3215
|
const userAgent = context.headers?.['user-agent'] || '';
|
|
3214
3216
|
|
|
3215
|
-
|
|
3217
|
+
// Ne pas enregistrer une IP en attaquant si le score élevé provient uniquement d'un seul appareil isolé
|
|
3218
|
+
const isConfirmedClusterAttack = (subnetData.deviceIds.length > 1 && finalScore >= 70) || finalScore >= 95;
|
|
3219
|
+
if (isConfirmedClusterAttack) {
|
|
3216
3220
|
const existingAttacker = subnetData.attackerIps.find(a => a.ip === context.clientIp);
|
|
3217
3221
|
if (existingAttacker) {
|
|
3218
3222
|
existingAttacker.lastSeen = now;
|
|
@@ -3272,6 +3276,9 @@ export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPo
|
|
|
3272
3276
|
* @returns {Promise<{subnetScore: number}>}
|
|
3273
3277
|
*/
|
|
3274
3278
|
async function getSubnetScore(context, deviceId = '', securityConfig = null) {
|
|
3279
|
+
if (!context.clientIp || isLoopbackIp(context.clientIp)) {
|
|
3280
|
+
return { subnetScore: 0.0 };
|
|
3281
|
+
}
|
|
3275
3282
|
if (typeof deviceId === 'object' && deviceId !== null && !securityConfig) {
|
|
3276
3283
|
securityConfig = deviceId;
|
|
3277
3284
|
}
|
|
@@ -3299,7 +3306,7 @@ async function getSubnetScore(context, deviceId = '', securityConfig = null) {
|
|
|
3299
3306
|
}
|
|
3300
3307
|
let uaCount = subnetData.uas ? subnetData.uas.length : 1;
|
|
3301
3308
|
|
|
3302
|
-
if (deviceCount
|
|
3309
|
+
if (deviceCount <= 1 && highScoreCount <= 1) {
|
|
3303
3310
|
return { subnetScore: 0.0 };
|
|
3304
3311
|
}
|
|
3305
3312
|
|
|
@@ -3948,7 +3955,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3948
3955
|
_ // store.set result
|
|
3949
3956
|
] = await Promise.all([
|
|
3950
3957
|
getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
|
|
3951
|
-
getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3958
|
+
getThreatIntelScore(context, zkpY, securityConfig), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3952
3959
|
getTlsSpoofingScore(context),
|
|
3953
3960
|
getSubnetScore(context, deviceId, securityConfig),
|
|
3954
3961
|
getIpReputationScore(clientIp),
|
|
@@ -4383,15 +4390,27 @@ function parseGraphQLQuery(body) {
|
|
|
4383
4390
|
* @param {string} zkpY - The 'y' component of the ZKP proof (public key).
|
|
4384
4391
|
* @returns {Promise<{threatIntelScore: number}>}
|
|
4385
4392
|
*/
|
|
4386
|
-
async function getThreatIntelScore(context, zkpY) {
|
|
4387
|
-
let score = 0;
|
|
4393
|
+
async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
|
|
4394
|
+
let score = 0.0;
|
|
4395
|
+
const signals = [];
|
|
4396
|
+
|
|
4397
|
+
// 1. Détection déterministe : Clé publique ZKP bannie par consensus fédéré
|
|
4388
4398
|
if (zkpY) {
|
|
4389
4399
|
const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
|
|
4390
4400
|
if (isBanned) {
|
|
4391
|
-
score = 100;
|
|
4401
|
+
score = 100.0;
|
|
4402
|
+
signals.push({
|
|
4403
|
+
ruleId: 'FEDERATED_ZKP_BANNED',
|
|
4404
|
+
confidence: 1.0,
|
|
4405
|
+
score: 100.0,
|
|
4406
|
+
rationale: 'Cryptographic identity matched banned list consensus'
|
|
4407
|
+
});
|
|
4392
4408
|
}
|
|
4393
4409
|
}
|
|
4394
4410
|
|
|
4411
|
+
// 2. Détection physique : Modélisation continue du différentiel de transport (RTT TCP Edge vs Transit Applicatif)
|
|
4412
|
+
// Rationale : Un RTT TCP de socket très faible (datacenter/edge CDN) couplé à un délai applicatif WAN élevé
|
|
4413
|
+
// prouve l'interposition d'un relais/tunnel/proxy résidentiel entre le client réel et le point d'entrée.
|
|
4395
4414
|
const tcpRttHeader = context.headers?.['x-tcp-rtt'] || context.headers?.['x-real-rtt'];
|
|
4396
4415
|
const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
|
|
4397
4416
|
|
|
@@ -4399,24 +4418,40 @@ async function getThreatIntelScore(context, zkpY) {
|
|
|
4399
4418
|
if (behaviorHeader) {
|
|
4400
4419
|
try {
|
|
4401
4420
|
const metrics = JSON.parse(behaviorHeader);
|
|
4402
|
-
if (metrics && metrics.clientTimestamp && context.requestTimestamp) {
|
|
4421
|
+
if (metrics && typeof metrics.clientTimestamp === 'number' && typeof context.requestTimestamp === 'number') {
|
|
4403
4422
|
const appLatency = context.requestTimestamp - metrics.clientTimestamp;
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4423
|
+
|
|
4424
|
+
// Évaluation uniquement si la latence est mesurable et positive avec une mesure socket RTT valide
|
|
4425
|
+
if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0 && appLatency > 0) {
|
|
4426
|
+
// Marge de tolérance contre le jitter réseau et le scheduling JS (Garbage Collector, event-loop)
|
|
4427
|
+
const jitterAllowance = 60.0;
|
|
4428
|
+
const effectiveRtt = Math.max(tcpRtt, 5.0);
|
|
4429
|
+
const tunnelDelta = appLatency - (tcpRtt + jitterAllowance);
|
|
4430
|
+
const divergenceRatio = appLatency / effectiveRtt;
|
|
4431
|
+
|
|
4432
|
+
// Condition de disjonction : liaison edge ultra-proche (< 40ms) + transit applicatif au moins 3x supérieur
|
|
4433
|
+
if (tcpRtt <= 40 && divergenceRatio >= 3.0 && tunnelDelta > 0) {
|
|
4434
|
+
// Progression sigmoïdale continue calibrée sur la propagation optique intercontinentale (120ms)
|
|
4435
|
+
const scaling = 120.0;
|
|
4436
|
+
const ratioWeight = Math.min(1.0, (divergenceRatio - 3.0) / 5.0);
|
|
4437
|
+
const proxyScore = Math.min(95.0, 40.0 + 55.0 * Math.tanh(tunnelDelta / scaling) * ratioWeight);
|
|
4438
|
+
const finalProxyScore = Math.round(proxyScore * 10) / 10;
|
|
4439
|
+
|
|
4440
|
+
score = Math.max(score, finalProxyScore);
|
|
4441
|
+
signals.push({
|
|
4442
|
+
ruleId: 'RESIDENTIAL_PROXY_RTT_DISCREPANCY',
|
|
4443
|
+
confidence: Math.round(ratioWeight * 100) / 100,
|
|
4444
|
+
score: finalProxyScore,
|
|
4445
|
+
rationale: `TCP RTT (${tcpRtt}ms) diverges from application transit (${appLatency}ms) with ratio ${divergenceRatio.toFixed(1)}:1`
|
|
4446
|
+
});
|
|
4412
4447
|
}
|
|
4413
4448
|
}
|
|
4414
4449
|
}
|
|
4415
4450
|
} catch (e) {
|
|
4416
|
-
// Ignorer silencieusement
|
|
4451
|
+
// Ignorer silencieusement les erreurs de parsing des métriques
|
|
4417
4452
|
}
|
|
4418
4453
|
}
|
|
4419
|
-
return { threatIntelScore: score };
|
|
4454
|
+
return { threatIntelScore: score, threatIntelSignals: signals };
|
|
4420
4455
|
}
|
|
4421
4456
|
|
|
4422
4457
|
export class FingerprintEngine {
|
|
@@ -4603,6 +4638,9 @@ export class FingerprintEngine {
|
|
|
4603
4638
|
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
|
|
4604
4639
|
(suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
4605
4640
|
(suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) + // NOUVEAU: HTTP/2 Anomaly
|
|
4641
|
+
(suspicionVector.protocolAnomalyScore || 0) * (weights.protocolAnomalyScore || 0) +
|
|
4642
|
+
(suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
|
|
4643
|
+
(suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
|
|
4606
4644
|
(suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
4607
4645
|
(suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
|
|
4608
4646
|
|
|
@@ -4802,7 +4840,7 @@ export class FingerprintEngine {
|
|
|
4802
4840
|
const { whitelist = [] } = this.securityConfig;
|
|
4803
4841
|
const botRules = whitelist.filter(rule => rule.hostnameSuffix);
|
|
4804
4842
|
if (botRules.length === 0) {
|
|
4805
|
-
return
|
|
4843
|
+
return null;
|
|
4806
4844
|
}
|
|
4807
4845
|
|
|
4808
4846
|
const { clientIp, headers } = requestContext;
|
|
@@ -4818,7 +4856,7 @@ export class FingerprintEngine {
|
|
|
4818
4856
|
}
|
|
4819
4857
|
});
|
|
4820
4858
|
if (!matchedRule) {
|
|
4821
|
-
return
|
|
4859
|
+
return null;
|
|
4822
4860
|
}
|
|
4823
4861
|
|
|
4824
4862
|
const botName = matchedRule.userAgent; // e.g., 'Googlebot'
|
|
@@ -4885,6 +4923,13 @@ export class FingerprintEngine {
|
|
|
4885
4923
|
}
|
|
4886
4924
|
|
|
4887
4925
|
async processRequest(requestContext) {
|
|
4926
|
+
if (this.securityConfig?.reset) {
|
|
4927
|
+
const subnet = getIpSubnet(requestContext.clientIp);
|
|
4928
|
+
if (subnet) {
|
|
4929
|
+
await store.delete(`subnet:${subnet}`);
|
|
4930
|
+
}
|
|
4931
|
+
await store.delete(`ip-reputation:${requestContext.clientIp}`);
|
|
4932
|
+
}
|
|
4888
4933
|
sanitizeProxyHeaders(requestContext, this.securityConfig);
|
|
4889
4934
|
|
|
4890
4935
|
const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
@@ -157,6 +157,16 @@ export function hashNetwork(ip, prefix = 24) {
|
|
|
157
157
|
}
|
|
158
158
|
return hash.toString(16);
|
|
159
159
|
}
|
|
160
|
+
export function isLoopbackIp(ip) {
|
|
161
|
+
if (!ip || typeof ip !== 'string') return true;
|
|
162
|
+
let cleanIp = ip.trim().toLowerCase();
|
|
163
|
+
if (cleanIp.startsWith('::ffff:')) {
|
|
164
|
+
cleanIp = cleanIp.substring(7);
|
|
165
|
+
}
|
|
166
|
+
return cleanIp === '127.0.0.1' || cleanIp === '::1' || cleanIp === 'localhost' ||
|
|
167
|
+
cleanIp.startsWith('127.') || cleanIp === '0.0.0.0' || cleanIp === '::';
|
|
168
|
+
}
|
|
169
|
+
|
|
160
170
|
export function normalizeReferer(referer) {
|
|
161
171
|
try {
|
|
162
172
|
const url = new URL(referer);
|
|
@@ -167,11 +177,28 @@ export function normalizeReferer(referer) {
|
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
export function isPrivateIp(ip) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
if (!ip || typeof ip !== 'string') return false;
|
|
181
|
+
let cleanIp = ip.trim().toLowerCase();
|
|
182
|
+
if (cleanIp.startsWith('::ffff:')) {
|
|
183
|
+
cleanIp = cleanIp.substring(7);
|
|
184
|
+
}
|
|
185
|
+
if (cleanIp === '127.0.0.1' || cleanIp === '::1' || cleanIp === 'localhost' || cleanIp.startsWith('127.')) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
const parts = cleanIp.split('.');
|
|
189
|
+
if (parts.length === 4) {
|
|
190
|
+
const first = parseInt(parts[0], 10);
|
|
191
|
+
const second = parseInt(parts[1], 10);
|
|
192
|
+
if (first === 127 || first === 10 || first === 0) return true;
|
|
193
|
+
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
194
|
+
if (first === 192 && second === 168) return true;
|
|
195
|
+
if (first === 169 && second === 254) return true;
|
|
196
|
+
if (first === 100 && second >= 64 && second <= 127) return true;
|
|
197
|
+
}
|
|
198
|
+
if (cleanIp.startsWith('fc') || cleanIp.startsWith('fd') || cleanIp.startsWith('fe80:')) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
175
202
|
}
|
|
176
203
|
// Utility parsing functions
|
|
177
204
|
export function parseUserAgent(ua) {
|
|
@@ -1759,7 +1759,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1759
1759
|
};
|
|
1760
1760
|
|
|
1761
1761
|
const isVerified = await engine._verifyWhitelistedBot({ ...requestContext, fingerprint: {} });
|
|
1762
|
-
expect(isVerified).
|
|
1762
|
+
expect(isVerified).toBeNull();
|
|
1763
1763
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('[Fingerprint] Invalid regex in whitelist rule'));
|
|
1764
1764
|
consoleErrorSpy.mockRestore();
|
|
1765
1765
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { __internal, configureStore } from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
describe('Threat Intelligence - Rationale-Driven Defensive Logic', () => {
|
|
5
|
+
const inMemoryStore = {
|
|
6
|
+
_map: new Map(),
|
|
7
|
+
async get(key) { return this._map.get(key); },
|
|
8
|
+
async set(key, value) { this._map.set(key, value); },
|
|
9
|
+
async has(key) { return this._map.has(key); },
|
|
10
|
+
async delete(key) { this._map.delete(key); },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
inMemoryStore._map.clear();
|
|
15
|
+
configureStore(inMemoryStore);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should assign a deterministic score of 100 with audit trace for banned ZKP public keys', async () => {
|
|
19
|
+
await inMemoryStore.set('banned-zkp-y:banned-key-999', true);
|
|
20
|
+
|
|
21
|
+
const context = { headers: {} };
|
|
22
|
+
const result = await __internal.getSuspicionVector(context, {
|
|
23
|
+
weights: { threatIntelScore: 1.0 },
|
|
24
|
+
thresholds: { low: 20 }
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Direct check via internal function with banned key
|
|
28
|
+
const threatResult = await __internal.store.has('banned-zkp-y:banned-key-999');
|
|
29
|
+
expect(threatResult).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return score 0 for consistent local connections without proxy delay', async () => {
|
|
33
|
+
const reqTime = 1700000000000;
|
|
34
|
+
const context = {
|
|
35
|
+
requestTimestamp: reqTime,
|
|
36
|
+
headers: {
|
|
37
|
+
'x-tcp-rtt': '15',
|
|
38
|
+
'x-behavior-metrics': JSON.stringify({ clientTimestamp: reqTime - 25 })
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const vector = await __internal.getSuspicionVector(context, { weights: { threatIntelScore: 1.0 } });
|
|
43
|
+
expect(vector.threatIntelScore).toBe(0.0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should ignore mild JS event loop jitter and network lag without triggering false positives', async () => {
|
|
47
|
+
const reqTime = 1700000000000;
|
|
48
|
+
// App latency 65ms with a 20ms RTT falls well within the 60ms jitter tolerance window
|
|
49
|
+
const context = {
|
|
50
|
+
requestTimestamp: reqTime,
|
|
51
|
+
headers: {
|
|
52
|
+
'x-tcp-rtt': '20',
|
|
53
|
+
'x-behavior-metrics': JSON.stringify({ clientTimestamp: reqTime - 65 })
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const vector = await __internal.getSuspicionVector(context, { weights: { threatIntelScore: 1.0 } });
|
|
58
|
+
expect(vector.threatIntelScore).toBe(0.0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should smoothly calculate elevated suspicion when residential proxy tunneling is detected', async () => {
|
|
62
|
+
const reqTime = 1700000000000;
|
|
63
|
+
// Edge socket is 10ms (datacenter gateway), but application payload arrives with 280ms latency (WAN proxy hop)
|
|
64
|
+
const context = {
|
|
65
|
+
requestTimestamp: reqTime,
|
|
66
|
+
headers: {
|
|
67
|
+
'x-tcp-rtt': '10',
|
|
68
|
+
'x-behavior-metrics': JSON.stringify({ clientTimestamp: reqTime - 280 })
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const vector = await __internal.getSuspicionVector(context, { weights: { threatIntelScore: 1.0 } });
|
|
73
|
+
expect(vector.threatIntelScore).toBeGreaterThan(70.0);
|
|
74
|
+
expect(vector.threatIntelScore).toBeLessThanOrEqual(95.0);
|
|
75
|
+
});
|
|
76
|
+
});
|
package/src/php/AutoTuner.php
CHANGED
|
@@ -129,7 +129,8 @@ class AutoTuner
|
|
|
129
129
|
try {
|
|
130
130
|
call_user_func($this->onCleanup, $removed);
|
|
131
131
|
} catch (\Throwable $e) {
|
|
132
|
-
|
|
132
|
+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
|
|
133
|
+
$this->logError("[AutoTuning] Error in onCleanup callback: " . $e->getMessage());
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
}
|
|
@@ -155,24 +156,24 @@ class AutoTuner
|
|
|
155
156
|
|
|
156
157
|
if (count($sanitizedData) < $this->minDataPoints || !$hasEnoughSignal) {
|
|
157
158
|
if (count($sanitizedData) < $this->minDataPoints) {
|
|
158
|
-
|
|
159
|
+
$this->logMessage(sprintf("[AutoTuning] Postponed: %d/%d data points collected.\n", count($sanitizedData), $this->minDataPoints));
|
|
159
160
|
} else {
|
|
160
|
-
|
|
161
|
+
$this->logMessage(sprintf("[AutoTuning] Postponed: Insufficient confidence signals (Ratio: %.2f%% < %.2f%% and count: %d < %d).\n", $highConfidenceRatio * 100, $minConfidenceRatio * 100, $highConfidenceLogs, $minHighConfidenceCount));
|
|
161
162
|
}
|
|
162
163
|
return;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
if (count($this->trafficData) > $this->maxDataPoints) {
|
|
166
|
-
|
|
167
|
+
$this->logMessage(sprintf("[AutoTuning] Traffic log reached %d entries (max: %d). Truncating oldest logs.\n", count($this->trafficData), $this->maxDataPoints));
|
|
167
168
|
$this->trafficData = array_slice($this->trafficData, count($this->trafficData) - $this->maxDataPoints);
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
$this->logMessage(sprintf("[AutoTuning] Starting complete optimization cycle with %d sanitized data points.\n", count($sanitizedData)));
|
|
171
172
|
|
|
172
173
|
$paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData, 'currentConfig' => $this->securityConfig], []);
|
|
173
174
|
|
|
174
175
|
if (empty($paretoFront)) {
|
|
175
|
-
|
|
176
|
+
$this->logMessage("[AutoTuning] Optimization returned no solutions.\n");
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
178
179
|
|
|
@@ -192,7 +193,7 @@ class AutoTuner
|
|
|
192
193
|
|
|
193
194
|
$filteredFront = array_filter($paretoFront, fn ($p) => $isValidSecurityConfig($p['solution']));
|
|
194
195
|
if (empty($filteredFront)) {
|
|
195
|
-
|
|
196
|
+
$this->logMessage("[AutoTuning] Warning: All Pareto solutions violated sanity guardrails. Restoring raw front.\n");
|
|
196
197
|
$filteredFront = $paretoFront;
|
|
197
198
|
} else {
|
|
198
199
|
$filteredFront = array_values($filteredFront);
|
|
@@ -277,7 +278,8 @@ class AutoTuner
|
|
|
277
278
|
$validationTolerance = $this->securityConfig['autotuning']['validationTolerance'] ?? $this->validationTolerance;
|
|
278
279
|
|
|
279
280
|
if ($proposedFPR > $currentFPR + $validationTolerance || $proposedFNR > $currentFNR + $validationTolerance) {
|
|
280
|
-
|
|
281
|
+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system alert
|
|
282
|
+
$this->logError(sprintf(
|
|
281
283
|
"[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: %.4f (Current: %.4f), Proposed FNR: %.4f (Current: %.4f)",
|
|
282
284
|
$proposedFPR, $currentFPR, $proposedFNR, $currentFNR
|
|
283
285
|
));
|
|
@@ -300,22 +302,23 @@ class AutoTuner
|
|
|
300
302
|
|
|
301
303
|
self::$lastBestSolution = $bestSolution;
|
|
302
304
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
$this->logMessage("[AutoTuning] New optimized security configuration applied.\n");
|
|
306
|
+
$this->logMessage("[AutoTuning] Objectives achieved: " . json_encode([
|
|
305
307
|
'falsePositiveRate' => round($bestSolution['objectives'][0], 4),
|
|
306
308
|
'falseNegativeRate' => round($bestSolution['objectives'][1], 4)
|
|
307
|
-
]) . "\n";
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
]) . "\n");
|
|
310
|
+
$this->logMessage("[AutoTuning] New thresholds: " . json_encode($this->securityConfig['thresholds']) . "\n");
|
|
311
|
+
$this->logMessage("[AutoTuning] New weights: " . json_encode($this->securityConfig['weights']) . "\n");
|
|
312
|
+
$this->logMessage("[AutoTuning] New patterns: " . json_encode($this->securityConfig['patterns']) . "\n");
|
|
311
313
|
|
|
312
314
|
// Persist best configuration if savePath is configured
|
|
313
315
|
if ($this->savePath !== null) {
|
|
314
316
|
try {
|
|
315
317
|
file_put_contents($this->savePath, json_encode($bestSolution['solution'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
316
|
-
|
|
318
|
+
$this->logMessage("[AutoTuning] Best configuration saved to: {$this->savePath}\n");
|
|
317
319
|
} catch (\Throwable $e) {
|
|
318
|
-
|
|
320
|
+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
|
|
321
|
+
$this->logError("[AutoTuning] Error saving optimized configuration: " . $e->getMessage());
|
|
319
322
|
}
|
|
320
323
|
}
|
|
321
324
|
|
|
@@ -325,13 +328,28 @@ class AutoTuner
|
|
|
325
328
|
try {
|
|
326
329
|
call_user_func($this->onCleanup, $cleared);
|
|
327
330
|
} catch (\Throwable $e) {
|
|
328
|
-
|
|
331
|
+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
|
|
332
|
+
$this->logError("[AutoTuning] Error in onCleanup callback after clearing: " . $e->getMessage());
|
|
329
333
|
}
|
|
330
334
|
}
|
|
331
|
-
|
|
335
|
+
$this->logMessage(sprintf("[AutoTuning] Explicitly cleared %d processed traffic data points.\n", count($cleared)));
|
|
332
336
|
}
|
|
333
337
|
}
|
|
334
338
|
|
|
339
|
+
private function logMessage(string $message): void
|
|
340
|
+
{
|
|
341
|
+
if (PHP_SAPI === 'cli') {
|
|
342
|
+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI message output
|
|
343
|
+
echo $message;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private function logError(string $message): void
|
|
348
|
+
{
|
|
349
|
+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
|
|
350
|
+
error_log($message);
|
|
351
|
+
}
|
|
352
|
+
|
|
335
353
|
/**
|
|
336
354
|
* Returns the latest best solution found by the auto-tuner.
|
|
337
355
|
* @return array<string, mixed>|null
|