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/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
+ }