@n8n/scan-community-package 0.2.0 → 0.4.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 +6 -5
- package/scanner/cli.mjs +6 -12
- package/scanner/scanner.mjs +52 -56
- package/scanner/eslint-plugin.mjs +0 -89
package/package.json
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n8n/scan-community-package",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
7
7
|
"files": [
|
|
8
8
|
"scanner",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"LICENSE_EE.md",
|
|
10
|
+
"LICENSE.md"
|
|
11
11
|
],
|
|
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.2.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,29 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from
|
|
4
|
-
import path from
|
|
5
|
-
import { ESLint } from
|
|
6
|
-
import {
|
|
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) => {
|
|
20
|
+
// Validate input to prevent command injection
|
|
21
|
+
if (!/^[a-zA-Z0-9@/_.-]+$/.test(packageSpec)) {
|
|
22
|
+
throw new Error('Invalid package specification');
|
|
23
|
+
}
|
|
24
|
+
|
|
22
25
|
let packageName, version;
|
|
23
|
-
if (packageSpec.startsWith(
|
|
24
|
-
if (packageSpec.includes(
|
|
26
|
+
if (packageSpec.startsWith('@')) {
|
|
27
|
+
if (packageSpec.includes('@', 1)) {
|
|
25
28
|
// Handle scoped packages with versions
|
|
26
|
-
const lastAtIndex = packageSpec.lastIndexOf(
|
|
29
|
+
const lastAtIndex = packageSpec.lastIndexOf('@');
|
|
27
30
|
return {
|
|
28
31
|
packageName: packageSpec.substring(0, lastAtIndex),
|
|
29
32
|
version: packageSpec.substring(lastAtIndex + 1),
|
|
@@ -34,27 +37,39 @@ export const resolvePackage = (packageSpec) => {
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
// Handle regular packages
|
|
37
|
-
const parts = packageSpec.split(
|
|
40
|
+
const parts = packageSpec.split('@');
|
|
38
41
|
return { packageName: parts[0], version: parts[1] || null };
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
const downloadAndExtractPackage = async (packageName, version) => {
|
|
42
45
|
try {
|
|
43
|
-
// Download the tarball
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// Download the tarball using safe arguments
|
|
47
|
+
const npmResult = spawnSync('npm', ['-q', 'pack', `${packageName}@${version}`], {
|
|
48
|
+
cwd: TEMP_DIR,
|
|
49
|
+
stdio: 'pipe',
|
|
50
|
+
});
|
|
51
|
+
if (npmResult.status !== 0) {
|
|
52
|
+
throw new Error(`npm pack failed: ${npmResult.stderr?.toString()}`);
|
|
53
|
+
}
|
|
54
|
+
const tarballName = fs.readdirSync(TEMP_DIR).find((file) => file.endsWith('.tgz'));
|
|
48
55
|
if (!tarballName) {
|
|
49
|
-
throw new Error(
|
|
56
|
+
throw new Error('Tarball not found');
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
// Unpack the tarball
|
|
53
60
|
const packageDir = path.join(TEMP_DIR, `${packageName}-${version}`);
|
|
54
61
|
fs.mkdirSync(packageDir, { recursive: true });
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
if (tarResult.status !== 0) {
|
|
71
|
+
throw new Error(`tar extraction failed: ${tarResult.stderr?.toString()}`);
|
|
72
|
+
}
|
|
58
73
|
fs.unlinkSync(path.join(TEMP_DIR, tarballName));
|
|
59
74
|
|
|
60
75
|
return packageDir;
|
|
@@ -65,50 +80,34 @@ const downloadAndExtractPackage = async (packageName, version) => {
|
|
|
65
80
|
};
|
|
66
81
|
|
|
67
82
|
const analyzePackage = async (packageDir) => {
|
|
68
|
-
const {
|
|
83
|
+
const { n8nCommunityNodesPlugin } = await import('@n8n/eslint-plugin-community-nodes');
|
|
69
84
|
const eslint = new ESLint({
|
|
70
85
|
cwd: packageDir,
|
|
71
86
|
allowInlineConfig: false,
|
|
72
87
|
overrideConfigFile: true,
|
|
73
|
-
overrideConfig: defineConfig(
|
|
74
|
-
{
|
|
75
|
-
plugins: {
|
|
76
|
-
"n8n-community-packages": plugin,
|
|
77
|
-
},
|
|
78
|
-
rules: {
|
|
79
|
-
"n8n-community-packages/no-restricted-globals": "error",
|
|
80
|
-
"n8n-community-packages/no-restricted-imports": "error",
|
|
81
|
-
},
|
|
82
|
-
languageOptions: {
|
|
83
|
-
parserOptions: {
|
|
84
|
-
ecmaVersion: 2022,
|
|
85
|
-
sourceType: "commonjs",
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
]),
|
|
88
|
+
overrideConfig: defineConfig(n8nCommunityNodesPlugin.configs.recommended),
|
|
90
89
|
});
|
|
91
90
|
|
|
92
91
|
try {
|
|
93
|
-
const jsFiles = glob.sync(
|
|
92
|
+
const jsFiles = glob.sync('**/*.js', {
|
|
94
93
|
cwd: packageDir,
|
|
95
94
|
absolute: true,
|
|
96
|
-
ignore: [
|
|
95
|
+
ignore: ['node_modules/**'],
|
|
97
96
|
});
|
|
98
97
|
|
|
99
98
|
if (jsFiles.length === 0) {
|
|
100
|
-
return { passed: true, message:
|
|
99
|
+
return { passed: true, message: 'No JavaScript files found to analyze' };
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
const results = await eslint.lintFiles(jsFiles);
|
|
104
103
|
const violations = results.filter((result) => result.errorCount > 0);
|
|
105
104
|
|
|
106
105
|
if (violations.length > 0) {
|
|
107
|
-
const formatter = await eslint.loadFormatter(
|
|
106
|
+
const formatter = await eslint.loadFormatter('stylish');
|
|
108
107
|
const formattedResults = await formatter.format(results);
|
|
109
108
|
return {
|
|
110
109
|
passed: false,
|
|
111
|
-
message:
|
|
110
|
+
message: 'ESLint violations found',
|
|
112
111
|
details: formattedResults,
|
|
113
112
|
};
|
|
114
113
|
}
|
|
@@ -142,21 +141,18 @@ export const analyzePackageByName = async (packageName, version) => {
|
|
|
142
141
|
// If no version specified, get the latest
|
|
143
142
|
if (!exactVersion) {
|
|
144
143
|
const { data } = await axios.get(`${registry}/${packageName}`);
|
|
145
|
-
exactVersion = data[
|
|
144
|
+
exactVersion = data['dist-tags'].latest;
|
|
146
145
|
}
|
|
147
146
|
|
|
148
147
|
const label = `${packageName}@${exactVersion}`;
|
|
149
148
|
|
|
150
149
|
stdout.write(`Downloading ${label}...`);
|
|
151
|
-
const packageDir = await downloadAndExtractPackage(
|
|
152
|
-
|
|
153
|
-
exactVersion,
|
|
154
|
-
);
|
|
155
|
-
if (stdout.TTY){
|
|
150
|
+
const packageDir = await downloadAndExtractPackage(packageName, exactVersion);
|
|
151
|
+
if (stdout.TTY) {
|
|
156
152
|
stdout.clearLine(0);
|
|
157
153
|
stdout.cursorTo(0);
|
|
158
154
|
}
|
|
159
|
-
|
|
155
|
+
stdout.write(`✅ Downloaded ${label} \n`);
|
|
160
156
|
|
|
161
157
|
stdout.write(`Analyzing ${label}...`);
|
|
162
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;
|