8
n8n 한국어amn8n.com

Gmail을 사용한 자동화된 주간 보안 감사 보고서

고급

이것은SecOps분야의자동화 워크플로우로, 23개의 노드를 포함합니다.주로 If, N8n, Set, Code, Cron 등의 노드를 사용하며. Gmail을 통한 자동화 주간 보안 감사 보고서

사전 요구사항
  • Google 계정 및 Gmail API 인증 정보

사용된 노드 (23)

카테고리

워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 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": "Send 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": "감사 보고서 형식화 - 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": "감사 보고서 형식화 - 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": "중복 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️⃣ 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": "메모지1",
      "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": "메모지2",
      "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": "메모지4",
      "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": "메모지5",
      "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": "메모지6",
      "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": "메모지7",
      "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": "메모지8",
      "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": "메모지3",
      "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": "메모지9",
      "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": "메모지10",
      "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": "메모지11",
      "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": "메모지12",
      "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": "메모지13",
      "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
          }
        ]
      ]
    }
  }
}
자주 묻는 질문

이 워크플로우를 어떻게 사용하나요?

위의 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에서 보기

이 워크플로우 공유

카테고리

카테고리: 34