honeyweb-core 2.0.3 → 2.0.4
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/ai/gemini.js +9 -34
- package/ai/index.js +4 -27
- package/config/defaults.js +13 -36
- package/config/index.js +7 -35
- package/config/schema.js +1 -17
- package/detection/behavioral.js +26 -118
- package/detection/bot-detector.js +11 -62
- package/detection/index.js +19 -35
- package/detection/patterns.js +14 -51
- package/detection/rate-limiter.js +15 -86
- package/detection/traversal.js +42 -0
- package/index.js +1 -13
- package/middleware/blocklist-checker.js +1 -15
- package/middleware/dashboard.js +0 -17
- package/middleware/index.js +10 -46
- package/middleware/request-analyzer.js +29 -74
- package/middleware/trap-injector.js +3 -28
- package/package.json +1 -1
- package/storage/bot-tracker.js +9 -56
- package/storage/index.js +3 -12
- package/storage/json-store.js +15 -89
- package/storage/memory-store.js +9 -61
- package/utils/cache.js +5 -48
- package/utils/dns-verify.js +8 -45
- package/utils/event-emitter.js +7 -39
- package/utils/logger.js +9 -35
package/ai/gemini.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// honeyweb-core/ai/gemini.js
|
|
2
|
-
// Google Gemini API client
|
|
3
2
|
|
|
4
3
|
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
|
5
4
|
|
|
6
|
-
// Retry config
|
|
7
5
|
const MAX_RETRIES = 3;
|
|
8
6
|
const INITIAL_BACKOFF_MS = 2000;
|
|
9
7
|
|
|
@@ -11,7 +9,7 @@ class GeminiClient {
|
|
|
11
9
|
constructor(config) {
|
|
12
10
|
this.enabled = config.enabled && !!config.apiKey;
|
|
13
11
|
this.timeout = config.timeout || 50000;
|
|
14
|
-
this.modelName = config.model || 'gemini-
|
|
12
|
+
this.modelName = config.model || 'gemini-2.5-flash';
|
|
15
13
|
|
|
16
14
|
if (this.enabled) {
|
|
17
15
|
this.genAI = new GoogleGenerativeAI(config.apiKey);
|
|
@@ -20,37 +18,20 @@ class GeminiClient {
|
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
/**
|
|
24
|
-
* Sleep helper
|
|
25
|
-
*/
|
|
26
21
|
_sleep(ms) {
|
|
27
22
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
/**
|
|
31
|
-
* Check if an error is retryable
|
|
32
|
-
*/
|
|
33
25
|
_isRetryable(err) {
|
|
34
26
|
const msg = err.message || '';
|
|
35
|
-
return msg.includes('503')
|
|
36
|
-
|| msg.includes('
|
|
37
|
-
|| msg.includes('
|
|
38
|
-
|| msg.includes('Service Unavailable')
|
|
39
|
-
|| msg.includes('overloaded')
|
|
40
|
-
|| msg.includes('high demand')
|
|
41
|
-
|| msg.includes('RESOURCE_EXHAUSTED')
|
|
27
|
+
return msg.includes('503') || msg.includes('429') || msg.includes('500')
|
|
28
|
+
|| msg.includes('Service Unavailable') || msg.includes('overloaded')
|
|
29
|
+
|| msg.includes('high demand') || msg.includes('RESOURCE_EXHAUSTED')
|
|
42
30
|
|| msg.includes('Quota exceeded');
|
|
43
31
|
}
|
|
44
32
|
|
|
45
|
-
/**
|
|
46
|
-
* Generate AI analysis with retry logic
|
|
47
|
-
* @param {string} prompt - Analysis prompt
|
|
48
|
-
* @returns {Promise<string|null>} - AI response or null on failure
|
|
49
|
-
*/
|
|
50
33
|
async analyze(prompt) {
|
|
51
|
-
if (!this.enabled)
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
34
|
+
if (!this.enabled) return null;
|
|
54
35
|
|
|
55
36
|
let lastError = null;
|
|
56
37
|
|
|
@@ -66,18 +47,16 @@ class GeminiClient {
|
|
|
66
47
|
const response = await result.response;
|
|
67
48
|
const text = response.text();
|
|
68
49
|
|
|
69
|
-
if (text)
|
|
70
|
-
return text;
|
|
71
|
-
}
|
|
50
|
+
if (text) return text;
|
|
72
51
|
|
|
73
|
-
console.warn('[Gemini] Empty response
|
|
52
|
+
console.warn('[Gemini] Empty response, retrying...');
|
|
74
53
|
lastError = new Error('Empty AI response');
|
|
75
54
|
} catch (err) {
|
|
76
55
|
lastError = err;
|
|
77
56
|
|
|
78
57
|
if (this._isRetryable(err) && attempt < MAX_RETRIES - 1) {
|
|
79
58
|
const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
80
|
-
console.warn(`[Gemini] Retryable error (
|
|
59
|
+
console.warn(`[Gemini] Retryable error (${attempt + 1}/${MAX_RETRIES}): ${err.message}. Retry in ${backoff}ms`);
|
|
81
60
|
await this._sleep(backoff);
|
|
82
61
|
continue;
|
|
83
62
|
}
|
|
@@ -86,14 +65,10 @@ class GeminiClient {
|
|
|
86
65
|
}
|
|
87
66
|
}
|
|
88
67
|
|
|
89
|
-
console.error(`[Gemini]
|
|
68
|
+
console.error(`[Gemini] Failed after ${MAX_RETRIES} attempts:`, lastError?.message);
|
|
90
69
|
return null;
|
|
91
70
|
}
|
|
92
71
|
|
|
93
|
-
/**
|
|
94
|
-
* Check if AI is enabled
|
|
95
|
-
* @returns {boolean}
|
|
96
|
-
*/
|
|
97
72
|
isEnabled() {
|
|
98
73
|
return this.enabled;
|
|
99
74
|
}
|
package/ai/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// honeyweb-core/ai/index.js
|
|
2
|
-
// AI orchestrator
|
|
3
2
|
|
|
4
3
|
const GeminiClient = require('./gemini');
|
|
5
4
|
const { generateThreatAnalysisPrompt, generateTrapAnalysisPrompt } = require('./prompt-templates');
|
|
@@ -9,38 +8,16 @@ class AIAnalyzer {
|
|
|
9
8
|
this.client = new GeminiClient(config);
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
/**
|
|
13
|
-
* Analyze threat with AI
|
|
14
|
-
* @param {Object} data - Threat data
|
|
15
|
-
* @returns {Promise<string|null>} - AI analysis report
|
|
16
|
-
*/
|
|
17
11
|
async analyzeThreat(data) {
|
|
18
|
-
if (!this.client.isEnabled())
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const prompt = generateThreatAnalysisPrompt(data);
|
|
23
|
-
return await this.client.analyze(prompt);
|
|
12
|
+
if (!this.client.isEnabled()) return null;
|
|
13
|
+
return await this.client.analyze(generateThreatAnalysisPrompt(data));
|
|
24
14
|
}
|
|
25
15
|
|
|
26
|
-
/**
|
|
27
|
-
* Analyze trap trigger with AI
|
|
28
|
-
* @param {Object} data - Trap trigger data
|
|
29
|
-
* @returns {Promise<string|null>} - AI analysis report
|
|
30
|
-
*/
|
|
31
16
|
async analyzeTrap(data) {
|
|
32
|
-
if (!this.client.isEnabled())
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const prompt = generateTrapAnalysisPrompt(data);
|
|
37
|
-
return await this.client.analyze(prompt);
|
|
17
|
+
if (!this.client.isEnabled()) return null;
|
|
18
|
+
return await this.client.analyze(generateTrapAnalysisPrompt(data));
|
|
38
19
|
}
|
|
39
20
|
|
|
40
|
-
/**
|
|
41
|
-
* Check if AI is enabled
|
|
42
|
-
* @returns {boolean}
|
|
43
|
-
*/
|
|
44
21
|
isEnabled() {
|
|
45
22
|
return this.client.isEnabled();
|
|
46
23
|
}
|
package/config/defaults.js
CHANGED
|
@@ -1,55 +1,39 @@
|
|
|
1
|
-
// honeyweb-core/config/defaults.js
|
|
2
|
-
// Default configuration values extracted from hardcoded constants
|
|
3
|
-
|
|
4
1
|
module.exports = {
|
|
5
|
-
// AI Configuration
|
|
6
2
|
ai: {
|
|
7
3
|
enabled: true,
|
|
8
4
|
apiKey: process.env.HONEYWEB_API_KEY || '',
|
|
9
|
-
model: 'gemini-
|
|
5
|
+
model: 'gemini-2.5-flash',
|
|
10
6
|
timeout: 30000
|
|
11
7
|
},
|
|
12
|
-
|
|
13
|
-
// Detection Configuration
|
|
14
8
|
detection: {
|
|
15
|
-
patterns: {
|
|
16
|
-
enabled: true,
|
|
17
|
-
sqli: true,
|
|
18
|
-
xss: true
|
|
19
|
-
},
|
|
9
|
+
patterns: { enabled: true, sqli: true, xss: true },
|
|
20
10
|
behavioral: {
|
|
21
|
-
enabled: true,
|
|
11
|
+
enabled: true,
|
|
22
12
|
suspicionThreshold: 50,
|
|
23
13
|
trackTiming: true,
|
|
24
14
|
trackNavigation: true
|
|
25
15
|
},
|
|
26
16
|
whitelist: {
|
|
27
|
-
enabled: true,
|
|
28
|
-
verifyDNS: true,
|
|
29
|
-
cacheTTL: 86400000,
|
|
17
|
+
enabled: true,
|
|
18
|
+
verifyDNS: true,
|
|
19
|
+
cacheTTL: 86400000,
|
|
30
20
|
bots: ['Googlebot', 'Bingbot', 'Slackbot', 'facebookexternalhit']
|
|
31
21
|
}
|
|
32
22
|
},
|
|
33
|
-
|
|
34
|
-
// Rate Limiting
|
|
35
23
|
rateLimit: {
|
|
36
24
|
enabled: true,
|
|
37
|
-
window: 10000,
|
|
25
|
+
window: 10000,
|
|
38
26
|
maxRequests: 50,
|
|
39
|
-
cleanupInterval: 60000
|
|
27
|
+
cleanupInterval: 60000
|
|
40
28
|
},
|
|
41
|
-
|
|
42
|
-
// Storage Configuration
|
|
43
29
|
storage: {
|
|
44
|
-
type: 'json',
|
|
30
|
+
type: 'json',
|
|
45
31
|
path: './blocked-ips.json',
|
|
46
32
|
async: true,
|
|
47
33
|
cache: true,
|
|
48
|
-
banDuration:
|
|
34
|
+
banDuration: 50000,
|
|
49
35
|
autoCleanup: true
|
|
50
36
|
},
|
|
51
|
-
|
|
52
|
-
// Trap Configuration
|
|
53
37
|
traps: {
|
|
54
38
|
paths: [
|
|
55
39
|
'/admin-backup-v2',
|
|
@@ -58,22 +42,15 @@ module.exports = {
|
|
|
58
42
|
'/auth/root-access',
|
|
59
43
|
'/sys/config-safe'
|
|
60
44
|
],
|
|
61
|
-
injection: {
|
|
62
|
-
enabled: true,
|
|
63
|
-
invisibleLinks: true
|
|
64
|
-
}
|
|
45
|
+
injection: { enabled: true, invisibleLinks: true }
|
|
65
46
|
},
|
|
66
|
-
|
|
67
|
-
// Dashboard Configuration
|
|
68
47
|
dashboard: {
|
|
69
48
|
enabled: true,
|
|
70
49
|
path: '/honeyweb-status',
|
|
71
50
|
secret: process.env.HONEYWEB_DASHBOARD_SECRET || 'admin123'
|
|
72
51
|
},
|
|
73
|
-
|
|
74
|
-
// Logging Configuration
|
|
75
52
|
logging: {
|
|
76
|
-
level: 'info',
|
|
77
|
-
format: 'pretty'
|
|
53
|
+
level: 'info',
|
|
54
|
+
format: 'pretty'
|
|
78
55
|
}
|
|
79
56
|
};
|
package/config/index.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
// honeyweb-core/config/index.js
|
|
2
|
-
// Configuration loader with validation
|
|
3
2
|
|
|
4
3
|
const defaults = require('./defaults');
|
|
5
4
|
const { validateConfig } = require('./schema');
|
|
6
5
|
const path = require('path');
|
|
7
6
|
const fs = require('fs');
|
|
8
7
|
|
|
9
|
-
/**
|
|
10
|
-
* Deep merge two objects
|
|
11
|
-
*/
|
|
12
8
|
function deepMerge(target, source) {
|
|
13
9
|
const output = { ...target };
|
|
14
10
|
|
|
15
11
|
if (isObject(target) && isObject(source)) {
|
|
16
12
|
Object.keys(source).forEach(key => {
|
|
17
13
|
if (isObject(source[key])) {
|
|
18
|
-
|
|
19
|
-
output[key] = source[key];
|
|
20
|
-
} else {
|
|
21
|
-
output[key] = deepMerge(target[key], source[key]);
|
|
22
|
-
}
|
|
14
|
+
output[key] = key in target ? deepMerge(target[key], source[key]) : source[key];
|
|
23
15
|
} else {
|
|
24
16
|
output[key] = source[key];
|
|
25
17
|
}
|
|
@@ -33,17 +25,10 @@ function isObject(item) {
|
|
|
33
25
|
return item && typeof item === 'object' && !Array.isArray(item);
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
* Load configuration from multiple sources
|
|
38
|
-
* Priority: userConfig > configFile > envVars > defaults
|
|
39
|
-
*
|
|
40
|
-
* @param {Object} userConfig - Configuration passed programmatically
|
|
41
|
-
* @returns {Object} - Merged and validated configuration
|
|
42
|
-
*/
|
|
28
|
+
// Priority: userConfig > configFile > envVars > defaults
|
|
43
29
|
function loadConfig(userConfig = {}) {
|
|
44
30
|
let config = { ...defaults };
|
|
45
31
|
|
|
46
|
-
// 1. Try to load from config file (honeyweb.config.js)
|
|
47
32
|
const configPath = path.join(process.cwd(), 'honeyweb.config.js');
|
|
48
33
|
if (fs.existsSync(configPath)) {
|
|
49
34
|
try {
|
|
@@ -54,27 +39,14 @@ function loadConfig(userConfig = {}) {
|
|
|
54
39
|
}
|
|
55
40
|
}
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
if (process.env.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (process.env.
|
|
62
|
-
config.ai.model = process.env.HONEYWEB_AI_MODEL;
|
|
63
|
-
}
|
|
64
|
-
if (process.env.HONEYWEB_DASHBOARD_SECRET) {
|
|
65
|
-
config.dashboard.secret = process.env.HONEYWEB_DASHBOARD_SECRET;
|
|
66
|
-
}
|
|
67
|
-
if (process.env.HONEYWEB_LOG_LEVEL) {
|
|
68
|
-
config.logging.level = process.env.HONEYWEB_LOG_LEVEL;
|
|
69
|
-
}
|
|
70
|
-
if (process.env.HONEYWEB_STORAGE_PATH) {
|
|
71
|
-
config.storage.path = process.env.HONEYWEB_STORAGE_PATH;
|
|
72
|
-
}
|
|
42
|
+
if (process.env.HONEYWEB_API_KEY) config.ai.apiKey = process.env.HONEYWEB_API_KEY;
|
|
43
|
+
if (process.env.HONEYWEB_AI_MODEL) config.ai.model = process.env.HONEYWEB_AI_MODEL;
|
|
44
|
+
if (process.env.HONEYWEB_DASHBOARD_SECRET) config.dashboard.secret = process.env.HONEYWEB_DASHBOARD_SECRET;
|
|
45
|
+
if (process.env.HONEYWEB_LOG_LEVEL) config.logging.level = process.env.HONEYWEB_LOG_LEVEL;
|
|
46
|
+
if (process.env.HONEYWEB_STORAGE_PATH) config.storage.path = process.env.HONEYWEB_STORAGE_PATH;
|
|
73
47
|
|
|
74
|
-
// 3. Override with user-provided config (highest priority)
|
|
75
48
|
config = deepMerge(config, userConfig);
|
|
76
49
|
|
|
77
|
-
// 4. Validate configuration
|
|
78
50
|
const validation = validateConfig(config);
|
|
79
51
|
if (!validation.valid) {
|
|
80
52
|
throw new Error(`[HoneyWeb] Invalid configuration:\n${validation.errors.join('\n')}`);
|
package/config/schema.js
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
// honeyweb-core/config/schema.js
|
|
2
|
-
// Configuration validation schema
|
|
3
2
|
|
|
4
|
-
/**
|
|
5
|
-
* Validates configuration object
|
|
6
|
-
* @param {Object} config - Configuration to validate
|
|
7
|
-
* @returns {Object} - { valid: boolean, errors: string[] }
|
|
8
|
-
*/
|
|
9
3
|
function validateConfig(config) {
|
|
10
4
|
const errors = [];
|
|
11
5
|
|
|
12
|
-
// Validate AI config
|
|
13
6
|
if (config.ai) {
|
|
14
|
-
// Note: API key is optional - AI will be disabled at runtime if missing
|
|
15
7
|
if (config.ai.timeout && (typeof config.ai.timeout !== 'number' || config.ai.timeout < 0)) {
|
|
16
8
|
errors.push('AI timeout must be a positive number');
|
|
17
9
|
}
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
// Validate rate limit config
|
|
21
12
|
if (config.rateLimit) {
|
|
22
13
|
if (config.rateLimit.window && (typeof config.rateLimit.window !== 'number' || config.rateLimit.window <= 0)) {
|
|
23
14
|
errors.push('Rate limit window must be a positive number');
|
|
@@ -27,7 +18,6 @@ function validateConfig(config) {
|
|
|
27
18
|
}
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
// Validate storage config
|
|
31
21
|
if (config.storage) {
|
|
32
22
|
if (config.storage.type && !['json', 'memory'].includes(config.storage.type)) {
|
|
33
23
|
errors.push('Storage type must be "json" or "memory"');
|
|
@@ -37,7 +27,6 @@ function validateConfig(config) {
|
|
|
37
27
|
}
|
|
38
28
|
}
|
|
39
29
|
|
|
40
|
-
// Validate traps config
|
|
41
30
|
if (config.traps && config.traps.paths) {
|
|
42
31
|
if (!Array.isArray(config.traps.paths)) {
|
|
43
32
|
errors.push('Trap paths must be an array');
|
|
@@ -46,14 +35,12 @@ function validateConfig(config) {
|
|
|
46
35
|
}
|
|
47
36
|
}
|
|
48
37
|
|
|
49
|
-
// Validate dashboard config
|
|
50
38
|
if (config.dashboard) {
|
|
51
39
|
if (config.dashboard.enabled && !config.dashboard.path) {
|
|
52
40
|
errors.push('Dashboard path is required when dashboard is enabled');
|
|
53
41
|
}
|
|
54
42
|
}
|
|
55
43
|
|
|
56
|
-
// Validate logging config
|
|
57
44
|
if (config.logging) {
|
|
58
45
|
if (config.logging.level && !['debug', 'info', 'warn', 'error'].includes(config.logging.level)) {
|
|
59
46
|
errors.push('Logging level must be one of: debug, info, warn, error');
|
|
@@ -63,10 +50,7 @@ function validateConfig(config) {
|
|
|
63
50
|
}
|
|
64
51
|
}
|
|
65
52
|
|
|
66
|
-
return {
|
|
67
|
-
valid: errors.length === 0,
|
|
68
|
-
errors
|
|
69
|
-
};
|
|
53
|
+
return { valid: errors.length === 0, errors };
|
|
70
54
|
}
|
|
71
55
|
|
|
72
56
|
module.exports = { validateConfig };
|
package/detection/behavioral.js
CHANGED
|
@@ -1,186 +1,94 @@
|
|
|
1
|
-
// honeyweb-core/detection/behavioral.js
|
|
2
|
-
// Behavioral analysis to detect bot-like patterns
|
|
3
|
-
|
|
4
1
|
class BehavioralAnalyzer {
|
|
5
2
|
constructor(config) {
|
|
6
3
|
this.enabled = config.enabled !== false;
|
|
7
4
|
this.suspicionThreshold = config.suspicionThreshold || 50;
|
|
8
5
|
this.trackTiming = config.trackTiming !== false;
|
|
9
6
|
this.trackNavigation = config.trackNavigation !== false;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.sessions = new Map(); // ip -> { requests: [], pages: [], firstSeen: timestamp }
|
|
13
|
-
|
|
14
|
-
// Auto-cleanup old sessions every 5 minutes
|
|
15
|
-
this.cleanupInterval = setInterval(() => {
|
|
16
|
-
this._cleanupOldSessions();
|
|
17
|
-
}, 300000);
|
|
7
|
+
this.sessions = new Map();
|
|
8
|
+
this.cleanupInterval = setInterval(() => this._cleanupOldSessions(), 300000);
|
|
18
9
|
}
|
|
19
10
|
|
|
20
|
-
/**
|
|
21
|
-
* Analyze request for bot-like behavior
|
|
22
|
-
* @param {Object} req - Express request object
|
|
23
|
-
* @param {string} ip - Client IP address
|
|
24
|
-
* @returns {Object} - { suspicious: boolean, suspicionScore: number, reasons: string[] }
|
|
25
|
-
*/
|
|
26
11
|
analyze(req, ip) {
|
|
27
|
-
if (!this.enabled) {
|
|
28
|
-
return { suspicious: false, suspicionScore: 0, reasons: [] };
|
|
29
|
-
}
|
|
12
|
+
if (!this.enabled) return { suspicious: false, suspicionScore: 0, reasons: [] };
|
|
30
13
|
|
|
31
14
|
const now = Date.now();
|
|
32
15
|
const reasons = [];
|
|
33
16
|
let suspicionScore = 0;
|
|
34
17
|
|
|
35
|
-
// Get or create session
|
|
36
18
|
let session = this.sessions.get(ip);
|
|
37
19
|
if (!session) {
|
|
38
|
-
session = {
|
|
39
|
-
requests: [],
|
|
40
|
-
pages: [],
|
|
41
|
-
firstSeen: now
|
|
42
|
-
};
|
|
20
|
+
session = { requests: [], pages: [], firstSeen: now };
|
|
43
21
|
this.sessions.set(ip, session);
|
|
44
22
|
}
|
|
45
23
|
|
|
46
|
-
//
|
|
24
|
+
// Timing analysis
|
|
47
25
|
if (this.trackTiming && session.requests.length > 0) {
|
|
48
|
-
const
|
|
49
|
-
const timeSinceLastRequest = now - lastRequest;
|
|
26
|
+
const timeSinceLast = now - session.requests[session.requests.length - 1];
|
|
50
27
|
|
|
51
|
-
|
|
52
|
-
if (timeSinceLastRequest < 100) {
|
|
28
|
+
if (timeSinceLast < 100) {
|
|
53
29
|
reasons.push('Requests too fast (< 100ms interval)');
|
|
54
30
|
suspicionScore += 30;
|
|
55
31
|
}
|
|
56
32
|
|
|
57
|
-
// Check for consistent timing (bot pattern)
|
|
58
33
|
if (session.requests.length >= 5) {
|
|
59
34
|
const intervals = [];
|
|
60
35
|
for (let i = 1; i < session.requests.length; i++) {
|
|
61
36
|
intervals.push(session.requests[i] - session.requests[i - 1]);
|
|
62
37
|
}
|
|
38
|
+
const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
39
|
+
const variance = intervals.reduce((s, v) => s + Math.pow(v - avg, 2), 0) / intervals.length;
|
|
63
40
|
|
|
64
|
-
|
|
65
|
-
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
66
|
-
const variance = intervals.reduce((sum, interval) => {
|
|
67
|
-
return sum + Math.pow(interval - avgInterval, 2);
|
|
68
|
-
}, 0) / intervals.length;
|
|
69
|
-
|
|
70
|
-
// Low variance = consistent timing = bot
|
|
71
|
-
if (variance < 1000 && avgInterval < 2000) {
|
|
41
|
+
if (variance < 1000 && avg < 2000) {
|
|
72
42
|
reasons.push('Consistent request timing (bot pattern)');
|
|
73
43
|
suspicionScore += 25;
|
|
74
44
|
}
|
|
75
45
|
}
|
|
76
46
|
}
|
|
77
47
|
|
|
78
|
-
//
|
|
48
|
+
// Navigation analysis
|
|
79
49
|
if (this.trackNavigation) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Detect direct deep link access (skipping homepage)
|
|
83
|
-
if (session.pages.length === 0 && path !== '/' && !path.startsWith('/public')) {
|
|
50
|
+
if (session.pages.length === 0 && req.path !== '/' && !req.path.startsWith('/public')) {
|
|
84
51
|
reasons.push('Direct deep link access (skipped homepage)');
|
|
85
52
|
suspicionScore += 15;
|
|
86
53
|
}
|
|
87
54
|
|
|
88
|
-
|
|
89
|
-
const uniquePaths = new Set(session.pages);
|
|
90
|
-
if (uniquePaths.size > 10 && (now - session.firstSeen) < 10000) {
|
|
55
|
+
if (new Set(session.pages).size > 10 && (now - session.firstSeen) < 10000) {
|
|
91
56
|
reasons.push('Breadth-first crawling detected');
|
|
92
57
|
suspicionScore += 20;
|
|
93
58
|
}
|
|
94
59
|
|
|
95
|
-
session.pages.push(path);
|
|
60
|
+
session.pages.push(req.path);
|
|
96
61
|
}
|
|
97
62
|
|
|
98
|
-
//
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Short session with many requests (scraping)
|
|
103
|
-
if (sessionDuration < 5000 && requestCount > 10) {
|
|
63
|
+
// Session analysis
|
|
64
|
+
const duration = now - session.firstSeen;
|
|
65
|
+
if (duration < 5000 && session.requests.length > 10) {
|
|
104
66
|
reasons.push('High request rate in short session');
|
|
105
67
|
suspicionScore += 20;
|
|
106
68
|
}
|
|
107
|
-
|
|
108
|
-
// Very long session with consistent activity (persistent bot)
|
|
109
|
-
if (sessionDuration > 300000 && requestCount > 100) {
|
|
69
|
+
if (duration > 300000 && session.requests.length > 100) {
|
|
110
70
|
reasons.push('Persistent automated activity');
|
|
111
71
|
suspicionScore += 15;
|
|
112
72
|
}
|
|
113
73
|
|
|
114
|
-
// Record this request
|
|
115
74
|
session.requests.push(now);
|
|
75
|
+
if (session.requests.length > 20) session.requests = session.requests.slice(-20);
|
|
76
|
+
if (session.pages.length > 50) session.pages = session.pages.slice(-50);
|
|
116
77
|
|
|
117
|
-
|
|
118
|
-
if (session.requests.length > 20) {
|
|
119
|
-
session.requests = session.requests.slice(-20);
|
|
120
|
-
}
|
|
121
|
-
if (session.pages.length > 50) {
|
|
122
|
-
session.pages = session.pages.slice(-50);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Cap score at 100
|
|
126
|
-
suspicionScore = Math.min(100, suspicionScore);
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
suspicious: suspicionScore >= this.suspicionThreshold,
|
|
130
|
-
suspicionScore,
|
|
131
|
-
reasons
|
|
132
|
-
};
|
|
78
|
+
return { suspicious: Math.min(100, suspicionScore) >= this.suspicionThreshold, suspicionScore: Math.min(100, suspicionScore), reasons };
|
|
133
79
|
}
|
|
134
80
|
|
|
135
|
-
/**
|
|
136
|
-
* Clean up old sessions (> 1 hour)
|
|
137
|
-
* @private
|
|
138
|
-
*/
|
|
139
81
|
_cleanupOldSessions() {
|
|
140
82
|
const now = Date.now();
|
|
141
|
-
const maxAge = 3600000; // 1 hour
|
|
142
|
-
|
|
143
83
|
for (const [ip, session] of this.sessions.entries()) {
|
|
144
|
-
if (now - session.firstSeen >
|
|
145
|
-
this.sessions.delete(ip);
|
|
146
|
-
}
|
|
84
|
+
if (now - session.firstSeen > 3600000) this.sessions.delete(ip);
|
|
147
85
|
}
|
|
148
86
|
}
|
|
149
87
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
getStats() {
|
|
155
|
-
return {
|
|
156
|
-
activeSessions: this.sessions.size,
|
|
157
|
-
enabled: this.enabled
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Reset session for an IP
|
|
163
|
-
* @param {string} ip
|
|
164
|
-
*/
|
|
165
|
-
resetSession(ip) {
|
|
166
|
-
this.sessions.delete(ip);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Clear all sessions
|
|
171
|
-
*/
|
|
172
|
-
clear() {
|
|
173
|
-
this.sessions.clear();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Cleanup and stop timers
|
|
178
|
-
*/
|
|
179
|
-
destroy() {
|
|
180
|
-
if (this.cleanupInterval) {
|
|
181
|
-
clearInterval(this.cleanupInterval);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
88
|
+
getStats() { return { activeSessions: this.sessions.size, enabled: this.enabled }; }
|
|
89
|
+
resetSession(ip) { this.sessions.delete(ip); }
|
|
90
|
+
clear() { this.sessions.clear(); }
|
|
91
|
+
destroy() { if (this.cleanupInterval) clearInterval(this.cleanupInterval); }
|
|
184
92
|
}
|
|
185
93
|
|
|
186
94
|
module.exports = BehavioralAnalyzer;
|