
Security Hub Alerts Triaged by AI
Description
Categories
🤖 AI & Machine Learning
Nodes Used
n8n-nodes-base.ifn8n-nodes-base.setn8n-nodes-base.setn8n-nodes-base.coden8n-nodes-base.coden8n-nodes-base.gmailn8n-nodes-base.webhookn8n-nodes-base.airtablen8n-nodes-base.stickyNoten8n-nodes-base.stickyNote
PriceGratuit
Views0
Last Updated11/28/2025
workflow.json
{
"id": "bBRp9TmumfujcoHs",
"meta": {
"instanceId": "b9d26382fbc087c17fe86e193d09f32e5d99f4a254d13f3b8ee28079e71766cf",
"templateCredsSetupCompleted": true
},
"name": "Security Hub Alerts Triaged by AI",
"tags": [],
"nodes": [
{
"id": "7be0eeba-8700-4b51-a40f-db84c7c533b1",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
0,
-272
],
"webhookId": "bebea408-3564-49c8-9407-14a4102fe0cf",
"parameters": {
"path": "aws-misconfig",
"options": {},
"httpMethod": "POST",
"responseMode": "lastNode"
},
"typeVersion": 2
},
{
"id": "ac0b1a8d-14a0-4e6d-be3a-779a57603869",
"name": "Normalize Finding",
"type": "n8n-nodes-base.code",
"position": [
672,
-176
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Code created by ca7ai\n// n8n Code node (Run Once for Each Item)\n// Normalizes Security Hub / AWS Config events whether they arrive at root or under body\nconst evt = $json.body ?? $json;\n\n// Security Hub finding (EventBridge or raw)\nconst sh = evt?.detail?.findings?.[0] || (Array.isArray(evt.findings) ? evt.findings[0] : null);\n// AWS Config notification\nconst cfg = evt?.detail?.configRuleName ? evt.detail : null;\n// Raw Security Hub finding at root\nconst rawSh = (!sh && evt?.ProductArn) ? evt : null;\n\nconst f = sh || rawSh || {};\n\nconst sev = f?.Severity?.Label || (cfg ? \"MEDIUM\" : \"UNKNOWN\");\nconst title = f?.Title || cfg?.configRuleName || \"Finding\";\nconst desc = f?.Description || cfg?.newEvaluationResult?.annotation || \"—\";\nconst id = f?.Id\n || cfg?.newEvaluationResult?.evaluationResultIdentifier?.evaluationResultQualifier?.configRuleName\n || String(Date.now());\nconst res = f?.Resources?.[0]?.Id || cfg?.resourceId || \"unknown\";\nconst types = f?.Types || [];\nconst account = evt?.account || f?.AwsAccountId || \"unknown\";\nconst region = evt?.region || f?.Region || \"unknown\";\n\n// Derive service + hints\nlet service = \"UNKNOWN\";\nif (/^arn:aws:s3:::/.test(res) || types.some(t => t.includes(\"S3\"))) service = \"S3\";\nelse if (types.some(t => /SecurityGroup/i.test(t)) || /sg-/.test(res)) service = \"EC2-SG\";\nelse if (types.some(t => /IAM/i.test(t))) service = \"IAM\";\nelse if (types.some(t => /RDS|SQL|DB/i.test(t))) service = \"RDS\";\nelse if (f?.ProductArn) service = (f.ProductArn.split(\":\")[5] || \"UNKNOWN\");\n\nconst misconfig_hints = [];\nif (service === \"S3\") misconfig_hints.push(\"s3\");\nif (/0\\.0\\.0\\.0\\/0|Public|Open|world/i.test(desc)) misconfig_hints.push(\"public\");\nif (service === \"EC2-SG\") misconfig_hints.push(\"sg\");\nif (service === \"IAM\") misconfig_hints.push(\"iam\");\nif (service === \"RDS\") misconfig_hints.push(\"db\");\n\n// IMPORTANT: return a SINGLE object (not an array) in this mode\nreturn {\n finding_id: id,\n title,\n description: desc,\n severity: sev,\n resource_id: res,\n service,\n account,\n region,\n product_types: types,\n misconfig_hints,\n raw: evt,\n};"
},
"typeVersion": 2
},
{
"id": "be7fa618-9c8c-4418-a405-a4f2787faaf5",
"name": "Send a message",
"type": "n8n-nodes-base.gmail",
"position": [
1520,
-176
],
"webhookId": "cfac9d61-e17a-467c-a742-7cb9da960a7f",
"parameters": {
"sendTo": "[email protected]",
"message": "=={{ (() => { const nf = $node[\"Normalize Finding\"].json; const ai = typeof $node[\"AI Prioritizer\"].json.message.content === 'string' ? JSON.parse($node[\"AI Prioritizer\"].json.message.content) : $node[\"AI Prioritizer\"].json.message.content; const steps = (ai.remediation || []).map(s => `<li>${s}</li>`).join(''); const tags = (ai.tags || []).join(', '); const airtableId = $node[\"Airtable - Create Record\"].json?.id || ''; const airtableLine = airtableId ? `<p><b>Airtable Record ID:</b> ${airtableId}</p>` : ''; return ` <h2>AWS Misconfig Alert</h2> <p><b>Priority:</b> ${ai.priority} <b>Severity:</b> ${nf.severity}</p> <p><b>Title:</b> ${nf.title}</p> <p><b>Service:</b> ${nf.service} <b>Resource:</b> ${nf.resource_id}</p> <p><b>Account:</b> ${nf.account} <b>Region:</b> ${nf.region}</p> <p><b>Why:</b> ${ai.rationale}</p> <p><b>Remediation:</b></p> <ol>${steps}</ol> <p><b>Tags:</b> ${tags || '—'}</p> ${airtableLine} <details><summary>Raw finding</summary> <pre style=\"background:#f6f8fa;padding:12px;border-radius:6px;white-space:pre-wrap\">${JSON.stringify(nf.raw || nf, null, 2)}</pre> </details> `;})() }}",
"options": {},
"subject": "=={{ `[${JSON.parse($node[\"AI Prioritizer\"].json.message.content).priority}] ${$node[\"Normalize Finding\"].json.title} — ${$node[\"Normalize Finding\"].json.resource_id} (${ $node[\"Normalize Finding\"].json.service })` }}"
},
"credentials": {
"gmailOAuth2": {
"id": "3P9F7770YiCuujN",
"name": "Gmail account"
}
},
"typeVersion": 2.1
},
{
"id": "c0fb42e3-e670-44f9-a3fa-0b2c3dec2ae1",
"name": "AI Prioritizer",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
896,
-176
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini",
"cachedResultName": "GPT-4.1-MINI"
},
"options": {},
"messages": {
"values": [
{
"content": "=You are a cloud SecOps triage assistant. Given a normalized AWS finding JSON, return a STRICT JSON object:\n\n{\n \"priority\": \"P0|P1|P2|P3\",\n \"rationale\": \"one-paragraph reason referencing severity, resource, and exposure\",\n \"remediation\": [\"step 1\", \"step 2\", \"...\"],\n \"tags\": [\"s3\",\"iam\",\"public\", \"...\"]\n}\n\nMapping guidance:\n- Treat publicly accessible data (e.g., public S3 buckets, 0.0.0.0/0 on admin ports, open RDS) as P0 or P1 depending on blast radius.\n- Internal-only or low impact → P2/P3.\n- If severity label is CRITICAL/HIGH, bias to P0/P1.\n\nFinding:\n{{ JSON.stringify($json, null, 2) }}\n"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"id": "IZ5MCCCsozmZld3",
"name": "OpenAi account"
}
},
"typeVersion": 1.8
},
{
"id": "42721c67-8c6d-4063-826b-b59e01a3d8ca",
"name": "Airtable - Create Record",
"type": "n8n-nodes-base.airtable",
"position": [
1296,
-176
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appzIE2wRRYUbvl50",
"cachedResultUrl": "https://airtable.com/uuuu",
"cachedResultName": "misconfigs"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblPDewIrVYYYYNyi",
"cachedResultUrl": "https://airtable.com/uuuu/uuuu",
"cachedResultName": "finding_table"
},
"columns": {
"value": {
"id": "={{ $('Normalize Finding').item.json.finding_id }}",
"Tags": "={{ $json.message.content.tags[0] }}{{ $json.message.content.tags[1] }}{{ $json.message.content.tags[2] }}{{ $json.message.content.tags[3] }}{{ $json.message.content.tags[4] }}",
"Title": "={{ $('Normalize Finding').item.json.title }}",
"Region": "={{ $('Normalize Finding').item.json.region }}",
"Account": "={{ $('Normalize Finding').item.json.account }}",
"Service": "={{ $('Normalize Finding').item.json.service }}",
"Priority": "={{ $json.message.content.priority }}",
"Resource": "={{ $('Normalize Finding').item.json.raw.detail.findings[0].Resources[0].Id }}",
"Severity": "={{ $('Normalize Finding').item.json.severity }}",
"Rationale": "={{ $json.message.content.rationale }}",
"Finding ID": "={{ $('Normalize Finding').item.json.raw.detail.findings[0].Id }}",
"Remediation": "={{ $json.message.content.remediation[0] }}{{ $json.message.content.remediation[1] }}{{ $json.message.content.remediation[2] }}{{ $json.message.content.remediation[3] }}"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "id",
"defaultMatch": true
},
{
"id": "Finding ID",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Finding ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Title",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Severity",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Severity",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Priority",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Priority",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Resource",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Resource",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Service",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Service",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Account",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Account",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Region",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Region",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tags",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Tags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Rationale",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Rationale",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Remediation",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Remediation",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"id"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "upsert"
},
"credentials": {
"airtableTokenApi": {
"id": "aftfB5RyLZKQ1111",
"name": "Airtable Access Token"
}
},
"typeVersion": 2.1
},
{
"id": "6f9bf9c6-61ee-4b25-844c-5f03c9979f9e",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"position": [
1744,
-176
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{\n {\n resp: {\n status: \"processed\",\n priority: $node[\"Airtable - Create Record\"].json.fields.Priority,\n finding_id: $node[\"Normalize Finding\"].json.finding_id\n }\n }\n}}\n"
},
"typeVersion": 3.4
},
{
"id": "fbb5d2af-7eb2-414f-b79e-2a51ddb9b21f",
"name": "SNS Handler",
"type": "n8n-nodes-base.code",
"position": [
224,
-272
],
"parameters": {
"jsCode": "const b = $json.body ?? $json;\nconst token = $json.query?.token ?? b.token;\nif (token !== 'MY_SUPER_TOKEN') throw new Error('unauthorized');\n\nif (b.Type === 'SubscriptionConfirmation' && b.SubscribeURL) {\n return { mode: 'confirm', subscribeUrl: b.SubscribeURL };\n}\n\nlet event = b;\nif (b.Type === 'Notification' && b.Message) {\n try { event = JSON.parse(b.Message); } catch {}\n}\nreturn { mode: 'notify', event };\n"
},
"typeVersion": 2
},
{
"id": "b61e5e70-697d-4fe4-9897-9a116aa5aff1",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
448,
-272
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d192995f-8809-4b60-8f6a-b7bd2e3e47b0",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "=={{ $json.mode === 'confirm' }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "5c1145c7-6dd6-47e3-9fee-e1fb15318a22",
"name": "SNS Confirm",
"type": "n8n-nodes-base.httpRequest",
"position": [
672,
-368
],
"parameters": {
"url": "=={{ $json.subscribeUrl }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"fullResponse": true,
"responseFormat": "json"
}
}
}
},
"typeVersion": 4.2
},
{
"id": "831507c3-b00f-44ec-8506-c0310904eb6e",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"position": [
984,
-368
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "346708e7-629e-4d8a-8a98-994b9526b55d",
"name": "resp",
"type": "object",
"value": "=resp = { status: \"subscribed\", statusCode: $json.statusCode || 200 }"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "b39a925c-911d-4e61-a0e6-5aadc4ccfd38",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-464,
-32
],
"parameters": {
"width": 336,
"content": "## Note\n\n: You must have the AWS Side pre-configured before testing / starting this workflow\n"
},
"typeVersion": 1
},
{
"id": "1bea05cd-6468-4c74-aeec-8c60d69411c4",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-464,
-368
],
"parameters": {
"color": 4,
"width": 336,
"height": 304,
"content": "## Title: How it works (wiring)\n\n- **Flow**: Webhook → SNS Handler → IF → (true) SNS Confirm → Done | (false) Normalize → AI → Airtable → Gmail → Respond\n \n- **Purpose**: triage AWS misconfigs and alert the team\n \n- Responds when last node finishes (returns small JSON)\n"
},
"typeVersion": 1
}
],
"active": true,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "044dc52b-81c7-43ab-a1c2-5be287a0d970",
"connections": {
"If": {
"main": [
[
{
"node": "SNS Confirm",
"type": "main",
"index": 0
}
],
[
{
"node": "Normalize Finding",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "SNS Handler",
"type": "main",
"index": 0
}
]
]
},
"SNS Confirm": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
},
"SNS Handler": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields1": {
"main": [
[]
]
},
"AI Prioritizer": {
"main": [
[
{
"node": "Airtable - Create Record",
"type": "main",
"index": 0
}
]
]
},
"Send a message": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Normalize Finding": {
"main": [
[
{
"node": "AI Prioritizer",
"type": "main",
"index": 0
}
]
]
},
"Airtable - Create Record": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
}
}
}