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.
@@ -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
+ };