honeyweb-core 2.0.0 → 2.0.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/ai/gemini.js CHANGED
@@ -1,43 +1,93 @@
1
1
  // honeyweb-core/ai/gemini.js
2
- // Google Gemini API client (extracted from main index.js)
2
+ // Google Gemini API client
3
3
 
4
4
  const { GoogleGenerativeAI } = require('@google/generative-ai');
5
5
 
6
+ // Retry config
7
+ const MAX_RETRIES = 3;
8
+ const INITIAL_BACKOFF_MS = 2000;
9
+
6
10
  class GeminiClient {
7
11
  constructor(config) {
8
- this.enabled = config.enabled && config.apiKey;
9
- this.timeout = config.timeout || 10000;
12
+ this.enabled = config.enabled && !!config.apiKey;
13
+ this.timeout = config.timeout || 50000;
14
+ this.modelName = config.model || 'gemini-3-flash-preview';
10
15
 
11
16
  if (this.enabled) {
12
17
  this.genAI = new GoogleGenerativeAI(config.apiKey);
13
- this.model = this.genAI.getGenerativeModel({ model: config.model || 'gemini-2.0-flash-exp' });
18
+ this.model = this.genAI.getGenerativeModel({ model: this.modelName });
19
+ console.log(`[Gemini] Initialized with model: ${this.modelName}`);
14
20
  }
15
21
  }
16
22
 
17
23
  /**
18
- * Generate AI analysis
24
+ * Sleep helper
25
+ */
26
+ _sleep(ms) {
27
+ return new Promise(resolve => setTimeout(resolve, ms));
28
+ }
29
+
30
+ /**
31
+ * Check if an error is retryable
32
+ */
33
+ _isRetryable(err) {
34
+ const msg = err.message || '';
35
+ return msg.includes('503')
36
+ || msg.includes('429')
37
+ || msg.includes('500')
38
+ || msg.includes('Service Unavailable')
39
+ || msg.includes('overloaded')
40
+ || msg.includes('high demand')
41
+ || msg.includes('RESOURCE_EXHAUSTED')
42
+ || msg.includes('Quota exceeded');
43
+ }
44
+
45
+ /**
46
+ * Generate AI analysis with retry logic
19
47
  * @param {string} prompt - Analysis prompt
20
- * @returns {Promise<string>} - AI response
48
+ * @returns {Promise<string|null>} - AI response or null on failure
21
49
  */
22
50
  async analyze(prompt) {
23
51
  if (!this.enabled) {
24
52
  return null;
25
53
  }
26
54
 
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;
55
+ let lastError = null;
56
+
57
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
58
+ try {
59
+ const result = await Promise.race([
60
+ this.model.generateContent(prompt),
61
+ new Promise((_, reject) =>
62
+ setTimeout(() => reject(new Error('AI request timeout')), this.timeout)
63
+ )
64
+ ]);
65
+
66
+ const response = await result.response;
67
+ const text = response.text();
68
+
69
+ if (text) {
70
+ return text;
71
+ }
72
+
73
+ console.warn('[Gemini] Empty response from AI, retrying...');
74
+ lastError = new Error('Empty AI response');
75
+ } catch (err) {
76
+ lastError = err;
77
+
78
+ if (this._isRetryable(err) && attempt < MAX_RETRIES - 1) {
79
+ const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
80
+ console.warn(`[Gemini] Retryable error (attempt ${attempt + 1}/${MAX_RETRIES}): ${err.message}. Retrying in ${backoff}ms...`);
81
+ await this._sleep(backoff);
82
+ continue;
83
+ }
84
+
85
+ break;
86
+ }
40
87
  }
88
+
89
+ console.error(`[Gemini] AI analysis failed after ${MAX_RETRIES} attempts:`, lastError?.message);
90
+ return null;
41
91
  }
42
92
 
43
93
  /**
@@ -1,61 +1,41 @@
1
1
  // honeyweb-core/ai/prompt-templates.js
2
2
  // AI prompt templates for threat analysis
3
3
 
4
- /**
5
- * Generate threat analysis prompt
6
- * @param {Object} data - Threat data
7
- * @returns {string} - Formatted prompt
8
- */
9
4
  function generateThreatAnalysisPrompt(data) {
10
- const { ip, path, threats, userAgent, referer, method, timestamp } = data;
5
+ const { ip, path, threats, userAgent, method, timestamp } = data;
11
6
 
12
- return `You are a cybersecurity analyst. Analyze this suspicious web request and provide a concise threat assessment.
7
+ return `Cybersecurity analyst for HoneyWeb honeypot. Analyze this attack. Plain text only, no markdown or asterisks. Use UPPERCASE headers.
13
8
 
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()}
9
+ REQUEST: ${method} ${path} from ${ip}
10
+ User-Agent: ${userAgent || 'N/A'}
11
+ Time: ${new Date(timestamp).toISOString()}
12
+ Threats: ${threats.join('; ')}
21
13
 
