AutomationFlowsSlack & Telegram › Run Weekly Security Audits via the N8n Api, Data Tables, and Telegram

Run Weekly Security Audits via the N8n Api, Data Tables, and Telegram

BySerhii Bondarenko @serhiilabs on n8n.io

The native n8n security audit misses tokens pasted into node parameters, active workflows without an error handler, plain http:// calls and leftover pinned data. This workflow runs the native audit plus five custom checks weekly, scores the result 0-100 and reports what changed…

Cron / scheduled trigger★★★★☆ complexity24 nodesn8nData TableTelegram
Slack & Telegram Trigger: Cron / scheduled Nodes: 24 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #16164 — 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
{
  "name": "Run weekly security audits with the n8n API, Data Tables and Telegram",
  "nodes": [
    {
      "id": "428b5475-203c-486b-bb75-ced8342d8474",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        80
      ],
      "parameters": {
        "width": 480,
        "height": 1008,
        "content": "## Weekly security audit\n\nRuns n8n's [native security audit](https://docs.n8n.io/hosting/securing/security-audit/) plus five checks it does not cover, scores the instance 0-100, and tracks the trend against last week.\n\n**Who it's for:** admins of self-hosted n8n instances who want drift caught early: a webhook exposed without auth, a token pasted into a node parameter, an active workflow with no error handler.\n\n### How it works\n* A weekly schedule starts the run.\n* The n8n node generates the native audit and lists every workflow.\n* Five chained Code nodes scan all workflows for hardcoded secrets, unauthenticated webhooks, plain http:// URLs, missing error workflows and leftover pinned data.\n* Findings become a 0-100 score with a severity summary.\n* The previous snapshot from a [Data Table](https://docs.n8n.io/data/data-tables/) gives the diff: new findings, fixed findings, score delta.\n* The snapshot is saved back and the report goes to Telegram.\n\n### How to use\n1. Set the weekday and hour in the Schedule Trigger.\n2. Keep the checks you need: disable any check node, the chain keeps working without it.\n3. Work through the three grey setup notes below the flow: the n8n API credential, the Data Table, the Telegram chat.\n4. Shape the report text in the Compose message node.\n5. Run once manually to seed the history and check the message in Telegram, then activate and leave the rest to the schedule.\n\n### Requirements\n* n8n API key (Settings > n8n API)\n* [Data Tables](https://docs.n8n.io/data/data-tables/) available on your instance\n* [Telegram bot credential](https://docs.n8n.io/integrations/builtin/credentials/telegram/)\n\n### Need help?\nAsk in the [n8n Forum](https://community.n8n.io/)!"
      },
      "typeVersion": 1
    },
    {
      "id": "507c76ec-6f05-4e14-8952-9385ead9bd11",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## 1. Collect\nThe schedule fires weekly, then the [n8n node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.n8n/) generates the native security audit and fetches every workflow for the checks."
      },
      "typeVersion": 1
    },
    {
      "id": "f64f2578-ec52-468f-9a6a-a3ad1c16e128",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 1136,
        "height": 304,
        "content": "## 2. Custom checks\nFive gaps the [native audit](https://docs.n8n.io/hosting/securing/security-audit/) leaves open. Each check appends its findings to the item and passes it on, so any check can be disabled without breaking the chain."
      },
      "typeVersion": 1
    },
    {
      "id": "6b58cdb5-4ee1-4f09-849a-f675e80f21bc",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1808,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 864,
        "height": 304,
        "content": "## 3. Score and compare\nFindings become one 0-100 score, then diff against last week's snapshot from the [Data Table](https://docs.n8n.io/data/data-tables/): new, fixed, score delta."
      },
      "typeVersion": 1
    },
    {
      "id": "56064d57-c701-436f-96a8-3d84599b36e4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2704,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## 4. Report\n[Read more about the Telegram node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/)\n\nThe message template lives in Compose message - edit the layout there."
      },
      "typeVersion": 1
    },
    {
      "id": "86d97e6f-407e-4275-9f10-776e63d5359e",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        752
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 256,
        "content": "**Setup - n8n API credential**\n1. In n8n: Settings > n8n API > create an [API key](https://docs.n8n.io/api/authentication/). The `securityAudit:generate` and `workflow:list` scopes are all it needs.\n2. Save it as an n8n API credential and select it in both n8n nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "69f59989-682a-46ef-a445-91e49adcd7e4",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1808,
        752
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 256,
        "content": "**Setup - Data Table**\nCreate a [Data Table](https://docs.n8n.io/data/data-tables/) named `security_audit_history` with columns:\n- `runAt` - string\n- `score` - number\n- `summary` - string\n- `findings` - string\n\nSelect it in both Data Table nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "bc603222-3613-4493-87f9-e230499645ea",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2704,
        752
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 256,
        "content": "**Setup - Telegram**\nSet your [Telegram credential](https://docs.n8n.io/integrations/builtin/credentials/telegram/) and chat ID. Any channel works: the report text is in `{{ $json.text }}` with a subject line in `{{ $json.subject }}`, so this node swaps for Slack, email or a webhook."
      },
      "typeVersion": 1
    },
    {
      "id": "9be4d59e-46c9-4de1-9a85-9528a7812b5a",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3376,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 656,
        "content": "## Final output\n\nExample of the report this workflow sends to Telegram:\n\nn8n Security Audit - 2026-06-12\n\nScore: 75/100 (0 vs last run, was 75)\nFindings: 5 total - high 0, medium 3, low 2\n\nAll current findings:\n  - [low] Credentials not used in any active workflow (3)\n  - [low] Credentials not used in recently executed workflows (1)\n  - [medium] Official risky nodes (29)\n  - [medium] Outdated instance\n  - [medium] Security settings"
      },
      "typeVersion": 1
    },
    {
      "id": "39dd99ae-209e-4b6c-b697-ca566ae08796",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        544
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "eaeb6ca9-32f0-42c4-9254-b4a295e638aa",
      "name": "Generate a security audit",
      "type": "n8n-nodes-base.n8n",
      "position": [
        224,
        544
      ],
      "parameters": {
        "resource": "audit",
        "operation": "generate",
        "requestOptions": {},
        "additionalOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "8c8d6d50-be1d-4440-a7fe-35fa8016e58b",
      "name": "Get all workflows",
      "type": "n8n-nodes-base.n8n",
      "position": [
        448,
        544
      ],
      "parameters": {
        "filters": {
          "excludePinnedData": false
        },
        "requestOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6fbfdb92-1d97-4f2a-a09b-b5e94e4b942f",
      "name": "Check 2: unauthenticated webhooks",
      "type": "n8n-nodes-base.code",
      "position": [
        896,
        544
      ],
      "parameters": {
        "jsCode": "const findings = $input.first().json.findings ?? [];\n\nfor (const item of $('Get all workflows').all()) {\n  const wf = item.json;\n  for (const node of wf.nodes ?? []) {\n    if (node.disabled || node.type !== 'n8n-nodes-base.webhook') continue;\n\n    const auth = node.parameters?.authentication ?? 'none';\n    if (auth !== 'none') continue;\n\n    findings.push({\n      check: 'webhook-without-auth',\n      severity: wf.active ? 'high' : 'low',\n      workflowId: wf.id,\n      workflowName: wf.name,\n      workflowActive: !!wf.active,\n      node: node.name,\n      detail: wf.active\n        ? `Webhook \"${node.name}\" accepts unauthenticated requests on an active workflow.`\n        : `Webhook \"${node.name}\" has no authentication (workflow currently inactive).`,\n    });\n  }\n}\n\nreturn [{ json: { findings } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "ad527fd5-7902-43ae-b39c-e809c221c6f1",
      "name": "Check 3: insecure HTTP URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        544
      ],
      "parameters": {
        "jsCode": "const findings = $input.first().json.findings ?? [];\n\nfor (const item of $('Get all workflows').all()) {\n  const wf = item.json;\n  for (const node of wf.nodes ?? []) {\n    if (node.disabled || node.type !== 'n8n-nodes-base.httpRequest') continue;\n\n    const url = typeof node.parameters?.url === 'string' ? node.parameters.url.trim() : '';\n    if (url.startsWith('=') || /\\{\\{.*\\}\\}/.test(url)) continue;\n    if (!/^http:\\/\\//i.test(url)) continue;\n    if (/^http:\\/\\/(localhost|127\\.0\\.0\\.1|\\[::1\\])/i.test(url)) continue;\n\n    findings.push({\n      check: 'insecure-http-url',\n      severity: wf.active ? 'medium' : 'low',\n      workflowId: wf.id,\n      workflowName: wf.name,\n      workflowActive: !!wf.active,\n      node: node.name,\n      url,\n      detail: `HTTP Request node \"${node.name}\" calls a plain http:// URL - data and credentials travel unencrypted.`,\n    });\n  }\n}\n\nreturn [{ json: { findings } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "203577ba-e0f8-479e-8a81-87bb5778964a",
      "name": "Check 4: missing error workflows",
      "type": "n8n-nodes-base.code",
      "position": [
        1344,
        544
      ],
      "parameters": {
        "jsCode": "const findings = $input.first().json.findings ?? [];\n\nfor (const item of $('Get all workflows').all()) {\n  const wf = item.json;\n  if (!wf.active || wf.settings?.errorWorkflow) continue;\n\n  findings.push({\n    check: 'no-error-workflow',\n    severity: 'medium',\n    workflowId: wf.id,\n    workflowName: wf.name,\n    workflowActive: true,\n    detail: `Active workflow \"${wf.name}\" has no error workflow set - failures run silently with no alert.`,\n  });\n}\n\nreturn [{ json: { findings } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "21922d54-ef83-49e3-b14c-ddd4f1ef6ce3",
      "name": "Check 5: pinned data",
      "type": "n8n-nodes-base.code",
      "position": [
        1568,
        544
      ],
      "parameters": {
        "jsCode": "const findings = $input.first().json.findings ?? [];\n\nfor (const item of $('Get all workflows').all()) {\n  const wf = item.json;\n  const pinnedNodes = Object.keys(wf.pinData ?? {});\n  if (pinnedNodes.length === 0) continue;\n\n  findings.push({\n    check: 'pinned-data',\n    severity: wf.active ? 'medium' : 'low',\n    workflowId: wf.id,\n    workflowName: wf.name,\n    workflowActive: !!wf.active,\n    pinnedNodes,\n    detail: `Workflow \"${wf.name}\" has pinned data on: ${pinnedNodes.join(', ')}. Pinned data overrides live data.`,\n  });\n}\n\nreturn [{ json: { findings } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "713feca5-a578-4b15-ae32-83848f4c178f",
      "name": "Score the audit",
      "type": "n8n-nodes-base.code",
      "position": [
        1856,
        544
      ],
      "parameters": {
        "jsCode": "const customFindings = $input.first().json.findings ?? [];\n\nconst findings = customFindings.map((f) => ({\n  source: 'custom',\n  category: f.check,\n  severity: f.severity,\n  workflow: f.workflowName ?? null,\n  node: f.node ?? null,\n  detail: f.detail,\n}));\n\n// The webhook check already reports this native section with per-workflow context - skip the duplicate.\nconst SKIP_SECTION = 'Unprotected webhooks in instance';\nconst NATIVE_SEVERITY = {\n  credentials: 'low', database: 'high', nodes: 'medium', filesystem: 'medium', instance: 'medium',\n};\n\nconst audit = $('Generate a security audit').first().json;\n\nfor (const report of Object.values(audit)) {\n  // Non-report values appear when the audit node is disabled (passthrough data) - skip them.\n  if (!report || !Array.isArray(report.sections)) continue;\n  for (const section of report.sections) {\n    if (section.title === SKIP_SECTION) continue;\n    const locations = section.location?.length ?? 0;\n    findings.push({\n      source: 'native',\n      category: report.risk,\n      severity: NATIVE_SEVERITY[report.risk] ?? 'medium',\n      workflow: null,\n      node: null,\n      detail: locations ? `${section.title} (${locations})` : section.title,\n    });\n  }\n}\n\nconst WEIGHTS = { high: 15, medium: 7, low: 2, info: 0 };\nconst summary = { high: 0, medium: 0, low: 0, info: 0, total: findings.length };\nlet penalty = 0;\nfor (const f of findings) {\n  const severity = f.severity in WEIGHTS ? f.severity : 'medium';\n  summary[severity] += 1;\n  penalty += WEIGHTS[severity];\n}\n\nreturn [{ json: {\n  generatedAt: new Date().toISOString(),\n  score: Math.max(0, 100 - penalty),\n  summary,\n  findings,\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "aa48665d-b03c-4a42-9cd9-f319f8c6e416",
      "name": "Get previous snapshot",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        2080,
        544
      ],
      "parameters": {
        "limit": 1,
        "orderBy": true,
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "security_audit_history"
        },
        "orderByColumn": "runAt"
      },
      "typeVersion": 1.1,
      "alwaysOutputData": true
    },
    {
      "id": "99673f0d-9be7-458c-8a32-c89fb6445d50",
      "name": "Diff vs last snapshot",
      "type": "n8n-nodes-base.code",
      "position": [
        2304,
        544
      ],
      "parameters": {
        "jsCode": "const current = $('Score the audit').first().json;\n\nconst prevRow = $('Get previous snapshot').all()[0]?.json;\nconst previous = prevRow?.findings ? prevRow : null;\n\nlet previousFindings = [];\nif (previous) {\n  try {\n    previousFindings = JSON.parse(previous.findings) ?? [];\n  } catch {\n    previousFindings = [];\n  }\n}\n\nconst keyOf = (f) => {\n  const detail = String(f.detail ?? '').replace(/ \\(\\d+\\)$/, '');\n  return [f.source, f.category, f.workflow ?? '', f.node ?? '', detail].join('|');\n};\n\nconst currentKeys = new Set(current.findings.map(keyOf));\nconst previousKeys = new Set(previousFindings.map(keyOf));\n\nconst newFindings = current.findings.filter((f) => !previousKeys.has(keyOf(f)));\nconst fixedFindings = previousFindings.filter((f) => !currentKeys.has(keyOf(f)));\n\nreturn [{ json: {\n  generatedAt: current.generatedAt,\n  score: current.score,\n  summary: current.summary,\n  findings: current.findings,\n  hasPrevious: previous !== null,\n  prevScore: previous ? Number(previous.score) : null,\n  scoreDelta: previous ? current.score - Number(previous.score) : null,\n  newCount: newFindings.length,\n  fixedCount: fixedFindings.length,\n  newFindings,\n  fixedFindings,\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "8c34fd68-665b-467e-bffb-59de9037dfa4",
      "name": "Save snapshot",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        2528,
        544
      ],
      "parameters": {
        "columns": {
          "value": {
            "runAt": "={{ $json.generatedAt }}",
            "score": "={{ $json.score }}",
            "summary": "={{ JSON.stringify($json.summary) }}",
            "findings": "={{ JSON.stringify($json.findings) }}"
          },
          "schema": [
            {
              "id": "runAt",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "runAt",
              "defaultMatch": false
            },
            {
              "id": "score",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "score",
              "defaultMatch": false
            },
            {
              "id": "summary",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "summary",
              "defaultMatch": false
            },
            {
              "id": "findings",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "findings",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "security_audit_history"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "46fa47c0-9a52-4b78-a445-d3e7e1c9e1ad",
      "name": "Prepare report fields",
      "type": "n8n-nodes-base.code",
      "position": [
        2752,
        544
      ],
      "parameters": {
        "jsCode": "const diff = $('Diff vs last snapshot').first().json;\n\n// The message is sent with Telegram parse mode HTML - escape user-controlled names in findings.\nconst esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst line = (f) => `  - [${f.severity}] ${esc(f.detail)}${f.workflow ? ` [${esc(f.workflow)}]` : ''}`;\n\nreturn [{ json: {\n  date: diff.generatedAt.slice(0, 10),\n  score: diff.score,\n  summary: diff.summary,\n  scoreLine: diff.hasPrevious\n    ? `Score: ${diff.score}/100 (${diff.scoreDelta > 0 ? '+' : ''}${diff.scoreDelta} vs last run, was ${diff.prevScore})`\n    : `Score: ${diff.score}/100 (first run, no previous snapshot)`,\n  newCount: diff.newCount,\n  fixedCount: diff.fixedCount,\n  newList: diff.newFindings.map(line).join('\\n'),\n  fixedList: diff.fixedFindings.map((f) => `  - ${esc(f.detail)}`).join('\\n'),\n  currentList: diff.findings.map(line).join('\\n'),\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "6c442115-fade-4ffe-bee2-05ff04c4903a",
      "name": "Compose message",
      "type": "n8n-nodes-base.set",
      "position": [
        2976,
        544
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "4ff086b7-6022-4213-b069-8044439f9b2f",
              "name": "subject",
              "type": "string",
              "value": "=n8n Security Audit: {{ $json.score }}/100, {{ $json.summary.total }} findings"
            },
            {
              "id": "6e19fc5b-e6c6-4fea-91b1-7c9b34d17235",
              "name": "text",
              "type": "string",
              "value": "=n8n Security Audit - {{ $json.date }}\n\n{{ $json.scoreLine }}\nFindings: {{ $json.summary.total }} total - high {{ $json.summary.high }}, medium {{ $json.summary.medium }}, low {{ $json.summary.low }}\n\n{{ $json.newCount > 0 ? 'NEW (' + $json.newCount + '):\\n' + $json.newList + '\\n\\n' : '' }}{{ $json.fixedCount > 0 ? 'FIXED (' + $json.fixedCount + '):\\n' + $json.fixedList + '\\n\\n' : '' }}All current findings:\n{{ $json.currentList || '  none - clean instance' }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "91c7c11f-53bb-43bc-9321-7ad76eb33584",
      "name": "Send report to Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3200,
        544
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "chatId": "",
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "c5720886-3690-4231-ae94-5a67ddce48d5",
      "name": "Check 1: hardcoded secrets",
      "type": "n8n-nodes-base.code",
      "position": [
        672,
        544
      ],
      "parameters": {
        "jsCode": "const findings = $input.first().json.findings ?? [];\n\nconst SECRET_PATTERNS = [\n  { id: 'openai-key', re: /\\bsk-[A-Za-z0-9_-]{20,}/ },\n  { id: 'github-token', re: /\\b(ghp|gho|ghs|ghr|ghu)_[A-Za-z0-9]{30,}/ },\n  { id: 'github-pat', re: /\\bgithub_pat_[A-Za-z0-9_]{30,}/ },\n  { id: 'slack-token', re: /\\bxox[baprs]-[A-Za-z0-9-]{10,}/ },\n  { id: 'aws-access-key', re: /\\bAKIA[0-9A-Z]{16}\\b/ },\n  { id: 'google-api-key', re: /\\bAIza[0-9A-Za-z_-]{35}/ },\n  { id: 'stripe-secret', re: /\\b(sk|rk)_live_[A-Za-z0-9]{20,}/ },\n  { id: 'bearer-token', re: /\\bBearer\\s+[A-Za-z0-9._-]{20,}/i },\n  { id: 'private-key', re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },\n  { id: 'jwt', re: /\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}/ },\n];\n\nconst SECRET_KEY_RE = /(password|passwd|secret|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key|auth[_-]?token|access[_-]?token|api[_-]?token|token)/i;\nconst PLACEHOLDER_RE = /^(x+|\\*+|<.*>|your[_-].*|change[_-]?me|placeholder|example|test|dummy)$/i;\n\nconst isExpression = (value) => typeof value === 'string' && (value.startsWith('=') || /\\{\\{.*\\}\\}/.test(value));\n\nconst looksLikeSecretValue = (value) => {\n  if (typeof value !== 'string' || value.length < 16) return false;\n  if (isExpression(value) || PLACEHOLDER_RE.test(value.trim())) return false;\n  return /[A-Za-z]/.test(value) && /[0-9]/.test(value);\n};\n\nfunction scan(value, path, keyHint, onHit) {\n  if (typeof value === 'string') {\n    for (const pattern of SECRET_PATTERNS) {\n      if (pattern.re.test(value)) onHit(pattern.id, path);\n    }\n    if (keyHint && SECRET_KEY_RE.test(keyHint) && looksLikeSecretValue(value)) {\n      onHit('hardcoded-value', path);\n    }\n    return;\n  }\n  if (Array.isArray(value)) {\n    value.forEach((el, i) => {\n      if (el && typeof el === 'object' && typeof el.name === 'string' && 'value' in el) {\n        scan(el.value, `${path}[${el.name}]`, el.name, onHit);\n      } else {\n        scan(el, `${path}[${i}]`, keyHint, onHit);\n      }\n    });\n    return;\n  }\n  if (value && typeof value === 'object') {\n    for (const key of Object.keys(value)) {\n      scan(value[key], path ? `${path}.${key}` : key, key, onHit);\n    }\n  }\n}\n\nfor (const item of $('Get all workflows').all()) {\n  const wf = item.json;\n  for (const node of wf.nodes ?? []) {\n    if (node.disabled) continue;\n    scan(node.parameters ?? {}, '', '', (secretType, location) => {\n      findings.push({\n        check: 'hardcoded-secret',\n        severity: wf.active ? 'high' : 'medium',\n        workflowId: wf.id,\n        workflowName: wf.name,\n        workflowActive: !!wf.active,\n        node: node.name,\n        secretType,\n        location,\n        detail: `Hardcoded secret (${secretType}) in node \"${node.name}\" at ${location}. Use a credential instead.`,\n      });\n    });\n  }\n}\n\nreturn [{ json: { findings } }];"
      },
      "typeVersion": 2
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Save snapshot": {
      "main": [
        [
          {
            "node": "Prepare report fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compose message": {
      "main": [
        [
          {
            "node": "Send report to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score the audit": {
      "main": [
        [
          {
            "node": "Get previous snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Generate a security audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get all workflows": {
      "main": [
        [
          {
            "node": "Check 1: hardcoded secrets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: pinned data": {
      "main": [
        [
          {
            "node": "Score the audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Diff vs last snapshot": {
      "main": [
        [
          {
            "node": "Save snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get previous snapshot": {
      "main": [
        [
          {
            "node": "Diff vs last snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare report fields": {
      "main": [
        [
          {
            "node": "Compose message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate a security audit": {
      "main": [
        [
          {
            "node": "Get all workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: hardcoded secrets": {
      "main": [
        [
          {
            "node": "Check 2: unauthenticated webhooks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: insecure HTTP URLs": {
      "main": [
        [
          {
            "node": "Check 4: missing error workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: missing error workflows": {
      "main": [
        [
          {
            "node": "Check 5: pinned data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: unauthenticated webhooks": {
      "main": [
        [
          {
            "node": "Check 3: insecure HTTP URLs",
            "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

The native n8n security audit misses tokens pasted into node parameters, active workflows without an error handler, plain http:// calls and leftover pinned data. This workflow runs the native audit plus five custom checks weekly, scores the result 0-100 and reports what changed…

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

More Slack & Telegram workflows → · Browse all categories →

Related workflows

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

Slack & Telegram

A robust workflow to back up and synchronize your n8n workflows to a GitHub repository, with intelligent change detection and support for file renames.

GitHub, n8n, Telegram +1
Slack & Telegram

TelegramQuery. Uses httpRequest, dataTable, postgres, telegram. Scheduled trigger; 26 nodes.

HTTP Request, Data Table, Postgres +1
Slack & Telegram

This workflow is for system administrators or self-hosted n8n users who want to automatically check and update their n8n instance to the latest version — with Telegram notifications for every step. Th

HTTP Request, Telegram, n8n +2
Slack & Telegram

This n8n template demonstrates how to automatically fetch upcoming movie releases from TMDB and let users add selected movies to their Google Calendar directly from Telegram. On a daily schedule, the

HTTP Request, Data Table, Telegram +2
Slack & Telegram

Telegram Filter. Uses telegram, scheduleTrigger, stickyNote, n8n. Scheduled trigger; 10 nodes.

Telegram, n8n