Automatisierter wöchentlicher Sicherheitsaudit-Bericht mit Gmail gesendet
Dies ist ein SecOps-Bereich Automatisierungsworkflow mit 23 Nodes. Hauptsächlich werden If, N8n, Set, Code, Cron und andere Nodes verwendet. Automatisierte wöchentliche Sicherheitsaudit-Berichte mit Gmail
- •Google-Konto + Gmail API-Anmeldedaten
Kategorie
{
"meta": {
"instanceId": "3568945d2a3f637c54ef170c26005913624678bc725f58cac81dfa10a714a2ca",
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "628f28dc-b550-4501-b3f7-656756a84f0b",
"name": "Konfigurationsvariablen setzen",
"type": "n8n-nodes-base.set",
"position": [
-1552,
64
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "af567071-143f-4361-9f18-12c730802196",
"name": "email_to",
"type": "string",
"value": "abc@xyz.com"
},
{
"id": "0b68ba86-9c64-4f67-9f1d-c1914f42722c",
"name": "project_name",
"type": "string",
"value": "N8N-main"
},
{
"id": "e6cf406c-ed0b-40bf-bd24-5e3a98990f66",
"name": "server_url",
"type": "string",
"value": "YOUR N8N SERVER URL WITHOUT THE / AT THE END"
},
{
"id": "195a7808-14a4-44b6-9b03-51eede551f87",
"name": "Language",
"type": "string",
"value": "EN"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "29de9117-4e5d-42c0-b2bb-b12b37cd6bf4",
"name": "Gmail senden (HTML)",
"type": "n8n-nodes-base.gmail",
"position": [
0,
64
],
"parameters": {
"toList": [
"={{ $('Set Config Variables').first().json.email_to }}"
],
"message": "=",
"subject": "={{ $json.emailSubject }}",
"resource": "message",
"htmlMessage": "={{ $json.html }}",
"includeHtml": true,
"additionalFields": {}
},
"credentials": {
"gmailOAuth2": {
"id": "ILUgVCush8y34I4O",
"name": "Gmail account"
}
},
"typeVersion": 1
},
{
"id": "3de2498a-7b6d-4971-aaa5-01c708e9a7a6",
"name": "Zeitplan-Trigger (Wöchentlich)",
"type": "n8n-nodes-base.cron",
"position": [
-1808,
64
],
"parameters": {
"triggerTimes": {
"item": [
{
"hour": 6,
"mode": "everyWeek"
}
]
}
},
"typeVersion": 1
},
{
"id": "57015778-7300-4c12-b7f2-c795b7316d59",
"name": "Sicherheitsaudit generieren",
"type": "n8n-nodes-base.n8n",
"position": [
-1280,
64
],
"parameters": {
"resource": "audit",
"operation": "generate",
"requestOptions": {},
"additionalOptions": {}
},
"credentials": {
"n8nApi": {
"id": "l5HQ7xfVxL2LP772",
"name": "n8n account"
}
},
"typeVersion": 1
},
{
"id": "fec96d1e-e966-4fcc-9900-9a8d32211008",
"name": "Auditbericht formatieren - FR",
"type": "n8n-nodes-base.code",
"position": [
-256,
-32
],
"parameters": {
"jsCode": "// === INPUTS / CONFIG ===\nconst data = $('Generate a security audit').first().json;\nconst project = $('Set Config Variables').first().json.project_name || 'n8n';\nconst date = new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' });\nconst baseUrl = $('Set Config Variables').first().json.server_url?.replace(/\\/$/, '') || 'https://n8n.example.com';\n\n\n// ✅ Récupère les résultats de la loop précédente, peu importe la structure ou la connexion\nlet workflowExecutions = [];\n\ntry {\n const allInputs = $input.all();\n for (const i of allInputs) {\n const j = i.json;\n if (Array.isArray(j) && j[0]?.workflowId) {\n workflowExecutions.push(...j); // cas d’un tableau complet\n } else if (j?.workflowId) {\n workflowExecutions.push(j); // cas d’un item unique\n }\n }\n\n console.log('Detected workflows:', workflowExecutions.length);\n if (workflowExecutions.length > 0) {\n console.log('First workflow execution:', workflowExecutions[0]);\n }\n} catch (e) {\n console.log('⚠️ Impossible de lire les exécutions:', e.message);\n}\n\n\n// Si le premier élément est lui-même un tableau, on le \"déplie\"\nif (Array.isArray(workflowExecutions[0])) {\n workflowExecutions = workflowExecutions[0];\n}\n\n\n// === STATS ===\nlet totalSections = 0;\nlet totalLocations = 0;\nlet totalCommunity = 0;\nlet totalCredentials = 0;\nlet totalNodes = 0;\nlet nodeTypeStats = {};\nlet sectionsPerReport = {};\nconst uniqueCredentials = new Set(); // ✅ Nouvel ensemble pour compter les credentials uniques\n\n// === HELPERS ===\nfunction getNodeIcon(nodeType) {\n const iconMap = {\n 'n8n-nodes-base.code': '💻',\n 'n8n-nodes-base.function': '⚡',\n 'n8n-nodes-base.httpRequest': '🌐',\n 'n8n-nodes-base.executeCommand': '⌨️',\n 'n8n-nodes-base.ssh': '🔐',\n 'n8n-nodes-base.ftp': '📁',\n 'n8n-nodes-base.webhook': '🪝'\n };\n return iconMap[nodeType] || '⚙️';\n}\n\nfunction groupNodesByWorkflow(locations) {\n const workflows = {};\n for (const loc of locations) {\n if (loc.kind === 'node' && loc.workflowId) {\n const id = loc.workflowId;\n if (!workflows[id]) {\n workflows[id] = { name: loc.workflowName || 'Workflow inconnu', id, nodes: [] };\n }\n workflows[id].nodes.push(loc);\n }\n }\n return workflows;\n}\n\nconsole.log('=== DEBUG WORKFLOW MATCH ===');\nconsole.log('Sample from workflowExecutions:', workflowExecutions[0]);\n\n// Extraire tous les IDs connus dans ta loop\nconst loopIds = new Set(workflowExecutions.map(w => String(w.workflowId).trim()));\nconsole.log('IDs connus dans loop:', Array.from(loopIds));\n\n// On va aussi log les IDs qu’on essaye de trouver\nconst exampleIds = [];\nfor (const [title, report] of Object.entries(data)) {\n if (!report.sections?.length) continue;\n for (const section of report.sections) {\n if (!section.location) continue;\n for (const loc of section.location) {\n if (loc.workflowId) exampleIds.push(loc.workflowId);\n }\n }\n}\nconsole.log('IDs trouvés dans rapport:', Array.from(new Set(exampleIds.map(i => String(i).trim()))));\n\n\n// ✅ Trouver les infos d'exécution correspondantes pour un workflowId\nfunction findWorkflowRun(workflowId) {\n if (!workflowExecutions || !Array.isArray(workflowExecutions)) return null;\n const normalizedId = String(workflowId).trim();\n\n const match = workflowExecutions.find(w => {\n const loopId = String(w.workflowId).trim();\n if (loopId === normalizedId) {\n console.log(`✅ MATCH trouvé: ${loopId}`);\n return true;\n }\n return false;\n });\n\n if (!match) console.log(`❌ Aucun match pour: ${normalizedId}`);\n return match || null;\n}\n\n\n// === FORMATTERS ===\nfunction formatSection(section) {\n if (!section) return '';\n totalSections++;\n if (!sectionsPerReport.current) sectionsPerReport.current = 0;\n sectionsPerReport.current++;\n if (section.location?.length) totalLocations += section.location.length;\n\n let md = `### 🔹 ${section.title}\\n${section.description || ''}\\n\\n`;\n if (section.location?.length) {\n const nodes = section.location.filter(l => l.kind === 'node');\n const others = section.location.filter(l => l.kind !== 'node');\n\n for (const loc of others) {\n if (loc.kind === 'community') {\n totalCommunity++;\n const pkg = loc.packageUrl ? `[${loc.nodeType}](${loc.packageUrl})` : loc.nodeType;\n md += `- 🧩 ${pkg}\\n`;\n } else if (loc.kind === 'credential') {\n totalCredentials++;\n const credName = loc.name?.trim() || 'Credential sans nom';\n uniqueCredentials.add(credName);\n md += `- 🔑 ${credName}\\n`;\n }\n }\n\n if (nodes.length > 0) {\n totalNodes += nodes.length;\n for (const n of nodes) {\n const t = n.nodeType || 'unknown';\n nodeTypeStats[t] = (nodeTypeStats[t] || 0) + 1;\n }\n const workflows = groupNodesByWorkflow(nodes);\n for (const [id, wf] of Object.entries(workflows)) {\n const link = `${baseUrl}/workflow/${id}`;\n md += `\\n**📋 Workflow : [${wf.name}](${link})**\\n`;\n for (const n of wf.nodes) {\n const icon = getNodeIcon(n.nodeType);\n md += ` - ${icon} ${n.nodeName || n.nodeType || 'Node inconnu'}\\n`;\n }\n }\n }\n }\n if (section.recommendation) md += `\\n> 💡 ${section.recommendation}\\n`;\n return md + '\\n';\n}\n\nfunction formatSectionHTML(section) {\n if (!section) return '';\n let html = `<h3>🔹 ${section.title}</h3>`;\n if (section.description) html += `<p>${section.description}</p>`;\n\n if (section.location?.length) {\n const nodes = section.location.filter(l => l.kind === 'node');\n const others = section.location.filter(l => l.kind !== 'node');\n html += `<ul>`;\n\n for (const loc of others) {\n if (loc.kind === 'community') {\n const pkg = loc.packageUrl\n ? `<a href=\"${loc.packageUrl}\" target=\"_blank\">${loc.nodeType}</a>`\n : loc.nodeType;\n html += `<li>🧩 ${pkg}</li>`;\n } else if (loc.kind === 'credential') {\n const credName = loc.name?.trim() || 'Credential sans nom';\n uniqueCredentials.add(credName);\n html += `<li>🔑 ${credName}</li>`;\n }\n }\n html += `</ul>`;\n\n if (nodes.length > 0) {\n const workflows = groupNodesByWorkflow(nodes);\n for (const [id, wf] of Object.entries(workflows)) {\n const link = `${baseUrl}/workflow/${id}`;\n const run = findWorkflowRun(id);\n\n // ✅ Construction du badge de statut et des horaires\n let runInfo = '';\nif (run) {\n const color = run.status === 'success' ? '🟢' : '🔴';\n\n const started = run.startedAt && run.startedAt !== 'NoRun'\n ? new Date(run.startedAt).toLocaleString('fr-FR', {\n timeZone: 'Europe/Paris',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n }).replace(',', '').replace(/\\//g, '-')\n : 'N/A';\n\n const stopped = run.stoppedAt && run.stoppedAt !== 'NoRun'\n ? new Date(run.stoppedAt).toLocaleString('fr-FR', {\n timeZone: 'Europe/Paris',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n }).replace(',', '').replace(/\\//g, '-')\n : 'N/A';\n\n runInfo = ` <small style=\"color:#888;\">${color} (${started} → ${stopped})</small>`;\n} else {\n runInfo = ` <small style=\"color:#aaa;\">⚪ Non exécuté récemment</small>`;\n }\n\n html += `<h4>📋 Workflow : <a href=\"${link}\" target=\"_blank\">${wf.name}</a>${runInfo}</h4><ul>`;\n for (const n of wf.nodes) {\n const icon = getNodeIcon(n.nodeType);\n html += `<li>${icon} ${n.nodeName || n.nodeType || 'Node inconnu'}</li>`;\n }\n html += `</ul>`;\n }\n }\n }\n\n if (section.recommendation) html += `<blockquote>💡 ${section.recommendation}</blockquote>`;\n return html;\n}\n\n// === SECURITY SETTINGS ===\nfunction formatSecuritySettings(settings) {\n if (!settings) return { md: '', html: '' };\n let md = `### 🔹 Security settings\\nVoici les paramètres de sécurité actuels de cette instance :\\n\\n`;\n for (const [cat, items] of Object.entries(settings)) {\n md += `**${cat.charAt(0).toUpperCase() + cat.slice(1)}:**\\n`;\n for (const [k, v] of Object.entries(items)) md += `- ${k}: ${v}\\n`;\n md += `\\n`;\n }\n\n let html = `<h3>🔹 Security settings</h3><ul>`;\n for (const [cat, items] of Object.entries(settings)) {\n for (const [k, v] of Object.entries(items)) html += `<li><b>${cat}</b> ${k}: ${v}</li>`;\n }\n html += `</ul>`;\n return { md, html };\n}\n\n// === BUILD REPORT ===\nlet markdown = `# 🔒 Rapport d'audit de sécurité ${project}\\n\\n**Date :** ${date}\\n\\n`;\nlet html = `<h1>🔒 Rapport d'audit de sécurité ${project}</h1><p><strong>Date :</strong> ${date}</p>`;\n\nconst reportIcons = {\n 'Credentials Risk Report': '🔐',\n 'Nodes Risk Report': '🧩',\n 'Instance Risk Report': '🏢'\n};\n\nfor (const [title, report] of Object.entries(data)) {\n sectionsPerReport.current = 0;\n const icon = reportIcons[title] || '📊';\n markdown += `## ${icon} ${title}\\n\\n`;\n html += `<h2>${icon} ${title}</h2>`;\n if (!report.sections?.length) continue;\n for (const section of report.sections) {\n if (section.settings) {\n const sec = formatSecuritySettings(section.settings);\n markdown += sec.md;\n html += sec.html;\n } else {\n markdown += formatSection(section);\n html += formatSectionHTML(section);\n }\n }\n sectionsPerReport[title] = sectionsPerReport.current;\n}\n\n// === SYNTHÈSE ===\nlet riskLevel = '🟩 Faible';\nlet riskEmoji = '🟩';\nlet riskText = 'Faible';\nif (totalCredentials > 5 || totalNodes > 10 || totalCommunity > 3) {\n riskLevel = '🟥 Élevé'; riskEmoji = '🟥'; riskText = 'Élevé';\n} else if (totalCredentials > 2 || totalNodes > 5 || totalCommunity > 1) {\n riskLevel = '🟧 Modéré'; riskEmoji = '🟧'; riskText = 'Modéré';\n}\n\n// === Breakdown des types de nodes ===\nlet nodeTypeBreakdown = '';\nlet nodeTypeBreakdownHTML = '';\nif (Object.keys(nodeTypeStats).length > 0) {\n const sortedNodeTypes = Object.entries(nodeTypeStats).sort((a, b) => b[1] - a[1]);\n nodeTypeBreakdown = '\\n**Détail par type de node :**\\n';\n nodeTypeBreakdownHTML = '<li><b>Détail par type de node :</b><ul>';\n for (const [nodeType, count] of sortedNodeTypes) {\n const icon = getNodeIcon(nodeType);\n const simpleName = nodeType.replace('n8n-nodes-base.', '');\n nodeTypeBreakdown += ` - ${icon} ${simpleName}: ${count}\\n`;\n nodeTypeBreakdownHTML += `<li>${icon} ${simpleName}: ${count}</li>`;\n }\n nodeTypeBreakdownHTML += '</ul></li>';\n}\n\n// === SYNTHÈSE (avec unique credentials) ===\nconst uniqueCredCount = uniqueCredentials.size;\n\nconst summaryText =\n`## 📊 Synthèse de l'audit\\n\n- Credentials concernés : ${totalCredentials} (${uniqueCredCount} uniques)\n- Nodes concernés : ${totalNodes}${nodeTypeBreakdown}\n- Community nodes : ${totalCommunity}\n- **Niveau de risque global : ${riskLevel}**\\n\\n`;\n\nmarkdown = markdown.replace(/^# 🔒.*?\\n\\n/, `$&${summaryText}`);\n\nconst emailSubject = `🔒 Rapport d'audit ${project} – Risque ${riskEmoji} ${riskText}`;\n\n// === HTML synthèse ===\nhtml = html.replace(\n /<p><strong>Date :<\\/strong>.*?<\\/p>/,\n `$&<h2>📊 Synthèse</h2>\n <ul>\n <li><b>Credentials concernés :</b> ${totalCredentials} (${uniqueCredCount} uniques)</li>\n <li><b>Nodes concernés :</b> ${totalNodes}</li>\n ${nodeTypeBreakdownHTML}\n <li><b>Community nodes :</b> ${totalCommunity}</li>\n <li><b>Niveau de risque global :</b> ${riskLevel}</li>\n </ul>`\n);\n\nreturn [{\n json: {\n markdown,\n html,\n project,\n date,\n emailSubject,\n riskLevel: riskText,\n riskEmoji,\n uniqueCredentials: uniqueCredCount\n }\n}];"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "2f877b5d-0bcc-43c1-804b-6ab835c98373",
"name": "Auditbericht formatieren - EN",
"type": "n8n-nodes-base.code",
"position": [
-256,
112
],
"parameters": {
"jsCode": "// === INPUTS / CONFIG ===\nconst data = $('Generate a security audit').first().json;\nconst project = $('Set Config Variables').first().json.project_name || 'n8n';\nconst date = new Date().toLocaleString('en-GB', { timeZone: 'Europe/Paris' });\nconst baseUrl = $('Set Config Variables').first().json.server_url?.replace(/\\/$/, '') || 'https://n8n.example.com';\n\n\n// ✅ Retrieve results from previous loop, regardless of structure or connection\nlet workflowExecutions = [];\n\ntry {\n const allInputs = $input.all();\n for (const i of allInputs) {\n const j = i.json;\n if (Array.isArray(j) && j[0]?.workflowId) {\n workflowExecutions.push(...j); // case of a full array\n } else if (j?.workflowId) {\n workflowExecutions.push(j); // case of a single item\n }\n }\n\n console.log('Detected workflows:', workflowExecutions.length);\n if (workflowExecutions.length > 0) {\n console.log('First workflow execution:', workflowExecutions[0]);\n }\n} catch (e) {\n console.log('⚠️ Unable to read executions:', e.message);\n}\n\n\n// If the first element is itself an array, flatten it\nif (Array.isArray(workflowExecutions[0])) {\n workflowExecutions = workflowExecutions[0];\n}\n\n\n// === STATS ===\nlet totalSections = 0;\nlet totalLocations = 0;\nlet totalCommunity = 0;\nlet totalCredentials = 0;\nlet totalNodes = 0;\nlet nodeTypeStats = {};\nlet sectionsPerReport = {};\nconst uniqueCredentials = new Set(); // ✅ New set to count unique credentials\n\n// === HELPERS ===\nfunction getNodeIcon(nodeType) {\n const iconMap = {\n 'n8n-nodes-base.code': '💻',\n 'n8n-nodes-base.function': '⚡',\n 'n8n-nodes-base.httpRequest': '🌐',\n 'n8n-nodes-base.executeCommand': '⌨️',\n 'n8n-nodes-base.ssh': '🔐',\n 'n8n-nodes-base.ftp': '📁',\n 'n8n-nodes-base.webhook': '🪝'\n };\n return iconMap[nodeType] || '⚙️';\n}\n\nfunction groupNodesByWorkflow(locations) {\n const workflows = {};\n for (const loc of locations) {\n if (loc.kind === 'node' && loc.workflowId) {\n const id = loc.workflowId;\n if (!workflows[id]) {\n workflows[id] = { name: loc.workflowName || 'Unknown workflow', id, nodes: [] };\n }\n workflows[id].nodes.push(loc);\n }\n }\n return workflows;\n}\n\nconsole.log('=== DEBUG WORKFLOW MATCH ===');\nconsole.log('Sample from workflowExecutions:', workflowExecutions[0]);\n\n// Extract all known IDs from loop\nconst loopIds = new Set(workflowExecutions.map(w => String(w.workflowId).trim()));\nconsole.log('Known IDs in loop:', Array.from(loopIds));\n\n// Also log IDs found in the report\nconst exampleIds = [];\nfor (const [title, report] of Object.entries(data)) {\n if (!report.sections?.length) continue;\n for (const section of report.sections) {\n if (!section.location) continue;\n for (const loc of section.location) {\n if (loc.workflowId) exampleIds.push(loc.workflowId);\n }\n }\n}\nconsole.log('IDs found in report:', Array.from(new Set(exampleIds.map(i => String(i).trim()))));\n\n\n// ✅ Find matching execution info for a given workflowId\nfunction findWorkflowRun(workflowId) {\n if (!workflowExecutions || !Array.isArray(workflowExecutions)) return null;\n const normalizedId = String(workflowId).trim();\n\n const match = workflowExecutions.find(w => {\n const loopId = String(w.workflowId).trim();\n if (loopId === normalizedId) {\n console.log(`✅ MATCH found: ${loopId}`);\n return true;\n }\n return false;\n });\n\n if (!match) console.log(`❌ No match for: ${normalizedId}`);\n return match || null;\n}\n\n\n// === FORMATTERS ===\nfunction formatSection(section) {\n if (!section) return '';\n totalSections++;\n if (!sectionsPerReport.current) sectionsPerReport.current = 0;\n sectionsPerReport.current++;\n if (section.location?.length) totalLocations += section.location.length;\n\n let md = `### 🔹 ${section.title}\\n${section.description || ''}\\n\\n`;\n if (section.location?.length) {\n const nodes = section.location.filter(l => l.kind === 'node');\n const others = section.location.filter(l => l.kind !== 'node');\n\n for (const loc of others) {\n if (loc.kind === 'community') {\n totalCommunity++;\n const pkg = loc.packageUrl ? `[${loc.nodeType}](${loc.packageUrl})` : loc.nodeType;\n md += `- 🧩 ${pkg}\\n`;\n } else if (loc.kind === 'credential') {\n totalCredentials++;\n const credName = loc.name?.trim() || 'Unnamed credential';\n uniqueCredentials.add(credName);\n md += `- 🔑 ${credName}\\n`;\n }\n }\n\n if (nodes.length > 0) {\n totalNodes += nodes.length;\n for (const n of nodes) {\n const t = n.nodeType || 'unknown';\n nodeTypeStats[t] = (nodeTypeStats[t] || 0) + 1;\n }\n const workflows = groupNodesByWorkflow(nodes);\n for (const [id, wf] of Object.entries(workflows)) {\n const link = `${baseUrl}/workflow/${id}`;\n md += `\\n**📋 Workflow: [${wf.name}](${link})**\\n`;\n for (const n of wf.nodes) {\n const icon = getNodeIcon(n.nodeType);\n md += ` - ${icon} ${n.nodeName || n.nodeType || 'Unknown node'}\\n`;\n }\n }\n }\n }\n if (section.recommendation) md += `\\n> 💡 ${section.recommendation}\\n`;\n return md + '\\n';\n}\n\nfunction formatSectionHTML(section) {\n if (!section) return '';\n let html = `<h3>🔹 ${section.title}</h3>`;\n if (section.description) html += `<p>${section.description}</p>`;\n\n if (section.location?.length) {\n const nodes = section.location.filter(l => l.kind === 'node');\n const others = section.location.filter(l => l.kind !== 'node');\n html += `<ul>`;\n\n for (const loc of others) {\n if (loc.kind === 'community') {\n const pkg = loc.packageUrl\n ? `<a href=\"${loc.packageUrl}\" target=\"_blank\">${loc.nodeType}</a>`\n : loc.nodeType;\n html += `<li>🧩 ${pkg}</li>`;\n } else if (loc.kind === 'credential') {\n const credName = loc.name?.trim() || 'Unnamed credential';\n uniqueCredentials.add(credName);\n html += `<li>🔑 ${credName}</li>`;\n }\n }\n html += `</ul>`;\n\n if (nodes.length > 0) {\n const workflows = groupNodesByWorkflow(nodes);\n for (const [id, wf] of Object.entries(workflows)) {\n const link = `${baseUrl}/workflow/${id}`;\n const run = findWorkflowRun(id);\n\n // ✅ Build status badge and timing info\nlet runInfo = '';\nif (run) {\n const color = run.status === 'success' ? '🟢' : '🔴';\n\n const started = run.startedAt && run.startedAt !== 'NoRun'\n ? new Date(run.startedAt).toLocaleString('fr-FR', {\n timeZone: 'Europe/Paris',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n }).replace(',', '').replace(/\\//g, '-')\n : 'N/A';\n\n const stopped = run.stoppedAt && run.stoppedAt !== 'NoRun'\n ? new Date(run.stoppedAt).toLocaleString('fr-FR', {\n timeZone: 'Europe/Paris',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n }).replace(',', '').replace(/\\//g, '-')\n : 'N/A';\n\n runInfo = ` <small style=\"color:#888;\">${color} (${started} → ${stopped})</small>`;\n} else {\n runInfo = ` <small style=\"color:#aaa;\">⚪ Not executed recently</small>`;\n}\n\n\n html += `<h4>📋 Workflow: <a href=\"${link}\" target=\"_blank\">${wf.name}</a>${runInfo}</h4><ul>`;\n for (const n of wf.nodes) {\n const icon = getNodeIcon(n.nodeType);\n html += `<li>${icon} ${n.nodeName || n.nodeType || 'Unknown node'}</li>`;\n }\n html += `</ul>`;\n }\n }\n }\n\n if (section.recommendation) html += `<blockquote>💡 ${section.recommendation}</blockquote>`;\n return html;\n}\n\n// === SECURITY SETTINGS ===\nfunction formatSecuritySettings(settings) {\n if (!settings) return { md: '', html: '' };\n let md = `### 🔹 Security settings\\nHere are the current security settings for this instance:\\n\\n`;\n for (const [cat, items] of Object.entries(settings)) {\n md += `**${cat.charAt(0).toUpperCase() + cat.slice(1)}:**\\n`;\n for (const [k, v] of Object.entries(items)) md += `- ${k}: ${v}\\n`;\n md += `\\n`;\n }\n\n let html = `<h3>🔹 Security settings</h3><ul>`;\n for (const [cat, items] of Object.entries(settings)) {\n for (const [k, v] of Object.entries(items)) html += `<li><b>${cat}</b> ${k}: ${v}</li>`;\n }\n html += `</ul>`;\n return { md, html };\n}\n\n// === BUILD REPORT ===\nlet markdown = `# 🔒 Security Audit Report ${project}\\n\\n**Date:** ${date}\\n\\n`;\nlet html = `<h1>🔒 Security Audit Report ${project}</h1><p><strong>Date:</strong> ${date}</p>`;\n\nconst reportIcons = {\n 'Credentials Risk Report': '🔐',\n 'Nodes Risk Report': '🧩',\n 'Instance Risk Report': '🏢'\n};\n\nfor (const [title, report] of Object.entries(data)) {\n sectionsPerReport.current = 0;\n const icon = reportIcons[title] || '📊';\n markdown += `## ${icon} ${title}\\n\\n`;\n html += `<h2>${icon} ${title}</h2>`;\n if (!report.sections?.length) continue;\n for (const section of report.sections) {\n if (section.settings) {\n const sec = formatSecuritySettings(section.settings);\n markdown += sec.md;\n html += sec.html;\n } else {\n markdown += formatSection(section);\n html += formatSectionHTML(section);\n }\n }\n sectionsPerReport[title] = sectionsPerReport.current;\n}\n\n// === SUMMARY ===\nlet riskLevel = '🟩 Low';\nlet riskEmoji = '🟩';\nlet riskText = 'Low';\nif (totalCredentials > 5 || totalNodes > 10 || totalCommunity > 3) {\n riskLevel = '🟥 High'; riskEmoji = '🟥'; riskText = 'High';\n} else if (totalCredentials > 2 || totalNodes > 5 || totalCommunity > 1) {\n riskLevel = '🟧 Moderate'; riskEmoji = '🟧'; riskText = 'Moderate';\n}\n\n// === Node type breakdown ===\nlet nodeTypeBreakdown = '';\nlet nodeTypeBreakdownHTML = '';\nif (Object.keys(nodeTypeStats).length > 0) {\n const sortedNodeTypes = Object.entries(nodeTypeStats).sort((a, b) => b[1] - a[1]);\n nodeTypeBreakdown = '\\n**Breakdown by node type:**\\n';\n nodeTypeBreakdownHTML = '<li><b>Breakdown by node type:</b><ul>';\n for (const [nodeType, count] of sortedNodeTypes) {\n const icon = getNodeIcon(nodeType);\n const simpleName = nodeType.replace('n8n-nodes-base.', '');\n nodeTypeBreakdown += ` - ${icon} ${simpleName}: ${count}\\n`;\n nodeTypeBreakdownHTML += `<li>${icon} ${simpleName}: ${count}</li>`;\n }\n nodeTypeBreakdownHTML += '</ul></li>';\n}\n\n// === SUMMARY (with unique credentials) ===\nconst uniqueCredCount = uniqueCredentials.size;\n\nconst summaryText =\n`## 📊 Audit Summary\\n\n- Credentials involved: ${totalCredentials} (${uniqueCredCount} unique)\n- Nodes involved: ${totalNodes}${nodeTypeBreakdown}\n- Community nodes: ${totalCommunity}\n- **Overall risk level: ${riskLevel}**\\n\\n`;\n\nmarkdown = markdown.replace(/^# 🔒.*?\\n\\n/, `$&${summaryText}`);\n\nconst emailSubject = `🔒 Audit Report ${project} – Risk ${riskEmoji} ${riskText}`;\n\n// === HTML summary ===\nhtml = html.replace(\n /<p><strong>Date:?<\\/strong>.*?<\\/p>/,\n `$&<h2>📊 Summary</h2>\n <ul>\n <li><b>Credentials involved:</b> ${totalCredentials} (${uniqueCredCount} unique)</li>\n <li><b>Nodes involved:</b> ${totalNodes}</li>\n ${nodeTypeBreakdownHTML}\n <li><b>Community nodes:</b> ${totalCommunity}</li>\n <li><b>Overall risk level:</b> ${riskLevel}</li>\n </ul>`\n);\n\nreturn [{\n json: {\n markdown,\n html,\n project,\n date,\n emailSubject,\n riskLevel: riskText,\n riskEmoji,\n uniqueCredentials: uniqueCredCount\n }\n}];"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "27bdda1e-fda2-4657-949b-5b231bae8d2a",
"name": "Doppelte WorkflowID filtern",
"type": "n8n-nodes-base.code",
"position": [
-1024,
64
],
"parameters": {
"jsCode": "// Récupération du tableau d'entrée\nconst locations = $json[\"Nodes Risk Report\"].sections[0].location;\n\n// Vérification\nif (!Array.isArray(locations)) {\n throw new Error(\"Le champ 'location' est introuvable ou n'est pas un tableau.\");\n}\n\n// Extraction des workflowId uniques\nconst uniqueWorkflows = Array.from(\n new Map(\n locations\n .filter(loc => loc.workflowId) // garde seulement ceux avec un ID\n .map(loc => [loc.workflowId, loc]) // on mappe workflowId → objet complet\n ).values()\n);\n\n// Sortie d'un item par workflow\nreturn uniqueWorkflows.map(wf => ({\n json: {\n workflowId: wf.workflowId,\n workflowName: wf.workflowName || 'Nom inconnu',\n nodeCount: locations.filter(l => l.workflowId === wf.workflowId).length,\n nodeTypes: Array.from(new Set(\n locations\n .filter(l => l.workflowId === wf.workflowId)\n .map(l => l.nodeType)\n )),\n }\n}));"
},
"typeVersion": 2
},
{
"id": "7c05f33f-787a-4ea8-9dcb-07349e6a1e46",
"name": "Letzte Ausführungen abrufen",
"type": "n8n-nodes-base.n8n",
"position": [
-768,
64
],
"parameters": {
"limit": 1,
"filters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.workflowId }}"
}
},
"options": {
"activeWorkflows": false
},
"resource": "execution",
"requestOptions": {}
},
"credentials": {
"n8nApi": {
"id": "l5HQ7xfVxL2LP772",
"name": "n8n account"
}
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "f2f6087e-70cc-4d18-988d-01fa1d7f7b79",
"name": "If Language",
"type": "n8n-nodes-base.if",
"position": [
-528,
64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "d0f639a0-be97-4dd4-a701-b35f85ccde45",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('Set Config Variables').first().json.Language }}",
"rightValue": "=FR"
}
]
},
"looseTypeValidation": true
},
"typeVersion": 2.2
},
{
"id": "7e88ca33-bf1f-4976-9b4d-2e98fc50e698",
"name": "Notizzettel",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1872,
-432
],
"parameters": {
"color": 4,
"height": 656,
"content": "## 1️⃣ Schedule Trigger (Weekly) \n**⏰ WEEKLY TRIGGER**\nAutomatically runs every Monday at 6 AM\n→ Change schedule in node settings if needed\n→ Can be set to daily, monthly, or custom cron"
},
"typeVersion": 1
},
{
"id": "6eeaba97-94de-4369-b0ce-8e962fa6a198",
"name": "Notizzettel1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1616,
-432
],
"parameters": {
"color": 4,
"height": 656,
"content": "## 2️⃣ Set Config Variables \n**⚙️ CONFIGURATION - EDIT THIS FIRST!**\n📧 email_to: your.email@domain.com\n📁 project_name: Your-Project-Name\n🌐 server_url: https://n8n.yourdomain.com\n ⚠️ NO trailing slash (/)!\n🌍 Language: \"EN\" or \"FR\"\n\n→ These variables control the entire workflow\n→ Must be configured before first run"
},
"typeVersion": 1
},
{
"id": "d1624f02-332d-4b7b-89ef-aceb3d3acd43",
"name": "Notizzettel2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1360,
-432
],
"parameters": {
"color": 5,
"height": 656,
"content": "## 3️⃣ Generate a security audit \n**🔍 SECURITY AUDIT GENERATOR**\nCalls N8N API to generate security audit\n\n📊 Analyzes:\n- Credentials risks\n- Dangerous nodes (Code, SSH, HTTP, etc.)\n- Instance security settings\n\n🔑 Required: N8N API credential\n→ Create API key in N8N Settings → API\n→ Add credential in this node"
},
"typeVersion": 1
},
{
"id": "7c1f7a9f-f0eb-4daa-99e2-592ac12036f8",
"name": "Notizzettel4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1104,
-432
],
"parameters": {
"color": 3,
"height": 656,
"content": "## 4️⃣ Filter duplicate WorkflowID \n**🔄 DEDUPLICATION**\nExtracts unique workflows from audit results\n\n→ Removes duplicate workflow entries\n→ Prepares data for execution lookup\n→ Automatic - no configuration needed\n"
},
"typeVersion": 1
},
{
"id": "78d9ef00-71f9-47c5-b5da-1b6d0db98242",
"name": "Notizzettel5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-848,
-432
],
"parameters": {
"color": 5,
"height": 656,
"content": "## 5️⃣ Get last executions\n**📊 EXECUTION STATUS FETCHER**\nGets last execution for each workflow\n\n→ Retrieves success/failure status\n→ Shows execution start/stop times\n→ Enriches report with real data\n\n🔑 Required: Same N8N API credential as node 3"
},
"typeVersion": 1
},
{
"id": "ae647e6b-448b-4fde-89f2-63b412822c18",
"name": "Notizzettel6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-592,
-432
],
"parameters": {
"color": 3,
"height": 656,
"content": "## 6️⃣ If Language\n\n**🌍 LANGUAGE ROUTER**\nRoutes to FR or EN formatter\n\nIf Language = \"FR\" → French report\nOtherwise → English report\n\n→ Based on variable set in node 2\n→ Automatic routing - no config needed"
},
"typeVersion": 1
},
{
"id": "45a3aaa6-9f14-4aec-80f0-22cca126e6a7",
"name": "Notizzettel7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-336,
-432
],
"parameters": {
"color": 3,
"height": 656,
"content": "## 7️⃣ Format Audit Report\n\n**FRENCH/ENGLISH FORMATTER**\n📝 Creates:\n- Markdown version\n- HTML email version\n- Email subject with risk level\n\n📊 Calculates:\n- Unique credentials count\n- Nodes breakdown by type\n- Overall risk level: 🟩 🟧 🟥\n\n→ Automatic - no configuration needed"
},
"typeVersion": 1
},
{
"id": "caf87b67-3966-4114-a9ef-24f32ff8d78f",
"name": "Notizzettel8",
"type": "n8n-nodes-base.stickyNote",
"position": [
-80,
-432
],
"parameters": {
"color": 5,
"height": 656,
"content": "## 8️⃣ Send Gmail (HTML)\n\n\n**📧 EMAIL SENDER**\nSends formatted HTML report via Gmail\n\n✉️ Sends to: Address from node 2 (email_to)\n📨 Format: Rich HTML with links & colors\n🔗 Includes: Direct links to workflows\n\n🔑 Required: Gmail OAuth2 credential\n→ Setup OAuth2 in Google Cloud Console\n→ Add Gmail credential in this node\n\n⚠️ Can be replaced with SMTP, Outlook, etc."
},
"typeVersion": 1
},
{
"id": "2ad8307a-b770-46b4-b8ea-478b490e80b3",
"name": "Notizzettel3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1872,
-960
],
"parameters": {
"width": 320,
"height": 496,
"content": "## 🎯 Quick Setup Checklist\n✅ 1. Create N8N API key (Settings → API)\n✅ 2. Setup Gmail OAuth2 credential\n✅ 3. Edit \"Set Config Variables\" node:\n - email_to\n - project_name\n - server_url (no trailing /)\n - Language (EN or FR)\n✅ 4. Test workflow manually\n✅ 5. Activate for weekly execution"
},
"typeVersion": 1
},
{
"id": "3b9d3299-d93e-4e5b-9f16-66cc40607402",
"name": "Notizzettel9",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1536,
-960
],
"parameters": {
"width": 304,
"height": 496,
"content": "## 💡 Configuration Tips\n**🔧 CUSTOMIZATION OPTIONS:**\n\nSchedule:\n→ Node 1: Change trigger frequency\n\nRisk Thresholds:\n→ Nodes 7: Edit JavaScript conditions\n if (totalCredentials > 5) { ... }\n\nEmail Recipients:\n→ Node 8: Add multiple emails in toList\n\nEmail Service:\n→ Node 8: Replace with SMTP/Outlook/etc."
},
"typeVersion": 1
},
{
"id": "d992360f-2eb3-4f03-9fbb-769ab9f56e52",
"name": "Notizzettel10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1216,
-960
],
"parameters": {
"width": 352,
"height": 496,
"content": "## 📊 Report Contents\n**📧 YOU WILL RECEIVE:**\n\n📊 Summary Section:\n- Total & unique credentials\n- Nodes breakdown by type\n- Community nodes count\n- Overall risk: 🟩 Low / 🟧 Moderate / 🟥 High\n\n🔐 Credentials Risk Report:\n- Exposed credentials list\n- Associated workflows\n\n🧩 Nodes Risk Report:\n- Dangerous nodes detected\n- 🔗 Clickable workflow links\n- 🟢/🔴 Last execution status\n- ⏰ Execution timestamps\n\n🏢 Instance Risk Report:\n- Security settings review\n- Recommendations"
},
"typeVersion": 1
},
{
"id": "efcad0e0-a50b-4e0f-95dc-5bc221b8cdd2",
"name": "Notizzettel11",
"type": "n8n-nodes-base.stickyNote",
"position": [
-848,
-960
],
"parameters": {
"width": 272,
"height": 496,
"content": "## ⚠️ Important Notes\n**🚨 BEFORE FIRST RUN:**\n\n1. Server URL Format:\n ✅ https://n8n.domain.com\n ❌ https://n8n.domain.com/\n\n2. Language Parameter:\n ✅ \"EN\" or \"FR\" (uppercase)\n ❌ \"en\" or \"English\"\n\n3. API Permissions:\n → N8N API key must have audit access\n → Check in Settings → API → Permissions\n\n4. Gmail Setup:\n → OAuth2 required (not just password)\n → Enable Gmail API in Google Cloud"
},
"typeVersion": 1
},
{
"id": "5d147ec8-9000-41c1-b67f-650666c23f95",
"name": "Notizzettel12",
"type": "n8n-nodes-base.stickyNote",
"position": [
-560,
-960
],
"parameters": {
"width": 288,
"height": 496,
"content": "## 🐛 Troubleshooting\n\n❌ Empty report?\n→ Check N8N API key permissions\n\n❌ Workflow links broken?\n→ Verify server_url format (no trailing /)\n\n❌ No execution status?\n→ Workflows must be executed at least once\n\n❌ Wrong language?\n→ Language must be exactly \"EN\" or \"FR\"\n\n❌ Email not sent?\n→ Check Gmail OAuth2 credential\n→ Verify email_to address is valid"
},
"typeVersion": 1
},
{
"id": "4ca7fc9a-1bd6-4514-b72f-23474abac940",
"name": "Notizzettel13",
"type": "n8n-nodes-base.stickyNote",
"position": [
-256,
-960
],
"parameters": {
"width": 416,
"height": 496,
"content": "## 📈 Expected Results\n**✅ WEEKLY EMAIL WITH:**\n\nSubject: \n\"🔒 Audit Report [Project] – Risk 🟧 Moderate\"\n\nContent:\n- Executive summary with metrics\n- Color-coded risk levels\n- Direct links to affected workflows\n- Real-time execution statuses\n- Actionable security recommendations\n\n📊 Typical execution: 10-20 seconds\n📧 Email arrives within 1 minute"
},
"typeVersion": 1
}
],
"pinData": {},
"connections": {
"f2f6087e-70cc-4d18-988d-01fa1d7f7b79": {
"main": [
[
{
"node": "fec96d1e-e966-4fcc-9900-9a8d32211008",
"type": "main",
"index": 0
}
],
[
{
"node": "2f877b5d-0bcc-43c1-804b-6ab835c98373",
"type": "main",
"index": 0
}
]
]
},
"7c05f33f-787a-4ea8-9dcb-07349e6a1e46": {
"main": [
[
{
"node": "f2f6087e-70cc-4d18-988d-01fa1d7f7b79",
"type": "main",
"index": 0
}
]
]
},
"628f28dc-b550-4501-b3f7-656756a84f0b": {
"main": [
[
{
"node": "57015778-7300-4c12-b7f2-c795b7316d59",
"type": "main",
"index": 0
}
]
]
},
"2f877b5d-0bcc-43c1-804b-6ab835c98373": {
"main": [
[
{
"node": "29de9117-4e5d-42c0-b2bb-b12b37cd6bf4",
"type": "main",
"index": 0
}
]
]
},
"fec96d1e-e966-4fcc-9900-9a8d32211008": {
"main": [
[
{
"node": "29de9117-4e5d-42c0-b2bb-b12b37cd6bf4",
"type": "main",
"index": 0
}
]
]
},
"57015778-7300-4c12-b7f2-c795b7316d59": {
"main": [
[
{
"node": "27bdda1e-fda2-4657-949b-5b231bae8d2a",
"type": "main",
"index": 0
}
]
]
},
"3de2498a-7b6d-4971-aaa5-01c708e9a7a6": {
"main": [
[
{
"node": "628f28dc-b550-4501-b3f7-656756a84f0b",
"type": "main",
"index": 0
}
]
]
},
"27bdda1e-fda2-4657-949b-5b231bae8d2a": {
"main": [
[
{
"node": "7c05f33f-787a-4ea8-9dcb-07349e6a1e46",
"type": "main",
"index": 0
}
]
]
}
}
}Wie verwende ich diesen Workflow?
Kopieren Sie den obigen JSON-Code, erstellen Sie einen neuen Workflow in Ihrer n8n-Instanz und wählen Sie "Aus JSON importieren". Fügen Sie die Konfiguration ein und passen Sie die Anmeldedaten nach Bedarf an.
Für welche Szenarien ist dieser Workflow geeignet?
Experte - Sicherheitsbetrieb
Ist es kostenpflichtig?
Dieser Workflow ist völlig kostenlos. Beachten Sie jedoch, dass Drittanbieterdienste (wie OpenAI API), die im Workflow verwendet werden, möglicherweise kostenpflichtig sind.
Verwandte Workflows
Matthieu
@neon8n8 years exp in IT over different kind of task, from support to sysadmin. Automation learner and builder. I will share simple workflow that can be used in various situation.
Diesen Workflow teilen