security-detections-mcp 3.1.1 → 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.
package/dist/db/schema.js CHANGED
@@ -17,6 +17,9 @@ export function createSchema(db) {
17
17
  createStoriesFts(db);
18
18
  createStoriesTriggers(db);
19
19
  createStoriesIndexes(db);
20
+ createProcedureReferenceTable(db);
21
+ createJunctionTables(db);
22
+ createAttackTables(db);
20
23
  }
21
24
  /**
22
25
  * Create the main detections table with all enhanced fields.
@@ -198,6 +201,28 @@ function createStoriesTriggers(db) {
198
201
  function createStoriesIndexes(db) {
199
202
  db.exec(`CREATE INDEX IF NOT EXISTS idx_story_category ON stories(category)`);
200
203
  }
204
+ /**
205
+ * Create the procedure_reference table for storing auto-extracted
206
+ * and hand-curated procedure-level ATT&CK coverage data.
207
+ */
208
+ function createProcedureReferenceTable(db) {
209
+ db.exec(`
210
+ CREATE TABLE IF NOT EXISTS procedure_reference (
211
+ id TEXT PRIMARY KEY,
212
+ technique_id TEXT NOT NULL,
213
+ name TEXT NOT NULL,
214
+ category TEXT NOT NULL,
215
+ description TEXT NOT NULL,
216
+ source TEXT NOT NULL DEFAULT 'auto',
217
+ indicators TEXT NOT NULL,
218
+ detection_count INTEGER DEFAULT 0,
219
+ confidence REAL DEFAULT 1.0,
220
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
221
+ )
222
+ `);
223
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_proc_ref_technique ON procedure_reference(technique_id)`);
224
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_proc_ref_source ON procedure_reference(source)`);
225
+ }
201
226
  /**
202
227
  * Create the saved queries table for caching.
203
228
  * Called on-demand when first accessed.
@@ -217,3 +242,92 @@ export function createSavedQueriesTable(db) {
217
242
  db.exec(`CREATE INDEX IF NOT EXISTS idx_saved_query_type ON saved_queries(query_type)`);
218
243
  db.exec(`CREATE INDEX IF NOT EXISTS idx_saved_query_name ON saved_queries(name)`);
219
244
  }
245
+ /**
246
+ * Create junction tables for materialized many-to-many relationships.
247
+ * Replaces JSON LIKE scans with indexed JOINs.
248
+ */
249
+ function createJunctionTables(db) {
250
+ // Detection → Technique many-to-many
251
+ db.exec(`
252
+ CREATE TABLE IF NOT EXISTS detection_techniques (
253
+ detection_id TEXT NOT NULL,
254
+ technique_id TEXT NOT NULL,
255
+ PRIMARY KEY (detection_id, technique_id)
256
+ )
257
+ `);
258
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_dt_technique ON detection_techniques(technique_id)`);
259
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_dt_detection ON detection_techniques(detection_id)`);
260
+ // Technique → Tactic many-to-many (populated from detections + STIX)
261
+ db.exec(`
262
+ CREATE TABLE IF NOT EXISTS technique_tactics (
263
+ technique_id TEXT NOT NULL,
264
+ tactic_name TEXT NOT NULL,
265
+ source TEXT NOT NULL DEFAULT 'detection',
266
+ PRIMARY KEY (technique_id, tactic_name)
267
+ )
268
+ `);
269
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_tt_tactic ON technique_tactics(tactic_name)`);
270
+ }
271
+ /**
272
+ * Create tables for MITRE ATT&CK STIX data (threat actors, software, techniques).
273
+ * Populated from enterprise-attack.json when ATTACK_STIX_PATH is configured.
274
+ */
275
+ function createAttackTables(db) {
276
+ // Full ATT&CK technique catalog from STIX
277
+ db.exec(`
278
+ CREATE TABLE IF NOT EXISTS attack_techniques (
279
+ technique_id TEXT PRIMARY KEY,
280
+ name TEXT NOT NULL,
281
+ description TEXT,
282
+ platforms TEXT,
283
+ data_sources TEXT,
284
+ is_subtechnique INTEGER DEFAULT 0,
285
+ parent_technique_id TEXT,
286
+ url TEXT
287
+ )
288
+ `);
289
+ // Threat actor catalog from STIX intrusion-sets
290
+ db.exec(`
291
+ CREATE TABLE IF NOT EXISTS attack_actors (
292
+ actor_id TEXT PRIMARY KEY,
293
+ name TEXT NOT NULL,
294
+ aliases TEXT,
295
+ description TEXT,
296
+ external_references TEXT,
297
+ created TEXT,
298
+ modified TEXT
299
+ )
300
+ `);
301
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_actor_name ON attack_actors(name)`);
302
+ // Software (malware + tools) from STIX
303
+ db.exec(`
304
+ CREATE TABLE IF NOT EXISTS attack_software (
305
+ software_id TEXT PRIMARY KEY,
306
+ name TEXT NOT NULL,
307
+ software_type TEXT,
308
+ description TEXT,
309
+ platforms TEXT,
310
+ aliases TEXT
311
+ )
312
+ `);
313
+ // Actor → Technique junction with procedure context
314
+ db.exec(`
315
+ CREATE TABLE IF NOT EXISTS actor_techniques (
316
+ actor_id TEXT NOT NULL,
317
+ technique_id TEXT NOT NULL,
318
+ description TEXT,
319
+ PRIMARY KEY (actor_id, technique_id)
320
+ )
321
+ `);
322
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_at_technique ON actor_techniques(technique_id)`);
323
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_at_actor ON actor_techniques(actor_id)`);
324
+ // Software → Technique junction
325
+ db.exec(`
326
+ CREATE TABLE IF NOT EXISTS software_techniques (
327
+ software_id TEXT NOT NULL,
328
+ technique_id TEXT NOT NULL,
329
+ description TEXT,
330
+ PRIMARY KEY (software_id, technique_id)
331
+ )
332
+ `);
333
+ }
package/dist/db.d.ts CHANGED
@@ -97,6 +97,7 @@ export interface NavigatorLayerOptions {
97
97
  source_type?: 'sigma' | 'splunk_escu' | 'elastic';
98
98
  tactic?: string;
99
99
  severity?: string;
100
+ actor_name?: string;
100
101
  }
