@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 CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "@n8n/scan-community-package",
3
- "version": "0.2.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
- "LICENSE.md",
10
- "LICENSE_EE.md"
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.8.3",
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 "./scanner.mjs";
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("\nDetails:");
23
+ console.log('\nDetails:');
30
24
  console.log(result.details);
31
25
  }
32
26
  }
33
27
  } catch (error) {
34
- console.error("Analysis failed:", error);
28
+ console.error('Analysis failed:', error);
35
29
  process.exit(1);
36
30
  }
@@ -1,29 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import fs from "fs";
4
- import path from "path";
5
- import { ESLint } from "eslint";
6
- import { execSync } 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";
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 = "https://registry.npmjs.org/";
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("@", 1)) {
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
- execSync(`npm -q pack ${packageName}@${version}`, { cwd: TEMP_DIR });
45
- const tarballName = fs
46
- .readdirSync(TEMP_DIR)
47
- .find((file) => file.endsWith(".tgz"));
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("Tarball not found");
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
- execSync(`tar -xzf ${tarballName} -C ${packageDir} --strip-components=1`, {
56
- cwd: TEMP_DIR,
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 { default: eslintPlugin } = await import("./eslint-plugin.mjs");
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("**/*.js", {
92
+ const jsFiles = glob.sync('**/*.js', {
94
93
  cwd: packageDir,
95
94
  absolute: true,
96
- ignore: ["node_modules/**"],
95
+ ignore: ['node_modules/**'],
97
96
  });
98
97
 
99
98
  if (jsFiles.length === 0) {
100
- return { passed: true, message: "No JavaScript files found to analyze" };
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("stylish");
106
+ const formatter = await eslint.loadFormatter('stylish');
108
107
  const formattedResults = await formatter.format(results);
109
108
  return {
110
109
  passed: false,
111
- message: "ESLint violations found",
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["dist-tags"].latest;
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
- packageName,
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
- stdout.write(`✅ Downloaded ${label} \n`);
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;