@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
165 lines • 6.39 kB
JavaScript
// Import Node.js Dependencies
import path from "node:path";
// Import Internal Dependencies
import { VariableTracer } from "./VariableTracer.js";
import { InlinedRequire } from "./probes/isRequire/InlinedRequire.js";
import { Deobfuscator } from "./Deobfuscator.js";
import { rootLocation, toArrayLocation, isSvg, isStringBase64 } from "./utils/index.js";
import { generateWarning } from "./warnings.js";
import { CollectableSetRegistry } from "./CollectableSetRegistry.js";
// CONSTANTS
const kMaximumEncodedLiterals = 10;
export class SourceFile {
tracer = new VariableTracer().enableDefaultTracing();
inTryStatement = false;
dependencyAutoWarning = false;
deobfuscator = new Deobfuscator();
dependencies = new Map();
encodedLiterals = new Map();
warnings = [];
flags = new Set();
path = new SourceFilePath();
sensitivity;
metadata;
collectablesSetRegistry;
packageName;
constructor(sourceLocation, options) {
const { metadata, collectables = [] } = options ?? {};
this.path.use(sourceLocation);
this.metadata = metadata;
this.collectablesSetRegistry = new CollectableSetRegistry(collectables ?? []);
this.packageName = options?.packageName;
}
addDependency(name, location, unsafe = this.dependencyAutoWarning) {
if (typeof name !== "string" || name.trim() === "") {
return;
}
const dependencyName = name.charAt(name.length - 1) === "/" ?
name.slice(0, -1) : name;
this.dependencies.set(dependencyName, {
unsafe,
inTry: this.inTryStatement,
...(location ? { location } : {})
});
if (this.packageName !== dependencyName) {
this.collectablesSetRegistry.add("dependency", {
value: dependencyName,
file: this.path.location,
location: toArrayLocation(location ?? undefined),
metadata: {
...this.metadata,
inTry: this.inTryStatement,
unsafe
}
});
}
if (this.dependencyAutoWarning) {
this.warnings.push(generateWarning("unsafe-import", {
value: dependencyName,
location: location || void 0
}));
}
}
addEncodedLiteral(value, location) {
if (this.encodedLiterals.size > kMaximumEncodedLiterals) {
return;
}
if (this.encodedLiterals.has(value)) {
const index = this.encodedLiterals.get(value);
this.warnings[index].location.push(toArrayLocation(location ?? rootLocation()));
return;
}
this.warnings.push(generateWarning("encoded-literal", { value, location }));
this.encodedLiterals.set(value, String(this.warnings.length - 1));
}
analyzeLiteral(node, inArrayExpr = false) {
if (typeof node.value !== "string" || isSvg(node)) {
return;
}
this.deobfuscator.analyzeString(node.value);
const hasRawValue = "raw" in node;
const hasHexadecimalSequence = hasRawValue ?
/\\x[a-fA-F0-9]{2}/g.exec(node.raw) !== null :
null;
const hasUnicodeSequence = hasRawValue ?
/\\u[a-fA-F0-9]{4}/g.exec(node.raw) !== null :
null;
const isBase64 = isStringBase64(String(node.value), { allowEmpty: false });
if ((hasHexadecimalSequence || hasUnicodeSequence) && isBase64) {
if (inArrayExpr) {
this.deobfuscator.encodedArrayValue++;
}
else {
this.addEncodedLiteral(node.value, node.loc);
}
}
}
getResult(isMinified) {
const obfuscatorName = this.deobfuscator.assertObfuscation();
if (obfuscatorName !== null) {
this.warnings.push(generateWarning("obfuscated-code", { value: obfuscatorName }));
}
const identifiersLengthArr = this.deobfuscator.identifiers
.filter((value) => value.type !== "Property" && typeof value.name === "string")
.map((value) => value.name.length);
const [idsLengthAvg, stringScore] = [
sum(identifiersLengthArr),
sum(this.deobfuscator.literalScores)
];
if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) {
this.warnings.push(generateWarning("short-identifiers", { value: String(idsLengthAvg) }));
}
if (stringScore >= 3) {
this.warnings.push(generateWarning("suspicious-literal", { value: String(stringScore) }));
}
if (this.encodedLiterals.size > kMaximumEncodedLiterals) {
this.warnings.push(generateWarning("suspicious-file", { value: null }));
this.warnings = this.warnings
.filter((warning) => warning.kind !== "encoded-literal");
}
return {
idsLengthAvg,
stringScore,
warnings: this.warnings
};
}
walk(node) {
const split = InlinedRequire.split(node);
if (split !== null) {
this.tracer.walk(split.virtualDeclaration);
if (split.rebuildExpression) {
this.tracer.walk(split.rebuildExpression);
}
return [
split.virtualDeclaration,
...(split.rebuildExpression ? [split.rebuildExpression] : [])
];
}
this.tracer.walk(node);
this.deobfuscator.walk(node);
// Detect TryStatement and CatchClause to known which dependency is required in a Try {} clause
if (node.type === "TryStatement" && node.handler) {
this.inTryStatement = true;
}
else if (node.type === "CatchClause") {
this.inTryStatement = false;
}
return [node];
}
}
export class SourceFilePath {
location = null;
use(location) {
this.location = location ?? null;
}
resolve(...parts) {
if (this.location === null) {
return path.posix.join(...parts);
}
return path.posix.join(this.location, ...parts);
}
}
function sum(arr = []) {
return arr.length === 0 ? 0 : (arr.reduce((prev, curr) => prev + curr, 0) / arr.length);
}
//# sourceMappingURL=SourceFile.js.map