@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
183 lines • 7.19 kB
JavaScript
// Import Node.js Dependencies
import fs from "node:fs/promises";
import fsSync from "node:fs";
import path from "node:path";
import isMinified from "is-minified-code";
// Import Internal Dependencies
import { generateWarning } from "./warnings.js";
import { SourceFile } from "./SourceFile.js";
import { JsSourceParser } from "./JsSourceParser.js";
import { ProbeRunner } from "./ProbeRunner.js";
import { walkEnter } from "./walker/index.js";
import * as trojan from "./obfuscators/trojan-source.js";
import { isOneLineExpressionExport } from "./utils/index.js";
import { PipelineRunner } from "./pipelines/index.js";
export class AstAnalyser {
#pipelineRunner;
parser;
probes;
constructor(options = {}) {
const { customProbes = [], optionalWarnings = false, skipDefaultProbes = false, pipelines = [] } = options;
this.#pipelineRunner = new PipelineRunner(pipelines);
this.parser = options.customParser ?? new JsSourceParser();
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 { isMinified = false, module = true, removeHTMLComments = false, initialize, finalize } = options;
const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
isEcmaScriptModule: Boolean(module)
});
const source = new SourceFile();
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);
walkEnter(reducedBody, function walk(node) {
// Skip the root of the AST.
if (Array.isArray(node)) {
return;
}
source.walk(node);
const action = probeRunner.walk(node);
if (action === "skip") {
this.skip();
}
});
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),
dependencies: source.dependencies,
flags: source.flags
};
}
async analyseFile(pathToFile, options = {}) {
try {
const { packageName = null, module = true, removeHTMLComments = false, initialize, finalize } = options;
const str = await fs.readFile(pathToFile, "utf-8");
const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
const isMin = filePathString.includes(".min") || isMinified(str);
const data = this.analyse(str, {
isMinified: isMin,
module: path.extname(filePathString) === ".mjs" ? true : module,
removeHTMLComments,
initialize,
finalize
});
if (packageName !== null) {
data.dependencies.delete(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,
dependencies: data.dependencies,
warnings: data.warnings,
flags: data.flags
};
}
catch (error) {
return {
ok: false,
warnings: [
generateWarning("parsing-error", {
value: error.message
})
]
};
}
}
analyseFileSync(pathToFile, options = {}) {
try {
const { packageName = null, module = true, removeHTMLComments = false, initialize, finalize } = options;
const str = fsSync.readFileSync(pathToFile, "utf-8");
const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
const isMin = filePathString.includes(".min") || isMinified(str);
const data = this.analyse(str, {
isMinified: isMin,
module: path.extname(filePathString) === ".mjs" ? true : module,
removeHTMLComments,
initialize,
finalize
});
if (packageName !== null) {
data.dependencies.delete(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,
dependencies: data.dependencies,
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, "");
}
}
//# sourceMappingURL=AstAnalyser.js.map