honeyweb-core 1.0.2 → 2.0.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/ai/gemini.js +52 -0
- package/ai/index.js +49 -0
- package/ai/prompt-templates.js +64 -0
- package/config/defaults.js +79 -0
- package/config/index.js +83 -0
- package/config/schema.js +72 -0
- package/detection/behavioral.js +186 -0
- package/detection/bot-detector.js +140 -0
- package/detection/index.js +136 -0
- package/detection/patterns.js +83 -0
- package/detection/rate-limiter.js +109 -0
- package/detection/whitelist.js +116 -0
- package/index.js +57 -200
- package/middleware/blocklist-checker.js +34 -0
- package/middleware/dashboard.js +176 -0
- package/middleware/index.js +72 -0
- package/middleware/request-analyzer.js +111 -0
- package/middleware/trap-injector.js +52 -0
- package/package.json +27 -18
- package/storage/index.js +25 -0
- package/storage/json-store.js +206 -0
- package/storage/memory-store.js +128 -0
- package/utils/cache.js +106 -0
- package/utils/dns-verify.js +95 -0
- package/utils/event-emitter.js +65 -0
- package/utils/logger.js +88 -0
- package/blocked-ips.json +0 -0
- package/index V1.js +0 -85
package/ai/gemini.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// honeyweb-core/ai/gemini.js
|
|
2
|
+
// Google Gemini API client (extracted from main index.js)
|
|
3
|
+
|
|
4
|
+
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
|
5
|
+
|
|
6
|
+
class GeminiClient {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.enabled = config.enabled && config.apiKey;
|
|
9
|
+
this.timeout = config.timeout || 10000;
|
|
10
|
+
|
|
11
|
+
if (this.enabled) {
|
|
12
|
+
this.genAI = new GoogleGenerativeAI(config.apiKey);
|
|
13
|
+
this.model = this.genAI.getGenerativeModel({ model: config.model || 'gemini-2.0-flash-exp' });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate AI analysis
|
|
19
|
+
* @param {string} prompt - Analysis prompt
|
|
20
|
+
* @returns {Promise<string>} - AI response
|
|
21
|
+
*/
|
|
22
|
+
async analyze(prompt) {
|
|
23
|
+
if (!this.enabled) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await Promise.race([
|
|
29
|
+
this.model.generateContent(prompt),
|
|
30
|
+
new Promise((_, reject) =>
|
|
31
|
+
setTimeout(() => reject(new Error('AI request timeout')), this.timeout)
|
|
32
|
+
)
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const response = await result.response;
|
|
36
|
+
return response.text();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('[Gemini] AI analysis failed:', err.message);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if AI is enabled
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
isEnabled() {
|
|
48
|
+
return this.enabled;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = GeminiClient;
|
package/ai/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// honeyweb-core/ai/index.js
|
|
2
|
+
// AI orchestrator
|
|
3
|
+
|
|
4
|
+
const GeminiClient = require('./gemini');
|
|
5
|
+
const { generateThreatAnalysisPrompt, generateTrapAnalysisPrompt } = require('./prompt-templates');
|
|
6
|
+
|
|
7
|
+
class AIAnalyzer {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.client = new GeminiClient(config);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Analyze threat with AI
|
|
14
|
+
* @param {Object} data - Threat data
|
|
15
|
+
* @returns {Promise<string|null>} - AI analysis report
|
|
16
|
+
*/
|
|
17
|
+
async analyzeThreat(data) {
|
|
18
|
+
if (!this.client.isEnabled()) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const prompt = generateThreatAnalysisPrompt(data);
|
|
23
|
+
return await this.client.analyze(prompt);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Analyze trap trigger with AI
|
|
28
|
+
* @param {Object} data - Trap trigger data
|
|
29
|
+
* @returns {Promise<string|null>} - AI analysis report
|
|
30
|
+
*/
|
|
31
|
+
async analyzeTrap(data) {
|
|
32
|
+
if (!this.client.isEnabled()) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const prompt = generateTrapAnalysisPrompt(data);
|
|
37
|
+
return await this.client.analyze(prompt);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if AI is enabled
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
isEnabled() {
|
|
45
|
+
return this.client.isEnabled();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = AIAnalyzer;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// honeyweb-core/ai/prompt-templates.js
|
|
2
|
+
// AI prompt templates for threat analysis
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate threat analysis prompt
|
|
6
|
+
* @param {Object} data - Threat data
|
|
7
|
+
* @returns {string} - Formatted prompt
|
|
8
|
+
*/
|
|
9
|
+
function generateThreatAnalysisPrompt(data) {
|
|
10
|
+
const { ip, path, threats, userAgent, referer, method, timestamp } = data;
|
|
11
|
+
|
|
12
|
+
return `You are a cybersecurity analyst. Analyze this suspicious web request and provide a concise threat assessment.
|
|
13
|
+
|
|
14
|
+
**Request Details:**
|
|
15
|
+
- IP Address: ${ip}
|
|
16
|
+
- Path: ${path}
|
|
17
|
+
- Method: ${method}
|
|
18
|
+
- User-Agent: ${userAgent || 'Not provided'}
|
|
19
|
+
- Referer: ${referer || 'Not provided'}
|
|
20
|
+
- Timestamp: ${new Date(timestamp).toISOString()}
|
|
21
|
+
|
|
22
|
+
**Detected Threats:**
|
|
23
|
+
${threats.map(t => `- ${t}`).join('\n')}
|
|
24
|
+
|
|
25
|
+
**Analysis Required:**
|
|
26
|
+
1. What type of attack is this likely to be?
|
|
27
|
+
2. What is the attacker's probable intent?
|
|
28
|
+
3. What vulnerabilities are they trying to exploit?
|
|
29
|
+
4. Recommended defensive actions?
|
|
30
|
+
|
|
31
|
+
Provide a brief, actionable report (3-4 sentences).`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate trap trigger analysis prompt
|
|
36
|
+
* @param {Object} data - Trap trigger data
|
|
37
|
+
* @returns {string} - Formatted prompt
|
|
38
|
+
*/
|
|
39
|
+
function generateTrapAnalysisPrompt(data) {
|
|
40
|
+
const { ip, path, userAgent, timestamp } = data;
|
|
41
|
+
|
|
42
|
+
return `A honeypot trap was triggered. Analyze this bot behavior:
|
|
43
|
+
|
|
44
|
+
**Bot Details:**
|
|
45
|
+
- IP Address: ${ip}
|
|
46
|
+
- Trap Path: ${path}
|
|
47
|
+
- User-Agent: ${userAgent || 'Not provided'}
|
|
48
|
+
- Timestamp: ${new Date(timestamp).toISOString()}
|
|
49
|
+
|
|
50
|
+
**Context:**
|
|
51
|
+
The bot accessed an invisible honeypot link that legitimate users cannot see. This indicates automated crawling behavior.
|
|
52
|
+
|
|
53
|
+
**Analysis Required:**
|
|
54
|
+
1. What type of bot is this (scraper, vulnerability scanner, etc.)?
|
|
55
|
+
2. What is the bot's likely purpose?
|
|
56
|
+
3. Is this a sophisticated or basic bot?
|
|
57
|
+
|
|
58
|
+
Provide a brief assessment (2-3 sentences).`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
generateThreatAnalysisPrompt,
|
|
63
|
+
generateTrapAnalysisPrompt
|
|
64
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// honeyweb-core/config/defaults.js
|
|
2
|
+
// Default configuration values extracted from hardcoded constants
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
// AI Configuration
|
|
6
|
+
ai: {
|
|
7
|
+
enabled: true,
|
|
8
|
+
apiKey: process.env.HONEYWEB_API_KEY || '',
|
|
9
|
+
model: 'gemini-2.0-flash-exp',
|
|
10
|
+
timeout: 10000
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
// Detection Configuration
|
|
14
|
+
detection: {
|
|
15
|
+
patterns: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
sqli: true,
|
|
18
|
+
xss: true
|
|
19
|
+
},
|
|
20
|
+
behavioral: {
|
|
21
|
+
enabled: false, // Phase 2 feature
|
|
22
|
+
suspicionThreshold: 50,
|
|
23
|
+
trackTiming: true,
|
|
24
|
+
trackNavigation: true
|
|
25
|
+
},
|
|
26
|
+
whitelist: {
|
|
27
|
+
enabled: false, // Phase 2 feature
|
|
28
|
+
verifyDNS: true,
|
|
29
|
+
cacheTTL: 86400000, // 24 hours
|
|
30
|
+
bots: ['Googlebot', 'Bingbot', 'Slackbot', 'facebookexternalhit']
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Rate Limiting
|
|
35
|
+
rateLimit: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
window: 10000, // 10 seconds
|
|
38
|
+
maxRequests: 50,
|
|
39
|
+
cleanupInterval: 60000 // Auto-cleanup every 60s
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Storage Configuration
|
|
43
|
+
storage: {
|
|
44
|
+
type: 'json', // 'json' or 'memory'
|
|
45
|
+
path: './blocked-ips.json',
|
|
46
|
+
async: true,
|
|
47
|
+
cache: true,
|
|
48
|
+
banDuration: 86400000, // 24 hours
|
|
49
|
+
autoCleanup: true
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Trap Configuration
|
|
53
|
+
traps: {
|
|
54
|
+
paths: [
|
|
55
|
+
'/admin-backup-v2',
|
|
56
|
+
'/wp-login-hidden',
|
|
57
|
+
'/db-dump-2024',
|
|
58
|
+
'/auth/root-access',
|
|
59
|
+
'/sys/config-safe'
|
|
60
|
+
],
|
|
61
|
+
injection: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
invisibleLinks: true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Dashboard Configuration
|
|
68
|
+
dashboard: {
|
|
69
|
+
enabled: true,
|
|
70
|
+
path: '/honeyweb-status',
|
|
71
|
+
secret: process.env.HONEYWEB_DASHBOARD_SECRET || 'admin123'
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Logging Configuration
|
|
75
|
+
logging: {
|
|
76
|
+
level: 'info', // 'debug', 'info', 'warn', 'error'
|
|
77
|
+
format: 'pretty' // 'pretty' or 'json'
|
|
78
|
+
}
|
|
79
|
+
};
|
package/config/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// honeyweb-core/config/index.js
|
|
2
|
+
// Configuration loader with validation
|
|
3
|
+
|
|
4
|
+
const defaults = require('./defaults');
|
|
5
|
+
const { validateConfig } = require('./schema');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Deep merge two objects
|
|
11
|
+
*/
|
|
12
|
+
function deepMerge(target, source) {
|
|
13
|
+
const output = { ...target };
|
|
14
|
+
|
|
15
|
+
if (isObject(target) && isObject(source)) {
|
|
16
|
+
Object.keys(source).forEach(key => {
|
|
17
|
+
if (isObject(source[key])) {
|
|
18
|
+
if (!(key in target)) {
|
|
19
|
+
output[key] = source[key];
|
|
20
|
+
} else {
|
|
21
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
output[key] = source[key];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return output;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isObject(item) {
|
|
33
|
+
return item && typeof item === 'object' && !Array.isArray(item);
|
|
34
|
+
}
|
|
35
|
+
|
|
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
|
+
*/
|
|
43
|
+
function loadConfig(userConfig = {}) {
|
|
44
|
+
let config = { ...defaults };
|
|
45
|
+
|
|
46
|
+
// 1. Try to load from config file (honeyweb.config.js)
|
|
47
|
+
const configPath = path.join(process.cwd(), 'honeyweb.config.js');
|
|
48
|
+
if (fs.existsSync(configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const fileConfig = require(configPath);
|
|
51
|
+
config = deepMerge(config, fileConfig);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn('[HoneyWeb] Warning: Failed to load honeyweb.config.js:', err.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Override with environment variables
|
|
58
|
+
if (process.env.HONEYWEB_API_KEY) {
|
|
59
|
+
config.ai.apiKey = process.env.HONEYWEB_API_KEY;
|
|
60
|
+
}
|
|
61
|
+
if (process.env.HONEYWEB_DASHBOARD_SECRET) {
|
|
62
|
+
config.dashboard.secret = process.env.HONEYWEB_DASHBOARD_SECRET;
|
|
63
|
+
}
|
|
64
|
+
if (process.env.HONEYWEB_LOG_LEVEL) {
|
|
65
|
+
config.logging.level = process.env.HONEYWEB_LOG_LEVEL;
|
|
66
|
+
}
|
|
67
|
+
if (process.env.HONEYWEB_STORAGE_PATH) {
|
|
68
|
+
config.storage.path = process.env.HONEYWEB_STORAGE_PATH;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Override with user-provided config (highest priority)
|
|
72
|
+
config = deepMerge(config, userConfig);
|
|
73
|
+
|
|
74
|
+
// 4. Validate configuration
|
|
75
|
+
const validation = validateConfig(config);
|
|
76
|
+
if (!validation.valid) {
|
|
77
|
+
throw new Error(`[HoneyWeb] Invalid configuration:\n${validation.errors.join('\n')}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { loadConfig };
|
package/config/schema.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// honeyweb-core/config/schema.js
|
|
2
|
+
// Configuration validation schema
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates configuration object
|
|
6
|
+
* @param {Object} config - Configuration to validate
|
|
7
|
+
* @returns {Object} - { valid: boolean, errors: string[] }
|
|
8
|
+
*/
|
|
9
|
+
function validateConfig(config) {
|
|
10
|
+
const errors = [];
|
|
11
|
+
|
|
12
|
+
// Validate AI config
|
|
13
|
+
if (config.ai) {
|
|
14
|
+
// Note: API key is optional - AI will be disabled at runtime if missing
|
|
15
|
+
if (config.ai.timeout && (typeof config.ai.timeout !== 'number' || config.ai.timeout < 0)) {
|
|
16
|
+
errors.push('AI timeout must be a positive number');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Validate rate limit config
|
|
21
|
+
if (config.rateLimit) {
|
|
22
|
+
if (config.rateLimit.window && (typeof config.rateLimit.window !== 'number' || config.rateLimit.window <= 0)) {
|
|
23
|
+
errors.push('Rate limit window must be a positive number');
|
|
24
|
+
}
|
|
25
|
+
if (config.rateLimit.maxRequests && (typeof config.rateLimit.maxRequests !== 'number' || config.rateLimit.maxRequests <= 0)) {
|
|
26
|
+
errors.push('Rate limit maxRequests must be a positive number');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate storage config
|
|
31
|
+
if (config.storage) {
|
|
32
|
+
if (config.storage.type && !['json', 'memory'].includes(config.storage.type)) {
|
|
33
|
+
errors.push('Storage type must be "json" or "memory"');
|
|
34
|
+
}
|
|
35
|
+
if (config.storage.type === 'json' && !config.storage.path) {
|
|
36
|
+
errors.push('Storage path is required for JSON storage');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate traps config
|
|
41
|
+
if (config.traps && config.traps.paths) {
|
|
42
|
+
if (!Array.isArray(config.traps.paths)) {
|
|
43
|
+
errors.push('Trap paths must be an array');
|
|
44
|
+
} else if (config.traps.paths.length === 0) {
|
|
45
|
+
errors.push('At least one trap path is required');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate dashboard config
|
|
50
|
+
if (config.dashboard) {
|
|
51
|
+
if (config.dashboard.enabled && !config.dashboard.path) {
|
|
52
|
+
errors.push('Dashboard path is required when dashboard is enabled');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validate logging config
|
|
57
|
+
if (config.logging) {
|
|
58
|
+
if (config.logging.level && !['debug', 'info', 'warn', 'error'].includes(config.logging.level)) {
|
|
59
|
+
errors.push('Logging level must be one of: debug, info, warn, error');
|
|
60
|
+
}
|
|
61
|
+
if (config.logging.format && !['pretty', 'json'].includes(config.logging.format)) {
|
|
62
|
+
errors.push('Logging format must be "pretty" or "json"');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
valid: errors.length === 0,
|
|
68
|
+
errors
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { validateConfig };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// honeyweb-core/detection/behavioral.js
|
|
2
|
+
// Behavioral analysis to detect bot-like patterns
|
|
3
|
+
|
|
4
|
+
class BehavioralAnalyzer {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.enabled = config.enabled !== false;
|
|
7
|
+
this.suspicionThreshold = config.suspicionThreshold || 50;
|
|
8
|
+
this.trackTiming = config.trackTiming !== false;
|
|
9
|
+
this.trackNavigation = config.trackNavigation !== false;
|
|
10
|
+
|
|
11
|
+
// Track sessions per IP
|
|
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);
|
|
18
|
+
}
|
|
19
|
+
|
|
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
|
+
analyze(req, ip) {
|
|
27
|
+
if (!this.enabled) {
|
|
28
|
+
return { suspicious: false, suspicionScore: 0, reasons: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const reasons = [];
|
|
33
|
+
let suspicionScore = 0;
|
|
34
|
+
|
|
35
|
+
// Get or create session
|
|
36
|
+
let session = this.sessions.get(ip);
|
|
37
|
+
if (!session) {
|
|
38
|
+
session = {
|
|
39
|
+
requests: [],
|
|
40
|
+
pages: [],
|
|
41
|
+
firstSeen: now
|
|
42
|
+
};
|
|
43
|
+
this.sessions.set(ip, session);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. TIMING ANALYSIS
|
|
47
|
+
if (this.trackTiming && session.requests.length > 0) {
|
|
48
|
+
const lastRequest = session.requests[session.requests.length - 1];
|
|
49
|
+
const timeSinceLastRequest = now - lastRequest;
|
|
50
|
+
|
|
51
|
+
// Too fast (< 100ms between requests)
|
|
52
|
+
if (timeSinceLastRequest < 100) {
|
|
53
|
+
reasons.push('Requests too fast (< 100ms interval)');
|
|
54
|
+
suspicionScore += 30;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for consistent timing (bot pattern)
|
|
58
|
+
if (session.requests.length >= 5) {
|
|
59
|
+
const intervals = [];
|
|
60
|
+
for (let i = 1; i < session.requests.length; i++) {
|
|
61
|
+
intervals.push(session.requests[i] - session.requests[i - 1]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Calculate variance
|
|
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) {
|
|
72
|
+
reasons.push('Consistent request timing (bot pattern)');
|
|
73
|
+
suspicionScore += 25;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. NAVIGATION ANALYSIS
|
|
79
|
+
if (this.trackNavigation) {
|
|
80
|
+
const path = req.path;
|
|
81
|
+
|
|
82
|
+
// Detect direct deep link access (skipping homepage)
|
|
83
|
+
if (session.pages.length === 0 && path !== '/' && !path.startsWith('/public')) {
|
|
84
|
+
reasons.push('Direct deep link access (skipped homepage)');
|
|
85
|
+
suspicionScore += 15;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Detect breadth-first crawling (accessing many different paths quickly)
|
|
89
|
+
const uniquePaths = new Set(session.pages);
|
|
90
|
+
if (uniquePaths.size > 10 && (now - session.firstSeen) < 10000) {
|
|
91
|
+
reasons.push('Breadth-first crawling detected');
|
|
92
|
+
suspicionScore += 20;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
session.pages.push(path);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3. SESSION ANALYSIS
|
|
99
|
+
const sessionDuration = now - session.firstSeen;
|
|
100
|
+
const requestCount = session.requests.length;
|
|
101
|
+
|
|
102
|
+
// Short session with many requests (scraping)
|
|
103
|
+
if (sessionDuration < 5000 && requestCount > 10) {
|
|
104
|
+
reasons.push('High request rate in short session');
|
|
105
|
+
suspicionScore += 20;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Very long session with consistent activity (persistent bot)
|
|
109
|
+
if (sessionDuration > 300000 && requestCount > 100) {
|
|
110
|
+
reasons.push('Persistent automated activity');
|
|
111
|
+
suspicionScore += 15;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Record this request
|
|
115
|
+
session.requests.push(now);
|
|
116
|
+
|
|
117
|
+
// Keep only last 20 requests to prevent memory bloat
|
|
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
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Clean up old sessions (> 1 hour)
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
_cleanupOldSessions() {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const maxAge = 3600000; // 1 hour
|
|
142
|
+
|
|
143
|
+
for (const [ip, session] of this.sessions.entries()) {
|
|
144
|
+
if (now - session.firstSeen > maxAge) {
|
|
145
|
+
this.sessions.delete(ip);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get statistics
|
|
152
|
+
* @returns {Object}
|
|
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
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = BehavioralAnalyzer;
|