22
- **Detected Threats:**
23
- ${threats.map(t => `- ${t}`).join('\n')}
14
+ Respond with:
15
+ CLASSIFICATION: Attack type and technique.
16
+ RISK: Severity (CRITICAL/HIGH/MEDIUM/LOW), impact if successful.
17
+ PROFILE: Skill level, likely tool, automated or manual.
18
+ ACTIONS: Immediate and long-term defenses.
24
19
 
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).`;
20
+ Keep to 4-6 sentences total.`;
32
21
  }
33
22
 
34
- /**
35
- * Generate trap trigger analysis prompt
36
- * @param {Object} data - Trap trigger data
37
- * @returns {string} - Formatted prompt
38
- */
39
23
  function generateTrapAnalysisPrompt(data) {
40
24
  const { ip, path, userAgent, timestamp } = data;
41
25
 
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()}
26
+ return `Cybersecurity analyst for HoneyWeb honeypot. A hidden trap link (invisible to humans) was accessed. Plain text only, no markdown or asterisks. Use UPPERCASE headers.
49
27
 
50
- **Context:**
51
- The bot accessed an invisible honeypot link that legitimate users cannot see. This indicates automated crawling behavior.
28
+ TRAP HIT: ${path} from ${ip}
29
+ User-Agent: ${userAgent || 'N/A'}
30
+ Time: ${new Date(timestamp).toISOString()}
52
31
 
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?
32
+ Respond with:
33
+ BOT TYPE: What kind of bot and why.
34
+ SOPHISTICATION: BASIC/INTERMEDIATE/ADVANCED with reason.
35
+ THREAT: Severity (CRITICAL/HIGH/MEDIUM/LOW), malicious or benign.
36
+ ACTIONS: Recommended next steps.
57
37
 
58
- Provide a brief assessment (2-3 sentences).`;
38
+ Keep to 3-5 sentences total.`;
59
39
  }
60
40
 
61
41
  module.exports = {
@@ -6,8 +6,8 @@ module.exports = {
6
6
  ai: {
7
7
  enabled: true,
8
8
  apiKey: process.env.HONEYWEB_API_KEY || '',
9
- model: 'gemini-2.0-flash-exp',
10
- timeout: 10000
9
+ model: 'gemini-3-flash-preview',
10
+ timeout: 30000
11
11
  },
12
12
 
13
13
  // Detection Configuration
@@ -18,14 +18,14 @@ module.exports = {
18
18
  xss: true
19
19
  },
20
20
  behavioral: {
21
- enabled: false, // Phase 2 feature
21
+ enabled: true, // Phase 2 feature - ENABLED
22
22
  suspicionThreshold: 50,
23
23
  trackTiming: true,
24
24
  trackNavigation: true
25
25
  },
26
26
  whitelist: {
27
- enabled: false, // Phase 2 feature
28
- verifyDNS: true,
27
+ enabled: true, // Phase 2 feature - ENABLED
28
+ verifyDNS: true, // DNS verification ENABLED
29
29
  cacheTTL: 86400000, // 24 hours
30
30
  bots: ['Googlebot', 'Bingbot', 'Slackbot', 'facebookexternalhit']
31
31
  }
@@ -45,7 +45,7 @@ module.exports = {
45
45
  path: './blocked-ips.json',
46
46
  async: true,
47
47
  cache: true,
48
- banDuration: 86400000, // 24 hours
48
+ banDuration: 300000, // 24 hours
49
49
  autoCleanup: true
50
50
  },
51
51
 
package/config/index.js CHANGED
@@ -58,6 +58,9 @@ function loadConfig(userConfig = {}) {
58
58
  if (process.env.HONEYWEB_API_KEY) {
59
59
  config.ai.apiKey = process.env.HONEYWEB_API_KEY;
60
60
  }
61
+ if (process.env.HONEYWEB_AI_MODEL) {
62
+ config.ai.model = process.env.HONEYWEB_AI_MODEL;
63
+ }
61
64
  if (process.env.HONEYWEB_DASHBOARD_SECRET) {
62
65
  config.dashboard.secret = process.env.HONEYWEB_DASHBOARD_SECRET;
63
66
  }
package/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const { loadConfig } = require('./config');
6
6
  const { createStorage } = require('./storage');
7
+ const BotTracker = require('./storage/bot-tracker');
7
8
  const DetectionEngine = require('./detection');
8
9
  const AIAnalyzer = require('./ai');
9
10
  const Logger = require('./utils/logger');
@@ -23,11 +24,12 @@ function honeyWeb(userConfig = {}) {
23
24
  const logger = new Logger(config.logging);
24
25
  const events = new HoneyWebEvents();
25
26
  const storage = createStorage(config.storage);
27
+ const botTracker = new BotTracker(); // Track legitimate bot visits
26
28
  const detector = new DetectionEngine(config);
27
29
  const aiAnalyzer = new AIAnalyzer(config.ai);
28
30
 
29
31
  // 3. Create middleware
30
- const middleware = createMiddleware(config, storage, detector, aiAnalyzer, logger, events);
32
+ const middleware = createMiddleware(config, storage, botTracker, detector, aiAnalyzer, logger, events);
31
33
 
32
34
  // 4. Attach event emitter and utilities to middleware function
33
35
  middleware.on = events.on.bind(events);
@@ -35,6 +37,7 @@ function honeyWeb(userConfig = {}) {
35
37
  middleware.off = events.off.bind(events);
36
38
  middleware.events = events;
37
39
  middleware.storage = storage;
40
+ middleware.botTracker = botTracker;
38
41
  middleware.detector = detector;
39
42
  middleware.logger = logger;
40
43
  middleware.config = config;
@@ -1,169 +1,291 @@
1
1
  // honeyweb-core/middleware/dashboard.js
2
- // Dashboard middleware (extracted from main index.js)
3
-
4
- /**
5
- * Create dashboard middleware
6
- * @param {Object} config - Configuration
7
- * @param {Object} storage - Storage instance
8
- * @param {Object} detector - Detection engine
9
- * @returns {Function} - Middleware function
10
- */
11
- function createDashboard(config, storage, detector) {
2
+ // Dashboard middleware with cookie-based auth (no credentials in URL)
3
+
4
+ const crypto = require('crypto');
5
+
6
+ function createDashboard(config, storage, botTracker, detector, events) {
12
7
  const dashboardPath = config.dashboard.path;
13
8
  const dashboardSecret = config.dashboard.secret;
9
+ const sessionToken = crypto.createHash('sha256').update(dashboardSecret + ':honeyweb-session').digest('hex');
10
+
11
+ const aiReports = [];
12
+ const MAX_AI_REPORTS = 20;
13
+
14
+ if (events && events.on) {
15
+ events.on('ai:analysis', ({ ip, report, timestamp }) => {
16
+ aiReports.unshift({ ip, report, timestamp });
17
+ if (aiReports.length > MAX_AI_REPORTS) aiReports.pop();
18
+ });
19
+ }
20
+
21
+ function isAuthenticated(req) {
22
+ const cookieHeader = req.headers.cookie || '';
23
+ const match = cookieHeader.match(/honeyweb_session=([^;]+)/);
24
+ return match && match[1] === sessionToken;
25
+ }
26
+
27
+ function renderLoginPage(res, error) {
28
+ const errorHtml = error
29
+ ? `<div class="error-msg">${error}</div>`
30
+ : '';
31
+ res.send(`<!DOCTYPE html>
32
+ <html lang="en">
33
+ <head>
34
+ <meta charset="UTF-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
+ <title>HoneyWeb &mdash; Dashboard Login</title>
37
+ <style>
38
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
39
+ body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#0f1923;color:#c9d1d9;display:flex;justify-content:center;align-items:center;min-height:100vh}
40
+ .card{background:#161b22;border:1px solid #21262d;border-radius:12px;padding:44px 36px;width:380px;text-align:center}
41
+ .card h1{font-size:1.5em;color:#e6edf3;margin-bottom:4px}
42
+ .card .sub{color:#8b949e;font-size:0.85em;margin-bottom:28px}
43
+ .error-msg{background:rgba(248,81,73,0.1);border:1px solid #f85149;color:#f85149;border-radius:6px;padding:10px 14px;margin-bottom:18px;font-size:0.88em}
44
+ input[type="password"]{width:100%;padding:12px 14px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:0.95em;transition:border-color .2s}
45
+ input[type="password"]:focus{outline:none;border-color:#58a6ff;box-shadow:0 0 0 3px rgba(88,166,255,0.15)}
46
+ input[type="password"]::placeholder{color:#484f58}
47
+ button{width:100%;padding:13px;background:#238636;color:#fff;border:none;border-radius:6px;font-size:1em;font-weight:600;cursor:pointer;margin-top:14px;transition:background .2s}
48
+ button:hover{background:#2ea043}
49
+ .footer-note{margin-top:20px;font-size:0.72em;color:#484f58}
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div class="card">
54
+ <h1>HoneyWeb Dashboard</h1>
55
+ <p class="sub">Enter the dashboard secret to continue</p>
56
+ ${errorHtml}
57
+ <form method="POST" action="${dashboardPath}">
58
+ <input type="password" name="secret" placeholder="Dashboard secret" autofocus required>
59
+ <button type="submit">Authenticate</button>
60
+ </form>
61
+ <p class="footer-note">Session expires after 1 hour</p>
62
+ </div>
63
+ </body>
64
+ </html>`);
65
+ }
14
66
 
15
67
  return async function dashboard(req, res, next) {
16
- // Only handle dashboard path
17
- if (req.path !== dashboardPath) {
18
- return next();
68
+ if (req.path !== dashboardPath) return next();
69
+
70
+ // POST login
71
+ if (req.method === 'POST') {
72
+ const submitted = req.body && req.body.secret;
73
+ if (submitted === dashboardSecret) {
74
+ res.setHeader('Set-Cookie', `honeyweb_session=${sessionToken}; HttpOnly; Path=${dashboardPath}; SameSite=Strict; Max-Age=3600`);
75
+ return res.redirect(303, dashboardPath);
76
+ }
77
+ return renderLoginPage(res, 'Invalid secret. Try again.');
19
78
  }
20
79
 
21
- // Check authentication
22
- const key = req.query.key;
23
- if (key !== dashboardSecret) {
24
- return res.status(401).send('<h1>401 Unauthorized</h1><p>Invalid dashboard key.</p>');
80
+ // Legacy ?key= support (redirect to clean URL)
81
+ if (req.query.key) {
82
+ if (req.query.key === dashboardSecret) {
83
+ res.setHeader('Set-Cookie', `honeyweb_session=${sessionToken}; HttpOnly; Path=${dashboardPath}; SameSite=Strict; Max-Age=3600`);
84
+ return res.redirect(303, dashboardPath);
85
+ }
86
+ return res.status(401).send('<h1>401 Unauthorized</h1>');
25
87
  }
26
88
 
89
+ if (!isAuthenticated(req)) return renderLoginPage(res);
90
+
27
91
  try {
28
- // Get banned IPs
29
92
  const bannedIPs = await storage.getAll();
30
93
  const stats = await storage.getStats();
31
94
  const detectionStats = detector.getStats();
95
+ const botVisits = botTracker.getAllVisits();
96
+ const botStats = botTracker.getStats();
32
97
 
33
- // Generate HTML dashboard
34
- const html = `
35
- <!DOCTYPE html>
36
- <html>
98
+ const html = `<!DOCTYPE html>
99
+ <html lang="en">
37
100
  <head>
101
+ <meta charset="UTF-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
38
103
  <title>HoneyWeb Status Dashboard</title>
104
+ <meta http-equiv="refresh" content="10">
39
105
  <style>
40
- body {
41
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
42
- max-width: 1200px;
43
- margin: 40px auto;
44
- padding: 20px;
45
- background: #f5f5f5;
46
- }
47
- h1 {
48
- color: #333;
49
- border-bottom: 3px solid #ff6b6b;
50
- padding-bottom: 10px;
51
- }
52
- .stats {
53
- display: grid;
54
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
55
- gap: 20px;
56
- margin: 20px 0;
57
- }
58
- .stat-card {
59
- background: white;
60
- padding: 20px;
61
- border-radius: 8px;
62
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
63
- }
64
- .stat-card h3 {
65
- margin: 0 0 10px 0;
66
- color: #666;
67
- font-size: 14px;
68
- text-transform: uppercase;
69
- }
70
- .stat-card .value {
71
- font-size: 32px;
72
- font-weight: bold;
73
- color: #ff6b6b;
74
- }
75
- table {
76
- width: 100%;
77
- background: white;
78
- border-collapse: collapse;
79
- border-radius: 8px;
80
- overflow: hidden;
81
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
82
- }
83
- th {
84
- background: #ff6b6b;
85
- color: white;
86
- padding: 12px;
87
- text-align: left;
88
- }
89
- td {
90
- padding: 12px;
91
- border-bottom: 1px solid #eee;
92
- }
93
- tr:hover {
94
- background: #f9f9f9;
95
- }
96
- .timestamp {
97
- color: #666;
98
- font-size: 12px;
99
- }
100
- .reason {
101
- color: #ff6b6b;
102
- font-weight: 500;
103
- }
106
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
107
+ body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#0f1923;color:#c9d1d9;min-height:100vh}
108
+ a{color:#58a6ff;text-decoration:none}
109
+ code{font-family:'Cascadia Code','Fira Code',Consolas,monospace;background:#0d1117;padding:2px 6px;border-radius:4px;font-size:0.88em;color:#e6edf3}
110
+ pre{font-family:'Cascadia Code','Fira Code',Consolas,monospace}
111
+
112
+ /* Layout */
113
+ .wrap{max-width:1100px;margin:0 auto;padding:32px 24px 48px}
114
+ .top-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:32px;flex-wrap:wrap;gap:12px}
115
+ .top-bar h1{font-size:1.6em;color:#e6edf3}
116
+ .top-bar h1 span{color:#58a6ff}
117
+ .top-bar .meta{color:#484f58;font-size:0.78em}
118
+
119
+ /* Stat cards */
120
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:36px}
121
+ .stat{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:20px}
122
+ .stat .label{font-size:0.75em;text-transform:uppercase;letter-spacing:.5px;color:#8b949e;margin-bottom:8px;font-weight:600}
123
+ .stat .val{font-size:1.8em;font-weight:700;color:#f85149}
124
+ .stat .val.green{color:#238636}
125
+ .stat .val.blue{color:#58a6ff}
126
+
127
+ /* Section headers */
128
+ .section-hdr{font-size:1.15em;color:#e6edf3;margin:32px 0 16px;padding-bottom:8px;border-bottom:1px solid #21262d;display:flex;align-items:center;gap:10px}
129
+
130
+ /* Card generic */
131
+ .card{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:20px 24px;margin-bottom:14px}
132
+
133
+ /* Bot cards */
134
+ .bot-hdr{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}
135
+ .bot-hdr h3{color:#e6edf3;font-size:1em;margin:0}
136
+ .badge{padding:3px 8px;border-radius:12px;font-size:0.7em;font-weight:700;text-transform:uppercase;letter-spacing:.3px}
137
+ .badge-green{background:rgba(35,134,54,0.15);color:#3fb950}
138
+ .badge-orange{background:rgba(210,153,34,0.15);color:#d29922}
139
+ .bot-meta{color:#8b949e;font-size:0.82em;margin-bottom:10px}
140
+ .visit-row{padding:10px 14px;border-left:3px solid #238636;background:#0d1117;border-radius:0 6px 6px 0;margin:8px 0;font-size:0.88em}
141
+ .visit-row .path{color:#e6edf3;font-weight:600;font-family:'Cascadia Code','Fira Code',Consolas,monospace}
142
+ .visit-row .vmeta{color:#484f58;font-size:0.8em;margin-top:4px}
143
+ details summary{cursor:pointer;color:#58a6ff;font-size:0.88em;margin-top:4px}
144
+ details summary:hover{text-decoration:underline}
145
+
146
+ /* AI reports */
147
+ .ai-card{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:20px 24px;margin-bottom:14px}
148
+ .ai-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px}
149
+ .ai-header .ip-tag{background:rgba(248,81,73,0.1);color:#f85149;padding:4px 10px;border-radius:12px;font-size:0.78em;font-weight:600}
150
+ .ai-header .ts{color:#484f58;font-size:0.78em}
151
+ .ai-body{background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:16px;white-space:pre-wrap;word-wrap:break-word;font-size:0.85em;line-height:1.6;color:#c9d1d9}
152
+
153
+ /* Banned table */
154
+ .ban-table{width:100%;border-collapse:collapse;background:#161b22;border:1px solid #21262d;border-radius:8px;overflow:hidden}
155
+ .ban-table th{background:#1a2332;color:#8b949e;font-size:0.75em;text-transform:uppercase;letter-spacing:.5px;font-weight:600;padding:12px 16px;text-align:left;border-bottom:1px solid #21262d}
156
+ .ban-table td{padding:12px 16px;border-bottom:1px solid #21262d;font-size:0.9em}
157
+ .ban-table tr:last-child td{border-bottom:none}
158
+ .ban-table tr:hover td{background:#1a2332}
159
+ .ban-table .reason{color:#f85149;font-weight:500}
160
+ .ban-table .ts{color:#484f58;font-size:0.8em}
161
+
162
+ /* Empty states */
163
+ .empty{text-align:center;padding:36px;color:#484f58;background:#161b22;border:1px solid #21262d;border-radius:8px}
164
+ .empty p:first-child{font-size:0.95em;color:#8b949e}
165
+ .empty p.hint{font-size:0.78em;margin-top:8px}
166
+
167
+ /* Footer */
168
+ .dash-footer{text-align:center;color:#484f58;font-size:0.75em;margin-top:40px;padding-top:16px;border-top:1px solid #21262d}
169
+
170
+ /* Logout */
171
+ .logout-btn{background:none;border:1px solid #30363d;color:#8b949e;padding:6px 14px;border-radius:6px;font-size:0.8em;cursor:pointer;transition:all .2s}
172
+ .logout-btn:hover{border-color:#f85149;color:#f85149}
104
173
  </style>
105
174
  </head>
106
175
  <body>
107
- <h1>🍯 HoneyWeb Status Dashboard</h1>
176
+ <div class="wrap">
108
177
 
178
+ <div class="top-bar">
179
+ <h1><span>HoneyWeb</span> Dashboard</h1>
180
+ <div style="display:flex;align-items:center;gap:16px;">
181
+ <span class="meta">Auto-refresh every 10s</span>
182
+ <form method="POST" action="${dashboardPath}" style="margin:0">
183
+ <input type="hidden" name="secret" value="">
184
+ <button type="button" class="logout-btn" onclick="document.cookie='honeyweb_session=;Path=${dashboardPath};Max-Age=0';location.reload()">Logout</button>
185
+ </form>
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Stats -->
109
190
  <div class="stats">
110
- <div class="stat-card">
111
- <h3>Total Banned IPs</h3>
112
- <div class="value">${stats.total}</div>
191
+ <div class="stat">
192
+ <div class="label">Banned IPs (Total)</div>
193
+ <div class="val">${stats.total}</div>
113
194
  </div>
114
- <div class="stat-card">
115
- <h3>Active Bans</h3>
116
- <div class="value">${stats.active}</div>
195
+ <div class="stat">
196
+ <div class="label">Active Bans</div>
197
+ <div class="val">${stats.active}</div>
117
198
  </div>
118
- <div class="stat-card">
119
- <h3>Expired Bans</h3>
120
- <div class="value">${stats.expired}</div>
199
+ <div class="stat">
200
+ <div class="label">Legitimate Bots</div>
201
+ <div class="val green">${botStats.totalBots}</div>
121
202
  </div>
122
- ${detectionStats.rateLimiter ? `
123
- <div class="stat-card">
124
- <h3>Tracked IPs</h3>
125
- <div class="value">${detectionStats.rateLimiter.trackedIPs}</div>
203
+ <div class="stat">
204
+ <div class="label">Bot Visits</div>
205
+ <div class="val green">${botStats.totalVisits}</div>
126
206
  </div>
127
- ` : ''}
207
+ ${detectionStats.rateLimiter ? `
208
+ <div class="stat">
209
+ <div class="label">Tracked IPs</div>
210
+ <div class="val blue">${detectionStats.rateLimiter.trackedIPs}</div>
211
+ </div>` : ''}
128
212
  </div>
129
213
 
130
- <h2>Banned IP Addresses</h2>
131
- ${bannedIPs.length === 0 ? '<p>No banned IPs yet.</p>' : `
132
- <table>
214
+ <!-- Legitimate Bots -->
215
+ <div class="section-hdr">Legitimate Bot Activity</div>
216
+ ${Object.keys(botVisits).length === 0 ? `
217
+ <div class="empty">
218
+ <p>No legitimate bots detected yet.</p>
219
+ <p class="hint">Googlebot, Bingbot, and other search engines will appear here when they visit your site.</p>
220
+ </div>
221
+ ` : Object.entries(botVisits).map(([botName, visits]) => `
222
+ <div class="card">
223
+ <div class="bot-hdr">
224
+ <h3>${botName}</h3>
225
+ <span class="badge badge-green">${visits.filter(v => v.verified).length} verified</span>
226
+ ${visits.filter(v => !v.verified).length > 0 ? `<span class="badge badge-orange">${visits.filter(v => !v.verified).length} unverified</span>` : ''}
227
+ </div>
228
+ <div class="bot-meta">Total visits: ${visits.length} &middot; Most recent: ${new Date(visits[0].timestamp).toLocaleString()}</div>
229
+ <details>
230
+ <summary>View recent visits (last ${Math.min(visits.length, 10)})</summary>
231
+ ${visits.slice(0, 10).map(visit => `
232
+ <div class="visit-row">
233
+ <div class="path">${visit.path}</div>
234
+ <div class="vmeta">IP: <code>${visit.ip}</code> &middot; ${visit.verified ? 'DNS Verified' : 'Unverified'} &middot; ${new Date(visit.timestamp).toLocaleString()}</div>
235
+ </div>
236
+ `).join('')}
237
+ </details>
238
+ </div>
239
+ `).join('')}
240
+
241
+ <!-- AI Analysis -->
242
+ <div class="section-hdr">AI Threat Analysis</div>
243
+ ${aiReports.length === 0 ? `
244
+ <div class="empty">
245
+ <p>No AI analysis reports yet.</p>
246
+ <p class="hint">Reports will appear here when threats or traps are detected and analyzed by Gemini.</p>
247
+ </div>
248
+ ` : aiReports.slice(0, 10).map(r => `
249
+ <div class="ai-card">
250
+ <div class="ai-header">
251
+ <span class="ip-tag">IP: ${r.ip}</span>
252
+ <span class="ts">${new Date(r.timestamp).toLocaleString()}</span>
253
+ </div>
254
+ <div class="ai-body">${r.report}</div>
255
+ </div>
256
+ `).join('')}
257
+
258
+ <!-- Banned IPs -->
259
+ <div class="section-hdr">Banned IP Addresses</div>
260
+ ${bannedIPs.length === 0 ? `
261
+ <div class="empty"><p>No banned IPs yet.</p></div>
262
+ ` : `
263
+ <table class="ban-table">
133
264
  <thead>
134
265
  <tr>
135
266
  <th>IP Address</th>
136
267
  <th>Reason</th>
137
268
  <th>Banned At</th>
138
- <th>Expires At</th>
269
+ <th>Expires</th>
139
270
  </tr>
140
271
  </thead>
141
272
  <tbody>
142
273
  ${bannedIPs.map(entry => {
143
274
  const ip = typeof entry === 'string' ? entry : entry.ip;
144
275
  const reason = typeof entry === 'string' ? 'Suspicious activity' : entry.reason;
145
- const timestamp = typeof entry === 'string' ? 'N/A' : new Date(entry.timestamp).toLocaleString();
276
+ const ts = typeof entry === 'string' ? 'N/A' : new Date(entry.timestamp).toLocaleString();
146
277
  const expiry = typeof entry === 'string' ? 'Never' : new Date(entry.expiry).toLocaleString();
147
-
148
- return `
149
- <tr>
150
- <td><code>${ip}</code></td>
151
- <td class="reason">${reason}</td>
152
- <td class="timestamp">${timestamp}</td>
153
- <td class="timestamp">${expiry}</td>
154
- </tr>
155
- `;
278
+ return `<tr><td><code>${ip}</code></td><td class="reason">${reason}</td><td class="ts">${ts}</td><td class="ts">${expiry}</td></tr>`;
156
279
  }).join('')}
157
280
  </tbody>
158
281
  </table>
159
282
  `}
160
283
 
161
- <p style="margin-top: 40px; color: #666; text-align: center;">
162
- HoneyWeb v1.0.2 | Refresh page to update data
163
- </p>
284
+ <div class="dash-footer">HoneyWeb v2.0.1 &middot; Refresh page to update data</div>
285
+
286
+ </div>
164
287
  </body>
165
- </html>
166
- `;
288
+ </html>`;
167
289
 
168
290
  res.send(html);
169
291
  } catch (err) {
@@ -10,19 +10,21 @@ const createDashboard = require('./dashboard');
10
10
  * Create HoneyWeb middleware stack
11
11
  * @param {Object} config - Configuration
12
12
  * @param {Object} storage - Storage instance
13
+ * @param {Object} botTracker - Bot tracker instance
13
14
  * @param {Object} detector - Detection engine
14
15
  * @param {Object} aiAnalyzer - AI analyzer
15
16
  * @param {Object} logger - Logger instance
16
17
  * @param {Object} events - Event emitter
17
18
  * @returns {Function} - Express middleware function
18
19
  */
19
- function createMiddleware(config, storage, detector, aiAnalyzer, logger, events) {
20
+ function createMiddleware(config, storage, botTracker, detector, aiAnalyzer, logger, events) {
20
21
  // Create individual middleware components
21
22
  const blocklistChecker = createBlocklistChecker(storage, logger, events);
22
- const requestAnalyzer = createRequestAnalyzer(config, storage, detector, aiAnalyzer, logger, events);
23
+ const requestAnalyzer = createRequestAnalyzer(config, storage, botTracker, detector, aiAnalyzer, logger, events);
23
24
  const trapInjector = createTrapInjector(config);
25
+ const trapPaths = config.traps.paths;
24
26
  const dashboard = config.dashboard.enabled
25
- ? createDashboard(config, storage, detector)
27
+ ? createDashboard(config, storage, botTracker, detector, events)
26
28
  : null;
27
29
 
28
30
  // Return combined middleware function
@@ -33,7 +35,20 @@ function createMiddleware(config, storage, detector, aiAnalyzer, logger, events)
33
35
  return dashboard(req, res, next);
34
36
  }
35
37
 
36
- // 2. Check blocklist
38
+ // 2. If this is a trap path, ALWAYS run the analyzer first
39
+ // so trap triggers are logged + AI-analyzed even for banned IPs
40
+ if (trapPaths.includes(req.path)) {
41
+ await new Promise((resolve, reject) => {
42
+ requestAnalyzer(req, res, (err) => {
43
+ if (err) reject(err);
44
+ else resolve();
45
+ });
46
+ });
47
+ // Analyzer already sent 403 for traps
48
+ if (res.headersSent) return;
49
+ }
50
+
51
+ // 3. Check blocklist (non-trap requests)
37
52
  await new Promise((resolve, reject) => {
38
53
  blocklistChecker(req, res, (err) => {
39
54
  if (err) reject(err);
@@ -46,7 +61,7 @@ function createMiddleware(config, storage, detector, aiAnalyzer, logger, events)
46
61
  return;
47
62
  }
48
63
 
49
- // 3. Analyze request (traps + threats)
64
+ // 4. Analyze request (patterns, rate limiting, behavioral)
50
65
  await new Promise((resolve, reject) => {
51
66
  requestAnalyzer(req, res, (err) => {
52
67
  if (err) reject(err);
@@ -59,7 +74,7 @@ function createMiddleware(config, storage, detector, aiAnalyzer, logger, events)
59
74
  return;
60
75
  }
61
76
 
62
- // 4. Inject traps into HTML responses
77
+ // 5. Inject traps into HTML responses
63
78
  trapInjector(req, res, next);
64
79
 
65
80
  } catch (err) {
@@ -5,19 +5,44 @@
5
5
  * Create request analyzer middleware
6
6
  * @param {Object} config - Configuration
7
7
  * @param {Object} storage - Storage instance
8
+ * @param {Object} botTracker - Bot tracker instance
8
9
  * @param {Object} detector - Detection engine
9
10
  * @param {Object} aiAnalyzer - AI analyzer
10
11
  * @param {Object} logger - Logger instance
11
12
  * @param {Object} events - Event emitter
12
13
  * @returns {Function} - Middleware function
13
14
  */
14
- function createRequestAnalyzer(config, storage, detector, aiAnalyzer, logger, events) {
15
+ function createRequestAnalyzer(config, storage, botTracker, detector, aiAnalyzer, logger, events) {
15
16
  const trapPaths = config.traps.paths;
16
17
 
17
18
  return async function analyzeRequest(req, res, next) {
18
19
  const clientIP = req.ip || req.connection.remoteAddress;
19
20
 
20
- // 1. CHECK IF TRAP ACCESSED
21
+ // 1. THREAT DETECTION (whitelist check happens FIRST to protect legit bots)
22
+ const analysis = await detector.analyze(req, clientIP);
23
+
24
+ // Skip ALL checks if legitimate bot (even trap paths!)
25
+ if (analysis.legitimateBot) {
26
+ // Record legitimate bot visit for dashboard
27
+ botTracker.recordVisit({
28
+ botName: analysis.whitelist.botName,
29
+ ip: clientIP,
30
+ path: req.path,
31
+ userAgent: req.headers['user-agent'],
32
+ verified: analysis.whitelist.verified,
33
+ timestamp: Date.now()
34
+ });
35
+
36
+ logger.info('Legitimate bot detected', {
37
+ ip: clientIP,
38
+ botName: analysis.whitelist.botName,
39
+ verified: analysis.whitelist.verified,
40
+ path: req.path
41
+ });
42
+ return next();
43
+ }
44
+
45
+ // 2. CHECK IF TRAP ACCESSED (only after confirming not a legit bot)
21
46
  if (trapPaths.includes(req.path)) {
22
47
  logger.trapTriggered(clientIP, req.path);
23
48
  events.emitTrapTriggered(clientIP, req.path);
@@ -41,10 +66,14 @@ function createRequestAnalyzer(config, storage, detector, aiAnalyzer, logger, ev
41
66
  if (report) {
42
67
  logger.info('AI Trap Analysis', { ip: clientIP, report });
43
68
  events.emitAiAnalysis(clientIP, report);
69
+ } else {
70
+ logger.warn('AI returned empty report for trap', { ip: clientIP, path: req.path });
44
71
  }
45
72
  }).catch(err => {
46
- logger.error('AI analysis failed', { error: err.message });
73
+ logger.error('AI trap analysis failed', { error: err.message });
47
74
  });
75
+ } else {
76
+ logger.info('AI is disabled, skipping trap analysis');
48
77
  }
49
78
 
50
79
  return res.status(403).send(
@@ -52,18 +81,7 @@ function createRequestAnalyzer(config, storage, detector, aiAnalyzer, logger, ev
52
81
  );
53
82
  }
54
83
 
55
- // 2. THREAT DETECTION (patterns + rate limiting + behavioral + bot detection)
56
- const analysis = await detector.analyze(req, clientIP);
57
-
58
- // Skip blocking if legitimate bot
59
- if (analysis.legitimateBot) {
60
- logger.info('Legitimate bot detected', {
61
- ip: clientIP,
62
- botName: analysis.whitelist.botName,
63
- verified: analysis.whitelist.verified
64
- });
65
- return next();
66
- }
84
+ // 3. OTHER THREAT DETECTION (patterns, rate limiting, behavioral)
67
85
 
68
86
  if (analysis.detected) {
69
87
  logger.threatDetected(clientIP, analysis.threats, analysis.threatLevel);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeyweb-core",
3
- "version": "2.0.0",
3
+ "version": "2.0.3",
4
4
  "description": "Production-ready honeypot middleware with behavioral analysis, bot fingerprinting, and AI threat intelligence",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,109 @@
1
+ // honeyweb-core/storage/bot-tracker.js
2
+ // Track legitimate bot visits for dashboard display
3
+
4
+ class BotTracker {
5
+ constructor() {
6
+ this.botVisits = new Map(); // botName -> array of visits
7
+ this.maxVisitsPerBot = 50; // Keep last 50 visits per bot
8
+ }
9
+
10
+ /**
11
+ * Record a legitimate bot visit
12
+ * @param {Object} visit - Visit data
13
+ */
14
+ recordVisit(visit) {
15
+ const { botName, ip, path, userAgent, verified, timestamp } = visit;
16
+
17
+ if (!this.botVisits.has(botName)) {
18
+ this.botVisits.set(botName, []);
19
+ }
20
+
21
+ const visits = this.botVisits.get(botName);
22
+
23
+ // Add new visit at the beginning (most recent first)
24
+ visits.unshift({
25
+ ip,
26
+ path,
27
+ userAgent,
28
+ verified,
29
+ timestamp: timestamp || Date.now()
30
+ });
31
+
32
+ // Keep only last N visits
33
+ if (visits.length > this.maxVisitsPerBot) {
34
+ visits.pop();
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get all bot visits grouped by bot name
40
+ * @returns {Object} - Bot visits grouped by name
41
+ */
42
+ getAllVisits() {
43
+ const result = {};
44
+
45
+ for (const [botName, visits] of this.botVisits.entries()) {
46
+ result[botName] = visits;
47
+ }
48
+
49
+ return result;
50
+ }
51
+
52
+ /**
53
+ * Get visits for a specific bot
54
+ * @param {string} botName - Bot name
55
+ * @returns {Array} - Array of visits
56
+ */
57
+ getVisitsByBot(botName) {
58
+ return this.botVisits.get(botName) || [];
59
+ }
60
+
61
+ /**
62
+ * Get statistics
63
+ * @returns {Object} - Statistics
64
+ */
65
+ getStats() {
66
+ let totalVisits = 0;
67
+ const botCounts = {};
68
+
69
+ for (const [botName, visits] of this.botVisits.entries()) {
70
+ totalVisits += visits.length;
71
+ botCounts[botName] = visits.length;
72
+ }
73
+
74
+ return {
75
+ totalBots: this.botVisits.size,
76
+ totalVisits,
77
+ botCounts
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Clear all bot visits
83
+ */
84
+ clear() {
85
+ this.botVisits.clear();
86
+ }
87
+
88
+ /**
89
+ * Cleanup old visits (older than 7 days)
90
+ */
91
+ cleanup() {
92
+ const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
93
+ const now = Date.now();
94
+
95
+ for (const [botName, visits] of this.botVisits.entries()) {
96
+ const filtered = visits.filter(visit => {
97
+ return (now - visit.timestamp) < maxAge;
98
+ });
99
+
100
+ if (filtered.length === 0) {
101
+ this.botVisits.delete(botName);
102
+ } else {
103
+ this.botVisits.set(botName, filtered);
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ module.exports = BotTracker;