Rapport automatisé hebdomadaire d'audit de sécurité avec Gmail
Ceci est unSecOpsworkflow d'automatisation du domainecontenant 23 nœuds.Utilise principalement des nœuds comme If, N8n, Set, Code, Cron. Rapport hebdomadaire automatisé d'audit de sécurité avec Gmail
- •Compte Google et informations d'identification Gmail API
Catégorie
{
"meta": {
"instanceId": "3568945d2a3f637c54ef170c26005913624678bc725f58cac81dfa10a714a2ca",
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "628f28dc-b550-4501-b3f7-656756a84f0b",
"name": "Définir les variables de configuration",
"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": "Envoyer 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": "Déclencheur planifié (Hebdomadaire)",
"type": "n8n-nodes-base.cron",
"position": [
-1808,
64
],
"parameters": {
"triggerTimes": {
"item": [
{
"hour": 6,
"mode": "everyWeek"
}
]
}
},
"typeVersion": 1
},
{
"id": "57015778-7300-4c12-b7f2-c795b7316d59",
"name": "Générer un audit de sécurité",
"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": "Formater le rapport d'audit - 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": "Formater le rapport d'audit - 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": "Filtrer les WorkflowID en double",
"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": "Obtenir les dernières exécutions",
"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 Langue",
"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": "Note adhésive",
"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": "Note adhésive1",
"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": "Note adhésive2",
"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": "Note adhésive4",
"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": "Note adhésive5",
"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": "Note adhésive6",
"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": "Note adhésive7",
"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": "Note adhésive8",
"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": "Note adhésive3",
"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": "Note adhésive9",
"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": "Note adhésive10",
"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": "Note adhésive11",
"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": "Note adhésive12",
"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": "Note adhésive13",
"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
}
]
]
}
}
}Comment utiliser ce workflow ?
Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.
Dans quelles scénarios ce workflow est-il adapté ?
Avancé - Opérations de sécurité
Est-ce payant ?
Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.
Workflows recommandés
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.
Partager ce workflow