UNPKG

@nodesecure/js-x-ray

Version:
222 lines 9.01 kB
// Import Node.js Dependencies import fsSync from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; // 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 { ProbeRunner } from "./ProbeRunner.js"; import { SourceFile } from "./SourceFile.js"; import { isMinifiedCode, isOneLineExpressionExport } from "./utils/index.js"; import { walkEnter } from "./walker/index.js"; import { getCallExpressionIdentifier } from "./estree/index.js"; import { generateWarning } from "./warnings.js"; import { CollectableSetRegistry } from "./CollectableSetRegistry.js"; export class AstAnalyser { static DefaultParser = new JsSourceParser(); #pipelineRunner; probes; #collectables; #sensitivity; #collectableSetRegistry; constructor(options = {}) { const { customProbes = [], optionalWarnings = false, skipDefaultProbes = false, pipelines = [], collectables = [], sensitivity = "conservative" } = options; this.#pipelineRunner = new PipelineRunner(pipelines); this.#collectables = 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 optionalProbes = Array.from(optionalWarnings ?? []) .flatMap((warning) => ProbeRunner.Optionals[warning] ?? []); probes = [...probes, ...optionalProbes]; } this.probes = probes; } analyse(str, options = {}) { 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, collectables: this.#collectables, packageName }); this.#collectableSetRegistry = source.collectablesSetRegistry; 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"); } return { ...source.getResult(isMinified), flags: source.flags }; } #walkEnter(body, probeRunner) { const recur = this.#walkEnter.bind(this); walkEnter(body, function walk(node) { if (Array.isArray(node)) { return; } for (const probeNode of probeRunner.sourceFile.walk(node)) { const action = probeRunner.walk(probeNode); if (action === "skip") { this.skip(); } if (probeNode.type === "CallExpression" && getCallExpressionIdentifier(probeNode, { resolveCallExpression: true }) === "eval" && probeNode.arguments[0].type === "Literal" && typeof probeNode.arguments[0].value === "string") { const evalBody = AstAnalyser.DefaultParser.parse(probeNode.arguments[0].value, void 0); recur(evalBody, probeRunner); } } }); } async analyseFile(pathToFile, options = {}) { const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile; if (filePathString.includes("d.ts")) { throw new Error("Declaration files are not supported"); } try { 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 data = this.analyse(str, { location: path.dirname(filePathString), 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"); } return { ok: true, warnings: data.warnings, flags: data.flags }; } catch (error) { return { ok: false, warnings: [ generateWarning("parsing-error", { value: error.message }) ] }; } } analyseFileSync(pathToFile, options = {}) { const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile; if (filePathString.includes("d.ts")) { throw new Error("Declaration files are not supported"); } try { 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 data = this.analyse(str, { location: path.dirname(filePathString), 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"); } return { ok: true, warnings: data.warnings, flags: data.flags }; } catch (error) { return { ok: false, warnings: [ generateWarning("parsing-error", { value: error.message }) ] }; } } 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); } } //# sourceMappingURL=AstAnalyser.js.map