n8n Incident Response Automation: Building Security Playbooks
Automating incident response reduces mean time to detect and respond to security threats. This guide shows how to build comprehensive security playbooks with n8n.
Alert Ingestion and Triage
Multi-Source Alert Collector
// n8n Function Node - Alert Normalizer
const rawAlert = $input.first().json;
const sourceType = rawAlert.source || 'unknown';
// Normalize alerts from different sources to common format
const normalizers = {
splunk: (alert) => ({
id: alert.sid || `splunk_${Date.now()}`,
source: 'splunk',
timestamp: alert.trigger_time || new Date().toISOString(),
title: alert.search_name,
description: alert.description || alert.search_name,
severity: mapSplunkSeverity(alert.severity),
category: alert.category || 'unknown',
raw_data: alert.result || {},
indicators: extractIndicators(alert.result)
}),
crowdstrike: (alert) => ({
id: alert.detection_id,
source: 'crowdstrike',
timestamp: alert.created_timestamp,
title: alert.tactic + ': ' + alert.technique,
description: alert.description,
severity: mapCrowdstrikeSeverity(alert.max_severity),
category: 'endpoint',
raw_data: alert,
indicators: {
hostnames: [alert.device?.hostname],
ips: [alert.device?.local_ip],
users: [alert.user_name],
processes: [alert.process?.file_name],
hashes: [alert.process?.sha256]
}
}),
palo_alto: (alert) => ({
id: alert.seqno || `pan_${Date.now()}`,
source: 'palo_alto',
timestamp: alert.receive_time,
title: alert.threatid_name || alert.rule,
description: alert.misc || alert.threatid_name,
severity: mapPaloAltoSeverity(alert.severity),
category: alert.type || 'network',
raw_data: alert,
indicators: {
src_ip: alert.src,
dst_ip: alert.dst,
src_port: alert.sport,
dst_port: alert.dport,
url: alert.url,
application: alert.app
}
}),
azure_sentinel: (alert) => ({
id: alert.SystemAlertId,
source: 'azure_sentinel',
timestamp: alert.TimeGenerated,
title: alert.AlertName,
description: alert.Description,
severity: alert.AlertSeverity.toLowerCase(),
category: alert.Category,
raw_data: alert,
indicators: {
entities: alert.Entities,
tactics: alert.Tactics,
techniques: alert.Techniques
}
}),
custom_webhook: (alert) => ({
id: alert.alert_id || `custom_${Date.now()}`,
source: alert.source_name || 'custom',
timestamp: alert.timestamp || new Date().toISOString(),
title: alert.title,
description: alert.description,
severity: alert.severity || 'medium',
category: alert.category || 'custom',
raw_data: alert,
indicators: alert.indicators || {}
})
};
function mapSplunkSeverity(severity) {
const mapping = { '1': 'info', '2': 'low', '3': 'medium', '4': 'high', '5': 'critical' };
return mapping[severity] || 'medium';
}
function mapCrowdstrikeSeverity(severity) {
if (severity >= 80) return 'critical';
if (severity >= 60) return 'high';
if (severity >= 40) return 'medium';
if (severity >= 20) return 'low';
return 'info';
}
function mapPaloAltoSeverity(severity) {
const mapping = {
'informational': 'info',
'low': 'low',
'medium': 'medium',
'high': 'high',
'critical': 'critical'
};
return mapping[severity?.toLowerCase()] || 'medium';
}
function extractIndicators(data) {
const indicators = {
ips: [],
domains: [],
hashes: [],
emails: [],
urls: []
};
const text = JSON.stringify(data);
// Extract IPs
const ipRegex = /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g;
indicators.ips = [...new Set(text.match(ipRegex) || [])];
// Extract domains
const domainRegex = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
indicators.domains = [...new Set(text.match(domainRegex) || [])];
// Extract hashes (MD5, SHA1, SHA256)
const hashRegex = /\b[a-fA-F0-9]{32}\b|\b[a-fA-F0-9]{40}\b|\b[a-fA-F0-9]{64}\b/g;
indicators.hashes = [...new Set(text.match(hashRegex) || [])];
return indicators;
}
// Normalize the incoming alert
const normalizer = normalizers[sourceType] || normalizers.custom_webhook;
const normalizedAlert = normalizer(rawAlert);
// Add enrichment metadata
normalizedAlert.received_at = new Date().toISOString();
normalizedAlert.status = 'new';
normalizedAlert.workflow_id = $execution.id;
return { json: normalizedAlert };Alert Triage and Prioritization
// n8n Function Node - Alert Triage Engine
const alert = $input.first().json;
// Triage configuration
const triageConfig = {
severityWeights: {
critical: 100,
high: 75,
medium: 50,
low: 25,
info: 10
},
categoryWeights: {
ransomware: 50,
data_exfiltration: 45,
malware: 40,
intrusion: 35,
phishing: 30,
authentication: 25,
network: 20,
endpoint: 15,
custom: 10,
unknown: 5
},
assetCriticality: {
'domain_controller': 50,
'database_server': 45,
'web_server': 30,
'workstation': 15,
'unknown': 10
},
timeDecay: {
enabled: true,
halfLife: 3600000 // 1 hour in ms
}
};
// Calculate priority score
function calculatePriorityScore(alert, config) {
let score = 0;
// Base severity score
score += config.severityWeights[alert.severity] || 25;
// Category weight
score += config.categoryWeights[alert.category] || 10;
// Asset criticality (if available)
const assetType = alert.raw_data?.asset_type || 'unknown';
score += config.assetCriticality[assetType] || 10;
// Boost for multiple indicators
const indicatorCount = Object.values(alert.indicators || {})
.flat()
.filter(i => i).length;
if (indicatorCount > 5) score += 20;
else if (indicatorCount > 2) score += 10;
// Time decay for older alerts
if (config.timeDecay.enabled) {
const alertAge = Date.now() - new Date(alert.timestamp).getTime();
const decayFactor = Math.exp(-alertAge / config.timeDecay.halfLife);
score = score * (0.5 + 0.5 * decayFactor);
}
return Math.round(score);
}
// Determine response actions based on triage
function determineResponseActions(alert, score) {
const actions = [];
// Critical and high severity always get immediate attention
if (alert.severity === 'critical' || score >= 150) {
actions.push({
type: 'escalate',
target: 'security_lead',
urgency: 'immediate'
});
actions.push({
type: 'page',
target: 'on_call',
message: `Critical alert: ${alert.title}`
});
}
// Auto-enrichment for all alerts
actions.push({
type: 'enrich',
sources: ['virustotal', 'shodan', 'abuseipdb']
});
// Auto-containment for specific categories
if (['ransomware', 'malware'].includes(alert.category)) {
actions.push({
type: 'containment',
action: 'isolate_endpoint',
target: alert.indicators?.hostnames?.[0]
});
}
// Create ticket for tracking
actions.push({
type: 'create_ticket',
priority: score >= 100 ? 'high' : score >= 50 ? 'medium' : 'low'
});
return actions;
}
// Check for correlation with existing incidents
function checkCorrelation(alert, existingAlerts) {
const correlations = [];
for (const existing of existingAlerts) {
// Same source IP
const alertIPs = alert.indicators?.ips || [];
const existingIPs = existing.indicators?.ips || [];
const ipOverlap = alertIPs.filter(ip => existingIPs.includes(ip));
if (ipOverlap.length > 0) {
correlations.push({
type: 'ip_correlation',
related_alert: existing.id,
matching_ips: ipOverlap
});
}
// Same host
const alertHosts = alert.indicators?.hostnames || [];
const existingHosts = existing.indicators?.hostnames || [];
const hostOverlap = alertHosts.filter(h => existingHosts.includes(h));
if (hostOverlap.length > 0) {
correlations.push({
type: 'host_correlation',
related_alert: existing.id,
matching_hosts: hostOverlap
});
}
// Similar timeframe (within 1 hour)
const timeDiff = Math.abs(
new Date(alert.timestamp) - new Date(existing.timestamp)
);
if (timeDiff < 3600000) {
correlations.push({
type: 'temporal_correlation',
related_alert: existing.id,
time_difference_ms: timeDiff
});
}
}
return correlations;
}
// Get existing alerts from context (would come from previous node)
const existingAlerts = $('Get Recent Alerts').all().map(i => i.json);
// Perform triage
const priorityScore = calculatePriorityScore(alert, triageConfig);
const responseActions = determineResponseActions(alert, priorityScore);
const correlations = checkCorrelation(alert, existingAlerts);
// Build triage result
const triageResult = {
alert: alert,
triage: {
priority_score: priorityScore,
priority_level: priorityScore >= 150 ? 'P1' :
priorityScore >= 100 ? 'P2' :
priorityScore >= 50 ? 'P3' : 'P4',
response_actions: responseActions,
correlations: correlations,
correlation_count: correlations.length,
is_correlated: correlations.length > 0,
triaged_at: new Date().toISOString()
}
};
return { json: triageResult };Indicator Enrichment
// n8n Function Node - Enrichment Aggregator
const triageResult = $input.first().json;
const alert = triageResult.alert;
// Collect enrichment results from parallel queries
const vtResults = $('VirusTotal Query').first()?.json || {};
const shodanResults = $('Shodan Query').first()?.json || {};
const abuseResults = $('AbuseIPDB Query').first()?.json || {};
const internalResults = $('Internal Asset Lookup').first()?.json || {};
// Process enrichments
const enrichments = {
threat_intel: {
virustotal: processVirusTotalResults(vtResults),
shodan: processShodanResults(shodanResults),
abuseipdb: processAbuseIPDBResults(abuseResults)
},
internal: {
asset_info: internalResults.asset || null,
user_info: internalResults.user || null,
recent_activity: internalResults.activity || []
},
risk_indicators: [],
enriched_at: new Date().toISOString()
};
function processVirusTotalResults(results) {
if (!results.data) return null;
const data = results.data.attributes || {};
return {
reputation: data.reputation,
malicious_count: data.last_analysis_stats?.malicious || 0,
suspicious_count: data.last_analysis_stats?.suspicious || 0,
harmless_count: data.last_analysis_stats?.harmless || 0,
categories: data.categories,
last_analysis_date: data.last_analysis_date,
is_malicious: (data.last_analysis_stats?.malicious || 0) > 3
};
}
function processShodanResults(results) {
if (!results.ip) return null;
return {
ip: results.ip,
organization: results.org,
country: results.country_name,
city: results.city,
asn: results.asn,
open_ports: results.ports || [],
hostnames: results.hostnames || [],
vulnerabilities: results.vulns || [],
is_cloud: isCloudProvider(results.org),
is_vpn: isVPNProvider(results.org)
};
}
function processAbuseIPDBResults(results) {
if (!results.data) return null;
const data = results.data;
return {
ip: data.ipAddress,
abuse_confidence: data.abuseConfidenceScore,
is_public: data.isPublic,
is_whitelisted: data.isWhitelisted,
country: data.countryCode,
isp: data.isp,
domain: data.domain,
total_reports: data.totalReports,
last_reported_at: data.lastReportedAt,
is_abusive: data.abuseConfidenceScore > 50
};
}
function isCloudProvider(org) {
const cloudProviders = ['amazon', 'aws', 'google', 'microsoft', 'azure', 'digitalocean'];
return cloudProviders.some(p => org?.toLowerCase().includes(p));
}
function isVPNProvider(org) {
const vpnProviders = ['nordvpn', 'expressvpn', 'private internet', 'mullvad'];
return vpnProviders.some(p => org?.toLowerCase().includes(p));
}
// Calculate risk indicators based on enrichments
function calculateRiskIndicators(enrichments) {
const indicators = [];
// VirusTotal indicators
if (enrichments.threat_intel.virustotal?.is_malicious) {
indicators.push({
type: 'known_malicious',
source: 'virustotal',
severity: 'high',
description: 'Indicator flagged as malicious by multiple AV engines'
});
}
// Shodan indicators
const shodan = enrichments.threat_intel.shodan;
if (shodan?.vulnerabilities?.length > 0) {
indicators.push({
type: 'known_vulnerabilities',
source: 'shodan',
severity: 'medium',
description: `Host has ${shodan.vulnerabilities.length} known vulnerabilities`
});
}
// AbuseIPDB indicators
if (enrichments.threat_intel.abuseipdb?.is_abusive) {
indicators.push({
type: 'abuse_reported',
source: 'abuseipdb',
severity: 'high',
description: `IP reported for abuse (confidence: ${enrichments.threat_intel.abuseipdb.abuse_confidence}%)`
});
}
// VPN/Cloud indicators
if (shodan?.is_vpn) {
indicators.push({
type: 'vpn_detected',
source: 'shodan',
severity: 'low',
description: 'Traffic originates from known VPN provider'
});
}
return indicators;
}
enrichments.risk_indicators = calculateRiskIndicators(enrichments);
// Update triage with enrichment
const enrichedResult = {
...triageResult,
enrichments: enrichments,
enrichment_summary: {
sources_queried: 4,
risk_indicator_count: enrichments.risk_indicators.length,
high_risk_indicators: enrichments.risk_indicators.filter(i => i.severity === 'high').length,
is_known_malicious: enrichments.threat_intel.virustotal?.is_malicious ||
enrichments.threat_intel.abuseipdb?.is_abusive
}
};
return { json: enrichedResult };Automated Response Actions
Containment Actions
// n8n Function Node - Containment Decision Engine
const enrichedAlert = $input.first().json;
const alert = enrichedAlert.alert;
const triage = enrichedAlert.triage;
const enrichments = enrichedAlert.enrichments;
// Containment policy configuration
const containmentPolicy = {
auto_contain: {
enabled: true,
conditions: {
min_priority_score: 100,
require_malicious_confirmation: true,
excluded_assets: ['domain_controller', 'critical_server'],
business_hours_only: false
}
},
actions: {
isolate_endpoint: {
enabled: true,
providers: ['crowdstrike', 'carbon_black', 'sentinel_one']
},
block_ip: {
enabled: true,
firewalls: ['palo_alto', 'fortinet']
},
disable_user: {
enabled: true,
idp: 'okta'
},
quarantine_email: {
enabled: true,
provider: 'microsoft_365'
}
}
};
// Determine containment actions
function determineContainmentActions(alert, triage, enrichments, policy) {
const actions = [];
// Check if auto-containment is enabled and conditions are met
if (!policy.auto_contain.enabled) {
return [{
type: 'manual_review',
reason: 'Auto-containment disabled',
recommended_actions: suggestContainmentActions(alert, enrichments)
}];
}
// Check priority threshold
if (triage.priority_score < policy.auto_contain.conditions.min_priority_score) {
return [{
type: 'monitor_only',
reason: 'Priority below containment threshold',
priority_score: triage.priority_score,
threshold: policy.auto_contain.conditions.min_priority_score
}];
}
// Check for malicious confirmation if required
if (policy.auto_contain.conditions.require_malicious_confirmation) {
if (!enrichments.enrichment_summary.is_known_malicious) {
return [{
type: 'pending_confirmation',
reason: 'Waiting for malicious confirmation',
recommended_actions: suggestContainmentActions(alert, enrichments)
}];
}
}
// Check for excluded assets
const assetType = enrichments.internal.asset_info?.type;
if (policy.auto_contain.conditions.excluded_assets.includes(assetType)) {
return [{
type: 'manual_review',
reason: `Asset type '${assetType}' excluded from auto-containment`,
recommended_actions: suggestContainmentActions(alert, enrichments)
}];
}
// Determine specific containment actions based on alert type
if (alert.category === 'endpoint' || alert.category === 'malware') {
const hostname = alert.indicators?.hostnames?.[0];
if (hostname && policy.actions.isolate_endpoint.enabled) {
actions.push({
type: 'isolate_endpoint',
target: hostname,
provider: policy.actions.isolate_endpoint.providers[0],
severity: 'high',
reversible: true
});
}
}
// Block malicious IPs
const maliciousIPs = (alert.indicators?.ips || []).filter(ip => {
// Check if IP is confirmed malicious
return enrichments.threat_intel.abuseipdb?.is_abusive ||
enrichments.threat_intel.virustotal?.is_malicious;
});
if (maliciousIPs.length > 0 && policy.actions.block_ip.enabled) {
for (const ip of maliciousIPs) {
actions.push({
type: 'block_ip',
target: ip,
firewall: policy.actions.block_ip.firewalls[0],
duration: 86400, // 24 hours
reversible: true
});
}
}
// Disable compromised user
if (alert.category === 'authentication' || alert.category === 'phishing') {
const user = alert.indicators?.users?.[0];
if (user && policy.actions.disable_user.enabled) {
actions.push({
type: 'disable_user',
target: user,
provider: policy.actions.disable_user.idp,
reason: `Security incident: ${alert.id}`,
reversible: true
});
}
}
return actions;
}
function suggestContainmentActions(alert, enrichments) {
const suggestions = [];
if (alert.indicators?.hostnames?.length > 0) {
suggestions.push({
action: 'isolate_endpoint',
target: alert.indicators.hostnames[0],
justification: 'Endpoint may be compromised'
});
}
if (alert.indicators?.ips?.length > 0) {
suggestions.push({
action: 'block_ip',
target: alert.indicators.ips[0],
justification: 'Suspicious network activity detected'
});
}
return suggestions;
}
// Execute containment decision
const containmentActions = determineContainmentActions(
alert,
triage,
enrichments,
containmentPolicy
);
const containmentResult = {
...enrichedAlert,
containment: {
decision_time: new Date().toISOString(),
actions: containmentActions,
auto_contained: containmentActions.some(a =>
['isolate_endpoint', 'block_ip', 'disable_user'].includes(a.type)
),
requires_manual_review: containmentActions.some(a =>
['manual_review', 'pending_confirmation'].includes(a.type)
)
}
};
// Output different paths for automation vs manual review
if (containmentResult.containment.auto_contained) {
return [{ json: { ...containmentResult, path: 'auto_response' } }];
} else {
return [{ json: { ...containmentResult, path: 'manual_review' } }];
}Notification and Escalation
// n8n Function Node - Notification Builder
const incidentData = $input.first().json;
const alert = incidentData.alert;
const triage = incidentData.triage;
const containment = incidentData.containment;
// Build notification for different channels
const notifications = {
slack: buildSlackNotification(incidentData),
email: buildEmailNotification(incidentData),
pagerduty: buildPagerDutyNotification(incidentData),
teams: buildTeamsNotification(incidentData),
ticket: buildTicketContent(incidentData)
};
function buildSlackNotification(data) {
const severityEmoji = {
critical: '🔴',
high: '🟠',
medium: '🟡',
low: '🟢',
info: '🔵'
};
const blocks = [
{
type: 'header',
text: {
type: 'plain_text',
text: `${severityEmoji[data.alert.severity]} Security Alert: ${data.alert.title}`
}
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Severity:*\n${data.alert.severity.toUpperCase()}` },
{ type: 'mrkdwn', text: `*Priority:*\n${data.triage.priority_level}` },
{ type: 'mrkdwn', text: `*Source:*\n${data.alert.source}` },
{ type: 'mrkdwn', text: `*Category:*\n${data.alert.category}` }
]
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Description:*\n${data.alert.description}`
}
}
];
// Add indicators
if (Object.keys(data.alert.indicators || {}).length > 0) {
const indicatorText = Object.entries(data.alert.indicators)
.filter(([k, v]) => v && (Array.isArray(v) ? v.length > 0 : true))
.map(([k, v]) => `• *${k}:* ${Array.isArray(v) ? v.join(', ') : v}`)
.join('\n');
blocks.push({
type: 'section',
text: { type: 'mrkdwn', text: `*Indicators:*\n${indicatorText}` }
});
}
// Add containment actions
if (data.containment?.actions?.length > 0) {
const actionText = data.containment.actions
.map(a => `• ${a.type}: ${a.target || 'N/A'} (${a.type === 'manual_review' ? '⚠️ Manual' : '✅ Auto'})`)
.join('\n');
blocks.push({
type: 'section',
text: { type: 'mrkdwn', text: `*Response Actions:*\n${actionText}` }
});
}
// Add action buttons
blocks.push({
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: '📋 View Details' },
url: `https://siem.company.com/alerts/${data.alert.id}`,
action_id: 'view_details'
},
{
type: 'button',
text: { type: 'plain_text', text: '✅ Acknowledge' },
style: 'primary',
action_id: 'acknowledge',
value: data.alert.id
},
{
type: 'button',
text: { type: 'plain_text', text: '🚨 Escalate' },
style: 'danger',
action_id: 'escalate',
value: data.alert.id
}
]
});
return {
channel: getSlackChannel(data.triage.priority_level),
blocks: blocks,
text: `Security Alert: ${data.alert.title}` // Fallback
};
}
function buildEmailNotification(data) {
return {
to: getEmailRecipients(data.triage.priority_level),
subject: `[${data.alert.severity.toUpperCase()}] Security Alert: ${data.alert.title}`,
html: `
<h2>Security Alert</h2>
<p><strong>Alert ID:</strong> ${data.alert.id}</p>
<p><strong>Severity:</strong> ${data.alert.severity}</p>
<p><strong>Priority:</strong> ${data.triage.priority_level}</p>
<p><strong>Source:</strong> ${data.alert.source}</p>
<p><strong>Category:</strong> ${data.alert.category}</p>
<p><strong>Description:</strong> ${data.alert.description}</p>
<h3>Indicators</h3>
<ul>
${Object.entries(data.alert.indicators || {})
.filter(([k, v]) => v && (Array.isArray(v) ? v.length > 0 : true))
.map(([k, v]) => `<li><strong>${k}:</strong> ${Array.isArray(v) ? v.join(', ') : v}</li>`)
.join('')}
</ul>
<h3>Response Actions</h3>
<ul>
${(data.containment?.actions || [])
.map(a => `<li>${a.type}: ${a.target || 'N/A'}</li>`)
.join('')}
</ul>
<p><a href="https://siem.company.com/alerts/${data.alert.id}">View in SIEM</a></p>
`
};
}
function buildPagerDutyNotification(data) {
return {
routing_key: process.env.PAGERDUTY_ROUTING_KEY,
event_action: 'trigger',
dedup_key: data.alert.id,
payload: {
summary: `${data.alert.severity.toUpperCase()}: ${data.alert.title}`,
severity: mapToPagerDutySeverity(data.alert.severity),
source: data.alert.source,
timestamp: data.alert.timestamp,
custom_details: {
alert_id: data.alert.id,
category: data.alert.category,
priority: data.triage.priority_level,
indicators: data.alert.indicators
}
},
links: [{
href: `https://siem.company.com/alerts/${data.alert.id}`,
text: 'View in SIEM'
}]
};
}
function buildTeamsNotification(data) {
return {
'@type': 'MessageCard',
'@context': 'http://schema.org/extensions',
themeColor: getSeverityColor(data.alert.severity),
summary: `Security Alert: ${data.alert.title}`,
sections: [{
activityTitle: `Security Alert: ${data.alert.title}`,
facts: [
{ name: 'Severity', value: data.alert.severity },
{ name: 'Priority', value: data.triage.priority_level },
{ name: 'Source', value: data.alert.source },
{ name: 'Category', value: data.alert.category }
],
text: data.alert.description
}],
potentialAction: [{
'@type': 'OpenUri',
name: 'View Details',
targets: [{ os: 'default', uri: `https://siem.company.com/alerts/${data.alert.id}` }]
}]
};
}
function buildTicketContent(data) {
return {
title: `[${data.alert.severity.toUpperCase()}] ${data.alert.title}`,
description: `
## Alert Details
- **Alert ID:** ${data.alert.id}
- **Source:** ${data.alert.source}
- **Category:** ${data.alert.category}
- **Severity:** ${data.alert.severity}
- **Priority:** ${data.triage.priority_level}
- **Timestamp:** ${data.alert.timestamp}
## Description
${data.alert.description}
## Indicators
${Object.entries(data.alert.indicators || {})
.filter(([k, v]) => v && (Array.isArray(v) ? v.length > 0 : true))
.map(([k, v]) => `- **${k}:** ${Array.isArray(v) ? v.join(', ') : v}`)
.join('\n')}
## Enrichment Summary
- Sources Queried: ${data.enrichments?.enrichment_summary?.sources_queried || 0}
- Risk Indicators: ${data.enrichments?.enrichment_summary?.risk_indicator_count || 0}
- Known Malicious: ${data.enrichments?.enrichment_summary?.is_known_malicious ? 'Yes' : 'No'}
## Response Actions
${(data.containment?.actions || [])
.map(a => `- ${a.type}: ${a.target || 'N/A'}`)
.join('\n')}
`,
priority: mapToTicketPriority(data.triage.priority_level),
assignee: getAssignee(data.triage.priority_level),
labels: [
'security',
data.alert.category,
data.alert.severity
]
};
}
function getSlackChannel(priority) {
const channels = {
'P1': '#security-critical',
'P2': '#security-high',
'P3': '#security-alerts',
'P4': '#security-low'
};
return channels[priority] || '#security-alerts';
}
function getEmailRecipients(priority) {
const recipients = {
'P1': ['security-team@company.com', 'ciso@company.com'],
'P2': ['security-team@company.com'],
'P3': ['security-analysts@company.com'],
'P4': ['security-analysts@company.com']
};
return recipients[priority] || ['security-analysts@company.com'];
}
function mapToPagerDutySeverity(severity) {
const mapping = {
'critical': 'critical',
'high': 'error',
'medium': 'warning',
'low': 'info',
'info': 'info'
};
return mapping[severity] || 'warning';
}
function getSeverityColor(severity) {
const colors = {
'critical': 'FF0000',
'high': 'FF8C00',
'medium': 'FFD700',
'low': '32CD32',
'info': '1E90FF'
};
return colors[severity] || 'FFD700';
}
function mapToTicketPriority(priority) {
return priority; // P1, P2, P3, P4 mapping
}
function getAssignee(priority) {
return priority === 'P1' ? 'security-lead' : 'security-triage';
}
return {
json: {
...incidentData,
notifications: notifications
}
};Best Practices
Playbook Design
- Start simple: Begin with basic alert triage and escalation
- Iterate: Add automation based on analyst feedback
- Test thoroughly: Use simulated alerts to verify playbook behavior
- Document decisions: Log all automated actions for review
Integration Tips
- Use webhooks for real-time alert ingestion
- Implement retry logic for external API calls
- Cache enrichment results to reduce API costs
- Use connection pooling for database queries
Security Considerations
- Secure credential storage using n8n's credential system
- Implement rate limiting on containment actions
- Require approval for high-impact containment
- Maintain audit trail of all automated actions
Incident response automation with n8n significantly reduces response time while maintaining the human oversight needed for complex security decisions.