security-detections-mcp 3.1.0 → 3.2.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.
@@ -1,6 +1,7 @@
1
1
  // Coverage analysis tools for detections
2
2
  import { defineTool } from '../registry.js';
3
- import { getStats, getTechniqueIds, analyzeCoverage, identifyGaps, suggestDetections, validateTechniqueId, } from '../../db/index.js';
3
+ import { getStats, getTechniqueIds, analyzeCoverage, identifyGaps, suggestDetections, validateTechniqueId, generateNavigatorLayer, listByMitre, getDb, } from '../../db/index.js';
4
+ import { PROCEDURE_REFERENCE } from '../../db/procedure-reference.js';
4
5
  const THREAT_PROFILE_VALUES = ['ransomware', 'apt', 'initial-access', 'persistence', 'credential-access', 'defense-evasion'];
5
6
  export const analysisTools = [
6
7
  defineTool({
@@ -157,4 +158,462 @@ export const analysisTools = [
157
158
  return suggestions;
158
159
  },
159
160
  }),
161
+ // ═══════════════════════════════════════════════════════════════════════
162
+ // NAVIGATOR LAYER GENERATION
163
+ // ═══════════════════════════════════════════════════════════════════════
164
+ defineTool({
165
+ name: 'generate_navigator_layer',
166
+ description: 'Generate a MITRE ATT&CK Navigator layer JSON from detection coverage. Returns valid Navigator JSON ready for import at https://mitre-attack.github.io/attack-navigator/. Filter by source, tactic, or severity.',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ name: {
171
+ type: 'string',
172
+ description: 'Layer name (e.g., "Sigma Coverage Q1 2026")',
173
+ },
174
+ description: {
175
+ type: 'string',
176
+ description: 'Optional layer description',
177
+ },
178
+ source_type: {
179
+ type: 'string',
180
+ enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
181
+ description: 'Filter to specific source type (optional — includes all if omitted)',
182
+ },
183
+ tactic: {
184
+ type: 'string',
185
+ enum: [
186
+ 'reconnaissance', 'resource-development', 'initial-access', 'execution',
187
+ 'persistence', 'privilege-escalation', 'defense-evasion', 'credential-access',
188
+ 'discovery', 'lateral-movement', 'collection', 'command-and-control',
189
+ 'exfiltration', 'impact',
190
+ ],
191
+ description: 'Filter by MITRE tactic (optional)',
192
+ },
193
+ severity: {
194
+ type: 'string',
195
+ enum: ['informational', 'low', 'medium', 'high', 'critical'],
196
+ description: 'Filter by minimum severity (optional)',
197
+ },
198
+ },
199
+ required: ['name'],
200
+ },
201
+ handler: async (args) => {
202
+ const name = args.name;
203
+ if (!name) {
204
+ return { error: true, code: 'MISSING_REQUIRED_ARG', message: 'name is required' };
205
+ }
206
+ const layer = generateNavigatorLayer({
207
+ name,
208
+ description: args.description,
209
+ source_type: args.source_type,
210
+ tactic: args.tactic,
211
+ severity: args.severity,
212
+ });
213
+ return layer;
214
+ },
215
+ }),
216
+ // ═══════════════════════════════════════════════════════════════════════
217
+ // PROCEDURE-LEVEL COVERAGE ANALYSIS
218
+ // ═══════════════════════════════════════════════════════════════════════
219
+ defineTool({
220
+ name: 'analyze_procedure_coverage',
221
+ description: 'Analyze procedure-level coverage for a MITRE technique. Goes beyond "we cover T1059.001" to show WHICH specific behaviors/procedures your detections actually catch (e.g., encoded commands, download cradles, AMSI bypass). Shows covered and uncovered procedures with detection names.',
222
+ inputSchema: {
223
+ type: 'object',
224
+ properties: {
225
+ technique_id: {
226
+ type: 'string',
227
+ description: 'MITRE technique ID (e.g., T1059.001, T1003.001)',
228
+ },
229
+ source_type: {
230
+ type: 'string',
231
+ enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
232
+ description: 'Filter to specific source (optional — analyzes all if omitted)',
233
+ },
234
+ include_query_snippets: {
235
+ type: 'boolean',
236
+ description: 'Include relevant query snippets showing what each detection checks (default: false)',
237
+ },
238
+ },
239
+ required: ['technique_id'],
240
+ },
241
+ handler: async (args) => {
242
+ const techniqueId = args.technique_id;
243
+ if (!techniqueId) {
244
+ return { error: true, code: 'MISSING_REQUIRED_ARG', message: 'technique_id is required', examples: ['T1059.001', 'T1003.001', 'T1547.001'] };
245
+ }
246
+ const validation = validateTechniqueId(techniqueId);
247
+ if (!validation.valid) {
248
+ return { error: true, code: 'INVALID_TECHNIQUE_ID', message: validation.error, suggestion: validation.suggestion, similar: validation.similar };
249
+ }
250
+ const sourceType = args.source_type;
251
+ const includeSnippets = args.include_query_snippets === true;
252
+ return analyzeProcedureCoverageForTechnique(techniqueId, sourceType, includeSnippets);
253
+ },
254
+ }),
255
+ defineTool({
256
+ name: 'compare_procedure_coverage',
257
+ description: 'Compare procedure-level detection coverage across sources for a technique. Shows which source catches which specific behaviors — two orgs can both tag T1059.001 but detect completely different procedures. Returns a matrix of source × procedure.',
258
+ inputSchema: {
259
+ type: 'object',
260
+ properties: {
261
+ technique_id: {
262
+ type: 'string',
263
+ description: 'MITRE technique ID to compare across sources',
264
+ },
265
+ sources: {
266
+ type: 'array',
267
+ items: {
268
+ type: 'string',
269
+ enum: ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'],
270
+ },
271
+ description: 'Sources to compare (default: all available)',
272
+ },
273
+ },
274
+ required: ['technique_id'],
275
+ },
276
+ handler: async (args) => {
277
+ const techniqueId = args.technique_id;
278
+ if (!techniqueId) {
279
+ return { error: true, code: 'MISSING_REQUIRED_ARG', message: 'technique_id is required' };
280
+ }
281
+ const validation = validateTechniqueId(techniqueId);
282
+ if (!validation.valid) {
283
+ return { error: true, code: 'INVALID_TECHNIQUE_ID', message: validation.error, suggestion: validation.suggestion, similar: validation.similar };
284
+ }
285
+ const sources = args.sources;
286
+ return compareProcedureCoverage(techniqueId, sources);
287
+ },
288
+ }),
160
289
  ];
