@n8n/scan-community-package 0.3.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.3.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,22 +1,20 @@
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 { 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";
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) => {
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("@", 1)) {
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("Tarball not found");
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('tar', ['-xzf', tarballName, '-C', packageDir, '--strip-components=1'], {
67
- cwd: TEMP_DIR,
68
- stdio: 'pipe'
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 { default: eslintPlugin } = await import("./eslint-plugin.mjs");
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("**/*.js", {
92
+ const jsFiles = glob.sync('**/*.js', {
109
93
  cwd: packageDir,
110
94
  absolute: true,
111
- ignore: ["node_modules/**"],
95
+ ignore: ['node_modules/**'],
112
96
  });
113
97
 
114
98
  if (jsFiles.length === 0) {
115
- return { passed: true, message: "No JavaScript files found to analyze" };
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("stylish");
106
+ const formatter = await eslint.loadFormatter('stylish');
123
107
  const formattedResults = await formatter.format(results);
124
108
  return {
125
109
  passed: false,
126
- message: "ESLint violations found",
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["dist-tags"].latest;
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
- packageName,
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
- stdout.write(`✅ Downloaded ${label} \n`);
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;