security-detections-mcp 2.1.1 → 3.1.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
@@ -51,7 +51,10 @@ export function initDb() {
51
51
  platforms TEXT,
52
52
  kql_category TEXT,
53
53
  kql_tags TEXT,
54
- kql_keywords TEXT
54
+ kql_keywords TEXT,
55
+ sublime_attack_types TEXT,
56
+ sublime_detection_methods TEXT,
57
+ sublime_tactics TEXT
55
58
  )
56
59
  `);
57
60
  // Create FTS5 virtual table for full-text search with all searchable fields
@@ -74,6 +77,9 @@ export function initDb() {
74
77
  kql_category,
75
78
  kql_tags,
76
79
  kql_keywords,
80
+ sublime_attack_types,
81
+ sublime_detection_methods,
82
+ sublime_tactics,
77
83
  content='detections',
78
84
  content_rowid='rowid'
79
85
  )
@@ -81,22 +87,22 @@ export function initDb() {
81
87
  // Create triggers to keep FTS in sync
82
88
  db.exec(`
83
89
  CREATE TRIGGER IF NOT EXISTS detections_ai AFTER INSERT ON detections BEGIN
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);
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, platforms, kql_category, kql_tags, kql_keywords, sublime_attack_types, sublime_detection_methods, sublime_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, NEW.platforms, NEW.kql_category, NEW.kql_tags, NEW.kql_keywords, NEW.sublime_attack_types, NEW.sublime_detection_methods, NEW.sublime_tactics);
86
92
  END
87
93
  `);
88
94
  db.exec(`
89
95
  CREATE TRIGGER IF NOT EXISTS detections_ad AFTER DELETE ON detections BEGIN
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);
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, sublime_attack_types, sublime_detection_methods, sublime_tactics)
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, OLD.sublime_attack_types, OLD.sublime_detection_methods, OLD.sublime_tactics);
92
98
  END
93
99
  `);
94
100
  db.exec(`
95
101
  CREATE TRIGGER IF NOT EXISTS detections_au AFTER UPDATE ON detections BEGIN
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);
102
+ 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, sublime_attack_types, sublime_detection_methods, sublime_tactics)
103
+ 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, OLD.sublime_attack_types, OLD.sublime_detection_methods, OLD.sublime_tactics);
104
+ 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, sublime_attack_types, sublime_detection_methods, sublime_tactics)
105
+ 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, NEW.sublime_attack_types, NEW.sublime_detection_methods, NEW.sublime_tactics);
100
106
  END
101
107
  `);
102
108
  // Create indexes for common queries
@@ -199,15 +205,16 @@ export function recreateDb() {
199
205
  export function insertDetection(detection) {
200
206
  const database = initDb();
201
207
  const stmt = database.prepare(`
202
- INSERT OR REPLACE INTO detections
203
- (id, name, description, query, source_type, mitre_ids, logsource_category,
204
- logsource_product, logsource_service, severity, status, author,
208
+ INSERT OR REPLACE INTO detections
209
+ (id, name, description, query, source_type, mitre_ids, logsource_category,
210
+ logsource_product, logsource_service, severity, status, author,
205
211
  date_created, date_modified, refs, falsepositives, tags, file_path, raw_yaml,
206
212
  cves, analytic_stories, data_sources, detection_type, asset_type, security_domain,
207
- process_names, file_paths, registry_paths, mitre_tactics, platforms, kql_category, kql_tags, kql_keywords)
208
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
213
+ process_names, file_paths, registry_paths, mitre_tactics, platforms, kql_category, kql_tags, kql_keywords,
214
+ sublime_attack_types, sublime_detection_methods, sublime_tactics)
215
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
209
216
  `);
210
- 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));
217
+ 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), JSON.stringify(detection.sublime_attack_types), JSON.stringify(detection.sublime_detection_methods), JSON.stringify(detection.sublime_tactics));
211
218
  }
212
219
  function rowToDetection(row) {
213
220
  return {
@@ -244,6 +251,9 @@ function rowToDetection(row) {
244
251
  kql_category: row.kql_category,
245
252
  kql_tags: JSON.parse(row.kql_tags || '[]'),
246
253
  kql_keywords: JSON.parse(row.kql_keywords || '[]'),
254
+ sublime_attack_types: JSON.parse(row.sublime_attack_types || '[]'),
255
+ sublime_detection_methods: JSON.parse(row.sublime_detection_methods || '[]'),
256
+ sublime_tactics: JSON.parse(row.sublime_tactics || '[]'),
247
257
  };
248
258
  }
249
259
  export function searchDetections(query, limit = 50) {
@@ -419,6 +429,7 @@ export function getStats() {
419
429
  const splunk = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'splunk_escu'").get().count;
420
430
  const elastic = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'elastic'").get().count;
421
431
  const kql = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'kql'").get().count;
432
+ const sublime = database.prepare("SELECT COUNT(*) as count FROM detections WHERE source_type = 'sublime'").get().count;
422
433
  // Count by severity
423
434
  const severityRows = database.prepare(`
