mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat: Adds community scanner package (#18946)
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
1
packages/@n8n/scan-community-package/.gitignore
vendored
Normal file
1
packages/@n8n/scan-community-package/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
7
packages/@n8n/scan-community-package/README.md
Normal file
7
packages/@n8n/scan-community-package/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## n8n community-package static analysis tool
|
||||
|
||||
### How to use this
|
||||
|
||||
```
|
||||
$ npx @n8n/scan-community-package n8n-nodes-PACKAGE
|
||||
```
|
||||
17
packages/@n8n/scan-community-package/package.json
Normal file
17
packages/@n8n/scan-community-package/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@n8n/scan-community-package",
|
||||
"version": "0.1.2",
|
||||
"description": "Static code analyser for n8n community packages",
|
||||
"license": "none",
|
||||
"bin": "scanner/cli.mjs",
|
||||
"files": [
|
||||
"scanner"
|
||||
],
|
||||
"dependencies": {
|
||||
"eslint": "catalog:",
|
||||
"fast-glob": "catalog:",
|
||||
"axios": "catalog:",
|
||||
"semver": "^7.5.4",
|
||||
"tmp": "0.2.4"
|
||||
}
|
||||
}
|
||||
36
packages/@n8n/scan-community-package/scanner/cli.mjs
Executable file
36
packages/@n8n/scan-community-package/scanner/cli.mjs
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 1) {
|
||||
console.error(
|
||||
"Usage: npx @n8n/scan-community-package <package-name>[@version]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { resolvePackage, analyzePackageByName } from "./scanner.mjs";
|
||||
|
||||
const packageSpec = args[0];
|
||||
const { packageName, version } = resolvePackage(packageSpec);
|
||||
try {
|
||||
const result = await analyzePackageByName(packageName, version);
|
||||
|
||||
if (result.passed) {
|
||||
console.log(
|
||||
`✅ Package ${packageName}@${result.version} has passed all security checks`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`❌ Package ${packageName}@${result.version} has failed security checks`,
|
||||
);
|
||||
console.log(`Reason: ${result.message}`);
|
||||
|
||||
if (result.details) {
|
||||
console.log("\nDetails:");
|
||||
console.log(result.details);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Analysis failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
const restrictedGlobals = [
|
||||
"clearInterval",
|
||||
"clearTimeout",
|
||||
"global",
|
||||
"process",
|
||||
"setInterval",
|
||||
"setTimeout",
|
||||
];
|
||||
|
||||
const allowedModules = [
|
||||
"n8n-workflow",
|
||||
"lodash",
|
||||
"moment",
|
||||
"p-limit",
|
||||
"luxon",
|
||||
"zod",
|
||||
"crypto",
|
||||
"node:crypto"
|
||||
];
|
||||
|
||||
const isModuleAllowed = (modulePath) => {
|
||||
// Allow relative paths
|
||||
if (modulePath.startsWith("./") || modulePath.startsWith("../")) return true;
|
||||
|
||||
// Extract module name from imports that might contain additional path
|
||||
const moduleName = modulePath.startsWith("@")
|
||||
? modulePath.split("/").slice(0, 2).join("/")
|
||||
: modulePath.split("/")[0];
|
||||
return allowedModules.includes(moduleName);
|
||||
};
|
||||
|
||||
/** @type {import('@types/eslint').ESLint.Plugin} */
|
||||
const plugin = {
|
||||
rules: {
|
||||
"no-restricted-globals": {
|
||||
create(context) {
|
||||
return {
|
||||
Identifier(node) {
|
||||
if (
|
||||
restrictedGlobals.includes(node.name) &&
|
||||
(!node.parent ||
|
||||
node.parent.type !== "MemberExpression" ||
|
||||
node.parent.object === node)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Use of restricted global '${node.name}' is not allowed`,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
"no-restricted-imports": {
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const modulePath = node.source.value;
|
||||
if (!isModuleAllowed(modulePath)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Import of '${modulePath}' is not allowed.`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.name === "require" &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === "Literal"
|
||||
) {
|
||||
const modulePath = node.arguments[0].value;
|
||||
if (!isModuleAllowed(modulePath)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Require of '${modulePath}' is not allowed.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
183
packages/@n8n/scan-community-package/scanner/scanner.mjs
Normal file
183
packages/@n8n/scan-community-package/scanner/scanner.mjs
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { ESLint } from "eslint";
|
||||
import { execSync } from "child_process";
|
||||
import tmp from "tmp";
|
||||
import semver from "semver";
|
||||
import axios from "axios";
|
||||
import glob from "fast-glob";
|
||||
import { fileURLToPath } from "url";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
import plugin from "./eslint-plugin.mjs";
|
||||
|
||||
const { stdout } = process;
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const TEMP_DIR = tmp.dirSync({ unsafeCleanup: true }).name;
|
||||
const registry = "https://registry.npmjs.org/";
|
||||
|
||||
export const resolvePackage = (packageSpec) => {
|
||||
let packageName, version;
|
||||
if (packageSpec.startsWith("@")) {
|
||||
if (packageSpec.includes("@", 1)) {
|
||||
// Handle scoped packages with versions
|
||||
const lastAtIndex = packageSpec.lastIndexOf("@");
|
||||
return {
|
||||
packageName: packageSpec.substring(0, lastAtIndex),
|
||||
version: packageSpec.substring(lastAtIndex + 1),
|
||||
};
|
||||
} else {
|
||||
// Handle scoped packages without version
|
||||
return { packageName: packageSpec, version: null };
|
||||
}
|
||||
}
|
||||
// Handle regular packages
|
||||
const parts = packageSpec.split("@");
|
||||
return { packageName: parts[0], version: parts[1] || null };
|
||||
};
|
||||
|
||||
const downloadAndExtractPackage = async (packageName, version) => {
|
||||
try {
|
||||
// Download the tarball
|
||||
execSync(`npm -q pack ${packageName}@${version}`, { cwd: TEMP_DIR });
|
||||
const tarballName = fs
|
||||
.readdirSync(TEMP_DIR)
|
||||
.find((file) => file.endsWith(".tgz"));
|
||||
if (!tarballName) {
|
||||
throw new Error("Tarball not found");
|
||||
}
|
||||
|
||||
// Unpack the tarball
|
||||
const packageDir = path.join(TEMP_DIR, `${packageName}-${version}`);
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
execSync(`tar -xzf ${tarballName} -C ${packageDir} --strip-components=1`, {
|
||||
cwd: TEMP_DIR,
|
||||
});
|
||||
fs.unlinkSync(path.join(TEMP_DIR, tarballName));
|
||||
|
||||
return packageDir;
|
||||
} catch (error) {
|
||||
console.error(`\nFailed to download package: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const analyzePackage = async (packageDir) => {
|
||||
const { default: eslintPlugin } = await import("./eslint-plugin.mjs");
|
||||
const eslint = new ESLint({
|
||||
cwd: packageDir,
|
||||
allowInlineConfig: false,
|
||||
overrideConfigFile: true,
|
||||
overrideConfig: defineConfig([
|
||||
{
|
||||
plugins: {
|
||||
"n8n-community-packages": plugin,
|
||||
},
|
||||
rules: {
|
||||
"n8n-community-packages/no-restricted-globals": "error",
|
||||
"n8n-community-packages/no-restricted-imports": "error",
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "commonjs",
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
try {
|
||||
const jsFiles = glob.sync("**/*.js", {
|
||||
cwd: packageDir,
|
||||
absolute: true,
|
||||
ignore: ["node_modules/**"],
|
||||
});
|
||||
|
||||
if (jsFiles.length === 0) {
|
||||
return { passed: true, message: "No JavaScript files found to analyze" };
|
||||
}
|
||||
|
||||
const results = await eslint.lintFiles(jsFiles);
|
||||
const violations = results.filter((result) => result.errorCount > 0);
|
||||
|
||||
if (violations.length > 0) {
|
||||
const formatter = await eslint.loadFormatter("stylish");
|
||||
const formattedResults = await formatter.format(results);
|
||||
return {
|
||||
passed: false,
|
||||
message: "ESLint violations found",
|
||||
details: formattedResults,
|
||||
};
|
||||
}
|
||||
|
||||
return { passed: true };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
passed: false,
|
||||
message: `Analysis failed: ${error.message}`,
|
||||
error,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const analyzePackageByName = async (packageName, version) => {
|
||||
try {
|
||||
let exactVersion = version;
|
||||
|
||||
// If version is a range, get the latest matching version
|
||||
if (version && semver.validRange(version) && !semver.valid(version)) {
|
||||
const { data } = await axios.get(`${registry}/${packageName}`);
|
||||
const versions = Object.keys(data.versions);
|
||||
exactVersion = semver.maxSatisfying(versions, version);
|
||||
|
||||
if (!exactVersion) {
|
||||
throw new Error(`No version found matching ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If no version specified, get the latest
|
||||
if (!exactVersion) {
|
||||
const { data } = await axios.get(`${registry}/${packageName}`);
|
||||
exactVersion = data["dist-tags"].latest;
|
||||
}
|
||||
|
||||
const label = `${packageName}@${exactVersion}`;
|
||||
|
||||
stdout.write(`Downloading ${label}...`);
|
||||
const packageDir = await downloadAndExtractPackage(
|
||||
packageName,
|
||||
exactVersion,
|
||||
);
|
||||
if (stdout.TTY){
|
||||
stdout.clearLine(0);
|
||||
stdout.cursorTo(0);
|
||||
}
|
||||
stdout.write(`✅ Downloaded ${label} \n`);
|
||||
|
||||
stdout.write(`Analyzing ${label}...`);
|
||||
const analysisResult = await analyzePackage(packageDir);
|
||||
if (stdout.TTY) {
|
||||
stdout.clearLine(0);
|
||||
stdout.cursorTo(0);
|
||||
}
|
||||
stdout.write(`✅ Analyzed ${label} \n`);
|
||||
|
||||
return {
|
||||
packageName,
|
||||
version: exactVersion,
|
||||
...analysisResult,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to analyze ${packageName}@${version}:`, error);
|
||||
return {
|
||||
packageName,
|
||||
version,
|
||||
passed: false,
|
||||
message: `Analysis failed: ${error.message}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user