{
  "name": "Ship Lean - n8n Workflow JSON Auditor",
  "nodes": [
    {
      "parameters": {},
      "id": "3f627f2f-60fc-4667-b1a5-71f9d97257d8",
      "name": "Manual test trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [220, 300]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "workflow_json",
              "name": "workflow_json",
              "type": "string",
              "value": "{\n  \"name\": \"Example risky imported workflow\",\n  \"nodes\": [\n    {\n      \"name\": \"Webhook Intake\",\n      \"type\": \"n8n-nodes-base.webhook\",\n      \"parameters\": {}\n    },\n    {\n      \"name\": \"AI Agent Draft Reply\",\n      \"type\": \"@n8n/n8n-nodes-langchain.agent\",\n      \"parameters\": {}\n    },\n    {\n      \"name\": \"Send Customer Email\",\n      \"type\": \"n8n-nodes-base.emailSend\",\n      \"parameters\": {}\n    },\n    {\n      \"name\": \"Raw API Call\",\n      \"type\": \"n8n-nodes-base.httpRequest\",\n      \"parameters\": {\n        \"headers\": {\n          \"Authorization\": \"Bearer sk-test-example-do-not-use\"\n        }\n      }\n    }\n  ],\n  \"connections\": {\n    \"Webhook Intake\": {\n      \"main\": [[{\"node\": \"AI Agent Draft Reply\", \"type\": \"main\", \"index\": 0}]]\n    },\n    \"AI Agent Draft Reply\": {\n      \"main\": [[{\"node\": \"Send Customer Email\", \"type\": \"main\", \"index\": 0}, {\"node\": \"Raw API Call\", \"type\": \"main\", \"index\": 0}]]\n    }\n  }\n}"
            }
          ]
        },
        "options": {}
      },
      "id": "060ecb41-daa2-4179-8f4e-8afac19672af",
      "name": "Paste workflow JSON here",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [480, 300]
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst raw = input.workflow_json || input.workflowJson || input.body?.workflow_json || input.body?.workflowJson || input.body || input;\n\nfunction parseWorkflow(value) {\n  if (typeof value === 'string') return JSON.parse(value);\n  if (value && typeof value === 'object') return value;\n  throw new Error('Expected workflow JSON as a string or object.');\n}\n\nconst workflow = parseWorkflow(raw);\nconst nodes = Array.isArray(workflow.nodes) ? workflow.nodes : [];\nconst connections = workflow.connections || {};\nconst findings = [];\n\nconst secretPatterns = [\n  { label: 'OpenAI-style key', regex: /sk-[A-Za-z0-9_-]{12,}/g },\n  { label: 'GitHub token', regex: /gh[pousr]_[A-Za-z0-9_]{12,}/g },\n  { label: 'Slack token', regex: /xox[baprs]-[A-Za-z0-9-]{12,}/g },\n  { label: 'Google API key', regex: /AIza[0-9A-Za-z_-]{20,}/g },\n  { label: 'Bearer token literal', regex: /Bearer\\s+[A-Za-z0-9._~+\\/-]{12,}/gi },\n  { label: 'API key assignment', regex: /(api[_-]?key|access[_-]?token|secret)\\s*[:=]\\s*[\"'][^\"']{8,}[\"']/gi },\n  { label: 'JWT-like token', regex: /eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}/g }\n];\n\nconst workflowString = JSON.stringify(workflow);\nfor (const pattern of secretPatterns) {\n  const matches = workflowString.match(pattern.regex) || [];\n  if (matches.length) {\n    findings.push({\n      severity: 'critical',\n      category: 'possible_secret',\n      node: null,\n      message: `${pattern.label} pattern found ${matches.length} time(s). Remove secrets before sharing or importing.`,\n      evidence: matches.slice(0, 3).map((match) => `${match.slice(0, 8)}...`)\n    });\n  }\n}\n\nconst riskyTypeRules = [\n  { match: /emailSend|gmail/i, label: 'email send node' },\n  { match: /slack|telegram|discord/i, label: 'message/notification node' },\n  { match: /twitter|x\\.com|linkedIn|facebook|instagram|reddit/i, label: 'social posting node' },\n  { match: /stripe|paypal|quickbooks|xero/i, label: 'money/accounting node' },\n  { match: /postgres|mysql|mongoDb|supabase|airtable|notion/i, label: 'database/write-capable node' },\n  { match: /httpRequest/i, label: 'HTTP request node' }\n];\n\nfunction nodeType(node) {\n  return String(node.type || '');\n}\n\nfunction nodeText(node) {\n  return JSON.stringify(node || {});\n}\n\nfunction isApprovalNode(node) {\n  const haystack = `${node.name || ''} ${node.type || ''} ${nodeText(node)}`.toLowerCase();\n  return /approval|approve|reject|human|review|manual|wait for|confirm/.test(haystack);\n}\n\nfunction isAiNode(node) {\n  const haystack = `${node.type || ''} ${node.name || ''}`.toLowerCase();\n  return /langchain|agent|openai|anthropic|lmchat|llm|chat model|gemini|mistral/.test(haystack);\n}\n\nfunction isTriggerNode(node) {\n  const type = nodeType(node).toLowerCase();\n  return /webhook|scheduletrigger|cron|interval|trigger/.test(type) && !/manualtrigger/.test(type);\n}\n\nfunction isRiskyOutbound(node) {\n  const type = nodeType(node);\n  const name = String(node.name || '');\n  return riskyTypeRules.some((rule) => rule.match.test(type) || rule.match.test(name));\n}\n\nfor (const node of nodes) {\n  const type = nodeType(node);\n  const text = nodeText(node);\n\n  if (isTriggerNode(node)) {\n    findings.push({\n      severity: 'medium',\n      category: 'automatic_trigger',\n      node: node.name,\n      message: 'Workflow has a non-manual trigger. Inspect before activating so it does not run unexpectedly.'\n    });\n  }\n\n  for (const rule of riskyTypeRules) {\n    if (rule.match.test(type) || rule.match.test(String(node.name || ''))) {\n      findings.push({\n        severity: /stripe|paypal|emailSend|gmail|twitter|linkedIn|facebook|instagram|reddit/i.test(type) ? 'high' : 'medium',\n        category: 'risky_action_node',\n        node: node.name,\n        message: `Found ${rule.label}. Confirm credentials, destination, and approval path before activation.`\n      });\n    }\n  }\n\n  if (/httpRequest/i.test(type) && /authorization|bearer|api[_-]?key|access[_-]?token|secret/i.test(text)) {\n    findings.push({\n      severity: 'high',\n      category: 'http_auth_inline',\n      node: node.name,\n      message: 'HTTP Request node appears to include auth/header/token text. Prefer n8n credentials over inline headers.'\n    });\n  }\n\n  if (/code/i.test(type) && /fetch\\(|axios|child_process|eval\\(|Function\\(/i.test(text)) {\n    findings.push({\n      severity: 'medium',\n      category: 'code_node_review',\n      node: node.name,\n      message: 'Code node contains network or dynamic execution patterns. Read the code before importing.'\n    });\n  }\n}\n\nconst hasErrorHandling = nodes.some((node) => /errorTrigger|error workflow/i.test(`${node.type || ''} ${node.name || ''}`));\nif (!hasErrorHandling) {\n  findings.push({\n    severity: 'medium',\n    category: 'missing_error_handling',\n    node: null,\n    message: 'No Error Trigger or obvious error workflow found.'\n  });\n}\n\nconst stickyCount = nodes.filter((node) => /stickyNote/i.test(node.type || '')).length;\nif (nodes.length >= 8 && stickyCount === 0) {\n  findings.push({\n    severity: 'low',\n    category: 'missing_documentation',\n    node: null,\n    message: 'No Sticky Notes found. Large imported workflows should explain what to change before activation.'\n  });\n}\n\nconst nodeByName = new Map(nodes.map((node) => [node.name, node]));\nfunction nextNames(name) {\n  const entry = connections[name];\n  const out = [];\n  for (const branch of Object.values(entry || {})) {\n    for (const path of branch || []) {\n      for (const edge of path || []) {\n        if (edge?.node) out.push(edge.node);\n      }\n    }\n  }\n  return out;\n}\n\nfunction riskyPathFrom(startName, maxDepth = 4) {\n  const queue = [{ name: startName, depth: 0, sawApproval: false, path: [startName] }];\n  const visited = new Set();\n  while (queue.length) {\n    const current = queue.shift();\n    const key = `${current.name}:${current.depth}:${current.sawApproval}`;\n    if (visited.has(key)) continue;\n    visited.add(key);\n\n    for (const next of nextNames(current.name)) {\n      const node = nodeByName.get(next);\n      if (!node) continue;\n      const sawApproval = current.sawApproval || isApprovalNode(node);\n      const path = [...current.path, next];\n      if (isRiskyOutbound(node) && !sawApproval) return path;\n      if (current.depth + 1 < maxDepth) queue.push({ name: next, depth: current.depth + 1, sawApproval, path });\n    }\n  }\n  return null;\n}\n\nfor (const node of nodes) {\n  if (!isAiNode(node)) continue;\n  const path = riskyPathFrom(node.name);\n  if (path) {\n    findings.push({\n      severity: 'high',\n      category: 'ai_to_outbound_without_approval',\n      node: node.name,\n      message: 'AI/LLM node appears connected to an outbound action without an obvious approval node in between.',\n      evidence: path.join(' -> ')\n    });\n  }\n}\n\nconst severityWeights = { critical: 35, high: 20, medium: 10, low: 4 };\nconst rawScore = findings.reduce((sum, finding) => sum + (severityWeights[finding.severity] || 0), 0);\nconst riskScore = Math.min(100, rawScore);\nconst verdict = riskScore >= 70 ? 'do_not_activate_yet' : riskScore >= 30 ? 'needs_manual_review' : 'safe_to_inspect';\nconst suggestedFixes = [];\nif (findings.some((finding) => finding.category === 'possible_secret' || finding.category === 'http_auth_inline')) suggestedFixes.push('Move secrets into n8n credentials and remove inline headers/tokens before sharing or activating.');\nif (findings.some((finding) => finding.category === 'ai_to_outbound_without_approval')) suggestedFixes.push('Add an approval step between AI judgment and customer-facing, public, financial, or database-write actions.');\nif (findings.some((finding) => finding.category === 'missing_error_handling')) suggestedFixes.push('Add an Error Trigger or error workflow before activating.');\nif (findings.some((finding) => finding.category === 'automatic_trigger')) suggestedFixes.push('Disable schedules/webhooks until credentials and destinations are reviewed.');\nif (!suggestedFixes.length) suggestedFixes.push('Still inspect credentials, node destinations, and execution history before activating imported workflows.');\n\nreturn [{\n  json: {\n    workflow_name: workflow.name || 'Untitled workflow',\n    node_count: nodes.length,\n    trigger_nodes: nodes.filter(isTriggerNode).map((node) => node.name),\n    ai_nodes: nodes.filter(isAiNode).map((node) => node.name),\n    outbound_nodes: nodes.filter(isRiskyOutbound).map((node) => node.name),\n    risk_score: riskScore,\n    verdict,\n    findings,\n    suggested_fixes: suggestedFixes,\n    rule: 'Import slowly. Audit first. Activate last.'\n  }\n}];"
      },
      "id": "67f787a7-c208-441b-8d11-d7c96bc071ea",
      "name": "Audit workflow JSON",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [740, 300]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "id": "do-not-activate",
                    "leftValue": "={{ $json.verdict }}",
                    "rightValue": "do_not_activate_yet",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "id": "manual-review",
                    "leftValue": "={{ $json.verdict }}",
                    "rightValue": "needs_manual_review",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "id": "3ac00ef7-fbe8-4f06-8e23-f83c1eb42f98",
      "name": "Route by audit verdict",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [1000, 300]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "status",
              "name": "status",
              "type": "string",
              "value": "do_not_activate_yet"
            },
            {
              "id": "next_step",
              "name": "next_step",
              "type": "string",
              "value": "Remove secrets, add approval gates, and inspect outbound actions before importing into a real workspace."
            }
          ]
        },
        "options": {}
      },
      "id": "4f674293-f4ec-45bb-847c-e45b4c217c59",
      "name": "High risk report",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1260, 120]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "status",
              "name": "status",
              "type": "string",
              "value": "needs_manual_review"
            },
            {
              "id": "next_step",
              "name": "next_step",
              "type": "string",
              "value": "Review findings, credentials, trigger settings, and approval path before activation."
            }
          ]
        },
        "options": {}
      },
      "id": "d8801310-6b98-40aa-8ce8-d8957d363084",
      "name": "Manual review report",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1260, 300]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "status",
              "name": "status",
              "type": "string",
              "value": "safe_to_inspect"
            },
            {
              "id": "next_step",
              "name": "next_step",
              "type": "string",
              "value": "Safe enough to inspect, but still review credentials and destinations before activation."
            }
          ]
        },
        "options": {}
      },
      "id": "505d8be6-1ef7-44b5-853b-3cbb6f235151",
      "name": "Safe to inspect report",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1260, 480]
    }
  ],
  "connections": {
    "Manual test trigger": {
      "main": [
        [
          {
            "node": "Paste workflow JSON here",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Paste workflow JSON here": {
      "main": [
        [
          {
            "node": "Audit workflow JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Audit workflow JSON": {
      "main": [
        [
          {
            "node": "Route by audit verdict",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by audit verdict": {
      "main": [
        [
          {
            "node": "High risk report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Manual review report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe to inspect report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": []
}
