@panguard-ai/threat-cloud 0.2.1 → 0.2.3
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 +509 -0
- package/dist/admin-dashboard.js.map +1 -0
- package/dist/backup.d.ts.map +1 -1
- package/dist/backup.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/database.d.ts +26 -3
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +390 -65
- package/dist/database.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/llm-reviewer.d.ts.map +1 -1
- package/dist/llm-reviewer.js +1 -3
- package/dist/llm-reviewer.js.map +1 -1
- package/dist/server.d.ts +26 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +327 -24
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/database.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"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"}
|
package/dist/database.js
CHANGED
|
@@ -39,6 +39,10 @@ export class ThreatCloudDB {
|
|
|
39
39
|
rule_content TEXT NOT NULL,
|
|
40
40
|
published_at TEXT NOT NULL,
|
|
41
41
|
source TEXT NOT NULL,
|
|
42
|
+
category TEXT,
|
|
43
|
+
severity TEXT,
|
|
44
|
+
mitre_techniques TEXT,
|
|
45
|
+
tags TEXT,
|
|
42
46
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
43
47
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
44
48
|
);
|
|
@@ -104,6 +108,15 @@ export class ThreatCloudDB {
|
|
|
104
108
|
last_reported TEXT DEFAULT (datetime('now'))
|
|
105
109
|
);
|
|
106
110
|
|
|
111
|
+
-- Migration: add classification columns to existing rules table
|
|
112
|
+
-- SQLite allows ADD COLUMN on existing tables; IF NOT EXISTS not supported,
|
|
113
|
+
-- so we catch errors for already-existing columns in migrate().
|
|
114
|
+
`);
|
|
115
|
+
this.migrate();
|
|
116
|
+
this.db.exec(`
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_rules_category ON rules(category);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_rules_severity ON rules(severity);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_rules_source ON rules(source);
|
|
107
120
|
CREATE INDEX IF NOT EXISTS idx_atr_proposals_status ON atr_proposals(status);
|
|
108
121
|
CREATE INDEX IF NOT EXISTS idx_atr_proposals_pattern ON atr_proposals(pattern_hash);
|
|
109
122
|
CREATE INDEX IF NOT EXISTS idx_skill_threats_hash ON skill_threats(skill_hash);
|
|
@@ -114,6 +127,21 @@ export class ThreatCloudDB {
|
|
|
114
127
|
CREATE INDEX IF NOT EXISTS idx_skill_whitelist_name ON skill_whitelist(normalized_name);
|
|
115
128
|
`);
|
|
116
129
|
}
|
|
130
|
+
/** Run schema migrations for existing databases / 執行既有資料庫的 schema 遷移 */
|
|
131
|
+
migrate() {
|
|
132
|
+
const addColumn = (table, column, type) => {
|
|
133
|
+
try {
|
|
134
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Column already exists — safe to ignore
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
addColumn('rules', 'category', 'TEXT');
|
|
141
|
+
addColumn('rules', 'severity', 'TEXT');
|
|
142
|
+
addColumn('rules', 'mitre_techniques', 'TEXT');
|
|
143
|
+
addColumn('rules', 'tags', 'TEXT');
|
|
144
|
+
}
|
|
117
145
|
/** Insert anonymized threat data / 插入匿名化威脅數據 */
|
|
118
146
|
insertThreat(data) {
|
|
119
147
|
const stmt = this.db.prepare(`
|
|
@@ -122,38 +150,219 @@ export class ThreatCloudDB {
|
|
|
122
150
|
`);
|
|
123
151
|
stmt.run(data.attackSourceIP, data.attackType, data.mitreTechnique, data.sigmaRuleMatched, data.timestamp, data.industry ?? null, data.region);
|
|
124
152
|
}
|
|
153
|
+
/** Get paginated threat events (admin) / 取得分頁威脅事件 */
|
|
154
|
+
getThreats(limit = 50, offset = 0) {
|
|
155
|
+
return this.db
|
|
156
|
+
.prepare('SELECT * FROM threats ORDER BY timestamp DESC LIMIT ? OFFSET ?')
|
|
157
|
+
.all(limit, offset);
|
|
158
|
+
}
|
|
159
|
+
/** Get total threat count / 取得威脅總數 */
|
|
160
|
+
getThreatCount() {
|
|
161
|
+
return this.db.prepare('SELECT COUNT(*) as count FROM threats').get().count;
|
|
162
|
+
}
|
|
163
|
+
/** Extract classification metadata from rule content / 從規則內容提取分類元資料 */
|
|
164
|
+
extractMetadata(ruleContent, source) {
|
|
165
|
+
let category = 'unknown';
|
|
166
|
+
let severity = 'medium';
|
|
167
|
+
let mitreTechniques = '';
|
|
168
|
+
let tags = '';
|
|
169
|
+
try {
|
|
170
|
+
if (source === 'yara') {
|
|
171
|
+
// YARA rules: extract from meta section
|
|
172
|
+
const metaMatch = ruleContent.match(/meta\s*:\s*([\s\S]*?)(?:strings|condition)\s*:/);
|
|
173
|
+
if (metaMatch) {
|
|
174
|
+
const meta = metaMatch[1];
|
|
175
|
+
const catMatch = meta.match(/category\s*=\s*"([^"]+)"/);
|
|
176
|
+
if (catMatch)
|
|
177
|
+
category = catMatch[1].toLowerCase();
|
|
178
|
+
const sevMatch = meta.match(/severity\s*=\s*"([^"]+)"/);
|
|
179
|
+
if (sevMatch)
|
|
180
|
+
severity = sevMatch[1].toLowerCase();
|
|
181
|
+
const mitreMatch = meta.match(/mitre_att(?:ack|&ck)\s*=\s*"([^"]+)"/i);
|
|
182
|
+
if (mitreMatch)
|
|
183
|
+
mitreTechniques = mitreMatch[1];
|
|
184
|
+
}
|
|
185
|
+
// Fallback: infer category from rule content keywords
|
|
186
|
+
if (category === 'unknown') {
|
|
187
|
+
if (/malware|trojan|ransom|backdoor|rat_|infostealer/i.test(ruleContent))
|
|
188
|
+
category = 'malware';
|
|
189
|
+
else if (/exploit|cve-/i.test(ruleContent))
|
|
190
|
+
category = 'exploit';
|
|
191
|
+
else if (/hack_?tool|offensive|cobalt/i.test(ruleContent))
|
|
192
|
+
category = 'hacktool';
|
|
193
|
+
else if (/webshell/i.test(ruleContent))
|
|
194
|
+
category = 'webshell';
|
|
195
|
+
else if (/packer|obfusc|crypter/i.test(ruleContent))
|
|
196
|
+
category = 'packer';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Sigma / ATR: YAML-based extraction
|
|
201
|
+
// Extract level/severity
|
|
202
|
+
const sevMatch = ruleContent.match(/(?:^|\n)\s*(?:level|severity)\s*:\s*(\w+)/);
|
|
203
|
+
if (sevMatch)
|
|
204
|
+
severity = sevMatch[1].toLowerCase();
|
|
205
|
+
// Extract tags list
|
|
206
|
+
const tagMatch = ruleContent.match(/(?:^|\n)\s*tags\s*:\s*\n((?:\s+-\s*.+\n?)+)/);
|
|
207
|
+
if (tagMatch) {
|
|
208
|
+
const tagLines = tagMatch[1].match(/-\s*(.+)/g) ?? [];
|
|
209
|
+
const tagList = tagLines.map((t) => t.replace(/^-\s*/, '').trim());
|
|
210
|
+
tags = tagList.join(',');
|
|
211
|
+
// Derive MITRE techniques from tags (attack.tXXXX)
|
|
212
|
+
const mitreTags = tagList.filter((t) => /^attack\.t\d+/i.test(t));
|
|
213
|
+
if (mitreTags.length > 0) {
|
|
214
|
+
mitreTechniques = mitreTags
|
|
215
|
+
.map((t) => t.replace(/^attack\./i, '').toUpperCase())
|
|
216
|
+
.join(',');
|
|
217
|
+
}
|
|
218
|
+
// Derive category from MITRE ATT&CK tactic tags
|
|
219
|
+
const attackTags = tagList.filter((t) => t.startsWith('attack.'));
|
|
220
|
+
for (const tag of attackTags) {
|
|
221
|
+
if (/initial.access/i.test(tag)) {
|
|
222
|
+
category = 'initial-access';
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (/execution/i.test(tag)) {
|
|
226
|
+
category = 'execution';
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (/persistence/i.test(tag)) {
|
|
230
|
+
category = 'persistence';
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (/privilege.escalation/i.test(tag)) {
|
|
234
|
+
category = 'privilege-escalation';
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
if (/defense.evasion/i.test(tag)) {
|
|
238
|
+
category = 'defense-evasion';
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
if (/credential.access/i.test(tag)) {
|
|
242
|
+
category = 'credential-access';
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
if (/discovery/i.test(tag)) {
|
|
246
|
+
category = 'discovery';
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
if (/lateral.movement/i.test(tag)) {
|
|
250
|
+
category = 'lateral-movement';
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
if (/collection/i.test(tag)) {
|
|
254
|
+
category = 'collection';
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (/exfiltration/i.test(tag)) {
|
|
258
|
+
category = 'exfiltration';
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
if (/command.and.control|c2/i.test(tag)) {
|
|
262
|
+
category = 'command-and-control';
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
if (/impact/i.test(tag)) {
|
|
266
|
+
category = 'impact';
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
if (/resource.development/i.test(tag)) {
|
|
270
|
+
category = 'resource-development';
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
if (/reconnaissance/i.test(tag)) {
|
|
274
|
+
category = 'reconnaissance';
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Fallback: infer from logsource (Sigma)
|
|
280
|
+
if (category === 'unknown') {
|
|
281
|
+
const lsCatMatch = ruleContent.match(/(?:^|\n)\s*logsource\s*:\s*\n(?:\s+\w+\s*:.+\n)*?\s+category\s*:\s*(\S+)/);
|
|
282
|
+
if (lsCatMatch) {
|
|
283
|
+
category = lsCatMatch[1].toLowerCase();
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const lsProdMatch = ruleContent.match(/(?:^|\n)\s*logsource\s*:\s*\n(?:\s+\w+\s*:.+\n)*?\s+product\s*:\s*(\S+)/);
|
|
287
|
+
if (lsProdMatch)
|
|
288
|
+
category = lsProdMatch[1].toLowerCase();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Extraction failed — keep defaults
|
|
295
|
+
}
|
|
296
|
+
// Normalize category to lowercase
|
|
297
|
+
category = category.toLowerCase();
|
|
298
|
+
severity = severity.toLowerCase();
|
|
299
|
+
return { category, severity, mitreTechniques, tags };
|
|
300
|
+
}
|
|
125
301
|
/** Insert or update a community rule / 插入或更新社群規則 */
|
|
126
302
|
upsertRule(rule) {
|
|
303
|
+
// Extract classification from content if not provided
|
|
304
|
+
const meta = this.extractMetadata(rule.ruleContent, rule.source);
|
|
305
|
+
const category = rule.category ?? meta.category;
|
|
306
|
+
const severity = rule.severity ?? meta.severity;
|
|
307
|
+
const mitreTechniques = rule.mitreTechniques ?? meta.mitreTechniques;
|
|
308
|
+
const tags = rule.tags ?? meta.tags;
|
|
127
309
|
const stmt = this.db.prepare(`
|
|
128
|
-
INSERT INTO rules (rule_id, rule_content, published_at, source)
|
|
129
|
-
VALUES (?, ?, ?, ?)
|
|
310
|
+
INSERT INTO rules (rule_id, rule_content, published_at, source, category, severity, mitre_techniques, tags)
|
|
311
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
130
312
|
ON CONFLICT(rule_id) DO UPDATE SET
|
|
131
313
|
rule_content = excluded.rule_content,
|
|
132
314
|
published_at = excluded.published_at,
|
|
133
315
|
source = excluded.source,
|
|
316
|
+
category = excluded.category,
|
|
317
|
+
severity = excluded.severity,
|
|
318
|
+
mitre_techniques = excluded.mitre_techniques,
|
|
319
|
+
tags = excluded.tags,
|
|
134
320
|
updated_at = datetime('now')
|
|
135
321
|
`);
|
|
136
|
-
stmt.run(rule.ruleId, rule.ruleContent, rule.publishedAt, rule.source);
|
|
322
|
+
stmt.run(rule.ruleId, rule.ruleContent, rule.publishedAt, rule.source, category, severity, mitreTechniques, tags);
|
|
137
323
|
}
|
|
138
324
|
/** Fetch rules published after a given timestamp / 取得指定時間後發佈的規則 */
|
|
139
|
-
getRulesSince(since) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
FROM rules
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
325
|
+
getRulesSince(since, filters) {
|
|
326
|
+
let sql = `SELECT rule_id as ruleId, rule_content as ruleContent, published_at as publishedAt, source,
|
|
327
|
+
category, severity, mitre_techniques as mitreTechniques, tags
|
|
328
|
+
FROM rules WHERE published_at > ?`;
|
|
329
|
+
const params = [since];
|
|
330
|
+
if (filters?.category) {
|
|
331
|
+
sql += ' AND category = ?';
|
|
332
|
+
params.push(filters.category);
|
|
333
|
+
}
|
|
334
|
+
if (filters?.severity) {
|
|
335
|
+
sql += ' AND severity = ?';
|
|
336
|
+
params.push(filters.severity);
|
|
337
|
+
}
|
|
338
|
+
if (filters?.source) {
|
|
339
|
+
sql += ' AND source = ?';
|
|
340
|
+
params.push(filters.source);
|
|
341
|
+
}
|
|
342
|
+
sql += ' ORDER BY published_at ASC';
|
|
343
|
+
return this.db.prepare(sql).all(...params);
|
|
147
344
|
}
|
|
148
345
|
/** Fetch all rules with limit / 取得所有規則(含限制) */
|
|
149
|
-
getAllRules(limit = 5000) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
FROM rules
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
346
|
+
getAllRules(limit = 5000, filters) {
|
|
347
|
+
let sql = `SELECT rule_id as ruleId, rule_content as ruleContent, published_at as publishedAt, source,
|
|
348
|
+
category, severity, mitre_techniques as mitreTechniques, tags
|
|
349
|
+
FROM rules WHERE 1=1`;
|
|
350
|
+
const params = [];
|
|
351
|
+
if (filters?.category) {
|
|
352
|
+
sql += ' AND category = ?';
|
|
353
|
+
params.push(filters.category);
|
|
354
|
+
}
|
|
355
|
+
if (filters?.severity) {
|
|
356
|
+
sql += ' AND severity = ?';
|
|
357
|
+
params.push(filters.severity);
|
|
358
|
+
}
|
|
359
|
+
if (filters?.source) {
|
|
360
|
+
sql += ' AND source = ?';
|
|
361
|
+
params.push(filters.source);
|
|
362
|
+
}
|
|
363
|
+
sql += ' ORDER BY published_at DESC LIMIT ?';
|
|
364
|
+
params.push(limit);
|
|
365
|
+
return this.db.prepare(sql).all(...params);
|
|
157
366
|
}
|
|
158
367
|
/** Insert ATR rule proposal / 插入 ATR 規則提案 */
|
|
159
368
|
insertATRProposal(proposal) {
|
|
@@ -166,36 +375,48 @@ export class ThreatCloudDB {
|
|
|
166
375
|
/** Get ATR proposals, optionally filtered by status / 取得 ATR 提案 */
|
|
167
376
|
getATRProposals(status) {
|
|
168
377
|
if (status) {
|
|
169
|
-
return this.db
|
|
378
|
+
return this.db
|
|
379
|
+
.prepare('SELECT * FROM atr_proposals WHERE status = ? ORDER BY created_at DESC')
|
|
380
|
+
.all(status);
|
|
170
381
|
}
|
|
171
382
|
return this.db.prepare('SELECT * FROM atr_proposals ORDER BY created_at DESC').all();
|
|
172
383
|
}
|
|
173
384
|
/** Increment confirmations for a proposal; auto-confirm at >= 3 / 增加提案確認數 */
|
|
174
385
|
confirmATRProposal(patternHash) {
|
|
175
|
-
this.db
|
|
386
|
+
this.db
|
|
387
|
+
.prepare(`
|
|
176
388
|
UPDATE atr_proposals
|
|
177
389
|
SET confirmations = confirmations + 1,
|
|
178
390
|
status = CASE WHEN confirmations + 1 >= 3 THEN 'confirmed' ELSE status END,
|
|
179
391
|
updated_at = datetime('now')
|
|
180
392
|
WHERE pattern_hash = ?
|
|
181
|
-
`)
|
|
393
|
+
`)
|
|
394
|
+
.run(patternHash);
|
|
182
395
|
}
|
|
183
396
|
/** Update LLM review verdict for a proposal / 更新 LLM 審查結果 */
|
|
184
397
|
updateATRProposalLLMReview(patternHash, verdict) {
|
|
185
|
-
this.db
|
|
398
|
+
this.db
|
|
399
|
+
.prepare(`
|
|
186
400
|
UPDATE atr_proposals SET llm_review_verdict = ?, updated_at = datetime('now') WHERE pattern_hash = ?
|
|
187
|
-
`)
|
|
401
|
+
`)
|
|
402
|
+
.run(verdict, patternHash);
|
|
188
403
|
}
|
|
189
404
|
/** Insert ATR feedback / 插入 ATR 回饋 */
|
|
190
405
|
insertATRFeedback(ruleId, isTruePositive, clientId) {
|
|
191
|
-
this.db
|
|
406
|
+
this.db
|
|
407
|
+
.prepare(`
|
|
192
408
|
INSERT INTO atr_feedback (rule_id, is_true_positive, client_id) VALUES (?, ?, ?)
|
|
193
|
-
`)
|
|
409
|
+
`)
|
|
410
|
+
.run(ruleId, isTruePositive ? 1 : 0, clientId ?? null);
|
|
194
411
|
}
|
|
195
412
|
/** Get feedback stats for a rule / 取得規則回饋統計 */
|
|
196
413
|
getATRFeedbackStats(ruleId) {
|
|
197
|
-
const tp = this.db
|
|
198
|
-
|
|
414
|
+
const tp = this.db
|
|
415
|
+
.prepare('SELECT COUNT(*) as count FROM atr_feedback WHERE rule_id = ? AND is_true_positive = 1')
|
|
416
|
+
.get(ruleId).count;
|
|
417
|
+
const fp = this.db
|
|
418
|
+
.prepare('SELECT COUNT(*) as count FROM atr_feedback WHERE rule_id = ? AND is_true_positive = 0')
|
|
419
|
+
.get(ruleId).count;
|
|
199
420
|
return { truePositives: tp, falsePositives: fp };
|
|
200
421
|
}
|
|
201
422
|
/** Insert skill threat submission / 插入技能威脅提交 */
|
|
@@ -208,13 +429,21 @@ export class ThreatCloudDB {
|
|
|
208
429
|
}
|
|
209
430
|
/** Get recent skill threats / 取得最近技能威脅 */
|
|
210
431
|
getSkillThreats(limit = 50) {
|
|
211
|
-
return this.db
|
|
432
|
+
return this.db
|
|
433
|
+
.prepare('SELECT * FROM skill_threats ORDER BY created_at DESC LIMIT ?')
|
|
434
|
+
.all(limit);
|
|
212
435
|
}
|
|
213
436
|
/** Get proposal statistics / 取得提案統計 */
|
|
214
437
|
getProposalStats() {
|
|
215
|
-
const pending = this.db
|
|
216
|
-
|
|
217
|
-
|
|
438
|
+
const pending = this.db
|
|
439
|
+
.prepare("SELECT COUNT(*) as count FROM atr_proposals WHERE status = 'pending'")
|
|
440
|
+
.get().count;
|
|
441
|
+
const confirmed = this.db
|
|
442
|
+
.prepare("SELECT COUNT(*) as count FROM atr_proposals WHERE status = 'confirmed'")
|
|
443
|
+
.get().count;
|
|
444
|
+
const rejected = this.db
|
|
445
|
+
.prepare("SELECT COUNT(*) as count FROM atr_proposals WHERE status = 'rejected'")
|
|
446
|
+
.get().count;
|
|
218
447
|
const total = this.db.prepare('SELECT COUNT(*) as count FROM atr_proposals').get().count;
|
|
219
448
|
return { pending, confirmed, rejected, total };
|
|
220
449
|
}
|
|
@@ -222,23 +451,48 @@ export class ThreatCloudDB {
|
|
|
222
451
|
getStats() {
|
|
223
452
|
const totalThreats = this.db.prepare('SELECT COUNT(*) as count FROM threats').get().count;
|
|
224
453
|
const totalRules = this.db.prepare('SELECT COUNT(*) as count FROM rules').get().count;
|
|
225
|
-
const last24h = this.db
|
|
226
|
-
|
|
454
|
+
const last24h = this.db
|
|
455
|
+
.prepare("SELECT COUNT(*) as count FROM threats WHERE received_at > datetime('now', '-1 day')")
|
|
456
|
+
.get().count;
|
|
457
|
+
const topAttackTypes = this.db
|
|
458
|
+
.prepare(`
|
|
227
459
|
SELECT attack_type as type, COUNT(*) as count
|
|
228
460
|
FROM threats
|
|
229
461
|
GROUP BY attack_type
|
|
230
462
|
ORDER BY count DESC
|
|
231
463
|
LIMIT 10
|
|
232
|
-
`)
|
|
233
|
-
|
|
464
|
+
`)
|
|
465
|
+
.all();
|
|
466
|
+
const topMitreTechniques = this.db
|
|
467
|
+
.prepare(`
|
|
234
468
|
SELECT mitre_technique as technique, COUNT(*) as count
|
|
235
469
|
FROM threats
|
|
236
470
|
GROUP BY mitre_technique
|
|
237
471
|
ORDER BY count DESC
|
|
238
472
|
LIMIT 10
|
|
239
|
-
`)
|
|
473
|
+
`)
|
|
474
|
+
.all();
|
|
240
475
|
const proposalStats = this.getProposalStats();
|
|
241
476
|
const skillThreatsTotal = this.db.prepare('SELECT COUNT(*) as count FROM skill_threats').get().count;
|
|
477
|
+
const skillBlacklistTotal = this.getSkillBlacklist().length;
|
|
478
|
+
const rulesByCategory = this.db
|
|
479
|
+
.prepare(`
|
|
480
|
+
SELECT COALESCE(category, 'unknown') as category, COUNT(*) as count
|
|
481
|
+
FROM rules GROUP BY category ORDER BY count DESC LIMIT 20
|
|
482
|
+
`)
|
|
483
|
+
.all();
|
|
484
|
+
const rulesBySeverity = this.db
|
|
485
|
+
.prepare(`
|
|
486
|
+
SELECT COALESCE(severity, 'unknown') as severity, COUNT(*) as count
|
|
487
|
+
FROM rules GROUP BY severity ORDER BY count DESC
|
|
488
|
+
`)
|
|
489
|
+
.all();
|
|
490
|
+
const rulesBySource = this.db
|
|
491
|
+
.prepare(`
|
|
492
|
+
SELECT source, COUNT(*) as count
|
|
493
|
+
FROM rules GROUP BY source ORDER BY count DESC
|
|
494
|
+
`)
|
|
495
|
+
.all();
|
|
242
496
|
return {
|
|
243
497
|
totalThreats,
|
|
244
498
|
totalRules,
|
|
@@ -247,40 +501,52 @@ export class ThreatCloudDB {
|
|
|
247
501
|
last24hThreats: last24h,
|
|
248
502
|
proposalStats,
|
|
249
503
|
skillThreatsTotal,
|
|
504
|
+
skillBlacklistTotal,
|
|
505
|
+
rulesByCategory,
|
|
506
|
+
rulesBySeverity,
|
|
507
|
+
rulesBySource,
|
|
250
508
|
};
|
|
251
509
|
}
|
|
252
510
|
/** Get confirmed/promoted ATR rules, optionally filtered by date / 取得已確認 ATR 規則 */
|
|
253
511
|
getConfirmedATRRules(since) {
|
|
254
512
|
if (since) {
|
|
255
|
-
return this.db
|
|
513
|
+
return this.db
|
|
514
|
+
.prepare(`
|
|
256
515
|
SELECT pattern_hash as ruleId, rule_content as ruleContent, updated_at as publishedAt, 'atr-community' as source
|
|
257
516
|
FROM atr_proposals
|
|
258
517
|
WHERE (status = 'confirmed' OR status = 'promoted') AND updated_at > ?
|
|
259
518
|
ORDER BY updated_at ASC
|
|
260
|
-
`)
|
|
519
|
+
`)
|
|
520
|
+
.all(since);
|
|
261
521
|
}
|
|
262
|
-
return this.db
|
|
522
|
+
return this.db
|
|
523
|
+
.prepare(`
|
|
263
524
|
SELECT pattern_hash as ruleId, rule_content as ruleContent, updated_at as publishedAt, 'atr-community' as source
|
|
264
525
|
FROM atr_proposals
|
|
265
526
|
WHERE status = 'confirmed' OR status = 'promoted'
|
|
266
527
|
ORDER BY updated_at ASC
|
|
267
|
-
`)
|
|
528
|
+
`)
|
|
529
|
+
.all();
|
|
268
530
|
}
|
|
269
531
|
/** Get IP blocklist from IoC entries and aggregated threat data / 取得 IP 封鎖清單 */
|
|
270
532
|
getIPBlocklist(minReputation) {
|
|
271
533
|
// IoC entries with sufficient reputation
|
|
272
|
-
const iocIPs = this.db
|
|
534
|
+
const iocIPs = this.db
|
|
535
|
+
.prepare(`
|
|
273
536
|
SELECT value FROM ioc_entries
|
|
274
537
|
WHERE type = 'ip' AND reputation >= ?
|
|
275
538
|
ORDER BY reputation DESC
|
|
276
|
-
`)
|
|
539
|
+
`)
|
|
540
|
+
.all(minReputation);
|
|
277
541
|
// Aggregate from threats table: distinct IPs with >= 3 occurrences
|
|
278
|
-
const threatIPs = this.db
|
|
542
|
+
const threatIPs = this.db
|
|
543
|
+
.prepare(`
|
|
279
544
|
SELECT attack_source_ip as value
|
|
280
545
|
FROM threats
|
|
281
546
|
GROUP BY attack_source_ip
|
|
282
547
|
HAVING COUNT(*) >= 3
|
|
283
|
-
`)
|
|
548
|
+
`)
|
|
549
|
+
.all();
|
|
284
550
|
// Merge and deduplicate
|
|
285
551
|
const ipSet = new Set();
|
|
286
552
|
for (const row of iocIPs)
|
|
@@ -291,16 +557,19 @@ export class ThreatCloudDB {
|
|
|
291
557
|
}
|
|
292
558
|
/** Get domain blocklist from IoC entries / 取得域名封鎖清單 */
|
|
293
559
|
getDomainBlocklist(minReputation) {
|
|
294
|
-
const iocDomains = this.db
|
|
560
|
+
const iocDomains = this.db
|
|
561
|
+
.prepare(`
|
|
295
562
|
SELECT value FROM ioc_entries
|
|
296
563
|
WHERE type = 'domain' AND reputation >= ?
|
|
297
564
|
ORDER BY reputation DESC
|
|
298
|
-
`)
|
|
565
|
+
`)
|
|
566
|
+
.all(minReputation);
|
|
299
567
|
return iocDomains.map((row) => row.value);
|
|
300
568
|
}
|
|
301
569
|
/** Upsert an IoC entry / 插入或更新 IoC 條目 */
|
|
302
570
|
upsertIoC(type, value, reputation, source) {
|
|
303
|
-
this.db
|
|
571
|
+
this.db
|
|
572
|
+
.prepare(`
|
|
304
573
|
INSERT INTO ioc_entries (type, value, reputation, source)
|
|
305
574
|
VALUES (?, ?, ?, ?)
|
|
306
575
|
ON CONFLICT(value) DO UPDATE SET
|
|
@@ -308,15 +577,18 @@ export class ThreatCloudDB {
|
|
|
308
577
|
source = excluded.source,
|
|
309
578
|
last_seen = datetime('now'),
|
|
310
579
|
sighting_count = sighting_count + 1
|
|
311
|
-
`)
|
|
580
|
+
`)
|
|
581
|
+
.run(type, value, reputation, source);
|
|
312
582
|
}
|
|
313
583
|
/** Promote confirmed proposals with approved LLM review to rules / 推廣已確認提案為規則 */
|
|
314
584
|
promoteConfirmedProposals() {
|
|
315
|
-
const proposals = this.db
|
|
585
|
+
const proposals = this.db
|
|
586
|
+
.prepare(`
|
|
316
587
|
SELECT pattern_hash, rule_content, llm_review_verdict
|
|
317
588
|
FROM atr_proposals
|
|
318
589
|
WHERE status = 'confirmed' AND llm_review_verdict IS NOT NULL
|
|
319
|
-
`)
|
|
590
|
+
`)
|
|
591
|
+
.all();
|
|
320
592
|
let promoted = 0;
|
|
321
593
|
for (const proposal of proposals) {
|
|
322
594
|
try {
|
|
@@ -329,10 +601,12 @@ export class ThreatCloudDB {
|
|
|
329
601
|
publishedAt: new Date().toISOString(),
|
|
330
602
|
source: 'atr-community',
|
|
331
603
|
});
|
|
332
|
-
this.db
|
|
604
|
+
this.db
|
|
605
|
+
.prepare(`
|
|
333
606
|
UPDATE atr_proposals SET status = 'promoted', updated_at = datetime('now')
|
|
334
607
|
WHERE pattern_hash = ?
|
|
335
|
-
`)
|
|
608
|
+
`)
|
|
609
|
+
.run(proposal.pattern_hash);
|
|
336
610
|
promoted++;
|
|
337
611
|
}
|
|
338
612
|
catch {
|
|
@@ -343,32 +617,41 @@ export class ThreatCloudDB {
|
|
|
343
617
|
}
|
|
344
618
|
/** Reject an ATR proposal / 拒絕 ATR 提案 */
|
|
345
619
|
rejectATRProposal(patternHash) {
|
|
346
|
-
this.db
|
|
620
|
+
this.db
|
|
621
|
+
.prepare(`
|
|
347
622
|
UPDATE atr_proposals SET status = 'rejected', updated_at = datetime('now')
|
|
348
623
|
WHERE pattern_hash = ?
|
|
349
|
-
`)
|
|
624
|
+
`)
|
|
625
|
+
.run(patternHash);
|
|
350
626
|
}
|
|
351
627
|
/** Get rules by source type, optionally filtered by date / 依來源取得規則 */
|
|
352
628
|
getRulesBySource(source, since) {
|
|
353
629
|
if (since) {
|
|
354
|
-
return this.db
|
|
355
|
-
|
|
630
|
+
return this.db
|
|
631
|
+
.prepare(`
|
|
632
|
+
SELECT rule_id as ruleId, rule_content as ruleContent, published_at as publishedAt, source,
|
|
633
|
+
category, severity, mitre_techniques as mitreTechniques, tags
|
|
356
634
|
FROM rules
|
|
357
635
|
WHERE source = ? AND published_at > ?
|
|
358
636
|
ORDER BY published_at ASC
|
|
359
|
-
`)
|
|
637
|
+
`)
|
|
638
|
+
.all(source, since);
|
|
360
639
|
}
|
|
361
|
-
return this.db
|
|
362
|
-
|
|
640
|
+
return this.db
|
|
641
|
+
.prepare(`
|
|
642
|
+
SELECT rule_id as ruleId, rule_content as ruleContent, published_at as publishedAt, source,
|
|
643
|
+
category, severity, mitre_techniques as mitreTechniques, tags
|
|
363
644
|
FROM rules
|
|
364
645
|
WHERE source = ?
|
|
365
646
|
ORDER BY published_at ASC
|
|
366
|
-
`)
|
|
647
|
+
`)
|
|
648
|
+
.all(source);
|
|
367
649
|
}
|
|
368
650
|
/** Report a safe skill (increment confirmations, auto-confirm at 3+) / 回報安全 skill */
|
|
369
651
|
reportSafeSkill(skillName, fingerprintHash) {
|
|
370
652
|
const normalized = skillName.toLowerCase().trim().replace(/\s+/g, '-');
|
|
371
|
-
this.db
|
|
653
|
+
this.db
|
|
654
|
+
.prepare(`
|
|
372
655
|
INSERT INTO skill_whitelist (skill_name, normalized_name, fingerprint_hash)
|
|
373
656
|
VALUES (?, ?, ?)
|
|
374
657
|
ON CONFLICT(normalized_name) DO UPDATE SET
|
|
@@ -376,16 +659,58 @@ export class ThreatCloudDB {
|
|
|
376
659
|
status = CASE WHEN confirmations + 1 >= 3 THEN 'confirmed' ELSE status END,
|
|
377
660
|
fingerprint_hash = COALESCE(excluded.fingerprint_hash, fingerprint_hash),
|
|
378
661
|
last_reported = datetime('now')
|
|
379
|
-
`)
|
|
662
|
+
`)
|
|
663
|
+
.run(skillName, normalized, fingerprintHash ?? null);
|
|
380
664
|
}
|
|
381
665
|
/** Get confirmed community whitelist / 取得社群白名單 */
|
|
382
666
|
getSkillWhitelist() {
|
|
383
|
-
return this.db
|
|
667
|
+
return this.db
|
|
668
|
+
.prepare(`
|
|
384
669
|
SELECT skill_name as name, fingerprint_hash as hash, confirmations
|
|
385
670
|
FROM skill_whitelist
|
|
386
671
|
WHERE status = 'confirmed'
|
|
387
672
|
ORDER BY confirmations DESC
|
|
388
|
-
`)
|
|
673
|
+
`)
|
|
674
|
+
.all();
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Get skill blacklist: skills reported by 3+ distinct clients with avg risk >= 70
|
|
678
|
+
* 取得技能黑名單:3+ 不同客戶端回報且平均風險 >= 70 的技能
|
|
679
|
+
*/
|
|
680
|
+
getSkillBlacklist(minReports = 3, minAvgRisk = 70) {
|
|
681
|
+
return this.db
|
|
682
|
+
.prepare(`
|
|
683
|
+
SELECT
|
|
684
|
+
skill_hash as skillHash,
|
|
685
|
+
skill_name as skillName,
|
|
686
|
+
ROUND(AVG(risk_score)) as avgRiskScore,
|
|
687
|
+
MAX(risk_level) as maxRiskLevel,
|
|
688
|
+
COUNT(DISTINCT COALESCE(client_id, 'anonymous')) as reportCount,
|
|
689
|
+
MIN(created_at) as firstReported,
|
|
690
|
+
MAX(created_at) as lastReported
|
|
691
|
+
FROM skill_threats
|
|
692
|
+
GROUP BY skill_hash
|
|
693
|
+
HAVING reportCount >= ? AND AVG(risk_score) >= ?
|
|
694
|
+
ORDER BY avgRiskScore DESC
|
|
695
|
+
`)
|
|
696
|
+
.all(minReports, minAvgRisk);
|
|
697
|
+
}
|
|
698
|
+
/** Backfill classification for rules with NULL category / 回填缺少分類的規則 */
|
|
699
|
+
backfillClassification() {
|
|
700
|
+
const unclassified = this.db
|
|
701
|
+
.prepare(`SELECT rule_id, rule_content, source FROM rules WHERE category IS NULL`)
|
|
702
|
+
.all();
|
|
703
|
+
let updated = 0;
|
|
704
|
+
const stmt = this.db.prepare(`
|
|
705
|
+
UPDATE rules SET category = ?, severity = ?, mitre_techniques = ?, tags = ?, updated_at = datetime('now')
|
|
706
|
+
WHERE rule_id = ?
|
|
707
|
+
`);
|
|
708
|
+
for (const row of unclassified) {
|
|
709
|
+
const meta = this.extractMetadata(row.rule_content, row.source);
|
|
710
|
+
stmt.run(meta.category, meta.severity, meta.mitreTechniques, meta.tags, row.rule_id);
|
|
711
|
+
updated++;
|
|
712
|
+
}
|
|
713
|
+
return updated;
|
|
389
714
|
}
|
|
390
715
|
/** Close the database / 關閉資料庫 */
|
|
391
716
|
close() {
|