n8n Automation

n8n Incident Response Automation: Building Security Playbooks

DeviDevs Team
14 min read
#n8n#incident response#security automation#SOAR#playbooks

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

  1. Start simple: Begin with basic alert triage and escalation
  2. Iterate: Add automation based on analyst feedback
  3. Test thoroughly: Use simulated alerts to verify playbook behavior
  4. 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.

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.