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.
- package/ai/gemini.js +54 -29
- package/ai/index.js +4 -27
- package/ai/prompt-templates.js +22 -42
- package/config/defaults.js +14 -37
- package/config/index.js +7 -35
- package/config/schema.js +1 -17
- package/detection/behavioral.js +26 -118
- package/detection/bot-detector.js +11 -62
- package/detection/index.js +19 -35
- package/detection/patterns.js +14 -51
- package/detection/rate-limiter.js +15 -86
- package/detection/traversal.js +42 -0
- package/index.js +1 -13
- package/middleware/blocklist-checker.js +1 -15
- package/middleware/dashboard.js +218 -214
- package/middleware/index.js +17 -39
- package/middleware/request-analyzer.js +29 -70
- package/middleware/trap-injector.js +3 -28
- package/package.json +1 -1
- package/storage/bot-tracker.js +9 -56
- package/storage/index.js +3 -12
- package/storage/json-store.js +15 -89
- package/storage/memory-store.js +9 -61
- package/utils/cache.js +5 -48
- package/utils/dns-verify.js +8 -45
- package/utils/event-emitter.js +7 -39
- package/utils/logger.js +9 -35
package/middleware/dashboard.js
CHANGED
|
@@ -1,270 +1,274 @@
|
|
|
1
1
|
// honeyweb-core/middleware/dashboard.js
|
|
2
|
-
// Dashboard middleware (extracted from main index.js)
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 — 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.stats
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
.
|
|
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
|
-
|
|
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
|
|
172
|
-
<
|
|
173
|
-
<div class="
|
|
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
|
|
176
|
-
<
|
|
177
|
-
<div class="
|
|
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
|
|
180
|
-
<
|
|
181
|
-
<div class="
|
|
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
|
|
184
|
-
<
|
|
185
|
-
<div class="
|
|
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
|
|
189
|
-
<
|
|
190
|
-
<div class="
|
|
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="
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
<
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
</
|
|
213
|
+
<div class="bot-meta">Total visits: ${visits.length} · 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> · ${visit.verified ? 'DNS Verified' : 'Unverified'} · ${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
|
-
|
|
229
|
-
|
|
238
|
+
<div class="ai-body">${r.report}</div>
|
|
239
|
+
</div>
|
|
240
|
+
`).join('')}
|
|
230
241
|
|
|
231
|
-
<
|
|
232
|
-
${bannedIPs.length === 0 ?
|
|
233
|
-
|
|
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
|
|
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
|
|
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
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
<div class="dash-footer">HoneyWeb v2.0.1 · 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) {
|
package/middleware/index.js
CHANGED
|
@@ -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
|
-
//
|
|
17
|
+
// Dashboard route
|
|
33
18
|
if (dashboard && req.path === config.dashboard.path) {
|
|
34
19
|
return dashboard(req, res, next);
|
|
35
20
|
}
|
|
36
21
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
30
|
+
// Blocklist check
|
|
51
31
|
await new Promise((resolve, reject) => {
|
|
52
|
-
|
|
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
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
//
|
|
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);
|