UNPKG

@nodesecure/js-x-ray

Version:
263 lines 10.6 kB
// Import Node.js Dependencies import fsSync from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { EventEmitter } from "node:events"; import { performance } from "node:perf_hooks"; // Import Internal Dependencies import { JsSourceParser } from "./parsers/JsSourceParser.js"; import { TsSourceParser } from "./parsers/TsSourceParser.js"; import * as trojan from "./obfuscators/trojan-source.js"; import { PipelineRunner } from "./pipelines/index.js"; import { Inline } from "./pipelines/inline.js"; import { ProbeRunner } from "./ProbeRunner.js"; import { SourceFile } from "./SourceFile.js"; import { isMinifiedCode, isOneLineExpressionExport } from "./utils/index.js"; import { walkEnter } from "./walker/index.js"; import { getCallExpressionIdentifier, isCallExpression, isStringLiteral } from "./estree/index.js"; import { generateWarning } from "./warnings.js"; import { CollectableSetRegistry } from "./CollectableSetRegistry.js"; export class AstAnalyser extends EventEmitter { static ParsingError = Symbol("ParsingError"); static DefaultParser = new JsSourceParser(); #pipelineRunner; probes; #sensitivity; #collectableSetRegistry; constructor(options = {}) { super(); const { customProbes = [], optionalWarnings = false, skipDefaultProbes = false, pipelines = [], collectables = [], sensitivity = "conservative" } = options; this.#pipelineRunner = new PipelineRunner([...pipelines, new Inline()]); this.#collectableSetRegistry = new CollectableSetRegistry(collectables ?? []); this.#sensitivity = sensitivity; let probes = ProbeRunner.Defaults; if (Array.isArray(customProbes) && customProbes.length > 0) { probes = skipDefaultProbes === true ? customProbes : [...probes, ...customProbes]; } if (typeof optionalWarnings === "boolean") { if (optionalWarnings) { probes = [...probes, ...Object.values(ProbeRunner.Optionals)]; } } else { const allOptionalKeys = Object.keys(ProbeRunner.Optionals); const optionalProbes = Array.from(optionalWarnings ?? []) .flatMap((warning) => { if (isPattern(warning)) { const prefix = warning.slice(0, -2); return allOptionalKeys .filter((key) => key.startsWith(`${prefix}.`)) .map((key) => ProbeRunner.Optionals[key]); } return ProbeRunner.Optionals[warning] ?? []; }); probes = [...probes, ...optionalProbes]; } this.probes = probes; } analyse(str, options = {}) { const startTime = performance.now(); const { packageName, location, isMinified = false, removeHTMLComments = false, initialize, finalize, metadata } = options; const parser = options.customParser ?? AstAnalyser.DefaultParser; const body = parser.parse(this.prepareSource(str, { removeHTMLComments }), void 0); const source = new SourceFile(location, { metadata, packageName, collectableRegistry: this.#collectableSetRegistry }); source.sensitivity = this.#sensitivity; if (trojan.verify(str)) { source.warnings.push(generateWarning("obfuscated-code", { value: "trojan-source" })); } const probeRunner = new ProbeRunner(source, this.probes); if (initialize) { if (typeof initialize !== "function") { throw new TypeError("options.initialize must be a function"); } initialize(source); } // we walk each AST Nodes, this is a purely synchronous I/O const reducedBody = this.#pipelineRunner.reduce(body); this.#walkEnter(reducedBody, probeRunner); if (finalize) { if (typeof finalize !== "function") { throw new TypeError("options.finalize must be a function"); } finalize(source); } probeRunner.finalize(); // Add oneline-require flag if this is a one-line require expression if (isOneLineExpressionExport(body)) { source.flags.add("oneline-require"); } const executionTime = performance.now() - startTime; return { ...source.getResult(isMinified), flags: source.flags, executionTime }; } #walkEnter(body, probeRunner) { const recursiveWalkEnter = this.#walkEnter.bind(this); walkEnter(body, function walk(node) { if (Array.isArray(node)) { return; } probeRunner.sourceFile.walk(node, (probeNode) => { const action = probeRunner.walk(probeNode); if (action === "skip") { this.skip(); } if (isEvalCallExpr(probeNode) && isStringLiteral(probeNode.arguments[0])) { const evalBody = AstAnalyser.DefaultParser.parse(probeNode.arguments[0].value, void 0); recursiveWalkEnter(evalBody, probeRunner); } }); }); } async analyseFile(pathToFile, options = {}) { const startTime = performance.now(); const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile; if (filePathString.includes("d.ts")) { throw new Error("Declaration files are not supported"); } const { packageName, removeHTMLComments = false, initialize, finalize, customParser, metadata } = options; let customParserToUse = customParser; if (!customParser && path.extname(filePathString) === ".ts") { customParserToUse = new TsSourceParser(); } const str = await fs.readFile(pathToFile, "utf-8"); const isMin = filePathString.includes(".min") || isMinifiedCode(str); const location = path.dirname(filePathString); try { const data = this.analyse(str, { location, isMinified: isMin, removeHTMLComments, initialize, finalize, customParser: customParserToUse, metadata, packageName }); // Add is-minified flag if the file is minified and not a one-line require if (!data.flags.has("oneline-require") && isMin) { data.flags.add("is-minified"); } const executionTime = performance.now() - startTime; return { ok: true, warnings: data.warnings, flags: data.flags, executionTime }; } catch (error) { this.emit(AstAnalyser.ParsingError, { error, file: filePathString }); const executionTime = performance.now() - startTime; return { ok: false, warnings: [ generateWarning("parsing-error", { value: error.message }) ], executionTime }; } } analyseFileSync(pathToFile, options = {}) { const startTime = performance.now(); const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile; if (filePathString.includes("d.ts")) { throw new Error("Declaration files are not supported"); } const { packageName, removeHTMLComments = false, initialize, finalize, customParser, metadata } = options; let customParserToUse = customParser; if (!customParser && path.extname(filePathString) === ".ts") { customParserToUse = new TsSourceParser(); } const str = fsSync.readFileSync(pathToFile, "utf-8"); const isMin = filePathString.includes(".min") || isMinifiedCode(str); const location = path.dirname(filePathString); try { const data = this.analyse(str, { location, isMinified: isMin, removeHTMLComments, initialize, finalize, customParser: customParserToUse, metadata, packageName }); // Add is-minified flag if the file is minified and not a one-line require if (!data.flags.has("oneline-require") && isMin) { data.flags.add("is-minified"); } const executionTime = performance.now() - startTime; return { ok: true, warnings: data.warnings, flags: data.flags, executionTime }; } catch (error) { this.emit(AstAnalyser.ParsingError, { error, file: filePathString }); const executionTime = performance.now() - startTime; return { ok: false, warnings: [ generateWarning("parsing-error", { value: error.message }) ], executionTime }; } } prepareSource(source, options = {}) { if (typeof source !== "string") { throw new TypeError("source must be a string"); } const { removeHTMLComments = false } = options; /** * if the file start with a shebang then we remove it because meriyah.parseScript fail to parse it. * @example * #!/usr/bin/env node */ const rawNoShebang = source.startsWith("#") ? source.slice(source.indexOf("\n") + 1) : source; return removeHTMLComments ? this.#removeHTMLComment(rawNoShebang) : rawNoShebang; } #removeHTMLComment(str) { return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, ""); } getCollectableSet(type) { return this.#collectableSetRegistry?.get(type); } } function isEvalCallExpr(node) { return (isCallExpression(node) && getCallExpressionIdentifier(node, { resolveCallExpression: true }) === "eval"); } function isPattern(warning) { return warning.endsWith(".*"); } //# sourceMappingURL=AstAnalyser.js.map