101
102
  export declare function generateNavigatorLayer(options: NavigatorLayerOptions): object;
102
103
  export interface DetectionListItem {
package/dist/db.js CHANGED
@@ -990,8 +990,8 @@ export function generateNavigatorLayer(options) {
990
990
  return {
991
991
  name: options.name,
992
992
  versions: {
993
- attack: '18',
994
- navigator: '5.1.0',
993
+ attack: '18.1',
994
+ navigator: '5.3.1',
995
995
  layer: '4.5',
996
996
  },
997
997
  domain: 'enterprise-attack',
package/dist/index.js CHANGED
@@ -12,9 +12,10 @@
12
12
  */
13
13
  import { createServer, startServer } from './server.js';
14
14
  import { registerAllTools, getToolsSummary } from './tools/index.js';
15
- import { initDb } from './db/connection.js';
15
+ import { initDb, closeDb } from './db/connection.js';
16
16
  import { indexDetections, needsIndexing } from './indexer.js';
17
17
  import { initPatternsSchema, getPatternStats } from './db/patterns.js';
18
+ import { extractAllProcedures, populateJunctionTables } from './db/detections.js';
18
19
  // Parse comma-separated paths from env var
19
20
  function parsePaths(envVar) {
20
21
  if (!envVar)
@@ -29,6 +30,7 @@ const STORY_PATHS = parsePaths(process.env.STORY_PATHS);
29
30
  const KQL_PATHS = parsePaths(process.env.KQL_PATHS);
30
31
  const SUBLIME_PATHS = parsePaths(process.env.SUBLIME_PATHS);
31
32
  const CQL_HUB_PATHS = parsePaths(process.env.CQL_HUB_PATHS);
33
+ const ATTACK_STIX_PATH = process.env.ATTACK_STIX_PATH;
32
34
  // Auto-index on startup if paths are configured and DB is empty
33
35
  function autoIndex() {
34
36
  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) {
@@ -44,6 +46,31 @@ function autoIndex() {
44
46
  msg += `, ${result.stories_indexed} stories`;
45
47
  }
46
48
  console.error(msg);
49
+ // Re-initialize connection after indexer's recreateDb
50
+ closeDb();
51
+ initDb();
52
+ // Auto-extract procedure reference data from indexed detections
53
+ console.error('[security-detections-mcp] Extracting procedure-level coverage data...');
54
+ const procResult = extractAllProcedures();
55
+ console.error(`[security-detections-mcp] Procedures: ${procResult.procedures_generated} procedures across ${procResult.techniques_processed} techniques`);
56
+ // Populate junction tables for fast relational queries
57
+ console.error('[security-detections-mcp] Populating junction tables...');
58
+ const junctionResult = populateJunctionTables();
59
+ console.error(`[security-detections-mcp] Junction tables: ${junctionResult.detection_techniques} detection-technique links, ${junctionResult.technique_tactics} technique-tactic links`);
60
+ }
61
+ }
62
+ // Ingest MITRE ATT&CK STIX data if path is configured
63
+ async function ingestStixData() {
64
+ if (!ATTACK_STIX_PATH)
65
+ return;
66
+ try {
67
+ const { ingestStixBundle } = await import('./parsers/stix.js');
68
+ console.error(`[security-detections-mcp] Ingesting ATT&CK STIX data from ${ATTACK_STIX_PATH}...`);
69
+ const stixResult = ingestStixBundle(ATTACK_STIX_PATH);
70
+ console.error(`[security-detections-mcp] STIX: ${stixResult.techniques} techniques, ${stixResult.actors} actors, ${stixResult.software} software, ${stixResult.actor_technique_links} actor-technique links`);
71
+ }
72
+ catch (err) {
73
+ console.error(`[security-detections-mcp] STIX ingest failed: ${err instanceof Error ? err.message : String(err)}`);
47
74
  }
48
75
  }
49
76
  async function main() {
@@ -51,6 +78,8 @@ async function main() {
51
78
  initDb();
52
79
  // Auto-index if configured
53
80
  autoIndex();
81
+ // Ingest MITRE ATT&CK STIX data if configured
82
+ await ingestStixData();
54
83
  // Initialize patterns schema (Detection Engineering Intelligence)
55
84
  initPatternsSchema();
56
85
  const patternStats = getPatternStats();
@@ -95,8 +95,8 @@ function extractMitreTactics(mitreIds) {
95
95
  // Extract KQL queries from markdown code blocks
96
96
  function extractKqlQueries(content) {
97
97
  const queries = [];
98
- // Match ```KQL or ```kql blocks
99
- const kqlBlocks = content.matchAll(/```(?:KQL|kql)\s*\n([\s\S]*?)```/gi);
98
+ // Match ```KQL, ```kql, ```Kusto, or ```kusto blocks
99
+ const kqlBlocks = content.matchAll(/```(?:KQL|kql|Kusto|kusto)\s*\n([\s\S]*?)```/gi);
100
100
  for (const match of kqlBlocks) {
101
101
  const query = match[1].trim();
102
102
  if (query.length > 0) {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MITRE ATT&CK STIX 2.1 Bundle Parser
3
+ *
4
+ * Parses enterprise-attack.json to populate:
5
+ * - attack_techniques: Full technique catalog with metadata
6
+ * - attack_actors: Threat actor/intrusion-set catalog
7
+ * - attack_software: Malware and tool catalog
8
+ * - actor_techniques: Actor → Technique relationships
9
+ * - software_techniques: Software → Technique relationships
10
+ * - technique_tactics: Authoritative technique → tactic mappings
11
+ *
12
+ * Zero npm dependencies — pure JSON parsing.
13
+ */
14
+ export interface StixIngestResult {
15
+ techniques: number;
16
+ actors: number;
17
+ software: number;
18
+ actor_technique_links: number;
19
+ software_technique_links: number;
20
+ technique_tactic_links: number;
21
+ }
22
+ /**
23
+ * Parse and ingest a MITRE ATT&CK STIX 2.1 bundle into the database.
24
+ *
25
+ * @param stixPath Path to enterprise-attack.json
26
+ * @returns Counts of ingested entities
27
+ */
28
+ export declare function ingestStixBundle(stixPath: string): StixIngestResult;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * MITRE ATT&CK STIX 2.1 Bundle Parser
3
+ *
4
+ * Parses enterprise-attack.json to populate:
5
+ * - attack_techniques: Full technique catalog with metadata
6
+ * - attack_actors: Threat actor/intrusion-set catalog
7
+ * - attack_software: Malware and tool catalog
8
+ * - actor_techniques: Actor → Technique relationships
9
+ * - software_techniques: Software → Technique relationships
10
+ * - technique_tactics: Authoritative technique → tactic mappings
11
+ *
12
+ * Zero npm dependencies — pure JSON parsing.
13
+ */
14
+ import { readFileSync } from 'fs';
15
+ import { getDb } from '../db/connection.js';
16
+ // =============================================================================
17
+ // HELPERS
18
+ // =============================================================================
19
+ /**
20
+ * Extract the ATT&CK ID (e.g., T1059.001) from a STIX object's external references.
21
+ */
22
+ function getAttackId(obj) {
23
+ if (!obj.external_references)
24
+ return null;
25
+ for (const ref of obj.external_references) {
26
+ if (ref.source_name === 'mitre-attack' && ref.external_id) {
27
+ return ref.external_id;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Extract the ATT&CK URL from external references.
34
+ */
35
+ function getAttackUrl(obj) {
36
+ if (!obj.external_references)
37
+ return null;
38
+ for (const ref of obj.external_references) {
39
+ if (ref.source_name === 'mitre-attack' && ref.url) {
40
+ return ref.url;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ /**
46
+ * Get parent technique ID from a sub-technique ID (e.g., T1059.001 → T1059).
47
+ */
48
+ function getParentTechniqueId(techniqueId) {
49
+ const dotIndex = techniqueId.indexOf('.');
50
+ if (dotIndex === -1)
51
+ return null;
52
+ return techniqueId.substring(0, dotIndex);
53
+ }
54
+ /**
55
+ * Normalize STIX tactic phase names to the project's convention.
56
+ * STIX uses phase_name like "initial-access" which already matches.
57
+ */
58
+ function normalizeTactic(phaseName) {
59
+ return phaseName.toLowerCase().replace(/ /g, '-');
60
+ }
61
+ // =============================================================================
62
+ // MAIN INGEST FUNCTION
63
+ // =============================================================================
64
+ /**
65
+ * Parse and ingest a MITRE ATT&CK STIX 2.1 bundle into the database.
66
+ *
67
+ * @param stixPath Path to enterprise-attack.json
68
+ * @returns Counts of ingested entities
69
+ */
70
+ export function ingestStixBundle(stixPath) {
71
+ const database = getDb();
72
+ // Read and parse the STIX bundle
73
+ const raw = readFileSync(stixPath, 'utf-8');
74
+ const bundle = JSON.parse(raw);
75
+ if (!bundle.objects || !Array.isArray(bundle.objects)) {
76
+ throw new Error('Invalid STIX bundle: missing objects array');
77
+ }
78
+ // =========================================================================
79
+ // PASS 1: Build lookup maps and categorize objects
80
+ // =========================================================================
81
+ const stixIdToAttackId = new Map(); // STIX ID → T1059.001
82
+ const stixIdToType = new Map(); // STIX ID → object type
83
+ const techniques = [];
84
+ const actors = [];
85
+ const software = [];
86
+ const relationships = [];
87
+ for (const obj of bundle.objects) {
88
+ // Skip revoked or deprecated objects
89
+ if (obj.revoked === true || obj.x_mitre_deprecated === true)
90
+ continue;
91
+ switch (obj.type) {
92
+ case 'attack-pattern': {
93
+ const attackId = getAttackId(obj);
94
+ if (attackId) {
95
+ stixIdToAttackId.set(obj.id, attackId);
96
+ stixIdToType.set(obj.id, 'technique');
97
+ techniques.push(obj);
98
+ }
99
+ break;
100
+ }
101
+ case 'intrusion-set': {
102
+ stixIdToType.set(obj.id, 'actor');
103
+ actors.push(obj);
104
+ break;
105
+ }
106
+ case 'malware':
107
+ case 'tool': {
108
+ stixIdToType.set(obj.id, 'software');
109
+ software.push(obj);
110
+ break;
111
+ }
112
+ case 'relationship': {
113
+ if (obj.relationship_type === 'uses') {
114
+ relationships.push(obj);
115
+ }
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ // =========================================================================
121
+ // PASS 2: Insert entities in a transaction
122
+ // =========================================================================
123
+ const result = {
124
+ techniques: 0,
125
+ actors: 0,
126
+ software: 0,
127
+ actor_technique_links: 0,
128
+ software_technique_links: 0,
129
+ technique_tactic_links: 0,
130
+ };
131
+ const insertTransaction = database.transaction(() => {
132
+ // --- Techniques ---
133
+ const techStmt = database.prepare(`
134
+ INSERT OR REPLACE INTO attack_techniques
135
+ (technique_id, name, description, platforms, data_sources, is_subtechnique, parent_technique_id, url)
136
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
137
+ `);
138
+ // Clear existing STIX-sourced technique_tactics before re-inserting
139
+ database.prepare("DELETE FROM technique_tactics WHERE source = 'stix'").run();
140
+ const ttStmt = database.prepare("INSERT OR REPLACE INTO technique_tactics (technique_id, tactic_name, source) VALUES (?, ?, 'stix')");
141
+ for (const tech of techniques) {
142
+ const attackId = getAttackId(tech);
143
+ const isSubtechnique = tech.x_mitre_is_subtechnique === true ? 1 : 0;
144
+ const parentId = getParentTechniqueId(attackId);
145
+ techStmt.run(attackId, tech.name || attackId, tech.description || null, tech.x_mitre_platforms ? JSON.stringify(tech.x_mitre_platforms) : null, tech.x_mitre_data_sources ? JSON.stringify(tech.x_mitre_data_sources) : null, isSubtechnique, parentId, getAttackUrl(tech));
146
+ result.techniques++;
147
+ // Insert technique → tactic mappings from kill_chain_phases
148
+ if (tech.kill_chain_phases) {
149
+ for (const phase of tech.kill_chain_phases) {
150
+ if (phase.kill_chain_name === 'mitre-attack') {
151
+ ttStmt.run(attackId, normalizeTactic(phase.phase_name));
152
+ result.technique_tactic_links++;
153
+ }
154
+ }
155
+ }
156
+ }
157
+ // --- Actors (Intrusion Sets) ---
158
+ const actorStmt = database.prepare(`
159
+ INSERT OR REPLACE INTO attack_actors
160
+ (actor_id, name, aliases, description, external_references, created, modified)
161
+ VALUES (?, ?, ?, ?, ?, ?, ?)
162
+ `);
163
+ for (const actor of actors) {
164
+ actorStmt.run(actor.id, actor.name || 'Unknown', actor.aliases ? JSON.stringify(actor.aliases) : null, actor.description || null, actor.external_references ? JSON.stringify(actor.external_references) : null, actor.created || null, actor.modified || null);
165
+ result.actors++;
166
+ }
167
+ // --- Software (Malware + Tools) ---
168
+ const swStmt = database.prepare(`
169
+ INSERT OR REPLACE INTO attack_software
170
+ (software_id, name, software_type, description, platforms, aliases)
171
+ VALUES (?, ?, ?, ?, ?, ?)
172
+ `);
173
+ for (const sw of software) {
174
+ swStmt.run(sw.id, sw.name || 'Unknown', sw.type, // 'malware' or 'tool'
175
+ sw.description || null, sw.x_mitre_platforms ? JSON.stringify(sw.x_mitre_platforms) : null, sw.aliases ? JSON.stringify(sw.aliases) : null);
176
+ result.software++;
177
+ }
178
+ // =====================================================================
179
+ // PASS 3: Process "uses" relationships
180
+ // =====================================================================
181
+ const actorTechStmt = database.prepare('INSERT OR REPLACE INTO actor_techniques (actor_id, technique_id, description) VALUES (?, ?, ?)');
182
+ const swTechStmt = database.prepare('INSERT OR REPLACE INTO software_techniques (software_id, technique_id, description) VALUES (?, ?, ?)');
183
+ for (const rel of relationships) {
184
+ if (!rel.source_ref || !rel.target_ref)
185
+ continue;
186
+ const sourceType = stixIdToType.get(rel.source_ref);
187
+ const targetAttackId = stixIdToAttackId.get(rel.target_ref);
188
+ // Only process relationships where the target is a known technique
189
+ if (!targetAttackId)
190
+ continue;
191
+ // Truncate description to avoid storing massive text
192
+ const desc = rel.description
193
+ ? rel.description.substring(0, 2000)
194
+ : null;
195
+ if (sourceType === 'actor') {
196
+ actorTechStmt.run(rel.source_ref, targetAttackId, desc);
197
+ result.actor_technique_links++;
198
+ }
199
+ else if (sourceType === 'software') {
200
+ swTechStmt.run(rel.source_ref, targetAttackId, desc);
201
+ result.software_technique_links++;
202
+ }
203
+ }
204
+ });
205
+ insertTransaction();
206
+ return result;
207
+ }
@@ -15,6 +15,46 @@ const TACTICS_MAP = {
15
15
  'macros': 'execution',
16
16
  'encryption': 'defense-evasion',
17
17
  };
18
+ // Map Sublime attack_types and tactics_and_techniques to MITRE technique IDs
19
+ const ATTACK_TYPE_TO_MITRE = {
20
+ 'credential phishing': ['T1566', 'T1566.001', 'T1566.002', 'T1598'],
21
+ 'bec/fraud': ['T1566.002', 'T1534', 'T1656'],
22
+ 'malware/ransomware': ['T1566.001', 'T1204.002', 'T1486'],
23
+ 'spam': ['T1566'],
24
+ 'callback phishing': ['T1566.003', 'T1598'],
25
+ 'extortion': ['T1486', 'T1657'],
26
+ };
27
+ const TACTIC_TO_MITRE = {
28
+ 'social engineering': ['T1566', 'T1598'],
29
+ 'impersonation: brand': ['T1566.002', 'T1598.003'],
30
+ 'impersonation: employee': ['T1566.002', 'T1534'],
31
+ 'impersonation: vip': ['T1566.002', 'T1534'],
32
+ 'spoofing': ['T1566', 'T1598'],
33
+ 'lookalike domain': ['T1583.001', 'T1566.002'],
34
+ 'exploit': ['T1190', 'T1203'],
35
+ 'macros': ['T1204.002', 'T1059.005'],
36
+ 'scripting': ['T1059'],
37
+ 'evasion': ['T1036', 'T1027'],
38
+ 'encryption': ['T1027', 'T1573'],
39
+ };
40
+ function extractMitreIds(attackTypes, tactics) {
41
+ const ids = new Set();
42
+ if (attackTypes) {
43
+ for (const at of attackTypes) {
44
+ const mapped = ATTACK_TYPE_TO_MITRE[at.toLowerCase()];
45
+ if (mapped)
46
+ mapped.forEach(id => ids.add(id));
47
+ }
48
+ }
49
+ if (tactics) {
50
+ for (const t of tactics) {
51
+ const mapped = TACTIC_TO_MITRE[t.toLowerCase()];
52
+ if (mapped)
53
+ mapped.forEach(id => ids.add(id));
54
+ }
55
+ }
56
+ return [...ids];
57
+ }
18
58
  // Generate a stable ID from file path and rule name
19
59
  function generateId(filePath, name) {
20
60
  const hash = createHash('sha256')
@@ -64,7 +104,7 @@ export function parseSublimeFile(filePath) {
64
104
  description: rule.description || '',
65
105
  query: rule.source,
66
106
  source_type: 'sublime',
67
- mitre_ids: [],
107
+ mitre_ids: extractMitreIds(rule.attack_types, rule.tactics_and_techniques),
68
108
  logsource_category: 'email',
69
109
  logsource_product: 'email',
70
110
  logsource_service: null,
@@ -7,7 +7,7 @@
7
7
  * Static Resources: Fixed URIs returning current state
8
8
  * Resource Templates: Parameterized URIs for dynamic queries
9
9
  */
10
- import { getStats, analyzeCoverage, identifyGaps, listByMitre, listByMitreTactic, listBySource, } from '../db/index.js';
10
+ import { getStats, analyzeCoverage, identifyGaps, listByMitre, listByMitreTactic, listBySource, generateNavigatorLayer, isStixLoaded, getActorByName, getActorCoverage, getActorTechniques, getSoftwareForActor, } from '../db/index.js';
11
11
  import { openEntity, listDecisions, listLearnings, getKnowledgeStats, } from '../db/knowledge.js';
12
12
  // =============================================================================
13
13
  // STATIC RESOURCES
@@ -50,6 +50,13 @@ export const resources = [
50
50
  description: 'Quick comparison of detection counts across sources (sigma, splunk, elastic, kql)',
51
51
  mimeType: 'application/json',
52
52
  },
53
+ // Navigator layer
54
+ {
55
+ uri: 'detection://navigator/layer',
56
+ name: 'ATT&CK Navigator Layer',
57
+ description: 'Full MITRE ATT&CK Navigator layer JSON with detection coverage heatmap. Import at https://mitre-attack.github.io/attack-navigator/',
58
+ mimeType: 'application/json',
59
+ },
53
60
  // Knowledge graph resources
54
61
  {
55
62
  uri: 'knowledge://graph/summary',
@@ -104,6 +111,18 @@ export const resourceTemplates = [
104
111
  description: 'Get complete knowledge graph entity details including relations and observations',
105
112
  mimeType: 'application/json',
106
113
  },
114
+ {
115
+ uriTemplate: 'detection://navigator/layer/{sourceType}',
116
+ name: 'Source-Filtered Navigator Layer',
117
+ description: 'ATT&CK Navigator layer filtered by detection source (sigma, splunk_escu, elastic, kql, sublime, crowdstrike_cql)',
118
+ mimeType: 'application/json',
119
+ },
120
+ {
121
+ uriTemplate: 'detection://navigator/actor/{actorName}',
122
+ name: 'Actor-Filtered Navigator Layer',
123
+ description: 'ATT&CK Navigator layer showing detection coverage for a specific threat actor\'s known techniques',
124
+ mimeType: 'application/json',
125
+ },
107
126
  ];
108
127
  // =============================================================================
109
128
  // RESOURCE LISTING
@@ -217,24 +236,61 @@ function getSourceResource(sourceType) {
217
236
  };
218
237
  }
219
238
  /**
220
- * Get threat actor profile from knowledge graph
239
+ * Get threat actor profile checks STIX data first, falls back to knowledge graph
221
240
  */
222
241
  function getActorResource(actorName) {
242
+ // Try STIX-sourced data first (authoritative MITRE ATT&CK data)
243
+ try {
244
+ if (isStixLoaded()) {
245
+ const actor = getActorByName(actorName);
246
+ if (actor) {
247
+ const coverage = getActorCoverage(actor.actor_id);
248
+ const techniques = getActorTechniques(actor.actor_id);
249
+ const software = getSoftwareForActor(actor.actor_id);
250
+ return {
251
+ actor_name: actor.name,
252
+ found: true,
253
+ source: 'mitre_attack_stix',
254
+ aliases: actor.aliases,
255
+ description: actor.description,
256
+ technique_count: techniques.length,
257
+ techniques: techniques.slice(0, 30).map((t) => ({
258
+ technique_id: t.technique_id,
259
+ name: t.technique_name,
260
+ detection_count: t.detection_count,
261
+ covered: t.detection_count > 0,
262
+ tactics: t.tactics,
263
+ })),
264
+ software: software.slice(0, 20).map((s) => ({
265
+ name: s.name,
266
+ type: s.software_type,
267
+ })),
268
+ coverage_summary: {
269
+ total: coverage.total_techniques,
270
+ covered: coverage.covered_count,
271
+ gaps: coverage.gap_count,
272
+ percentage: coverage.coverage_percentage,
273
+ },
274
+ };
275
+ }
276
+ }
277
+ }
278
+ catch {
279
+ // STIX tables may not exist — fall through to knowledge graph
280
+ }
281
+ // Fallback to knowledge graph
223
282
  const entityResult = openEntity(actorName);
224
283
  if (!entityResult || !entityResult.entity) {
225
284
  return {
226
285
  actor_name: actorName,
227
286
  found: false,
228
- message: `No knowledge graph entry found for actor: ${actorName}`,
229
- suggestion: 'Use knowledge graph tools to create an entity for this threat actor',
287
+ message: `No data found for actor: ${actorName}. Set ATTACK_STIX_PATH env var to load MITRE ATT&CK data, or use knowledge graph tools to create an entity manually.`,
230
288
  };
231
289
  }
232
290
  const { entity, relations, observations } = entityResult;
233
- // Extract techniques from relations
234
291
  const techniques = relations.outgoing
235
292
  .filter((r) => r.relation_type === 'uses_technique' || r.relation_type === 'associated_with')
236
293
  .map((r) => r.to_entity);
237
- // Extract related entities
238
294
  const relatedEntities = [
239
295
  ...relations.outgoing.map((r) => ({
240
296
  name: r.to_entity,
@@ -250,6 +306,7 @@ function getActorResource(actorName) {
250
306
  return {
251
307
  actor_name: entity.name,
252
308
  found: true,
309
+ source: 'knowledge_graph',
253
310
  entity_type: entity.entity_type,
254
311
  created_at: entity.created_at,
255
312
  techniques,
@@ -342,6 +399,8 @@ function getStaticResourceContent(uri) {
342
399
  by_mitre_tactic: stats.by_mitre_tactic,
343
400
  };
344
401
  }
402
+ case 'detection://navigator/layer':
403
+ return generateNavigatorLayer({ name: 'Detection Coverage' });
345
404
  case 'knowledge://graph/summary':
346
405
  return getKnowledgeStats();
347
406
  case 'knowledge://decisions/recent': {
@@ -416,6 +475,20 @@ export async function readResource(uri) {
416
475
  content = getActorResource(actorName);
417
476
  return formatResourceResponse(uri, mimeType, content);
418
477
  }
478
+ // Actor Navigator layer template: detection://navigator/actor/{actorName}
479
+ const actorNavigatorMatch = uri.match(/^detection:\/\/navigator\/actor\/(.+)$/);
480
+ if (actorNavigatorMatch) {
481
+ const actorName = decodeURIComponent(actorNavigatorMatch[1]);
482
+ content = generateNavigatorLayer({ name: `${actorName} Coverage`, actor_name: actorName });
483
+ return formatResourceResponse(uri, mimeType, content);
484
+ }
485
+ // Navigator layer template: detection://navigator/layer/{sourceType}
486
+ const navigatorMatch = uri.match(/^detection:\/\/navigator\/layer\/(.+)$/);
487
+ if (navigatorMatch) {
488
+ const sourceType = decodeURIComponent(navigatorMatch[1]);
489
+ content = generateNavigatorLayer({ name: `${sourceType} Detection Coverage`, source_type: sourceType });
490
+ return formatResourceResponse(uri, mimeType, content);
491
+ }
419
492
  // Knowledge entity template: knowledge://entity/{entityName}
420
493
  const entityMatch = uri.match(/^knowledge:\/\/entity\/(.+)$/);
421
494
  if (entityMatch) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Threat Actor Coverage Analysis Tools
3
+ *
4
+ * MCP tools for analyzing detection coverage against specific
5
+ * MITRE ATT&CK threat actors using STIX-sourced data.
6
+ */
7
+ export declare const actorAnalysisTools: import("../registry.js").ToolDefinition[];