UNPKG

n8n-mcp

Version:

Integration between n8n workflow automation and Model Context Protocol (MCP)

194 lines 8.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.scanWorkflows = scanWorkflows; const credential_scanner_1 = require("./credential-scanner"); function checkHardcodedSecrets(workflow) { const detections = (0, credential_scanner_1.scanWorkflow)({ id: workflow.id, name: workflow.name, nodes: workflow.nodes, settings: workflow.settings, staticData: workflow.staticData, }); return detections.map((detection, index) => { const workflowId = workflow.id ?? ''; const nodeName = detection.location.nodeName ?? ''; const isPii = detection.category.toLowerCase() === 'pii'; return { id: `CRED-${String(index + 1).padStart(3, '0')}`, severity: detection.severity, category: 'hardcoded_secrets', title: `Hardcoded ${detection.label} detected`, description: `Found a hardcoded ${detection.label} (${detection.category}) in ${nodeName ? `node "${nodeName}"` : 'workflow-level settings'}. Masked value: ${detection.maskedSnippet ?? 'N/A'}.`, recommendation: isPii ? 'Review whether this PII is necessary in the workflow. If it is test data or a placeholder, consider using n8n expressions or environment variables instead of hardcoded values.' : 'Move this secret into n8n credentials. The agent can extract the hardcoded value from the workflow, create a credential, and update the node automatically.', remediationType: isPii ? 'review_recommended' : 'auto_fixable', remediation: isPii ? [] : [ { tool: 'n8n_get_workflow', args: { id: workflowId }, description: `Fetch workflow to extract the hardcoded ${detection.label} from node "${nodeName}"`, }, { tool: 'n8n_manage_credentials', args: { action: 'create', type: 'httpHeaderAuth' }, description: `Create credential with the extracted value (choose appropriate type for ${detection.label})`, }, { tool: 'n8n_update_partial_workflow', args: { id: workflowId, operations: [{ type: 'updateNode', nodeName }] }, description: `Update node to use credential and remove hardcoded value`, }, ], location: { workflowId, workflowName: workflow.name, workflowActive: workflow.active, nodeName: detection.location.nodeName, nodeType: detection.location.nodeType, }, }; }); } function checkUnauthenticatedWebhooks(workflow) { const findings = []; let sequence = 0; for (const node of workflow.nodes) { const nodeTypeLower = (node.type ?? '').toLowerCase(); if (nodeTypeLower.includes('respondtowebhook')) { continue; } if (!nodeTypeLower.includes('webhook') && !nodeTypeLower.includes('formtrigger')) { continue; } const auth = node.parameters?.authentication; if (typeof auth === 'string' && auth !== '' && auth !== 'none') { continue; } sequence++; const workflowId = workflow.id ?? ''; const isActive = workflow.active === true; findings.push({ id: `WEBHOOK-${String(sequence).padStart(3, '0')}`, severity: isActive ? 'high' : 'medium', category: 'unauthenticated_webhooks', title: `Unauthenticated webhook: "${node.name}"`, description: `Webhook node "${node.name}" (${node.type}) has no authentication configured.${isActive ? ' This workflow is active and publicly accessible.' : ''} Anyone with the webhook URL can trigger this workflow.`, recommendation: 'Add authentication to the webhook node. Header-based authentication with a random secret is the simplest approach.', remediationType: 'auto_fixable', remediation: [ { tool: 'n8n_manage_credentials', args: { action: 'create', type: 'httpHeaderAuth' }, description: `Create httpHeaderAuth credential with a generated random secret`, }, { tool: 'n8n_update_partial_workflow', args: { id: workflowId, operations: [{ type: 'updateNode', nodeName: node.name }] }, description: `Set authentication to "headerAuth" and assign the credential`, }, ], location: { workflowId, workflowName: workflow.name, workflowActive: isActive, nodeName: node.name, nodeType: node.type, }, }); } return findings; } function checkErrorHandlingGaps(workflow) { if (workflow.nodes.length < 3) { return []; } const hasContinueOnFail = workflow.nodes.some((node) => node.continueOnFail === true); const hasOnErrorHandling = workflow.nodes.some((node) => typeof node.onError === 'string' && node.onError !== 'stopWorkflow'); const hasErrorTrigger = workflow.nodes.some((node) => (node.type ?? '').toLowerCase() === 'n8n-nodes-base.errortrigger'); if (hasContinueOnFail || hasOnErrorHandling || hasErrorTrigger) { return []; } return [ { id: 'ERR-001', severity: 'medium', category: 'error_handling', title: `No error handling in workflow "${workflow.name}"`, description: `Workflow "${workflow.name}" has ${workflow.nodes.length} nodes but no error handling configured. There are no nodes with continueOnFail enabled, no custom onError behavior, and no Error Trigger node.`, recommendation: 'Add error handling to prevent silent failures. Consider adding an Error Trigger node for global error notifications, or set continueOnFail on critical nodes that should not block the workflow.', remediationType: 'review_recommended', location: { workflowId: workflow.id ?? '', workflowName: workflow.name, workflowActive: workflow.active, }, }, ]; } function checkDataRetentionSettings(workflow) { const settings = workflow.settings; if (!settings) { return []; } const savesAllData = settings.saveDataErrorExecution === 'all' && settings.saveDataSuccessExecution === 'all'; if (!savesAllData) { return []; } return [ { id: 'RETENTION-001', severity: 'low', category: 'data_retention', title: `Excessive data retention in workflow "${workflow.name}"`, description: `Workflow "${workflow.name}" is configured to save execution data for both successful and failed executions. This may store sensitive data in the n8n database longer than necessary.`, recommendation: 'Review data retention settings. Consider setting saveDataSuccessExecution to "none" for workflows that process sensitive data, or configure execution data pruning at the instance level.', remediationType: 'user_action_needed', location: { workflowId: workflow.id ?? '', workflowName: workflow.name, workflowActive: workflow.active, }, }, ]; } const CHECK_MAP = { hardcoded_secrets: checkHardcodedSecrets, unauthenticated_webhooks: checkUnauthenticatedWebhooks, error_handling: checkErrorHandlingGaps, data_retention: checkDataRetentionSettings, }; const ALL_CHECKS = Object.keys(CHECK_MAP); function scanWorkflows(workflows, checks) { const startTime = Date.now(); const checksToRun = checks ?? ALL_CHECKS; const allFindings = []; for (const workflow of workflows) { for (const checkType of checksToRun) { const findings = CHECK_MAP[checkType](workflow); allFindings.push(...findings); } } const scanDurationMs = Date.now() - startTime; const summary = { critical: 0, high: 0, medium: 0, low: 0, total: allFindings.length, }; for (const finding of allFindings) { summary[finding.severity]++; } return { findings: allFindings, workflowsScanned: workflows.length, scanDurationMs, summary, }; } //# sourceMappingURL=workflow-security-scanner.js.map