honeymcp 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- honeymcp/api/__init__.py +1 -0
- honeymcp/api/app.py +218 -0
- honeymcp/cli_tool_creator.py +110 -0
- honeymcp/core/catalog_updater.py +290 -0
- honeymcp/core/ghost_tools.py +437 -0
- honeymcp/core/middleware.py +57 -0
- honeymcp/core/tool_creator.py +499 -0
- honeymcp/dashboard/react_umd/app.js +375 -0
- honeymcp/dashboard/react_umd/index.html +24 -0
- honeymcp/dashboard/react_umd/styles.css +512 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/METADATA +148 -27
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/RECORD +15 -8
- honeymcp/dashboard/app.py +0 -228
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/WHEEL +0 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/entry_points.txt +0 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
const { useEffect, useMemo, useState, useCallback } = React;
|
|
2
|
+
|
|
3
|
+
// --- Utility Functions ---
|
|
4
|
+
|
|
5
|
+
function classNames(...classes) {
|
|
6
|
+
return classes.filter(Boolean).join(" ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function timeAgo(dateString) {
|
|
10
|
+
const date = new Date(dateString);
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const seconds = Math.floor((now - date) / 1000);
|
|
13
|
+
|
|
14
|
+
let interval = seconds / 31536000;
|
|
15
|
+
if (interval > 1) return Math.floor(interval) + " years ago";
|
|
16
|
+
interval = seconds / 2592000;
|
|
17
|
+
if (interval > 1) return Math.floor(interval) + " months ago";
|
|
18
|
+
interval = seconds / 86400;
|
|
19
|
+
if (interval > 1) return Math.floor(interval) + " days ago";
|
|
20
|
+
interval = seconds / 3600;
|
|
21
|
+
if (interval > 1) return Math.floor(interval) + " hours ago";
|
|
22
|
+
interval = seconds / 60;
|
|
23
|
+
if (interval > 1) return Math.floor(interval) + " minutes ago";
|
|
24
|
+
return Math.floor(seconds) + " seconds ago";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatDateTime(dateString) {
|
|
28
|
+
return new Date(dateString).toLocaleString(undefined, {
|
|
29
|
+
weekday: 'short',
|
|
30
|
+
year: 'numeric',
|
|
31
|
+
month: 'short',
|
|
32
|
+
day: 'numeric',
|
|
33
|
+
hour: '2-digit',
|
|
34
|
+
minute: '2-digit'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildQuery(params) {
|
|
39
|
+
const query = new URLSearchParams();
|
|
40
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
41
|
+
if (value !== null && value !== undefined && String(value).trim() !== "") {
|
|
42
|
+
query.set(key, String(value));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const encoded = query.toString();
|
|
46
|
+
return encoded ? `?${encoded}` : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Icons (SVG Components) ---
|
|
50
|
+
|
|
51
|
+
const IconActivity = () => (
|
|
52
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const IconAlert = () => (
|
|
56
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const IconShield = () => (
|
|
60
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const IconTerminal = () => (
|
|
64
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const IconChevronDown = ({ className }) => (
|
|
68
|
+
<svg className={className} width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// --- Components ---
|
|
72
|
+
|
|
73
|
+
function Badge({ children, type = "medium" }) {
|
|
74
|
+
return (
|
|
75
|
+
<span className={`badge ${type.toLowerCase()}`}>
|
|
76
|
+
{children}
|
|
77
|
+
</span>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function MetricCard({ label, value, icon, loading }) {
|
|
82
|
+
return (
|
|
83
|
+
<div className="metric-card">
|
|
84
|
+
<div className="header-top">
|
|
85
|
+
<span className="metric-label">{label}</span>
|
|
86
|
+
{icon && <span className="text-brand">{icon}</span>}
|
|
87
|
+
</div>
|
|
88
|
+
{loading ? (
|
|
89
|
+
<div className="skeleton" style={{ height: '36px', width: '60%' }}></div>
|
|
90
|
+
) : (
|
|
91
|
+
<div className="metric-value">{value}</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function EventRow({ event }) {
|
|
98
|
+
const [expanded, setExpanded] = useState(false);
|
|
99
|
+
const threatLevel = event.threat_level?.toLowerCase() || 'low';
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="event-row">
|
|
103
|
+
<div
|
|
104
|
+
className="event-summary"
|
|
105
|
+
onClick={() => setExpanded(!expanded)}
|
|
106
|
+
>
|
|
107
|
+
<Badge type={threatLevel}>{event.threat_level}</Badge>
|
|
108
|
+
|
|
109
|
+
<div className="event-main">
|
|
110
|
+
<div className="event-title">{event.ghost_tool_called}</div>
|
|
111
|
+
<div className="event-meta">
|
|
112
|
+
<span>{timeAgo(event.timestamp)}</span>
|
|
113
|
+
<span>•</span>
|
|
114
|
+
<span>{event.attack_category}</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<IconChevronDown className={`transition-transform ${expanded ? 'rotate-180' : ''}`} style={{ transition: 'transform 0.2s', transform: expanded ? 'rotate(180deg)' : 'rotate(0)' }} />
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{expanded && (
|
|
122
|
+
<div className="event-details">
|
|
123
|
+
<div className="grid" style={{ gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
|
124
|
+
<div className="detail-group">
|
|
125
|
+
<div className="detail-label">Session ID</div>
|
|
126
|
+
<div className="font-mono text-sm">{event.session_id}</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="detail-group">
|
|
129
|
+
<div className="detail-label">Full Timestamp</div>
|
|
130
|
+
<div className="text-sm">{formatDateTime(event.timestamp)}</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div className="detail-group">
|
|
135
|
+
<div className="detail-label">Tool Sequence</div>
|
|
136
|
+
<div className="text-sm">
|
|
137
|
+
{(event.tool_call_sequence || []).join(" → ") || "Single Call"}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="detail-group">
|
|
142
|
+
<div className="detail-label">Arguments</div>
|
|
143
|
+
<pre className="code-block">
|
|
144
|
+
{JSON.stringify(event.arguments || {}, null, 2)}
|
|
145
|
+
</pre>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{event.response_sent && (
|
|
149
|
+
<div className="detail-group">
|
|
150
|
+
<div className="detail-label">Response</div>
|
|
151
|
+
<pre className="code-block">
|
|
152
|
+
{event.response_sent}
|
|
153
|
+
</pre>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function FilterBar({ filters, options, onChange, onRefresh, loading, apiBase, onApiBaseChange }) {
|
|
163
|
+
return (
|
|
164
|
+
<section className="panel glass-panel">
|
|
165
|
+
<div className="grid filters">
|
|
166
|
+
{/* API Base input removed as requested */}
|
|
167
|
+
|
|
168
|
+
<div className="form-group">
|
|
169
|
+
<label>Threat Level</label>
|
|
170
|
+
<select name="threat_level" value={filters.threat_level} onChange={onChange}>
|
|
171
|
+
<option value="">All Levels</option>
|
|
172
|
+
{options.threat_levels.map(l => <option key={l} value={l}>{l}</option>)}
|
|
173
|
+
</select>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="form-group">
|
|
177
|
+
<label>Category</label>
|
|
178
|
+
<select name="category" value={filters.category} onChange={onChange}>
|
|
179
|
+
<option value="">All Categories</option>
|
|
180
|
+
{options.categories.map(c => <option key={c} value={c}>{c}</option>)}
|
|
181
|
+
</select>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="form-group">
|
|
185
|
+
<button className="primary" onClick={onRefresh} disabled={loading}>
|
|
186
|
+
{loading ? "Refreshing..." : "Apply Filters"}
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</section>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Main App ---
|
|
195
|
+
|
|
196
|
+
function App() {
|
|
197
|
+
// State
|
|
198
|
+
const [apiBase, setApiBase] = useState(() => {
|
|
199
|
+
return window.location.protocol.startsWith('http')
|
|
200
|
+
? window.location.origin.replace(/\/$/, "")
|
|
201
|
+
: "http://127.0.0.1:8001";
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const [filters, setFilters] = useState({
|
|
205
|
+
start_date: "",
|
|
206
|
+
end_date: "",
|
|
207
|
+
threat_level: "",
|
|
208
|
+
category: "",
|
|
209
|
+
tool: "",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const [options, setOptions] = useState({
|
|
213
|
+
threat_levels: [],
|
|
214
|
+
categories: [],
|
|
215
|
+
tools: [],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const [data, setData] = useState({
|
|
219
|
+
metrics: null,
|
|
220
|
+
events: [],
|
|
221
|
+
total: 0
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const [loading, setLoading] = useState(false);
|
|
225
|
+
const [error, setError] = useState(null);
|
|
226
|
+
|
|
227
|
+
// Data Fetching
|
|
228
|
+
const fetchJson = useCallback(async (path, queryParams = {}) => {
|
|
229
|
+
const url = `${apiBase}${path}${buildQuery(queryParams)}`;
|
|
230
|
+
try {
|
|
231
|
+
const response = await fetch(url);
|
|
232
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
233
|
+
return await response.json();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}, [apiBase]);
|
|
238
|
+
|
|
239
|
+
const loadData = useCallback(async () => {
|
|
240
|
+
setLoading(true);
|
|
241
|
+
setError(null);
|
|
242
|
+
try {
|
|
243
|
+
// Parallel fetch
|
|
244
|
+
const [filterData, metricsData, eventsResp] = await Promise.all([
|
|
245
|
+
fetchJson("/filters", { start_date: filters.start_date, end_date: filters.end_date }).catch(() => null),
|
|
246
|
+
fetchJson("/metrics", filters),
|
|
247
|
+
fetchJson("/events", { ...filters, limit: 50, offset: 0 })
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
if (filterData) setOptions(filterData);
|
|
251
|
+
|
|
252
|
+
setData({
|
|
253
|
+
metrics: metricsData,
|
|
254
|
+
events: eventsResp.events || [],
|
|
255
|
+
total: eventsResp.total || 0
|
|
256
|
+
});
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(err);
|
|
259
|
+
setError("Unable to connect to HoneyMCP API. Ensure the server is running.");
|
|
260
|
+
|
|
261
|
+
// Fallback for Development/Demo purposes if API fails
|
|
262
|
+
// remove this block in strict production
|
|
263
|
+
if (window.location.protocol === 'file:') {
|
|
264
|
+
console.warn("Using mock data for local file preview");
|
|
265
|
+
// ... mock data logic could go here
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
setLoading(false);
|
|
269
|
+
}
|
|
270
|
+
}, [fetchJson, filters]);
|
|
271
|
+
|
|
272
|
+
// Initial Load
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
loadData();
|
|
275
|
+
}, [loadData]);
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
// Handlers
|
|
279
|
+
const handleFilterChange = (e) => {
|
|
280
|
+
const { name, value } = e.target;
|
|
281
|
+
setFilters(prev => ({ ...prev, [name]: value }));
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div className="container">
|
|
286
|
+
<header className="header">
|
|
287
|
+
<div className="header-top">
|
|
288
|
+
<div>
|
|
289
|
+
<h1 className="title">HoneyMCP Dashboard</h1>
|
|
290
|
+
<p className="subtitle">Real-time Threat Monitoring Intelligence</p>
|
|
291
|
+
</div>
|
|
292
|
+
<div className="status-indicator">
|
|
293
|
+
{/* Could add a 'Live' dot here */}
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</header>
|
|
297
|
+
|
|
298
|
+
<FilterBar
|
|
299
|
+
filters={filters}
|
|
300
|
+
options={options}
|
|
301
|
+
onChange={handleFilterChange}
|
|
302
|
+
onRefresh={loadData}
|
|
303
|
+
loading={loading}
|
|
304
|
+
apiBase={apiBase}
|
|
305
|
+
onApiBaseChange={setApiBase}
|
|
306
|
+
/>
|
|
307
|
+
|
|
308
|
+
{error && (
|
|
309
|
+
<div className="panel" style={{ borderColor: 'var(--critical-border)', background: 'var(--critical-bg)', color: 'var(--critical-text)' }}>
|
|
310
|
+
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
|
|
311
|
+
<IconAlert />
|
|
312
|
+
<span>{error}</span>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{/* Metrics Grid */}
|
|
318
|
+
<section className="grid metrics" style={{ marginBottom: '32px' }}>
|
|
319
|
+
<MetricCard
|
|
320
|
+
label="Total Attacks"
|
|
321
|
+
value={data.metrics?.total_attacks || 0}
|
|
322
|
+
icon={<IconShield />}
|
|
323
|
+
loading={loading && !data.metrics}
|
|
324
|
+
/>
|
|
325
|
+
<MetricCard
|
|
326
|
+
label="Last 24h"
|
|
327
|
+
value={data.metrics?.attacks_last_24h || 0}
|
|
328
|
+
icon={<IconActivity />}
|
|
329
|
+
loading={loading && !data.metrics}
|
|
330
|
+
/>
|
|
331
|
+
<MetricCard
|
|
332
|
+
label="Critical Threats"
|
|
333
|
+
value={data.metrics?.critical_threats || 0}
|
|
334
|
+
icon={<IconAlert />}
|
|
335
|
+
loading={loading && !data.metrics}
|
|
336
|
+
/>
|
|
337
|
+
<MetricCard
|
|
338
|
+
label="Active Sessions"
|
|
339
|
+
value={data.metrics?.unique_sessions || 0}
|
|
340
|
+
icon={<IconTerminal />}
|
|
341
|
+
loading={loading && !data.metrics}
|
|
342
|
+
/>
|
|
343
|
+
</section>
|
|
344
|
+
|
|
345
|
+
{/* Events Feed */}
|
|
346
|
+
<section>
|
|
347
|
+
<div className="header-top" style={{ marginBottom: '16px' }}>
|
|
348
|
+
<h3>Recent Events</h3>
|
|
349
|
+
<span className="badge low">{data.total} Total</span>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div className="event-list">
|
|
353
|
+
{data.events.length === 0 && !loading ? (
|
|
354
|
+
<div className="panel empty-state">
|
|
355
|
+
<p>No events found for the selected timeframe.</p>
|
|
356
|
+
</div>
|
|
357
|
+
) : (
|
|
358
|
+
data.events.map(event => (
|
|
359
|
+
<EventRow key={event.event_id} event={event} />
|
|
360
|
+
))
|
|
361
|
+
)}
|
|
362
|
+
|
|
363
|
+
{loading && data.events.length === 0 && (
|
|
364
|
+
<div className="panel empty-state">
|
|
365
|
+
<div className="skeleton" style={{ height: '20px', width: '200px', margin: '0 auto' }}></div>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
</section>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const root = ReactDOM.createRoot(document.getElementById("root"));
|
|
375
|
+
root.render(<App />);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="description" content="HoneyMCP Premium Dashboard - Real-time threat monitoring and analysis" />
|
|
7
|
+
<title>HoneyMCP Dashboard</title>
|
|
8
|
+
|
|
9
|
+
<!-- Fonts -->
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
11
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
13
|
+
|
|
14
|
+
<link rel="stylesheet" href="/dashboard/assets/styles.css" />
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div id="root"></div>
|
|
18
|
+
|
|
19
|
+
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
20
|
+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
21
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
22
|
+
<script type="text/babel" data-presets="env,react" src="/dashboard/assets/app.js"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|