@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
package/dist/database.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* SQLite database layer for Threat Cloud
|
|
3
3
|
* 威脅雲 SQLite 資料庫層
|
|
4
4
|
*
|
|
5
|
-
* Stores anonymized threat data
|
|
5
|
+
* Stores anonymized threat data and community rules using better-sqlite3.
|
|
6
6
|
*
|
|
7
7
|
* @module @panguard-ai/threat-cloud/database
|
|
8
8
|
*/
|
|
9
|
-
import { createHash } from 'node:crypto';
|
|
10
9
|
import Database from 'better-sqlite3';
|
|
11
10
|
/**
|
|
12
11
|
* Threat Cloud database backed by SQLite
|
|
@@ -18,27 +17,9 @@ export class ThreatCloudDB {
|
|
|
18
17
|
this.db = new Database(dbPath);
|
|
19
18
|
this.db.pragma('journal_mode = WAL');
|
|
20
19
|
this.db.pragma('foreign_keys = ON');
|
|
21
|
-
this.db.pragma('busy_timeout = 15000');
|
|
22
|
-
this.db.pragma('synchronous = NORMAL');
|
|
23
|
-
this.db.pragma('cache_size = -64000');
|
|
24
|
-
this.db.pragma('temp_store = MEMORY');
|
|
25
|
-
this.db.pragma('wal_autocheckpoint = 1000');
|
|
26
|
-
this.db.pragma('journal_size_limit = 104857600');
|
|
27
20
|
this.initialize();
|
|
28
|
-
this.runMigrations();
|
|
29
21
|
}
|
|
30
|
-
/** Create
|
|
31
|
-
backup(destPath) {
|
|
32
|
-
this.db.backup(destPath);
|
|
33
|
-
}
|
|
34
|
-
/** Expose underlying db for sub-modules (IoCStore, etc.) / 暴露底層 DB 給子模組 */
|
|
35
|
-
getDB() {
|
|
36
|
-
return this.db;
|
|
37
|
-
}
|
|
38
|
-
// -------------------------------------------------------------------------
|
|
39
|
-
// Schema initialization / 資料表初始化
|
|
40
|
-
// -------------------------------------------------------------------------
|
|
41
|
-
/** Create original tables if they don't exist / 建立原始資料表 */
|
|
22
|
+
/** Create tables if they don't exist / 建立資料表 */
|
|
42
23
|
initialize() {
|
|
43
24
|
this.db.exec(`
|
|
44
25
|
CREATE TABLE IF NOT EXISTS threats (
|
|
@@ -58,6 +39,10 @@ export class ThreatCloudDB {
|
|
|
58
39
|
rule_content TEXT NOT NULL,
|
|
59
40
|
published_at TEXT NOT NULL,
|
|
60
41
|
source TEXT NOT NULL,
|
|
42
|
+
category TEXT,
|
|
43
|
+
severity TEXT,
|
|
44
|
+
mitre_techniques TEXT,
|
|
45
|
+
tags TEXT,
|
|
61
46
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
62
47
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
63
48
|
);
|
|
@@ -67,199 +52,96 @@ export class ThreatCloudDB {
|
|
|
67
52
|
CREATE INDEX IF NOT EXISTS idx_threats_mitre ON threats(mitre_technique);
|
|
68
53
|
CREATE INDEX IF NOT EXISTS idx_rules_published ON rules(published_at);
|
|
69
54
|
|
|
70
|
-
CREATE TABLE IF NOT EXISTS
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
CREATE TABLE IF NOT EXISTS atr_proposals (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
pattern_hash TEXT NOT NULL,
|
|
58
|
+
rule_content TEXT NOT NULL,
|
|
59
|
+
llm_provider TEXT NOT NULL,
|
|
60
|
+
llm_model TEXT NOT NULL,
|
|
61
|
+
self_review_verdict TEXT NOT NULL,
|
|
62
|
+
client_id TEXT,
|
|
63
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
64
|
+
confirmations INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
llm_review_verdict TEXT,
|
|
66
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
67
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS atr_feedback (
|
|
71
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
rule_id TEXT NOT NULL,
|
|
73
|
+
is_true_positive INTEGER NOT NULL,
|
|
74
|
+
client_id TEXT,
|
|
75
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS skill_threats (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
skill_hash TEXT NOT NULL,
|
|
81
|
+
skill_name TEXT NOT NULL,
|
|
82
|
+
risk_score INTEGER NOT NULL,
|
|
83
|
+
risk_level TEXT NOT NULL,
|
|
84
|
+
finding_summaries TEXT,
|
|
85
|
+
client_id TEXT,
|
|
86
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS ioc_entries (
|
|
90
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
type TEXT NOT NULL,
|
|
92
|
+
value TEXT NOT NULL UNIQUE,
|
|
93
|
+
reputation INTEGER NOT NULL DEFAULT 50,
|
|
94
|
+
source TEXT,
|
|
95
|
+
first_seen TEXT DEFAULT (datetime('now')),
|
|
96
|
+
last_seen TEXT DEFAULT (datetime('now')),
|
|
97
|
+
sighting_count INTEGER DEFAULT 1
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE TABLE IF NOT EXISTS skill_whitelist (
|
|
101
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
102
|
+
skill_name TEXT NOT NULL,
|
|
103
|
+
normalized_name TEXT NOT NULL UNIQUE,
|
|
104
|
+
fingerprint_hash TEXT,
|
|
105
|
+
confirmations INTEGER NOT NULL DEFAULT 1,
|
|
106
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
107
|
+
first_reported TEXT DEFAULT (datetime('now')),
|
|
108
|
+
last_reported TEXT DEFAULT (datetime('now'))
|
|
74
109
|
);
|
|
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);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_atr_proposals_status ON atr_proposals(status);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_atr_proposals_pattern ON atr_proposals(pattern_hash);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_skill_threats_hash ON skill_threats(skill_hash);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_atr_feedback_rule ON atr_feedback(rule_id);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_ioc_entries_type ON ioc_entries(type);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_ioc_entries_reputation ON ioc_entries(reputation);
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_skill_whitelist_status ON skill_whitelist(status);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_skill_whitelist_name ON skill_whitelist(normalized_name);
|
|
75
128
|
`);
|
|
76
129
|
}
|
|
77
|
-
/** Run
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
version: 1,
|
|
83
|
-
name: 'create_iocs_table',
|
|
84
|
-
sql: `
|
|
85
|
-
CREATE TABLE IF NOT EXISTS iocs (
|
|
86
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
87
|
-
type TEXT NOT NULL CHECK(type IN ('ip','domain','url','hash_md5','hash_sha1','hash_sha256')),
|
|
88
|
-
value TEXT NOT NULL,
|
|
89
|
-
normalized_value TEXT NOT NULL,
|
|
90
|
-
threat_type TEXT NOT NULL,
|
|
91
|
-
source TEXT NOT NULL,
|
|
92
|
-
confidence INTEGER NOT NULL DEFAULT 50 CHECK(confidence BETWEEN 0 AND 100),
|
|
93
|
-
reputation_score INTEGER NOT NULL DEFAULT 50 CHECK(reputation_score BETWEEN 0 AND 100),
|
|
94
|
-
first_seen TEXT NOT NULL,
|
|
95
|
-
last_seen TEXT NOT NULL,
|
|
96
|
-
sightings INTEGER NOT NULL DEFAULT 1,
|
|
97
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','expired','revoked','under_review')),
|
|
98
|
-
tags TEXT NOT NULL DEFAULT '[]',
|
|
99
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
100
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
101
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
102
|
-
UNIQUE(type, normalized_value)
|
|
103
|
-
);
|
|
104
|
-
CREATE INDEX IF NOT EXISTS idx_iocs_type ON iocs(type);
|
|
105
|
-
CREATE INDEX IF NOT EXISTS idx_iocs_normalized ON iocs(normalized_value);
|
|
106
|
-
CREATE INDEX IF NOT EXISTS idx_iocs_reputation ON iocs(reputation_score DESC);
|
|
107
|
-
CREATE INDEX IF NOT EXISTS idx_iocs_last_seen ON iocs(last_seen);
|
|
108
|
-
CREATE INDEX IF NOT EXISTS idx_iocs_status ON iocs(status);
|
|
109
|
-
CREATE INDEX IF NOT EXISTS idx_iocs_source ON iocs(source);
|
|
110
|
-
`,
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
version: 2,
|
|
114
|
-
name: 'create_enriched_threats_table',
|
|
115
|
-
sql: `
|
|
116
|
-
CREATE TABLE IF NOT EXISTS enriched_threats (
|
|
117
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
118
|
-
source_type TEXT NOT NULL CHECK(source_type IN ('guard','trap','external_feed')),
|
|
119
|
-
attack_source_ip TEXT NOT NULL,
|
|
120
|
-
attack_type TEXT NOT NULL,
|
|
121
|
-
mitre_techniques TEXT NOT NULL DEFAULT '[]',
|
|
122
|
-
sigma_rule_matched TEXT NOT NULL DEFAULT '',
|
|
123
|
-
timestamp TEXT NOT NULL,
|
|
124
|
-
industry TEXT,
|
|
125
|
-
region TEXT NOT NULL DEFAULT 'unknown',
|
|
126
|
-
confidence INTEGER NOT NULL DEFAULT 50,
|
|
127
|
-
severity TEXT NOT NULL DEFAULT 'medium',
|
|
128
|
-
service_type TEXT,
|
|
129
|
-
skill_level TEXT,
|
|
130
|
-
intent TEXT,
|
|
131
|
-
tools TEXT,
|
|
132
|
-
event_hash TEXT NOT NULL UNIQUE,
|
|
133
|
-
received_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
134
|
-
campaign_id TEXT
|
|
135
|
-
);
|
|
136
|
-
CREATE INDEX IF NOT EXISTS idx_enriched_timestamp ON enriched_threats(timestamp);
|
|
137
|
-
CREATE INDEX IF NOT EXISTS idx_enriched_attack_type ON enriched_threats(attack_type);
|
|
138
|
-
CREATE INDEX IF NOT EXISTS idx_enriched_ip ON enriched_threats(attack_source_ip);
|
|
139
|
-
CREATE INDEX IF NOT EXISTS idx_enriched_campaign ON enriched_threats(campaign_id);
|
|
140
|
-
CREATE INDEX IF NOT EXISTS idx_enriched_source_type ON enriched_threats(source_type);
|
|
141
|
-
CREATE INDEX IF NOT EXISTS idx_enriched_region ON enriched_threats(region);
|
|
142
|
-
CREATE INDEX IF NOT EXISTS idx_enriched_severity ON enriched_threats(severity);
|
|
143
|
-
CREATE INDEX IF NOT EXISTS idx_enriched_received ON enriched_threats(received_at);
|
|
144
|
-
`,
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
version: 3,
|
|
148
|
-
name: 'create_trap_credentials_table',
|
|
149
|
-
sql: `
|
|
150
|
-
CREATE TABLE IF NOT EXISTS trap_credentials (
|
|
151
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
152
|
-
enriched_threat_id INTEGER NOT NULL REFERENCES enriched_threats(id) ON DELETE CASCADE,
|
|
153
|
-
username TEXT NOT NULL,
|
|
154
|
-
attempt_count INTEGER NOT NULL DEFAULT 1,
|
|
155
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
156
|
-
);
|
|
157
|
-
CREATE INDEX IF NOT EXISTS idx_trap_creds_threat ON trap_credentials(enriched_threat_id);
|
|
158
|
-
CREATE INDEX IF NOT EXISTS idx_trap_creds_username ON trap_credentials(username);
|
|
159
|
-
`,
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
version: 4,
|
|
163
|
-
name: 'create_generated_patterns_table',
|
|
164
|
-
sql: `
|
|
165
|
-
CREATE TABLE IF NOT EXISTS generated_patterns (
|
|
166
|
-
pattern_hash TEXT PRIMARY KEY,
|
|
167
|
-
attack_type TEXT NOT NULL,
|
|
168
|
-
mitre_techniques TEXT NOT NULL,
|
|
169
|
-
rule_id TEXT NOT NULL REFERENCES rules(rule_id) ON DELETE CASCADE,
|
|
170
|
-
occurrences INTEGER NOT NULL,
|
|
171
|
-
distinct_ips INTEGER NOT NULL,
|
|
172
|
-
first_seen TEXT NOT NULL,
|
|
173
|
-
last_seen TEXT NOT NULL,
|
|
174
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
175
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
176
|
-
);
|
|
177
|
-
CREATE INDEX IF NOT EXISTS idx_gen_patterns_attack ON generated_patterns(attack_type);
|
|
178
|
-
CREATE INDEX IF NOT EXISTS idx_gen_patterns_rule ON generated_patterns(rule_id);
|
|
179
|
-
`,
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
version: 5,
|
|
183
|
-
name: 'create_daily_aggregates_table',
|
|
184
|
-
sql: `
|
|
185
|
-
CREATE TABLE IF NOT EXISTS daily_aggregates (
|
|
186
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
187
|
-
date TEXT NOT NULL,
|
|
188
|
-
attack_type TEXT NOT NULL,
|
|
189
|
-
region TEXT NOT NULL,
|
|
190
|
-
source_type TEXT NOT NULL,
|
|
191
|
-
event_count INTEGER NOT NULL,
|
|
192
|
-
unique_ips INTEGER NOT NULL,
|
|
193
|
-
avg_confidence REAL NOT NULL,
|
|
194
|
-
severity_distribution TEXT NOT NULL DEFAULT '{}',
|
|
195
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
196
|
-
UNIQUE(date, attack_type, region, source_type)
|
|
197
|
-
);
|
|
198
|
-
CREATE INDEX IF NOT EXISTS idx_daily_agg_date ON daily_aggregates(date);
|
|
199
|
-
CREATE INDEX IF NOT EXISTS idx_daily_agg_type ON daily_aggregates(attack_type);
|
|
200
|
-
`,
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
version: 6,
|
|
204
|
-
name: 'create_sightings_table',
|
|
205
|
-
sql: `
|
|
206
|
-
CREATE TABLE IF NOT EXISTS sightings (
|
|
207
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
208
|
-
ioc_id INTEGER NOT NULL REFERENCES iocs(id) ON DELETE CASCADE,
|
|
209
|
-
type TEXT NOT NULL CHECK(type IN ('positive','negative','false_positive')),
|
|
210
|
-
source TEXT NOT NULL,
|
|
211
|
-
confidence INTEGER NOT NULL DEFAULT 50 CHECK(confidence BETWEEN 0 AND 100),
|
|
212
|
-
details TEXT NOT NULL DEFAULT '',
|
|
213
|
-
actor_hash TEXT NOT NULL DEFAULT '',
|
|
214
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
215
|
-
);
|
|
216
|
-
CREATE INDEX IF NOT EXISTS idx_sightings_ioc ON sightings(ioc_id);
|
|
217
|
-
CREATE INDEX IF NOT EXISTS idx_sightings_type ON sightings(type);
|
|
218
|
-
CREATE INDEX IF NOT EXISTS idx_sightings_created ON sightings(created_at);
|
|
219
|
-
`,
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
version: 7,
|
|
223
|
-
name: 'create_audit_log_table',
|
|
224
|
-
sql: `
|
|
225
|
-
CREATE TABLE IF NOT EXISTS audit_log (
|
|
226
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
227
|
-
action TEXT NOT NULL,
|
|
228
|
-
entity_type TEXT NOT NULL,
|
|
229
|
-
entity_id TEXT NOT NULL,
|
|
230
|
-
actor_hash TEXT NOT NULL DEFAULT '',
|
|
231
|
-
ip_address TEXT NOT NULL DEFAULT '',
|
|
232
|
-
details TEXT NOT NULL DEFAULT '{}',
|
|
233
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
234
|
-
);
|
|
235
|
-
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action);
|
|
236
|
-
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id);
|
|
237
|
-
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at);
|
|
238
|
-
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log(actor_hash);
|
|
239
|
-
`,
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
version: 8,
|
|
243
|
-
name: 'add_source_reliability_to_iocs',
|
|
244
|
-
sql: `
|
|
245
|
-
ALTER TABLE iocs ADD COLUMN source_reliability TEXT NOT NULL DEFAULT 'F'
|
|
246
|
-
CHECK(source_reliability IN ('A','B','C','D','E','F'));
|
|
247
|
-
`,
|
|
248
|
-
},
|
|
249
|
-
];
|
|
250
|
-
const insertMigration = this.db.prepare('INSERT INTO schema_migrations (version, name) VALUES (?, ?)');
|
|
251
|
-
for (const m of migrations) {
|
|
252
|
-
if (!applied.has(m.version)) {
|
|
253
|
-
this.db.transaction(() => {
|
|
254
|
-
this.db.exec(m.sql);
|
|
255
|
-
insertMigration.run(m.version, m.name);
|
|
256
|
-
})();
|
|
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}`);
|
|
257
135
|
}
|
|
258
|
-
|
|
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');
|
|
259
144
|
}
|
|
260
|
-
// -------------------------------------------------------------------------
|
|
261
|
-
// Legacy threat operations (backward compatible) / 原始威脅操作
|
|
262
|
-
// -------------------------------------------------------------------------
|
|
263
145
|
/** Insert anonymized threat data / 插入匿名化威脅數據 */
|
|
264
146
|
insertThreat(data) {
|
|
265
147
|
const stmt = this.db.prepare(`
|
|
@@ -268,141 +150,293 @@ export class ThreatCloudDB {
|
|
|
268
150
|
`);
|
|
269
151
|
stmt.run(data.attackSourceIP, data.attackType, data.mitreTechnique, data.sigmaRuleMatched, data.timestamp, data.industry ?? null, data.region);
|
|
270
152
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
153
|
+
/** Extract classification metadata from rule content / 從規則內容提取分類元資料 */
|
|
154
|
+
extractMetadata(ruleContent, source) {
|
|
155
|
+
let category = 'unknown';
|
|
156
|
+
let severity = 'medium';
|
|
157
|
+
let mitreTechniques = '';
|
|
158
|
+
let tags = '';
|
|
159
|
+
try {
|
|
160
|
+
if (source === 'yara') {
|
|
161
|
+
// YARA rules: extract from meta section
|
|
162
|
+
const metaMatch = ruleContent.match(/meta\s*:\s*([\s\S]*?)(?:strings|condition)\s*:/);
|
|
163
|
+
if (metaMatch) {
|
|
164
|
+
const meta = metaMatch[1];
|
|
165
|
+
const catMatch = meta.match(/category\s*=\s*"([^"]+)"/);
|
|
166
|
+
if (catMatch)
|
|
167
|
+
category = catMatch[1].toLowerCase();
|
|
168
|
+
const sevMatch = meta.match(/severity\s*=\s*"([^"]+)"/);
|
|
169
|
+
if (sevMatch)
|
|
170
|
+
severity = sevMatch[1].toLowerCase();
|
|
171
|
+
const mitreMatch = meta.match(/mitre_att(?:ack|&ck)\s*=\s*"([^"]+)"/i);
|
|
172
|
+
if (mitreMatch)
|
|
173
|
+
mitreTechniques = mitreMatch[1];
|
|
174
|
+
}
|
|
175
|
+
// Fallback: infer category from rule content keywords
|
|
176
|
+
if (category === 'unknown') {
|
|
177
|
+
if (/malware|trojan|ransom|backdoor|rat_|infostealer/i.test(ruleContent))
|
|
178
|
+
category = 'malware';
|
|
179
|
+
else if (/exploit|cve-/i.test(ruleContent))
|
|
180
|
+
category = 'exploit';
|
|
181
|
+
else if (/hack_?tool|offensive|cobalt/i.test(ruleContent))
|
|
182
|
+
category = 'hacktool';
|
|
183
|
+
else if (/webshell/i.test(ruleContent))
|
|
184
|
+
category = 'webshell';
|
|
185
|
+
else if (/packer|obfusc|crypter/i.test(ruleContent))
|
|
186
|
+
category = 'packer';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Sigma / ATR: YAML-based extraction
|
|
191
|
+
// Extract level/severity
|
|
192
|
+
const sevMatch = ruleContent.match(/(?:^|\n)\s*(?:level|severity)\s*:\s*(\w+)/);
|
|
193
|
+
if (sevMatch)
|
|
194
|
+
severity = sevMatch[1].toLowerCase();
|
|
195
|
+
// Extract tags list
|
|
196
|
+
const tagMatch = ruleContent.match(/(?:^|\n)\s*tags\s*:\s*\n((?:\s+-\s*.+\n?)+)/);
|
|
197
|
+
if (tagMatch) {
|
|
198
|
+
const tagLines = tagMatch[1].match(/-\s*(.+)/g) ?? [];
|
|
199
|
+
const tagList = tagLines.map((t) => t.replace(/^-\s*/, '').trim());
|
|
200
|
+
tags = tagList.join(',');
|
|
201
|
+
// Derive MITRE techniques from tags (attack.tXXXX)
|
|
202
|
+
const mitreTags = tagList.filter((t) => /^attack\.t\d+/i.test(t));
|
|
203
|
+
if (mitreTags.length > 0) {
|
|
204
|
+
mitreTechniques = mitreTags
|
|
205
|
+
.map((t) => t.replace(/^attack\./i, '').toUpperCase())
|
|
206
|
+
.join(',');
|
|
207
|
+
}
|
|
208
|
+
// Derive category from MITRE ATT&CK tactic tags
|
|
209
|
+
const attackTags = tagList.filter((t) => t.startsWith('attack.'));
|
|
210
|
+
for (const tag of attackTags) {
|
|
211
|
+
if (/initial.access/i.test(tag)) {
|
|
212
|
+
category = 'initial-access';
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (/execution/i.test(tag)) {
|
|
216
|
+
category = 'execution';
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
if (/persistence/i.test(tag)) {
|
|
220
|
+
category = 'persistence';
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
if (/privilege.escalation/i.test(tag)) {
|
|
224
|
+
category = 'privilege-escalation';
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (/defense.evasion/i.test(tag)) {
|
|
228
|
+
category = 'defense-evasion';
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (/credential.access/i.test(tag)) {
|
|
232
|
+
category = 'credential-access';
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (/discovery/i.test(tag)) {
|
|
236
|
+
category = 'discovery';
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
if (/lateral.movement/i.test(tag)) {
|
|
240
|
+
category = 'lateral-movement';
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (/collection/i.test(tag)) {
|
|
244
|
+
category = 'collection';
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
if (/exfiltration/i.test(tag)) {
|
|
248
|
+
category = 'exfiltration';
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (/command.and.control|c2/i.test(tag)) {
|
|
252
|
+
category = 'command-and-control';
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (/impact/i.test(tag)) {
|
|
256
|
+
category = 'impact';
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
if (/resource.development/i.test(tag)) {
|
|
260
|
+
category = 'resource-development';
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
if (/reconnaissance/i.test(tag)) {
|
|
264
|
+
category = 'reconnaissance';
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Fallback: infer from logsource (Sigma)
|
|
270
|
+
if (category === 'unknown') {
|
|
271
|
+
const lsCatMatch = ruleContent.match(/(?:^|\n)\s*logsource\s*:\s*\n(?:\s+\w+\s*:.+\n)*?\s+category\s*:\s*(\S+)/);
|
|
272
|
+
if (lsCatMatch) {
|
|
273
|
+
category = lsCatMatch[1].toLowerCase();
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const lsProdMatch = ruleContent.match(/(?:^|\n)\s*logsource\s*:\s*\n(?:\s+\w+\s*:.+\n)*?\s+product\s*:\s*(\S+)/);
|
|
277
|
+
if (lsProdMatch)
|
|
278
|
+
category = lsProdMatch[1].toLowerCase();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
302
281
|
}
|
|
303
|
-
});
|
|
304
|
-
insertAll(credentials);
|
|
305
|
-
}
|
|
306
|
-
/** Get enriched threats count by source type / 依來源類型取得豐富化威脅數量 */
|
|
307
|
-
getEnrichedThreatCountBySource() {
|
|
308
|
-
const rows = this.db
|
|
309
|
-
.prepare('SELECT source_type, COUNT(*) as count FROM enriched_threats GROUP BY source_type')
|
|
310
|
-
.all();
|
|
311
|
-
const result = {};
|
|
312
|
-
for (const r of rows) {
|
|
313
|
-
result[r.source_type] = r.count;
|
|
314
282
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
// -------------------------------------------------------------------------
|
|
324
|
-
// Conversion helpers / 轉換輔助函式
|
|
325
|
-
// -------------------------------------------------------------------------
|
|
326
|
-
/** Convert AnonymizedThreatData to EnrichedThreatEvent / 轉換 Guard 資料 */
|
|
327
|
-
static guardToEnriched(data) {
|
|
328
|
-
const hashInput = `${data.attackSourceIP}|${data.attackType}|${data.mitreTechnique}|${data.timestamp}`;
|
|
329
|
-
const eventHash = createHash('sha256').update(hashInput).digest('hex');
|
|
330
|
-
return {
|
|
331
|
-
sourceType: 'guard',
|
|
332
|
-
attackSourceIP: data.attackSourceIP,
|
|
333
|
-
attackType: data.attackType,
|
|
334
|
-
mitreTechniques: [data.mitreTechnique],
|
|
335
|
-
sigmaRuleMatched: data.sigmaRuleMatched,
|
|
336
|
-
timestamp: data.timestamp,
|
|
337
|
-
industry: data.industry,
|
|
338
|
-
region: data.region,
|
|
339
|
-
confidence: 50,
|
|
340
|
-
severity: 'medium',
|
|
341
|
-
eventHash,
|
|
342
|
-
receivedAt: new Date().toISOString(),
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
/** Convert TrapIntelligencePayload to EnrichedThreatEvent / 轉換 Trap 資料 */
|
|
346
|
-
static trapToEnriched(data) {
|
|
347
|
-
const techniques = data.mitreTechniques ?? [];
|
|
348
|
-
const hashInput = `${data.sourceIP}|${data.attackType}|${techniques.join(',')}|${data.timestamp}`;
|
|
349
|
-
const eventHash = createHash('sha256').update(hashInput).digest('hex');
|
|
350
|
-
return {
|
|
351
|
-
sourceType: 'trap',
|
|
352
|
-
attackSourceIP: data.sourceIP,
|
|
353
|
-
attackType: data.attackType,
|
|
354
|
-
mitreTechniques: techniques,
|
|
355
|
-
sigmaRuleMatched: '',
|
|
356
|
-
timestamp: data.timestamp,
|
|
357
|
-
region: data.region ?? 'unknown',
|
|
358
|
-
confidence: 60,
|
|
359
|
-
severity: data.skillLevel === 'apt' || data.skillLevel === 'advanced' ? 'high' : 'medium',
|
|
360
|
-
serviceType: data.serviceType,
|
|
361
|
-
skillLevel: data.skillLevel,
|
|
362
|
-
intent: data.intent,
|
|
363
|
-
tools: data.tools,
|
|
364
|
-
eventHash,
|
|
365
|
-
receivedAt: new Date().toISOString(),
|
|
366
|
-
};
|
|
283
|
+
catch {
|
|
284
|
+
// Extraction failed — keep defaults
|
|
285
|
+
}
|
|
286
|
+
// Normalize category to lowercase
|
|
287
|
+
category = category.toLowerCase();
|
|
288
|
+
severity = severity.toLowerCase();
|
|
289
|
+
return { category, severity, mitreTechniques, tags };
|
|
367
290
|
}
|
|
368
|
-
// -------------------------------------------------------------------------
|
|
369
|
-
// Rules / 規則
|
|
370
|
-
// -------------------------------------------------------------------------
|
|
371
291
|
/** Insert or update a community rule / 插入或更新社群規則 */
|
|
372
292
|
upsertRule(rule) {
|
|
293
|
+
// Extract classification from content if not provided
|
|
294
|
+
const meta = this.extractMetadata(rule.ruleContent, rule.source);
|
|
295
|
+
const category = rule.category ?? meta.category;
|
|
296
|
+
const severity = rule.severity ?? meta.severity;
|
|
297
|
+
const mitreTechniques = rule.mitreTechniques ?? meta.mitreTechniques;
|
|
298
|
+
const tags = rule.tags ?? meta.tags;
|
|
373
299
|
const stmt = this.db.prepare(`
|
|
374
|
-
INSERT INTO rules (rule_id, rule_content, published_at, source)
|
|
375
|
-
VALUES (?, ?, ?, ?)
|
|
300
|
+
INSERT INTO rules (rule_id, rule_content, published_at, source, category, severity, mitre_techniques, tags)
|
|
301
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
376
302
|
ON CONFLICT(rule_id) DO UPDATE SET
|
|
377
303
|
rule_content = excluded.rule_content,
|
|
378
304
|
published_at = excluded.published_at,
|
|
379
305
|
source = excluded.source,
|
|
306
|
+
category = excluded.category,
|
|
307
|
+
severity = excluded.severity,
|
|
308
|
+
mitre_techniques = excluded.mitre_techniques,
|
|
309
|
+
tags = excluded.tags,
|
|
380
310
|
updated_at = datetime('now')
|
|
381
311
|
`);
|
|
382
|
-
stmt.run(rule.ruleId, rule.ruleContent, rule.publishedAt, rule.source);
|
|
312
|
+
stmt.run(rule.ruleId, rule.ruleContent, rule.publishedAt, rule.source, category, severity, mitreTechniques, tags);
|
|
383
313
|
}
|
|
384
314
|
/** Fetch rules published after a given timestamp / 取得指定時間後發佈的規則 */
|
|
385
|
-
getRulesSince(since) {
|
|
315
|
+
getRulesSince(since, filters) {
|
|
316
|
+
let sql = `SELECT rule_id as ruleId, rule_content as ruleContent, published_at as publishedAt, source,
|
|
317
|
+
category, severity, mitre_techniques as mitreTechniques, tags
|
|
318
|
+
FROM rules WHERE published_at > ?`;
|
|
319
|
+
const params = [since];
|
|
320
|
+
if (filters?.category) {
|
|
321
|
+
sql += ' AND category = ?';
|
|
322
|
+
params.push(filters.category);
|
|
323
|
+
}
|
|
324
|
+
if (filters?.severity) {
|
|
325
|
+
sql += ' AND severity = ?';
|
|
326
|
+
params.push(filters.severity);
|
|
327
|
+
}
|
|
328
|
+
if (filters?.source) {
|
|
329
|
+
sql += ' AND source = ?';
|
|
330
|
+
params.push(filters.source);
|
|
331
|
+
}
|
|
332
|
+
sql += ' ORDER BY published_at ASC';
|
|
333
|
+
return this.db.prepare(sql).all(...params);
|
|
334
|
+
}
|
|
335
|
+
/** Fetch all rules with limit / 取得所有規則(含限制) */
|
|
336
|
+
getAllRules(limit = 5000, filters) {
|
|
337
|
+
let sql = `SELECT rule_id as ruleId, rule_content as ruleContent, published_at as publishedAt, source,
|
|
338
|
+
category, severity, mitre_techniques as mitreTechniques, tags
|
|
339
|
+
FROM rules WHERE 1=1`;
|
|
340
|
+
const params = [];
|
|
341
|
+
if (filters?.category) {
|
|
342
|
+
sql += ' AND category = ?';
|
|
343
|
+
params.push(filters.category);
|
|
344
|
+
}
|
|
345
|
+
if (filters?.severity) {
|
|
346
|
+
sql += ' AND severity = ?';
|
|
347
|
+
params.push(filters.severity);
|
|
348
|
+
}
|
|
349
|
+
if (filters?.source) {
|
|
350
|
+
sql += ' AND source = ?';
|
|
351
|
+
params.push(filters.source);
|
|
352
|
+
}
|
|
353
|
+
sql += ' ORDER BY published_at DESC LIMIT ?';
|
|
354
|
+
params.push(limit);
|
|
355
|
+
return this.db.prepare(sql).all(...params);
|
|
356
|
+
}
|
|
357
|
+
/** Insert ATR rule proposal / 插入 ATR 規則提案 */
|
|
358
|
+
insertATRProposal(proposal) {
|
|
386
359
|
const stmt = this.db.prepare(`
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
WHERE published_at > ?
|
|
390
|
-
ORDER BY published_at ASC
|
|
360
|
+
INSERT INTO atr_proposals (pattern_hash, rule_content, llm_provider, llm_model, self_review_verdict, client_id)
|
|
361
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
391
362
|
`);
|
|
392
|
-
|
|
363
|
+
stmt.run(proposal.patternHash, proposal.ruleContent, proposal.llmProvider, proposal.llmModel, proposal.selfReviewVerdict, proposal.clientId ?? null);
|
|
364
|
+
}
|
|
365
|
+
/** Get ATR proposals, optionally filtered by status / 取得 ATR 提案 */
|
|
366
|
+
getATRProposals(status) {
|
|
367
|
+
if (status) {
|
|
368
|
+
return this.db
|
|
369
|
+
.prepare('SELECT * FROM atr_proposals WHERE status = ? ORDER BY created_at DESC')
|
|
370
|
+
.all(status);
|
|
371
|
+
}
|
|
372
|
+
return this.db.prepare('SELECT * FROM atr_proposals ORDER BY created_at DESC').all();
|
|
373
|
+
}
|
|
374
|
+
/** Increment confirmations for a proposal; auto-confirm at >= 3 / 增加提案確認數 */
|
|
375
|
+
confirmATRProposal(patternHash) {
|
|
376
|
+
this.db
|
|
377
|
+
.prepare(`
|
|
378
|
+
UPDATE atr_proposals
|
|
379
|
+
SET confirmations = confirmations + 1,
|
|
380
|
+
status = CASE WHEN confirmations + 1 >= 3 THEN 'confirmed' ELSE status END,
|
|
381
|
+
updated_at = datetime('now')
|
|
382
|
+
WHERE pattern_hash = ?
|
|
383
|
+
`)
|
|
384
|
+
.run(patternHash);
|
|
385
|
+
}
|
|
386
|
+
/** Update LLM review verdict for a proposal / 更新 LLM 審查結果 */
|
|
387
|
+
updateATRProposalLLMReview(patternHash, verdict) {
|
|
388
|
+
this.db
|
|
389
|
+
.prepare(`
|
|
390
|
+
UPDATE atr_proposals SET llm_review_verdict = ?, updated_at = datetime('now') WHERE pattern_hash = ?
|
|
391
|
+
`)
|
|
392
|
+
.run(verdict, patternHash);
|
|
393
393
|
}
|
|
394
|
-
/**
|
|
395
|
-
|
|
394
|
+
/** Insert ATR feedback / 插入 ATR 回饋 */
|
|
395
|
+
insertATRFeedback(ruleId, isTruePositive, clientId) {
|
|
396
|
+
this.db
|
|
397
|
+
.prepare(`
|
|
398
|
+
INSERT INTO atr_feedback (rule_id, is_true_positive, client_id) VALUES (?, ?, ?)
|
|
399
|
+
`)
|
|
400
|
+
.run(ruleId, isTruePositive ? 1 : 0, clientId ?? null);
|
|
401
|
+
}
|
|
402
|
+
/** Get feedback stats for a rule / 取得規則回饋統計 */
|
|
403
|
+
getATRFeedbackStats(ruleId) {
|
|
404
|
+
const tp = this.db
|
|
405
|
+
.prepare('SELECT COUNT(*) as count FROM atr_feedback WHERE rule_id = ? AND is_true_positive = 1')
|
|
406
|
+
.get(ruleId).count;
|
|
407
|
+
const fp = this.db
|
|
408
|
+
.prepare('SELECT COUNT(*) as count FROM atr_feedback WHERE rule_id = ? AND is_true_positive = 0')
|
|
409
|
+
.get(ruleId).count;
|
|
410
|
+
return { truePositives: tp, falsePositives: fp };
|
|
411
|
+
}
|
|
412
|
+
/** Insert skill threat submission / 插入技能威脅提交 */
|
|
413
|
+
insertSkillThreat(submission) {
|
|
396
414
|
const stmt = this.db.prepare(`
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
ORDER BY published_at DESC
|
|
415
|
+
INSERT INTO skill_threats (skill_hash, skill_name, risk_score, risk_level, finding_summaries, client_id)
|
|
416
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
400
417
|
`);
|
|
401
|
-
|
|
418
|
+
stmt.run(submission.skillHash, submission.skillName, submission.riskScore, submission.riskLevel, submission.findingSummaries ? JSON.stringify(submission.findingSummaries) : null, submission.clientId ?? null);
|
|
419
|
+
}
|
|
420
|
+
/** Get recent skill threats / 取得最近技能威脅 */
|
|
421
|
+
getSkillThreats(limit = 50) {
|
|
422
|
+
return this.db
|
|
423
|
+
.prepare('SELECT * FROM skill_threats ORDER BY created_at DESC LIMIT ?')
|
|
424
|
+
.all(limit);
|
|
425
|
+
}
|
|
426
|
+
/** Get proposal statistics / 取得提案統計 */
|
|
427
|
+
getProposalStats() {
|
|
428
|
+
const pending = this.db
|
|
429
|
+
.prepare("SELECT COUNT(*) as count FROM atr_proposals WHERE status = 'pending'")
|
|
430
|
+
.get().count;
|
|
431
|
+
const confirmed = this.db
|
|
432
|
+
.prepare("SELECT COUNT(*) as count FROM atr_proposals WHERE status = 'confirmed'")
|
|
433
|
+
.get().count;
|
|
434
|
+
const rejected = this.db
|
|
435
|
+
.prepare("SELECT COUNT(*) as count FROM atr_proposals WHERE status = 'rejected'")
|
|
436
|
+
.get().count;
|
|
437
|
+
const total = this.db.prepare('SELECT COUNT(*) as count FROM atr_proposals').get().count;
|
|
438
|
+
return { pending, confirmed, rejected, total };
|
|
402
439
|
}
|
|
403
|
-
// -------------------------------------------------------------------------
|
|
404
|
-
// Statistics / 統計
|
|
405
|
-
// -------------------------------------------------------------------------
|
|
406
440
|
/** Get threat statistics / 取得威脅統計 */
|
|
407
441
|
getStats() {
|
|
408
442
|
const totalThreats = this.db.prepare('SELECT COUNT(*) as count FROM threats').get().count;
|
|
@@ -426,6 +460,27 @@ export class ThreatCloudDB {
|
|
|
426
460
|
GROUP BY mitre_technique
|
|
427
461
|
ORDER BY count DESC
|
|
428
462
|
LIMIT 10
|
|
463
|
+
`)
|
|
464
|
+
.all();
|
|
465
|
+
const proposalStats = this.getProposalStats();
|
|
466
|
+
const skillThreatsTotal = this.db.prepare('SELECT COUNT(*) as count FROM skill_threats').get().count;
|
|
467
|
+
const skillBlacklistTotal = this.getSkillBlacklist().length;
|
|
468
|
+
const rulesByCategory = this.db
|
|
469
|
+
.prepare(`
|
|
470
|
+
SELECT COALESCE(category, 'unknown') as category, COUNT(*) as count
|
|
471
|
+
FROM rules GROUP BY category ORDER BY count DESC LIMIT 20
|
|
472
|
+
`)
|
|
473
|
+
.all();
|
|
474
|
+
const rulesBySeverity = this.db
|
|
475
|
+
.prepare(`
|
|
476
|
+
SELECT COALESCE(severity, 'unknown') as severity, COUNT(*) as count
|
|
477
|
+
FROM rules GROUP BY severity ORDER BY count DESC
|
|
478
|
+
`)
|
|
479
|
+
.all();
|
|
480
|
+
const rulesBySource = this.db
|
|
481
|
+
.prepare(`
|
|
482
|
+
SELECT source, COUNT(*) as count
|
|
483
|
+
FROM rules GROUP BY source ORDER BY count DESC
|
|
429
484
|
`)
|
|
430
485
|
.all();
|
|
431
486
|
return {
|
|
@@ -434,8 +489,219 @@ export class ThreatCloudDB {
|
|
|
434
489
|
topAttackTypes,
|
|
435
490
|
topMitreTechniques,
|
|
436
491
|
last24hThreats: last24h,
|
|
492
|
+
proposalStats,
|
|
493
|
+
skillThreatsTotal,
|
|
494
|
+
skillBlacklistTotal,
|
|
495
|
+
rulesByCategory,
|
|
496
|
+
rulesBySeverity,
|
|
497
|
+
rulesBySource,
|
|
437
498
|
};
|
|
438
499
|
}
|
|
500
|
+
/** Get confirmed/promoted ATR rules, optionally filtered by date / 取得已確認 ATR 規則 */
|
|
501
|
+
getConfirmedATRRules(since) {
|
|
502
|
+
if (since) {
|
|
503
|
+
return this.db
|
|
504
|
+
.prepare(`
|
|
505
|
+
SELECT pattern_hash as ruleId, rule_content as ruleContent, updated_at as publishedAt, 'atr-community' as source
|
|
506
|
+
FROM atr_proposals
|
|
507
|
+
WHERE (status = 'confirmed' OR status = 'promoted') AND updated_at > ?
|
|
508
|
+
ORDER BY updated_at ASC
|
|
509
|
+
`)
|
|
510
|
+
.all(since);
|
|
511
|
+
}
|
|
512
|
+
return this.db
|
|
513
|
+
.prepare(`
|
|
514
|
+
SELECT pattern_hash as ruleId, rule_content as ruleContent, updated_at as publishedAt, 'atr-community' as source
|
|
515
|
+
FROM atr_proposals
|
|
516
|
+
WHERE status = 'confirmed' OR status = 'promoted'
|
|
517
|
+
ORDER BY updated_at ASC
|
|
518
|
+
`)
|
|
519
|
+
.all();
|
|
520
|
+
}
|
|
521
|
+
/** Get IP blocklist from IoC entries and aggregated threat data / 取得 IP 封鎖清單 */
|
|
522
|
+
getIPBlocklist(minReputation) {
|
|
523
|
+
// IoC entries with sufficient reputation
|
|
524
|
+
const iocIPs = this.db
|
|
525
|
+
.prepare(`
|
|
526
|
+
SELECT value FROM ioc_entries
|
|
527
|
+
WHERE type = 'ip' AND reputation >= ?
|
|
528
|
+
ORDER BY reputation DESC
|
|
529
|
+
`)
|
|
530
|
+
.all(minReputation);
|
|
531
|
+
// Aggregate from threats table: distinct IPs with >= 3 occurrences
|
|
532
|
+
const threatIPs = this.db
|
|
533
|
+
.prepare(`
|
|
534
|
+
SELECT attack_source_ip as value
|
|
535
|
+
FROM threats
|
|
536
|
+
GROUP BY attack_source_ip
|
|
537
|
+
HAVING COUNT(*) >= 3
|
|
538
|
+
`)
|
|
539
|
+
.all();
|
|
540
|
+
// Merge and deduplicate
|
|
541
|
+
const ipSet = new Set();
|
|
542
|
+
for (const row of iocIPs)
|
|
543
|
+
ipSet.add(row.value);
|
|
544
|
+
for (const row of threatIPs)
|
|
545
|
+
ipSet.add(row.value);
|
|
546
|
+
return Array.from(ipSet);
|
|
547
|
+
}
|
|
548
|
+
/** Get domain blocklist from IoC entries / 取得域名封鎖清單 */
|
|
549
|
+
getDomainBlocklist(minReputation) {
|
|
550
|
+
const iocDomains = this.db
|
|
551
|
+
.prepare(`
|
|
552
|
+
SELECT value FROM ioc_entries
|
|
553
|
+
WHERE type = 'domain' AND reputation >= ?
|
|
554
|
+
ORDER BY reputation DESC
|
|
555
|
+
`)
|
|
556
|
+
.all(minReputation);
|
|
557
|
+
return iocDomains.map((row) => row.value);
|
|
558
|
+
}
|
|
559
|
+
/** Upsert an IoC entry / 插入或更新 IoC 條目 */
|
|
560
|
+
upsertIoC(type, value, reputation, source) {
|
|
561
|
+
this.db
|
|
562
|
+
.prepare(`
|
|
563
|
+
INSERT INTO ioc_entries (type, value, reputation, source)
|
|
564
|
+
VALUES (?, ?, ?, ?)
|
|
565
|
+
ON CONFLICT(value) DO UPDATE SET
|
|
566
|
+
reputation = excluded.reputation,
|
|
567
|
+
source = excluded.source,
|
|
568
|
+
last_seen = datetime('now'),
|
|
569
|
+
sighting_count = sighting_count + 1
|
|
570
|
+
`)
|
|
571
|
+
.run(type, value, reputation, source);
|
|
572
|
+
}
|
|
573
|
+
/** Promote confirmed proposals with approved LLM review to rules / 推廣已確認提案為規則 */
|
|
574
|
+
promoteConfirmedProposals() {
|
|
575
|
+
const proposals = this.db
|
|
576
|
+
.prepare(`
|
|
577
|
+
SELECT pattern_hash, rule_content, llm_review_verdict
|
|
578
|
+
FROM atr_proposals
|
|
579
|
+
WHERE status = 'confirmed' AND llm_review_verdict IS NOT NULL
|
|
580
|
+
`)
|
|
581
|
+
.all();
|
|
582
|
+
let promoted = 0;
|
|
583
|
+
for (const proposal of proposals) {
|
|
584
|
+
try {
|
|
585
|
+
const verdict = JSON.parse(proposal.llm_review_verdict);
|
|
586
|
+
if (verdict.approved !== true)
|
|
587
|
+
continue;
|
|
588
|
+
this.upsertRule({
|
|
589
|
+
ruleId: proposal.pattern_hash,
|
|
590
|
+
ruleContent: proposal.rule_content,
|
|
591
|
+
publishedAt: new Date().toISOString(),
|
|
592
|
+
source: 'atr-community',
|
|
593
|
+
});
|
|
594
|
+
this.db
|
|
595
|
+
.prepare(`
|
|
596
|
+
UPDATE atr_proposals SET status = 'promoted', updated_at = datetime('now')
|
|
597
|
+
WHERE pattern_hash = ?
|
|
598
|
+
`)
|
|
599
|
+
.run(proposal.pattern_hash);
|
|
600
|
+
promoted++;
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// Skip proposals with unparseable verdicts
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return promoted;
|
|
607
|
+
}
|
|
608
|
+
/** Reject an ATR proposal / 拒絕 ATR 提案 */
|
|
609
|
+
rejectATRProposal(patternHash) {
|
|
610
|
+
this.db
|
|
611
|
+
.prepare(`
|
|
612
|
+
UPDATE atr_proposals SET status = 'rejected', updated_at = datetime('now')
|
|
613
|
+
WHERE pattern_hash = ?
|
|
614
|
+
`)
|
|
615
|
+
.run(patternHash);
|
|
616
|
+
}
|
|
617
|
+
/** Get rules by source type, optionally filtered by date / 依來源取得規則 */
|
|
618
|
+
getRulesBySource(source, since) {
|
|
619
|
+
if (since) {
|
|
620
|
+
return this.db
|
|
621
|
+
.prepare(`
|
|
622
|
+
SELECT rule_id as ruleId, rule_content as ruleContent, published_at as publishedAt, source,
|
|
623
|
+
category, severity, mitre_techniques as mitreTechniques, tags
|
|
624
|
+
FROM rules
|
|
625
|
+
WHERE source = ? AND published_at > ?
|
|
626
|
+
ORDER BY published_at ASC
|
|
627
|
+
`)
|
|
628
|
+
.all(source, since);
|
|
629
|
+
}
|
|
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
|
|
634
|
+
FROM rules
|
|
635
|
+
WHERE source = ?
|
|
636
|
+
ORDER BY published_at ASC
|
|
637
|
+
`)
|
|
638
|
+
.all(source);
|
|
639
|
+
}
|
|
640
|
+
/** Report a safe skill (increment confirmations, auto-confirm at 3+) / 回報安全 skill */
|
|
641
|
+
reportSafeSkill(skillName, fingerprintHash) {
|
|
642
|
+
const normalized = skillName.toLowerCase().trim().replace(/\s+/g, '-');
|
|
643
|
+
this.db
|
|
644
|
+
.prepare(`
|
|
645
|
+
INSERT INTO skill_whitelist (skill_name, normalized_name, fingerprint_hash)
|
|
646
|
+
VALUES (?, ?, ?)
|
|
647
|
+
ON CONFLICT(normalized_name) DO UPDATE SET
|
|
648
|
+
confirmations = confirmations + 1,
|
|
649
|
+
status = CASE WHEN confirmations + 1 >= 3 THEN 'confirmed' ELSE status END,
|
|
650
|
+
fingerprint_hash = COALESCE(excluded.fingerprint_hash, fingerprint_hash),
|
|
651
|
+
last_reported = datetime('now')
|
|
652
|
+
`)
|
|
653
|
+
.run(skillName, normalized, fingerprintHash ?? null);
|
|
654
|
+
}
|
|
655
|
+
/** Get confirmed community whitelist / 取得社群白名單 */
|
|
656
|
+
getSkillWhitelist() {
|
|
657
|
+
return this.db
|
|
658
|
+
.prepare(`
|
|
659
|
+
SELECT skill_name as name, fingerprint_hash as hash, confirmations
|
|
660
|
+
FROM skill_whitelist
|
|
661
|
+
WHERE status = 'confirmed'
|
|
662
|
+
ORDER BY confirmations DESC
|
|
663
|
+
`)
|
|
664
|
+
.all();
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get skill blacklist: skills reported by 3+ distinct clients with avg risk >= 70
|
|
668
|
+
* 取得技能黑名單:3+ 不同客戶端回報且平均風險 >= 70 的技能
|
|
669
|
+
*/
|
|
670
|
+
getSkillBlacklist(minReports = 3, minAvgRisk = 70) {
|
|
671
|
+
return this.db
|
|
672
|
+
.prepare(`
|
|
673
|
+
SELECT
|
|
674
|
+
skill_hash as skillHash,
|
|
675
|
+
skill_name as skillName,
|
|
676
|
+
ROUND(AVG(risk_score)) as avgRiskScore,
|
|
677
|
+
MAX(risk_level) as maxRiskLevel,
|
|
678
|
+
COUNT(DISTINCT COALESCE(client_id, 'anonymous')) as reportCount,
|
|
679
|
+
MIN(created_at) as firstReported,
|
|
680
|
+
MAX(created_at) as lastReported
|
|
681
|
+
FROM skill_threats
|
|
682
|
+
GROUP BY skill_hash
|
|
683
|
+
HAVING reportCount >= ? AND AVG(risk_score) >= ?
|
|
684
|
+
ORDER BY avgRiskScore DESC
|
|
685
|
+
`)
|
|
686
|
+
.all(minReports, minAvgRisk);
|
|
687
|
+
}
|
|
688
|
+
/** Backfill classification for rules with NULL category / 回填缺少分類的規則 */
|
|
689
|
+
backfillClassification() {
|
|
690
|
+
const unclassified = this.db
|
|
691
|
+
.prepare(`SELECT rule_id, rule_content, source FROM rules WHERE category IS NULL`)
|
|
692
|
+
.all();
|
|
693
|
+
let updated = 0;
|
|
694
|
+
const stmt = this.db.prepare(`
|
|
695
|
+
UPDATE rules SET category = ?, severity = ?, mitre_techniques = ?, tags = ?, updated_at = datetime('now')
|
|
696
|
+
WHERE rule_id = ?
|
|
697
|
+
`);
|
|
698
|
+
for (const row of unclassified) {
|
|
699
|
+
const meta = this.extractMetadata(row.rule_content, row.source);
|
|
700
|
+
stmt.run(meta.category, meta.severity, meta.mitreTechniques, meta.tags, row.rule_id);
|
|
701
|
+
updated++;
|
|
702
|
+
}
|
|
703
|
+
return updated;
|
|
704
|
+
}
|
|
439
705
|
/** Close the database / 關閉資料庫 */
|
|
440
706
|
close() {
|
|
441
707
|
this.db.close();
|