{
  "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
          }
        ]
      ]
    }
  }
}