security-detections-mcp 3.0.0 → 3.1.1
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 +97 -21
- 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 +4 -1
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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
// Best-effort mapping from Sublime tactics_and_techniques to MITRE ATT&CK tactics
|
|
5
|
+
const TACTICS_MAP = {
|
|
6
|
+
'evasion': 'defense-evasion',
|
|
7
|
+
'exploit': 'execution',
|
|
8
|
+
'social engineering': 'initial-access',
|
|
9
|
+
'impersonation: brand': 'initial-access',
|
|
10
|
+
'impersonation: employee': 'initial-access',
|
|
11
|
+
'impersonation: vip': 'initial-access',
|
|
12
|
+
'lookalike domain': 'initial-access',
|
|
13
|
+
'spoofing': 'initial-access',
|
|
14
|
+
'scripting': 'execution',
|
|
15
|
+
'macros': 'execution',
|
|
16
|
+
'encryption': 'defense-evasion',
|
|
17
|
+
};
|
|
18
|
+
// Generate a stable ID from file path and rule name
|
|
19
|
+
function generateId(filePath, name) {
|
|
20
|
+
const hash = createHash('sha256')
|
|
21
|
+
.update(`${filePath}:${name}`)
|
|
22
|
+
.digest('hex')
|
|
23
|
+
.substring(0, 32);
|
|
24
|
+
return `sublime-${hash}`;
|
|
25
|
+
}
|
|
26
|
+
// Extract author names from the authors array
|
|
27
|
+
function extractAuthorNames(authors) {
|
|
28
|
+
if (!authors || authors.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
const names = authors
|
|
31
|
+
.map(a => a.name || a.twitter || a.github || 'Unknown')
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
return names.length > 0 ? names.join(', ') : null;
|
|
34
|
+
}
|
|
35
|
+
// Map Sublime tactics to MITRE ATT&CK tactics (best-effort)
|
|
36
|
+
function mapToMitreTactics(tactics) {
|
|
37
|
+
if (!tactics)
|
|
38
|
+
return [];
|
|
39
|
+
const mitreTactics = new Set();
|
|
40
|
+
for (const tactic of tactics) {
|
|
41
|
+
const mapped = TACTICS_MAP[tactic.toLowerCase()];
|
|
42
|
+
if (mapped) {
|
|
43
|
+
mitreTactics.add(mapped);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...mitreTactics];
|
|
47
|
+
}
|
|
48
|
+
export function parseSublimeFile(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
51
|
+
const rule = parseYaml(content);
|
|
52
|
+
// name and source are required
|
|
53
|
+
if (!rule.name || !rule.source) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// type must be 'rule' or 'exclusion'
|
|
57
|
+
if (rule.type !== 'rule' && rule.type !== 'exclusion') {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const id = rule.id || generateId(filePath, rule.name);
|
|
61
|
+
const detection = {
|
|
62
|
+
id,
|
|
63
|
+
name: rule.name,
|
|
64
|
+
description: rule.description || '',
|
|
65
|
+
query: rule.source,
|
|
66
|
+
source_type: 'sublime',
|
|
67
|
+
mitre_ids: [],
|
|
68
|
+
logsource_category: 'email',
|
|
69
|
+
logsource_product: 'email',
|
|
70
|
+
logsource_service: null,
|
|
71
|
+
severity: rule.severity || null,
|
|
72
|
+
status: null,
|
|
73
|
+
author: extractAuthorNames(rule.authors),
|
|
74
|
+
date_created: null,
|
|
75
|
+
date_modified: null,
|
|
76
|
+
references: rule.references || [],
|
|
77
|
+
falsepositives: rule.false_positives || [],
|
|
78
|
+
tags: rule.tags || [],
|
|
79
|
+
file_path: filePath,
|
|
80
|
+
raw_yaml: content,
|
|
81
|
+
cves: [],
|
|
82
|
+
analytic_stories: [],
|
|
83
|
+
data_sources: ['Email Messages', 'Email Headers', 'Email Attachments'],
|
|
84
|
+
detection_type: rule.type === 'exclusion' ? 'Exclusion' : 'Rule',
|
|
85
|
+
asset_type: 'Email',
|
|
86
|
+
security_domain: 'access',
|
|
87
|
+
process_names: [],
|
|
88
|
+
file_paths: [],
|
|
89
|
+
registry_paths: [],
|
|
90
|
+
mitre_tactics: mapToMitreTactics(rule.tactics_and_techniques),
|
|
91
|
+
platforms: ['email'],
|
|
92
|
+
kql_category: null,
|
|
93
|
+
kql_tags: [],
|
|
94
|
+
kql_keywords: [],
|
|
95
|
+
// Sublime-specific fields
|
|
96
|
+
sublime_attack_types: rule.attack_types || [],
|
|
97
|
+
sublime_detection_methods: rule.detection_methods || [],
|
|
98
|
+
sublime_tactics: rule.tactics_and_techniques || [],
|
|
99
|
+
};
|
|
100
|
+
return detection;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Skip files that can't be parsed
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/resources/index.js
CHANGED
|
@@ -182,7 +182,7 @@ function getTacticResource(tactic) {
|
|
|
182
182
|
* Get statistics and sample detections for a source type
|
|
183
183
|
*/
|
|
184
184
|
function getSourceResource(sourceType) {
|
|
185
|
-
const validSources = ['sigma', 'splunk_escu', 'elastic', 'kql'];
|
|
185
|
+
const validSources = ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'];
|
|
186
186
|
const normalizedSource = sourceType.toLowerCase();
|
|
187
187
|
if (!validSources.includes(normalizedSource)) {
|
|
188
188
|
return {
|
|
@@ -23,7 +23,7 @@ export const analysisTools = [
|
|
|
23
23
|
properties: {
|
|
24
24
|
source_type: {
|
|
25
25
|
type: 'string',
|
|
26
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
26
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
27
27
|
description: 'Filter by source type',
|
|
28
28
|
},
|
|
29
29
|
tactic: {
|
|
@@ -66,7 +66,7 @@ export const analysisTools = [
|
|
|
66
66
|
properties: {
|
|
67
67
|
source_type: {
|
|
68
68
|
type: 'string',
|
|
69
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
69
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
70
70
|
description: 'Filter by source type (optional - analyzes all if not specified)',
|
|
71
71
|
},
|
|
72
72
|
},
|
|
@@ -90,7 +90,7 @@ export const analysisTools = [
|
|
|
90
90
|
},
|
|
91
91
|
source_type: {
|
|
92
92
|
type: 'string',
|
|
93
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
93
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
94
94
|
description: 'Filter by source type (optional)',
|
|
95
95
|
},
|
|
96
96
|
},
|
|
@@ -124,7 +124,7 @@ export const analysisTools = [
|
|
|
124
124
|
},
|
|
125
125
|
source_type: {
|
|
126
126
|
type: 'string',
|
|
127
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
127
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
128
128
|
description: 'Filter by source type (optional)',
|
|
129
129
|
},
|
|
130
130
|
},
|
|
@@ -15,7 +15,7 @@ export const comparisonTools = [
|
|
|
15
15
|
},
|
|
16
16
|
source_type: {
|
|
17
17
|
type: 'string',
|
|
18
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
18
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
19
19
|
description: 'Optional: filter by source type',
|
|
20
20
|
},
|
|
21
21
|
limit: {
|
|
@@ -147,7 +147,7 @@ export const comparisonTools = [
|
|
|
147
147
|
},
|
|
148
148
|
source_type: {
|
|
149
149
|
type: 'string',
|
|
150
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
150
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
151
151
|
description: 'Optional: filter to specific source',
|
|
152
152
|
},
|
|
153
153
|
},
|
|
@@ -235,7 +235,7 @@ export const comparisonTools = [
|
|
|
235
235
|
properties: {
|
|
236
236
|
source_type: {
|
|
237
237
|
type: 'string',
|
|
238
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
238
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
239
239
|
description: 'Filter by source type (optional)',
|
|
240
240
|
},
|
|
241
241
|
},
|
|
@@ -10,7 +10,7 @@ export const filterTools = [
|
|
|
10
10
|
properties: {
|
|
11
11
|
source_type: {
|
|
12
12
|
type: 'string',
|
|
13
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
13
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
14
14
|
description: 'Source type to filter by',
|
|
15
15
|
},
|
|
16
16
|
limit: {
|
|
@@ -22,7 +22,7 @@ export const searchTools = [
|
|
|
22
22
|
},
|
|
23
23
|
source_type: {
|
|
24
24
|
type: 'string',
|
|
25
|
-
enum: ['sigma', 'splunk_escu', 'elastic', 'kql'],
|
|
25
|
+
enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
|
|
26
26
|
description: 'Filter results by detection source type',
|
|
27
27
|
},
|
|
28
28
|
},
|