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 +69 -19
- package/ai/prompt-templates.js +22 -42
- package/config/defaults.js +3 -3
- package/middleware/dashboard.js +235 -214
- package/middleware/index.js +18 -4
- package/middleware/request-analyzer.js +5 -1
- package/package.json +1 -1
package/ai/gemini.js
CHANGED
|
@@ -1,43 +1,93 @@
|
|
|
1
1
|
// honeyweb-core/ai/gemini.js
|
|
2
|
-
// Google Gemini API client
|
|
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 ||
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
/**
|
package/ai/prompt-templates.js
CHANGED
|
@@ -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,
|
|
5
|
+
const { ip, path, threats, userAgent, method, timestamp } = data;
|
|
11
6
|
|
|
12
|
-
return `
|
|
7
|
+
return `Cybersecurity analyst for HoneyWeb honeypot. Analyze this attack. Plain text only, no markdown or asterisks. Use UPPERCASE headers.
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
51
|
-
|
|
28
|
+
TRAP HIT: ${path} from ${ip}
|
|
29
|
+
User-Agent: ${userAgent || 'N/A'}
|
|
30
|
+
Time: ${new Date(timestamp).toISOString()}
|
|
52
31
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
38
|
+
Keep to 3-5 sentences total.`;
|
|
59
39
|
}
|
|
60
40
|
|
|
61
41
|
module.exports = {
|
package/config/defaults.js
CHANGED
|
@@ -6,8 +6,8 @@ module.exports = {
|
|
|
6
6
|
ai: {
|
|
7
7
|
enabled: true,
|
|
8
8
|
apiKey: process.env.HONEYWEB_API_KEY || '',
|
|
9
|
-
model: 'gemini-
|
|
10
|
-
timeout:
|
|
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:
|
|
48
|
+
banDuration: 300000, // 24 hours
|
|
49
49
|
autoCleanup: true
|
|
50
50
|
},
|
|
51
51
|
|
package/middleware/dashboard.js
CHANGED
|
@@ -1,270 +1,291 @@
|
|
|
1
1
|
// honeyweb-core/middleware/dashboard.js
|
|
2
|
-
// Dashboard middleware (
|
|
2
|
+
// Dashboard middleware with cookie-based auth (no credentials in URL)
|
|
3
3
|
|
|
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) {
|
|
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 — 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
.stat
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
172
|
-
<
|
|
173
|
-
<div class="
|
|
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
|
|
176
|
-
<
|
|
177
|
-
<div class="
|
|
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
|
|
180
|
-
<
|
|
181
|
-
<div class="
|
|
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
|
|
184
|
-
<
|
|
185
|
-
<div class="
|
|
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
|
|
189
|
-
<
|
|
190
|
-
<div class="
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
<
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
<div class="bot-meta">Total visits: ${visits.length} · 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> · ${visit.verified ? 'DNS Verified' : 'Unverified'} · ${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
|
-
|
|
229
|
-
|
|
254
|
+
<div class="ai-body">${r.report}</div>
|
|
255
|
+
</div>
|
|
256
|
+
`).join('')}
|
|
230
257
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
|
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
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
284
|
+
<div class="dash-footer">HoneyWeb v2.0.1 · 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) {
|
package/middleware/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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