424
435
  SELECT severity, COUNT(*) as count FROM detections
@@ -496,6 +507,7 @@ export function getStats() {
496
507
  splunk_escu: splunk,
497
508
  elastic,
498
509
  kql,
510
+ sublime,
499
511
  by_severity,
500
512
  by_logsource_product,
501
513
  mitre_coverage,
package/dist/index.js CHANGED
@@ -27,17 +27,19 @@ const SPLUNK_PATHS = parsePaths(process.env.SPLUNK_PATHS);
27
27
  const ELASTIC_PATHS = parsePaths(process.env.ELASTIC_PATHS);
28
28
  const STORY_PATHS = parsePaths(process.env.STORY_PATHS);
29
29
  const KQL_PATHS = parsePaths(process.env.KQL_PATHS);
30
+ const SUBLIME_PATHS = parsePaths(process.env.SUBLIME_PATHS);
31
+ const CQL_HUB_PATHS = parsePaths(process.env.CQL_HUB_PATHS);
30
32
  // Auto-index on startup if paths are configured and DB is empty
31
33
  function autoIndex() {
32
- if (SIGMA_PATHS.length === 0 && SPLUNK_PATHS.length === 0 && ELASTIC_PATHS.length === 0 && KQL_PATHS.length === 0) {
34
+ if (SIGMA_PATHS.length === 0 && SPLUNK_PATHS.length === 0 && ELASTIC_PATHS.length === 0 && KQL_PATHS.length === 0 && SUBLIME_PATHS.length === 0 && CQL_HUB_PATHS.length === 0) {
33
35
  return;
34
36
  }
35
37
  initDb();
36
38
  if (needsIndexing()) {
37
39
  console.error('[security-detections-mcp] Auto-indexing detections...');
38
- const result = indexDetections(SIGMA_PATHS, SPLUNK_PATHS, STORY_PATHS, ELASTIC_PATHS, KQL_PATHS);
40
+ const result = indexDetections(SIGMA_PATHS, SPLUNK_PATHS, STORY_PATHS, ELASTIC_PATHS, KQL_PATHS, SUBLIME_PATHS, CQL_HUB_PATHS);
39
41
  let msg = `[security-detections-mcp] Indexed ${result.total} detections`;
40
- msg += ` (${result.sigma_indexed} Sigma, ${result.splunk_indexed} Splunk, ${result.elastic_indexed} Elastic, ${result.kql_indexed} KQL)`;
42
+ msg += ` (${result.sigma_indexed} Sigma, ${result.splunk_indexed} Splunk, ${result.elastic_indexed} Elastic, ${result.kql_indexed} KQL, ${result.sublime_indexed} Sublime, ${result.cql_hub_indexed} CrowdStrike CQL)`;
41
43
  if (result.stories_indexed > 0) {
42
44
  msg += `, ${result.stories_indexed} stories`;
43
45
  }
package/dist/indexer.d.ts CHANGED
@@ -7,9 +7,13 @@ export interface IndexResult {
7
7
  elastic_failed: number;
8
8
  kql_indexed: number;
9
9
  kql_failed: number;
10
+ sublime_indexed: number;
11
+ sublime_failed: number;
12
+ cql_hub_indexed: number;
13
+ cql_hub_failed: number;
10
14
  stories_indexed: number;
11
15
  stories_failed: number;
12
16
  total: number;
13
17
  }
14
- export declare function indexDetections(sigmaPaths: string[], splunkPaths: string[], storyPaths?: string[], elasticPaths?: string[], kqlPaths?: string[]): IndexResult;
18
+ export declare function indexDetections(sigmaPaths: string[], splunkPaths: string[], storyPaths?: string[], elasticPaths?: string[], kqlPaths?: string[], sublimePaths?: string[], cqlHubPaths?: string[]): IndexResult;
15
19
  export declare function needsIndexing(): boolean;
package/dist/indexer.js CHANGED
@@ -5,6 +5,8 @@ import { parseSplunkFile } from './parsers/splunk.js';
5
5
  import { parseStoryFile } from './parsers/story.js';
6
6
  import { parseElasticFile } from './parsers/elastic.js';
7
7
  import { parseKqlFile, parseRawKqlFile } from './parsers/kql.js';
8
+ import { parseSublimeFile } from './parsers/sublime.js';
9
+ import { parseCqlHubFile } from './parsers/crowdstrike_cql.js';
8
10
  import { recreateDb, insertDetection, insertStory, getDetectionCount, initDb } from './db.js';
9
11
  // Recursively find all YAML files in a directory
10
12
  function findYamlFiles(dir) {
@@ -108,7 +110,7 @@ function findKqlFiles(dir) {
108
110
  }
109
111
  return files;
110
112
  }
111
- export function indexDetections(sigmaPaths, splunkPaths, storyPaths = [], elasticPaths = [], kqlPaths = []) {
113
+ export function indexDetections(sigmaPaths, splunkPaths, storyPaths = [], elasticPaths = [], kqlPaths = [], sublimePaths = [], cqlHubPaths = []) {
112
114
  // Recreate DB to ensure schema is up to date
113
115
  recreateDb();
114
116
  initDb();
@@ -120,6 +122,10 @@ export function indexDetections(sigmaPaths, splunkPaths, storyPaths = [], elasti
120
122
  let elastic_failed = 0;
121
123
  let kql_indexed = 0;
122
124
  let kql_failed = 0;
125
+ let sublime_indexed = 0;
126
+ let sublime_failed = 0;
127
+ let cql_hub_indexed = 0;
128
+ let cql_hub_failed = 0;
123
129
  let stories_indexed = 0;
124
130
  let stories_failed = 0;
125
131
  // Index Sigma rules
@@ -190,6 +196,34 @@ export function indexDetections(sigmaPaths, splunkPaths, storyPaths = [], elasti
190
196
  }
191
197
  }
192
198
  }
199
+ // Index Sublime Security rules (YAML with MQL source)
200
+ for (const basePath of sublimePaths) {
201
+ const files = findYamlFiles(basePath);
202
+ for (const file of files) {
203
+ const detection = parseSublimeFile(file);
204
+ if (detection) {
205
+ insertDetection(detection);
206
+ sublime_indexed++;
207
+ }
208
+ else {
209
+ sublime_failed++;
210
+ }
211
+ }
212
+ }
213
+ // Index CQL Hub queries (CrowdStrike Query Language)
214
+ for (const basePath of cqlHubPaths) {
215
+ const files = findYamlFiles(basePath);
216
+ for (const file of files) {
217
+ const detection = parseCqlHubFile(file);
218
+ if (detection) {
219
+ insertDetection(detection);
220
+ cql_hub_indexed++;
221
+ }
222
+ else {
223
+ cql_hub_failed++;
224
+ }
225
+ }
226
+ }
193
227
  // Index Splunk Analytic Stories (optional)
194
228
  for (const basePath of storyPaths) {
195
229
  const files = findYamlFiles(basePath);
@@ -213,9 +247,13 @@ export function indexDetections(sigmaPaths, splunkPaths, storyPaths = [], elasti
213
247
  elastic_failed,
214
248
  kql_indexed,
215
249
  kql_failed,
250
+ sublime_indexed,
251
+ sublime_failed,
252
+ cql_hub_indexed,
253
+ cql_hub_failed,
216
254
  stories_indexed,
217
255
  stories_failed,
218
- total: sigma_indexed + splunk_indexed + elastic_indexed + kql_indexed,
256
+ total: sigma_indexed + splunk_indexed + elastic_indexed + kql_indexed + sublime_indexed + cql_hub_indexed,
219
257
  };
220
258
  }
221
259
  export function needsIndexing() {
@@ -0,0 +1,2 @@
1
+ import type { Detection } from '../types.js';
2
+ export declare function parseCqlHubFile(filePath: string): Detection | null;
@@ -0,0 +1,302 @@
1
+ import { readFileSync } from 'fs';
2
+ import { parse as parseYaml } from 'yaml';
3
+ import { createHash } from 'crypto';
4
+ // MITRE technique ID to tactic(s) mapping (top-level techniques only)
5
+ // Some techniques belong to multiple tactics (e.g., T1078 -> initial-access & persistence)
6
+ const TECHNIQUE_TO_TACTICS = {
7
+ 'T1595': ['reconnaissance'], 'T1592': ['reconnaissance'], 'T1589': ['reconnaissance'],
8
+ 'T1590': ['reconnaissance'], 'T1591': ['reconnaissance'], 'T1598': ['reconnaissance'],
9
+ 'T1597': ['reconnaissance'], 'T1596': ['reconnaissance'], 'T1593': ['reconnaissance'],
10
+ 'T1594': ['reconnaissance'],
11
+ 'T1583': ['resource-development'], 'T1586': ['resource-development'], 'T1584': ['resource-development'],
12
+ 'T1587': ['resource-development'], 'T1585': ['resource-development'], 'T1588': ['resource-development'],
13
+ 'T1608': ['resource-development'],
14
+ 'T1189': ['initial-access'], 'T1190': ['initial-access'],
15
+ 'T1200': ['initial-access'], 'T1566': ['initial-access'],
16
+ 'T1195': ['initial-access'], 'T1199': ['initial-access'],
17
+ 'T1133': ['initial-access', 'persistence'],
18
+ 'T1078': ['initial-access', 'persistence', 'privilege-escalation', 'defense-evasion'],
19
+ 'T1091': ['initial-access', 'lateral-movement'],
20
+ 'T1059': ['execution'], 'T1203': ['execution'], 'T1559': ['execution'],
21
+ 'T1106': ['execution'], 'T1129': ['execution'],
22
+ 'T1204': ['execution'], 'T1047': ['execution'], 'T1569': ['execution'],
23
+ 'T1053': ['execution', 'persistence', 'privilege-escalation'],
24
+ 'T1098': ['persistence'], 'T1197': ['persistence'], 'T1547': ['persistence', 'privilege-escalation'],
25
+ 'T1037': ['persistence'], 'T1136': ['persistence'], 'T1543': ['persistence', 'privilege-escalation'],
26
+ 'T1546': ['persistence', 'privilege-escalation'], 'T1574': ['persistence', 'privilege-escalation'],
27
+ 'T1525': ['persistence'], 'T1556': ['persistence', 'credential-access', 'defense-evasion'],
28
+ 'T1137': ['persistence'], 'T1542': ['persistence', 'defense-evasion'],
29
+ 'T1505': ['persistence'], 'T1205': ['persistence', 'defense-evasion'],
30
+ 'T1548': ['privilege-escalation', 'defense-evasion'], 'T1134': ['privilege-escalation', 'defense-evasion'],
31
+ 'T1068': ['privilege-escalation'], 'T1484': ['privilege-escalation', 'defense-evasion'],
32
+ 'T1611': ['privilege-escalation'],
33
+ 'T1562': ['defense-evasion'], 'T1070': ['defense-evasion'], 'T1202': ['defense-evasion'],
34
+ 'T1036': ['defense-evasion'], 'T1055': ['defense-evasion', 'privilege-escalation'],
35
+ 'T1027': ['defense-evasion'], 'T1218': ['defense-evasion'], 'T1216': ['defense-evasion'],
36
+ 'T1220': ['defense-evasion'], 'T1140': ['defense-evasion'], 'T1112': ['defense-evasion'],
37
+ 'T1564': ['defense-evasion'],
38
+ 'T1003': ['credential-access'], 'T1110': ['credential-access'], 'T1555': ['credential-access'],
39
+ 'T1212': ['credential-access'], 'T1187': ['credential-access'], 'T1606': ['credential-access'],
40
+ 'T1056': ['credential-access', 'collection'], 'T1557': ['credential-access', 'collection'],
41
+ 'T1111': ['credential-access'], 'T1552': ['credential-access'], 'T1558': ['credential-access'],
42
+ 'T1539': ['credential-access'], 'T1528': ['credential-access'], 'T1649': ['credential-access'],
43
+ 'T1087': ['discovery'], 'T1010': ['discovery'], 'T1217': ['discovery'],
44
+ 'T1580': ['discovery'], 'T1538': ['discovery'], 'T1526': ['discovery'],
45
+ 'T1482': ['discovery'], 'T1083': ['discovery'], 'T1046': ['discovery'],
46
+ 'T1135': ['discovery'], 'T1040': ['discovery', 'credential-access'],
47
+ 'T1201': ['discovery'], 'T1120': ['discovery'], 'T1069': ['discovery'],
48
+ 'T1057': ['discovery'], 'T1012': ['discovery'], 'T1018': ['discovery'],
49
+ 'T1518': ['discovery'], 'T1082': ['discovery'], 'T1016': ['discovery'],
50
+ 'T1049': ['discovery'], 'T1033': ['discovery'], 'T1007': ['discovery'],
51
+ 'T1124': ['discovery'],
52
+ 'T1021': ['lateral-movement'], 'T1072': ['lateral-movement'],
53
+ 'T1080': ['lateral-movement'], 'T1550': ['lateral-movement', 'defense-evasion'],
54
+ 'T1563': ['lateral-movement'], 'T1570': ['lateral-movement'],
55
+ 'T1560': ['collection'], 'T1123': ['collection'], 'T1119': ['collection'],
56
+ 'T1115': ['collection'], 'T1530': ['collection'], 'T1602': ['collection'],
57
+ 'T1213': ['collection'], 'T1005': ['collection'], 'T1039': ['collection'],
58
+ 'T1025': ['collection'], 'T1074': ['collection'], 'T1114': ['collection'],
59
+ 'T1113': ['collection'], 'T1125': ['collection'],
60
+ 'T1071': ['command-and-control'], 'T1132': ['command-and-control'],
61
+ 'T1001': ['command-and-control'], 'T1568': ['command-and-control'],
62
+ 'T1573': ['command-and-control'], 'T1008': ['command-and-control'],
63
+ 'T1105': ['command-and-control'], 'T1104': ['command-and-control'],
64
+ 'T1095': ['command-and-control'], 'T1571': ['command-and-control'],
65
+ 'T1572': ['command-and-control'], 'T1090': ['command-and-control'],
66
+ 'T1219': ['command-and-control'], 'T1102': ['command-and-control'],
67
+ 'T1048': ['exfiltration'], 'T1041': ['exfiltration'], 'T1011': ['exfiltration'],
68
+ 'T1052': ['exfiltration'], 'T1567': ['exfiltration'], 'T1029': ['exfiltration'],
69
+ 'T1537': ['exfiltration'],
70
+ 'T1531': ['impact'], 'T1485': ['impact'], 'T1486': ['impact'],
71
+ 'T1565': ['impact'], 'T1491': ['impact'], 'T1561': ['impact'],
72
+ 'T1499': ['impact'], 'T1495': ['impact'], 'T1489': ['impact'],
73
+ 'T1490': ['impact'], 'T1498': ['impact'], 'T1496': ['impact'],
74
+ };
75
+ // Generate a stable ID from file path and rule name
76
+ function generateId(filePath, name) {
77
+ const hash = createHash('sha256')
78
+ .update(`${filePath}:${name}`)
79
+ .digest('hex')
80
+ .substring(0, 32);
81
+ return `crowdstrike-cql-${hash}`;
82
+ }
83
+ // Extract MITRE tactics from technique IDs
84
+ function extractMitreTactics(mitreIds) {
85
+ const tactics = new Set();
86
+ for (const id of mitreIds) {
87
+ // Get the parent technique ID (T1003.001 -> T1003)
88
+ const parentId = id.split('.')[0];
89
+ const mapped = TECHNIQUE_TO_TACTICS[parentId];
90
+ if (mapped) {
91
+ for (const tactic of mapped) {
92
+ tactics.add(tactic);
93
+ }
94
+ }
95
+ }
96
+ return [...tactics];
97
+ }
98
+ // Extract process names from CQL query text
99
+ function extractProcessNames(cql) {
100
+ const processNames = new Set();
101
+ // Match .exe references in CQL patterns like FileName=/cmd.exe/, ImageFileName=/\\mimikatz\.exe$/
102
+ const exeMatches = cql.match(/[\w.-]+\.exe/gi);
103
+ if (exeMatches) {
104
+ for (const match of exeMatches) {
105
+ const name = match.toLowerCase();
106
+ if (name.length > 4 && !name.includes('*')) {
107
+ processNames.add(name);
108
+ }
109
+ }
110
+ }
111
+ return [...processNames];
112
+ }
113
+ // Extract file paths from CQL query text
114
+ function extractFilePaths(cql) {
115
+ const filePaths = new Set();
116
+ const interestingPaths = [
117
+ 'C:\\Windows\\Temp', 'C:\\Windows\\System32', 'C:\\Windows\\SysWOW64',
118
+ 'C:\\ProgramData', 'C:\\Users\\Public', '\\AppData\\Local\\Temp',
119
+ '\\AppData\\Roaming',
120
+ ];
121
+ const cqlLower = cql.toLowerCase();
122
+ for (const path of interestingPaths) {
123
+ if (cqlLower.includes(path.toLowerCase())) {
124
+ filePaths.add(path);
125
+ }
126
+ }
127
+ if (cqlLower.includes('\\temp\\') || cqlLower.includes('\\tmp\\')) {
128
+ filePaths.add('Temp directory');
129
+ }
130
+ return [...filePaths];
131
+ }
132
+ // Extract registry paths from CQL query text
133
+ function extractRegistryPaths(cql) {
134
+ const registryPaths = new Set();
135
+ const cqlLower = cql.toLowerCase();
136
+ if (cqlLower.includes('hklm') || cqlLower.includes('hkey_local_machine')) {
137
+ registryPaths.add('HKLM registry');
138
+ }
139
+ if (cqlLower.includes('hkcu') || cqlLower.includes('hkey_current_user')) {
140
+ registryPaths.add('HKCU registry');
141
+ }
142
+ if (cqlLower.includes('\\run\\') || cqlLower.includes('\\runonce\\')) {
143
+ registryPaths.add('Run/RunOnce keys');
144
+ }
145
+ if (cqlLower.includes('\\services\\')) {
146
+ registryPaths.add('Services registry');
147
+ }
148
+ return [...registryPaths];
149
+ }
150
+ // Extract CVE IDs from name and description
151
+ function extractCves(text) {
152
+ const matches = text.match(/CVE-\d{4}-\d+/gi);
153
+ if (!matches)
154
+ return [];
155
+ return [...new Set(matches.map(m => m.toUpperCase()))];
156
+ }
157
+ // Infer platforms from CQL query and log_sources
158
+ function extractPlatforms(cql, logSources) {
159
+ const platforms = new Set();
160
+ // Check event_platform in CQL
161
+ if (/event_platform\s*=\s*"?Win/i.test(cql))
162
+ platforms.add('windows');
163
+ if (/event_platform\s*=\s*"?Mac/i.test(cql))
164
+ platforms.add('macos');
165
+ if (/event_platform\s*=\s*"?Lin/i.test(cql))
166
+ platforms.add('linux');
167
+ // Infer from log_sources
168
+ for (const src of logSources) {
169
+ const lower = src.toLowerCase();
170
+ if (lower === 'endpoint') {
171
+ // Endpoint could be any OS; only add if no specific platform found
172
+ if (platforms.size === 0) {
173
+ platforms.add('windows');
174
+ platforms.add('linux');
175
+ }
176
+ }
177
+ else if (lower === 'network') {
178
+ platforms.add('network');
179
+ }
180
+ else if (lower === 'cloud' || lower.includes('aws') || lower.includes('azure') || lower.includes('gcp')) {
181
+ platforms.add('cloud');
182
+ }
183
+ }
184
+ return [...platforms];
185
+ }
186
+ // Map log_sources to asset type
187
+ function deriveAssetType(logSources) {
188
+ if (!logSources || logSources.length === 0)
189
+ return null;
190
+ const first = logSources[0].toLowerCase();
191
+ if (first === 'endpoint')
192
+ return 'Endpoint';
193
+ if (first === 'network')
194
+ return 'Network';
195
+ if (first === 'cloud' || first.includes('aws') || first.includes('azure') || first.includes('gcp'))
196
+ return 'Cloud';
197
+ if (first === 'identity' || first === 'idp')
198
+ return 'Endpoint';
199
+ return null;
200
+ }
201
+ // Map log_sources to security domain
202
+ function deriveSecurityDomain(logSources) {
203
+ if (!logSources || logSources.length === 0)
204
+ return null;
205
+ const first = logSources[0].toLowerCase();
206
+ if (first === 'endpoint')
207
+ return 'endpoint';
208
+ if (first === 'network')
209
+ return 'network';
210
+ if (first === 'cloud' || first.includes('aws') || first.includes('azure') || first.includes('gcp'))
211
+ return 'cloud';
212
+ if (first === 'identity' || first === 'idp')
213
+ return 'access';
214
+ return null;
215
+ }
216
+ // Map tags to detection type
217
+ function deriveDetectionType(tags) {
218
+ for (const tag of tags) {
219
+ const lower = tag.toLowerCase();
220
+ if (lower === 'hunting')
221
+ return 'Hunting';
222
+ if (lower === 'detection')
223
+ return 'TTP';
224
+ if (lower === 'anomaly')
225
+ return 'Anomaly';
226
+ if (lower === 'correlation')
227
+ return 'Correlation';
228
+ }
229
+ return null;
230
+ }
231
+ export function parseCqlHubFile(filePath) {
232
+ try {
233
+ const content = readFileSync(filePath, 'utf-8');
234
+ const rule = parseYaml(content);
235
+ // name and cql are required
236
+ if (!rule.name || !rule.cql) {
237
+ return null;
238
+ }
239
+ const id = generateId(filePath, rule.name);
240
+ const mitreIds = rule.mitre_ids || [];
241
+ const logSources = rule.log_sources || [];
242
+ const tags = rule.tags || [];
243
+ const csModules = rule.cs_required_modules || [];
244
+ // Build description: include explanation if present
245
+ let description = rule.description || '';
246
+ if (rule.explanation) {
247
+ description = description
248
+ ? `${description}\n\n${rule.explanation}`
249
+ : rule.explanation;
250
+ }
251
+ // Combine text fields for CVE extraction
252
+ const combinedText = `${rule.name} ${rule.description || ''}`;
253
+ // Build enriched tags: include cs_required_modules as prefixed tags
254
+ const enrichedTags = [
255
+ ...tags,
256
+ ...csModules.map(m => `cs_module:${m}`),
257
+ ];
258
+ const detection = {
259
+ id,
260
+ name: rule.name,
261
+ description,
262
+ query: rule.cql,
263
+ source_type: 'crowdstrike_cql',
264
+ mitre_ids: mitreIds,
265
+ logsource_category: logSources.length > 0 ? logSources[0].toLowerCase() : null,
266
+ logsource_product: 'crowdstrike',
267
+ logsource_service: 'falcon_logscale',
268
+ severity: null,
269
+ status: null,
270
+ author: rule.author || null,
271
+ date_created: null,
272
+ date_modified: null,
273
+ references: [],
274
+ falsepositives: [],
275
+ tags: enrichedTags,
276
+ file_path: filePath,
277
+ raw_yaml: content,
278
+ cves: extractCves(combinedText),
279
+ analytic_stories: [],
280
+ data_sources: logSources,
281
+ detection_type: deriveDetectionType(tags),
282
+ asset_type: deriveAssetType(logSources),
283
+ security_domain: deriveSecurityDomain(logSources),
284
+ process_names: extractProcessNames(rule.cql),
285
+ file_paths: extractFilePaths(rule.cql),
286
+ registry_paths: extractRegistryPaths(rule.cql),
287
+ mitre_tactics: extractMitreTactics(mitreIds),
288
+ platforms: extractPlatforms(rule.cql, logSources),
289
+ kql_category: null,
290
+ kql_tags: [],
291
+ kql_keywords: [],
292
+ sublime_attack_types: [],
293
+ sublime_detection_methods: [],
294
+ sublime_tactics: [],
295
+ };
296
+ return detection;
297
+ }
298
+ catch {
299
+ // Skip files that can't be parsed
300
+ return null;
301
+ }
302
+ }
@@ -235,6 +235,9 @@ export function parseElasticFile(filePath) {
235
235
  kql_category: null,
236
236
  kql_tags: [],
237
237
  kql_keywords: [],
238
+ sublime_attack_types: [],
239
+ sublime_detection_methods: [],
240
+ sublime_tactics: [],
238
241
  };
239
242
  return detection;
240
243
  }
@@ -341,6 +341,9 @@ export function parseRawKqlFile(filePath, basePath) {
341
341
  kql_category: category,
342
342
  kql_tags: tags,
343
343
  kql_keywords: keywords,
344
+ sublime_attack_types: [],
345
+ sublime_detection_methods: [],
346
+ sublime_tactics: [],
344
347
  };
345
348
  return detection;
346
349
  }
@@ -415,6 +418,9 @@ export function parseKqlFile(filePath, basePath) {
415
418
  kql_category: category,
416
419
  kql_tags: tags,
417
420
  kql_keywords: keywords,
421
+ sublime_attack_types: [],
422
+ sublime_detection_methods: [],
423
+ sublime_tactics: [],
418
424
  };
419
425
  return detection;
420
426
  }
@@ -388,6 +388,9 @@ export function parseSigmaFile(filePath) {
388
388
  kql_category: null,
389
389
  kql_tags: [],
390
390
  kql_keywords: [],
391
+ sublime_attack_types: [],
392
+ sublime_detection_methods: [],
393
+ sublime_tactics: [],
391
394
  };
392
395
  return detection;
393
396
  }
@@ -182,6 +182,9 @@ export function parseSplunkFile(filePath) {
182
182
  kql_category: null,
183
183
  kql_tags: [],
184
184
  kql_keywords: [],
185
+ sublime_attack_types: [],
186
+ sublime_detection_methods: [],
187
+ sublime_tactics: [],
185
188
  };
186
189
  return detection;
187
190
  }
@@ -0,0 +1,2 @@
1
+ import type { Detection } from '../types.js';
2
+ export declare function parseSublimeFile(filePath: string): Detection | null;