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/README.md +125 -3
- package/dist/db/attack.d.ts +102 -0
- package/dist/db/attack.js +311 -0
- package/dist/db/detections.d.ts +29 -0
- package/dist/db/detections.js +337 -16
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +10 -0
- package/dist/db/procedure-reference.d.ts +28 -0
- package/dist/db/procedure-reference.js +51772 -0
- package/dist/db/schema.js +114 -0
- package/dist/db.d.ts +1 -0
- package/dist/db.js +2 -2
- package/dist/index.js +30 -1
- package/dist/parsers/kql.js +2 -2
- package/dist/parsers/stix.d.ts +28 -0
- package/dist/parsers/stix.js +207 -0
- package/dist/parsers/sublime.js +41 -1
- package/dist/resources/index.js +79 -6
- package/dist/tools/detections/actor-analysis.d.ts +7 -0
- package/dist/tools/detections/actor-analysis.js +251 -0
- package/dist/tools/detections/analysis.js +460 -1
- package/dist/tools/detections/index.d.ts +2 -0
- package/dist/tools/detections/index.js +5 -1
- package/package.json +1 -1
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
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();
|
package/dist/parsers/kql.js
CHANGED
|
@@ -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 ```
|
|
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
|
+
}
|
package/dist/parsers/sublime.js
CHANGED
|
@@ -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,
|
package/dist/resources/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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) {
|