Informe semanal automatizado de auditoría de seguridad con Gmail
Este es unSecOpsflujo de automatización del dominio deautomatización que contiene 23 nodos.Utiliza principalmente nodos como If, N8n, Set, Code, Cron. Informe semanal automatizado de auditoría de seguridad con Gmail
- •Cuenta de Google y credenciales de API de Gmail
Categoría
{
"meta": {
"instanceId": "3568945d2a3f637c54ef170c26005913624678bc725f58cac81dfa10a714a2ca",
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "628f28dc-b550-4501-b3f7-656756a84f0b",
"name": "Establecer Variables de Configuración",
"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": "Enviar Gmail (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": "Activador Programado (Semanal)",
"type": "n8n-nodes-base.cron",
"position": [
-1808,
64
],
"parameters": {
"triggerTimes": {
"item": [
{
"hour": 6,
"mode": "everyWeek"
}
]
}
},
"typeVersion": 1
},
{
"id": "57015778-7300-4c12-b7f2-c795b7316d59",
"name": "Generar una auditoría de seguridad",
"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": "Formatear Informe de Auditoría - 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": "Formatear Informe de Auditoría - 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": "Filtrar WorkflowID duplicado",
"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": "Obtener últimas ejecuciones",
"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": "Si Idioma",
"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": "Nota Adhesiva",
"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": "Nota Adhesiva1",
"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": "Nota Adhesiva2",
"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": "Nota Adhesiva4",
"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": "Nota Adhesiva5",
"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": "Nota Adhesiva6",
"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": "Nota Adhesiva7",
"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": "Nota Adhesiva8",
"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": "Nota Adhesiva3",
"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": "Nota Adhesiva9",
"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": "Nota Adhesiva10",
"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": "Nota Adhesiva11",
"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": "Nota Adhesiva12",
"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": "Nota Adhesiva13",
"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
}
]
]
}
}
}¿Cómo usar este flujo de trabajo?
Copie el código de configuración JSON de arriba, cree un nuevo flujo de trabajo en su instancia de n8n y seleccione "Importar desde JSON", pegue la configuración y luego modifique la configuración de credenciales según sea necesario.
¿En qué escenarios es adecuado este flujo de trabajo?
Avanzado - Operaciones de seguridad
¿Es de pago?
Este flujo de trabajo es completamente gratuito, puede importarlo y usarlo directamente. Sin embargo, tenga en cuenta que los servicios de terceros utilizados en el flujo de trabajo (como la API de OpenAI) pueden requerir un pago por su cuenta.
Flujos de trabajo relacionados recomendados
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.
Compartir este flujo de trabajo