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/README.md +186 -17
- package/dist/db/detections.d.ts +8 -8
- package/dist/db/detections.js +14 -7
- package/dist/db/schema.js +15 -9
- package/dist/db.d.ts +3 -3
- package/dist/db.js +27 -15
- package/dist/index.js +5 -3
- package/dist/indexer.d.ts +5 -1
- package/dist/indexer.js +40 -2
- package/dist/parsers/crowdstrike_cql.d.ts +2 -0
- package/dist/parsers/crowdstrike_cql.js +302 -0
- package/dist/parsers/elastic.js +3 -0
- package/dist/parsers/kql.js +6 -0
- package/dist/parsers/sigma.js +3 -0
- package/dist/parsers/splunk.js +3 -0
- package/dist/parsers/sublime.d.ts +2 -0
- package/dist/parsers/sublime.js +106 -0
- package/dist/resources/index.js +1 -1
- package/dist/tools/detections/analysis.js +4 -4
- package/dist/tools/detections/comparison.js +3 -3
- package/dist/tools/detections/filters.js +1 -1
- package/dist/tools/detections/search.js +1 -1
- package/dist/tools/engineering/index.js +2 -2
- package/dist/types/detection.d.ts +46 -4
- package/dist/types/detection.js +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +1 -1
- package/dist/types/stats.d.ts +1 -0
- package/package.json +1 -1
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
|
-
|
|
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,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
|
+
}
|
package/dist/parsers/elastic.js
CHANGED
package/dist/parsers/kql.js
CHANGED
|
@@ -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
|
}
|
package/dist/parsers/sigma.js
CHANGED
package/dist/parsers/splunk.js
CHANGED