security-detections-mcp 1.2.0 → 1.3.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 +22 -15
- package/dist/indexer.js +27 -10
- package/dist/parsers/kql.d.ts +1 -0
- package/dist/parsers/kql.js +78 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,14 +115,15 @@ cd security_content && git sparse-checkout set detections stories && cd ..
|
|
|
115
115
|
git clone --depth 1 --filter=blob:none --sparse https://github.com/elastic/detection-rules.git
|
|
116
116
|
cd detection-rules && git sparse-checkout set rules && cd ..
|
|
117
117
|
|
|
118
|
-
# Download KQL hunting queries (~
|
|
119
|
-
git clone --depth 1 https://github.com/Bert-JanP/Hunting-Queries-Detection-Rules.git kql
|
|
118
|
+
# Download KQL hunting queries (~400+ queries from 2 repos)
|
|
119
|
+
git clone --depth 1 https://github.com/Bert-JanP/Hunting-Queries-Detection-Rules.git kql-bertjanp
|
|
120
|
+
git clone --depth 1 https://github.com/jkerai1/KQL-Queries.git kql-jkerai1
|
|
120
121
|
|
|
121
122
|
echo "Done! Configure your MCP with these paths:"
|
|
122
123
|
echo " SIGMA_PATHS: $(pwd)/sigma/rules,$(pwd)/sigma/rules-threat-hunting"
|
|
123
124
|
echo " SPLUNK_PATHS: $(pwd)/security_content/detections"
|
|
124
125
|
echo " ELASTIC_PATHS: $(pwd)/detection-rules/rules"
|
|
125
|
-
echo " KQL_PATHS: $(pwd)/kql"
|
|
126
|
+
echo " KQL_PATHS: $(pwd)/kql-bertjanp,$(pwd)/kql-jkerai1"
|
|
126
127
|
echo " STORY_PATHS: $(pwd)/security_content/stories"
|
|
127
128
|
```
|
|
128
129
|
|
|
@@ -143,9 +144,10 @@ git clone https://github.com/splunk/security_content.git
|
|
|
143
144
|
git clone https://github.com/elastic/detection-rules.git
|
|
144
145
|
# Use rules/ directory
|
|
145
146
|
|
|
146
|
-
# KQL Hunting Queries
|
|
147
|
+
# KQL Hunting Queries (multiple sources supported)
|
|
147
148
|
git clone https://github.com/Bert-JanP/Hunting-Queries-Detection-Rules.git
|
|
148
|
-
|
|
149
|
+
git clone https://github.com/jkerai1/KQL-Queries.git
|
|
150
|
+
# Use entire repos, combine paths with comma
|
|
149
151
|
```
|
|
150
152
|
|
|
151
153
|
## MCP Tools
|
|
@@ -377,15 +379,20 @@ From [Elastic Detection Rules](https://github.com/elastic/detection-rules):
|
|
|
377
379
|
- Optional: `rule.description`, `rule.query`, `rule.severity`, `rule.tags`, `rule.threat` (MITRE mappings)
|
|
378
380
|
- Supports EQL, KQL, Lucene, and ESQL query languages
|
|
379
381
|
|
|
380
|
-
### KQL Hunting Queries (Markdown)
|
|
382
|
+
### KQL Hunting Queries (Markdown & Raw .kql)
|
|
381
383
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
-
|
|
385
|
-
-
|
|
384
|
+
Supports multiple KQL repositories:
|
|
385
|
+
|
|
386
|
+
**[Bert-JanP/Hunting-Queries-Detection-Rules](https://github.com/Bert-JanP/Hunting-Queries-Detection-Rules)** (~290 queries)
|
|
387
|
+
- Microsoft Defender XDR and Azure Sentinel hunting queries in Markdown format
|
|
388
|
+
- Extracts title from markdown heading, KQL from fenced code blocks
|
|
386
389
|
- Extracts MITRE technique IDs from tables
|
|
387
|
-
-
|
|
388
|
-
|
|
390
|
+
- Categories: Defender For Endpoint, Azure AD, Threat Hunting, DFIR, etc.
|
|
391
|
+
|
|
392
|
+
**[jkerai1/KQL-Queries](https://github.com/jkerai1/KQL-Queries)** (~130 queries)
|
|
393
|
+
- Raw `.kql` files for Defender, Entra, Azure, Office 365
|
|
394
|
+
- Title derived from filename
|
|
395
|
+
- Lightweight queries for kqlsearch.com
|
|
389
396
|
|
|
390
397
|
## Development
|
|
391
398
|
|
|
@@ -414,9 +421,9 @@ When fully indexed with all sources:
|
|
|
414
421
|
| Sigma Rules | ~3,000+ |
|
|
415
422
|
| Splunk ESCU | ~2,000+ |
|
|
416
423
|
| Elastic Rules | ~1,500+ |
|
|
417
|
-
| KQL Queries | ~
|
|
424
|
+
| KQL Queries | ~420+ |
|
|
418
425
|
| Analytic Stories | ~330 |
|
|
419
|
-
| **Total** | **~7,
|
|
426
|
+
| **Total** | **~7,200+** |
|
|
420
427
|
|
|
421
428
|
## 🔗 Using with MITRE ATT&CK MCP
|
|
422
429
|
|
|
@@ -424,7 +431,7 @@ When fully indexed with all sources:
|
|
|
424
431
|
|
|
425
432
|
| MCP | Purpose |
|
|
426
433
|
|-----|---------|
|
|
427
|
-
| **security-detections-mcp** | Query 7,
|
|
434
|
+
| **security-detections-mcp** | Query 7,200+ detection rules (Sigma, Splunk ESCU, Elastic, KQL) |
|
|
428
435
|
| **mitre-attack-mcp** | Analyze coverage against ATT&CK framework, generate Navigator layers |
|
|
429
436
|
|
|
430
437
|
### Combined Workflow (Efficient)
|
package/dist/indexer.js
CHANGED
|
@@ -4,7 +4,7 @@ import { parseSigmaFile } from './parsers/sigma.js';
|
|
|
4
4
|
import { parseSplunkFile } from './parsers/splunk.js';
|
|
5
5
|
import { parseStoryFile } from './parsers/story.js';
|
|
6
6
|
import { parseElasticFile } from './parsers/elastic.js';
|
|
7
|
-
import { parseKqlFile } from './parsers/kql.js';
|
|
7
|
+
import { parseKqlFile, parseRawKqlFile } from './parsers/kql.js';
|
|
8
8
|
import { recreateDb, insertDetection, insertStory, getDetectionCount, initDb } from './db.js';
|
|
9
9
|
// Recursively find all YAML files in a directory
|
|
10
10
|
function findYamlFiles(dir) {
|
|
@@ -67,9 +67,8 @@ function findTomlFiles(dir) {
|
|
|
67
67
|
}
|
|
68
68
|
return files;
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const files = [];
|
|
70
|
+
function findKqlFiles(dir) {
|
|
71
|
+
const files = { markdown: [], raw: [] };
|
|
73
72
|
try {
|
|
74
73
|
const entries = readdirSync(dir);
|
|
75
74
|
for (const entry of entries) {
|
|
@@ -79,18 +78,24 @@ function findMarkdownFiles(dir) {
|
|
|
79
78
|
if (stat.isDirectory()) {
|
|
80
79
|
// Skip common non-query directories
|
|
81
80
|
if (!['Images', 'images', '.git', 'node_modules'].includes(entry)) {
|
|
82
|
-
|
|
81
|
+
const subFiles = findKqlFiles(fullPath);
|
|
82
|
+
files.markdown.push(...subFiles.markdown);
|
|
83
|
+
files.raw.push(...subFiles.raw);
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
else if (stat.isFile()) {
|
|
86
87
|
const ext = extname(entry).toLowerCase();
|
|
88
|
+
const lowerName = entry.toLowerCase();
|
|
87
89
|
if (ext === '.md') {
|
|
88
90
|
// Skip README files and common non-query files
|
|
89
|
-
const lowerName = entry.toLowerCase();
|
|
90
91
|
if (lowerName !== 'readme.md' && lowerName !== 'license' && lowerName !== 'contributing.md') {
|
|
91
|
-
files.push(fullPath);
|
|
92
|
+
files.markdown.push(fullPath);
|
|
92
93
|
}
|
|
93
94
|
}
|
|
95
|
+
else if (ext === '.kql') {
|
|
96
|
+
// Raw KQL files (jkerai1/KQL-Queries format)
|
|
97
|
+
files.raw.push(fullPath);
|
|
98
|
+
}
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
catch {
|
|
@@ -159,10 +164,11 @@ export function indexDetections(sigmaPaths, splunkPaths, storyPaths = [], elasti
|
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
}
|
|
162
|
-
// Index KQL hunting queries (markdown
|
|
167
|
+
// Index KQL hunting queries (markdown and raw .kql formats)
|
|
163
168
|
for (const basePath of kqlPaths) {
|
|
164
|
-
const files =
|
|
165
|
-
|
|
169
|
+
const files = findKqlFiles(basePath);
|
|
170
|
+
// Parse markdown files (Bert-JanP format)
|
|
171
|
+
for (const file of files.markdown) {
|
|
166
172
|
const detection = parseKqlFile(file, basePath);
|
|
167
173
|
if (detection) {
|
|
168
174
|
insertDetection(detection);
|
|
@@ -172,6 +178,17 @@ export function indexDetections(sigmaPaths, splunkPaths, storyPaths = [], elasti
|
|
|
172
178
|
kql_failed++;
|
|
173
179
|
}
|
|
174
180
|
}
|
|
181
|
+
// Parse raw .kql files (jkerai1 format)
|
|
182
|
+
for (const file of files.raw) {
|
|
183
|
+
const detection = parseRawKqlFile(file, basePath);
|
|
184
|
+
if (detection) {
|
|
185
|
+
insertDetection(detection);
|
|
186
|
+
kql_indexed++;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
kql_failed++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
175
192
|
}
|
|
176
193
|
// Index Splunk Analytic Stories (optional)
|
|
177
194
|
for (const basePath of storyPaths) {
|
package/dist/parsers/kql.d.ts
CHANGED
package/dist/parsers/kql.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, statSync } from 'fs';
|
|
2
|
-
import { relative } from 'path';
|
|
2
|
+
import { basename, relative } from 'path';
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
4
|
// Generate a stable ID from file path
|
|
5
5
|
function generateId(filePath, name) {
|
|
@@ -272,9 +272,85 @@ function extractPlatforms(content, dataSources) {
|
|
|
272
272
|
}
|
|
273
273
|
return Array.from(platforms);
|
|
274
274
|
}
|
|
275
|
+
// Parse a raw .kql file (just query content, filename is title)
|
|
276
|
+
export function parseRawKqlFile(filePath, basePath) {
|
|
277
|
+
try {
|
|
278
|
+
const stat = statSync(filePath);
|
|
279
|
+
if (!stat.isFile()) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
283
|
+
const query = content.trim();
|
|
284
|
+
if (!query || query.length < 10) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
// Title from filename (remove .kql extension)
|
|
288
|
+
const fileName = basename(filePath, '.kql');
|
|
289
|
+
const title = fileName;
|
|
290
|
+
const id = generateId(filePath, title);
|
|
291
|
+
const category = extractCategory(filePath, basePath);
|
|
292
|
+
const dataSources = extractDataSources(query);
|
|
293
|
+
const processNames = extractProcessNames(query);
|
|
294
|
+
const keywords = extractKeywords(query, query);
|
|
295
|
+
const tags = extractTags(query, category);
|
|
296
|
+
const platforms = extractPlatforms(query, dataSources);
|
|
297
|
+
// Extract CVEs from query comments or content
|
|
298
|
+
const cves = [];
|
|
299
|
+
const cveMatches = query.matchAll(/CVE-\d{4}-\d+/gi);
|
|
300
|
+
for (const match of cveMatches) {
|
|
301
|
+
cves.push(match[0].toUpperCase());
|
|
302
|
+
}
|
|
303
|
+
// Extract description from first comment line if present
|
|
304
|
+
let description = '';
|
|
305
|
+
const commentMatch = query.match(/\/\/\s*(.+)/);
|
|
306
|
+
if (commentMatch) {
|
|
307
|
+
description = commentMatch[1].trim();
|
|
308
|
+
}
|
|
309
|
+
const detection = {
|
|
310
|
+
id,
|
|
311
|
+
name: title,
|
|
312
|
+
description,
|
|
313
|
+
query,
|
|
314
|
+
source_type: 'kql',
|
|
315
|
+
mitre_ids: [],
|
|
316
|
+
logsource_category: null,
|
|
317
|
+
logsource_product: 'microsoft',
|
|
318
|
+
logsource_service: 'kql',
|
|
319
|
+
severity: null,
|
|
320
|
+
status: null,
|
|
321
|
+
author: null,
|
|
322
|
+
date_created: null,
|
|
323
|
+
date_modified: null,
|
|
324
|
+
references: [],
|
|
325
|
+
falsepositives: [],
|
|
326
|
+
tags,
|
|
327
|
+
file_path: filePath,
|
|
328
|
+
raw_yaml: content,
|
|
329
|
+
cves,
|
|
330
|
+
analytic_stories: [],
|
|
331
|
+
data_sources: dataSources,
|
|
332
|
+
detection_type: 'Hunting',
|
|
333
|
+
asset_type: dataSources.some(ds => ds.startsWith('Device')) ? 'Endpoint' : 'Cloud',
|
|
334
|
+
security_domain: dataSources.some(ds => ds.startsWith('Device')) ? 'endpoint' :
|
|
335
|
+
dataSources.some(ds => ds.includes('Email')) ? 'email' : 'identity',
|
|
336
|
+
process_names: processNames,
|
|
337
|
+
file_paths: [],
|
|
338
|
+
registry_paths: [],
|
|
339
|
+
mitre_tactics: [],
|
|
340
|
+
platforms,
|
|
341
|
+
kql_category: category,
|
|
342
|
+
kql_tags: tags,
|
|
343
|
+
kql_keywords: keywords,
|
|
344
|
+
};
|
|
345
|
+
return detection;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Parse markdown file with KQL code blocks (Bert-JanP format)
|
|
275
352
|
export function parseKqlFile(filePath, basePath) {
|
|
276
353
|
try {
|
|
277
|
-
// Check if file exists and is a markdown file
|
|
278
354
|
const stat = statSync(filePath);
|
|
279
355
|
if (!stat.isFile()) {
|
|
280
356
|
return null;
|