DEV Community

Alex Kane
Alex Kane

Posted on

n8n for Security Teams: 5 Automations That Speed Up Incident Response (Free Workflow JSON)

Security teams drown in alerts. The average SOC analyst handles 500+ alerts per shift — over 40% are false positives. That's hours of manual triage that could run automatically.

n8n is uniquely suited for security automation: self-hosted (no data leaves your network), connects to any security tool via HTTP or webhook, git-versionable workflows, and free on self-hosted. No per-task pricing, no vendor lock-in.

Here are 5 n8n automations every InfoSec team should have running.


1. Security Alert Triage & Routing

Problem: Alerts from SIEM, WAF, and IDS all land in the same Slack channel. Critical incidents get buried in noise.

Automation: Ingest alerts via webhook → classify severity by keyword rules → route critical to #sec-incident → log everything else to Sheets.

{
  "nodes": [
    {
      "name": "Alert Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "security-alert",
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 1,
      "position": [250, 300]
    },
    {
      "name": "Classify Severity",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const alert = $json.body;\nconst keywords = {\n  critical: ['ransomware','data exfil','privilege escalation','lateral movement','root access'],\n  high: ['brute force','sql injection','xss','unauthorized access'],\n  medium: ['port scan','failed login','policy violation']\n};\nconst text = (alert.message || alert.description || '').toLowerCase();\nlet severity = 'low';\nif (keywords.critical.some(k => text.includes(k))) severity = 'critical';\nelse if (keywords.high.some(k => text.includes(k))) severity = 'high';\nelse if (keywords.medium.some(k => text.includes(k))) severity = 'medium';\nreturn [{ json: { ...alert, severity, ts: new Date().toISOString() } }];"
      },
      "typeVersion": 2,
      "position": [450, 300]
    },
    {
      "name": "IF Critical",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "string": [{"value1": "={{ $json.severity }}", "operation": "equal", "value2": "critical"}]
        }
      },
      "typeVersion": 1,
      "position": [650, 300]
    },
    {
      "name": "Slack #sec-incident",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#sec-incident",
        "text": "🚨 CRITICAL ALERT\\nMessage: {{ $json.message }}\\nSource: {{ $json.source || 'unknown' }}\\nTime: {{ $json.ts }}"
      },
      "typeVersion": 2,
      "position": [850, 200]
    },
    {
      "name": "Log to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "append",
        "sheetName": "AlertLog",
        "columns": { "mappingMode": "autoMapInputData" }
      },
      "typeVersion": 4,
      "position": [850, 400]
    },
    {
      "name": "Respond OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "parameters": { "responseBody": "{\"status\":\"received\"}" },
      "typeVersion": 1,
      "position": [1050, 300]
    }
  ],
  "connections": {
    "Alert Webhook": { "main": [[{"node": "Classify Severity", "type": "main", "index": 0}]] },
    "Classify Severity": { "main": [[{"node": "IF Critical", "type": "main", "index": 0}]] },
    "IF Critical": {
      "main": [
        [{"node": "Slack #sec-incident", "type": "main", "index": 0}],
        [{"node": "Log to Sheets", "type": "main", "index": 0}]
      ]
    },
    "Slack #sec-incident": { "main": [[{"node": "Respond OK", "type": "main", "index": 0}]] },
    "Log to Sheets": { "main": [[{"node": "Respond OK", "type": "main", "index": 0}]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Setup: Point your SIEM/WAF alert webhook at the n8n production URL. Extend the keyword lists to match your environment. Add a Sheets credential to keep a permanent log.


2. Failed Login Anomaly Monitor

Problem: Brute force and credential stuffing show up as clusters of failed logins. Manual log review catches them hours too late.

Automation: Query Elasticsearch every 15 minutes. If any user or IP has 5+ failures in the window, alert immediately.

{
  "nodes": [
    {
      "name": "Every 15 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": { "interval": [{ "field": "minutes", "minutesInterval": 15 }] }
      },
      "typeVersion": 1,
      "position": [250, 300]
    },
    {
      "name": "Query Elasticsearch",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "url": "https://your-elastic:9200/auth-logs/_search",
        "method": "POST",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [{"name": "Content-Type", "value": "application/json"}]
        },
        "sendBody": true,
        "bodyContentType": "raw",
        "rawBody": "{\"query\":{\"bool\":{\"must\":[{\"term\":{\"event.action\":\"failed-login\"}},{\"range\":{\"@timestamp\":{\"gte\":\"now-15m\"}}}]}},\"aggs\":{\"by_user\":{\"terms\":{\"field\":\"user.name\",\"size\":20}},\"by_ip\":{\"terms\":{\"field\":\"source.ip\",\"size\":20}}}}"
      },
      "typeVersion": 3,
      "position": [450, 300]
    },
    {
      "name": "Find Anomalies",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const aggs = $json.aggregations;\nconst threshold = 5;\nconst anomalies = [];\nfor (const b of aggs.by_user?.buckets || []) {\n  if (b.doc_count >= threshold) anomalies.push({ type: 'user', value: b.key, count: b.doc_count });\n}\nfor (const b of aggs.by_ip?.buckets || []) {\n  if (b.doc_count >= threshold) anomalies.push({ type: 'ip', value: b.key, count: b.doc_count });\n}\nif (anomalies.length === 0) return [];\nreturn [{ json: { anomalies, total: anomalies.length } }];"
      },
      "typeVersion": 2,
      "position": [650, 300]
    },
    {
      "name": "Slack #sec-alerts",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#sec-alerts",
        "text": "⚠️ Failed Login Anomaly\\n{{ $json.anomalies.map(a => `${a.type}: ${a.value} — ${a.count} failures in 15min`).join('\\n') }}"
      },
      "typeVersion": 2,
      "position": [850, 300]
    }
  ],
  "connections": {
    "Every 15 Minutes": { "main": [[{"node": "Query Elasticsearch", "type": "main", "index": 0}]] },
    "Query Elasticsearch": { "main": [[{"node": "Find Anomalies", "type": "main", "index": 0}]] },
    "Find Anomalies": { "main": [[{"node": "Slack #sec-alerts", "type": "main", "index": 0}]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Adapt for Splunk: Replace the Elasticsearch node with an HTTP Request to /services/search/jobs using SPL. Same Code node logic applies.


3. CVE / Vulnerability Monitor

Problem: Your team finds out about critical CVEs days after publication. Threat actors are already exploiting them by then.

Automation: Poll NVD and CISA feeds every morning. Filter for CVSS ≥ 7.0 and known-exploited vulnerabilities. Post to Slack immediately.

{
  "nodes": [
    {
      "name": "Daily 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": { "interval": [{ "field": "hours", "hoursInterval": 24, "triggerAtHour": 8 }] }
      },
      "typeVersion": 1,
      "position": [250, 300]
    },
    {
      "name": "NVD RSS",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "url": "https://nvd.nist.gov/feeds/xml/cve/misc/nvd-rss-analyzed.xml",
        "method": "GET"
      },
      "typeVersion": 3,
      "position": [450, 200]
    },
    {
      "name": "CISA KEV JSON",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "url": "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
        "method": "GET"
      },
      "typeVersion": 3,
      "position": [450, 400]
    },
    {
      "name": "Parse & Filter",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const today = new Date().toISOString().split('T')[0];\nconst results = [];\n\nconst nvdXml = $('NVD RSS').item?.json?.data || '';\nconst itemRegex = /<item>(.*?)<\\/item>/gs;\nlet match;\nwhile ((match = itemRegex.exec(nvdXml)) !== null) {\n  const item = match[1];\n  const title = item.match(/<title>(.*?)<\\/title>/s)?.[1] || '';\n  const link = item.match(/<link>(.*?)<\\/link>/s)?.[1] || '';\n  const desc = item.match(/<description>(.*?)<\\/description>/s)?.[1] || '';\n  const cvssMatch = desc.match(/([89]\\.[0-9]|10\\.0)/);\n  if (cvssMatch) results.push({ type: 'NVD', title, link, score: cvssMatch[1] });\n}\n\nconst kevData = $('CISA KEV JSON').item?.json?.vulnerabilities || [];\nconst todayKev = kevData.filter(v => v.dateAdded === today);\nfor (const v of todayKev.slice(0, 5)) {\n  results.push({ type: 'CISA KEV', title: `${v.cveID}: ${v.vulnerabilityName}`, link: `https://nvd.nist.gov/vuln/detail/${v.cveID}`, score: 'KNOWN EXPLOITED' });\n}\n\nif (results.length === 0) return [];\nreturn results.map(r => ({ json: r }));"
      },
      "typeVersion": 2,
      "position": [700, 300]
    },
    {
      "name": "Slack #vuln-intel",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#vuln-intel",
        "text": "{{ $json.type === 'CISA KEV' ? '🔴 ACTIVELY EXPLOITED' : '⚠️ HIGH CVE' }} (Score: {{ $json.score }}): {{ $json.title }}\\n{{ $json.link }}"
      },
      "typeVersion": 2,
      "position": [900, 300]
    }
  ],
  "connections": {
    "Daily 8AM": {
      "main": [
        [{"node": "NVD RSS", "type": "main", "index": 0}],
        [{"node": "CISA KEV JSON", "type": "main", "index": 0}]
      ]
    },
    "Parse & Filter": { "main": [[{"node": "Slack #vuln-intel", "type": "main", "index": 0}]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Pro tip: CISA KEV entries are the ones that matter most — these are CVEs actively being exploited in the wild right now. Prioritize patching those first.


4. Phishing Email Auto-Triage

Problem: Users forward suspicious emails to security@company.com. Someone has to manually triage each one — usually when they have time, not when it's urgent.

Automation: Monitor the security inbox. AI classifies the email. Confirmed phishing → #sec-phishing Slack alert. User gets a warning reply automatically.

{
  "nodes": [
    {
      "name": "Gmail Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "parameters": {
        "pollTimes": { "item": [{ "mode": "everyMinute" }] },
        "filters": { "labelIds": ["INBOX"] }
      },
      "typeVersion": 1,
      "position": [250, 300]
    },
    {
      "name": "Extract Email",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const e = $json;\nconst links = (e.html || '').match(/href=\"([^\"]+)\"/g)?.slice(0, 10)?.map(m => m.replace('href=\"','').replace('\"','')) || [];\nreturn [{ json: {\n  from: e.from,\n  subject: e.subject,\n  body: (e.text || e.html || '').replace(/<[^>]+>/g,'').substring(0, 2000),\n  links: links.join(', ')\n}}];"
      },
      "typeVersion": 2,
      "position": [450, 300]
    },
    {
      "name": "AI Classify",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "parameters": {
        "model": "gpt-4o-mini",
        "prompt": "Analyze this email for phishing. Respond with JSON only: {\\\"is_phishing\\\": true/false, \\\"confidence\\\": \\\"high/medium/low\\\", \\\"indicators\\\": [\\\"list of red flags\\\"]}\\n\\nFrom: {{ $json.from }}\\nSubject: {{ $json.subject }}\\nBody: {{ $json.body }}\\nLinks: {{ $json.links }}"
      },
      "typeVersion": 1,
      "position": [650, 300]
    },
    {
      "name": "IF Phishing",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "string": [{"value1": "={{ $json.message.content }}", "operation": "contains", "value2": "true"}]
        }
      },
      "typeVersion": 1,
      "position": [850, 300]
    },
    {
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#sec-phishing",
        "text": "🎣 Phishing Detected\\nFrom: {{ $('Extract Email').item.json.from }}\\nSubject: {{ $('Extract Email').item.json.subject }}\\nAI Analysis: {{ $('AI Classify').item.json.message.content }}"
      },
      "typeVersion": 2,
      "position": [1050, 200]
    }
  ],
  "connections": {
    "Gmail Trigger": { "main": [[{"node": "Extract Email", "type": "main", "index": 0}]] },
    "Extract Email": { "main": [[{"node": "AI Classify", "type": "main", "index": 0}]] },
    "AI Classify": { "main": [[{"node": "IF Phishing", "type": "main", "index": 0}]] },
    "IF Phishing": { "main": [[{"node": "Slack Alert", "type": "main", "index": 0}]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Cost: gpt-4o-mini costs ~$0.0001 per email. At 100 forwarded emails/day = $0.01/day. Basically free for the time it saves.


5. Daily Threat Intelligence Brief

Problem: Your team manually checks CISA alerts, threat feeds, and abuse.ch every morning. 30-45 minutes of copy-paste work that produces a digest no one has time to read.

Automation: Pull CISA ICS alerts and abuse.ch URLhaus every morning at 7AM. Filter for today's entries. Post a single Slack digest.

{
  "nodes": [
    {
      "name": "Daily 7AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": { "interval": [{ "field": "hours", "hoursInterval": 24, "triggerAtHour": 7 }] }
      },
      "typeVersion": 1,
      "position": [250, 300]
    },
    {
      "name": "CISA Alerts RSS",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "url": "https://www.cisa.gov/news-network/feeds/ics-alerts.xml",
        "method": "GET"
      },
      "typeVersion": 3,
      "position": [450, 200]
    },
    {
      "name": "Abuse.ch URLhaus",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "url": "https://urlhaus-api.abuse.ch/v1/urls/recent/",
        "method": "POST",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [{"name": "Content-Type", "value": "application/x-www-form-urlencoded"}]
        },
        "sendBody": true,
        "bodyContentType": "raw",
        "rawBody": "limit=10"
      },
      "typeVersion": 3,
      "position": [450, 400]
    },
    {
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "parameters": { "mode": "append" },
      "typeVersion": 2,
      "position": [650, 300]
    },
    {
      "name": "Build Digest",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const today = new Date().toISOString().split('T')[0];\nconst items = $input.all();\nconst lines = [];\n\nfor (const item of items) {\n  const d = item.json;\n  if (d.url && d.url_status) {\n    lines.push(`• [URLhaus] ${d.url} — ${d.url_status} (${d.threat || 'unknown'})`);\n  } else if (d.title || d.description) {\n    const title = (d.title || d.description || '').substring(0, 100);\n    const link = d.link || d.url || '';\n    lines.push(`• [CISA] ${title}${link ? ' — ' + link : ''}`);\n  }\n}\n\nif (lines.length === 0) return [{ json: { digest: 'No new threat intel today.', count: 0 } }];\nreturn [{ json: { digest: lines.join('\\n'), count: lines.length } }];"
      },
      "typeVersion": 2,
      "position": [850, 300]
    },
    {
      "name": "Slack #threat-intel",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#threat-intel",
        "text": "🛡️ Daily Threat Intel Brief — {{ new Date().toISOString().split('T')[0] }} ({{ $json.count }} items)\\n\\n{{ $json.digest }}"
      },
      "typeVersion": 2,
      "position": [1050, 300]
    }
  ],
  "connections": {
    "Daily 7AM": {
      "main": [
        [{"node": "CISA Alerts RSS", "type": "main", "index": 0}],
        [{"node": "Abuse.ch URLhaus", "type": "main", "index": 0}]
      ]
    },
    "CISA Alerts RSS": { "main": [[{"node": "Merge", "type": "main", "index": 0}]] },
    "Abuse.ch URLhaus": { "main": [[{"node": "Merge", "type": "main", "index": 1}]] },
    "Merge": { "main": [[{"node": "Build Digest", "type": "main", "index": 0}]] },
    "Build Digest": { "main": [[{"node": "Slack #threat-intel", "type": "main", "index": 0}]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Extend with: MISP feeds, AlienVault OTX, Shodan Monitor alerts — all support RSS or REST APIs that plug directly into HTTP Request nodes.


Why n8n for security automation?

Factor n8n (self-hosted) Zapier / Make.com
Data stays in your network ✅ Yes ❌ Data leaves to cloud
Free on self-hosted ✅ Yes ❌ Per-task pricing
Git-version your workflows ✅ JSON files ❌ Proprietary format
Connect to any API ✅ HTTP Request node Limited
Audit log every execution ✅ Built-in Limited

The security team case is one of the strongest ROI scenarios for n8n: you're replacing $50-80/hour analyst time with an automation that runs in milliseconds.


These 5 workflows are available as ready-to-import templates in the FlowKit n8n Template Collection — pre-built, documented, and ready to customize for your environment.

Individual templates from $12. Full bundle (15 templates) for $97.

Top comments (0)