security-detections-mcp 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/db.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
- import { mkdirSync, existsSync } from 'fs';
4
+ import { mkdirSync, existsSync, unlinkSync } from 'fs';
5
5
  const CACHE_DIR = join(homedir(), '.cache', 'security-detections-mcp');
6
6
  const DB_PATH = join(CACHE_DIR, 'detections.sqlite');
7
7
  let db = null;
@@ -16,7 +16,7 @@ export function initDb() {
16
16
  mkdirSync(CACHE_DIR, { recursive: true });
17
17
  }
18
18
  db = new Database(DB_PATH);
19
- // Create main detections table
19
+ // Create main detections table with all enhanced fields
20
20
  db.exec(`
21
21
  CREATE TABLE IF NOT EXISTS detections (
22
22
  id TEXT PRIMARY KEY,
@@ -37,10 +37,24 @@ export function initDb() {
37
37
  falsepositives TEXT,
38
38
  tags TEXT,
39
39
  file_path TEXT,
40
- raw_yaml TEXT
40
+ raw_yaml TEXT,
41
+ cves TEXT,
42
+ analytic_stories TEXT,
43
+ data_sources TEXT,
44
+ detection_type TEXT,
45
+ asset_type TEXT,
46
+ security_domain TEXT,
47
+ process_names TEXT,
48
+ file_paths TEXT,
49
+ registry_paths TEXT,
50
+ mitre_tactics TEXT,
51
+ platforms TEXT,
52
+ kql_category TEXT,
53
+ kql_tags TEXT,
54
+ kql_keywords TEXT
41
55
  )
42
56
  `);
43
- // Create FTS5 virtual table for full-text search
57
+ // Create FTS5 virtual table for full-text search with all searchable fields
44
58
  db.exec(`
45
59
  CREATE VIRTUAL TABLE IF NOT EXISTS detections_fts USING fts5(
46
60
  id,
@@ -49,6 +63,17 @@ export function initDb() {
49
63
  query,
50
64
  mitre_ids,
51
65
  tags,
66
+ cves,
67
+ analytic_stories,
68
+ data_sources,
69
+ process_names,
70
+ file_paths,
71
+ registry_paths,
72
+ mitre_tactics,
73
+ platforms,
74
+ kql_category,
75
+ kql_tags,
76
+ kql_keywords,
52
77
  content='detections',
53
78
  content_rowid='rowid'
54
79
  )
@@ -56,22 +81,22 @@ export function initDb() {
56
81
  // Create triggers to keep FTS in sync
57
82
  db.exec(`
58
83
  CREATE TRIGGER IF NOT EXISTS detections_ai AFTER INSERT ON detections BEGIN
59
- INSERT INTO detections_fts(rowid, id, name, description, query, mitre_ids, tags)
60
- VALUES (NEW.rowid, NEW.id, NEW.name, NEW.description, NEW.query, NEW.mitre_ids, NEW.tags);
84
+ INSERT INTO detections_fts(rowid, id, name, description, query, mitre_ids, tags, cves, analytic_stories, data_sources, process_names, file_paths, registry_paths, mitre_tactics, platforms, kql_category, kql_tags, kql_keywords)
85
+ VALUES (NEW.rowid, NEW.id, NEW.name, NEW.description, NEW.query, NEW.mitre_ids, NEW.tags, NEW.cves, NEW.analytic_stories, NEW.data_sources, NEW.process_names, NEW.file_paths, NEW.registry_paths, NEW.mitre_tactics, NEW.platforms, NEW.kql_category, NEW.kql_tags, NEW.kql_keywords);
61
86
  END
62
87
  `);
63
88
  db.exec(`
64
89
  CREATE TRIGGER IF NOT EXISTS detections_ad AFTER DELETE ON detections BEGIN
65
- INSERT INTO detections_fts(detections_fts, rowid, id, name, description, query, mitre_ids, tags)
66
- VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.description, OLD.query, OLD.mitre_ids, OLD.tags);
90
+ INSERT INTO detections_fts(detections_fts, rowid, id, name, description, query, mitre_ids, tags, cves, analytic_stories, data_sources, process_names, file_paths, registry_paths, mitre_tactics, platforms, kql_category, kql_tags, kql_keywords)
91
+ VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.description, OLD.query, OLD.mitre_ids, OLD.tags, OLD.cves, OLD.analytic_stories, OLD.data_sources, OLD.process_names, OLD.file_paths, OLD.registry_paths, OLD.mitre_tactics, OLD.platforms, OLD.kql_category, OLD.kql_tags, OLD.kql_keywords);
67
92
  END
68
93
  `);
69
94
  db.exec(`
70
95
  CREATE TRIGGER IF NOT EXISTS detections_au AFTER UPDATE ON detections BEGIN
