n8n-mcp
Version:
Integration between n8n workflow automation and Model Context Protocol (MCP)
194 lines • 8.8 kB
JavaScript
;
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