honeyweb-core 2.0.2 → 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.
@@ -1,270 +1,274 @@
1
1
  // honeyweb-core/middleware/dashboard.js
2
- // Dashboard middleware (extracted from main index.js)
3
2
 
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) {
3
+ const crypto = require('crypto');
4
+
5
+ function createDashboard(config, storage, botTracker, detector, events) {
13
6
  const dashboardPath = config.dashboard.path;
14
7
  const dashboardSecret = config.dashboard.secret;
8
+ const sessionToken = crypto.createHash('sha256').update(dashboardSecret + ':honeyweb-session').digest('hex');
9
+
10
+ const aiReports = [];
11
+ const MAX_AI_REPORTS = 20;
12
+
13
+ if (events && events.on) {
14
+ events.on('ai:analysis', ({ ip, report, timestamp }) => {
15
+ aiReports.unshift({ ip, report, timestamp });
16
+ if (aiReports.length > MAX_AI_REPORTS) aiReports.pop();
17
+ });
18
+ }
19
+
20
+ function isAuthenticated(req) {
21
+ const cookieHeader = req.headers.cookie || '';
22
+ const match = cookieHeader.match(/honeyweb_session=([^;]+)/);
23
+ return match && match[1] === sessionToken;
24
+ }
25
+
26
+ function renderLoginPage(res, error) {
27
+ const errorHtml = error
28
+ ? `<div class="error-msg">${error}</div>`
29
+ : '';
30
+ res.send(`<!DOCTYPE html>
31
+ <html lang="en">
32
+ <head>
33
+ <meta charset="UTF-8">
34
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
35
+ <title>HoneyWeb &mdash; Dashboard Login</title>
36
+ <style>
37
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
38
+ 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}
39
+ .card{background:#161b22;border:1px solid #21262d;border-radius:12px;padding:44px 36px;width:380px;text-align:center}
40
+ .card h1{font-size:1.5em;color:#e6edf3;margin-bottom:4px}
41
+ .card .sub{color:#8b949e;font-size:0.85em;margin-bottom:28px}
42
+ .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}
43
+ 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}
44
+ input[type="password"]:focus{outline:none;border-color:#58a6ff;box-shadow:0 0 0 3px rgba(88,166,255,0.15)}
45
+ input[type="password"]::placeholder{color:#484f58}
46
+ 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}
47
+ button:hover{background:#2ea043}
48
+ .footer-note{margin-top:20px;font-size:0.72em;color:#484f58}
49
+ </style>
50
+ </head>
51
+ <body>
52
+ <div class="card">
53
+ <h1>HoneyWeb Dashboard</h1>
54
+ <p class="sub">Enter the dashboard secret to continue</p>
55
+ ${errorHtml}
56
+ <form method="POST" action="${dashboardPath}">
57
+ <input type="password" name="secret" placeholder="Dashboard secret" autofocus required>
58
+ <button type="submit">Authenticate</button>
59
+ </form>
60
+ <p class="footer-note">Session expires after 1 hour</p>
61
+ </div>
62
+ </body>
63
+ </html>`);
64
+ }
15
65
 
16
66
  return async function dashboard(req, res, next) {
17
- // Only handle dashboard path
18
- if (req.path !== dashboardPath) {
19
- return next();
67
+ if (req.path !== dashboardPath) return next();
68
+
69
+ if (req.method === 'POST') {
70
+ const submitted = req.body && req.body.secret;
71
+ if (submitted === dashboardSecret) {
72
+ res.setHeader('Set-Cookie', `honeyweb_session=${sessionToken}; HttpOnly; Path=${dashboardPath}; SameSite=Strict; Max-Age=3600`);
73
+ return res.redirect(303, dashboardPath);
74
+ }
75
+ return renderLoginPage(res, 'Invalid secret. Try again.');
20
76
  }
21
77
 
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>');
78
+ if (req.query.key) {
79
+ if (req.query.key === dashboardSecret) {
80
+ res.setHeader('Set-Cookie', `honeyweb_session=${sessionToken}; HttpOnly; Path=${dashboardPath}; SameSite=Strict; Max-Age=3600`);
81
+ return res.redirect(303, dashboardPath);
82
+ }
83
+ return res.status(401).send('<h1>401 Unauthorized</h1>');
26
84
  }
27
85
 
86
+ if (!isAuthenticated(req)) return renderLoginPage(res);
87
+
28
88
  try {
29
- // Get banned IPs
30
89
  const bannedIPs = await storage.getAll();
31
90
  const stats = await storage.getStats();
32
91
  const detectionStats = detector.getStats();
33
-
34
- // Get legitimate bot visits
35
92
  const botVisits = botTracker.getAllVisits();
36
93
  const botStats = botTracker.getStats();
37
94
 
38
- // Generate HTML dashboard
39
- const html = `
40
- <!DOCTYPE html>
41
- <html>
95
+ const html = `<!DOCTYPE html>
96
+ <html lang="en">
42
97
  <head>
98
+ <meta charset="UTF-8">
99
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
43
100
  <title>HoneyWeb Status Dashboard</title>
101
+ <meta http-equiv="refresh" content="10">
44
102
  <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
- }
103
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
104
+ body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#0f1923;color:#c9d1d9;min-height:100vh}
105
+ a{color:#58a6ff;text-decoration:none}
106
+ code{font-family:'Cascadia Code','Fira Code',Consolas,monospace;background:#0d1117;padding:2px 6px;border-radius:4px;font-size:0.88em;color:#e6edf3}
107
+ pre{font-family:'Cascadia Code','Fira Code',Consolas,monospace}
108
+
109
+ .wrap{max-width:1100px;margin:0 auto;padding:32px 24px 48px}
110
+ .top-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:32px;flex-wrap:wrap;gap:12px}
111
+ .top-bar h1{font-size:1.6em;color:#e6edf3}
112
+ .top-bar h1 span{color:#58a6ff}
113
+ .top-bar .meta{color:#484f58;font-size:0.78em}
114
+
115
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:36px}
116
+ .stat{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:20px}
117
+ .stat .label{font-size:0.75em;text-transform:uppercase;letter-spacing:.5px;color:#8b949e;margin-bottom:8px;font-weight:600}
118
+ .stat .val{font-size:1.8em;font-weight:700;color:#f85149}
119
+ .stat .val.green{color:#238636}
120
+ .stat .val.blue{color:#58a6ff}
121
+
122
+ .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}
123
+
124
+ .card{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:20px 24px;margin-bottom:14px}
125
+
126
+ .bot-hdr{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}
127
+ .bot-hdr h3{color:#e6edf3;font-size:1em;margin:0}
128
+ .badge{padding:3px 8px;border-radius:12px;font-size:0.7em;font-weight:700;text-transform:uppercase;letter-spacing:.3px}
129
+ .badge-green{background:rgba(35,134,54,0.15);color:#3fb950}
130
+ .badge-orange{background:rgba(210,153,34,0.15);color:#d29922}
131
+ .bot-meta{color:#8b949e;font-size:0.82em;margin-bottom:10px}
132
+ .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}
133
+ .visit-row .path{color:#e6edf3;font-weight:600;font-family:'Cascadia Code','Fira Code',Consolas,monospace}
134
+ .visit-row .vmeta{color:#484f58;font-size:0.8em;margin-top:4px}
135
+ details summary{cursor:pointer;color:#58a6ff;font-size:0.88em;margin-top:4px}
136
+ details summary:hover{text-decoration:underline}
137
+
138
+ .ai-card{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:20px 24px;margin-bottom:14px}
139
+ .ai-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px}
140
+ .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}
141
+ .ai-header .ts{color:#484f58;font-size:0.78em}
142
+ .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}
143
+
144
+ .ban-table{width:100%;border-collapse:collapse;background:#161b22;border:1px solid #21262d;border-radius:8px;overflow:hidden}
145
+ .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}
146
+ .ban-table td{padding:12px 16px;border-bottom:1px solid #21262d;font-size:0.9em}
147
+ .ban-table tr:last-child td{border-bottom:none}
148
+ .ban-table tr:hover td{background:#1a2332}
149
+ .ban-table .reason{color:#f85149;font-weight:500}
150
+ .ban-table .ts{color:#484f58;font-size:0.8em}
151
+
152
+ .empty{text-align:center;padding:36px;color:#484f58;background:#161b22;border:1px solid #21262d;border-radius:8px}
153
+ .empty p:first-child{font-size:0.95em;color:#8b949e}
154
+ .empty p.hint{font-size:0.78em;margin-top:8px}
155
+
156
+ .dash-footer{text-align:center;color:#484f58;font-size:0.75em;margin-top:40px;padding-top:16px;border-top:1px solid #21262d}
157
+
158
+ .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}
159
+ .logout-btn:hover{border-color:#f85149;color:#f85149}
165
160
  </style>
166
161
  </head>
167
162
  <body>
168
- <h1>🍯 HoneyWeb Status Dashboard</h1>
163
+ <div class="wrap">
164
+
165
+ <div class="top-bar">
166
+ <h1><span>HoneyWeb</span> Dashboard</h1>
167
+ <div style="display:flex;align-items:center;gap:16px;">
168
+ <span class="meta">Auto-refresh every 10s</span>
169
+ <form method="POST" action="${dashboardPath}" style="margin:0">
170
+ <input type="hidden" name="secret" value="">
171
+ <button type="button" class="logout-btn" onclick="document.cookie='honeyweb_session=;Path=${dashboardPath};Max-Age=0';location.reload()">Logout</button>
172
+ </form>
173
+ </div>
174
+ </div>
169
175
 
170
176
  <div class="stats">
171
- <div class="stat-card">
172
- <h3>Total Banned IPs</h3>
173
- <div class="value">${stats.total}</div>
177
+ <div class="stat">
178
+ <div class="label">Banned IPs (Total)</div>
179
+ <div class="val">${stats.total}</div>
174
180
  </div>
175
- <div class="stat-card">
176
- <h3>Active Bans</h3>
177
- <div class="value">${stats.active}</div>
181
+ <div class="stat">
182
+ <div class="label">Active Bans</div>
183
+ <div class="val">${stats.active}</div>
178
184
  </div>
179
- <div class="stat-card">
180
- <h3>Legitimate Bots</h3>
181
- <div class="value" style="color: #4CAF50;">${botStats.totalBots}</div>
185
+ <div class="stat">
186
+ <div class="label">Legitimate Bots</div>
187
+ <div class="val green">${botStats.totalBots}</div>
182
188
  </div>
183
- <div class="stat-card">
184
- <h3>Bot Visits</h3>
185
- <div class="value" style="color: #4CAF50;">${botStats.totalVisits}</div>
189
+ <div class="stat">
190
+ <div class="label">Bot Visits</div>
191
+ <div class="val green">${botStats.totalVisits}</div>
186
192
  </div>
187
193
  ${detectionStats.rateLimiter ? `
188
- <div class="stat-card">
189
- <h3>Tracked IPs</h3>
190
- <div class="value">${detectionStats.rateLimiter.trackedIPs}</div>
191
- </div>
192
- ` : ''}
194
+ <div class="stat">
195
+ <div class="label">Tracked IPs</div>
196
+ <div class="val blue">${detectionStats.rateLimiter.trackedIPs}</div>
197
+ </div>` : ''}
193
198
  </div>
194
199
 
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>
200
+ <div class="section-hdr">Legitimate Bot Activity</div>
201
+ ${Object.keys(botVisits).length === 0 ? `
202
+ <div class="empty">
203
+ <p>No legitimate bots detected yet.</p>
204
+ <p class="hint">Googlebot, Bingbot, and other search engines will appear here when they visit your site.</p>
205
+ </div>
206
+ ` : Object.entries(botVisits).map(([botName, visits]) => `
207
+ <div class="card">
208
+ <div class="bot-hdr">
209
+ <h3>${botName}</h3>
210
+ <span class="badge badge-green">${visits.filter(v => v.verified).length} verified</span>
211
+ ${visits.filter(v => !v.verified).length > 0 ? `<span class="badge badge-orange">${visits.filter(v => !v.verified).length} unverified</span>` : ''}
203
212
  </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>
213
+ <div class="bot-meta">Total visits: ${visits.length} &middot; Most recent: ${new Date(visits[0].timestamp).toLocaleString()}</div>
214
+ <details>
215
+ <summary>View recent visits (last ${Math.min(visits.length, 10)})</summary>
216
+ ${visits.slice(0, 10).map(visit => `
217
+ <div class="visit-row">
218
+ <div class="path">${visit.path}</div>
219
+ <div class="vmeta">IP: <code>${visit.ip}</code> &middot; ${visit.verified ? 'DNS Verified' : 'Unverified'} &middot; ${new Date(visit.timestamp).toLocaleString()}</div>
220
+ </div>
221
+ `).join('')}
222
+ </details>
223
+ </div>
224
+ `).join('')}
225
+
226
+ <div class="section-hdr">AI Threat Analysis</div>
227
+ ${aiReports.length === 0 ? `
228
+ <div class="empty">
229
+ <p>No AI analysis reports yet.</p>
230
+ <p class="hint">Reports will appear here when threats or traps are detected and analyzed by Gemini.</p>
231
+ </div>
232
+ ` : aiReports.slice(0, 10).map(r => `
233
+ <div class="ai-card">
234
+ <div class="ai-header">
235
+ <span class="ip-tag">IP: ${r.ip}</span>
236
+ <span class="ts">${new Date(r.timestamp).toLocaleString()}</span>
227
237
  </div>
228
- `).join('')}
229
- </div>
238
+ <div class="ai-body">${r.report}</div>
239
+ </div>
240
+ `).join('')}
230
241
 
231
- <h2>🚫 Banned IP Addresses</h2>
232
- ${bannedIPs.length === 0 ? '<p>No banned IPs yet.</p>' : `
233
- <table>
242
+ <div class="section-hdr">Banned IP Addresses</div>
243
+ ${bannedIPs.length === 0 ? `
244
+ <div class="empty"><p>No banned IPs yet.</p></div>
245
+ ` : `
246
+ <table class="ban-table">
234
247
  <thead>
235
248
  <tr>
236
249
  <th>IP Address</th>
237
250
  <th>Reason</th>
238
251
  <th>Banned At</th>
239
- <th>Expires At</th>
252
+ <th>Expires</th>
240
253
  </tr>
241
254
  </thead>
242
255
  <tbody>
243
256
  ${bannedIPs.map(entry => {
244
257
  const ip = typeof entry === 'string' ? entry : entry.ip;
245
258
  const reason = typeof entry === 'string' ? 'Suspicious activity' : entry.reason;
246
- const timestamp = typeof entry === 'string' ? 'N/A' : new Date(entry.timestamp).toLocaleString();
259
+ const ts = typeof entry === 'string' ? 'N/A' : new Date(entry.timestamp).toLocaleString();
247
260
  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
- `;
261
+ return `<tr><td><code>${ip}</code></td><td class="reason">${reason}</td><td class="ts">${ts}</td><td class="ts">${expiry}</td></tr>`;
257
262
  }).join('')}
258
263
  </tbody>
259
264
  </table>
260
265
  `}
261
266
 
262
- <p style="margin-top: 40px; color: #666; text-align: center;">
263
- HoneyWeb v2.0.1 | Refresh page to update data
264
- </p>
267
+ <div class="dash-footer">HoneyWeb v2.0.1 &middot; Refresh page to update data</div>
268
+
269
+ </div>
265
270
  </body>
266
- </html>
267
- `;
271
+ </html>`;
268
272
 
269
273
  res.send(html);
270
274
  } catch (err) {
@@ -1,68 +1,46 @@
1
- // honeyweb-core/middleware/index.js
2
- // Middleware factory - orchestrates all middleware components
3
-
4
1
  const createBlocklistChecker = require('./blocklist-checker');
5
2
  const createRequestAnalyzer = require('./request-analyzer');
6
3
  const createTrapInjector = require('./trap-injector');
7
4
  const createDashboard = require('./dashboard');
8
5
 
9
- /**
10
- * Create HoneyWeb middleware stack
11
- * @param {Object} config - Configuration
12
- * @param {Object} storage - Storage instance
13
- * @param {Object} botTracker - Bot tracker instance
14
- * @param {Object} detector - Detection engine
15
- * @param {Object} aiAnalyzer - AI analyzer
16
- * @param {Object} logger - Logger instance
17
- * @param {Object} events - Event emitter
18
- * @returns {Function} - Express middleware function
19
- */
20
6
  function createMiddleware(config, storage, botTracker, detector, aiAnalyzer, logger, events) {
21
- // Create individual middleware components
22
7
  const blocklistChecker = createBlocklistChecker(storage, logger, events);
23
8
  const requestAnalyzer = createRequestAnalyzer(config, storage, botTracker, detector, aiAnalyzer, logger, events);
24
9
  const trapInjector = createTrapInjector(config);
10
+ const trapPaths = config.traps.paths;
25
11
  const dashboard = config.dashboard.enabled
26
- ? createDashboard(config, storage, botTracker, detector)
12
+ ? createDashboard(config, storage, botTracker, detector, events)
27
13
  : null;
28
14
 
29
- // Return combined middleware function
30
15
  return async function honeyWebMiddleware(req, res, next) {
31
16
  try {
32
- // 1. Dashboard (if enabled and path matches)
17
+ // Dashboard route
33
18
  if (dashboard && req.path === config.dashboard.path) {
34
19
  return dashboard(req, res, next);
35
20
  }
36
21
 
37
- // 2. Check blocklist
38
- await new Promise((resolve, reject) => {
39
- blocklistChecker(req, res, (err) => {
40
- if (err) reject(err);
41
- else resolve();
22
+ // Trap paths: always analyze first
23
+ if (trapPaths.includes(req.path)) {
24
+ await new Promise((resolve, reject) => {
25
+ requestAnalyzer(req, res, (err) => err ? reject(err) : resolve());
42
26
  });
43
- });
44
-
45
- // If response was sent (blocked), stop here
46
- if (res.headersSent) {
47
- return;
27
+ if (res.headersSent) return;
48
28
  }
49
29
 
50
- // 3. Analyze request (traps + threats)
30
+ // Blocklist check
51
31
  await new Promise((resolve, reject) => {
52
- requestAnalyzer(req, res, (err) => {
53
- if (err) reject(err);
54
- else resolve();
55
- });
32
+ blocklistChecker(req, res, (err) => err ? reject(err) : resolve());
56
33
  });
34
+ if (res.headersSent) return;
57
35
 
58
- // If response was sent (blocked), stop here
59
- if (res.headersSent) {
60
- return;
61
- }
36
+ // Full request analysis
37
+ await new Promise((resolve, reject) => {
38
+ requestAnalyzer(req, res, (err) => err ? reject(err) : resolve());
39
+ });
40
+ if (res.headersSent) return;
62
41
 
63
- // 4. Inject traps into HTML responses
42
+ // Inject traps into HTML responses
64
43
  trapInjector(req, res, next);
65
-
66
44
  } catch (err) {
67
45
  logger.error('Middleware error', { error: err.message });
68
46
  next(err);