@panguard-ai/threat-cloud 0.2.0 → 0.2.2

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.
Files changed (66) hide show
  1. package/dist/admin-dashboard.d.ts +11 -0
  2. package/dist/admin-dashboard.d.ts.map +1 -0
  3. package/dist/admin-dashboard.js +482 -0
  4. package/dist/admin-dashboard.js.map +1 -0
  5. package/dist/backup.d.ts +40 -0
  6. package/dist/backup.d.ts.map +1 -0
  7. package/dist/backup.js +123 -0
  8. package/dist/backup.js.map +1 -0
  9. package/dist/cli.js +24 -64
  10. package/dist/cli.js.map +1 -1
  11. package/dist/database.d.ts +78 -37
  12. package/dist/database.d.ts.map +1 -1
  13. package/dist/database.js +590 -324
  14. package/dist/database.js.map +1 -1
  15. package/dist/index.d.ts +4 -10
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -9
  18. package/dist/index.js.map +1 -1
  19. package/dist/llm-reviewer.d.ts +47 -0
  20. package/dist/llm-reviewer.d.ts.map +1 -0
  21. package/dist/llm-reviewer.js +203 -0
  22. package/dist/llm-reviewer.js.map +1 -0
  23. package/dist/server.d.ts +56 -63
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +525 -635
  26. package/dist/server.js.map +1 -1
  27. package/dist/types.d.ts +71 -301
  28. package/dist/types.d.ts.map +1 -1
  29. package/package.json +20 -18
  30. package/LICENSE +0 -21
  31. package/dist/audit-logger.d.ts +0 -46
  32. package/dist/audit-logger.d.ts.map +0 -1
  33. package/dist/audit-logger.js +0 -105
  34. package/dist/audit-logger.js.map +0 -1
  35. package/dist/correlation-engine.d.ts +0 -41
  36. package/dist/correlation-engine.d.ts.map +0 -1
  37. package/dist/correlation-engine.js +0 -313
  38. package/dist/correlation-engine.js.map +0 -1
  39. package/dist/feed-distributor.d.ts +0 -36
  40. package/dist/feed-distributor.d.ts.map +0 -1
  41. package/dist/feed-distributor.js +0 -125
  42. package/dist/feed-distributor.js.map +0 -1
  43. package/dist/ioc-store.d.ts +0 -83
  44. package/dist/ioc-store.d.ts.map +0 -1
  45. package/dist/ioc-store.js +0 -278
  46. package/dist/ioc-store.js.map +0 -1
  47. package/dist/query-handlers.d.ts +0 -40
  48. package/dist/query-handlers.d.ts.map +0 -1
  49. package/dist/query-handlers.js +0 -211
  50. package/dist/query-handlers.js.map +0 -1
  51. package/dist/reputation-engine.d.ts +0 -44
  52. package/dist/reputation-engine.d.ts.map +0 -1
  53. package/dist/reputation-engine.js +0 -169
  54. package/dist/reputation-engine.js.map +0 -1
  55. package/dist/rule-generator.d.ts +0 -47
  56. package/dist/rule-generator.d.ts.map +0 -1
  57. package/dist/rule-generator.js +0 -238
  58. package/dist/rule-generator.js.map +0 -1
  59. package/dist/scheduler.d.ts +0 -52
  60. package/dist/scheduler.d.ts.map +0 -1
  61. package/dist/scheduler.js +0 -143
  62. package/dist/scheduler.js.map +0 -1
  63. package/dist/sighting-store.d.ts +0 -61
  64. package/dist/sighting-store.d.ts.map +0 -1
  65. package/dist/sighting-store.js +0 -191
  66. package/dist/sighting-store.js.map +0 -1
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Threat Cloud Admin Dashboard
3
+ * 威脅雲管理後台 - 需要 Admin API Key 認證
4
+ *
5
+ * Single-page admin UI served at /admin
6
+ * All data fetched client-side via existing API endpoints.
7
+ *
8
+ * @module @panguard-ai/threat-cloud/admin-dashboard
9
+ */
10
+ export declare function getAdminHTML(): string;
11
+ //# sourceMappingURL=admin-dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-dashboard.d.ts","sourceRoot":"","sources":["../src/admin-dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,wBAAgB,YAAY,IAAI,MAAM,CAudrC"}
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Threat Cloud Admin Dashboard
3
+ * 威脅雲管理後台 - 需要 Admin API Key 認證
4
+ *
5
+ * Single-page admin UI served at /admin
6
+ * All data fetched client-side via existing API endpoints.
7
+ *
8
+ * @module @panguard-ai/threat-cloud/admin-dashboard
9
+ */
10
+ export function getAdminHTML() {
11
+ return `<!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8"/>
15
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
16
+ <meta name="robots" content="noindex,nofollow"/>
17
+ <title>Threat Cloud Admin</title>
18
+ <style>
19
+ *{margin:0;padding:0;box-sizing:border-box}
20
+ :root{--bg:#0a0e14;--surface:#111820;--surface2:#1a2230;--border:#243040;--text:#e0e6ed;--dim:#6b7d8f;--accent:#4fd1c5;--accent2:#38b2ac;--red:#f56565;--orange:#ed8936;--yellow:#ecc94b;--green:#48bb78;--blue:#4299e1}
21
+ body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:14px;line-height:1.5}
22
+ a{color:var(--accent);text-decoration:none}
23
+ /* Login */
24
+ #login{display:flex;align-items:center;justify-content:center;height:100vh}
25
+ #login form{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:40px;width:380px;text-align:center}
26
+ #login h1{font-size:20px;margin-bottom:8px}
27
+ #login p{color:var(--dim);font-size:13px;margin-bottom:24px}
28
+ #login input{width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;margin-bottom:16px}
29
+ #login input:focus{outline:none;border-color:var(--accent)}
30
+ #login button{width:100%;padding:10px;background:var(--accent);color:var(--bg);border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer}
31
+ #login button:hover{background:var(--accent2)}
32
+ #login .error{color:var(--red);font-size:13px;margin-top:8px;display:none}
33
+ /* Layout */
34
+ #app{display:none}
35
+ header{background:var(--surface);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:10}
36
+ header h1{font-size:16px;font-weight:600}
37
+ header h1 span{color:var(--accent)}
38
+ header .meta{display:flex;align-items:center;gap:16px;font-size:13px;color:var(--dim)}
39
+ header .logout{color:var(--red);cursor:pointer;font-size:12px}
40
+ nav{background:var(--surface);border-bottom:1px solid var(--border);padding:0 24px;display:flex;gap:0;overflow-x:auto}
41
+ nav button{background:none;border:none;border-bottom:2px solid transparent;color:var(--dim);padding:10px 16px;font-size:13px;cursor:pointer;white-space:nowrap}
42
+ nav button:hover{color:var(--text)}
43
+ nav button.active{color:var(--accent);border-bottom-color:var(--accent)}
44
+ main{padding:24px;max-width:1400px;margin:0 auto}
45
+ /* Cards */
46
+ .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:24px}
47
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px}
48
+ .card .label{font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}
49
+ .card .value{font-size:28px;font-weight:700}
50
+ .card .value.green{color:var(--green)}
51
+ .card .value.blue{color:var(--blue)}
52
+ .card .value.orange{color:var(--orange)}
53
+ .card .value.red{color:var(--red)}
54
+ /* Table */
55
+ .table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:24px}
56
+ .table-header{padding:16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px}
57
+ .table-header h2{font-size:15px;font-weight:600}
58
+ .table-header .controls{display:flex;gap:8px;flex-wrap:wrap}
59
+ .table-header input,.table-header select{padding:6px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px}
60
+ .table-header input:focus,.table-header select:focus{outline:none;border-color:var(--accent)}
61
+ table{width:100%;border-collapse:collapse}
62
+ th{background:var(--surface2);text-align:left;padding:10px 14px;font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:0.5px;font-weight:600}
63
+ td{padding:10px 14px;border-top:1px solid var(--border);font-size:13px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
64
+ tr:hover td{background:var(--surface2)}
65
+ /* Badges */
66
+ .badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;text-transform:uppercase}
67
+ .badge.critical{background:rgba(245,101,101,.15);color:var(--red)}
68
+ .badge.high{background:rgba(237,137,54,.15);color:var(--orange)}
69
+ .badge.medium{background:rgba(236,201,75,.15);color:var(--yellow)}
70
+ .badge.low{background:rgba(72,187,120,.15);color:var(--green)}
71
+ .badge.informational{background:rgba(66,153,225,.15);color:var(--blue)}
72
+ .badge.sigma{background:rgba(66,153,225,.15);color:var(--blue)}
73
+ .badge.yara{background:rgba(237,137,54,.15);color:var(--orange)}
74
+ .badge.atr{background:rgba(79,209,197,.15);color:var(--accent)}
75
+ /* Charts */
76
+ .chart-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px}
77
+ .chart-box{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px}
78
+ .chart-box h3{font-size:14px;margin-bottom:12px}
79
+ .bar-chart .bar-item{display:flex;align-items:center;gap:8px;margin-bottom:6px}
80
+ .bar-chart .bar-label{width:140px;font-size:12px;color:var(--dim);text-align:right;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
81
+ .bar-chart .bar-track{flex:1;height:20px;background:var(--bg);border-radius:4px;overflow:hidden}
82
+ .bar-chart .bar-fill{height:100%;border-radius:4px;min-width:2px;display:flex;align-items:center;padding-left:6px;font-size:11px;font-weight:600;color:var(--bg)}
83
+ .bar-chart .bar-count{width:50px;font-size:12px;color:var(--dim);text-align:right}
84
+ /* Pagination */
85
+ .pagination{display:flex;justify-content:center;gap:8px;padding:16px}
86
+ .pagination button{padding:6px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;cursor:pointer}
87
+ .pagination button:hover{border-color:var(--accent)}
88
+ .pagination button:disabled{opacity:.4;cursor:default}
89
+ .pagination span{padding:6px 8px;font-size:13px;color:var(--dim)}
90
+ /* Loading */
91
+ .loading{text-align:center;padding:40px;color:var(--dim)}
92
+ /* Empty */
93
+ .empty{text-align:center;padding:40px;color:var(--dim);font-size:13px}
94
+ /* Responsive */
95
+ @media(max-width:768px){.chart-row{grid-template-columns:1fr}.cards{grid-template-columns:repeat(2,1fr)}}
96
+ </style>
97
+ </head>
98
+ <body>
99
+
100
+ <div id="login">
101
+ <form onsubmit="return doLogin(event)">
102
+ <h1>Threat Cloud <span>Admin</span></h1>
103
+ <p>Enter your admin API key to continue.</p>
104
+ <input type="password" id="keyInput" placeholder="TC_ADMIN_API_KEY" autofocus/>
105
+ <button type="submit">Login</button>
106
+ <div class="error" id="loginError">Invalid API key</div>
107
+ </form>
108
+ </div>
109
+
110
+ <div id="app">
111
+ <header>
112
+ <h1>Threat Cloud <span>Admin</span></h1>
113
+ <div class="meta">
114
+ <span id="uptimeText"></span>
115
+ <span class="logout" onclick="doLogout()">Logout</span>
116
+ </div>
117
+ </header>
118
+ <nav id="tabs">
119
+ <button class="active" data-tab="overview">Overview</button>
120
+ <button data-tab="rules">Rules</button>
121
+ <button data-tab="threats">Threats</button>
122
+ <button data-tab="proposals">ATR Proposals</button>
123
+ <button data-tab="skills">Skill Threats</button>
124
+ <button data-tab="blacklist">Blacklist</button>
125
+ <button data-tab="feeds">Feeds</button>
126
+ </nav>
127
+ <main id="content">
128
+ <div class="loading">Loading...</div>
129
+ </main>
130
+ </div>
131
+
132
+ <script>
133
+ let API_KEY='';
134
+ let stats=null;
135
+ let currentTab='overview';
136
+
137
+ // Auth
138
+ function doLogin(e){
139
+ e.preventDefault();
140
+ const key=document.getElementById('keyInput').value.trim();
141
+ if(!key)return false;
142
+ API_KEY=key;
143
+ fetch('/api/stats',{headers:{Authorization:'Bearer '+key}})
144
+ .then(r=>{if(!r.ok)throw new Error();return r.json()})
145
+ .then(d=>{
146
+ if(!d.ok)throw new Error();
147
+ stats=d.data;
148
+ sessionStorage.setItem('tc_key',key);
149
+ document.getElementById('login').style.display='none';
150
+ document.getElementById('app').style.display='block';
151
+ renderOverview();
152
+ fetchUptime();
153
+ })
154
+ .catch(()=>{
155
+ document.getElementById('loginError').style.display='block';
156
+ });
157
+ return false;
158
+ }
159
+ function doLogout(){
160
+ sessionStorage.removeItem('tc_key');
161
+ API_KEY='';
162
+ document.getElementById('app').style.display='none';
163
+ document.getElementById('login').style.display='flex';
164
+ document.getElementById('keyInput').value='';
165
+ document.getElementById('loginError').style.display='none';
166
+ }
167
+ // Auto-login from session
168
+ (function(){
169
+ const k=sessionStorage.getItem('tc_key');
170
+ if(k){document.getElementById('keyInput').value=k;doLogin(new Event('submit'));}
171
+ })();
172
+
173
+ // Tabs
174
+ document.getElementById('tabs').addEventListener('click',e=>{
175
+ if(e.target.tagName!=='BUTTON')return;
176
+ document.querySelectorAll('#tabs button').forEach(b=>b.classList.remove('active'));
177
+ e.target.classList.add('active');
178
+ currentTab=e.target.dataset.tab;
179
+ renderTab(currentTab);
180
+ });
181
+
182
+ function renderTab(tab){
183
+ switch(tab){
184
+ case 'overview':renderOverview();break;
185
+ case 'rules':renderRules();break;
186
+ case 'threats':renderThreats();break;
187
+ case 'proposals':renderProposals();break;
188
+ case 'skills':renderSkills();break;
189
+ case 'blacklist':renderBlacklist();break;
190
+ case 'feeds':renderFeeds();break;
191
+ }
192
+ }
193
+
194
+ function api(path){
195
+ return fetch(path,{headers:{Authorization:'Bearer '+API_KEY}}).then(r=>r.json());
196
+ }
197
+ function apiText(path){
198
+ return fetch(path,{headers:{Authorization:'Bearer '+API_KEY}}).then(r=>r.text());
199
+ }
200
+ function $(s){return document.getElementById(s)||document.querySelector(s)}
201
+ function h(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
202
+ function num(n){return Number(n).toLocaleString()}
203
+ function badge(text,cls){return '<span class="badge '+cls+'">'+h(text)+'</span>'}
204
+ function severityBadge(s){return badge(s,(s||'').toLowerCase())}
205
+ function sourceBadge(s){return badge(s,(s||'').toLowerCase())}
206
+ function timeAgo(iso){
207
+ if(!iso)return'-';
208
+ const d=new Date(iso),now=Date.now(),diff=now-d.getTime();
209
+ if(diff<60000)return 'just now';
210
+ if(diff<3600000)return Math.floor(diff/60000)+'m ago';
211
+ if(diff<86400000)return Math.floor(diff/3600000)+'h ago';
212
+ return Math.floor(diff/86400000)+'d ago';
213
+ }
214
+
215
+ function fetchUptime(){
216
+ api('/health').then(d=>{
217
+ if(d.ok){
218
+ const s=Math.round(d.data.uptime);
219
+ const h=Math.floor(s/3600),m=Math.floor((s%3600)/60);
220
+ $('uptimeText').textContent='Uptime: '+h+'h '+m+'m';
221
+ }
222
+ });
223
+ }
224
+
225
+ // Overview
226
+ function renderOverview(){
227
+ if(!stats){api('/api/stats').then(d=>{stats=d.data;renderOverview();});return;}
228
+ const s=stats;
229
+ const maxCat=Math.max(...s.rulesByCategory.map(c=>c.count));
230
+ const maxSev=Math.max(...s.rulesBySeverity.map(c=>c.count));
231
+ const catColors=['var(--accent)','var(--blue)','var(--orange)','var(--yellow)','var(--green)','var(--red)'];
232
+
233
+ let html='<div class="cards">';
234
+ html+='<div class="card"><div class="label">Total Rules</div><div class="value green">'+num(s.totalRules)+'</div></div>';
235
+ html+='<div class="card"><div class="label">Total Threats</div><div class="value blue">'+num(s.totalThreats)+'</div></div>';
236
+ html+='<div class="card"><div class="label">Last 24h Threats</div><div class="value orange">'+num(s.last24hThreats)+'</div></div>';
237
+ html+='<div class="card"><div class="label">ATR Proposals</div><div class="value">'+num(s.proposalStats.total)+'</div></div>';
238
+ html+='<div class="card"><div class="label">Skill Threats</div><div class="value">'+num(s.skillThreatsTotal)+'</div></div>';
239
+ html+='<div class="card"><div class="label">Blacklisted Skills</div><div class="value red">'+num(s.skillBlacklistTotal)+'</div></div>';
240
+ html+='</div>';
241
+
242
+ // Source breakdown
243
+ html+='<div class="cards" style="margin-bottom:24px">';
244
+ (s.rulesBySource||[]).forEach(r=>{
245
+ html+='<div class="card"><div class="label">'+h(r.source).toUpperCase()+' Rules</div><div class="value">'+num(r.count)+'</div></div>';
246
+ });
247
+ html+='</div>';
248
+
249
+ // Charts
250
+ html+='<div class="chart-row">';
251
+ // Category chart
252
+ html+='<div class="chart-box"><h3>Rules by Category</h3><div class="bar-chart">';
253
+ (s.rulesByCategory||[]).slice(0,12).forEach((c,i)=>{
254
+ const pct=Math.round(c.count/maxCat*100);
255
+ const color=catColors[i%catColors.length];
256
+ html+='<div class="bar-item"><span class="bar-label">'+h(c.category)+'</span><div class="bar-track"><div class="bar-fill" style="width:'+pct+'%;background:'+color+'">'+num(c.count)+'</div></div></div>';
257
+ });
258
+ html+='</div></div>';
259
+
260
+ // Severity chart
261
+ html+='<div class="chart-box"><h3>Rules by Severity</h3><div class="bar-chart">';
262
+ const sevColors={'critical':'var(--red)','high':'var(--orange)','medium':'var(--yellow)','low':'var(--green)','informational':'var(--blue)'};
263
+ (s.rulesBySeverity||[]).forEach(c=>{
264
+ const pct=Math.round(c.count/maxSev*100);
265
+ const color=sevColors[c.severity]||'var(--dim)';
266
+ html+='<div class="bar-item"><span class="bar-label">'+h(c.severity)+'</span><div class="bar-track"><div class="bar-fill" style="width:'+pct+'%;background:'+color+'">'+num(c.count)+'</div></div></div>';
267
+ });
268
+ html+='</div></div>';
269
+ html+='</div>';
270
+
271
+ // MITRE + Attack Types
272
+ html+='<div class="chart-row">';
273
+ html+='<div class="chart-box"><h3>Top Attack Types</h3>';
274
+ if(s.topAttackTypes.length===0)html+='<div class="empty">No threat data yet</div>';
275
+ else{html+='<table><tr><th>Type</th><th>Count</th></tr>';s.topAttackTypes.forEach(t=>{html+='<tr><td>'+h(t.type)+'</td><td>'+num(t.count)+'</td></tr>';});html+='</table>';}
276
+ html+='</div>';
277
+ html+='<div class="chart-box"><h3>Top MITRE Techniques</h3>';
278
+ if(s.topMitreTechniques.length===0)html+='<div class="empty">No threat data yet</div>';
279
+ else{html+='<table><tr><th>Technique</th><th>Count</th></tr>';s.topMitreTechniques.forEach(t=>{html+='<tr><td>'+h(t.technique)+'</td><td>'+num(t.count)+'</td></tr>';});html+='</table>';}
280
+ html+='</div>';
281
+ html+='</div>';
282
+
283
+ $('content').innerHTML=html;
284
+ }
285
+
286
+ // Rules
287
+ let rulesPage=0;const RULES_PER_PAGE=50;let rulesFilter={search:'',source:'',category:'',severity:''};
288
+ function renderRules(){
289
+ $('content').innerHTML='<div class="loading">Loading rules...</div>';
290
+ let url='/api/rules?limit=5000';
291
+ api(url).then(d=>{
292
+ const allRules=d.data||[];
293
+ const filtered=allRules.filter(r=>{
294
+ if(rulesFilter.source&&r.source!==rulesFilter.source)return false;
295
+ if(rulesFilter.category&&r.category!==rulesFilter.category)return false;
296
+ if(rulesFilter.severity&&r.severity!==rulesFilter.severity)return false;
297
+ if(rulesFilter.search){
298
+ const q=rulesFilter.search.toLowerCase();
299
+ return (r.ruleId||'').toLowerCase().includes(q)||(r.ruleContent||'').toLowerCase().includes(q);
300
+ }
301
+ return true;
302
+ });
303
+ const total=filtered.length;
304
+ const pages=Math.ceil(total/RULES_PER_PAGE);
305
+ if(rulesPage>=pages)rulesPage=Math.max(0,pages-1);
306
+ const slice=filtered.slice(rulesPage*RULES_PER_PAGE,(rulesPage+1)*RULES_PER_PAGE);
307
+
308
+ let html='<div class="table-wrap"><div class="table-header"><h2>Rules ('+num(total)+' of '+num(allRules.length)+')</h2>';
309
+ html+='<div class="controls">';
310
+ html+='<input type="text" placeholder="Search..." value="'+h(rulesFilter.search)+'" onchange="rulesFilter.search=this.value;rulesPage=0;renderRules()"/>';
311
+ html+='<select onchange="rulesFilter.source=this.value;rulesPage=0;renderRules()"><option value="">All Sources</option><option value="sigma"'+(rulesFilter.source==='sigma'?' selected':'')+'>Sigma</option><option value="yara"'+(rulesFilter.source==='yara'?' selected':'')+'>YARA</option><option value="atr"'+(rulesFilter.source==='atr'?' selected':'')+'>ATR</option></select>';
312
+ html+='<select onchange="rulesFilter.severity=this.value;rulesPage=0;renderRules()"><option value="">All Severity</option><option value="critical"'+(rulesFilter.severity==='critical'?' selected':'')+'>Critical</option><option value="high"'+(rulesFilter.severity==='high'?' selected':'')+'>High</option><option value="medium"'+(rulesFilter.severity==='medium'?' selected':'')+'>Medium</option><option value="low"'+(rulesFilter.severity==='low'?' selected':'')+'>Low</option></select>';
313
+ html+='</div></div>';
314
+ html+='<table><tr><th>Rule ID</th><th>Source</th><th>Category</th><th>Severity</th><th>Published</th></tr>';
315
+ slice.forEach(r=>{
316
+ html+='<tr><td title="'+h(r.ruleId)+'">'+h((r.ruleId||'').slice(0,60))+'</td>';
317
+ html+='<td>'+sourceBadge(r.source||'unknown')+'</td>';
318
+ html+='<td>'+h(r.category||'unknown')+'</td>';
319
+ html+='<td>'+severityBadge(r.severity||'unknown')+'</td>';
320
+ html+='<td>'+timeAgo(r.publishedAt)+'</td></tr>';
321
+ });
322
+ html+='</table>';
323
+ html+='<div class="pagination">';
324
+ html+='<button onclick="rulesPage=0;renderRules()" '+(rulesPage===0?'disabled':'')+'>First</button>';
325
+ html+='<button onclick="rulesPage--;renderRules()" '+(rulesPage===0?'disabled':'')+'>Prev</button>';
326
+ html+='<span>Page '+(rulesPage+1)+' of '+Math.max(1,pages)+'</span>';
327
+ html+='<button onclick="rulesPage++;renderRules()" '+(rulesPage>=pages-1?'disabled':'')+'>Next</button>';
328
+ html+='<button onclick="rulesPage='+(pages-1)+';renderRules()" '+(rulesPage>=pages-1?'disabled':'')+'>Last</button>';
329
+ html+='</div></div>';
330
+ $('content').innerHTML=html;
331
+ });
332
+ }
333
+
334
+ // Threats (via stats - no direct GET /api/threats endpoint for list)
335
+ function renderThreats(){
336
+ api('/api/stats').then(d=>{
337
+ const s=d.data;
338
+ let html='<div class="cards">';
339
+ html+='<div class="card"><div class="label">Total Threats</div><div class="value blue">'+num(s.totalThreats)+'</div></div>';
340
+ html+='<div class="card"><div class="label">Last 24h</div><div class="value orange">'+num(s.last24hThreats)+'</div></div>';
341
+ html+='</div>';
342
+ html+='<div class="chart-row">';
343
+ html+='<div class="chart-box"><h3>Attack Types</h3>';
344
+ if(!s.topAttackTypes.length)html+='<div class="empty">No threat data collected yet. Threats are submitted by Guard instances.</div>';
345
+ else{html+='<table><tr><th>Type</th><th>Count</th></tr>';s.topAttackTypes.forEach(t=>{html+='<tr><td>'+h(t.type)+'</td><td>'+num(t.count)+'</td></tr>';});html+='</table>';}
346
+ html+='</div>';
347
+ html+='<div class="chart-box"><h3>MITRE Techniques</h3>';
348
+ if(!s.topMitreTechniques.length)html+='<div class="empty">No MITRE technique data yet.</div>';
349
+ else{html+='<table><tr><th>Technique</th><th>Count</th></tr>';s.topMitreTechniques.forEach(t=>{html+='<tr><td><a href="https://attack.mitre.org/techniques/'+h(t.technique).replace('.','/')+'" target="_blank">'+h(t.technique)+'</a></td><td>'+num(t.count)+'</td></tr>';});html+='</table>';}
350
+ html+='</div></div>';
351
+ $('content').innerHTML=html;
352
+ });
353
+ }
354
+
355
+ // ATR Proposals
356
+ function renderProposals(){
357
+ $('content').innerHTML='<div class="loading">Loading proposals...</div>';
358
+ api('/api/atr-proposals').then(d=>{
359
+ const proposals=d.data||[];
360
+ let html='<div class="table-wrap"><div class="table-header"><h2>ATR Proposals ('+proposals.length+')</h2></div>';
361
+ if(!proposals.length){html+='<div class="empty">No ATR proposals submitted yet. Proposals are auto-generated when Guard detects new threat patterns.</div>';}
362
+ else{
363
+ html+='<table><tr><th>Pattern Hash</th><th>Status</th><th>Confirmations</th><th>LLM Verdict</th><th>Submitted</th></tr>';
364
+ proposals.forEach(p=>{
365
+ const status=p.status||p.llm_verdict||'pending';
366
+ const cls=status==='approved'?'low':status==='rejected'?'critical':'medium';
367
+ html+='<tr><td title="'+h(p.pattern_hash)+'">'+h((p.pattern_hash||'').slice(0,16))+'...</td>';
368
+ html+='<td>'+badge(status,cls)+'</td>';
369
+ html+='<td>'+num(p.confirmation_count||0)+'</td>';
370
+ html+='<td>'+(p.llm_verdict?badge(p.llm_verdict,p.llm_verdict==='approve'?'low':'critical'):'<span style="color:var(--dim)">pending</span>')+'</td>';
371
+ html+='<td>'+timeAgo(p.created_at)+'</td></tr>';
372
+ });
373
+ html+='</table>';
374
+ }
375
+ html+='</div>';
376
+ $('content').innerHTML=html;
377
+ }).catch(()=>{
378
+ $('content').innerHTML='<div class="empty">Cannot load proposals. Admin auth may be required for this endpoint.</div>';
379
+ });
380
+ }
381
+
382
+ // Skill Threats
383
+ function renderSkills(){
384
+ $('content').innerHTML='<div class="loading">Loading skill threats...</div>';
385
+ api('/api/skill-threats?limit=200').then(d=>{
386
+ const threats=d.data||[];
387
+ let html='<div class="table-wrap"><div class="table-header"><h2>Skill Threats ('+threats.length+')</h2></div>';
388
+ if(!threats.length){html+='<div class="empty">No skill threats reported yet. Skill threats are submitted when Guard audits a new MCP skill installation.</div>';}
389
+ else{
390
+ html+='<table><tr><th>Skill Name</th><th>Risk Score</th><th>Risk Level</th><th>Skill Hash</th><th>Reported</th></tr>';
391
+ threats.forEach(t=>{
392
+ const cls=(t.risk_level||'').toLowerCase();
393
+ html+='<tr><td>'+h(t.skill_name)+'</td>';
394
+ html+='<td>'+num(t.risk_score)+'</td>';
395
+ html+='<td>'+severityBadge(t.risk_level)+'</td>';
396
+ html+='<td title="'+h(t.skill_hash)+'">'+h((t.skill_hash||'').slice(0,12))+'...</td>';
397
+ html+='<td>'+timeAgo(t.created_at)+'</td></tr>';
398
+ });
399
+ html+='</table>';
400
+ }
401
+ html+='</div>';
402
+ $('content').innerHTML=html;
403
+ }).catch(()=>{
404
+ $('content').innerHTML='<div class="empty">Cannot load skill threats. Admin auth required.</div>';
405
+ });
406
+ }
407
+
408
+ // Blacklist
409
+ function renderBlacklist(){
410
+ $('content').innerHTML='<div class="loading">Loading blacklist...</div>';
411
+ api('/api/skill-blacklist').then(d=>{
412
+ const list=d.data||[];
413
+ let html='<div class="table-wrap"><div class="table-header"><h2>Community Blacklist ('+list.length+')</h2></div>';
414
+ if(!list.length){html+='<div class="empty">No blacklisted skills yet. Skills are blacklisted when 3+ distinct clients report avg risk >= 70.</div>';}
415
+ else{
416
+ html+='<table><tr><th>Skill Name</th><th>Avg Risk</th><th>Max Level</th><th>Reports</th><th>First Seen</th><th>Last Seen</th></tr>';
417
+ list.forEach(s=>{
418
+ html+='<tr><td>'+h(s.skillName)+'</td>';
419
+ html+='<td>'+num(s.avgRiskScore)+'</td>';
420
+ html+='<td>'+severityBadge(s.maxRiskLevel)+'</td>';
421
+ html+='<td>'+num(s.reportCount)+'</td>';
422
+ html+='<td>'+timeAgo(s.firstReported)+'</td>';
423
+ html+='<td>'+timeAgo(s.lastReported)+'</td></tr>';
424
+ });
425
+ html+='</table>';
426
+ }
427
+ html+='</div>';
428
+ $('content').innerHTML=html;
429
+ });
430
+ }
431
+
432
+ // Feeds
433
+ function renderFeeds(){
434
+ Promise.all([
435
+ apiText('/api/feeds/ip-blocklist'),
436
+ apiText('/api/feeds/domain-blocklist'),
437
+ api('/api/skill-whitelist')
438
+ ]).then(([ips,domains,wl])=>{
439
+ const ipList=(ips||'').trim().split('\\n').filter(Boolean);
440
+ const domList=(domains||'').trim().split('\\n').filter(Boolean);
441
+ const whiteList=(wl.data||[]);
442
+
443
+ let html='<div class="cards">';
444
+ html+='<div class="card"><div class="label">IP Blocklist</div><div class="value red">'+num(ipList.length)+'</div></div>';
445
+ html+='<div class="card"><div class="label">Domain Blocklist</div><div class="value orange">'+num(domList.length)+'</div></div>';
446
+ html+='<div class="card"><div class="label">Skill Whitelist</div><div class="value green">'+num(whiteList.length)+'</div></div>';
447
+ html+='</div>';
448
+
449
+ html+='<div class="chart-row">';
450
+ // IP Blocklist
451
+ html+='<div class="chart-box"><h3>IP Blocklist</h3>';
452
+ if(!ipList.length)html+='<div class="empty">No blocked IPs yet.</div>';
453
+ else{html+='<table><tr><th>IP Address</th></tr>';ipList.slice(0,50).forEach(ip=>{html+='<tr><td>'+h(ip)+'</td></tr>';});if(ipList.length>50)html+='<tr><td style="color:var(--dim)">...and '+(ipList.length-50)+' more</td></tr>';html+='</table>';}
454
+ html+='</div>';
455
+ // Domain Blocklist
456
+ html+='<div class="chart-box"><h3>Domain Blocklist</h3>';
457
+ if(!domList.length)html+='<div class="empty">No blocked domains yet.</div>';
458
+ else{html+='<table><tr><th>Domain</th></tr>';domList.slice(0,50).forEach(d=>{html+='<tr><td>'+h(d)+'</td></tr>';});if(domList.length>50)html+='<tr><td style="color:var(--dim)">...and '+(domList.length-50)+' more</td></tr>';html+='</table>';}
459
+ html+='</div></div>';
460
+
461
+ // Skill Whitelist
462
+ html+='<div class="table-wrap"><div class="table-header"><h2>Community Skill Whitelist ('+whiteList.length+')</h2></div>';
463
+ if(!whiteList.length){html+='<div class="empty">No whitelisted skills yet.</div>';}
464
+ else{
465
+ html+='<table><tr><th>Skill Name</th><th>Reports</th><th>Fingerprint</th></tr>';
466
+ whiteList.forEach(s=>{
467
+ html+='<tr><td>'+h(s.skill_name||s.skillName)+'</td>';
468
+ html+='<td>'+num(s.report_count||s.reportCount||0)+'</td>';
469
+ html+='<td title="'+h(s.fingerprint_hash||'')+'">'+h((s.fingerprint_hash||'-').slice(0,16))+'</td></tr>';
470
+ });
471
+ html+='</table>';
472
+ }
473
+ html+='</div>';
474
+
475
+ $('content').innerHTML=html;
476
+ });
477
+ }
478
+ </script>
479
+ </body>
480
+ </html>`;
481
+ }
482
+ //# sourceMappingURL=admin-dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-dashboard.js","sourceRoot":"","sources":["../src/admin-dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,UAAU,YAAY;IAC1B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAqdD,CAAC;AACT,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * BackupManager - SQLite database backup with rotation
3
+ *
4
+ * Uses WAL checkpoint + file copy for a consistent backup.
5
+ * Keeps the last N backups (default 7) and rotates old ones automatically.
6
+ */
7
+ export interface BackupResult {
8
+ readonly path: string;
9
+ readonly sizeBytes: number;
10
+ readonly timestamp: string;
11
+ }
12
+ export declare class BackupManager {
13
+ private readonly dbPath;
14
+ private readonly backupDir;
15
+ private readonly maxBackups;
16
+ private readonly dbName;
17
+ constructor(dbPath: string, backupDir: string, maxBackups?: number);
18
+ /**
19
+ * Create a consistent backup of the SQLite database.
20
+ *
21
+ * 1. Opens the DB in readonly mode
22
+ * 2. Runs a WAL checkpoint (TRUNCATE) to flush pending writes
23
+ * 3. Copies the database file to the backup directory
24
+ * 4. Rotates old backups beyond maxBackups
25
+ */
26
+ backup(): BackupResult;
27
+ /**
28
+ * Remove old backups beyond maxBackups, keeping the newest ones.
29
+ */
30
+ rotate(): void;
31
+ /**
32
+ * List existing backups for this database, sorted newest-first.
33
+ */
34
+ listBackups(): string[];
35
+ /**
36
+ * Format a byte count as a human-readable string.
37
+ */
38
+ static formatSize(bytes: number): string;
39
+ }
40
+ //# sourceMappingURL=backup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../src/backup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,SAAI;IAkB7D;;;;;;;OAOG;IACH,MAAM,IAAI,YAAY;IA0CtB;;OAEG;IACH,MAAM,IAAI,IAAI;IAsBd;;OAEG;IACH,WAAW,IAAI,MAAM,EAAE;IAevB;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;CAKzC"}