@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.
@@ -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
+ })();