8
n8n 中文网amn8n.com

使用Gmail发送的自动化每周安全审计报告

高级

这是一个SecOps领域的自动化工作流,包含 23 个节点。主要使用 If, N8n, Set, Code, Cron 等节点。 使用Gmail发送的自动化每周安全审计报告

前置要求
  • Google 账号和 Gmail API 凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "meta": {
    "instanceId": "3568945d2a3f637c54ef170c26005913624678bc725f58cac81dfa10a714a2ca",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "628f28dc-b550-4501-b3f7-656756a84f0b",
      "name": "设置配置变量",
      "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(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": "计划触发器(每周)",
      "type": "n8n-nodes-base.cron",
      "position": [
        -1808,
        64
      ],
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "hour": 6,
              "mode": "everyWeek"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "57015778-7300-4c12-b7f2-c795b7316d59",
      "name": "生成安全审计",
      "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": "格式化审计报告 - 法语",
      "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": "格式化审计报告 - 英语",
      "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": "过滤重复的 WorkflowID",
      "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": "获取最后执行记录",
      "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": "如果语言",
      "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": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1872,
        -432
      ],
      "parameters": {
        "color": 4,
        "height": 656,
        "content": "## 1️⃣ 计划触发器(每周)"
      },
      "typeVersion": 1
    },
    {
      "id": "6eeaba97-94de-4369-b0ce-8e962fa6a198",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1616,
        -432
      ],
      "parameters": {
        "color": 4,
        "height": 656,
        "content": "## 2️⃣ 设置配置变量"
      },
      "typeVersion": 1
    },
    {
      "id": "d1624f02-332d-4b7b-89ef-aceb3d3acd43",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1360,
        -432
      ],
      "parameters": {
        "color": 5,
        "height": 656,
        "content": "## 3️⃣ 生成安全审计"
      },
      "typeVersion": 1
    },
    {
      "id": "7c1f7a9f-f0eb-4daa-99e2-592ac12036f8",
      "name": "便签说明4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1104,
        -432
      ],
      "parameters": {
        "color": 3,
        "height": 656,
        "content": "## 4️⃣ 过滤重复的 WorkflowID"
      },
      "typeVersion": 1
    },
    {
      "id": "78d9ef00-71f9-47c5-b5da-1b6d0db98242",
      "name": "便签说明5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        -432
      ],
      "parameters": {
        "color": 5,
        "height": 656,
        "content": "## 5️⃣ 获取最后执行记录"
      },
      "typeVersion": 1
    },
    {
      "id": "ae647e6b-448b-4fde-89f2-63b412822c18",
      "name": "便签 6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -592,
        -432
      ],
      "parameters": {
        "color": 3,
        "height": 656,
        "content": "## 6️⃣ 如果语言"
      },
      "typeVersion": 1
    },
    {
      "id": "45a3aaa6-9f14-4aec-80f0-22cca126e6a7",
      "name": "便签 7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -336,
        -432
      ],
      "parameters": {
        "color": 3,
        "height": 656,
        "content": "## 7️⃣ 格式化审计报告"
      },
      "typeVersion": 1
    },
    {
      "id": "caf87b67-3966-4114-a9ef-24f32ff8d78f",
      "name": "## 为什么选择 4o 模型?👆",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -432
      ],
      "parameters": {
        "color": 5,
        "height": 656,
        "content": "## 8️⃣ 发送 Gmail(HTML)"
      },
      "typeVersion": 1
    },
    {
      "id": "2ad8307a-b770-46b4-b8ea-478b490e80b3",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1872,
        -960
      ],
      "parameters": {
        "width": 320,
        "height": 496,
        "content": "## 🎯 快速设置清单"
      },
      "typeVersion": 1
    },
    {
      "id": "3b9d3299-d93e-4e5b-9f16-66cc40607402",
      "name": "便签 9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1536,
        -960
      ],
      "parameters": {
        "width": 304,
        "height": 496,
        "content": "## 💡 配置提示"
      },
      "typeVersion": 1
    },
    {
      "id": "d992360f-2eb3-4f03-9fbb-769ab9f56e52",
      "name": "便签10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1216,
        -960
      ],
      "parameters": {
        "width": 352,
        "height": 496,
        "content": "## 📊 报告内容"
      },
      "typeVersion": 1
    },
    {
      "id": "efcad0e0-a50b-4e0f-95dc-5bc221b8cdd2",
      "name": "便签11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        -960
      ],
      "parameters": {
        "width": 272,
        "height": 496,
        "content": "## ⚠️ 重要说明"
      },
      "typeVersion": 1
    },
    {
      "id": "5d147ec8-9000-41c1-b67f-650666c23f95",
      "name": "便签12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        -960
      ],
      "parameters": {
        "width": 288,
        "height": 496,
        "content": "## 🐛 故障排除"
      },
      "typeVersion": 1
    },
    {
      "id": "4ca7fc9a-1bd6-4514-b72f-23474abac940",
      "name": "便签13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -256,
        -960
      ],
      "parameters": {
        "width": 416,
        "height": 496,
        "content": "## 📈 预期结果"
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "If Language": {
      "main": [
        [
          {
            "node": "Format Audit Report - FR",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Audit Report - EN",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get last executions": {
      "main": [
        [
          {
            "node": "If Language",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Config Variables": {
      "main": [
        [
          {
            "node": "Generate a security audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Audit Report - EN": {
      "main": [
        [
          {
            "node": "Send Gmail (HTML)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Audit Report - FR": {
      "main": [
        [
          {
            "node": "Send Gmail (HTML)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate a security audit": {
      "main": [
        [
          {
            "node": "Filter duplicate WorkflowID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger (Weekly)": {
      "main": [
        [
          {
            "node": "Set Config Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter duplicate WorkflowID": {
      "main": [
        [
          {
            "node": "Get last executions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 安全运维

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量23
分类1
节点类型7
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
Matthieu

Matthieu

@neon8n

8 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.

外部链接
在 n8n.io 查看

分享此工作流