@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.
- package/dist/admin-dashboard.d.ts +11 -0
- package/dist/admin-dashboard.d.ts.map +1 -0
- package/dist/admin-dashboard.js +482 -0
- package/dist/admin-dashboard.js.map +1 -0
- package/dist/backup.d.ts +40 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +123 -0
- package/dist/backup.js.map +1 -0
- package/dist/cli.js +24 -64
- package/dist/cli.js.map +1 -1
- package/dist/database.d.ts +78 -37
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +590 -324
- package/dist/database.js.map +1 -1
- package/dist/index.d.ts +4 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -9
- package/dist/index.js.map +1 -1
- package/dist/llm-reviewer.d.ts +47 -0
- package/dist/llm-reviewer.d.ts.map +1 -0
- package/dist/llm-reviewer.js +203 -0
- package/dist/llm-reviewer.js.map +1 -0
- package/dist/server.d.ts +56 -63
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +525 -635
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +71 -301
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -18
- package/LICENSE +0 -21
- package/dist/audit-logger.d.ts +0 -46
- package/dist/audit-logger.d.ts.map +0 -1
- package/dist/audit-logger.js +0 -105
- package/dist/audit-logger.js.map +0 -1
- package/dist/correlation-engine.d.ts +0 -41
- package/dist/correlation-engine.d.ts.map +0 -1
- package/dist/correlation-engine.js +0 -313
- package/dist/correlation-engine.js.map +0 -1
- package/dist/feed-distributor.d.ts +0 -36
- package/dist/feed-distributor.d.ts.map +0 -1
- package/dist/feed-distributor.js +0 -125
- package/dist/feed-distributor.js.map +0 -1
- package/dist/ioc-store.d.ts +0 -83
- package/dist/ioc-store.d.ts.map +0 -1
- package/dist/ioc-store.js +0 -278
- package/dist/ioc-store.js.map +0 -1
- package/dist/query-handlers.d.ts +0 -40
- package/dist/query-handlers.d.ts.map +0 -1
- package/dist/query-handlers.js +0 -211
- package/dist/query-handlers.js.map +0 -1
- package/dist/reputation-engine.d.ts +0 -44
- package/dist/reputation-engine.d.ts.map +0 -1
- package/dist/reputation-engine.js +0 -169
- package/dist/reputation-engine.js.map +0 -1
- package/dist/rule-generator.d.ts +0 -47
- package/dist/rule-generator.d.ts.map +0 -1
- package/dist/rule-generator.js +0 -238
- package/dist/rule-generator.js.map +0 -1
- package/dist/scheduler.d.ts +0 -52
- package/dist/scheduler.d.ts.map +0 -1
- package/dist/scheduler.js +0 -143
- package/dist/scheduler.js.map +0 -1
- package/dist/sighting-store.d.ts +0 -61
- package/dist/sighting-store.d.ts.map +0 -1
- package/dist/sighting-store.js +0 -191
- 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
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"}
|
package/dist/backup.d.ts
ADDED
|
@@ -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"}
|