security-detections-mcp 1.1.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/README.md +177 -20
- package/dist/db.d.ts +61 -1
- package/dist/db.js +354 -12
- package/dist/index.js +318 -9
- package/dist/indexer.d.ts +3 -1
- package/dist/indexer.js +57 -2
- package/dist/parsers/elastic.js +4 -0
- package/dist/parsers/kql.d.ts +2 -0
- package/dist/parsers/kql.js +348 -0
- package/dist/parsers/sigma.js +4 -0
- package/dist/parsers/splunk.js +4 -0
- package/dist/types.d.ts +6 -1
- package/package.json +7 -2
package/dist/db.js
CHANGED
|
@@ -47,7 +47,11 @@ export function initDb() {
|
|
|
47
47
|
process_names TEXT,
|
|
48
48
|
file_paths TEXT,
|
|
49
49
|
registry_paths TEXT,
|
|
50
|
-
mitre_tactics TEXT
|
|
50
|
+
mitre_tactics TEXT,
|
|
51
|
+
platforms TEXT,
|
|
52
|
+
kql_category TEXT,
|
|
53
|
+
kql_tags TEXT,
|
|
54
|
+
kql_keywords TEXT
|
|
51
55
|
)
|
|
52
56
|
`);
|
|
53
57
|
// Create FTS5 virtual table for full-text search with all searchable fields
|
|
@@ -66,6 +70,10 @@ export function initDb() {
|
|
|
66
70
|
file_paths,
|
|
67
71
|
registry_paths,
|
|
68
72
|
mitre_tactics,
|
|
73
|
+
platforms,
|
|
74
|
+
kql_category,
|
|
75
|
+
kql_tags,
|
|
76
|
+
kql_keywords,
|
|
69
77
|
content='detections',
|
|
70
78
|
content_rowid='rowid'
|
|
71
79
|
)
|
|
@@ -73,22 +81,22 @@ export function initDb() {
|
|
|
73
81
|
// Create triggers to keep FTS in sync
|
|
74
82
|
db.exec(`
|
|
75
83
|
CREATE TRIGGER IF NOT EXISTS detections_ai AFTER INSERT ON detections BEGIN
|
|
76
|
-
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)
|
|
77
|
-
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);
|
|
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);
|
|
78
86
|
END
|
|
79
87
|
`);
|
|
80
88
|
db.exec(`
|
|
81
89
|
CREATE TRIGGER IF NOT EXISTS detections_ad AFTER DELETE ON detections BEGIN
|
|
82
|
-
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)
|
|
83
|
-
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);
|
|
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);
|
|
84
92
|
END
|
|
85
93
|
`);
|
|
86
94
|
db.exec(`
|
|
87
95
|
CREATE TRIGGER IF NOT EXISTS detections_au AFTER UPDATE ON detections BEGIN
|
|
88
|
-
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)
|
|
89
|
-
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);
|
|
90
|
-
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)
|
|
91
|
-
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);
|
|
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);
|
|
92
100
|
END
|
|
93
101
|
`);
|
|
94
102
|
// Create indexes for common queries
|
|
@@ -99,6 +107,7 @@ export function initDb() {
|
|
|
99
107
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_detection_type ON detections(detection_type)`);
|
|
100
108
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_asset_type ON detections(asset_type)`);
|
|
101
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)`);
|
|
102
111
|
// Create stories table (optional - provides rich context for analytic stories)
|
|
103
112
|
db.exec(`
|
|
104
113
|
CREATE TABLE IF NOT EXISTS stories (
|
|
@@ -167,10 +176,10 @@ export function insertDetection(detection) {
|
|
|
167
176
|
logsource_product, logsource_service, severity, status, author,
|
|
168
177
|
date_created, date_modified, refs, falsepositives, tags, file_path, raw_yaml,
|
|
169
178
|
cves, analytic_stories, data_sources, detection_type, asset_type, security_domain,
|
|
170
|
-
process_names, file_paths, registry_paths, mitre_tactics)
|
|
171
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
179
|
+
process_names, file_paths, registry_paths, mitre_tactics, platforms, kql_category, kql_tags, kql_keywords)
|
|
180
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
172
181
|
`);
|
|
173
|
-
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));
|
|
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));
|
|
174
183
|
}
|
|
175
184
|
function rowToDetection(row) {
|
|
176
185
|
return {
|
|
@@ -203,6 +212,10 @@ function rowToDetection(row) {
|
|
|
203
212
|
file_paths: JSON.parse(row.file_paths || '[]'),
|
|
204
213
|
registry_paths: JSON.parse(row.registry_paths || '[]'),
|
|
205
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 || '[]'),
|
|
206
219
|
};
|
|
207
220
|
}
|
|
208
221
|
export function searchDetections(query, limit = 50) {
|
|
@@ -327,6 +340,39 @@ export function listByDataSource(dataSource, limit = 100, offset = 0) {
|
|
|
327
340
|
const rows = stmt.all(`%${dataSource}%`, limit, offset);
|
|
328
341
|
return rows.map(rowToDetection);
|
|
329
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
|
+
}
|
|
330
376
|
export function listByMitreTactic(tactic, limit = 100, offset = 0) {
|
|
331
377
|
const database = initDb();
|
|
332
378
|
const stmt = database.prepare(`
|
|
@@ -344,6 +390,7 @@ export function getStats() {
|
|
|
344
390
|
const sigma = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'sigma'").get().count;
|
|
345
391
|
const splunk = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'splunk_escu'").get().count;
|
|
346
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;
|
|
347
394
|
// Count by severity
|
|
348
395
|
const severityRows = database.prepare(`
|
|
349
396
|
SELECT severity, COUNT(*) as count FROM detections
|
|
@@ -420,6 +467,7 @@ export function getStats() {
|
|
|
420
467
|
sigma,
|
|
421
468
|
splunk_escu: splunk,
|
|
422
469
|
elastic,
|
|
470
|
+
kql,
|
|
423
471
|
by_severity,
|
|
424
472
|
by_logsource_product,
|
|
425
473
|
mitre_coverage,
|
|
@@ -533,3 +581,297 @@ export function getStoryCount() {
|
|
|
533
581
|
return 0;
|
|
534
582
|
}
|
|
535
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
|
+
}
|