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
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
import { defineTool } from '../registry.js';
|
|
8
|
+
import { isStixLoaded, getActorByName, listActors, getActorTechniques, getActorCoverage, getSoftwareForActor, getAttackStats, generateNavigatorLayer, } from '../../db/index.js';
|
|
9
|
+
const SOURCE_TYPES = ['sigma', 'splunk_escu', 'elastic', 'kql', 'sublime', 'crowdstrike_cql'];
|
|
10
|
+
function stixNotLoadedError() {
|
|
11
|
+
return {
|
|
12
|
+
error: true,
|
|
13
|
+
code: 'STIX_NOT_LOADED',
|
|
14
|
+
message: 'MITRE ATT&CK STIX data not loaded. Set the ATTACK_STIX_PATH environment variable to the path of enterprise-attack.json. Download from: https://github.com/mitre-attack/attack-stix-data',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export const actorAnalysisTools = [
|
|
18
|
+
defineTool({
|
|
19
|
+
name: 'analyze_actor_coverage',
|
|
20
|
+
description: 'Analyze detection coverage against a specific threat actor. Shows which of the actor\'s known MITRE ATT&CK techniques have detections, coverage percentage, and prioritized gaps. Requires STIX data (ATTACK_STIX_PATH env var).',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
actor_name: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Threat actor name or alias (e.g., APT29, Cozy Bear, Lazarus Group, FIN7, HAFNIUM)',
|
|
27
|
+
},
|
|
28
|
+
source_type: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: [...SOURCE_TYPES],
|
|
31
|
+
description: 'Filter detections to a specific source (optional)',
|
|
32
|
+
},
|
|
33
|
+
include_navigator_layer: {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
description: 'Include ATT&CK Navigator layer JSON in response (default: false)',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ['actor_name'],
|
|
39
|
+
},
|
|
40
|
+
handler: async (args) => {
|
|
41
|
+
if (!isStixLoaded())
|
|
42
|
+
return stixNotLoadedError();
|
|
43
|
+
const actorName = args.actor_name;
|
|
44
|
+
const sourceType = args.source_type;
|
|
45
|
+
const includeNavigator = args.include_navigator_layer;
|
|
46
|
+
const actor = getActorByName(actorName);
|
|
47
|
+
if (!actor) {
|
|
48
|
+
return {
|
|
49
|
+
error: true,
|
|
50
|
+
code: 'ACTOR_NOT_FOUND',
|
|
51
|
+
message: `Threat actor not found: "${actorName}". Use list_actors to browse available actors.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const coverage = getActorCoverage(actor.actor_id, sourceType);
|
|
55
|
+
const result = {
|
|
56
|
+
actor_name: coverage.actor.name,
|
|
57
|
+
aliases: coverage.actor.aliases,
|
|
58
|
+
total_techniques: coverage.total_techniques,
|
|
59
|
+
covered_count: coverage.covered_count,
|
|
60
|
+
gap_count: coverage.gap_count,
|
|
61
|
+
coverage_percentage: coverage.coverage_percentage,
|
|
62
|
+
coverage_by_tactic: coverage.by_tactic,
|
|
63
|
+
covered_techniques: coverage.covered_techniques.map(t => ({
|
|
64
|
+
technique_id: t.technique_id,
|
|
65
|
+
name: t.technique_name,
|
|
66
|
+
detection_count: t.detection_count,
|
|
67
|
+
tactics: t.tactics,
|
|
68
|
+
})),
|
|
69
|
+
gap_techniques: coverage.gap_techniques.map(t => ({
|
|
70
|
+
technique_id: t.technique_id,
|
|
71
|
+
name: t.technique_name,
|
|
72
|
+
tactics: t.tactics,
|
|
73
|
+
priority: t.tactics.some(tac => ['initial-access', 'execution', 'persistence', 'credential-access'].includes(tac)) ? 'HIGH' : 'MEDIUM',
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
if (includeNavigator) {
|
|
77
|
+
result.navigator_layer = generateNavigatorLayer({
|
|
78
|
+
name: `${coverage.actor.name} Coverage`,
|
|
79
|
+
description: `Detection coverage for ${coverage.actor.name} (${coverage.actor.aliases.join(', ')})`,
|
|
80
|
+
actor_name: actorName,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
defineTool({
|
|
87
|
+
name: 'list_actors',
|
|
88
|
+
description: 'List all known MITRE ATT&CK threat actors with aliases and technique counts. Requires STIX data (ATTACK_STIX_PATH env var).',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
search: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'Search by actor name or alias (optional)',
|
|
95
|
+
},
|
|
96
|
+
limit: {
|
|
97
|
+
type: 'number',
|
|
98
|
+
description: 'Maximum results to return (default: 50)',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
handler: async (args) => {
|
|
103
|
+
if (!isStixLoaded())
|
|
104
|
+
return stixNotLoadedError();
|
|
105
|
+
const search = args.search;
|
|
106
|
+
const limit = args.limit || 50;
|
|
107
|
+
const actors = listActors(search, limit);
|
|
108
|
+
const stats = getAttackStats();
|
|
109
|
+
return {
|
|
110
|
+
total_actors: stats.actors,
|
|
111
|
+
showing: actors.length,
|
|
112
|
+
actors: actors.map(a => ({
|
|
113
|
+
name: a.name,
|
|
114
|
+
aliases: a.aliases,
|
|
115
|
+
technique_count: a.technique_count,
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
defineTool({
|
|
121
|
+
name: 'compare_actor_coverage',
|
|
122
|
+
description: 'Compare detection coverage across multiple threat actors. Shows shared technique gaps and unique risks per actor. Requires STIX data (ATTACK_STIX_PATH env var).',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
actor_names: {
|
|
127
|
+
type: 'array',
|
|
128
|
+
items: { type: 'string' },
|
|
129
|
+
description: 'List of threat actor names to compare (2-5 actors)',
|
|
130
|
+
},
|
|
131
|
+
source_type: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
enum: [...SOURCE_TYPES],
|
|
134
|
+
description: 'Filter detections to a specific source (optional)',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
required: ['actor_names'],
|
|
138
|
+
},
|
|
139
|
+
handler: async (args) => {
|
|
140
|
+
if (!isStixLoaded())
|
|
141
|
+
return stixNotLoadedError();
|
|
142
|
+
const actorNames = args.actor_names;
|
|
143
|
+
const sourceType = args.source_type;
|
|
144
|
+
if (actorNames.length < 2 || actorNames.length > 5) {
|
|
145
|
+
return { error: true, message: 'Provide 2-5 actor names for comparison' };
|
|
146
|
+
}
|
|
147
|
+
const coverages = [];
|
|
148
|
+
for (const name of actorNames) {
|
|
149
|
+
const actor = getActorByName(name);
|
|
150
|
+
if (!actor) {
|
|
151
|
+
return { error: true, message: `Actor not found: "${name}"` };
|
|
152
|
+
}
|
|
153
|
+
const cov = getActorCoverage(actor.actor_id, sourceType);
|
|
154
|
+
coverages.push({
|
|
155
|
+
name: cov.actor.name,
|
|
156
|
+
coverage_percentage: cov.coverage_percentage,
|
|
157
|
+
total: cov.total_techniques,
|
|
158
|
+
covered: cov.covered_count,
|
|
159
|
+
gaps: new Set(cov.gap_techniques.map(t => t.technique_id)),
|
|
160
|
+
techniques: new Set([
|
|
161
|
+
...cov.covered_techniques.map(t => t.technique_id),
|
|
162
|
+
...cov.gap_techniques.map(t => t.technique_id),
|
|
163
|
+
]),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Find shared gaps (techniques that are gaps for ALL actors)
|
|
167
|
+
const allGaps = coverages.map(c => c.gaps);
|
|
168
|
+
const sharedGaps = [...allGaps[0]].filter(gap => allGaps.every(gs => gs.has(gap)));
|
|
169
|
+
// Find unique techniques per actor (not used by any other)
|
|
170
|
+
const uniqueTechniques = {};
|
|
171
|
+
for (const cov of coverages) {
|
|
172
|
+
const otherTechs = coverages
|
|
173
|
+
.filter(c => c.name !== cov.name)
|
|
174
|
+
.flatMap(c => [...c.techniques]);
|
|
175
|
+
const otherSet = new Set(otherTechs);
|
|
176
|
+
uniqueTechniques[cov.name] = [...cov.techniques].filter(t => !otherSet.has(t));
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
comparison: coverages.map(c => ({
|
|
180
|
+
actor: c.name,
|
|
181
|
+
total_techniques: c.total,
|
|
182
|
+
covered: c.covered,
|
|
183
|
+
coverage_percentage: c.coverage_percentage,
|
|
184
|
+
})),
|
|
185
|
+
shared_gaps: sharedGaps,
|
|
186
|
+
shared_gap_count: sharedGaps.length,
|
|
187
|
+
unique_techniques_per_actor: Object.fromEntries(Object.entries(uniqueTechniques).map(([k, v]) => [k, { count: v.length, techniques: v.slice(0, 20) }])),
|
|
188
|
+
recommendation: sharedGaps.length > 0
|
|
189
|
+
? `${sharedGaps.length} techniques are gaps across ALL compared actors — prioritize these for maximum coverage improvement.`
|
|
190
|
+
: 'No shared gaps — each actor has distinct coverage gaps.',
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
defineTool({
|
|
195
|
+
name: 'get_actor_profile',
|
|
196
|
+
description: 'Get full threat actor dossier: description, aliases, known techniques, software employed, and detection coverage status. Requires STIX data (ATTACK_STIX_PATH env var).',
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
actor_name: {
|
|
201
|
+
type: 'string',
|
|
202
|
+
description: 'Threat actor name or alias',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
required: ['actor_name'],
|
|
206
|
+
},
|
|
207
|
+
handler: async (args) => {
|
|
208
|
+
if (!isStixLoaded())
|
|
209
|
+
return stixNotLoadedError();
|
|
210
|
+
const actorName = args.actor_name;
|
|
211
|
+
const actor = getActorByName(actorName);
|
|
212
|
+
if (!actor) {
|
|
213
|
+
return {
|
|
214
|
+
error: true,
|
|
215
|
+
code: 'ACTOR_NOT_FOUND',
|
|
216
|
+
message: `Threat actor not found: "${actorName}". Use list_actors to browse available actors.`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const techniques = getActorTechniques(actor.actor_id);
|
|
220
|
+
const software = getSoftwareForActor(actor.actor_id);
|
|
221
|
+
const coverage = getActorCoverage(actor.actor_id);
|
|
222
|
+
return {
|
|
223
|
+
name: actor.name,
|
|
224
|
+
aliases: actor.aliases,
|
|
225
|
+
description: actor.description,
|
|
226
|
+
modified: actor.modified,
|
|
227
|
+
technique_count: techniques.length,
|
|
228
|
+
techniques: techniques.map(t => ({
|
|
229
|
+
technique_id: t.technique_id,
|
|
230
|
+
name: t.technique_name,
|
|
231
|
+
detection_count: t.detection_count,
|
|
232
|
+
covered: t.detection_count > 0,
|
|
233
|
+
tactics: t.tactics,
|
|
234
|
+
procedure_context: t.description ? t.description.substring(0, 300) : null,
|
|
235
|
+
})),
|
|
236
|
+
software: software.map(s => ({
|
|
237
|
+
name: s.name,
|
|
238
|
+
type: s.software_type,
|
|
239
|
+
platforms: s.platforms,
|
|
240
|
+
})),
|
|
241
|
+
coverage_summary: {
|
|
242
|
+
total_techniques: coverage.total_techniques,
|
|
243
|
+
covered: coverage.covered_count,
|
|
244
|
+
gaps: coverage.gap_count,
|
|
245
|
+
coverage_percentage: coverage.coverage_percentage,
|
|
246
|
+
by_tactic: coverage.by_tactic,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
];
|