alert2action 1.0.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 +203 -0
- package/alert2action.cmd +2 -0
- package/bin/alert2action.js +77 -0
- package/examples/brute-force-alert.json +33 -0
- package/examples/credential-dump-alert.json +32 -0
- package/examples/lateral-movement-alert.json +29 -0
- package/examples/malware-alert-2.json +30 -0
- package/examples/malware-alert.json +35 -0
- package/examples/phishing-alert.json +28 -0
- package/examples/privesc-alert.json +112 -0
- package/examples/soc-test-alert.json +80 -0
- package/package.json +48 -0
- package/src/formatter.js +267 -0
- package/src/guide-generator.js +478 -0
- package/src/index.js +28 -0
- package/src/mitre.js +837 -0
- package/src/parser.js +309 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Investigation Guide Generator
|
|
3
|
+
* Builds comprehensive investigation guides from parsed alerts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { mapToMitre } = require('./mitre');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a complete investigation guide from a parsed alert
|
|
10
|
+
*/
|
|
11
|
+
function generateGuide(parsedAlert) {
|
|
12
|
+
const mitreMatches = mapToMitre(parsedAlert);
|
|
13
|
+
|
|
14
|
+
const guide = {
|
|
15
|
+
// Section 1: What Happened
|
|
16
|
+
whatHappened: generateWhatHappened(parsedAlert),
|
|
17
|
+
|
|
18
|
+
// Section 2: MITRE ATT&CK Mapping
|
|
19
|
+
mitreMapping: generateMitreSection(mitreMatches),
|
|
20
|
+
|
|
21
|
+
// Section 3: Logs to Check
|
|
22
|
+
logsToCheck: generateLogsToCheck(parsedAlert, mitreMatches),
|
|
23
|
+
|
|
24
|
+
// Section 4: Commands to Run
|
|
25
|
+
commands: generateCommands(parsedAlert, mitreMatches),
|
|
26
|
+
|
|
27
|
+
// Section 5: Containment Steps
|
|
28
|
+
containment: generateContainment(parsedAlert, mitreMatches),
|
|
29
|
+
|
|
30
|
+
// Section 6: False Positive Hints
|
|
31
|
+
falsePositives: generateFalsePositives(parsedAlert, mitreMatches),
|
|
32
|
+
|
|
33
|
+
// Metadata
|
|
34
|
+
severity: parsedAlert.severity,
|
|
35
|
+
alertTitle: generateSmartTitle(parsedAlert, mitreMatches),
|
|
36
|
+
timestamp: parsedAlert.timestamp || new Date().toISOString(),
|
|
37
|
+
indicators: parsedAlert.indicators
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return guide;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate a smart, descriptive alert title based on context
|
|
45
|
+
*/
|
|
46
|
+
function generateSmartTitle(alert, mitreMatches) {
|
|
47
|
+
// If we have a good title already (not just a process name), use it
|
|
48
|
+
if (alert.title && !alert.title.endsWith('.exe') && alert.title.length > 20) {
|
|
49
|
+
return alert.title;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parts = [];
|
|
53
|
+
|
|
54
|
+
// Check for encoded/obfuscated commands
|
|
55
|
+
const cmdLine = (alert.processCommandLine || '').toLowerCase();
|
|
56
|
+
if (cmdLine.includes('-enc') || cmdLine.includes('base64') || cmdLine.includes('-encoded')) {
|
|
57
|
+
parts.push('Encoded');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for suspicious processes
|
|
61
|
+
const procName = (alert.processName || '').toLowerCase();
|
|
62
|
+
if (procName.includes('powershell')) {
|
|
63
|
+
parts.push('PowerShell Execution');
|
|
64
|
+
} else if (procName.includes('cmd')) {
|
|
65
|
+
parts.push('Command Shell Execution');
|
|
66
|
+
} else if (procName.includes('wscript') || procName.includes('cscript')) {
|
|
67
|
+
parts.push('Script Execution');
|
|
68
|
+
} else if (alert.processName) {
|
|
69
|
+
parts.push(`${alert.processName} Execution`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for network activity
|
|
73
|
+
if (alert.destIp && isExternalIP(alert.destIp)) {
|
|
74
|
+
parts.push('with External Network Connection');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for privilege escalation keywords
|
|
78
|
+
const allText = [alert.title, alert.description, alert.category, alert.eventType].join(' ').toLowerCase();
|
|
79
|
+
if (allText.includes('privilege') || allText.includes('escalation') || allText.includes('system')) {
|
|
80
|
+
if (!parts.some(p => p.includes('Privilege'))) {
|
|
81
|
+
parts.unshift('Privilege Escalation:');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for credential access
|
|
86
|
+
if (allText.includes('lsass') || allText.includes('credential') || allText.includes('dump')) {
|
|
87
|
+
if (!parts.some(p => p.includes('Credential'))) {
|
|
88
|
+
parts.unshift('Credential Access:');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add top MITRE tactic if available
|
|
93
|
+
if (mitreMatches.length > 0 && parts.length < 3) {
|
|
94
|
+
const topTactic = mitreMatches[0].technique.tactic;
|
|
95
|
+
if (!parts.some(p => p.toLowerCase().includes(topTactic.toLowerCase()))) {
|
|
96
|
+
parts.push(`(${topTactic})`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fallback
|
|
101
|
+
if (parts.length === 0) {
|
|
102
|
+
return alert.title || alert.eventType || 'Security Alert';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return parts.join(' ');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate "What Happened" section - plain English summary
|
|
110
|
+
*/
|
|
111
|
+
function generateWhatHappened(alert) {
|
|
112
|
+
const parts = [];
|
|
113
|
+
|
|
114
|
+
// Build event description
|
|
115
|
+
if (alert.title) {
|
|
116
|
+
parts.push(`**Alert:** ${alert.title}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (alert.description) {
|
|
120
|
+
parts.push(`**Details:** ${alert.description}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Build context
|
|
124
|
+
const context = [];
|
|
125
|
+
|
|
126
|
+
if (alert.timestamp) {
|
|
127
|
+
context.push(`Detected at ${new Date(alert.timestamp).toLocaleString()}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (alert.hostname) {
|
|
131
|
+
context.push(`on host **${alert.hostname}**`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (alert.username) {
|
|
135
|
+
context.push(`involving user **${alert.username}**`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (context.length > 0) {
|
|
139
|
+
parts.push(context.join(' '));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Network context
|
|
143
|
+
if (alert.sourceIp || alert.destIp) {
|
|
144
|
+
const netContext = [];
|
|
145
|
+
if (alert.sourceIp) {
|
|
146
|
+
const isInternal = !isExternalIP(alert.sourceIp);
|
|
147
|
+
netContext.push(`Source IP: ${alert.sourceIp}${isInternal ? ' (internal - possible lateral movement or local execution)' : ''}`);
|
|
148
|
+
}
|
|
149
|
+
if (alert.destIp) {
|
|
150
|
+
const isExternal = isExternalIP(alert.destIp);
|
|
151
|
+
netContext.push(`Destination IP: ${alert.destIp}${isExternal ? ' (external - potential C2 or exfiltration)' : ''}`);
|
|
152
|
+
}
|
|
153
|
+
if (alert.protocol) netContext.push(`Protocol: ${alert.protocol}`);
|
|
154
|
+
parts.push(`**Network:** ${netContext.join(' | ')}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Process context
|
|
158
|
+
if (alert.processName || alert.processCommandLine) {
|
|
159
|
+
const procParts = [];
|
|
160
|
+
if (alert.processName) procParts.push(`Process: ${alert.processName}`);
|
|
161
|
+
if (alert.parentProcess) procParts.push(`Parent: ${alert.parentProcess}`);
|
|
162
|
+
if (alert.processCommandLine) {
|
|
163
|
+
// Truncate long command lines
|
|
164
|
+
const cmd = alert.processCommandLine.length > 150
|
|
165
|
+
? alert.processCommandLine.substring(0, 150) + '...'
|
|
166
|
+
: alert.processCommandLine;
|
|
167
|
+
procParts.push(`Command: \`${cmd}\``);
|
|
168
|
+
}
|
|
169
|
+
parts.push(`**Process:** ${procParts.join(' | ')}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// File context
|
|
173
|
+
if (alert.filePath || alert.fileHash) {
|
|
174
|
+
const fileParts = [];
|
|
175
|
+
if (alert.fileName || alert.filePath) fileParts.push(`File: ${alert.fileName || alert.filePath}`);
|
|
176
|
+
if (alert.fileHash) fileParts.push(`Hash: ${alert.fileHash}`);
|
|
177
|
+
parts.push(`**File:** ${fileParts.join(' | ')}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Severity assessment
|
|
181
|
+
const severityDesc = {
|
|
182
|
+
'critical': '🔴 **CRITICAL** - Immediate action required!',
|
|
183
|
+
'high': '🟠 **HIGH** - Urgent investigation needed',
|
|
184
|
+
'medium': '🟡 **MEDIUM** - Investigate promptly',
|
|
185
|
+
'low': '🟢 **LOW** - Review when possible',
|
|
186
|
+
'informational': 'ℹ️ **INFO** - For awareness only'
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
parts.push(severityDesc[alert.severity] || severityDesc['medium']);
|
|
190
|
+
|
|
191
|
+
return parts;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Generate MITRE ATT&CK mapping section
|
|
196
|
+
*/
|
|
197
|
+
function generateMitreSection(mitreMatches) {
|
|
198
|
+
if (mitreMatches.length === 0) {
|
|
199
|
+
return [{
|
|
200
|
+
id: 'Unknown',
|
|
201
|
+
name: 'No MITRE Technique Identified',
|
|
202
|
+
tactic: 'N/A',
|
|
203
|
+
confidence: 'low',
|
|
204
|
+
description: 'Unable to map this alert to a specific MITRE ATT&CK technique. Manual analysis recommended.'
|
|
205
|
+
}];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return mitreMatches.map(match => ({
|
|
209
|
+
id: match.technique.id,
|
|
210
|
+
name: match.technique.name,
|
|
211
|
+
tactic: match.technique.tactic,
|
|
212
|
+
confidence: match.confidence,
|
|
213
|
+
description: match.technique.description,
|
|
214
|
+
matchedKeywords: match.matchedKeywords,
|
|
215
|
+
url: `https://attack.mitre.org/techniques/${match.technique.id.replace('.', '/')}/`
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Generate logs to check section
|
|
221
|
+
*/
|
|
222
|
+
function generateLogsToCheck(alert, mitreMatches) {
|
|
223
|
+
const logs = new Set();
|
|
224
|
+
|
|
225
|
+
// Add technique-specific logs
|
|
226
|
+
for (const match of mitreMatches) {
|
|
227
|
+
if (match.technique.logsToCheck) {
|
|
228
|
+
match.technique.logsToCheck.forEach(log => logs.add(log));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Add context-based logs
|
|
233
|
+
if (alert.hostname) {
|
|
234
|
+
logs.add('Endpoint security logs (EDR/AV)');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (alert.sourceIp || alert.destIp) {
|
|
238
|
+
logs.add('Firewall connection logs');
|
|
239
|
+
logs.add('Network flow data (NetFlow/IPFIX)');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (alert.username) {
|
|
243
|
+
logs.add('Active Directory/LDAP logs');
|
|
244
|
+
logs.add('Identity provider logs (Azure AD, Okta, etc.)');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (alert.processName || alert.processCommandLine) {
|
|
248
|
+
logs.add('Process creation logs (Sysmon Event ID 1, Security 4688)');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Always recommend
|
|
252
|
+
logs.add('SIEM correlation rules for related events');
|
|
253
|
+
|
|
254
|
+
return Array.from(logs);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Generate investigation commands
|
|
259
|
+
*/
|
|
260
|
+
function generateCommands(alert, mitreMatches) {
|
|
261
|
+
// Detect if this is a Windows-specific alert
|
|
262
|
+
const isWindowsAlert = detectWindowsContext(alert);
|
|
263
|
+
|
|
264
|
+
const commands = {
|
|
265
|
+
windows: [],
|
|
266
|
+
linux: [],
|
|
267
|
+
linuxNote: isWindowsAlert ? '(Cross-platform reference - use if environment includes Linux/Mac)' : null
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Add technique-specific commands
|
|
271
|
+
for (const match of mitreMatches) {
|
|
272
|
+
if (match.technique.commands) {
|
|
273
|
+
if (match.technique.commands.windows) {
|
|
274
|
+
commands.windows.push(...match.technique.commands.windows);
|
|
275
|
+
}
|
|
276
|
+
if (match.technique.commands.linux) {
|
|
277
|
+
commands.linux.push(...match.technique.commands.linux);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Add context-specific commands
|
|
283
|
+
if (alert.sourceIp) {
|
|
284
|
+
commands.windows.push(`# Check connections from source IP ${alert.sourceIp}`);
|
|
285
|
+
commands.windows.push(`Get-NetTCPConnection | Where-Object {$_.RemoteAddress -eq "${alert.sourceIp}"}`);
|
|
286
|
+
commands.linux.push(`# Check connections from source IP ${alert.sourceIp}`);
|
|
287
|
+
commands.linux.push(`netstat -an | grep "${alert.sourceIp}"`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (alert.username) {
|
|
291
|
+
commands.windows.push(`# Get recent activity for user ${alert.username}`);
|
|
292
|
+
commands.windows.push(`Get-WinEvent -FilterHashtable @{LogName="Security";Id=4624,4625,4648} | Where-Object {$_.Message -match "${alert.username}"} | Select-Object -First 20`);
|
|
293
|
+
commands.linux.push(`# Get recent activity for user ${alert.username}`);
|
|
294
|
+
commands.linux.push(`grep "${alert.username}" /var/log/auth.log | tail -50`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (alert.hostname) {
|
|
298
|
+
commands.windows.push(`# Quick system health check on ${alert.hostname}`);
|
|
299
|
+
commands.windows.push(`Get-Process | Sort-Object CPU -Descending | Select-Object -First 10`);
|
|
300
|
+
commands.windows.push(`Get-Service | Where-Object {$_.Status -eq "Running" -and $_.StartType -eq "Automatic"}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (alert.processName) {
|
|
304
|
+
commands.windows.push(`# Find all instances of suspicious process`);
|
|
305
|
+
commands.windows.push(`Get-Process -Name "${alert.processName.replace('.exe', '')}" -ErrorAction SilentlyContinue | Select-Object Id,Name,Path,StartTime`);
|
|
306
|
+
commands.linux.push(`# Find all instances of suspicious process`);
|
|
307
|
+
commands.linux.push(`ps aux | grep -i "${alert.processName}"`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (alert.fileHash) {
|
|
311
|
+
commands.windows.push(`# Search for file by hash (requires PowerShell 4.0+)`);
|
|
312
|
+
commands.windows.push(`Get-ChildItem -Path C:\\ -Recurse -File -ErrorAction SilentlyContinue | Get-FileHash | Where-Object {$_.Hash -eq "${alert.fileHash}"}`);
|
|
313
|
+
commands.linux.push(`# Search for file by hash`);
|
|
314
|
+
commands.linux.push(`find / -type f -exec sha256sum {} \\; 2>/dev/null | grep "${alert.fileHash}"`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Deduplicate
|
|
318
|
+
commands.windows = [...new Set(commands.windows)];
|
|
319
|
+
commands.linux = [...new Set(commands.linux)];
|
|
320
|
+
|
|
321
|
+
return commands;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Detect if alert is Windows-specific based on context
|
|
326
|
+
*/
|
|
327
|
+
function detectWindowsContext(alert) {
|
|
328
|
+
const windowsIndicators = [
|
|
329
|
+
// Process paths
|
|
330
|
+
alert.processPath?.includes('C:\\'),
|
|
331
|
+
alert.processPath?.includes('Windows'),
|
|
332
|
+
// Process names
|
|
333
|
+
alert.processName?.endsWith('.exe'),
|
|
334
|
+
alert.processName?.toLowerCase().includes('powershell'),
|
|
335
|
+
alert.processName?.toLowerCase().includes('schtasks'),
|
|
336
|
+
alert.processName?.toLowerCase().includes('cmd.exe'),
|
|
337
|
+
// Command line
|
|
338
|
+
alert.processCommandLine?.includes('C:\\'),
|
|
339
|
+
alert.processCommandLine?.includes('powershell'),
|
|
340
|
+
// Hostname patterns
|
|
341
|
+
alert.hostname?.includes('.local'),
|
|
342
|
+
alert.hostname?.match(/^[A-Z]+-?[A-Z0-9]+$/i), // WORKSTATION-01 pattern
|
|
343
|
+
// Source indicators
|
|
344
|
+
alert.source?.toLowerCase().includes('defender'),
|
|
345
|
+
alert.source?.toLowerCase().includes('windows'),
|
|
346
|
+
alert.source?.toLowerCase().includes('sysmon')
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
return windowsIndicators.filter(Boolean).length >= 2;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Generate containment steps
|
|
354
|
+
*/
|
|
355
|
+
function generateContainment(alert, mitreMatches) {
|
|
356
|
+
const steps = [];
|
|
357
|
+
const priority = {
|
|
358
|
+
immediate: [],
|
|
359
|
+
shortTerm: [],
|
|
360
|
+
longTerm: []
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Severity-based immediate actions
|
|
364
|
+
if (alert.severity === 'critical') {
|
|
365
|
+
priority.immediate.push('🚨 CRITICAL: Escalate to incident commander immediately');
|
|
366
|
+
priority.immediate.push('Consider isolating affected endpoint from network');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Add technique-specific containment
|
|
370
|
+
for (const match of mitreMatches) {
|
|
371
|
+
if (match.technique.containment) {
|
|
372
|
+
match.technique.containment.forEach((step, idx) => {
|
|
373
|
+
if (idx === 0) {
|
|
374
|
+
priority.immediate.push(step);
|
|
375
|
+
} else if (idx < 3) {
|
|
376
|
+
priority.shortTerm.push(step);
|
|
377
|
+
} else {
|
|
378
|
+
priority.longTerm.push(step);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Context-based containment
|
|
385
|
+
if (alert.sourceIp && isExternalIP(alert.sourceIp)) {
|
|
386
|
+
priority.immediate.push(`Block source IP ${alert.sourceIp} at perimeter firewall`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (alert.username) {
|
|
390
|
+
priority.shortTerm.push(`Review account ${alert.username} for compromise indicators`);
|
|
391
|
+
priority.shortTerm.push(`Consider temporary account lockout if suspicious`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (alert.hostname) {
|
|
395
|
+
priority.shortTerm.push(`Collect forensic image of ${alert.hostname} if needed`);
|
|
396
|
+
priority.shortTerm.push('Preserve volatile data (memory, network connections)');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Deduplicate
|
|
400
|
+
priority.immediate = [...new Set(priority.immediate)];
|
|
401
|
+
priority.shortTerm = [...new Set(priority.shortTerm)];
|
|
402
|
+
priority.longTerm = [...new Set(priority.longTerm)];
|
|
403
|
+
|
|
404
|
+
// Combine with headers
|
|
405
|
+
if (priority.immediate.length > 0) {
|
|
406
|
+
steps.push({ phase: 'Immediate (0-30 min)', actions: priority.immediate.slice(0, 5) });
|
|
407
|
+
}
|
|
408
|
+
if (priority.shortTerm.length > 0) {
|
|
409
|
+
steps.push({ phase: 'Short-term (1-4 hours)', actions: priority.shortTerm.slice(0, 5) });
|
|
410
|
+
}
|
|
411
|
+
if (priority.longTerm.length > 0) {
|
|
412
|
+
steps.push({ phase: 'Long-term (Post-incident)', actions: priority.longTerm.slice(0, 3) });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return steps;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Generate false positive hints
|
|
420
|
+
*/
|
|
421
|
+
function generateFalsePositives(alert, mitreMatches) {
|
|
422
|
+
const hints = [];
|
|
423
|
+
|
|
424
|
+
// Add technique-specific false positives
|
|
425
|
+
for (const match of mitreMatches) {
|
|
426
|
+
if (match.technique.falsePositives) {
|
|
427
|
+
hints.push(...match.technique.falsePositives);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Add general investigation questions
|
|
432
|
+
hints.push('--- Investigation Questions ---');
|
|
433
|
+
|
|
434
|
+
if (alert.username) {
|
|
435
|
+
hints.push(`Is ${alert.username} a legitimate admin or service account?`);
|
|
436
|
+
hints.push('Was this activity during normal working hours for this user?');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (alert.hostname) {
|
|
440
|
+
hints.push(`Is ${alert.hostname} a development/test machine where this behavior is expected?`);
|
|
441
|
+
hints.push('Is there scheduled maintenance or patching occurring?');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (alert.processName) {
|
|
445
|
+
hints.push(`Is ${alert.processName} part of approved software inventory?`);
|
|
446
|
+
hints.push('Is this a known IT management or security tool?');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (alert.sourceIp) {
|
|
450
|
+
hints.push('Is the source IP from a known corporate or VPN range?');
|
|
451
|
+
hints.push('Is this a known penetration testing or vulnerability scanning source?');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return [...new Set(hints)];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Simple check if IP is likely external (not RFC1918)
|
|
459
|
+
*/
|
|
460
|
+
function isExternalIP(ip) {
|
|
461
|
+
if (!ip) return false;
|
|
462
|
+
|
|
463
|
+
// Common internal ranges
|
|
464
|
+
const internalPatterns = [
|
|
465
|
+
/^10\./,
|
|
466
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
467
|
+
/^192\.168\./,
|
|
468
|
+
/^127\./,
|
|
469
|
+
/^169\.254\./,
|
|
470
|
+
/^::1$/,
|
|
471
|
+
/^fc00:/,
|
|
472
|
+
/^fe80:/
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
return !internalPatterns.some(pattern => pattern.test(ip));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = { generateGuide };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alert2action - Main Module
|
|
3
|
+
* Exports core functionality for programmatic use
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { parseAlert } = require('./parser');
|
|
7
|
+
const { generateGuide } = require('./guide-generator');
|
|
8
|
+
const { formatOutput } = require('./formatter');
|
|
9
|
+
const { mapToMitre, getTechnique, getAllTechniques } = require('./mitre');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
// Core functions
|
|
13
|
+
parseAlert,
|
|
14
|
+
generateGuide,
|
|
15
|
+
formatOutput,
|
|
16
|
+
|
|
17
|
+
// MITRE utilities
|
|
18
|
+
mapToMitre,
|
|
19
|
+
getTechnique,
|
|
20
|
+
getAllTechniques,
|
|
21
|
+
|
|
22
|
+
// Convenience function - all in one
|
|
23
|
+
analyze: function (alertJson, options = {}) {
|
|
24
|
+
const parsed = parseAlert(alertJson);
|
|
25
|
+
const guide = generateGuide(parsed);
|
|
26
|
+
return options.raw ? guide : formatOutput(guide, options);
|
|
27
|
+
}
|
|
28
|
+
};
|