AutomationFlowsEmail & Gmail › Automated Weekly Security Audit Reports with Gmail Delivery

Automated Weekly Security Audit Reports with Gmail Delivery

ByMatthieu @neon8n on n8n.io

This workflow automatically generates and emails a comprehensive security audit report for your N8N instance every week. It identifies potential security risks related to: Credentials 🔑 : Exposed or insecure credentials Nodes 🧩 : Sensitive nodes (Code, HTTP Request, SSH, FTP,…

Cron / scheduled trigger★★★★☆ complexity23 nodesGmailn8n
Email & Gmail Trigger: Cron / scheduled Nodes: 23 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #10112 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "628f28dc-b550-4501-b3f7-656756a84f0b",
      "name": "Set Config Variables",
      "type": "n8n-nodes-base.set",
      "position": [
        -1552,
        64
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "af567071-143f-4361-9f18-12c730802196",
              "name": "email_to",
              "type": "string",
              "value": "user@example.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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3de2498a-7b6d-4971-aaa5-01c708e9a7a6",
      "name": "Schedule Trigger (Weekly)",
      "type": "n8n-nodes-base.cron",
      "position": [
        -1808,
        64
      ],
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "hour": 6,
              "mode": "everyWeek"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "57015778-7300-4c12-b7f2-c795b7316d59",
      "name": "Generate a security audit",
      "type": "n8n-nodes-base.n8n",
      "position": [
        -1280,
        64
      ],
      "parameters": {
        "resource": "audit",
        "operation": "generate",
        "requestOptions": {},
        "additionalOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "fec96d1e-e966-4fcc-9900-9a8d32211008",
      "name": "Format Audit Report - 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// \u2705 R\u00e9cup\u00e8re les r\u00e9sultats de la loop pr\u00e9c\u00e9dente, 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\u2019un tableau complet\n    } else if (j?.workflowId) {\n      workflowExecutions.push(j); // cas d\u2019un 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('\u26a0\ufe0f Impossible de lire les ex\u00e9cutions:', e.message);\n}\n\n\n// Si le premier \u00e9l\u00e9ment est lui-m\u00eame un tableau, on le \"d\u00e9plie\"\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(); // \u2705 Nouvel ensemble pour compter les credentials uniques\n\n// === HELPERS ===\nfunction getNodeIcon(nodeType) {\n  const iconMap = {\n    'n8n-nodes-base.code': '\ud83d\udcbb',\n    'n8n-nodes-base.function': '\u26a1',\n    'n8n-nodes-base.httpRequest': '\ud83c\udf10',\n    'n8n-nodes-base.executeCommand': '\u2328\ufe0f',\n    'n8n-nodes-base.ssh': '\ud83d\udd10',\n    'n8n-nodes-base.ftp': '\ud83d\udcc1',\n    'n8n-nodes-base.webhook': '\ud83e\ude9d'\n  };\n  return iconMap[nodeType] || '\u2699\ufe0f';\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\u2019on 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\u00e9s dans rapport:', Array.from(new Set(exampleIds.map(i => String(i).trim()))));\n\n\n// \u2705 Trouver les infos d'ex\u00e9cution 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(`\u2705 MATCH trouv\u00e9: ${loopId}`);\n      return true;\n    }\n    return false;\n  });\n\n  if (!match) console.log(`\u274c 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 = `### \ud83d\udd39 ${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 += `- \ud83e\udde9 ${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 += `- \ud83d\udd11 ${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**\ud83d\udccb 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> \ud83d\udca1 ${section.recommendation}\\n`;\n  return md + '\\n';\n}\n\nfunction formatSectionHTML(section) {\n  if (!section) return '';\n  let html = `<h3>\ud83d\udd39 ${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>\ud83e\udde9 ${pkg}</li>`;\n      } else if (loc.kind === 'credential') {\n        const credName = loc.name?.trim() || 'Credential sans nom';\n        uniqueCredentials.add(credName);\n        html += `<li>\ud83d\udd11 ${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        // \u2705 Construction du badge de statut et des horaires\n        let runInfo = '';\nif (run) {\n  const color = run.status === 'success' ? '\ud83d\udfe2' : '\ud83d\udd34';\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} \u2192 ${stopped})</small>`;\n} else {\n          runInfo = ` <small style=\"color:#aaa;\">\u26aa Non ex\u00e9cut\u00e9 r\u00e9cemment</small>`;\n        }\n\n        html += `<h4>\ud83d\udccb 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>\ud83d\udca1 ${section.recommendation}</blockquote>`;\n  return html;\n}\n\n// === SECURITY SETTINGS ===\nfunction formatSecuritySettings(settings) {\n  if (!settings) return { md: '', html: '' };\n  let md = `### \ud83d\udd39 Security settings\\nVoici les param\u00e8tres de s\u00e9curit\u00e9 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>\ud83d\udd39 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 = `# \ud83d\udd12 Rapport d'audit de s\u00e9curit\u00e9 ${project}\\n\\n**Date :** ${date}\\n\\n`;\nlet html = `<h1>\ud83d\udd12 Rapport d'audit de s\u00e9curit\u00e9 ${project}</h1><p><strong>Date :</strong> ${date}</p>`;\n\nconst reportIcons = {\n  'Credentials Risk Report': '\ud83d\udd10',\n  'Nodes Risk Report': '\ud83e\udde9',\n  'Instance Risk Report': '\ud83c\udfe2'\n};\n\nfor (const [title, report] of Object.entries(data)) {\n  sectionsPerReport.current = 0;\n  const icon = reportIcons[title] || '\ud83d\udcca';\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\u00c8SE ===\nlet riskLevel = '\ud83d\udfe9 Faible';\nlet riskEmoji = '\ud83d\udfe9';\nlet riskText = 'Faible';\nif (totalCredentials > 5 || totalNodes > 10 || totalCommunity > 3) {\n  riskLevel = '\ud83d\udfe5 \u00c9lev\u00e9'; riskEmoji = '\ud83d\udfe5'; riskText = '\u00c9lev\u00e9';\n} else if (totalCredentials > 2 || totalNodes > 5 || totalCommunity > 1) {\n  riskLevel = '\ud83d\udfe7 Mod\u00e9r\u00e9'; riskEmoji = '\ud83d\udfe7'; riskText = 'Mod\u00e9r\u00e9';\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\u00e9tail par type de node :**\\n';\n  nodeTypeBreakdownHTML = '<li><b>D\u00e9tail 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\u00c8SE (avec unique credentials) ===\nconst uniqueCredCount = uniqueCredentials.size;\n\nconst summaryText =\n`## \ud83d\udcca Synth\u00e8se de l'audit\\n\n- Credentials concern\u00e9s : ${totalCredentials} (${uniqueCredCount} uniques)\n- Nodes concern\u00e9s : ${totalNodes}${nodeTypeBreakdown}\n- Community nodes : ${totalCommunity}\n- **Niveau de risque global : ${riskLevel}**\\n\\n`;\n\nmarkdown = markdown.replace(/^# \ud83d\udd12.*?\\n\\n/, `$&${summaryText}`);\n\nconst emailSubject = `\ud83d\udd12 Rapport d'audit ${project} \u2013 Risque ${riskEmoji} ${riskText}`;\n\n// === HTML synth\u00e8se ===\nhtml = html.replace(\n  /<p><strong>Date :<\\/strong>.*?<\\/p>/,\n  `$&<h2>\ud83d\udcca Synth\u00e8se</h2>\n  <ul>\n    <li><b>Credentials concern\u00e9s :</b> ${totalCredentials} (${uniqueCredCount} uniques)</li>\n    <li><b>Nodes concern\u00e9s :</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": "Format Audit Report - 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// \u2705 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('\u26a0\ufe0f 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(); // \u2705 New set to count unique credentials\n\n// === HELPERS ===\nfunction getNodeIcon(nodeType) {\n  const iconMap = {\n    'n8n-nodes-base.code': '\ud83d\udcbb',\n    'n8n-nodes-base.function': '\u26a1',\n    'n8n-nodes-base.httpRequest': '\ud83c\udf10',\n    'n8n-nodes-base.executeCommand': '\u2328\ufe0f',\n    'n8n-nodes-base.ssh': '\ud83d\udd10',\n    'n8n-nodes-base.ftp': '\ud83d\udcc1',\n    'n8n-nodes-base.webhook': '\ud83e\ude9d'\n  };\n  return iconMap[nodeType] || '\u2699\ufe0f';\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// \u2705 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(`\u2705 MATCH found: ${loopId}`);\n      return true;\n    }\n    return false;\n  });\n\n  if (!match) console.log(`\u274c 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 = `### \ud83d\udd39 ${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 += `- \ud83e\udde9 ${pkg}\\n`;\n      } else if (loc.kind === 'credential') {\n        totalCredentials++;\n        const credName = loc.name?.trim() || 'Unnamed credential';\n        uniqueCredentials.add(credName);\n        md += `- \ud83d\udd11 ${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**\ud83d\udccb 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> \ud83d\udca1 ${section.recommendation}\\n`;\n  return md + '\\n';\n}\n\nfunction formatSectionHTML(section) {\n  if (!section) return '';\n  let html = `<h3>\ud83d\udd39 ${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>\ud83e\udde9 ${pkg}</li>`;\n      } else if (loc.kind === 'credential') {\n        const credName = loc.name?.trim() || 'Unnamed credential';\n        uniqueCredentials.add(credName);\n        html += `<li>\ud83d\udd11 ${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        // \u2705 Build status badge and timing info\nlet runInfo = '';\nif (run) {\n  const color = run.status === 'success' ? '\ud83d\udfe2' : '\ud83d\udd34';\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} \u2192 ${stopped})</small>`;\n} else {\n  runInfo = ` <small style=\"color:#aaa;\">\u26aa Not executed recently</small>`;\n}\n\n\n        html += `<h4>\ud83d\udccb 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>\ud83d\udca1 ${section.recommendation}</blockquote>`;\n  return html;\n}\n\n// === SECURITY SETTINGS ===\nfunction formatSecuritySettings(settings) {\n  if (!settings) return { md: '', html: '' };\n  let md = `### \ud83d\udd39 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>\ud83d\udd39 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 = `# \ud83d\udd12 Security Audit Report ${project}\\n\\n**Date:** ${date}\\n\\n`;\nlet html = `<h1>\ud83d\udd12 Security Audit Report ${project}</h1><p><strong>Date:</strong> ${date}</p>`;\n\nconst reportIcons = {\n  'Credentials Risk Report': '\ud83d\udd10',\n  'Nodes Risk Report': '\ud83e\udde9',\n  'Instance Risk Report': '\ud83c\udfe2'\n};\n\nfor (const [title, report] of Object.entries(data)) {\n  sectionsPerReport.current = 0;\n  const icon = reportIcons[title] || '\ud83d\udcca';\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 = '\ud83d\udfe9 Low';\nlet riskEmoji = '\ud83d\udfe9';\nlet riskText = 'Low';\nif (totalCredentials > 5 || totalNodes > 10 || totalCommunity > 3) {\n  riskLevel = '\ud83d\udfe5 High'; riskEmoji = '\ud83d\udfe5'; riskText = 'High';\n} else if (totalCredentials > 2 || totalNodes > 5 || totalCommunity > 1) {\n  riskLevel = '\ud83d\udfe7 Moderate'; riskEmoji = '\ud83d\udfe7'; 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`## \ud83d\udcca 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(/^# \ud83d\udd12.*?\\n\\n/, `$&${summaryText}`);\n\nconst emailSubject = `\ud83d\udd12 Audit Report ${project} \u2013 Risk ${riskEmoji} ${riskText}`;\n\n// === HTML summary ===\nhtml = html.replace(\n  /<p><strong>Date:?<\\/strong>.*?<\\/p>/,\n  `$&<h2>\ud83d\udcca 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": "Filter duplicate WorkflowID",
      "type": "n8n-nodes-base.code",
      "position": [
        -1024,
        64
      ],
      "parameters": {
        "jsCode": "// R\u00e9cup\u00e9ration du tableau d'entr\u00e9e\nconst locations = $json[\"Nodes Risk Report\"].sections[0].location;\n\n// V\u00e9rification\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 \u2192 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": "Get last executions",
      "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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "f2f6087e-70cc-4d18-988d-01fa1d7f7b79",
      "name": "If Language",
      "type": "n8n-nodes-base.if",
      "position": [
        -528,
        64
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "d0f639a0-be97-4dd4-a701-b35f85ccde45",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $('Set Config Variables').first().json.Language }}",
              "rightValue": "=FR"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "7e88ca33-bf1f-4976-9b4d-2e98fc50e698",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1872,
        -432
      ],
      "parameters": {
        "color": 4,
        "height": 656,
        "content": "## 1\ufe0f\u20e3 Schedule Trigger (Weekly) \n**\u23f0 WEEKLY TRIGGER**\nAutomatically runs every Monday at 6 AM\n\u2192 Change schedule in node settings if needed\n\u2192 Can be set to daily, monthly, or custom cron"
      },
      "typeVersion": 1
    },
    {
      "id": "6eeaba97-94de-4369-b0ce-8e962fa6a198",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1616,
        -432
      ],
      "parameters": {
        "color": 4,
        "height": 656,
        "content": "## 2\ufe0f\u20e3 Set Config Variables \n**\u2699\ufe0f CONFIGURATION - EDIT THIS FIRST!**\n\ud83d\udce7 email_to: your.email@domain.com\n\ud83d\udcc1 project_name: Your-Project-Name\n\ud83c\udf10 server_url: https://n8n.yourdomain.com\n   \u26a0\ufe0f NO trailing slash (/)!\n\ud83c\udf0d Language: \"EN\" or \"FR\"\n\n\u2192 These variables control the entire workflow\n\u2192 Must be configured before first run"
      },
      "typeVersion": 1
    },
    {
      "id": "d1624f02-332d-4b7b-89ef-aceb3d3acd43",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1360,
        -432
      ],
      "parameters": {
        "color": 5,
        "height": 656,
        "content": "## 3\ufe0f\u20e3 Generate a security audit \n**\ud83d\udd0d SECURITY AUDIT GENERATOR**\nCalls N8N API to generate security audit\n\n\ud83d\udcca Analyzes:\n- Credentials risks\n- Dangerous nodes (Code, SSH, HTTP, etc.)\n- Instance security settings\n\n\ud83d\udd11 Required: N8N API credential\n\u2192 Create API key in N8N Settings \u2192 API\n\u2192 Add credential in this node"
      },
      "typeVersion": 1
    },
    {
      "id": "7c1f7a9f-f0eb-4daa-99e2-592ac12036f8",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1104,
        -432
      ],
      "parameters": {
        "color": 3,
        "height": 656,
        "content": "## 4\ufe0f\u20e3 Filter duplicate WorkflowID \n**\ud83d\udd04 DEDUPLICATION**\nExtracts unique workflows from audit results\n\n\u2192 Removes duplicate workflow entries\n\u2192 Prepares data for execution lookup\n\u2192 Automatic - no configuration needed\n"
      },
      "typeVersion": 1
    },
    {
      "id": "78d9ef00-71f9-47c5-b5da-1b6d0db98242",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        -432
      ],
      "parameters": {
        "color": 5,
        "height": 656,
        "content": "## 5\ufe0f\u20e3 Get last executions\n**\ud83d\udcca EXECUTION STATUS FETCHER**\nGets last execution for each workflow\n\n\u2192 Retrieves success/failure status\n\u2192 Shows execution start/stop times\n\u2192 Enriches report with real data\n\n\ud83d\udd11 Required: Same N8N API credential as node 3"
      },
      "typeVersion": 1
    },
    {
      "id": "ae647e6b-448b-4fde-89f2-63b412822c18",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -592,
        -432
      ],
      "parameters": {
        "color": 3,
        "height": 656,
        "content": "## 6\ufe0f\u20e3 If Language\n\n**\ud83c\udf0d LANGUAGE ROUTER**\nRoutes to FR or EN formatter\n\nIf Language = \"FR\" \u2192 French report\nOtherwise \u2192 English report\n\n\u2192 Based on variable set in node 2\n\u2192 Automatic routing - no config needed"
      },
      "typeVersion": 1
    },
    {
      "id": "45a3aaa6-9f14-4aec-80f0-22cca126e6a7",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -336,
        -432
      ],
      "parameters": {
        "color": 3,
        "height": 656,
        "content": "## 7\ufe0f\u20e3 Format Audit Report\n\n**FRENCH/ENGLISH FORMATTER**\n\ud83d\udcdd Creates:\n- Markdown version\n- HTML email version\n- Email subject with risk level\n\n\ud83d\udcca Calculates:\n- Unique credentials count\n- Nodes breakdown by type\n- Overall risk level: \ud83d\udfe9 \ud83d\udfe7 \ud83d\udfe5\n\n\u2192 Automatic - no configuration needed"
      },
      "typeVersion": 1
    },
    {
      "id": "caf87b67-3966-4114-a9ef-24f32ff8d78f",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -432
      ],
      "parameters": {
        "color": 5,
        "height": 656,
        "content": "## 8\ufe0f\u20e3 Send Gmail (HTML)\n\n\n**\ud83d\udce7 EMAIL SENDER**\nSends formatted HTML report via Gmail\n\n\u2709\ufe0f Sends to: Address from node 2 (email_to)\n\ud83d\udce8 Format: Rich HTML with links & colors\n\ud83d\udd17 Includes: Direct links to workflows\n\n\ud83d\udd11 Required: Gmail OAuth2 credential\n\u2192 Setup OAuth2 in Google Cloud Console\n\u2192 Add Gmail credential in this node\n\n\u26a0\ufe0f Can be replaced with SMTP, Outlook, etc."
      },
      "typeVersion": 1
    },
    {
      "id": "2ad8307a-b770-46b4-b8ea-478b490e80b3",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1872,
        -960
      ],
      "parameters": {
        "width": 320,
        "height": 496,
        "content": "## \ud83c\udfaf Quick Setup Checklist\n\u2705 1. Create N8N API key (Settings \u2192 API)\n\u2705 2. Setup Gmail OAuth2 credential\n\u2705 3. Edit \"Set Config Variables\" node:\n      - email_to\n      - project_name\n      - server_url (no trailing /)\n      - Language (EN or FR)\n\u2705 4. Test workflow manually\n\u2705 5. Activate for weekly execution"
      },
      "typeVersion": 1
    },
    {
      "id": "3b9d3299-d93e-4e5b-9f16-66cc40607402",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1536,
        -960
      ],
      "parameters": {
        "width": 304,
        "height": 496,
        "content": "## \ud83d\udca1 Configuration Tips\n**\ud83d\udd27 CUSTOMIZATION OPTIONS:**\n\nSchedule:\n\u2192 Node 1: Change trigger frequency\n\nRisk Thresholds:\n\u2192 Nodes 7: Edit JavaScript conditions\n   if (totalCredentials > 5) { ... }\n\nEmail Recipients:\n\u2192 Node 8: Add multiple emails in toList\n\nEmail Service:\n\u2192 Node 8: Replace with SMTP/Outlook/etc."
      },
      "typeVersion": 1
    },
    {
      "id": "d992360f-2eb3-4f03-9fbb-769ab9f56e52",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1216,
        -960
      ],
      "parameters": {
        "width": 352,
        "height": 496,
        "content": "## \ud83d\udcca Report Contents\n**\ud83d\udce7 YOU WILL RECEIVE:**\n\n\ud83d\udcca Summary Section:\n- Total & unique credentials\n- Nodes breakdown by type\n- Community nodes count\n- Overall risk: \ud83d\udfe9 Low / \ud83d\udfe7 Moderate / \ud83d\udfe5 High\n\n\ud83d\udd10 Credentials Risk Report:\n- Exposed credentials list\n- Associated workflows\n\n\ud83e\udde9 Nodes Risk Report:\n- Dangerous nodes detected\n- \ud83d\udd17 Clickable workflow links\n- \ud83d\udfe2/\ud83d\udd34 Last execution status\n- \u23f0 Execution timestamps\n\n\ud83c\udfe2 Instance Risk Report:\n- Security settings review\n- Recommendations"
      },
      "typeVersion": 1
    },
    {
      "id": "efcad0e0-a50b-4e0f-95dc-5bc221b8cdd2",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        -960
      ],
      "parameters": {
        "width": 272,
        "height": 496,
        "content": "## \u26a0\ufe0f Important Notes\n**\ud83d\udea8 BEFORE FIRST RUN:**\n\n1. Server URL Format:\n   \u2705 https://n8n.domain.com\n   \u274c https://n8n.domain.com/\n\n2. Language Parameter:\n   \u2705 \"EN\" or \"FR\" (uppercase)\n   \u274c \"en\" or \"English\"\n\n3. API Permissions:\n   \u2192 N8N API key must have audit access\n   \u2192 Check in Settings \u2192 API \u2192 Permissions\n\n4. Gmail Setup:\n   \u2192 OAuth2 required (not just password)\n   \u2192 Enable Gmail API in Google Cloud"
      },
      "typeVersion": 1
    },
    {
      "id": "5d147ec8-9000-41c1-b67f-650666c23f95",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        -960
      ],
      "parameters": {
        "width": 288,
        "height": 496,
        "content": "## \ud83d\udc1b Troubleshooting\n\n\u274c Empty report?\n\u2192 Check N8N API key permissions\n\n\u274c Workflow links broken?\n\u2192 Verify server_url format (no trailing /)\n\n\u274c No execution status?\n\u2192 Workflows must be executed at least once\n\n\u274c Wrong language?\n\u2192 Language must be exactly \"EN\" or \"FR\"\n\n\u274c Email not sent?\n\u2192 Check Gmail OAuth2 credential\n\u2192 Verify email_to address is valid"
      },
      "typeVersion": 1
    },
    {
      "id": "4ca7fc9a-1bd6-4514-b72f-23474abac940",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -256,
        -960
      ],
      "parameters": {
        "width": 416,
        "height": 496,
        "content": "## \ud83d\udcc8 Expected Results\n**\u2705 WEEKLY EMAIL WITH:**\n\nSubject: \n\"\ud83d\udd12 Audit Report [Project] \u2013 Risk \ud83d\udfe7 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\ud83d\udcca Typical execution: 10-20 seconds\n\ud83d\udce7 Email arrives within 1 minute"
      },
      "typeVersion": 1
    }
  ],
  "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
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow automatically generates and emails a comprehensive security audit report for your N8N instance every week. It identifies potential security risks related to: Credentials 🔑 : Exposed or insecure credentials Nodes 🧩 : Sensitive nodes (Code, HTTP Request, SSH, FTP,…

Source: https://n8n.io/workflows/10112/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

This workflow automates the backup of all your n8n workflows to a specified Google Drive folder. It operates in two main phases: Orchestration (Scheduled Task): The workflow is initiated by a Schedule

n8n, Google Drive, Execution Data +3
Email & Gmail

This workflow is an automated invoice payment tracking and reminder system for the Polish accounting service iFirma.pl. It monitors unpaid and overdue invoices, then automatically sends escalating rem

HTTP Request, Stop And Error, Slack +1
Email & Gmail

Automatically extract structured information from emails using AI-powered document analysis. This workflow processes emails from specified domains, classifies them by type, and extracts structured dat

Gmail, HTTP Request, AWS S3 +1
Email & Gmail

This weekly workflow helps you stay on top of SEO visibility losses by automatically detecting when your previously strong keywords fall out of Google’s top 10 results.

N8N Nodes Dataforseo, Google Sheets, Gmail
Email & Gmail

What This Flow Does

Gmail, Google Sheets, HTTP Request +1