71
- INSERT INTO detections_fts(detections_fts, rowid, id, name, description, query, mitre_ids, tags)
72
- VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.description, OLD.query, OLD.mitre_ids, OLD.tags);
73
- INSERT INTO detections_fts(rowid, id, name, description, query, mitre_ids, tags)
74
- VALUES (NEW.rowid, NEW.id, NEW.name, NEW.description, NEW.query, NEW.mitre_ids, NEW.tags);
96
+ INSERT INTO detections_fts(detections_fts, rowid, id, name, description, query, mitre_ids, tags, cves, analytic_stories, data_sources, process_names, file_paths, registry_paths, mitre_tactics, platforms, kql_category, kql_tags, kql_keywords)
97
+ VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.description, OLD.query, OLD.mitre_ids, OLD.tags, OLD.cves, OLD.analytic_stories, OLD.data_sources, OLD.process_names, OLD.file_paths, OLD.registry_paths, OLD.mitre_tactics, OLD.platforms, OLD.kql_category, OLD.kql_tags, OLD.kql_keywords);
98
+ INSERT INTO detections_fts(rowid, id, name, description, query, mitre_ids, tags, cves, analytic_stories, data_sources, process_names, file_paths, registry_paths, mitre_tactics, platforms, kql_category, kql_tags, kql_keywords)
99
+ VALUES (NEW.rowid, NEW.id, NEW.name, NEW.description, NEW.query, NEW.mitre_ids, NEW.tags, NEW.cves, NEW.analytic_stories, NEW.data_sources, NEW.process_names, NEW.file_paths, NEW.registry_paths, NEW.mitre_tactics, NEW.platforms, NEW.kql_category, NEW.kql_tags, NEW.kql_keywords);
75
100
  END
76
101
  `);
77
102
  // Create indexes for common queries
@@ -79,22 +104,82 @@ export function initDb() {
79
104
  db.exec(`CREATE INDEX IF NOT EXISTS idx_severity ON detections(severity)`);
80
105
  db.exec(`CREATE INDEX IF NOT EXISTS idx_logsource_product ON detections(logsource_product)`);
81
106
  db.exec(`CREATE INDEX IF NOT EXISTS idx_logsource_category ON detections(logsource_category)`);
107
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_detection_type ON detections(detection_type)`);
108
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_asset_type ON detections(asset_type)`);
109
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_security_domain ON detections(security_domain)`);
110
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_kql_category ON detections(kql_category)`);
111
+ // Create stories table (optional - provides rich context for analytic stories)
112
+ db.exec(`
113
+ CREATE TABLE IF NOT EXISTS stories (
114
+ id TEXT PRIMARY KEY,
115
+ name TEXT NOT NULL,
116
+ description TEXT,
117
+ narrative TEXT,
118
+ author TEXT,
119
+ date TEXT,
120
+ version INTEGER,
121
+ status TEXT,
122
+ refs TEXT,
123
+ category TEXT,
124
+ usecase TEXT,
125
+ detection_names TEXT
126
+ )
127
+ `);
128
+ // Create FTS5 for stories (narrative is key for semantic search!)
129
+ db.exec(`
130
+ CREATE VIRTUAL TABLE IF NOT EXISTS stories_fts USING fts5(
131
+ id,
132
+ name,
133
+ description,
134
+ narrative,
135
+ category,
136
+ usecase,
137
+ content='stories',
138
+ content_rowid='rowid'
139
+ )
140
+ `);
141
+ // Triggers for stories FTS
142
+ db.exec(`
143
+ CREATE TRIGGER IF NOT EXISTS stories_ai AFTER INSERT ON stories BEGIN
144
+ INSERT INTO stories_fts(rowid, id, name, description, narrative, category, usecase)
145
+ VALUES (NEW.rowid, NEW.id, NEW.name, NEW.description, NEW.narrative, NEW.category, NEW.usecase);
146
+ END
147
+ `);
148
+ db.exec(`
149
+ CREATE TRIGGER IF NOT EXISTS stories_ad AFTER DELETE ON stories BEGIN
150
+ INSERT INTO stories_fts(stories_fts, rowid, id, name, description, narrative, category, usecase)
151
+ VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.description, OLD.narrative, OLD.category, OLD.usecase);
152
+ END
153
+ `);
154
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_story_category ON stories(category)`);
82
155
  return db;
83
156
  }
84
157
  export function clearDb() {
85
158
  const database = initDb();
86
159
  database.exec('DELETE FROM detections');
87
160
  }