290
+ // ═══════════════════════════════════════════════════════════════════════════
291
+ // PROCEDURE ANALYSIS HELPERS
292
+ // ═══════════════════════════════════════════════════════════════════════════
293
+ /**
294
+ * Get procedures for a technique. Checks DB first (auto-extracted + hand-curated),
295
+ * falls back to the static PROCEDURE_REFERENCE file.
296
+ */
297
+ function getProcedures(techniqueId) {
298
+ try {
299
+ const db = getDb();
300
+ const rows = db.prepare(`SELECT id, name, category, description, indicators FROM procedure_reference
301
+ WHERE technique_id = ? ORDER BY source DESC, confidence DESC`).all(techniqueId);
302
+ if (rows.length > 0) {
303
+ return rows.map(row => ({
304
+ id: row.id,
305
+ name: row.name,
306
+ category: row.category,
307
+ description: row.description,
308
+ indicators: JSON.parse(row.indicators || '{}'),
309
+ }));
310
+ }
311
+ }
312
+ catch {
313
+ // Table may not exist yet — fall through to static
314
+ }
315
+ return PROCEDURE_REFERENCE[techniqueId] || null;
316
+ }
317
+ function matchDetectionToProcedures(detection, procedures, includeSnippets) {
318
+ const matched = [];
319
+ const searchText = [
320
+ detection.description || '',
321
+ detection.query || '',
322
+ detection.name || '',
323
+ ].join(' ').toLowerCase();
324
+ const processNamesLower = (detection.process_names || []).map(p => p.toLowerCase());
325
+ const filePathsLower = (detection.file_paths || []).map(f => f.toLowerCase());
326
+ const registryPathsLower = (detection.registry_paths || []).map(r => r.toLowerCase());
327
+ for (const proc of procedures) {
328
+ let score = 0;
329
+ // Check process name matches
330
+ if (proc.indicators.process_names) {
331
+ for (const pn of proc.indicators.process_names) {
332
+ if (processNamesLower.some(dp => dp.includes(pn.toLowerCase()))) {
333
+ score += 2;
334
+ break;
335
+ }
336
+ if (searchText.includes(pn.toLowerCase())) {
337
+ score += 1;
338
+ break;
339
+ }
340
+ }
341
+ }
342
+ // Check command pattern matches
343
+ if (proc.indicators.command_patterns) {
344
+ for (const cp of proc.indicators.command_patterns) {
345
+ if (searchText.includes(cp.toLowerCase())) {
346
+ score += 2;
347
+ break;
348
+ }
349
+ }
350
+ }
351
+ // Check description keyword matches
352
+ if (proc.indicators.description_keywords) {
353
+ let kwMatches = 0;
354
+ for (const kw of proc.indicators.description_keywords) {
355
+ if (searchText.includes(kw.toLowerCase())) {
356
+ kwMatches++;
357
+ }
358
+ }
359
+ if (kwMatches >= 2)
360
+ score += 3;
361
+ else if (kwMatches >= 1)
362
+ score += 1;
363
+ }
364
+ // Check registry path matches
365
+ if (proc.indicators.registry_paths) {
366
+ for (const rp of proc.indicators.registry_paths) {
367
+ if (registryPathsLower.some(dr => dr.includes(rp.toLowerCase()))) {
368
+ score += 2;
369
+ break;
370
+ }
371
+ if (searchText.includes(rp.toLowerCase())) {
372
+ score += 1;
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ // Check file path matches
378
+ if (proc.indicators.file_paths) {
379
+ for (const fp of proc.indicators.file_paths) {
380
+ if (filePathsLower.some(df => df.includes(fp.toLowerCase()))) {
381
+ score += 2;
382
+ break;
383
+ }
384
+ if (searchText.includes(fp.toLowerCase())) {
385
+ score += 1;
386
+ break;
387
+ }
388
+ }
389
+ }
390
+ // Check event ID matches
391
+ if (proc.indicators.event_ids) {
392
+ for (const eid of proc.indicators.event_ids) {
393
+ if (searchText.includes(`eventid ${eid}`) || searchText.includes(`event_id: ${eid}`) || searchText.includes(`eventcode=${eid}`) || searchText.includes(`"${eid}"`)) {
394
+ score += 2;
395
+ break;
396
+ }
397
+ }
398
+ }
399
+ // Check field pattern matches
400
+ if (proc.indicators.field_patterns) {
401
+ for (const fp of proc.indicators.field_patterns) {
402
+ if (searchText.includes(fp.toLowerCase())) {
403
+ score += 1;
404
+ break;
405
+ }
406
+ }
407
+ }
408
+ // Threshold: need at least 3 points to count as a match
409
+ if (score >= 3) {
410
+ matched.push(proc.id);
411
+ }
412
+ }
413
+ const result = {
414
+ detection_name: detection.name,
415
+ source_type: detection.source_type,
416
+ procedures_matched: matched,
417
+ };
418
+ if (includeSnippets && detection.query) {
419
+ result.query_snippet = detection.query.substring(0, 200) + (detection.query.length > 200 ? '...' : '');
420
+ }
421
+ return result;
422
+ }
423
+ function analyzeProcedureCoverageForTechnique(techniqueId, sourceType, includeSnippets = false) {
424
+ const procedures = getProcedures(techniqueId);
425
+ const detections = listByMitre(techniqueId, 500);
426
+ const filtered = sourceType ? detections.filter(d => d.source_type === sourceType) : detections;
427
+ if (filtered.length === 0) {
428
+ return {
429
+ technique_id: techniqueId,
430
+ total_detections: 0,
431
+ has_procedure_reference: !!procedures,
432
+ message: sourceType
433
+ ? `No detections found for ${techniqueId} from source ${sourceType}`
434
+ : `No detections found for ${techniqueId}`,
435
+ };
436
+ }
437
+ // If no procedure reference, do best-effort extraction
438
+ if (!procedures) {
439
+ const sources = {};
440
+ const allProcessNames = new Set();
441
+ for (const d of filtered) {
442
+ const src = d.source_type;
443
+ if (!sources[src])
444
+ sources[src] = [];
445
+ sources[src].push(d.name);
446
+ for (const pn of d.process_names || [])
447
+ allProcessNames.add(pn);
448
+ }
449
+ return {
450
+ technique_id: techniqueId,
451
+ total_detections: filtered.length,
452
+ has_procedure_reference: false,
453
+ message: `No procedure reference data for ${techniqueId}. Showing detection inventory. Procedures are auto-extracted at index time for techniques with 2+ detections.`,
454
+ detections_by_source: sources,
455
+ process_names_observed: Array.from(allProcessNames),
456
+ };
457
+ }
458
+ // Match each detection against procedures
459
+ const detectionMatches = filtered.map(d => matchDetectionToProcedures(d, procedures, includeSnippets));
460
+ // Aggregate: which procedures are covered?
461
+ const procedureCoverage = {};
462
+ for (const proc of procedures) {
463
+ procedureCoverage[proc.id] = { detection_count: 0, sources: new Set(), detections: [] };
464
+ }
465
+ for (const dm of detectionMatches) {
466
+ for (const procId of dm.procedures_matched) {
467
+ const pc = procedureCoverage[procId];
468
+ if (pc) {
469
+ pc.detection_count++;
470
+ pc.sources.add(dm.source_type);
471
+ if (pc.detections.length < 5)
472
+ pc.detections.push(dm.detection_name);
473
+ }
474
+ }
475
+ }
476
+ const covered = [];
477
+ const uncovered = [];
478
+ for (const proc of procedures) {
479
+ const pc = procedureCoverage[proc.id];
480
+ if (pc.detection_count > 0) {
481
+ covered.push({
482
+ procedure: proc.name,
483
+ id: proc.id,
484
+ category: proc.category,
485
+ detection_count: pc.detection_count,
486
+ sources: Array.from(pc.sources),
487
+ detections: pc.detections,
488
+ });
489
+ }
490
+ else {
491
+ uncovered.push({
492
+ procedure: proc.name,
493
+ id: proc.id,
494
+ category: proc.category,
495
+ description: proc.description,
496
+ recommendation: `Add detection for: ${proc.description}`,
497
+ });
498
+ }
499
+ }
500
+ // Coverage depth
501
+ const coveredCount = covered.length;
502
+ const totalProcs = procedures.length;
503
+ let coverage_depth;
504
+ const coverageRatio = coveredCount / totalProcs;
505
+ if (coverageRatio >= 0.8 && coveredCount >= 5)
506
+ coverage_depth = 'deep';
507
+ else if (coverageRatio >= 0.5 && coveredCount >= 3)
508
+ coverage_depth = 'moderate';
509
+ else if (coveredCount >= 1)
510
+ coverage_depth = 'shallow';
511
+ else
512
+ coverage_depth = 'none';
513
+ // Unmatched detections (detections that didn't match any procedure)
514
+ const unmatchedCount = detectionMatches.filter(dm => dm.procedures_matched.length === 0).length;
515
+ return {
516
+ technique_id: techniqueId,
517
+ total_detections: filtered.length,
518
+ coverage_depth,
519
+ procedures_covered: coveredCount,
520
+ procedures_total: totalProcs,
521
+ coverage_percent: Math.round((coveredCount / totalProcs) * 100),
522
+ covered,
523
+ uncovered,
524
+ unmatched_detections: unmatchedCount,
525
+ unmatched_note: unmatchedCount > 0
526
+ ? `${unmatchedCount} detection(s) didn't match any known procedure. They may cover behaviors not yet in the reference data.`
527
+ : undefined,
528
+ };
529
+ }
530
+ function compareProcedureCoverage(techniqueId, sources) {
531
+ const procedures = getProcedures(techniqueId);
532
+ const detections = listByMitre(techniqueId, 500);
533
+ if (!procedures) {
534
+ return {
535
+ technique_id: techniqueId,
536
+ has_procedure_reference: false,
537
+ message: `No procedure reference data for ${techniqueId}. Procedures are auto-extracted at index time for techniques with 2+ detections.`,
538
+ };
539
+ }
540
+ // Determine available sources
541
+ const availableSources = new Set(detections.map(d => d.source_type));
542
+ const sourcesToCompare = sources
543
+ ? sources.filter(s => availableSources.has(s))
544
+ : Array.from(availableSources);
545
+ if (sourcesToCompare.length === 0) {
546
+ return {
547
+ technique_id: techniqueId,
548
+ message: 'No detections found for the requested sources',
549
+ available_sources: Array.from(availableSources),
550
+ };
551
+ }
552
+ // Build matrix: procedure × source
553
+ const matrix = {};
554
+ for (const proc of procedures) {
555
+ matrix[proc.id] = {};
556
+ for (const src of sourcesToCompare) {
557
+ matrix[proc.id][src] = { covered: false, count: 0, detections: [] };
558
+ }
559
+ }
560
+ // Fill matrix
561
+ for (const src of sourcesToCompare) {
562
+ const srcDetections = detections.filter(d => d.source_type === src);
563
+ const matches = srcDetections.map(d => matchDetectionToProcedures(d, procedures, false));
564
+ for (const dm of matches) {
565
+ for (const procId of dm.procedures_matched) {
566
+ if (matrix[procId]?.[src]) {
567
+ matrix[procId][src].covered = true;
568
+ matrix[procId][src].count++;
569
+ if (matrix[procId][src].detections.length < 3) {
570
+ matrix[procId][src].detections.push(dm.detection_name);
571
+ }
572
+ }
573
+ }
574
+ }
575
+ }
576
+ // Build readable comparison
577
+ const comparison = procedures.map(proc => {
578
+ const row = { procedure: proc.name, id: proc.id, category: proc.category };
579
+ let coveredByCount = 0;
580
+ const coveredBy = [];
581
+ for (const src of sourcesToCompare) {
582
+ const cell = matrix[proc.id][src];
583
+ row[src] = cell.covered ? `${cell.count} detection(s)` : '—';
584
+ if (cell.covered) {
585
+ coveredByCount++;
586
+ coveredBy.push(src);
587
+ }
588
+ }
589
+ row.covered_by_sources = coveredByCount;
590
+ row.redundancy = coveredByCount > 1 ? 'redundant' : coveredByCount === 1 ? 'single-source' : 'gap';
591
+ return row;
592
+ });
593
+ // Summary
594
+ const totalProcs = procedures.length;
595
+ const fullyCovered = comparison.filter(r => r.redundancy === 'redundant').length;
596
+ const singleSource = comparison.filter(r => r.redundancy === 'single-source').length;
597
+ const gaps = comparison.filter(r => r.redundancy === 'gap').length;
598
+ // Per-source score
599
+ const sourceScores = {};
600
+ for (const src of sourcesToCompare) {
601
+ const covered = comparison.filter(r => {
602
+ const val = r[src];
603
+ return val !== '—';
604
+ }).length;
605
+ sourceScores[src] = { procedures_covered: covered, total: totalProcs, percent: Math.round((covered / totalProcs) * 100) };
606
+ }
607
+ return {
608
+ technique_id: techniqueId,
609
+ sources_compared: sourcesToCompare,
610
+ procedures_total: totalProcs,
611
+ summary: {
612
+ multi_source_coverage: fullyCovered,
613
+ single_source_only: singleSource,
614
+ uncovered_gaps: gaps,
615
+ },
616
+ source_scores: sourceScores,
617
+ matrix: comparison,
618
+ };
619
+ }
@@ -2,11 +2,13 @@ export { searchTools } from './search.js';
2
2
  export { filterTools } from './filters.js';
3
3
  export { analysisTools } from './analysis.js';
4
4
  export { comparisonTools } from './comparison.js';
5
+ export { actorAnalysisTools } from './actor-analysis.js';
5
6
  export declare const detectionTools: import("../registry.js").ToolDefinition[];
6
7
  export declare const detectionToolCounts: {
7
8
  search: number;
8
9
  filters: number;
9
10
  analysis: number;
10
11
  comparison: number;
12
+ actor_analysis: number;
11
13
  total: number;
12
14
  };
@@ -3,17 +3,20 @@ import { searchTools } from './search.js';
3
3
  import { filterTools } from './filters.js';
4
4
  import { analysisTools } from './analysis.js';
5
5
  import { comparisonTools } from './comparison.js';
6
+ import { actorAnalysisTools } from './actor-analysis.js';
6
7
  // Re-export individual tool arrays for granular imports
7
8
  export { searchTools } from './search.js';
8
9
  export { filterTools } from './filters.js';
9
10
  export { analysisTools } from './analysis.js';
10
11
  export { comparisonTools } from './comparison.js';
12
+ export { actorAnalysisTools } from './actor-analysis.js';
11
13
  // Combined export of all detection tools
12
14
  export const detectionTools = [
13
15
  ...searchTools,
14
16
  ...filterTools,
15
17
  ...analysisTools,
16
18
  ...comparisonTools,
19
+ ...actorAnalysisTools,
17
20
  ];
18
21
  // Tool counts for debugging/stats
19
22
  export const detectionToolCounts = {
@@ -21,5 +24,6 @@ export const detectionToolCounts = {
21
24
  filters: filterTools.length,
22
25
  analysis: analysisTools.length,
23
26
  comparison: comparisonTools.length,
24
- total: searchTools.length + filterTools.length + analysisTools.length + comparisonTools.length,
27
+ actor_analysis: actorAnalysisTools.length,
28
+ total: searchTools.length + filterTools.length + analysisTools.length + comparisonTools.length + actorAnalysisTools.length,
25
29
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "security-detections-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Advanced MCP server for security detections with Detection Engineering Intelligence, Knowledge Graph (Tribal Knowledge), Elicitation, and Resource Subscriptions",
5
5
  "sigmaSpecVersion": "2.0.0",
6
6
  "type": "module",
@@ -40,6 +40,9 @@
40
40
  "kusto",
41
41
  "defender",
42
42
  "sentinel",
43
+ "sublime",
44
+ "crowdstrike",
45
+ "cql",
43
46
  "siem",
44
47
  "mitre",
45
48
  "attack"