honeyweb-core 2.0.2 → 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-1.5-flash' });
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-1.5-flash', // Using 1.5 for wider geographic availability
10
- timeout: 10000
9
+ model: 'gemini-3-flash-preview',
10
+ timeout: 30000
11
11
  },
12
12
 
13
13
  // Detection Configuration
@@ -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
 
@@ -1,270 +1,291 @@
1
1
  // honeyweb-core/middleware/dashboard.js
2
- // Dashboard middleware (extracted from main index.js)
2
+ // Dashboard middleware with cookie-based auth (no credentials in URL)
3
3
 
4
- /**
5
- * Create dashboard middleware
6
- * @param {Object} config - Configuration
7
- * @param {Object} storage - Storage instance
8
- * @param {Object} botTracker - Bot tracker instance
9
- * @param {Object} detector - Detection engine
10
- * @returns {Function} - Middleware function
11
- */
12
- function createDashboard(config, storage, botTracker, detector) {
4
+ const crypto = require('crypto');
5
+
6
+ function createDashboard(config, storage, botTracker, detector, events) {
13
7
  const dashboardPath = config.dashboard.path;
14
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
+ }
15
66
 
16
67
  return async function dashboard(req, res, next) {
17
- // Only handle dashboard path
18
- if (req.path !== dashboardPath) {
19
- 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.');
20
78
  }
21
79
 
22
- // Check authentication
23
- const key = req.query.key;
24
- if (key !== dashboardSecret) {
25
- 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>');
26
87
  }
27
88
 
89
+ if (!isAuthenticated(req)) return renderLoginPage(res);
90
+
28
91
  try {
29
- // Get banned IPs
30
92
  const bannedIPs = await storage.getAll();
31
93
  const stats = await storage.getStats();
32
94
  const detectionStats = detector.getStats();
33
-
34
- // Get legitimate bot visits
35
95
  const botVisits = botTracker.getAllVisits();
36
96
  const botStats = botTracker.getStats();
37
97
 
38
- // Generate HTML dashboard
39
- const html = `
40
- <!DOCTYPE html>
41
- <html>
98
+ const html = `<!DOCTYPE html>
99
+ <html lang="en">
42
100
  <head>
101
+ <meta charset="UTF-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
43
103
  <title>HoneyWeb Status Dashboard</title>
104
+ <meta http-equiv="refresh" content="10">
44
105
  <style>
45
- body {
46
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
47
- max-width: 1200px;
48
- margin: 40px auto;
49
- padding: 20px;
50
- background: #f5f5f5;
51
- }
52
- h1 {
53
- color: #333;
54
- border-bottom: 3px solid #ff6b6b;
55
- padding-bottom: 10px;
56
- }
57
- .stats {
58
- display: grid;
59
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
60
- gap: 20px;
61
- margin: 20px 0;
62
- }
63
- .stat-card {
64
- background: white;
65
- padding: 20px;
66
- border-radius: 8px;
67
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
68
- }
69
- .stat-card h3 {
70
- margin: 0 0 10px 0;
71
- color: #666;
72
- font-size: 14px;
73
- text-transform: uppercase;
74
- }
75
- .stat-card .value {
76
- font-size: 32px;
77
- font-weight: bold;
78
- color: #ff6b6b;
79
- }
80
- table {
81
- width: 100%;
82
- background: white;
83
- border-collapse: collapse;
84
- border-radius: 8px;
85
- overflow: hidden;
86
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
87
- }
88
- th {
89
- background: #ff6b6b;
90
- color: white;
91
- padding: 12px;
92
- text-align: left;
93
- }
94
- td {
95
- padding: 12px;
96
- border-bottom: 1px solid #eee;
97
- }
98
- tr:hover {
99
- background: #f9f9f9;
100
- }
101
- .timestamp {
102
- color: #666;
103
- font-size: 12px;
104
- }
105
- .reason {
106
- color: #ff6b6b;
107
- font-weight: 500;
108
- }
109
- .bot-section {
110
- margin-top: 40px;
111
- }
112
- .bot-card {
113
- background: white;
114
- border-radius: 8px;
115
- padding: 20px;
116
- margin-bottom: 20px;
117
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
118
- }
119
- .bot-card h3 {
120
- margin: 0 0 10px 0;
121
- color: #4CAF50;
122
- display: flex;
123
- align-items: center;
124
- gap: 10px;
125
- }
126
- .verified-badge {
127
- background: #4CAF50;
128
- color: white;
129
- padding: 4px 8px;
130
- border-radius: 4px;
131
- font-size: 11px;
132
- font-weight: bold;
133
- }
134
- .unverified-badge {
135
- background: #ff9800;
136
- color: white;
137
- padding: 4px 8px;
138
- border-radius: 4px;
139
- font-size: 11px;
140
- font-weight: bold;
141
- }
142
- .visit-entry {
143
- padding: 10px;
144
- border-left: 3px solid #4CAF50;
145
- margin: 10px 0;
146
- background: #f9f9f9;
147
- }
148
- .visit-path {
149
- font-family: 'Courier New', monospace;
150
- color: #333;
151
- font-weight: bold;
152
- }
153
- .visit-meta {
154
- color: #666;
155
- font-size: 12px;
156
- margin-top: 5px;
157
- }
158
- .no-bots {
159
- text-align: center;
160
- padding: 40px;
161
- color: #666;
162
- background: white;
163
- border-radius: 8px;
164
- }
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}
165
173
  </style>
166
174
  </head>
167
175
  <body>
168
- <h1>🍯 HoneyWeb Status Dashboard</h1>
176
+ <div class="wrap">
169
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 -->
170
190
  <div class="stats">
171
- <div class="stat-card">
172
- <h3>Total Banned IPs</h3>
173
- <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>
174
194
  </div>
175
- <div class="stat-card">
176
- <h3>Active Bans</h3>
177
- <div class="value">${stats.active}</div>
195
+ <div class="stat">
196
+ <div class="label">Active Bans</div>
197
+ <div class="val">${stats.active}</div>
178
198
  </div>
179
- <div class="stat-card">
180
- <h3>Legitimate Bots</h3>
181
- <div class="value" style="color: #4CAF50;">${botStats.totalBots}</div>
199
+ <div class="stat">
200
+ <div class="label">Legitimate Bots</div>
201
+ <div class="val green">${botStats.totalBots}</div>
182
202
  </div>
183
- <div class="stat-card">
184
- <h3>Bot Visits</h3>
185
- <div class="value" style="color: #4CAF50;">${botStats.totalVisits}</div>
203
+ <div class="stat">
204
+ <div class="label">Bot Visits</div>
205
+ <div class="val green">${botStats.totalVisits}</div>
186
206
  </div>
187
207
  ${detectionStats.rateLimiter ? `
188
- <div class="stat-card">
189
- <h3>Tracked IPs</h3>
190
- <div class="value">${detectionStats.rateLimiter.trackedIPs}</div>
191
- </div>
192
- ` : ''}
208
+ <div class="stat">
209
+ <div class="label">Tracked IPs</div>
210
+ <div class="val blue">${detectionStats.rateLimiter.trackedIPs}</div>
211
+ </div>` : ''}
193
212
  </div>
194
213
 
195
- <div class="bot-section">
196
- <h2>🤖 Legitimate Bot Activity</h2>
197
- ${Object.keys(botVisits).length === 0 ? `
198
- <div class="no-bots">
199
- <p>No legitimate bots detected yet.</p>
200
- <p style="font-size: 12px; margin-top: 10px;">
201
- Googlebot, Bingbot, and other search engines will appear here when they visit your site.
202
- </p>
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>` : ''}
203
227
  </div>
204
- ` : Object.entries(botVisits).map(([botName, visits]) => `
205
- <div class="bot-card">
206
- <h3>
207
- ${botName}
208
- <span class="verified-badge">${visits.filter(v => v.verified).length} verified</span>
209
- ${visits.filter(v => !v.verified).length > 0 ? `<span class="unverified-badge">${visits.filter(v => !v.verified).length} unverified</span>` : ''}
210
- </h3>
211
- <p style="color: #666; font-size: 14px; margin-bottom: 15px;">
212
- Total visits: ${visits.length} | Most recent: ${new Date(visits[0].timestamp).toLocaleString()}
213
- </p>
214
- <details>
215
- <summary style="cursor: pointer; margin-bottom: 10px;">View recent visits (last ${Math.min(visits.length, 10)})</summary>
216
- ${visits.slice(0, 10).map(visit => `
217
- <div class="visit-entry">
218
- <div class="visit-path">${visit.path}</div>
219
- <div class="visit-meta">
220
- IP: <code>${visit.ip}</code> |
221
- ${visit.verified ? '✅ DNS Verified' : '⚠️ Unverified'} |
222
- ${new Date(visit.timestamp).toLocaleString()}
223
- </div>
224
- </div>
225
- `).join('')}
226
- </details>
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>
227
253
  </div>
228
- `).join('')}
229
- </div>
254
+ <div class="ai-body">${r.report}</div>
255
+ </div>
256
+ `).join('')}
230
257
 
231
- <h2>🚫 Banned IP Addresses</h2>
232
- ${bannedIPs.length === 0 ? '<p>No banned IPs yet.</p>' : `
233
- <table>
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">
234
264
  <thead>
235
265
  <tr>
236
266
  <th>IP Address</th>
237
267
  <th>Reason</th>
238
268
  <th>Banned At</th>
239
- <th>Expires At</th>
269
+ <th>Expires</th>
240
270
  </tr>
241
271
  </thead>
242
272
  <tbody>
243
273
  ${bannedIPs.map(entry => {
244
274
  const ip = typeof entry === 'string' ? entry : entry.ip;
245
275
  const reason = typeof entry === 'string' ? 'Suspicious activity' : entry.reason;
246
- 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();
247
277
  const expiry = typeof entry === 'string' ? 'Never' : new Date(entry.expiry).toLocaleString();
248
-
249
- return `
250
- <tr>
251
- <td><code>${ip}</code></td>
252
- <td class="reason">${reason}</td>
253
- <td class="timestamp">${timestamp}</td>
254
- <td class="timestamp">${expiry}</td>
255
- </tr>
256
- `;
278
+ return `<tr><td><code>${ip}</code></td><td class="reason">${reason}</td><td class="ts">${ts}</td><td class="ts">${expiry}</td></tr>`;
257
279
  }).join('')}
258
280
  </tbody>
259
281
  </table>
260
282
  `}
261
283
 
262
- <p style="margin-top: 40px; color: #666; text-align: center;">
263
- HoneyWeb v2.0.1 | Refresh page to update data
264
- </p>
284
+ <div class="dash-footer">HoneyWeb v2.0.1 &middot; Refresh page to update data</div>
285
+
286
+ </div>
265
287
  </body>
266
- </html>
267
- `;
288
+ </html>`;
268
289
 
269
290
  res.send(html);
270
291
  } catch (err) {
@@ -22,8 +22,9 @@ function createMiddleware(config, storage, botTracker, detector, aiAnalyzer, log
22
22
  const blocklistChecker = createBlocklistChecker(storage, logger, events);
23
23
  const requestAnalyzer = createRequestAnalyzer(config, storage, botTracker, detector, aiAnalyzer, logger, events);
24
24
  const trapInjector = createTrapInjector(config);
25
+ const trapPaths = config.traps.paths;
25
26
  const dashboard = config.dashboard.enabled
26
- ? createDashboard(config, storage, botTracker, detector)
27
+ ? createDashboard(config, storage, botTracker, detector, events)
27
28
  : null;
28
29
 
29
30
  // Return combined middleware function
@@ -34,7 +35,20 @@ function createMiddleware(config, storage, botTracker, detector, aiAnalyzer, log
34
35
  return dashboard(req, res, next);
35
36
  }
36
37
 
37
- // 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)
38
52
  await new Promise((resolve, reject) => {
39
53
  blocklistChecker(req, res, (err) => {
40
54
  if (err) reject(err);
@@ -47,7 +61,7 @@ function createMiddleware(config, storage, botTracker, detector, aiAnalyzer, log
47
61
  return;
48
62
  }
49
63
 
50
- // 3. Analyze request (traps + threats)
64
+ // 4. Analyze request (patterns, rate limiting, behavioral)
51
65
  await new Promise((resolve, reject) => {
52
66
  requestAnalyzer(req, res, (err) => {
53
67
  if (err) reject(err);
@@ -60,7 +74,7 @@ function createMiddleware(config, storage, botTracker, detector, aiAnalyzer, log
60
74
  return;
61
75
  }
62
76
 
63
- // 4. Inject traps into HTML responses
77
+ // 5. Inject traps into HTML responses
64
78
  trapInjector(req, res, next);
65
79
 
66
80
  } catch (err) {
@@ -66,10 +66,14 @@ function createRequestAnalyzer(config, storage, botTracker, detector, aiAnalyzer
66
66
  if (report) {
67
67
  logger.info('AI Trap Analysis', { ip: clientIP, report });
68
68
  events.emitAiAnalysis(clientIP, report);
69
+ } else {
70
+ logger.warn('AI returned empty report for trap', { ip: clientIP, path: req.path });
69
71
  }
70
72
  }).catch(err => {
71
- logger.error('AI analysis failed', { error: err.message });
73
+ logger.error('AI trap analysis failed', { error: err.message });
72
74
  });
75
+ } else {
76
+ logger.info('AI is disabled, skipping trap analysis');
73
77
  }
74
78
 
75
79
  return res.status(403).send(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeyweb-core",
3
- "version": "2.0.2",
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": {