Files
n8n-mcp/tests/unit/services/audit-report-builder.test.ts
Romuald Członkowski 796c427317 feat: add n8n_audit_instance and n8n_manage_credentials tools (v2.47.0) (#702)
Add comprehensive security auditing combining n8n's built-in POST /audit
API with deep workflow scanning using 50+ regex patterns for hardcoded
secrets, unauthenticated webhook detection, error handling gap analysis,
data retention risk assessment, and PII detection.

The audit returns a compact markdown report grouped by workflow with a
Remediation Playbook showing auto-fixable items (with tool chains), items
requiring review, and items requiring user action.

Also adds n8n_manage_credentials tool (list/get/create/update/delete/getSchema)
enabling AI agents to create credentials and assign them to nodes as part
of security remediation.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:27:20 +02:00

341 lines
12 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
buildAuditReport,
type AuditReportInput,
type UnifiedAuditReport,
} from '@/services/audit-report-builder';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const DEFAULT_PERFORMANCE = {
builtinAuditMs: 100,
workflowFetchMs: 50,
customScanMs: 200,
totalMs: 350,
};
function makeInput(overrides: Partial<AuditReportInput> = {}): AuditReportInput {
return {
builtinAudit: [],
customReport: null,
performance: DEFAULT_PERFORMANCE,
instanceUrl: 'https://n8n.example.com',
...overrides,
};
}
function makeFinding(overrides: Record<string, unknown> = {}) {
return {
id: 'CRED-001',
severity: 'critical' as const,
category: 'hardcoded_secrets',
title: 'Hardcoded openai_key detected',
description: 'Found a hardcoded openai_key in node "HTTP Request".',
recommendation: 'Move this secret into n8n credentials.',
remediationType: 'auto_fixable' as const,
remediation: [
{
tool: 'n8n_manage_credentials',
args: { action: 'create' },
description: 'Create credential',
},
],
location: {
workflowId: 'wf-1',
workflowName: 'Test Workflow',
workflowActive: true,
nodeName: 'HTTP Request',
nodeType: 'n8n-nodes-base.httpRequest',
},
...overrides,
};
}
function makeCustomReport(findings: any[], workflowsScanned = 1) {
const summary = { critical: 0, high: 0, medium: 0, low: 0, total: findings.length };
for (const f of findings) {
summary[f.severity as keyof typeof summary]++;
}
return {
findings,
workflowsScanned,
scanDurationMs: 150,
summary,
};
}
// ===========================================================================
// Tests
// ===========================================================================
describe('audit-report-builder', () => {
describe('empty reports', () => {
it('should produce "No issues found" when built-in audit is empty and no custom findings', () => {
const input = makeInput({ builtinAudit: [], customReport: null });
const result = buildAuditReport(input);
expect(result.markdown).toContain('No issues found');
expect(result.summary.totalFindings).toBe(0);
expect(result.summary.critical).toBe(0);
expect(result.summary.high).toBe(0);
expect(result.summary.medium).toBe(0);
expect(result.summary.low).toBe(0);
});
it('should produce "No issues found" when built-in audit is null-like', () => {
const input = makeInput({ builtinAudit: null });
const result = buildAuditReport(input);
expect(result.markdown).toContain('No issues found');
});
});
describe('built-in audit rendering', () => {
it('should render built-in audit with Nodes Risk Report', () => {
// Real n8n API uses { risk: "nodes", sections: [...] } format
const builtinAudit = {
'Nodes Risk Report': {
risk: 'nodes',
sections: [
{
title: 'Insecure node detected',
description: 'Node X uses deprecated API',
recommendation: 'Update to latest version',
location: [{ id: 'node-1' }, { id: 'node-2' }],
},
],
},
};
const input = makeInput({ builtinAudit });
const result = buildAuditReport(input);
expect(result.markdown).toContain('Nodes Risk Report');
expect(result.markdown).toContain('Insecure node detected');
expect(result.markdown).toContain('deprecated API');
expect(result.markdown).toContain('Affected: 2 items');
// Built-in locations are counted as low severity
expect(result.summary.low).toBe(2);
expect(result.summary.totalFindings).toBe(2);
});
it('should render Instance Risk Report with version and settings info', () => {
const builtinAudit = {
'Instance Risk Report': {
risk: 'instance',
sections: [
{
title: 'Outdated instance',
description: 'Running an old version',
recommendation: 'Update n8n',
nextVersions: [{ name: '1.20.0' }, { name: '1.21.0' }],
settings: {
authenticationMethod: 'none',
publicApiDisabled: false,
},
},
],
},
};
const input = makeInput({ builtinAudit });
const result = buildAuditReport(input);
expect(result.markdown).toContain('Instance Risk Report');
expect(result.markdown).toContain('Available versions: 1.20.0, 1.21.0');
expect(result.markdown).toContain('authenticationMethod');
});
});
describe('grouped by workflow', () => {
it('should group findings by workflow with table format', () => {
const findings = [
makeFinding({ id: 'CRED-001', severity: 'critical', title: 'Critical issue' }),
makeFinding({ id: 'ERR-001', severity: 'medium', title: 'Medium issue', category: 'error_handling' }),
];
const input = makeInput({ customReport: makeCustomReport(findings) });
const result = buildAuditReport(input);
// Should have a workflow heading
expect(result.markdown).toContain('Test Workflow');
// Should have a table with findings
expect(result.markdown).toContain('| ID | Severity | Finding | Node | Fix |');
expect(result.markdown).toContain('CRED-001');
expect(result.markdown).toContain('ERR-001');
});
it('should sort findings within workflow by severity', () => {
const findings = [
makeFinding({ id: 'LOW-001', severity: 'low', title: 'Low issue' }),
makeFinding({ id: 'CRIT-001', severity: 'critical', title: 'Critical issue' }),
];
const input = makeInput({ customReport: makeCustomReport(findings) });
const result = buildAuditReport(input);
const critIdx = result.markdown.indexOf('CRIT-001');
const lowIdx = result.markdown.indexOf('LOW-001');
expect(critIdx).toBeLessThan(lowIdx);
});
it('should sort workflows by worst severity first', () => {
const findings = [
makeFinding({ id: 'LOW-001', severity: 'low', title: 'Low issue', location: { workflowId: 'wf-2', workflowName: 'Safe Workflow', nodeName: 'Set', nodeType: 'n8n-nodes-base.set' } }),
makeFinding({ id: 'CRIT-001', severity: 'critical', title: 'Critical issue', location: { workflowId: 'wf-1', workflowName: 'Danger Workflow', nodeName: 'HTTP', nodeType: 'n8n-nodes-base.httpRequest' } }),
];
const input = makeInput({ customReport: makeCustomReport(findings, 2) });
const result = buildAuditReport(input);
const dangerIdx = result.markdown.indexOf('Danger Workflow');
const safeIdx = result.markdown.indexOf('Safe Workflow');
expect(dangerIdx).toBeLessThan(safeIdx);
});
});
describe('remediation playbook', () => {
it('should show auto-fixable section for secrets and webhooks', () => {
const findings = [
makeFinding({ remediationType: 'auto_fixable', category: 'hardcoded_secrets' }),
makeFinding({ id: 'WEBHOOK-001', severity: 'medium', remediationType: 'auto_fixable', category: 'unauthenticated_webhooks', title: 'Unauthenticated webhook' }),
];
const input = makeInput({ customReport: makeCustomReport(findings) });
const result = buildAuditReport(input);
expect(result.markdown).toContain('Auto-fixable by agent');
expect(result.markdown).toContain('Hardcoded secrets');
expect(result.markdown).toContain('Unauthenticated webhooks');
expect(result.markdown).toContain('n8n_manage_credentials');
});
it('should show review section for error handling and PII', () => {
const findings = [
makeFinding({ id: 'ERR-001', severity: 'medium', remediationType: 'review_recommended', category: 'error_handling', title: 'No error handling' }),
makeFinding({ id: 'PII-001', severity: 'medium', remediationType: 'review_recommended', category: 'hardcoded_secrets', title: 'PII found' }),
];
const input = makeInput({ customReport: makeCustomReport(findings) });
const result = buildAuditReport(input);
expect(result.markdown).toContain('Requires review');
expect(result.markdown).toContain('Error handling gaps');
expect(result.markdown).toContain('PII in parameters');
});
it('should show user action section for data retention', () => {
const findings = [
makeFinding({ id: 'RET-001', severity: 'low', remediationType: 'user_action_needed', category: 'data_retention', title: 'Excessive retention' }),
];
const input = makeInput({ customReport: makeCustomReport(findings) });
const result = buildAuditReport(input);
expect(result.markdown).toContain('Requires your action');
expect(result.markdown).toContain('Data retention');
});
it('should surface built-in audit actionables in playbook', () => {
const builtinAudit = {
'Instance Risk Report': {
risk: 'instance',
sections: [
{ title: 'Outdated instance', description: 'Old version', recommendation: 'Update' },
],
},
'Nodes Risk Report': {
risk: 'nodes',
sections: [
{ title: 'Community nodes', description: 'Unvetted', recommendation: 'Review', location: [{ id: '1' }, { id: '2' }] },
],
},
};
const input = makeInput({ builtinAudit });
const result = buildAuditReport(input);
expect(result.markdown).toContain('Outdated instance');
expect(result.markdown).toContain('Community nodes');
});
});
describe('warnings', () => {
it('should include warnings in the report when provided', () => {
const input = makeInput({
warnings: [
'Could not fetch 2 workflows due to permissions',
'Built-in audit endpoint returned partial results',
],
});
const result = buildAuditReport(input);
expect(result.markdown).toContain('Could not fetch 2 workflows');
expect(result.markdown).toContain('partial results');
});
it('should not include warnings when none are provided', () => {
const input = makeInput({ warnings: undefined });
const result = buildAuditReport(input);
expect(result.markdown).not.toContain('Warning');
});
});
describe('performance timing', () => {
it('should include scan performance metrics in the report', () => {
const input = makeInput({
performance: {
builtinAuditMs: 120,
workflowFetchMs: 80,
customScanMs: 250,
totalMs: 450,
},
});
const result = buildAuditReport(input);
expect(result.markdown).toContain('120ms');
expect(result.markdown).toContain('80ms');
expect(result.markdown).toContain('250ms');
});
});
describe('summary counts', () => {
it('should aggregate counts across both built-in and custom sources', () => {
const builtinAudit = {
'Nodes Risk Report': {
risk: 'nodes',
sections: [
{
title: 'Issue',
description: 'Desc',
location: [{ id: '1' }, { id: '2' }, { id: '3' }],
},
],
},
};
const findings = [
makeFinding({ severity: 'critical' }),
makeFinding({ id: 'CRED-002', severity: 'high' }),
makeFinding({ id: 'CRED-003', severity: 'medium' }),
];
const input = makeInput({
builtinAudit,
customReport: makeCustomReport(findings, 5),
});
const result = buildAuditReport(input);
expect(result.summary.critical).toBe(1);
expect(result.summary.high).toBe(1);
expect(result.summary.medium).toBe(1);
// 3 built-in locations counted as low
expect(result.summary.low).toBe(3);
expect(result.summary.totalFindings).toBe(6);
expect(result.summary.workflowsScanned).toBe(5);
});
});
});