Files
n8n-enterprise-unlocked/packages/@n8n/scan-community-package/scanner/scanner.mjs
2025-09-03 17:29:40 +01:00

199 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { ESLint } from "eslint";
import { spawnSync } 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) => {
// Validate input to prevent command injection
if (!/^[a-zA-Z0-9@/_.-]+$/.test(packageSpec)) {
throw new Error('Invalid package specification');
}
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 using safe arguments
const npmResult = spawnSync('npm', ['-q', 'pack', `${packageName}@${version}`], {
cwd: TEMP_DIR,
stdio: 'pipe'
});
if (npmResult.status !== 0) {
throw new Error(`npm pack failed: ${npmResult.stderr?.toString()}`);
}
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 });
const tarResult = spawnSync('tar', ['-xzf', tarballName, '-C', packageDir, '--strip-components=1'], {
cwd: TEMP_DIR,
stdio: 'pipe'
});
if (tarResult.status !== 0) {
throw new Error(`tar extraction failed: ${tarResult.stderr?.toString()}`);
}
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}`,
};
}
};