161
+ // Force recreation of the database (needed when schema changes)
162
+ export function recreateDb() {
163
+ if (db) {
164
+ db.close();
165
+ db = null;
166
+ }
167
+ if (existsSync(DB_PATH)) {
168
+ unlinkSync(DB_PATH);
169
+ }
170
+ }
88
171
  export function insertDetection(detection) {
89
172
  const database = initDb();
90
173
  const stmt = database.prepare(`
91
174
  INSERT OR REPLACE INTO detections
92
175
  (id, name, description, query, source_type, mitre_ids, logsource_category,
93
176
  logsource_product, logsource_service, severity, status, author,
94
- date_created, date_modified, refs, falsepositives, tags, file_path, raw_yaml)
95
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
177
+ date_created, date_modified, refs, falsepositives, tags, file_path, raw_yaml,
178
+ cves, analytic_stories, data_sources, detection_type, asset_type, security_domain,
179
+ process_names, file_paths, registry_paths, mitre_tactics, platforms, kql_category, kql_tags, kql_keywords)
180
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
96
181
  `);
97
- stmt.run(detection.id, detection.name, detection.description, detection.query, detection.source_type, JSON.stringify(detection.mitre_ids), detection.logsource_category, detection.logsource_product, detection.logsource_service, detection.severity, detection.status, detection.author, detection.date_created, detection.date_modified, JSON.stringify(detection.references), JSON.stringify(detection.falsepositives), JSON.stringify(detection.tags), detection.file_path, detection.raw_yaml);
182
+ stmt.run(detection.id, detection.name, detection.description, detection.query, detection.source_type, JSON.stringify(detection.mitre_ids), detection.logsource_category, detection.logsource_product, detection.logsource_service, detection.severity, detection.status, detection.author, detection.date_created, detection.date_modified, JSON.stringify(detection.references), JSON.stringify(detection.falsepositives), JSON.stringify(detection.tags), detection.file_path, detection.raw_yaml, JSON.stringify(detection.cves), JSON.stringify(detection.analytic_stories), JSON.stringify(detection.data_sources), detection.detection_type, detection.asset_type, detection.security_domain, JSON.stringify(detection.process_names), JSON.stringify(detection.file_paths), JSON.stringify(detection.registry_paths), JSON.stringify(detection.mitre_tactics), JSON.stringify(detection.platforms), detection.kql_category, JSON.stringify(detection.kql_tags), JSON.stringify(detection.kql_keywords));
98
183
  }
