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 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 (~300+ 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
- # Use entire repo
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
- From [Bert-JanP/Hunting-Queries-Detection-Rules](https://github.com/Bert-JanP/Hunting-Queries-Detection-Rules):
383
- - Microsoft Defender XDR and Azure Sentinel hunting queries
384
- - Extracts title from markdown heading
385
- - Extracts KQL from fenced code blocks
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
- - Derives category from folder path
388
- - Extracts data sources (DeviceProcessEvents, SigninLogs, etc.)
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 | ~300+ |
424
+ | KQL Queries | ~420+ |
418
425
  | Analytic Stories | ~330 |
419
- | **Total** | **~7,000+** |
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,000+ detection rules (Sigma, Splunk ESCU, Elastic, KQL) |
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
- // Recursively find all markdown files in a directory (for KQL queries)
71
- function findMarkdownFiles(dir) {
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
- files.push(...findMarkdownFiles(fullPath));
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 format)
167
+ // Index KQL hunting queries (markdown and raw .kql formats)
163
168
  for (const basePath of kqlPaths) {
164
- const files = findMarkdownFiles(basePath);
165
- for (const file of files) {
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) {
@@ -1,2 +1,3 @@
1
1
  import type { Detection } from '../types.js';
2
+ export declare function parseRawKqlFile(filePath: string, basePath: string): Detection | null;
2
3
  export declare function parseKqlFile(filePath: string, basePath: string): Detection | null;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "security-detections-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for querying Sigma, Splunk ESCU, Elastic, and KQL security detection rules",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",