@chdlabs/my-shield 0.1.0
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/LICENSE +21 -0
- package/README.md +197 -0
- package/package.json +45 -0
- package/src/admin/adminRouter.js +221 -0
- package/src/admin/dashboard/dashboard.css +534 -0
- package/src/admin/dashboard/dashboard.js +610 -0
- package/src/admin/dashboard/index.html +266 -0
- package/src/client/shield-client.js +63 -0
- package/src/core/bloomFilter.js +191 -0
- package/src/core/lruCache.js +178 -0
- package/src/core/ringBuffer.js +87 -0
- package/src/index.d.ts +97 -0
- package/src/index.js +317 -0
- package/src/intelligence/autoPromoter.js +91 -0
- package/src/intelligence/collector.js +219 -0
- package/src/intelligence/filePersistence.js +226 -0
- package/src/middleware/costThrottle.js +75 -0
- package/src/middleware/honeypot.js +215 -0
- package/src/middleware/keyLeakDetector.js +98 -0
- package/src/middleware/notFoundCollector.js +171 -0
- package/src/middleware/shieldGuard.js +93 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shield Security Dashboard Client Controller
|
|
3
|
+
* Secure DOM manipulation using native standards (no innerHTML).
|
|
4
|
+
* Real-time event streaming via Server-Sent Events (SSE).
|
|
5
|
+
*
|
|
6
|
+
* @author ChdLabs
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
(function() {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// State
|
|
13
|
+
let activeTab = 'overview';
|
|
14
|
+
let sseSource = null;
|
|
15
|
+
let attackVelocityCounter = 0;
|
|
16
|
+
const recentEventTimestamps = [];
|
|
17
|
+
|
|
18
|
+
// DOM References
|
|
19
|
+
const tabs = document.querySelectorAll('.nav-tab');
|
|
20
|
+
const panels = document.querySelectorAll('.view-panel');
|
|
21
|
+
const btnRefresh = document.getElementById('btn-refresh');
|
|
22
|
+
const btnClearFeed = document.getElementById('btn-clear-feed');
|
|
23
|
+
const eventFeed = document.getElementById('event-feed');
|
|
24
|
+
const toastContainer = document.getElementById('toast-container');
|
|
25
|
+
|
|
26
|
+
// Modals
|
|
27
|
+
const modalBlockIp = document.getElementById('modal-block-ip');
|
|
28
|
+
const btnOpenBlockModal = document.getElementById('btn-open-block-modal');
|
|
29
|
+
const btnCloseModal = document.getElementById('btn-close-modal');
|
|
30
|
+
const btnCancelModal = document.getElementById('btn-cancel-modal');
|
|
31
|
+
const formBlockIp = document.getElementById('form-block-ip');
|
|
32
|
+
const formAddTrap = document.getElementById('form-add-trap');
|
|
33
|
+
|
|
34
|
+
// UI Toast Notification helper (replaces alert())
|
|
35
|
+
function showToast(message, type = 'info') {
|
|
36
|
+
const toast = document.createElement('div');
|
|
37
|
+
toast.className = 'toast';
|
|
38
|
+
toast.textContent = message;
|
|
39
|
+
if (type === 'error') {
|
|
40
|
+
toast.style.borderColor = 'var(--accent-red)';
|
|
41
|
+
} else if (type === 'success') {
|
|
42
|
+
toast.style.borderColor = 'var(--accent-green)';
|
|
43
|
+
}
|
|
44
|
+
toastContainer.appendChild(toast);
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
if (toast.parentNode) {
|
|
47
|
+
toast.parentNode.removeChild(toast);
|
|
48
|
+
}
|
|
49
|
+
}, 4000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getStoredToken() {
|
|
53
|
+
return localStorage.getItem('pqc_session_token');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function apiFetch(url, options = {}) {
|
|
57
|
+
const token = getStoredToken();
|
|
58
|
+
const headers = { ...options.headers };
|
|
59
|
+
if (token) {
|
|
60
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
61
|
+
}
|
|
62
|
+
const res = await fetch(url, { ...options, headers });
|
|
63
|
+
if (res.status === 401) {
|
|
64
|
+
showToast('Authentication required. Please login with an admin account.', 'error');
|
|
65
|
+
} else if (res.status === 403) {
|
|
66
|
+
showToast('Access denied: Administrator privileges required.', 'error');
|
|
67
|
+
}
|
|
68
|
+
return res;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Tab Navigation
|
|
72
|
+
tabs.forEach(tab => {
|
|
73
|
+
tab.addEventListener('click', () => {
|
|
74
|
+
const targetView = tab.getAttribute('data-view');
|
|
75
|
+
if (!targetView) return;
|
|
76
|
+
|
|
77
|
+
tabs.forEach(t => t.classList.remove('active'));
|
|
78
|
+
panels.forEach(p => p.classList.remove('active'));
|
|
79
|
+
|
|
80
|
+
tab.classList.add('active');
|
|
81
|
+
const targetPanel = document.getElementById(`view-${targetView}`);
|
|
82
|
+
if (targetPanel) {
|
|
83
|
+
targetPanel.classList.add('active');
|
|
84
|
+
}
|
|
85
|
+
activeTab = targetView;
|
|
86
|
+
loadViewData(targetView);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Modal Handlers
|
|
91
|
+
if (btnOpenBlockModal) {
|
|
92
|
+
btnOpenBlockModal.addEventListener('click', () => {
|
|
93
|
+
modalBlockIp.removeAttribute('hidden');
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function closeModal() {
|
|
98
|
+
modalBlockIp.setAttribute('hidden', '');
|
|
99
|
+
formBlockIp.reset();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (btnCloseModal) btnCloseModal.addEventListener('click', closeModal);
|
|
103
|
+
if (btnCancelModal) btnCancelModal.addEventListener('click', closeModal);
|
|
104
|
+
|
|
105
|
+
// Form: Manual Block IP
|
|
106
|
+
if (formBlockIp) {
|
|
107
|
+
formBlockIp.addEventListener('submit', async (e) => {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
const ipInput = document.getElementById('input-block-ip');
|
|
110
|
+
const reasonInput = document.getElementById('input-block-reason');
|
|
111
|
+
const ip = ipInput.value.trim();
|
|
112
|
+
const reason = reasonInput.value.trim() || 'manual_admin_ban';
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const res = await apiFetch('/admin/shield/api/block', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ ip, reason })
|
|
119
|
+
});
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
if (res.ok) {
|
|
122
|
+
showToast(`Blocked IP: ${ip}`, 'success');
|
|
123
|
+
closeModal();
|
|
124
|
+
loadOverview();
|
|
125
|
+
} else {
|
|
126
|
+
showToast(data.error || 'Failed to block IP', 'error');
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
showToast(`Request failed: ${err.message}`, 'error');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Form: Add Honeypot Trap
|
|
135
|
+
if (formAddTrap) {
|
|
136
|
+
formAddTrap.addEventListener('submit', async (e) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
const pathInput = document.getElementById('input-trap-path');
|
|
139
|
+
const path = pathInput.value.trim();
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const res = await apiFetch('/admin/shield/api/honeypot/add', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({ path })
|
|
146
|
+
});
|
|
147
|
+
const data = await res.json();
|
|
148
|
+
if (res.ok) {
|
|
149
|
+
showToast(`Added trap: ${path}`, 'success');
|
|
150
|
+
pathInput.value = '';
|
|
151
|
+
loadHoneypots();
|
|
152
|
+
} else {
|
|
153
|
+
showToast(data.error || 'Failed to add trap', 'error');
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
showToast(`Request failed: ${err.message}`, 'error');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Clear Feed Button
|
|
162
|
+
if (btnClearFeed) {
|
|
163
|
+
btnClearFeed.addEventListener('click', () => {
|
|
164
|
+
eventFeed.replaceChildren();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Refresh Button
|
|
169
|
+
if (btnRefresh) {
|
|
170
|
+
btnRefresh.addEventListener('click', () => {
|
|
171
|
+
loadViewData(activeTab);
|
|
172
|
+
showToast('Refreshed telemetry data', 'info');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Dispatcher for View Data Loading
|
|
177
|
+
function loadViewData(view) {
|
|
178
|
+
switch(view) {
|
|
179
|
+
case 'overview':
|
|
180
|
+
loadOverview();
|
|
181
|
+
break;
|
|
182
|
+
case 'intelligence':
|
|
183
|
+
loadIntelligence();
|
|
184
|
+
break;
|
|
185
|
+
case 'honeypots':
|
|
186
|
+
loadHoneypots();
|
|
187
|
+
break;
|
|
188
|
+
case 'health':
|
|
189
|
+
loadHealth();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── 1. LOAD OVERVIEW ──
|
|
195
|
+
async function loadOverview() {
|
|
196
|
+
try {
|
|
197
|
+
const [statsRes, blockedRes] = await Promise.all([
|
|
198
|
+
apiFetch('/admin/shield/api/stats'),
|
|
199
|
+
apiFetch('/admin/shield/api/blocked')
|
|
200
|
+
]);
|
|
201
|
+
const stats = await statsRes.json();
|
|
202
|
+
const blocked = await blockedRes.json();
|
|
203
|
+
|
|
204
|
+
// Populate Metric Cards
|
|
205
|
+
document.getElementById('val-blocked-ips').textContent = stats.blockedIPs ?? 0;
|
|
206
|
+
document.getElementById('val-total-events').textContent = stats.eventsTotal ?? 0;
|
|
207
|
+
|
|
208
|
+
if (stats.bloom) {
|
|
209
|
+
document.getElementById('val-bloom-fill').textContent = `${stats.bloom.fillPercentage}%`;
|
|
210
|
+
document.getElementById('val-bloom-fpr').textContent = `Est. False Positive: ${(stats.bloom.estimatedFPRate * 100).toFixed(4)}%`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Populate Blocked IPs Table
|
|
214
|
+
const tbody = document.getElementById('tbody-blocked-ips');
|
|
215
|
+
tbody.replaceChildren();
|
|
216
|
+
|
|
217
|
+
const list = blocked.blockedIPs || [];
|
|
218
|
+
if (list.length === 0) {
|
|
219
|
+
const tr = document.createElement('tr');
|
|
220
|
+
const td = document.createElement('td');
|
|
221
|
+
td.setAttribute('colspan', '4');
|
|
222
|
+
td.className = 'text-muted';
|
|
223
|
+
td.textContent = 'No active IP blocks. System is clean.';
|
|
224
|
+
tr.appendChild(td);
|
|
225
|
+
tbody.appendChild(tr);
|
|
226
|
+
} else {
|
|
227
|
+
list.forEach(item => {
|
|
228
|
+
const tr = document.createElement('tr');
|
|
229
|
+
|
|
230
|
+
const tdIp = document.createElement('td');
|
|
231
|
+
tdIp.textContent = item.ip;
|
|
232
|
+
tdIp.style.fontFamily = 'var(--font-mono)';
|
|
233
|
+
|
|
234
|
+
const tdReason = document.createElement('td');
|
|
235
|
+
tdReason.textContent = item.reason;
|
|
236
|
+
|
|
237
|
+
const tdTtl = document.createElement('td');
|
|
238
|
+
const remSec = Math.max(0, Math.round((item.remainingTtlMs || 0) / 1000));
|
|
239
|
+
tdTtl.textContent = `${remSec}s remaining`;
|
|
240
|
+
|
|
241
|
+
const tdActions = document.createElement('td');
|
|
242
|
+
const btnUnblock = document.createElement('button');
|
|
243
|
+
btnUnblock.className = 'btn btn-sm btn-secondary';
|
|
244
|
+
btnUnblock.textContent = 'Unblock';
|
|
245
|
+
btnUnblock.addEventListener('click', async () => {
|
|
246
|
+
try {
|
|
247
|
+
const res = await apiFetch(`/admin/shield/api/block/${encodeURIComponent(item.ip)}`, { method: 'DELETE' });
|
|
248
|
+
if (res.ok) {
|
|
249
|
+
showToast(`Unblocked ${item.ip}`, 'info');
|
|
250
|
+
loadOverview();
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
showToast(`Failed: ${err.message}`, 'error');
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
tdActions.appendChild(btnUnblock);
|
|
257
|
+
|
|
258
|
+
tr.appendChild(tdIp);
|
|
259
|
+
tr.appendChild(tdReason);
|
|
260
|
+
tr.appendChild(tdTtl);
|
|
261
|
+
tr.appendChild(tdActions);
|
|
262
|
+
tbody.appendChild(tr);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error('Failed to load overview data:', err);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── 2. LOAD 404 INTELLIGENCE ──
|
|
271
|
+
async function loadIntelligence() {
|
|
272
|
+
try {
|
|
273
|
+
const res = await apiFetch('/admin/shield/api/intelligence');
|
|
274
|
+
const data = await res.json();
|
|
275
|
+
|
|
276
|
+
document.getElementById('intel-total-hits').textContent = data.summary?.totalHits ?? 0;
|
|
277
|
+
document.getElementById('intel-unique-paths').textContent = data.summary?.uniquePaths ?? 0;
|
|
278
|
+
document.getElementById('intel-last-saved').textContent = data.lastSaved ? `Last snapshot: ${new Date(data.lastSaved).toLocaleTimeString()}` : 'Never saved';
|
|
279
|
+
|
|
280
|
+
// Top Paths Chart
|
|
281
|
+
const chartContainer = document.getElementById('intel-paths-chart');
|
|
282
|
+
chartContainer.replaceChildren();
|
|
283
|
+
|
|
284
|
+
const pathsEntries = Object.entries(data.paths || {})
|
|
285
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
286
|
+
.slice(0, 10);
|
|
287
|
+
|
|
288
|
+
if (pathsEntries.length === 0) {
|
|
289
|
+
const empty = document.createElement('div');
|
|
290
|
+
empty.className = 'text-muted';
|
|
291
|
+
empty.textContent = 'No 404 scan patterns captured yet.';
|
|
292
|
+
chartContainer.appendChild(empty);
|
|
293
|
+
} else {
|
|
294
|
+
const maxHits = Math.max(...pathsEntries.map(e => e[1].count), 1);
|
|
295
|
+
pathsEntries.forEach(([p, info]) => {
|
|
296
|
+
const row = document.createElement('div');
|
|
297
|
+
row.style.marginBottom = '0.75rem';
|
|
298
|
+
|
|
299
|
+
const labelBox = document.createElement('div');
|
|
300
|
+
labelBox.style.display = 'flex';
|
|
301
|
+
labelBox.style.justifyContent = 'space-between';
|
|
302
|
+
labelBox.style.fontSize = '0.8rem';
|
|
303
|
+
labelBox.style.marginBottom = '0.2rem';
|
|
304
|
+
|
|
305
|
+
const pathSpan = document.createElement('span');
|
|
306
|
+
pathSpan.textContent = p;
|
|
307
|
+
pathSpan.style.fontFamily = 'var(--font-mono)';
|
|
308
|
+
|
|
309
|
+
const countSpan = document.createElement('span');
|
|
310
|
+
countSpan.textContent = `${info.count} hits (${info.uniqueIPs || 1} IPs)`;
|
|
311
|
+
countSpan.className = 'text-muted';
|
|
312
|
+
|
|
313
|
+
labelBox.appendChild(pathSpan);
|
|
314
|
+
labelBox.appendChild(countSpan);
|
|
315
|
+
|
|
316
|
+
const barBg = document.createElement('div');
|
|
317
|
+
barBg.style.height = '8px';
|
|
318
|
+
barBg.style.backgroundColor = 'var(--bg-surface)';
|
|
319
|
+
barBg.style.borderRadius = '4px';
|
|
320
|
+
barBg.style.overflow = 'hidden';
|
|
321
|
+
|
|
322
|
+
const barFill = document.createElement('div');
|
|
323
|
+
barFill.style.height = '100%';
|
|
324
|
+
barFill.style.width = `${Math.max(5, (info.count / maxHits) * 100)}%`;
|
|
325
|
+
barFill.style.backgroundColor = info.autoPromoted ? 'var(--accent-red)' : 'var(--accent-cyan)';
|
|
326
|
+
barFill.style.borderRadius = '4px';
|
|
327
|
+
barBg.appendChild(barFill);
|
|
328
|
+
|
|
329
|
+
row.appendChild(labelBox);
|
|
330
|
+
row.appendChild(barBg);
|
|
331
|
+
chartContainer.appendChild(row);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Fingerprints Table
|
|
336
|
+
const tbodyFp = document.getElementById('tbody-fingerprints');
|
|
337
|
+
tbodyFp.replaceChildren();
|
|
338
|
+
|
|
339
|
+
const fpEntries = Object.entries(data.scannerFingerprints || {})
|
|
340
|
+
.sort((a, b) => b[1] - a[1])
|
|
341
|
+
.slice(0, 15);
|
|
342
|
+
|
|
343
|
+
if (fpEntries.length === 0) {
|
|
344
|
+
const tr = document.createElement('tr');
|
|
345
|
+
const td = document.createElement('td');
|
|
346
|
+
td.setAttribute('colspan', '2');
|
|
347
|
+
td.className = 'text-muted';
|
|
348
|
+
td.textContent = 'No scanner signatures recorded.';
|
|
349
|
+
tr.appendChild(td);
|
|
350
|
+
tbodyFp.appendChild(tr);
|
|
351
|
+
} else {
|
|
352
|
+
fpEntries.forEach(([ua, count]) => {
|
|
353
|
+
const tr = document.createElement('tr');
|
|
354
|
+
const tdUa = document.createElement('td');
|
|
355
|
+
tdUa.textContent = ua;
|
|
356
|
+
tdUa.style.fontFamily = 'var(--font-mono)';
|
|
357
|
+
tdUa.style.fontSize = '0.75rem';
|
|
358
|
+
|
|
359
|
+
const tdCount = document.createElement('td');
|
|
360
|
+
tdCount.textContent = count;
|
|
361
|
+
tdCount.style.fontWeight = '700';
|
|
362
|
+
|
|
363
|
+
tr.appendChild(tdUa);
|
|
364
|
+
tr.appendChild(tdCount);
|
|
365
|
+
tbodyFp.appendChild(tr);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.error('Failed to load intelligence data:', err);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── 3. LOAD HONEYPOTS ──
|
|
375
|
+
async function loadHoneypots() {
|
|
376
|
+
try {
|
|
377
|
+
const statsRes = await apiFetch('/admin/shield/api/stats');
|
|
378
|
+
const stats = await statsRes.json();
|
|
379
|
+
|
|
380
|
+
const countEl = document.getElementById('honeypot-count');
|
|
381
|
+
countEl.textContent = `${stats.honeypot?.activeTraps ?? 0} active traps (${stats.honeypot?.promotedTraps ?? 0} auto-promoted)`;
|
|
382
|
+
|
|
383
|
+
const tbody = document.getElementById('tbody-traps');
|
|
384
|
+
tbody.replaceChildren();
|
|
385
|
+
|
|
386
|
+
const trapsList = stats.honeypot?.traps || [];
|
|
387
|
+
if (trapsList.length === 0) {
|
|
388
|
+
const tr = document.createElement('tr');
|
|
389
|
+
const td = document.createElement('td');
|
|
390
|
+
td.setAttribute('colspan', '4');
|
|
391
|
+
td.className = 'text-muted';
|
|
392
|
+
td.textContent = 'No active traps registered.';
|
|
393
|
+
tr.appendChild(td);
|
|
394
|
+
tbody.appendChild(tr);
|
|
395
|
+
} else {
|
|
396
|
+
trapsList.forEach(trap => {
|
|
397
|
+
const tr = document.createElement('tr');
|
|
398
|
+
|
|
399
|
+
const tdRoute = document.createElement('td');
|
|
400
|
+
tdRoute.textContent = trap.path;
|
|
401
|
+
tdRoute.style.fontFamily = 'var(--font-mono)';
|
|
402
|
+
|
|
403
|
+
const tdSource = document.createElement('td');
|
|
404
|
+
const badge = document.createElement('span');
|
|
405
|
+
badge.className = `feed-badge ${trap.source === 'auto_promoted' ? 'honeypot' : 'anomaly'}`;
|
|
406
|
+
badge.textContent = trap.source || 'builtin';
|
|
407
|
+
tdSource.appendChild(badge);
|
|
408
|
+
|
|
409
|
+
const tdCatches = document.createElement('td');
|
|
410
|
+
tdCatches.textContent = trap.hitCount || 0;
|
|
411
|
+
|
|
412
|
+
const tdActions = document.createElement('td');
|
|
413
|
+
if (trap.source !== 'builtin') {
|
|
414
|
+
const btnRemove = document.createElement('button');
|
|
415
|
+
btnRemove.className = 'btn btn-sm btn-secondary';
|
|
416
|
+
btnRemove.textContent = 'Remove';
|
|
417
|
+
btnRemove.addEventListener('click', async () => {
|
|
418
|
+
try {
|
|
419
|
+
const cleanPath = trap.path.startsWith('/') ? trap.path.slice(1) : trap.path;
|
|
420
|
+
const res = await apiFetch(`/admin/shield/api/honeypot/${cleanPath}`, { method: 'DELETE' });
|
|
421
|
+
if (res.ok) {
|
|
422
|
+
showToast(`Removed trap ${trap.path}`, 'info');
|
|
423
|
+
loadHoneypots();
|
|
424
|
+
}
|
|
425
|
+
} catch (err) {
|
|
426
|
+
showToast(`Failed: ${err.message}`, 'error');
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
tdActions.appendChild(btnRemove);
|
|
430
|
+
} else {
|
|
431
|
+
const span = document.createElement('span');
|
|
432
|
+
span.className = 'text-muted text-xs';
|
|
433
|
+
span.textContent = 'Core trap';
|
|
434
|
+
tdActions.appendChild(span);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
tr.appendChild(tdRoute);
|
|
438
|
+
tr.appendChild(tdSource);
|
|
439
|
+
tr.appendChild(tdCatches);
|
|
440
|
+
tr.appendChild(tdActions);
|
|
441
|
+
tbody.appendChild(tr);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
} catch (err) {
|
|
445
|
+
console.error('Failed to load honeypot data:', err);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── 4. LOAD SYSTEM HEALTH ──
|
|
450
|
+
async function loadHealth() {
|
|
451
|
+
try {
|
|
452
|
+
const statsRes = await apiFetch('/admin/shield/api/stats');
|
|
453
|
+
const stats = await statsRes.json();
|
|
454
|
+
|
|
455
|
+
// Memory List
|
|
456
|
+
const memList = document.getElementById('health-memory-list');
|
|
457
|
+
memList.replaceChildren();
|
|
458
|
+
|
|
459
|
+
const memItems = [
|
|
460
|
+
{ label: 'Uptime', val: `${Math.round(stats.uptime || 0)}s` },
|
|
461
|
+
{ label: 'Bloom Buffer Memory', val: `${Math.round((stats.bloom?.sizeBytes || 0) / 1024)} KB` },
|
|
462
|
+
{ label: 'LRU Cache Entries', val: `${stats.lru?.size || 0} / ${stats.lru?.maxSize || 0}` },
|
|
463
|
+
{ label: 'Ring Buffer Utilization', val: `${stats.eventsTotal || 0} events` }
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
memItems.forEach(item => {
|
|
467
|
+
const el = document.createElement('div');
|
|
468
|
+
el.className = 'health-item';
|
|
469
|
+
const lbl = document.createElement('span');
|
|
470
|
+
lbl.className = 'health-item-label';
|
|
471
|
+
lbl.textContent = item.label;
|
|
472
|
+
const val = document.createElement('span');
|
|
473
|
+
val.className = 'health-item-val';
|
|
474
|
+
val.textContent = item.val;
|
|
475
|
+
el.appendChild(lbl);
|
|
476
|
+
el.appendChild(val);
|
|
477
|
+
memList.appendChild(el);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Telemetry List
|
|
481
|
+
const telList = document.getElementById('health-telemetry-list');
|
|
482
|
+
telList.replaceChildren();
|
|
483
|
+
|
|
484
|
+
const telItems = [
|
|
485
|
+
{ label: 'Persistence File', val: stats.persistence?.fileExists ? 'Synchronized' : 'Pending Save' },
|
|
486
|
+
{ label: 'File Size', val: `${stats.persistence?.fileSize || 0} bytes` },
|
|
487
|
+
{ label: 'Save Interval', val: `${Math.round((stats.persistence?.intervalMs || 0) / 1000)}s` },
|
|
488
|
+
{ label: 'LRU Hit Rate', val: `${((stats.lru?.hitRate || 0) * 100).toFixed(1)}%` },
|
|
489
|
+
{ label: 'Total 404 Discovered', val: stats.intelligence?.uniquePaths || 0 }
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
telItems.forEach(item => {
|
|
493
|
+
const el = document.createElement('div');
|
|
494
|
+
el.className = 'health-item';
|
|
495
|
+
const lbl = document.createElement('span');
|
|
496
|
+
lbl.className = 'health-item-label';
|
|
497
|
+
lbl.textContent = item.label;
|
|
498
|
+
const val = document.createElement('span');
|
|
499
|
+
val.className = 'health-item-val';
|
|
500
|
+
val.textContent = item.val;
|
|
501
|
+
el.appendChild(lbl);
|
|
502
|
+
el.appendChild(val);
|
|
503
|
+
telList.appendChild(el);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
} catch (err) {
|
|
507
|
+
console.error('Failed to load health telemetry:', err);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── 5. REAL-TIME SERVER-SENT EVENTS (SSE) STREAM ──
|
|
512
|
+
function connectEventStream() {
|
|
513
|
+
if (typeof EventSource === 'undefined') return;
|
|
514
|
+
|
|
515
|
+
const token = getStoredToken();
|
|
516
|
+
const streamUrl = token ? `/admin/shield/api/stream?token=${encodeURIComponent(token)}` : '/admin/shield/api/stream';
|
|
517
|
+
sseSource = new EventSource(streamUrl);
|
|
518
|
+
|
|
519
|
+
sseSource.onmessage = (event) => {
|
|
520
|
+
try {
|
|
521
|
+
const data = JSON.parse(event.data);
|
|
522
|
+
if (data.type === 'connected') return;
|
|
523
|
+
|
|
524
|
+
// Track attack velocity
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
recentEventTimestamps.push(now);
|
|
527
|
+
updateAttackVelocity();
|
|
528
|
+
|
|
529
|
+
// Append event to Live Feed
|
|
530
|
+
appendFeedEvent(data);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
// Ignore parse errors
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
sseSource.onerror = () => {
|
|
537
|
+
const ind = document.getElementById('live-indicator');
|
|
538
|
+
if (ind) {
|
|
539
|
+
ind.className = 'status-pill';
|
|
540
|
+
ind.textContent = 'DISCONNECTED';
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
sseSource.onopen = () => {
|
|
545
|
+
const ind = document.getElementById('live-indicator');
|
|
546
|
+
if (ind) {
|
|
547
|
+
ind.className = 'status-pill status-live';
|
|
548
|
+
ind.replaceChildren();
|
|
549
|
+
const dot = document.createElement('span');
|
|
550
|
+
dot.className = 'pulse-dot';
|
|
551
|
+
ind.appendChild(dot);
|
|
552
|
+
ind.appendChild(document.createTextNode(' LIVE TELEMETRY'));
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function appendFeedEvent(evt) {
|
|
558
|
+
if (!eventFeed) return;
|
|
559
|
+
|
|
560
|
+
const item = document.createElement('div');
|
|
561
|
+
item.className = 'feed-item';
|
|
562
|
+
|
|
563
|
+
const leftBox = document.createElement('div');
|
|
564
|
+
leftBox.style.display = 'flex';
|
|
565
|
+
leftBox.style.alignItems = 'center';
|
|
566
|
+
leftBox.style.gap = '0.5rem';
|
|
567
|
+
|
|
568
|
+
const badge = document.createElement('span');
|
|
569
|
+
badge.className = `feed-badge ${evt.type === 'honeypot' ? 'honeypot' : (evt.type === '404_anomaly' ? 'anomaly' : 'blocked')}`;
|
|
570
|
+
badge.textContent = evt.type;
|
|
571
|
+
|
|
572
|
+
const info = document.createElement('span');
|
|
573
|
+
info.textContent = `${evt.ip || 'Unknown IP'} -> ${evt.path || '/'}`;
|
|
574
|
+
|
|
575
|
+
leftBox.appendChild(badge);
|
|
576
|
+
leftBox.appendChild(info);
|
|
577
|
+
|
|
578
|
+
const time = document.createElement('span');
|
|
579
|
+
time.className = 'text-muted text-xs';
|
|
580
|
+
time.textContent = new Date().toLocaleTimeString();
|
|
581
|
+
|
|
582
|
+
item.appendChild(leftBox);
|
|
583
|
+
item.appendChild(time);
|
|
584
|
+
|
|
585
|
+
eventFeed.insertBefore(item, eventFeed.firstChild);
|
|
586
|
+
|
|
587
|
+
// Keep feed under 100 items in DOM
|
|
588
|
+
if (eventFeed.children.length > 100) {
|
|
589
|
+
eventFeed.removeChild(eventFeed.lastChild);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function updateAttackVelocity() {
|
|
594
|
+
const now = Date.now();
|
|
595
|
+
while (recentEventTimestamps.length > 0 && now - recentEventTimestamps[0] > 60000) {
|
|
596
|
+
recentEventTimestamps.shift();
|
|
597
|
+
}
|
|
598
|
+
const velEl = document.getElementById('val-attack-velocity');
|
|
599
|
+
if (velEl) {
|
|
600
|
+
velEl.textContent = `${recentEventTimestamps.length}/min`;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Interval to refresh attack velocity every 5s
|
|
605
|
+
setInterval(updateAttackVelocity, 5000);
|
|
606
|
+
|
|
607
|
+
// Initial Bootstrap
|
|
608
|
+
loadOverview();
|
|
609
|
+
connectEventStream();
|
|
610
|
+
})();
|