99
184
  function rowToDetection(row) {
100
185
  return {
@@ -117,6 +202,20 @@ function rowToDetection(row) {
117
202
  tags: JSON.parse(row.tags || '[]'),
118
203
  file_path: row.file_path,
119
204
  raw_yaml: row.raw_yaml,
205
+ cves: JSON.parse(row.cves || '[]'),
206
+ analytic_stories: JSON.parse(row.analytic_stories || '[]'),
207
+ data_sources: JSON.parse(row.data_sources || '[]'),
208
+ detection_type: row.detection_type,
209
+ asset_type: row.asset_type,
210
+ security_domain: row.security_domain,
211
+ process_names: JSON.parse(row.process_names || '[]'),
212
+ file_paths: JSON.parse(row.file_paths || '[]'),
213
+ registry_paths: JSON.parse(row.registry_paths || '[]'),
214
+ mitre_tactics: JSON.parse(row.mitre_tactics || '[]'),
215
+ platforms: JSON.parse(row.platforms || '[]'),
216
+ kql_category: row.kql_category,
217
+ kql_tags: JSON.parse(row.kql_tags || '[]'),
218
+ kql_keywords: JSON.parse(row.kql_keywords || '[]'),
120
219
  };
121
220
  }
122
221
  export function searchDetections(query, limit = 50) {
@@ -190,11 +289,108 @@ export function listBySeverity(level, limit = 100, offset = 0) {
190
289
  const rows = stmt.all(level, limit, offset);
191
290
  return rows.map(rowToDetection);
192
291
  }
292
+ // New query methods for enhanced fields
293
+ export function listByCve(cveId, limit = 100, offset = 0) {
294
+ const database = initDb();
295
+ const stmt = database.prepare(`
296
+ SELECT * FROM detections
297
+ WHERE cves LIKE ?
298
+ ORDER BY name
299
+ LIMIT ? OFFSET ?
300
+ `);
301
+ const rows = stmt.all(`%"${cveId}"%`, limit, offset);
302
+ return rows.map(rowToDetection);
303
+ }
304
+ export function listByAnalyticStory(story, limit = 100, offset = 0) {
305
+ const database = initDb();
306
+ const stmt = database.prepare(`
307
+ SELECT * FROM detections
308
+ WHERE analytic_stories LIKE ?
309
+ ORDER BY name
310
+ LIMIT ? OFFSET ?
311
+ `);
312
+ const rows = stmt.all(`%${story}%`, limit, offset);
313
+ return rows.map(rowToDetection);
314
+ }
315
+ export function listByProcessName(processName, limit = 100, offset = 0) {
316
+ const database = initDb();
317
+ const stmt = database.prepare(`
318
+ SELECT * FROM detections
319
+ WHERE process_names LIKE ?
320
+ ORDER BY name
321
+ LIMIT ? OFFSET ?
322
+ `);
323
+ const rows = stmt.all(`%${processName}%`, limit, offset);
324
+ return rows.map(rowToDetection);
325
+ }
326
+ export function listByDetectionType(detectionType, limit = 100, offset = 0) {
327
+ const database = initDb();
328
+ const stmt = database.prepare('SELECT * FROM detections WHERE detection_type = ? ORDER BY name LIMIT ? OFFSET ?');
329
+ const rows = stmt.all(detectionType, limit, offset);
330
+ return rows.map(rowToDetection);
331
+ }
332
+ export function listByDataSource(dataSource, limit = 100, offset = 0) {
333
+ const database = initDb();
334
+ const stmt = database.prepare(`
335
+ SELECT * FROM detections
336
+ WHERE data_sources LIKE ?
337
+ ORDER BY name
338
+ LIMIT ? OFFSET ?
339
+ `);
340
+ const rows = stmt.all(`%${dataSource}%`, limit, offset);
341
+ return rows.map(rowToDetection);
342
+ }
343
+ export function listByKqlCategory(category, limit = 100, offset = 0) {
344
+ const database = initDb();
345
+ const stmt = database.prepare(`
346
+ SELECT * FROM detections
347
+ WHERE source_type = 'kql' AND kql_category = ?
348
+ ORDER BY name
349
+ LIMIT ? OFFSET ?
350
+ `);
351
+ const rows = stmt.all(category, limit, offset);
352
+ return rows.map(rowToDetection);
353
+ }
354
+ export function listByKqlTag(tag, limit = 100, offset = 0) {
355
+ const database = initDb();
356
+ const stmt = database.prepare(`
357
+ SELECT * FROM detections
358
+ WHERE source_type = 'kql' AND kql_tags LIKE ?
359
+ ORDER BY name
360
+ LIMIT ? OFFSET ?
361
+ `);
362
+ const rows = stmt.all(`%"${tag}"%`, limit, offset);
363
+ return rows.map(rowToDetection);
364
+ }
365
+ export function listByKqlDatasource(dataSource, limit = 100, offset = 0) {
366
+ const database = initDb();
367
+ const stmt = database.prepare(`
368
+ SELECT * FROM detections
369
+ WHERE source_type = 'kql' AND data_sources LIKE ?
370
+ ORDER BY name
371
+ LIMIT ? OFFSET ?
372
+ `);
373
+ const rows = stmt.all(`%${dataSource}%`, limit, offset);
374
+ return rows.map(rowToDetection);
375
+ }
376
+ export function listByMitreTactic(tactic, limit = 100, offset = 0) {
377
+ const database = initDb();
378
+ const stmt = database.prepare(`
379
+ SELECT * FROM detections
380
+ WHERE mitre_tactics LIKE ?
381
+ ORDER BY name
382
+ LIMIT ? OFFSET ?
383
+ `);
384
+ const rows = stmt.all(`%"${tactic}"%`, limit, offset);
385
+ return rows.map(rowToDetection);
386
+ }
193
387
  export function getStats() {
194
388
  const database = initDb();
195
389
  const total = database.prepare('SELECT COUNT(*) as count FROM detections').get().count;
196
390
  const sigma = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'sigma'").get().count;
197
391
  const splunk = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'splunk_escu'").get().count;
392
+ const elastic = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'elastic'").get().count;
393
+ const kql = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'kql'").get().count;
198
394
  // Count by severity
199
395
  const severityRows = database.prepare(`
200
396
  SELECT severity, COUNT(*) as count FROM detections
@@ -222,13 +418,65 @@ export function getStats() {
222
418
  SELECT COUNT(*) as count FROM detections
223
419
  WHERE mitre_ids != '[]' AND mitre_ids IS NOT NULL
224
420
  `).get().count;
421
+ // Count detections with CVE mappings
422
+ const cve_coverage = database.prepare(`
423
+ SELECT COUNT(*) as count FROM detections
424
+ WHERE cves != '[]' AND cves IS NOT NULL
425
+ `).get().count;
426
+ // Count by MITRE tactic
427
+ const tacticRows = database.prepare(`
428
+ SELECT mitre_tactics FROM detections
429
+ WHERE mitre_tactics != '[]' AND mitre_tactics IS NOT NULL
430
+ `).all();
431
+ const by_mitre_tactic = {};
432
+ for (const row of tacticRows) {
433
+ const tactics = JSON.parse(row.mitre_tactics);
434
+ for (const tactic of tactics) {
435
+ by_mitre_tactic[tactic] = (by_mitre_tactic[tactic] || 0) + 1;
436
+ }
437
+ }
438
+ // Count by detection type
439
+ const typeRows = database.prepare(`
440
+ SELECT detection_type, COUNT(*) as count FROM detections
441
+ WHERE detection_type IS NOT NULL
442
+ GROUP BY detection_type
443
+ `).all();
444
+ const by_detection_type = {};
445
+ for (const row of typeRows) {
446
+ by_detection_type[row.detection_type] = row.count;
447
+ }
448
+ // Count stories (optional table)
449
+ let stories_count = 0;
450
+ const by_story_category = {};
451
+ try {
452
+ stories_count = database.prepare('SELECT COUNT(*) as count FROM stories').get().count;
453
+ const categoryRows = database.prepare(`
454
+ SELECT category, COUNT(*) as count FROM stories
455
+ WHERE category IS NOT NULL
456
+ GROUP BY category
457
+ `).all();
458
+ for (const row of categoryRows) {
459
+ by_story_category[row.category] = row.count;
460
+ }
461
+ }
462
+ catch {
463
+ // Stories table might not exist or be empty - that's fine
464
+ }
225
465
  return {
226
466
  total,
227
467
  sigma,
228
468
  splunk_escu: splunk,
469
+ elastic,
470
+ kql,
229
471
  by_severity,
230
472
  by_logsource_product,
231
473
  mitre_coverage,
474
+ cve_coverage,
475
+ by_mitre_tactic,
476
+ by_detection_type,
477
+ stories_count,
478
+ by_story_category,
479
+ by_elastic_index: {}, // Could be populated if needed
232
480
  };
233
481
  }
234
482
  export function getRawYaml(id) {
@@ -246,3 +494,384 @@ export function getDetectionCount() {
246
494
  const database = initDb();
247
495
  return database.prepare('SELECT COUNT(*) as count FROM detections').get().count;
248
496
  }
497
+ // Story-related functions
498
+ export function insertStory(story) {
499
+ const database = initDb();
500
+ const stmt = database.prepare(`
501
+ INSERT OR REPLACE INTO stories
502
+ (id, name, description, narrative, author, date, version, status, refs, category, usecase, detection_names)
503
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
504
+ `);
505
+ stmt.run(story.id, story.name, story.description, story.narrative, story.author, story.date, story.version, story.status, JSON.stringify(story.references), story.category, story.usecase, JSON.stringify(story.detection_names));
506
+ }
507
+ function rowToStory(row) {
508
+ return {
509
+ id: row.id,
510
+ name: row.name,
511
+ description: row.description || '',
512
+ narrative: row.narrative || '',
513
+ author: row.author,
514
+ date: row.date,
515
+ version: row.version,
516
+ status: row.status,
517
+ references: JSON.parse(row.refs || '[]'),
518
+ category: row.category,
519
+ usecase: row.usecase,
520
+ detection_names: JSON.parse(row.detection_names || '[]'),
521
+ };
522
+ }
523
+ export function getStoryByName(name) {
524
+ const database = initDb();
525
+ const stmt = database.prepare('SELECT * FROM stories WHERE name = ?');
526
+ const row = stmt.get(name);
527
+ return row ? rowToStory(row) : null;
528
+ }
529
+ export function getStoryById(id) {
530
+ const database = initDb();
531
+ const stmt = database.prepare('SELECT * FROM stories WHERE id = ?');
532
+ const row = stmt.get(id);
533
+ return row ? rowToStory(row) : null;
534
+ }
535
+ export function searchStories(query, limit = 20) {
536
+ const database = initDb();
537
+ try {
538
+ const stmt = database.prepare(`
539
+ SELECT s.* FROM stories s
540
+ JOIN stories_fts fts ON s.rowid = fts.rowid
541
+ WHERE stories_fts MATCH ?
542
+ ORDER BY rank
543
+ LIMIT ?
544
+ `);
545
+ const rows = stmt.all(query, limit);
546
+ return rows.map(rowToStory);
547
+ }
548
+ catch {
549
+ // If no stories indexed, return empty
550
+ return [];
551
+ }
552
+ }
553
+ export function listStories(limit = 100, offset = 0) {
554
+ const database = initDb();
555
+ try {
556
+ const stmt = database.prepare('SELECT * FROM stories ORDER BY name LIMIT ? OFFSET ?');
557
+ const rows = stmt.all(limit, offset);
558
+ return rows.map(rowToStory);
559
+ }
560
+ catch {
561
+ return [];
562
+ }
563
+ }
564
+ export function listStoriesByCategory(category, limit = 100, offset = 0) {
565
+ const database = initDb();
566
+ try {
567
+ const stmt = database.prepare('SELECT * FROM stories WHERE category = ? ORDER BY name LIMIT ? OFFSET ?');
568
+ const rows = stmt.all(category, limit, offset);
569
+ return rows.map(rowToStory);
570
+ }
571
+ catch {
572
+ return [];
573
+ }
574
+ }
575
+ export function getStoryCount() {
576
+ const database = initDb();
577
+ try {
578
+ return database.prepare('SELECT COUNT(*) as count FROM stories').get().count;
579
+ }
580
+ catch {
581
+ return 0;
582
+ }
583
+ }
584
+ export function getTechniqueIds(filters = {}) {
585
+ const database = initDb();
586
+ let sql = "SELECT DISTINCT mitre_ids FROM detections WHERE mitre_ids != '[]' AND mitre_ids IS NOT NULL";
587
+ const params = [];
588
+ if (filters.source_type) {
589
+ sql += ' AND source_type = ?';
590
+ params.push(filters.source_type);
591
+ }
592
+ if (filters.tactic) {
593
+ sql += ' AND mitre_tactics LIKE ?';
594
+ params.push(`%"${filters.tactic}"%`);
595
+ }
596
+ if (filters.severity) {
597
+ sql += ' AND severity = ?';
598
+ params.push(filters.severity);
599
+ }
600
+ const stmt = database.prepare(sql);
601
+ const rows = stmt.all(...params);
602
+ // Extract and dedupe all technique IDs
603
+ const techniqueSet = new Set();
604
+ for (const row of rows) {
605
+ const ids = JSON.parse(row.mitre_ids);
606
+ for (const id of ids) {
607
+ techniqueSet.add(id);
608
+ }
609
+ }
610
+ return Array.from(techniqueSet).sort();
611
+ }
612
+ // Threat profiles for gap analysis
613
+ const THREAT_PROFILES = {
614
+ ransomware: [
615
+ 'T1486', 'T1490', 'T1027', 'T1547.001', 'T1059.001', 'T1059.003',
616
+ 'T1562.001', 'T1112', 'T1070.004', 'T1048', 'T1567', 'T1078',
617
+ 'T1566.001', 'T1204.002', 'T1055', 'T1543.003'
618
+ ],
619
+ apt: [
620
+ 'T1003.001', 'T1003.002', 'T1003.003', 'T1021.001', 'T1021.002',
621
+ 'T1053.005', 'T1071.001', 'T1071.004', 'T1105', 'T1027', 'T1055',
622
+ 'T1078', 'T1136', 'T1098', 'T1087', 'T1069', 'T1018', 'T1082'
623
+ ],
624
+ 'initial-access': [
625
+ 'T1566.001', 'T1566.002', 'T1190', 'T1078', 'T1133', 'T1200',
626
+ 'T1091', 'T1195.002', 'T1199', 'T1189'
627
+ ],
628
+ persistence: [
629
+ 'T1547.001', 'T1547.004', 'T1543.003', 'T1053.005', 'T1136.001',
630
+ 'T1098', 'T1505.003', 'T1546.001', 'T1574.001', 'T1574.002'
631
+ ],
632
+ 'credential-access': [
633
+ 'T1003.001', 'T1003.002', 'T1003.003', 'T1003.004', 'T1003.006',
634
+ 'T1555', 'T1552.001', 'T1110', 'T1558.003', 'T1539', 'T1606.001'
635
+ ],
636
+ 'defense-evasion': [
637
+ 'T1027', 'T1070.001', 'T1070.004', 'T1055', 'T1036', 'T1562.001',
638
+ 'T1218', 'T1112', 'T1140', 'T1202', 'T1564.001'
639
+ ]
640
+ };
641
+ // Analyze coverage efficiently
642
+ export function analyzeCoverage(sourceType) {
643
+ const database = initDb();
644
+ // Get all technique IDs covered
645
+ let countSql = 'SELECT COUNT(DISTINCT id) as count FROM detections';
646
+ if (sourceType)
647
+ countSql += ' WHERE source_type = ?';
648
+ const totalDetections = sourceType
649
+ ? database.prepare(countSql).get(sourceType).count
650
+ : database.prepare(countSql).get().count;
651
+ // Get techniques with counts
652
+ let sql = "SELECT mitre_ids, mitre_tactics FROM detections WHERE mitre_ids != '[]'";
653
+ if (sourceType)
654
+ sql += ' AND source_type = ?';
655
+ const rows = sourceType
656
+ ? database.prepare(sql).all(sourceType)
657
+ : database.prepare(sql).all();
658
+ // Count techniques and tactics
659
+ const techCounts = {};
660
+ const tacticCounts = {};
661
+ const allTactics = [
662
+ 'reconnaissance', 'resource-development', 'initial-access', 'execution',
663
+ 'persistence', 'privilege-escalation', 'defense-evasion', 'credential-access',
664
+ 'discovery', 'lateral-movement', 'collection', 'command-and-control',
665
+ 'exfiltration', 'impact'
666
+ ];
667
+ for (const t of allTactics) {
668
+ tacticCounts[t] = new Set();
669
+ }
670
+ for (const row of rows) {
671
+ const ids = JSON.parse(row.mitre_ids);
672
+ const tactics = JSON.parse(row.mitre_tactics || '[]');
673
+ for (const id of ids) {
674
+ techCounts[id] = (techCounts[id] || 0) + 1;
675
+ for (const tactic of tactics) {
676
+ if (tacticCounts[tactic]) {
677
+ tacticCounts[tactic].add(id);
678
+ }
679
+ }
680
+ }
681
+ }
682
+ // Build coverage by tactic (approx totals from ATT&CK)
683
+ const tacticTotals = {
684
+ 'reconnaissance': 10, 'resource-development': 8, 'initial-access': 10,
685
+ 'execution': 14, 'persistence': 20, 'privilege-escalation': 14,
686
+ 'defense-evasion': 43, 'credential-access': 17, 'discovery': 31,
687
+ 'lateral-movement': 9, 'collection': 17, 'command-and-control': 18,
688
+ 'exfiltration': 9, 'impact': 14
689
+ };
690
+ const coverageByTactic = {};
691
+ for (const tactic of allTactics) {
692
+ const covered = tacticCounts[tactic].size;
693
+ const total = tacticTotals[tactic];
694
+ coverageByTactic[tactic] = {
695
+ covered,
696
+ total,
697
+ percent: Math.round((covered / total) * 100)
698
+ };
699
+ }
700
+ // Top covered and weak coverage
701
+ const sorted = Object.entries(techCounts).sort((a, b) => b[1] - a[1]);
702
+ const topCovered = sorted.slice(0, 10).map(([t, c]) => ({ technique: t, detection_count: c }));
703
+ const weakCoverage = sorted.filter(([_, c]) => c === 1).slice(0, 10).map(([t, c]) => ({ technique: t, detection_count: c }));
704
+ return {
705
+ summary: {
706
+ total_techniques: Object.keys(techCounts).length,
707
+ total_detections: totalDetections,
708
+ coverage_by_tactic: coverageByTactic,
709
+ },
710
+ top_covered: topCovered,
711
+ weak_coverage: weakCoverage,
712
+ };
713
+ }
714
+ // Identify gaps based on threat profile
715
+ export function identifyGaps(threatProfile, sourceType) {
716
+ const targetTechniques = THREAT_PROFILES[threatProfile.toLowerCase()] || THREAT_PROFILES['apt'];
717
+ // Get what we have coverage for
718
+ const coveredTechs = new Set(getTechniqueIds({ source_type: sourceType }));
719
+ // Find gaps
720
+ const gaps = [];
721
+ const covered = [];
722
+ for (const tech of targetTechniques) {
723
+ if (coveredTechs.has(tech)) {
724
+ covered.push(tech);
725
+ }
726
+ else {
727
+ // Check if we have sub-technique coverage
728
+ const hasSubCoverage = Array.from(coveredTechs).some(t => t.startsWith(tech + '.'));
729
+ const hasParentCoverage = coveredTechs.has(tech.split('.')[0]);
730
+ let priority = 'P0';
731
+ let reason = 'No detection coverage';
732
+ if (hasSubCoverage) {
733
+ priority = 'P2';
734
+ reason = 'Has sub-technique coverage but not parent';
735
+ }
736
+ else if (hasParentCoverage) {
737
+ priority = 'P1';
738
+ reason = 'Has parent technique coverage, may catch this';
739
+ }
740
+ gaps.push({ technique: tech, priority, reason });
741
+ }
742
+ }
743
+ // Sort by priority
744
+ gaps.sort((a, b) => a.priority.localeCompare(b.priority));
745
+ // Generate recommendations
746
+ const recommendations = [
747
+ `${covered.length}/${targetTechniques.length} techniques covered for ${threatProfile}`,
748
+ `${gaps.filter(g => g.priority === 'P0').length} critical gaps (P0) need immediate attention`,
749
+ ];
750
+ if (gaps.length > 0) {
751
+ recommendations.push(`Top priority: ${gaps[0].technique} - ${gaps[0].reason}`);
752
+ }
753
+ return {
754
+ threat_profile: threatProfile,
755
+ total_gaps: gaps.length,
756
+ critical_gaps: gaps.slice(0, 15),
757
+ covered,
758
+ recommendations,
759
+ };
760
+ }
761
+ // Suggest detections for a technique
762
+ export function suggestDetections(techniqueId, sourceType) {
763
+ const database = initDb();
764
+ // Find existing detections for this technique
765
+ let sql = "SELECT id, name, source_type, data_sources FROM detections WHERE mitre_ids LIKE ?";
766
+ const params = [`%"${techniqueId}"%`];
767
+ if (sourceType) {
768
+ sql += ' AND source_type = ?';
769
+ params.push(sourceType);
770
+ }
771
+ sql += ' LIMIT 10';
772
+ const rows = database.prepare(sql).all(...params);
773
+ const existingDetections = rows.map(r => ({
774
+ id: r.id,
775
+ name: r.name,
776
+ source: r.source_type
777
+ }));
778
+ // Collect data sources from existing detections
779
+ const dataSources = new Set();
780
+ for (const row of rows) {
781
+ const ds = JSON.parse(row.data_sources || '[]');
782
+ ds.forEach(d => dataSources.add(d));
783
+ }
784
+ // Generate detection ideas based on technique pattern
785
+ const ideas = [];
786
+ const techBase = techniqueId.split('.')[0];
787
+ const ideaMap = {
788
+ 'T1059': ['Monitor process creation for script interpreters', 'Track command-line arguments for encoded commands', 'Alert on unusual parent-child process relationships'],
789
+ 'T1003': ['Monitor LSASS access patterns', 'Track credential dumping tool signatures', 'Alert on suspicious memory access'],
790
+ 'T1547': ['Monitor registry run key modifications', 'Track startup folder changes', 'Alert on new autostart entries'],
791
+ 'T1055': ['Monitor for CreateRemoteThread', 'Track process injection patterns', 'Alert on memory allocation in remote processes'],
792
+ 'T1027': ['Detect encoded/obfuscated scripts', 'Monitor for packed executables', 'Track file entropy anomalies'],
793
+ 'T1071': ['Monitor for beaconing patterns', 'Track DNS query anomalies', 'Alert on unusual HTTP/S traffic'],
794
+ 'T1486': ['Monitor for mass file modifications', 'Track encryption-related API calls', 'Alert on ransom note creation'],
795
+ 'T1490': ['Monitor vssadmin/wbadmin usage', 'Track backup deletion attempts', 'Alert on bcdedit modifications'],
796
+ };
797
+ ideas.push(...(ideaMap[techBase] || ['Review MITRE ATT&CK for detection guidance', 'Check data source requirements', 'Consider behavioral vs signature detection']));
798
+ return {
799
+ technique_id: techniqueId,
800
+ existing_detections: existingDetections,
801
+ data_sources_needed: Array.from(dataSources).slice(0, 10),
802
+ detection_ideas: ideas,
803
+ };
804
+ }
805
+ export function generateNavigatorLayer(options) {
806
+ const techniqueIds = getTechniqueIds({
807
+ source_type: options.source_type,
808
+ tactic: options.tactic,
809
+ severity: options.severity,
810
+ });
811
+ // Build techniques array with scores based on detection count
812
+ const database = initDb();
813
+ const techniques = [];
814
+ // Color gradient: red (no coverage) -> yellow (low) -> green (good)
815
+ function getColorForScore(score) {
816
+ if (score >= 80)
817
+ return '#1a8c1a'; // Dark green - excellent
818
+ if (score >= 60)
819
+ return '#8ec843'; // Light green - good
820
+ if (score >= 40)
821
+ return '#ffe766'; // Yellow - moderate
822
+ if (score >= 20)
823
+ return '#ff9933'; // Orange - low
824
+ return '#ff6666'; // Red - minimal
825
+ }
826
+ for (const techId of techniqueIds) {
827
+ let countSql = 'SELECT COUNT(*) as count FROM detections WHERE mitre_ids LIKE ?';
828
+ const countParams = [`%"${techId}"%`];
829
+ if (options.source_type) {
830
+ countSql += ' AND source_type = ?';
831
+ countParams.push(options.source_type);
832
+ }
833
+ if (options.tactic) {
834
+ countSql += ' AND mitre_tactics LIKE ?';
835
+ countParams.push(`%"${options.tactic}"%`);
836
+ }
837
+ const count = database.prepare(countSql).get(...countParams).count;
838
+ const score = Math.min(count * 20, 100); // Scale: 1 detection = 20, max 100
839
+ techniques.push({
840
+ techniqueID: techId,
841
+ score,
842
+ comment: `${count} detection(s)`,
843
+ color: getColorForScore(score),
844
+ enabled: true,
845
+ showSubtechniques: false,
846
+ });
847
+ }
848
+ // ATT&CK Navigator layer format
849
+ return {
850
+ name: options.name,
851
+ versions: {
852
+ attack: '18',
853
+ navigator: '5.1.0',
854
+ layer: '4.5',
855
+ },
856
+ domain: 'enterprise-attack',
857
+ description: options.description || `Generated from ${techniqueIds.length} techniques`,
858
+ filters: { platforms: ['Windows', 'Linux', 'macOS'] },
859
+ sorting: 0,
860
+ layout: { layout: 'side', aggregateFunction: 'average', showID: true, showName: true },
861
+ hideDisabled: false,
862
+ techniques,
863
+ gradient: {
864
+ colors: ['#ff6666', '#ffe766', '#8ec843'],
865
+ minValue: 0,
866
+ maxValue: 100,
867
+ },
868
+ legendItems: [],
869
+ metadata: [],
870
+ links: [],
871
+ showTacticRowBackground: false,
872
+ tacticRowBackground: '#dddddd',
873
+ selectTechniquesAcrossTactics: true,
874
+ selectSubtechniquesWithParent: false,
875
+ selectVisibleTechniques: false,
876
+ };
877
+ }