n8n-nodes-trusera 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Trusera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,40 @@
1
+ import type {
2
+ IAuthenticateGeneric,
3
+ ICredentialType,
4
+ INodeProperties,
5
+ } from 'n8n-workflow';
6
+
7
+ export class TruseraApi implements ICredentialType {
8
+ name = 'truseraApi';
9
+ displayName = 'n8n API';
10
+ documentationUrl = 'https://docs.n8n.io/api/';
11
+
12
+ properties: INodeProperties[] = [
13
+ {
14
+ displayName: 'API Key',
15
+ name: 'apiKey',
16
+ type: 'string',
17
+ typeOptions: { password: true },
18
+ default: '',
19
+ required: true,
20
+ description: 'n8n API key from Settings > n8n API',
21
+ },
22
+ {
23
+ displayName: 'n8n Base URL',
24
+ name: 'baseUrl',
25
+ type: 'string',
26
+ default: 'http://localhost:5678',
27
+ required: false,
28
+ description: 'URL of your n8n instance',
29
+ },
30
+ ];
31
+
32
+ authenticate: IAuthenticateGeneric = {
33
+ type: 'generic',
34
+ properties: {
35
+ headers: {
36
+ 'X-N8N-API-KEY': '={{$credentials.apiKey}}',
37
+ },
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,79 @@
1
+ import type {
2
+ IExecuteFunctions,
3
+ INodeExecutionData,
4
+ INodeType,
5
+ INodeTypeDescription,
6
+ } from 'n8n-workflow';
7
+
8
+ import { scanWorkflows } from '../../lib/scanner';
9
+ import { generateDashboardHtml } from '../../lib/dashboardHtml';
10
+
11
+ export class TruseraDashboard implements INodeType {
12
+ description: INodeTypeDescription = {
13
+ displayName: 'Trusera Dashboard',
14
+ name: 'truseraDashboard',
15
+ icon: 'file:trusera.png',
16
+ group: ['output'],
17
+ version: 1,
18
+ subtitle: 'AI Security Dashboard',
19
+ description:
20
+ 'Fetch all n8n workflows, scan them for AI security risks, and return an interactive HTML dashboard',
21
+ defaults: {
22
+ name: 'Trusera Dashboard',
23
+ },
24
+ inputs: ['main'],
25
+ outputs: ['main'],
26
+ credentials: [
27
+ {
28
+ name: 'truseraApi',
29
+ required: true,
30
+ },
31
+ ],
32
+ properties: [
33
+ {
34
+ displayName: 'Dashboard Password',
35
+ name: 'password',
36
+ type: 'string',
37
+ typeOptions: { password: true },
38
+ default: '',
39
+ description:
40
+ 'Optional. If set, the dashboard is AES-256-GCM encrypted and visitors must enter this password to view it.',
41
+ },
42
+ ],
43
+ };
44
+
45
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
46
+ const creds = await this.getCredentials('truseraApi');
47
+ const baseUrl = (creds.baseUrl as string).replace(/\/$/, '');
48
+ const password = this.getNodeParameter('password', 0, '') as string;
49
+
50
+ // Fetch all workflows via n8n REST API (paginated)
51
+ const allWorkflows: Array<Record<string, unknown>> = [];
52
+ let cursor: string | null = null;
53
+ do {
54
+ const url =
55
+ `${baseUrl}/api/v1/workflows?limit=100` +
56
+ (cursor ? `&cursor=${cursor}` : '');
57
+ const resp = await this.helpers.httpRequestWithAuthentication.call(
58
+ this,
59
+ 'truseraApi',
60
+ { method: 'GET', url, json: true },
61
+ );
62
+ const data = resp as { data: Array<Record<string, unknown>>; nextCursor?: string };
63
+ allWorkflows.push(...data.data);
64
+ cursor = data.nextCursor ?? null;
65
+ } while (cursor);
66
+
67
+ // Scan all workflows
68
+ const workflows = allWorkflows.map((wf) => ({
69
+ data: wf,
70
+ filePath: (wf.name as string) || (wf.id as string) || 'unknown',
71
+ }));
72
+ const scanResult = scanWorkflows(workflows);
73
+
74
+ // Generate HTML dashboard
75
+ const html = generateDashboardHtml(scanResult, password || undefined);
76
+
77
+ return [[{ json: { html } }]];
78
+ }
79
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <path d="M32 4L56 18V46L32 60L8 46V18L32 4Z" fill="#0F172A" stroke="#3B82F6" stroke-width="2"/>
3
+ <text x="32" y="40" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" font-weight="bold" fill="#3B82F6">T</text>
4
+ </svg>
@@ -0,0 +1,17 @@
1
+ {
2
+ "node": "n8n-nodes-trusera.truseraPolicy",
3
+ "nodeVersion": "1.0",
4
+ "codexVersion": "1.0",
5
+ "categories": ["AI", "Miscellaneous"],
6
+ "resources": {
7
+ "primaryDocumentation": [
8
+ {
9
+ "url": "https://trusera.dev/docs/n8n"
10
+ }
11
+ ]
12
+ },
13
+ "alias": ["policy", "security", "compliance", "trusera", "ai-bom"],
14
+ "subcategories": {
15
+ "AI": ["Security"]
16
+ }
17
+ }
@@ -0,0 +1,128 @@
1
+ import type {
2
+ IExecuteFunctions,
3
+ INodeExecutionData,
4
+ INodeType,
5
+ INodeTypeDescription,
6
+ } from 'n8n-workflow';
7
+
8
+ import { evaluatePolicy, Policy } from '../../lib/policyEngine';
9
+ import type { ScanResult } from '../../lib/models';
10
+
11
+ export class TruseraPolicy implements INodeType {
12
+ description: INodeTypeDescription = {
13
+ displayName: 'Trusera Policy',
14
+ name: 'truseraPolicy',
15
+ icon: 'file:trusera.svg',
16
+ group: ['transform'],
17
+ version: 1,
18
+ subtitle: 'Enforce AI security policy',
19
+ description: 'Evaluate AI-BOM scan results against a security policy',
20
+ defaults: {
21
+ name: 'Trusera Policy',
22
+ },
23
+ inputs: ['main'],
24
+ outputs: ['main'],
25
+ properties: [
26
+ {
27
+ displayName: 'Scan Result Field',
28
+ name: 'scanResultField',
29
+ type: 'string',
30
+ default: '',
31
+ description:
32
+ 'Field containing the scan result. If empty, the entire input JSON is used.',
33
+ },
34
+ {
35
+ displayName: 'Max Critical',
36
+ name: 'maxCritical',
37
+ type: 'number',
38
+ default: 0,
39
+ description: 'Maximum number of critical-severity components allowed',
40
+ },
41
+ {
42
+ displayName: 'Max High',
43
+ name: 'maxHigh',
44
+ type: 'number',
45
+ default: -1,
46
+ description:
47
+ 'Maximum number of high-severity components allowed (-1 = unlimited)',
48
+ },
49
+ {
50
+ displayName: 'Max Risk Score',
51
+ name: 'maxRiskScore',
52
+ type: 'number',
53
+ default: -1,
54
+ description:
55
+ 'Maximum risk score allowed for any component (-1 = unlimited)',
56
+ },
57
+ {
58
+ displayName: 'Block Providers',
59
+ name: 'blockProviders',
60
+ type: 'string',
61
+ default: '',
62
+ description:
63
+ 'Comma-separated list of AI providers to block (e.g. "OpenAI,Anthropic")',
64
+ },
65
+ {
66
+ displayName: 'Block Flags',
67
+ name: 'blockFlags',
68
+ type: 'string',
69
+ default: '',
70
+ description:
71
+ 'Comma-separated list of risk flags to block (e.g. "hardcoded_api_key,no_auth")',
72
+ },
73
+ ],
74
+ };
75
+
76
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
77
+ const items = this.getInputData();
78
+ const returnData: INodeExecutionData[] = [];
79
+
80
+ for (let i = 0; i < items.length; i++) {
81
+ const scanResultField = this.getNodeParameter('scanResultField', i, '') as string;
82
+ const maxCritical = this.getNodeParameter('maxCritical', i, 0) as number;
83
+ const maxHigh = this.getNodeParameter('maxHigh', i, -1) as number;
84
+ const maxRiskScore = this.getNodeParameter('maxRiskScore', i, -1) as number;
85
+ const blockProvidersStr = this.getNodeParameter('blockProviders', i, '') as string;
86
+ const blockFlagsStr = this.getNodeParameter('blockFlags', i, '') as string;
87
+
88
+ const scanResult: ScanResult = scanResultField
89
+ ? (items[i].json[scanResultField] as unknown as ScanResult)
90
+ : (items[i].json as unknown as ScanResult);
91
+
92
+ if (!scanResult || !Array.isArray(scanResult.components)) {
93
+ returnData.push({
94
+ json: {
95
+ passed: false,
96
+ violations: ['Invalid scan result: missing components array'],
97
+ },
98
+ });
99
+ continue;
100
+ }
101
+
102
+ const policy: Policy = {
103
+ maxCritical,
104
+ blockProviders: blockProvidersStr
105
+ ? blockProvidersStr.split(',').map((s) => s.trim())
106
+ : [],
107
+ blockFlags: blockFlagsStr
108
+ ? blockFlagsStr.split(',').map((s) => s.trim())
109
+ : [],
110
+ };
111
+
112
+ if (maxHigh >= 0) policy.maxHigh = maxHigh;
113
+ if (maxRiskScore >= 0) policy.maxRiskScore = maxRiskScore;
114
+
115
+ const result = evaluatePolicy(scanResult, policy);
116
+
117
+ returnData.push({
118
+ json: {
119
+ ...result,
120
+ policyApplied: policy,
121
+ totalComponents: scanResult.components.length,
122
+ },
123
+ });
124
+ }
125
+
126
+ return [returnData];
127
+ }
128
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <path d="M32 4L56 18V46L32 60L8 46V18L32 4Z" fill="#0F172A" stroke="#3B82F6" stroke-width="2"/>
3
+ <text x="32" y="40" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" font-weight="bold" fill="#3B82F6">T</text>
4
+ </svg>
@@ -0,0 +1,17 @@
1
+ {
2
+ "node": "n8n-nodes-trusera.truseraReport",
3
+ "nodeVersion": "1.0",
4
+ "codexVersion": "1.0",
5
+ "categories": ["AI", "Miscellaneous"],
6
+ "resources": {
7
+ "primaryDocumentation": [
8
+ {
9
+ "url": "https://trusera.dev/docs/n8n"
10
+ }
11
+ ]
12
+ },
13
+ "alias": ["report", "security", "audit", "trusera", "ai-bom", "markdown"],
14
+ "subcategories": {
15
+ "AI": ["Security"]
16
+ }
17
+ }
@@ -0,0 +1,229 @@
1
+ import type {
2
+ IDataObject,
3
+ IExecuteFunctions,
4
+ INodeExecutionData,
5
+ INodeType,
6
+ INodeTypeDescription,
7
+ } from 'n8n-workflow';
8
+
9
+ import type { ScanResult, AIComponent } from '../../lib/models';
10
+ import { Severity } from '../../lib/models';
11
+ import { FLAG_DESCRIPTIONS } from '../../lib/riskScorer';
12
+
13
+ export class TruseraReport implements INodeType {
14
+ description: INodeTypeDescription = {
15
+ displayName: 'Trusera Report',
16
+ name: 'truseraReport',
17
+ icon: 'file:trusera.svg',
18
+ group: ['transform'],
19
+ version: 1,
20
+ subtitle: '={{$parameter["format"]}}',
21
+ description: 'Generate a human-readable AI security report from scan results',
22
+ defaults: {
23
+ name: 'Trusera Report',
24
+ },
25
+ inputs: ['main'],
26
+ outputs: ['main'],
27
+ properties: [
28
+ {
29
+ displayName: 'Scan Result Field',
30
+ name: 'scanResultField',
31
+ type: 'string',
32
+ default: '',
33
+ description:
34
+ 'Field containing the scan result. If empty, the entire input JSON is used.',
35
+ },
36
+ {
37
+ displayName: 'Format',
38
+ name: 'format',
39
+ type: 'options',
40
+ noDataExpression: true,
41
+ options: [
42
+ {
43
+ name: 'Markdown',
44
+ value: 'markdown',
45
+ description: 'Generate a Markdown report',
46
+ action: 'Generate Markdown report',
47
+ },
48
+ {
49
+ name: 'JSON Summary',
50
+ value: 'jsonSummary',
51
+ description: 'Generate a compact JSON summary',
52
+ action: 'Generate JSON summary',
53
+ },
54
+ ],
55
+ default: 'markdown',
56
+ },
57
+ {
58
+ displayName: 'Include Low Severity',
59
+ name: 'includeLow',
60
+ type: 'boolean',
61
+ default: false,
62
+ description: 'Whether to include low-severity findings in the report',
63
+ },
64
+ ],
65
+ };
66
+
67
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
68
+ const items = this.getInputData();
69
+ const returnData: INodeExecutionData[] = [];
70
+
71
+ for (let i = 0; i < items.length; i++) {
72
+ const scanResultField = this.getNodeParameter('scanResultField', i, '') as string;
73
+ const format = this.getNodeParameter('format', i) as string;
74
+ const includeLow = this.getNodeParameter('includeLow', i, false) as boolean;
75
+
76
+ const scanResult: ScanResult = scanResultField
77
+ ? (items[i].json[scanResultField] as unknown as ScanResult)
78
+ : (items[i].json as unknown as ScanResult);
79
+
80
+ if (!scanResult || !Array.isArray(scanResult.components)) {
81
+ returnData.push({
82
+ json: { error: 'Invalid scan result: missing components array' },
83
+ });
84
+ continue;
85
+ }
86
+
87
+ const filtered = includeLow
88
+ ? scanResult.components
89
+ : scanResult.components.filter((c) => c.risk.severity !== Severity.Low);
90
+
91
+ if (format === 'markdown') {
92
+ returnData.push({
93
+ json: { report: generateMarkdown(scanResult, filtered) },
94
+ });
95
+ } else {
96
+ returnData.push({
97
+ json: generateJsonSummary(scanResult, filtered) as unknown as IDataObject,
98
+ });
99
+ }
100
+ }
101
+
102
+ return [returnData];
103
+ }
104
+ }
105
+
106
+ function severityEmoji(severity: string): string {
107
+ switch (severity) {
108
+ case 'critical': return '[CRITICAL]';
109
+ case 'high': return '[HIGH]';
110
+ case 'medium': return '[MEDIUM]';
111
+ case 'low': return '[LOW]';
112
+ default: return '';
113
+ }
114
+ }
115
+
116
+ function generateMarkdown(result: ScanResult, components: AIComponent[]): string {
117
+ const lines: string[] = [];
118
+
119
+ lines.push('# Trusera AI-BOM Security Report');
120
+ lines.push('');
121
+ lines.push(`**Scan Date:** ${result.scanTimestamp}`);
122
+ lines.push(`**Target:** ${result.targetPath}`);
123
+ lines.push(`**AI-BOM Version:** ${result.aiBomVersion}`);
124
+ lines.push('');
125
+
126
+ // Summary
127
+ const summary = result.summary;
128
+ lines.push('## Summary');
129
+ lines.push('');
130
+ lines.push(`| Metric | Value |`);
131
+ lines.push(`| --- | --- |`);
132
+ lines.push(`| Total Components | ${summary.totalComponents} |`);
133
+ lines.push(`| Files Scanned | ${summary.totalFilesScanned} |`);
134
+ lines.push(`| Highest Risk Score | ${summary.highestRiskScore} |`);
135
+ lines.push(`| Scan Duration | ${summary.scanDurationSeconds.toFixed(2)}s |`);
136
+ lines.push('');
137
+
138
+ // Severity breakdown
139
+ if (Object.keys(summary.bySeverity).length > 0) {
140
+ lines.push('### By Severity');
141
+ lines.push('');
142
+ for (const [sev, count] of Object.entries(summary.bySeverity)) {
143
+ lines.push(`- ${severityEmoji(sev)} **${sev}**: ${count}`);
144
+ }
145
+ lines.push('');
146
+ }
147
+
148
+ // Provider breakdown
149
+ if (Object.keys(summary.byProvider).length > 0) {
150
+ lines.push('### By Provider');
151
+ lines.push('');
152
+ for (const [provider, count] of Object.entries(summary.byProvider)) {
153
+ lines.push(`- **${provider}**: ${count}`);
154
+ }
155
+ lines.push('');
156
+ }
157
+
158
+ // Findings
159
+ if (components.length > 0) {
160
+ lines.push('## Findings');
161
+ lines.push('');
162
+
163
+ const sorted = [...components].sort((a, b) => b.risk.score - a.risk.score);
164
+
165
+ for (const comp of sorted) {
166
+ lines.push(
167
+ `### ${severityEmoji(comp.risk.severity)} ${comp.name} (Score: ${comp.risk.score})`,
168
+ );
169
+ lines.push('');
170
+ lines.push(`- **Type:** ${comp.type}`);
171
+ lines.push(`- **Provider:** ${comp.provider}`);
172
+ if (comp.modelName) lines.push(`- **Model:** ${comp.modelName}`);
173
+ lines.push(`- **Location:** ${comp.location.filePath}`);
174
+ if (comp.location.contextSnippet) {
175
+ lines.push(`- **Context:** ${comp.location.contextSnippet}`);
176
+ }
177
+
178
+ if (comp.flags.length > 0) {
179
+ lines.push('- **Flags:**');
180
+ for (const flag of comp.flags) {
181
+ const desc = FLAG_DESCRIPTIONS[flag] || flag.replace(/_/g, ' ');
182
+ lines.push(` - \`${flag}\`: ${desc}`);
183
+ }
184
+ }
185
+
186
+ if (comp.risk.factors.length > 0) {
187
+ lines.push('- **Risk Factors:**');
188
+ for (const factor of comp.risk.factors) {
189
+ lines.push(` - ${factor}`);
190
+ }
191
+ }
192
+
193
+ lines.push('');
194
+ }
195
+ } else {
196
+ lines.push('## Findings');
197
+ lines.push('');
198
+ lines.push('No findings above the selected severity threshold.');
199
+ lines.push('');
200
+ }
201
+
202
+ lines.push('---');
203
+ lines.push('*Generated by [Trusera AI-BOM](https://trusera.dev)*');
204
+
205
+ return lines.join('\n');
206
+ }
207
+
208
+ function generateJsonSummary(
209
+ result: ScanResult,
210
+ components: AIComponent[],
211
+ ): Record<string, unknown> {
212
+ return {
213
+ scanTimestamp: result.scanTimestamp,
214
+ targetPath: result.targetPath,
215
+ aiBomVersion: result.aiBomVersion,
216
+ summary: result.summary,
217
+ findingsCount: components.length,
218
+ findings: components.map((c) => ({
219
+ name: c.name,
220
+ type: c.type,
221
+ provider: c.provider,
222
+ modelName: c.modelName,
223
+ severity: c.risk.severity,
224
+ score: c.risk.score,
225
+ flags: c.flags,
226
+ factors: c.risk.factors,
227
+ })),
228
+ };
229
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <path d="M32 4L56 18V46L32 60L8 46V18L32 4Z" fill="#0F172A" stroke="#3B82F6" stroke-width="2"/>
3
+ <text x="32" y="40" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" font-weight="bold" fill="#3B82F6">T</text>
4
+ </svg>
@@ -0,0 +1,17 @@
1
+ {
2
+ "node": "n8n-nodes-trusera.truseraScan",
3
+ "nodeVersion": "1.0",
4
+ "codexVersion": "1.0",
5
+ "categories": ["AI", "Miscellaneous"],
6
+ "resources": {
7
+ "primaryDocumentation": [
8
+ {
9
+ "url": "https://trusera.dev/docs/n8n"
10
+ }
11
+ ]
12
+ },
13
+ "alias": ["ai-bom", "security", "scan", "trusera", "ai", "risk"],
14
+ "subcategories": {
15
+ "AI": ["Security"]
16
+ }
17
+ }
@@ -0,0 +1,150 @@
1
+ import type {
2
+ IDataObject,
3
+ IExecuteFunctions,
4
+ INodeExecutionData,
5
+ INodeType,
6
+ INodeTypeDescription,
7
+ } from 'n8n-workflow';
8
+
9
+ import { scanWorkflow, scanWorkflows, isValidWorkflow } from '../../lib/scanner';
10
+
11
+ export class TruseraScan implements INodeType {
12
+ description: INodeTypeDescription = {
13
+ displayName: 'Trusera Scan',
14
+ name: 'truseraScan',
15
+ icon: 'file:trusera.svg',
16
+ group: ['transform'],
17
+ version: 1,
18
+ subtitle: '={{$parameter["operation"]}}',
19
+ description: 'Scan n8n workflows for AI security risks using Trusera AI-BOM',
20
+ defaults: {
21
+ name: 'Trusera Scan',
22
+ },
23
+ inputs: ['main'],
24
+ outputs: ['main'],
25
+ credentials: [
26
+ {
27
+ name: 'truseraApi',
28
+ required: false,
29
+ },
30
+ ],
31
+ properties: [
32
+ {
33
+ displayName: 'Operation',
34
+ name: 'operation',
35
+ type: 'options',
36
+ noDataExpression: true,
37
+ options: [
38
+ {
39
+ name: 'Scan Workflow JSON',
40
+ value: 'scanJson',
41
+ description: 'Scan a workflow JSON object from input',
42
+ action: 'Scan a workflow JSON object',
43
+ },
44
+ {
45
+ name: 'Scan Multiple Workflows',
46
+ value: 'scanMultiple',
47
+ description: 'Scan multiple workflow JSON objects from input array',
48
+ action: 'Scan multiple workflow JSON objects',
49
+ },
50
+ ],
51
+ default: 'scanJson',
52
+ },
53
+ {
54
+ displayName: 'Workflow JSON Field',
55
+ name: 'jsonField',
56
+ type: 'string',
57
+ default: 'json',
58
+ required: true,
59
+ description: 'Name of the input field containing the n8n workflow JSON',
60
+ displayOptions: {
61
+ show: {
62
+ operation: ['scanJson'],
63
+ },
64
+ },
65
+ },
66
+ {
67
+ displayName: 'Workflows Array Field',
68
+ name: 'arrayField',
69
+ type: 'string',
70
+ default: 'workflows',
71
+ required: true,
72
+ description: 'Name of the input field containing an array of workflow JSON objects',
73
+ displayOptions: {
74
+ show: {
75
+ operation: ['scanMultiple'],
76
+ },
77
+ },
78
+ },
79
+ {
80
+ displayName: 'File Path',
81
+ name: 'filePath',
82
+ type: 'string',
83
+ default: 'workflow.json',
84
+ required: false,
85
+ description: 'Virtual file path used in scan results for identification',
86
+ },
87
+ ],
88
+ };
89
+
90
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
91
+ const items = this.getInputData();
92
+ const returnData: INodeExecutionData[] = [];
93
+
94
+ for (let i = 0; i < items.length; i++) {
95
+ const operation = this.getNodeParameter('operation', i) as string;
96
+ const filePath = this.getNodeParameter('filePath', i, 'workflow.json') as string;
97
+
98
+ if (operation === 'scanJson') {
99
+ const jsonField = this.getNodeParameter('jsonField', i) as string;
100
+ const workflowData = items[i].json[jsonField] ?? items[i].json;
101
+
102
+ if (!isValidWorkflow(workflowData)) {
103
+ returnData.push({
104
+ json: {
105
+ error: 'Invalid n8n workflow JSON: missing nodes or connections',
106
+ components: [],
107
+ summary: null,
108
+ },
109
+ });
110
+ continue;
111
+ }
112
+
113
+ const components = scanWorkflow(workflowData, filePath);
114
+ returnData.push({
115
+ json: {
116
+ components,
117
+ totalComponents: components.length,
118
+ filePath,
119
+ },
120
+ });
121
+ } else if (operation === 'scanMultiple') {
122
+ const arrayField = this.getNodeParameter('arrayField', i) as string;
123
+ const workflowsArray = items[i].json[arrayField];
124
+
125
+ if (!Array.isArray(workflowsArray)) {
126
+ returnData.push({
127
+ json: {
128
+ error: `Field "${arrayField}" is not an array`,
129
+ components: [],
130
+ summary: null,
131
+ },
132
+ });
133
+ continue;
134
+ }
135
+
136
+ const workflows = workflowsArray.map((data: unknown, idx: number) => ({
137
+ data,
138
+ filePath: `${filePath.replace('.json', '')}_${idx}.json`,
139
+ }));
140
+
141
+ const result = scanWorkflows(workflows);
142
+ returnData.push({
143
+ json: result as unknown as IDataObject,
144
+ });
145
+ }
146
+ }
147
+
148
+ return [returnData];
149
+ }
150
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <path d="M32 4L56 18V46L32 60L8 46V18L32 4Z" fill="#0F172A" stroke="#3B82F6" stroke-width="2"/>
3
+ <text x="32" y="40" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" font-weight="bold" fill="#3B82F6">T</text>
4
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-trusera",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "n8n community node to scan workflows for AI security risks using Trusera AI-BOM",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -10,7 +10,7 @@
10
10
  "trusera",
11
11
  "workflow-security"
12
12
  ],
13
- "license": "Apache-2.0",
13
+ "license": "MIT",
14
14
  "homepage": "https://trusera.dev",
15
15
  "author": {
16
16
  "name": "Trusera",
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://github.com/trusera/ai-bom"
21
+ "url": "https://github.com/trusera/ai-bom",
22
+ "directory": "n8n-node"
22
23
  },
23
24
  "bugs": {
24
25
  "url": "https://github.com/trusera/ai-bom/issues"
@@ -45,6 +46,8 @@
45
46
  },
46
47
  "files": [
47
48
  "dist",
49
+ "credentials",
50
+ "nodes",
48
51
  "package.json",
49
52
  "LICENSE"
50
53
  ],