@n8n/scan-community-package 0.3.0 → 0.5.0
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/package.json +4 -3
- package/scanner/cli.mjs +6 -12
- package/scanner/scanner.mjs +38 -57
- package/scanner/eslint-plugin.mjs +0 -89
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n8n/scan-community-package",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Static code analyser for n8n community packages",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"bin": "scanner/cli.mjs",
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"eslint": "9.29.0",
|
|
14
14
|
"fast-glob": "3.2.12",
|
|
15
|
-
"axios": "1.
|
|
15
|
+
"axios": "1.12.0",
|
|
16
16
|
"semver": "^7.5.4",
|
|
17
|
-
"tmp": "0.2.4"
|
|
17
|
+
"tmp": "0.2.4",
|
|
18
|
+
"@n8n/eslint-plugin-community-nodes": "0.3.0"
|
|
18
19
|
},
|
|
19
20
|
"homepage": "https://n8n.io",
|
|
20
21
|
"author": {
|
package/scanner/cli.mjs
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const args = process.argv.slice(2);
|
|
4
4
|
if (args.length < 1) {
|
|
5
|
-
console.error(
|
|
6
|
-
"Usage: npx @n8n/scan-community-package <package-name>[@version]",
|
|
7
|
-
);
|
|
5
|
+
console.error('Usage: npx @n8n/scan-community-package <package-name>[@version]');
|
|
8
6
|
process.exit(1);
|
|
9
7
|
}
|
|
10
8
|
|
|
11
|
-
import { resolvePackage, analyzePackageByName } from
|
|
9
|
+
import { resolvePackage, analyzePackageByName } from './scanner.mjs';
|
|
12
10
|
|
|
13
11
|
const packageSpec = args[0];
|
|
14
12
|
const { packageName, version } = resolvePackage(packageSpec);
|
|
@@ -16,21 +14,17 @@ try {
|
|
|
16
14
|
const result = await analyzePackageByName(packageName, version);
|
|
17
15
|
|
|
18
16
|
if (result.passed) {
|
|
19
|
-
console.log(
|
|
20
|
-
`✅ Package ${packageName}@${result.version} has passed all security checks`,
|
|
21
|
-
);
|
|
17
|
+
console.log(`✅ Package ${packageName}@${result.version} has passed all security checks`);
|
|
22
18
|
} else {
|
|
23
|
-
console.log(
|
|
24
|
-
`❌ Package ${packageName}@${result.version} has failed security checks`,
|
|
25
|
-
);
|
|
19
|
+
console.log(`❌ Package ${packageName}@${result.version} has failed security checks`);
|
|
26
20
|
console.log(`Reason: ${result.message}`);
|
|
27
21
|
|
|
28
22
|
if (result.details) {
|
|
29
|
-
console.log(
|
|
23
|
+
console.log('\nDetails:');
|
|
30
24
|
console.log(result.details);
|
|
31
25
|
}
|
|
32
26
|
}
|
|
33
27
|
} catch (error) {
|
|
34
|
-
console.error(
|
|
28
|
+
console.error('Analysis failed:', error);
|
|
35
29
|
process.exit(1);
|
|
36
30
|
}
|
package/scanner/scanner.mjs
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from
|
|
4
|
-
import path from
|
|
5
|
-
import { ESLint } from
|
|
6
|
-
import { spawnSync } from
|
|
7
|
-
import tmp from
|
|
8
|
-
import semver from
|
|
9
|
-
import axios from
|
|
10
|
-
import glob from
|
|
11
|
-
import { fileURLToPath } from
|
|
12
|
-
import { defineConfig } from
|
|
13
|
-
|
|
14
|
-
import plugin from "./eslint-plugin.mjs";
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { ESLint } from 'eslint';
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
import tmp from 'tmp';
|
|
8
|
+
import semver from 'semver';
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
import glob from 'fast-glob';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { defineConfig } from 'eslint/config';
|
|
15
13
|
|
|
16
14
|
const { stdout } = process;
|
|
17
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
16
|
const TEMP_DIR = tmp.dirSync({ unsafeCleanup: true }).name;
|
|
19
|
-
const registry =
|
|
17
|
+
const registry = 'https://registry.npmjs.org/';
|
|
20
18
|
|
|
21
19
|
export const resolvePackage = (packageSpec) => {
|
|
22
20
|
// Validate input to prevent command injection
|
|
@@ -25,10 +23,10 @@ export const resolvePackage = (packageSpec) => {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
let packageName, version;
|
|
28
|
-
if (packageSpec.startsWith(
|
|
29
|
-
if (packageSpec.includes(
|
|
26
|
+
if (packageSpec.startsWith('@')) {
|
|
27
|
+
if (packageSpec.includes('@', 1)) {
|
|
30
28
|
// Handle scoped packages with versions
|
|
31
|
-
const lastAtIndex = packageSpec.lastIndexOf(
|
|
29
|
+
const lastAtIndex = packageSpec.lastIndexOf('@');
|
|
32
30
|
return {
|
|
33
31
|
packageName: packageSpec.substring(0, lastAtIndex),
|
|
34
32
|
version: packageSpec.substring(lastAtIndex + 1),
|
|
@@ -39,34 +37,36 @@ export const resolvePackage = (packageSpec) => {
|
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
39
|
// Handle regular packages
|
|
42
|
-
const parts = packageSpec.split(
|
|
40
|
+
const parts = packageSpec.split('@');
|
|
43
41
|
return { packageName: parts[0], version: parts[1] || null };
|
|
44
42
|
};
|
|
45
43
|
|
|
46
44
|
const downloadAndExtractPackage = async (packageName, version) => {
|
|
47
45
|
try {
|
|
48
46
|
// Download the tarball using safe arguments
|
|
49
|
-
const npmResult = spawnSync('npm', ['-q', 'pack', `${packageName}@${version}`], {
|
|
47
|
+
const npmResult = spawnSync('npm', ['-q', 'pack', `${packageName}@${version}`], {
|
|
50
48
|
cwd: TEMP_DIR,
|
|
51
|
-
stdio: 'pipe'
|
|
49
|
+
stdio: 'pipe',
|
|
52
50
|
});
|
|
53
51
|
if (npmResult.status !== 0) {
|
|
54
52
|
throw new Error(`npm pack failed: ${npmResult.stderr?.toString()}`);
|
|
55
53
|
}
|
|
56
|
-
const tarballName = fs
|
|
57
|
-
.readdirSync(TEMP_DIR)
|
|
58
|
-
.find((file) => file.endsWith(".tgz"));
|
|
54
|
+
const tarballName = fs.readdirSync(TEMP_DIR).find((file) => file.endsWith('.tgz'));
|
|
59
55
|
if (!tarballName) {
|
|
60
|
-
throw new Error(
|
|
56
|
+
throw new Error('Tarball not found');
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
// Unpack the tarball
|
|
64
60
|
const packageDir = path.join(TEMP_DIR, `${packageName}-${version}`);
|
|
65
61
|
fs.mkdirSync(packageDir, { recursive: true });
|
|
66
|
-
const tarResult = spawnSync(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
const tarResult = spawnSync(
|
|
63
|
+
'tar',
|
|
64
|
+
['-xzf', tarballName, '-C', packageDir, '--strip-components=1'],
|
|
65
|
+
{
|
|
66
|
+
cwd: TEMP_DIR,
|
|
67
|
+
stdio: 'pipe',
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
70
|
if (tarResult.status !== 0) {
|
|
71
71
|
throw new Error(`tar extraction failed: ${tarResult.stderr?.toString()}`);
|
|
72
72
|
}
|
|
@@ -80,50 +80,34 @@ const downloadAndExtractPackage = async (packageName, version) => {
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
const analyzePackage = async (packageDir) => {
|
|
83
|
-
const {
|
|
83
|
+
const { n8nCommunityNodesPlugin } = await import('@n8n/eslint-plugin-community-nodes');
|
|
84
84
|
const eslint = new ESLint({
|
|
85
85
|
cwd: packageDir,
|
|
86
86
|
allowInlineConfig: false,
|
|
87
87
|
overrideConfigFile: true,
|
|
88
|
-
overrideConfig: defineConfig(
|
|
89
|
-
{
|
|
90
|
-
plugins: {
|
|
91
|
-
"n8n-community-packages": plugin,
|
|
92
|
-
},
|
|
93
|
-
rules: {
|
|
94
|
-
"n8n-community-packages/no-restricted-globals": "error",
|
|
95
|
-
"n8n-community-packages/no-restricted-imports": "error",
|
|
96
|
-
},
|
|
97
|
-
languageOptions: {
|
|
98
|
-
parserOptions: {
|
|
99
|
-
ecmaVersion: 2022,
|
|
100
|
-
sourceType: "commonjs",
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
]),
|
|
88
|
+
overrideConfig: defineConfig(n8nCommunityNodesPlugin.configs.recommended),
|
|
105
89
|
});
|
|
106
90
|
|
|
107
91
|
try {
|
|
108
|
-
const jsFiles = glob.sync(
|
|
92
|
+
const jsFiles = glob.sync('**/*.js', {
|
|
109
93
|
cwd: packageDir,
|
|
110
94
|
absolute: true,
|
|
111
|
-
ignore: [
|
|
95
|
+
ignore: ['node_modules/**'],
|
|
112
96
|
});
|
|
113
97
|
|
|
114
98
|
if (jsFiles.length === 0) {
|
|
115
|
-
return { passed: true, message:
|
|
99
|
+
return { passed: true, message: 'No JavaScript files found to analyze' };
|
|
116
100
|
}
|
|
117
101
|
|
|
118
102
|
const results = await eslint.lintFiles(jsFiles);
|
|
119
103
|
const violations = results.filter((result) => result.errorCount > 0);
|
|
120
104
|
|
|
121
105
|
if (violations.length > 0) {
|
|
122
|
-
const formatter = await eslint.loadFormatter(
|
|
106
|
+
const formatter = await eslint.loadFormatter('stylish');
|
|
123
107
|
const formattedResults = await formatter.format(results);
|
|
124
108
|
return {
|
|
125
109
|
passed: false,
|
|
126
|
-
message:
|
|
110
|
+
message: 'ESLint violations found',
|
|
127
111
|
details: formattedResults,
|
|
128
112
|
};
|
|
129
113
|
}
|
|
@@ -157,21 +141,18 @@ export const analyzePackageByName = async (packageName, version) => {
|
|
|
157
141
|
// If no version specified, get the latest
|
|
158
142
|
if (!exactVersion) {
|
|
159
143
|
const { data } = await axios.get(`${registry}/${packageName}`);
|
|
160
|
-
exactVersion = data[
|
|
144
|
+
exactVersion = data['dist-tags'].latest;
|
|
161
145
|
}
|
|
162
146
|
|
|
163
147
|
const label = `${packageName}@${exactVersion}`;
|
|
164
148
|
|
|
165
149
|
stdout.write(`Downloading ${label}...`);
|
|
166
|
-
const packageDir = await downloadAndExtractPackage(
|
|
167
|
-
|
|
168
|
-
exactVersion,
|
|
169
|
-
);
|
|
170
|
-
if (stdout.TTY){
|
|
150
|
+
const packageDir = await downloadAndExtractPackage(packageName, exactVersion);
|
|
151
|
+
if (stdout.TTY) {
|
|
171
152
|
stdout.clearLine(0);
|
|
172
153
|
stdout.cursorTo(0);
|
|
173
154
|
}
|
|
174
|
-
|
|
155
|
+
stdout.write(`✅ Downloaded ${label} \n`);
|
|
175
156
|
|
|
176
157
|
stdout.write(`Analyzing ${label}...`);
|
|
177
158
|
const analysisResult = await analyzePackage(packageDir);
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const restrictedGlobals = [
|
|
2
|
-
"clearInterval",
|
|
3
|
-
"clearTimeout",
|
|
4
|
-
"global",
|
|
5
|
-
"process",
|
|
6
|
-
"setInterval",
|
|
7
|
-
"setTimeout",
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
const allowedModules = [
|
|
11
|
-
"n8n-workflow",
|
|
12
|
-
"lodash",
|
|
13
|
-
"moment",
|
|
14
|
-
"p-limit",
|
|
15
|
-
"luxon",
|
|
16
|
-
"zod",
|
|
17
|
-
"crypto",
|
|
18
|
-
"node:crypto"
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
const isModuleAllowed = (modulePath) => {
|
|
22
|
-
// Allow relative paths
|
|
23
|
-
if (modulePath.startsWith("./") || modulePath.startsWith("../")) return true;
|
|
24
|
-
|
|
25
|
-
// Extract module name from imports that might contain additional path
|
|
26
|
-
const moduleName = modulePath.startsWith("@")
|
|
27
|
-
? modulePath.split("/").slice(0, 2).join("/")
|
|
28
|
-
: modulePath.split("/")[0];
|
|
29
|
-
return allowedModules.includes(moduleName);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** @type {import('@types/eslint').ESLint.Plugin} */
|
|
33
|
-
const plugin = {
|
|
34
|
-
rules: {
|
|
35
|
-
"no-restricted-globals": {
|
|
36
|
-
create(context) {
|
|
37
|
-
return {
|
|
38
|
-
Identifier(node) {
|
|
39
|
-
if (
|
|
40
|
-
restrictedGlobals.includes(node.name) &&
|
|
41
|
-
(!node.parent ||
|
|
42
|
-
node.parent.type !== "MemberExpression" ||
|
|
43
|
-
node.parent.object === node)
|
|
44
|
-
) {
|
|
45
|
-
context.report({
|
|
46
|
-
node,
|
|
47
|
-
message: `Use of restricted global '${node.name}' is not allowed`,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
"no-restricted-imports": {
|
|
56
|
-
create(context) {
|
|
57
|
-
return {
|
|
58
|
-
ImportDeclaration(node) {
|
|
59
|
-
const modulePath = node.source.value;
|
|
60
|
-
if (!isModuleAllowed(modulePath)) {
|
|
61
|
-
context.report({
|
|
62
|
-
node,
|
|
63
|
-
message: `Import of '${modulePath}' is not allowed.`,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
CallExpression(node) {
|
|
69
|
-
if (
|
|
70
|
-
node.callee.name === "require" &&
|
|
71
|
-
node.arguments.length > 0 &&
|
|
72
|
-
node.arguments[0].type === "Literal"
|
|
73
|
-
) {
|
|
74
|
-
const modulePath = node.arguments[0].value;
|
|
75
|
-
if (!isModuleAllowed(modulePath)) {
|
|
76
|
-
context.report({
|
|
77
|
-
node,
|
|
78
|
-
message: `Require of '${modulePath}' is not allowed.`,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export default plugin;
|