From 9dab74b22cb245379f9a14e5f5ab9d31ecb3e55e Mon Sep 17 00:00:00 2001 From: fraxken Date: Sat, 3 Feb 2024 14:48:50 +0100 Subject: [PATCH] refactor!: implement NodeCounter & Deobfuscator class --- package.json | 4 +- src/Deobfuscator.js | 192 ++++++++++ src/NodeCounter.js | 76 ++++ src/ProbeRunner.js | 22 +- src/SourceFile.js | 74 +--- src/obfuscators/freejsobfuscator.js | 4 +- src/obfuscators/index.js | 69 ---- src/obfuscators/jjencode.js | 12 +- src/obfuscators/jsfuck.js | 12 +- src/obfuscators/obfuscator-io.js | 14 +- src/probes/isAssignmentExpression.js | 31 -- src/probes/isBinaryExpression.js | 2 +- src/probes/isClassDeclaration.js | 27 -- src/probes/isFunction.js | 40 -- src/probes/isLiteral.js | 2 +- src/probes/isMemberExpression.js | 19 - src/probes/isMethodDefinition.js | 27 -- src/probes/isObjectExpression.js | 31 -- src/probes/isUnaryExpression.js | 28 -- src/probes/isVariableDeclaration.js | 32 -- src/utils/index.js | 1 + src/utils/isNode.js | 5 + test/Deobfuscator.spec.js | 349 ++++++++++++++++++ test/NodeCounter.spec.js | 135 +++++++ .../class-objectExpression.js | 11 - .../object-objectExpression.js | 6 - test/probes/isAssignmentExpression.spec.js | 25 -- test/probes/isBinaryExpression.spec.js | 4 +- test/probes/isClassDeclaration.spec.js | 41 -- test/probes/isFunction.spec.js | 47 --- test/probes/isLiteral.spec.js | 8 +- test/probes/isMemberExpression.spec.js | 44 --- test/probes/isMethodDefinition.spec.js | 37 -- test/probes/isObjectExpression.spec.js | 32 -- test/probes/isUnaryExpression.spec.js | 25 -- test/probes/isVariableDeclaration.spec.js | 43 --- 36 files changed, 813 insertions(+), 718 deletions(-) create mode 100644 src/Deobfuscator.js create mode 100644 src/NodeCounter.js delete mode 100644 src/obfuscators/index.js delete mode 100644 src/probes/isAssignmentExpression.js delete mode 100644 src/probes/isClassDeclaration.js delete mode 100644 src/probes/isFunction.js delete mode 100644 src/probes/isMemberExpression.js delete mode 100644 src/probes/isMethodDefinition.js delete mode 100644 src/probes/isObjectExpression.js delete mode 100644 src/probes/isUnaryExpression.js delete mode 100644 src/probes/isVariableDeclaration.js create mode 100644 src/utils/isNode.js create mode 100644 test/Deobfuscator.spec.js create mode 100644 test/NodeCounter.spec.js delete mode 100644 test/probes/fixtures/objectExpression/class-objectExpression.js delete mode 100644 test/probes/fixtures/objectExpression/object-objectExpression.js delete mode 100644 test/probes/isAssignmentExpression.spec.js delete mode 100644 test/probes/isClassDeclaration.spec.js delete mode 100644 test/probes/isFunction.spec.js delete mode 100644 test/probes/isMemberExpression.spec.js delete mode 100644 test/probes/isMethodDefinition.spec.js delete mode 100644 test/probes/isObjectExpression.spec.js delete mode 100644 test/probes/isUnaryExpression.spec.js delete mode 100644 test/probes/isVariableDeclaration.spec.js diff --git a/package.json b/package.json index e138d43..7fbfe7d 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,11 @@ "@nodesecure/estree-ast-utils": "^1.3.1", "@nodesecure/sec-literal": "^1.2.0", "estree-walker": "^3.0.1", + "frequency-set": "^1.0.2", "is-minified-code": "^2.0.0", "meriyah": "^4.3.3", - "safe-regex": "^2.1.1" + "safe-regex": "^2.1.1", + "ts-pattern": "^5.0.6" }, "devDependencies": { "@nodesecure/eslint-config": "^1.6.0", diff --git a/src/Deobfuscator.js b/src/Deobfuscator.js new file mode 100644 index 0000000..580d75c --- /dev/null +++ b/src/Deobfuscator.js @@ -0,0 +1,192 @@ +// Import Third-party Dependencies +import { getVariableDeclarationIdentifiers } from "@nodesecure/estree-ast-utils"; +import { Utils, Patterns } from "@nodesecure/sec-literal"; +import { match } from "ts-pattern"; + +// Import Internal Dependencies +import { NodeCounter } from "./NodeCounter.js"; +import { extractNode } from "./utils/index.js"; + +import * as jjencode from "./obfuscators/jjencode.js"; +import * as jsfuck from "./obfuscators/jsfuck.js"; +import * as freejsobfuscator from "./obfuscators/freejsobfuscator.js"; +import * as obfuscatorio from "./obfuscators/obfuscator-io.js"; + +// CONSTANTS +const kIdentifierNodeExtractor = extractNode("Identifier"); +const kDictionaryStrParts = [ + "abcdefghijklmnopqrstuvwxyz", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "0123456789" +]; +const kMinimumIdsCount = 5; + +export class Deobfuscator { + deepBinaryExpression = 0; + encodedArrayValue = 0; + hasDictionaryString = false; + hasPrefixedIdentifiers = false; + + /** @type {Set} */ + morseLiterals = new Set(); + + /** @type {number[]} */ + literalScores = []; + + /** @type {({ name: string; type: string; })[]} */ + identifiers = []; + + #counters = [ + new NodeCounter("VariableDeclaration[kind]"), + new NodeCounter("AssignmentExpression", { + match: (node, nc) => this.#extractCounterIdentifiers(nc, node.left) + }), + new NodeCounter("FunctionDeclaration", { + match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id) + }), + new NodeCounter("MemberExpression[computed]"), + new NodeCounter("Property", { + filter: (node) => node.key.type === "Identifier", + match: (node, nc) => this.#extractCounterIdentifiers(nc, node.key) + }), + new NodeCounter("UnaryExpression", { + name: "DoubleUnaryExpression", + filter: ({ argument }) => argument.type === "UnaryExpression" && argument.argument.type === "ArrayExpression" + }), + new NodeCounter("VariableDeclarator", { + match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id) + }) + ]; + + /** + * @param {!NodeCounter} nc + * @param {*} node + */ + #extractCounterIdentifiers(nc, node) { + const { type } = nc; + + switch (type) { + case "VariableDeclarator": + case "AssignmentExpression": { + for (const { name } of getVariableDeclarationIdentifiers(node)) { + this.identifiers.push({ name, type }); + } + break; + } + case "Property": + case "FunctionDeclaration": + this.identifiers.push({ name: node.name, type }); + break; + } + } + + analyzeString(str) { + const score = Utils.stringSuspicionScore(str); + if (score !== 0) { + this.literalScores.push(score); + } + + if (!this.hasDictionaryString) { + const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word)); + if (isDictionaryStr) { + this.hasDictionaryString = true; + } + } + + // Searching for morse string like "--.- --.--" + if (Utils.isMorse(str)) { + this.morseLiterals.add(str); + } + } + + walk(node) { + const { type } = node; + + const isFunctionParams = node.type === "FunctionDeclaration" || node.type === "FunctionExpression"; + const nodesToExtract = match(type) + .with("ClassDeclaration", () => [node.id, node.superClass]) + .with("FunctionDeclaration", () => node.params) + .with("FunctionExpression", () => node.params) + .with("MethodDefinition", () => [node.key]) + .otherwise(() => []); + + kIdentifierNodeExtractor( + ({ name }) => this.identifiers.push({ name, type: isFunctionParams ? "FunctionParams" : type }), + nodesToExtract + ); + + this.#counters.forEach((counter) => counter.walk(node)); + } + + aggregateCounters() { + const defaultValue = { + Identifiers: this.identifiers.length + }; + + return this.#counters.reduce((result, counter) => { + result[counter.name] = counter.lookup ? + counter.properties : + counter.count; + + return result; + }, defaultValue); + } + + #calcAvgPrefixedIdentifiers( + counters, + prefix + ) { + const valuesArr = Object + .values(prefix) + .slice() + .sort((left, right) => left - right); + if (valuesArr.length === 0) { + return 0; + } + + const nbOfPrefixedIds = valuesArr.length === 1 ? + valuesArr.pop() : + (valuesArr.pop() + valuesArr.pop()); + const maxIds = counters.Identifiers - counters.Property; + + return ((nbOfPrefixedIds / maxIds) * 100); + } + + assertObfuscation() { + const counters = this.aggregateCounters(); + + if (jsfuck.verify(counters)) { + return "jsfuck"; + } + if (jjencode.verify(this.identifiers, counters)) { + return "jjencode"; + } + if (this.morseLiterals.size >= 36) { + return "morse"; + } + + const { prefix } = Patterns.commonHexadecimalPrefix( + this.identifiers.flatMap( + ({ name }) => (typeof name === "string" ? [name] : []) + ) + ); + const uPrefixNames = new Set(Object.keys(prefix)); + + if (this.identifiers.length > kMinimumIdsCount && uPrefixNames.size > 0) { + this.hasPrefixedIdentifiers = this.#calcAvgPrefixedIdentifiers(counters, prefix) > 80; + } + + if (uPrefixNames.size === 1 && freejsobfuscator.verify(this.identifiers, prefix)) { + return "freejsobfuscator"; + } + if (obfuscatorio.verify(this, counters)) { + return "obfuscator.io"; + } + // if ((identifierLength > (kMinimumIdsCount * 3) && this.hasPrefixedIdentifiers) + // && (oneTimeOccurence <= 3 || this.encodedArrayValue > 0)) { + // return "unknown"; + // } + + return null; + } +} diff --git a/src/NodeCounter.js b/src/NodeCounter.js new file mode 100644 index 0000000..cb3bced --- /dev/null +++ b/src/NodeCounter.js @@ -0,0 +1,76 @@ +// Import Third-party Dependencies +import FrequencySet from "frequency-set"; + +// Import Internal Dependencies +import { isNode } from "./utils/index.js"; + +// eslint-disable-next-line func-style +const noop = (node) => true; + +export class NodeCounter { + lookup = null; + + #count = 0; + #properties = null; + #filterFn = noop; + #matchFn = noop; + + /** + * @param {!string} type + * @param {Object} [options] + * @param {string} [options.name] + * @param {(node: any) => boolean} [options.filter] + * @param {(node: any, nc: NodeCounter) => void} [options.match] + * + * @example + * new NodeCounter("FunctionDeclaration"); + * new NodeCounter("VariableDeclaration[kind]"); + */ + constructor(type, options = {}) { + if (typeof type !== "string") { + throw new TypeError("type must be a string"); + } + + const typeResult = /([A-Za-z]+)(\[[a-zA-Z]+\])?/g.exec(type); + if (typeResult === null) { + throw new Error("invalid type argument syntax"); + } + this.type = typeResult[1]; + this.lookup = typeResult[2]?.slice(1, -1) ?? null; + this.name = options?.name ?? this.type; + if (this.lookup) { + this.#properties = new FrequencySet(); + } + + this.#filterFn = options.filter ?? noop; + this.#matchFn = options.match ?? noop; + } + + get count() { + return this.#count; + } + + get properties() { + return Object.fromEntries( + this.#properties?.entries() ?? [] + ); + } + + walk(node) { + if (!isNode(node) || node.type !== this.type) { + return; + } + if (!this.#filterFn(node)) { + return; + } + + this.#count++; + if (this.lookup === null) { + this.#matchFn(node, this); + } + else if (this.lookup in node) { + this.#properties.add(node[this.lookup]); + this.#matchFn(node, this); + } + } +} diff --git a/src/ProbeRunner.js b/src/ProbeRunner.js index 56c4cb9..0675fb4 100644 --- a/src/ProbeRunner.js +++ b/src/ProbeRunner.js @@ -6,18 +6,11 @@ import isUnsafeCallee from "./probes/isUnsafeCallee.js"; import isLiteral from "./probes/isLiteral.js"; import isLiteralRegex from "./probes/isLiteralRegex.js"; import isRegexObject from "./probes/isRegexObject.js"; -import isVariableDeclaration from "./probes/isVariableDeclaration.js"; import isRequire from "./probes/isRequire/isRequire.js"; import isImportDeclaration from "./probes/isImportDeclaration.js"; -import isMemberExpression from "./probes/isMemberExpression.js"; -import isArrayExpression from "./probes/isArrayExpression.js"; -import isFunction from "./probes/isFunction.js"; -import isAssignmentExpression from "./probes/isAssignmentExpression.js"; -import isObjectExpression from "./probes/isObjectExpression.js"; -import isUnaryExpression from "./probes/isUnaryExpression.js"; import isWeakCrypto from "./probes/isWeakCrypto.js"; -import isClassDeclaration from "./probes/isClassDeclaration.js"; -import isMethodDefinition from "./probes/isMethodDefinition.js"; +import isBinaryExpression from "./probes/isBinaryExpression.js"; +import isArrayExpression from "./probes/isArrayExpression.js"; // Import Internal Dependencies import { SourceFile } from "./SourceFile.js"; @@ -50,17 +43,10 @@ export class ProbeRunner { isLiteral, isLiteralRegex, isRegexObject, - isVariableDeclaration, isImportDeclaration, - isMemberExpression, - isAssignmentExpression, - isObjectExpression, - isArrayExpression, - isFunction, - isUnaryExpression, isWeakCrypto, - isClassDeclaration, - isMethodDefinition + isBinaryExpression, + isArrayExpression ]; /** diff --git a/src/SourceFile.js b/src/SourceFile.js index f90d10b..aa7513d 100644 --- a/src/SourceFile.js +++ b/src/SourceFile.js @@ -5,35 +5,20 @@ import { VariableTracer } from "@nodesecure/estree-ast-utils"; // Import Internal Dependencies import { rootLocation, toArrayLocation } from "./utils/index.js"; import { generateWarning } from "./warnings.js"; -import { isObfuscatedCode, hasTrojanSource } from "./obfuscators/index.js"; import { ProbeRunner } from "./ProbeRunner.js"; +import { Deobfuscator } from "./Deobfuscator.js"; +import * as trojan from "./obfuscators/trojan-source.js"; // CONSTANTS -const kDictionaryStrParts = [ - "abcdefghijklmnopqrstuvwxyz", - "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "0123456789" -]; - const kMaximumEncodedLiterals = 10; export class SourceFile { inTryStatement = false; - hasDictionaryString = false; - hasPrefixedIdentifiers = false; dependencyAutoWarning = false; - varkinds = { var: 0, let: 0, const: 0 }; - idtypes = { assignExpr: 0, property: 0, variableDeclarator: 0, functionDeclaration: 0 }; - counter = { - identifiers: 0, - doubleUnaryArray: 0, - computedMemberExpr: 0, - memberExpr: 0, - deepBinaryExpr: 0, - encodedArrayValue: 0 - }; - morseLiterals = new Set(); - identifiersName = []; + deobfuscator = new Deobfuscator(); + dependencies = new Map(); + encodedLiterals = new Map(); + warnings = []; constructor(sourceCodeString) { this.tracer = new VariableTracer() @@ -42,13 +27,8 @@ export class SourceFile { followConsecutiveAssignment: true, moduleName: "crypto" }); - this.dependencies = new Map(); - this.encodedLiterals = new Map(); - this.warnings = []; - this.literalScores = []; this.probesRunner = new ProbeRunner(this); - - if (hasTrojanSource(sourceCodeString)) { + if (trojan.verify(sourceCodeString)) { this.addWarning("obfuscated-code", "trojan-source"); } } @@ -92,35 +72,16 @@ export class SourceFile { } } - analyzeString(str) { - const score = Utils.stringSuspicionScore(str); - if (score !== 0) { - this.literalScores.push(score); - } - - if (!this.hasDictionaryString) { - const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word)); - if (isDictionaryStr) { - this.hasDictionaryString = true; - } - } - - // Searching for morse string like "--.- --.--." - if (Utils.isMorse(str)) { - this.morseLiterals.add(str); - } - } - analyzeLiteral(node, inArrayExpr = false) { if (typeof node.value !== "string" || Utils.isSvg(node)) { return; } - this.analyzeString(node.value); + this.deobfuscator.analyzeString(node.value); const { hasHexadecimalSequence, hasUnicodeSequence, isBase64 } = Literal.defaultAnalysis(node); if ((hasHexadecimalSequence || hasUnicodeSequence) && isBase64) { if (inArrayExpr) { - this.counter.encodedArrayValue++; + this.deobfuscator.encodedArrayValue++; } else { this.addWarning("encoded-literal", node.value, node.loc); @@ -129,17 +90,19 @@ export class SourceFile { } getResult(isMinified) { - this.counter.identifiers = this.identifiersName.length; - const [isObfuscated, kind] = isObfuscatedCode(this); - if (isObfuscated) { - this.addWarning("obfuscated-code", kind); + const obfuscatorName = this.deobfuscator.assertObfuscation(this); + if (obfuscatorName !== null) { + this.addWarning("obfuscated-code", obfuscatorName); } - const identifiersLengthArr = this.identifiersName - .filter((value) => value.type !== "property" && typeof value.name === "string") + 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.literalScores)]; + const [idsLengthAvg, stringScore] = [ + sum(identifiersLengthArr), + sum(this.deobfuscator.literalScores) + ]; if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) { this.addWarning("short-identifiers", idsLengthAvg); } @@ -158,6 +121,7 @@ export class SourceFile { walk(node) { 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" && typeof node.handler !== "undefined") { diff --git a/src/obfuscators/freejsobfuscator.js b/src/obfuscators/freejsobfuscator.js index 97d87d7..7b3f843 100644 --- a/src/obfuscators/freejsobfuscator.js +++ b/src/obfuscators/freejsobfuscator.js @@ -1,9 +1,9 @@ // Import Third-party Dependencies import { Utils } from "@nodesecure/sec-literal"; -export function verify(sourceFile, prefix) { +export function verify(identifiers, prefix) { const pValue = Object.keys(prefix).pop(); const regexStr = `^${Utils.escapeRegExp(pValue)}[a-zA-Z]{1,2}[0-9]{0,2}$`; - return sourceFile.identifiersName.every(({ name }) => new RegExp(regexStr).test(name)); + return identifiers.every(({ name }) => new RegExp(regexStr).test(name)); } diff --git a/src/obfuscators/index.js b/src/obfuscators/index.js deleted file mode 100644 index d31dda4..0000000 --- a/src/obfuscators/index.js +++ /dev/null @@ -1,69 +0,0 @@ -// Import Third-party Dependencies -import { Patterns } from "@nodesecure/sec-literal"; - -// Import Internal Dependencies -import * as jjencode from "./jjencode.js"; -import * as jsfuck from "./jsfuck.js"; -import * as freejsobfuscator from "./freejsobfuscator.js"; -import * as obfuscatorio from "./obfuscator-io.js"; -import * as trojan from "./trojan-source.js"; - -// CONSTANTS -const kMinimumIdsCount = 5; - -export function isObfuscatedCode(sourceFile) { - let encoderName = null; - - if (jsfuck.verify(sourceFile)) { - encoderName = "jsfuck"; - } - else if (jjencode.verify(sourceFile)) { - encoderName = "jjencode"; - } - else if (sourceFile.morseLiterals.size >= 36) { - encoderName = "morse"; - } - else { - // TODO: also implement Dictionnary checkup - const identifiers = sourceFile.identifiersName - .map((value) => value?.name ?? null) - .filter((name) => typeof name === "string"); - - const { prefix, oneTimeOccurence } = Patterns.commonHexadecimalPrefix( - identifiers - ); - const uPrefixNames = new Set(Object.keys(prefix)); - - if (sourceFile.counter.identifiers > kMinimumIdsCount && uPrefixNames.size > 0) { - sourceFile.hasPrefixedIdentifiers = calcAvgPrefixedIdentifiers(sourceFile, prefix) > 80; - } - - if (uPrefixNames.size === 1 && freejsobfuscator.verify(sourceFile, prefix)) { - encoderName = "freejsobfuscator"; - } - else if (obfuscatorio.verify(sourceFile)) { - encoderName = "obfuscator.io"; - } - // else if ((sourceFile.counter.identifiers > (kMinimumIdsCount * 3) && sourceFile.hasPrefixedIdentifiers) - // && (oneTimeOccurence <= 3 || sourceFile.counter.encodedArrayValue > 0)) { - // encoderName = "unknown"; - // } - } - - return [encoderName !== null, encoderName]; -} - -export function hasTrojanSource(sourceString) { - return trojan.verify(sourceString); -} - -function calcAvgPrefixedIdentifiers(sourceFile, prefix) { - const valuesArr = Object.values(prefix).slice().sort((left, right) => left - right); - if (valuesArr.length === 0) { - return 0; - } - const nbOfPrefixedIds = valuesArr.length === 1 ? valuesArr.pop() : (valuesArr.pop() + valuesArr.pop()); - const maxIds = sourceFile.counter.identifiers - sourceFile.idtypes.property; - - return ((nbOfPrefixedIds / maxIds) * 100); -} diff --git a/src/obfuscators/jjencode.js b/src/obfuscators/jjencode.js index d366c41..e191448 100644 --- a/src/obfuscators/jjencode.js +++ b/src/obfuscators/jjencode.js @@ -1,18 +1,18 @@ -// Require Internal Dependencies +// Import Internal Dependencies import { notNullOrUndefined } from "../utils/index.js"; // CONSTANTS const kJJRegularSymbols = new Set(["$", "_"]); -export function verify(sourceFile) { - if (sourceFile.counter.variableDeclarator > 0 || sourceFile.counter.functionDeclaration > 0) { +export function verify(identifiers, counters) { + if (counters.VariableDeclarator > 0 || counters.FunctionDeclaration > 0) { return false; } - if (sourceFile.idtypes.assignExpr > sourceFile.idtypes.property) { + if (counters.AssignmentExpression > counters.Property) { return false; } - const matchCount = sourceFile.identifiersName.filter(({ name }) => { + const matchCount = identifiers.filter(({ name }) => { if (!notNullOrUndefined(name)) { return false; } @@ -20,7 +20,7 @@ export function verify(sourceFile) { return charsCode.every((char) => kJJRegularSymbols.has(char)); }).length; - const pourcent = ((matchCount / sourceFile.identifiersName.length) * 100); + const pourcent = ((matchCount / identifiers.length) * 100); return pourcent > 80; } diff --git a/src/obfuscators/jsfuck.js b/src/obfuscators/jsfuck.js index c7aadac..d198289 100644 --- a/src/obfuscators/jsfuck.js +++ b/src/obfuscators/jsfuck.js @@ -1,11 +1,11 @@ // CONSTANTS const kJSFuckMinimumDoubleUnaryExpr = 5; -export function verify(sourceFile) { - const hasZeroAssign = sourceFile.idtypes.assignExpr === 0 - && sourceFile.idtypes.functionDeclaration === 0 - && sourceFile.idtypes.property === 0 - && sourceFile.idtypes.variableDeclarator === 0; +export function verify(counters) { + const hasZeroAssign = counters.AssignmentExpression === 0 + && counters.FunctionDeclaration === 0 + && counters.Property === 0 + && counters.VariableDeclarator === 0; - return hasZeroAssign && sourceFile.counter.doubleUnaryArray >= kJSFuckMinimumDoubleUnaryExpr; + return hasZeroAssign && counters.DoubleUnaryExpression >= kJSFuckMinimumDoubleUnaryExpr; } diff --git a/src/obfuscators/obfuscator-io.js b/src/obfuscators/obfuscator-io.js index c93931d..3e66c22 100644 --- a/src/obfuscators/obfuscator-io.js +++ b/src/obfuscators/obfuscator-io.js @@ -1,13 +1,13 @@ -export function verify(sourceFile) { - if (sourceFile.counter.memberExpr > 0) { +export function verify(deobfuscator, counters) { + if ((counters.MemberExpression?.false ?? 0) > 0) { return false; } - const hasSomePatterns = sourceFile.counter.doubleUnaryArray > 0 - || sourceFile.counter.deepBinaryExpr > 0 - || sourceFile.counter.encodedArrayValue > 0 - || sourceFile.hasDictionaryString; + const hasSomePatterns = counters.DoubleUnaryExpression > 0 + || deobfuscator.deepBinaryExpression > 0 + || deobfuscator.encodedArrayValue > 0 + || deobfuscator.hasDictionaryString; // TODO: hasPrefixedIdentifiers only work for hexadecimal id names generator - return sourceFile.hasPrefixedIdentifiers && hasSomePatterns; + return deobfuscator.hasPrefixedIdentifiers && hasSomePatterns; } diff --git a/src/probes/isAssignmentExpression.js b/src/probes/isAssignmentExpression.js deleted file mode 100644 index 636cd42..0000000 --- a/src/probes/isAssignmentExpression.js +++ /dev/null @@ -1,31 +0,0 @@ -// Import Third-party Dependencies -import { getVariableDeclarationIdentifiers } from "@nodesecure/estree-ast-utils"; - -/** - * @description Search for AssignmentExpression (Not to be confused with AssignmentPattern). - * - * @see https://github.com/estree/estree/blob/master/es5.md#assignmentexpression - * @example - * (foo = 5) - */ -function validateNode(node) { - return [ - node.type === "AssignmentExpression" - ]; -} - -function main(node, options) { - const { sourceFile } = options; - - sourceFile.idtypes.assignExpr++; - for (const { name } of getVariableDeclarationIdentifiers(node.left)) { - sourceFile.identifiersName.push({ name, type: "assignExpr" }); - } -} - -export default { - name: "isAssignmentExpression", - validateNode, - main, - breakOnMatch: false -}; diff --git a/src/probes/isBinaryExpression.js b/src/probes/isBinaryExpression.js index feec158..2779411 100644 --- a/src/probes/isBinaryExpression.js +++ b/src/probes/isBinaryExpression.js @@ -16,7 +16,7 @@ function main(node, options) { const [binaryExprDeepness, hasUnaryExpression] = walkBinaryExpression(node); if (binaryExprDeepness >= 3 && hasUnaryExpression) { - sourceFile.counter.deepBinaryExpr++; + sourceFile.deobfuscator.deepBinaryExpression++; } } diff --git a/src/probes/isClassDeclaration.js b/src/probes/isClassDeclaration.js deleted file mode 100644 index 4d6f6b4..0000000 --- a/src/probes/isClassDeclaration.js +++ /dev/null @@ -1,27 +0,0 @@ -// Import Internal Dependencies -import { extractNode } from "../utils/index.js"; - -// CONSTANTS -const kIdExtractor = extractNode("Identifier"); - -function validateNode(node) { - return [ - node.type === "ClassDeclaration" - ]; -} - -function main(node, options) { - const { sourceFile } = options; - - kIdExtractor( - ({ name }) => sourceFile.identifiersName.push({ name, type: "class" }), - [node.id, node.superClass] - ); -} - -export default { - name: "isClassDeclaration", - validateNode, - main, - breakOnMatch: false -}; diff --git a/src/probes/isFunction.js b/src/probes/isFunction.js deleted file mode 100644 index 2b4733c..0000000 --- a/src/probes/isFunction.js +++ /dev/null @@ -1,40 +0,0 @@ -// Import Internal Dependencies -import { extractNode } from "../utils/index.js"; - -// CONSTANTS -const kIdExtractor = extractNode("Identifier"); - -/** - * @description Search for FunctionDeclaration AST Node. - * - * @see https://github.com/estree/estree/blob/master/es5.md#functiondeclaration - * @example - * function foo() {} - */ -function validateNode(node) { - return [ - node.type === "FunctionDeclaration" || node.type === "FunctionExpression" - ]; -} - -function main(node, options) { - const { sourceFile } = options; - - kIdExtractor( - ({ name }) => sourceFile.identifiersName.push({ name, type: "params" }), - node.params - ); - - if (node.id === null || node.id.type !== "Identifier") { - return; - } - sourceFile.idtypes.functionDeclaration++; - sourceFile.identifiersName.push({ name: node.id.name, type: "functionDeclaration" }); -} - -export default { - name: "isFunctionDeclaration", - validateNode, - main, - breakOnMatch: false -}; diff --git a/src/probes/isLiteral.js b/src/probes/isLiteral.js index 176a84a..b626384 100644 --- a/src/probes/isLiteral.js +++ b/src/probes/isLiteral.js @@ -29,7 +29,7 @@ function main(node, options) { // We are searching for value obfuscated as hex of a minimum length of 4. if (/^[0-9A-Fa-f]{4,}$/g.test(node.value)) { const value = Buffer.from(node.value, "hex").toString(); - sourceFile.analyzeString(value); + sourceFile.deobfuscator.analyzeString(value); // If the value we are retrieving is the name of a Node.js dependency, // then we add it to the dependencies list and we throw an unsafe-import at the current location. diff --git a/src/probes/isMemberExpression.js b/src/probes/isMemberExpression.js deleted file mode 100644 index 4257e2b..0000000 --- a/src/probes/isMemberExpression.js +++ /dev/null @@ -1,19 +0,0 @@ -function validateNode(node) { - return [ - node.type === "MemberExpression" - ]; -} - -function main(node, options) { - const { sourceFile } = options; - - sourceFile.counter[node.computed ? "computedMemberExpr" : "memberExpr"]++; -} - -export default { - name: "isMemberExpression", - validateNode, - main, - breakOnMatch: true, - breakGroup: "import" -}; diff --git a/src/probes/isMethodDefinition.js b/src/probes/isMethodDefinition.js deleted file mode 100644 index 13f3304..0000000 --- a/src/probes/isMethodDefinition.js +++ /dev/null @@ -1,27 +0,0 @@ -// Import Internal Dependencies -import { extractNode } from "../utils/index.js"; - -// CONSTANTS -const kIdExtractor = extractNode("Identifier"); - -function validateNode(node) { - return [ - node.type === "MethodDefinition" - ]; -} - -function main(node, options) { - const { sourceFile } = options; - - kIdExtractor( - ({ name }) => sourceFile.identifiersName.push({ name, type: "method" }), - [node.key] - ); -} - -export default { - name: "isMethodDefinition", - validateNode, - main, - breakOnMatch: false -}; diff --git a/src/probes/isObjectExpression.js b/src/probes/isObjectExpression.js deleted file mode 100644 index ab110af..0000000 --- a/src/probes/isObjectExpression.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @description Search for ObjectExpression AST Node (commonly known as Object). - * @see https://github.com/estree/estree/blob/master/es5.md#objectexpression - * @example - * { foo: "bar" } - */ -function validateNode(node) { - return [ - node.type === "ObjectExpression" - ]; -} - -function main(node, options) { - const { sourceFile } = options; - - for (const property of node.properties) { - if (property.type !== "Property" || property.key.type !== "Identifier") { - continue; - } - - sourceFile.idtypes.property++; - sourceFile.identifiersName.push({ name: property.key.name, type: "property" }); - } -} - -export default { - name: "isObjectExpression", - validateNode, - main, - breakOnMatch: false -}; diff --git a/src/probes/isUnaryExpression.js b/src/probes/isUnaryExpression.js deleted file mode 100644 index e6654dc..0000000 --- a/src/probes/isUnaryExpression.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @description Search for UnaryExpression AST Node - * @see https://github.com/estree/estree/blob/master/es5.md#unaryexpression - * @example - * -2 - */ -function validateNode(node) { - return [ - node.type === "UnaryExpression" - ]; -} - -function main(node, options) { - const { sourceFile } = options; - - // Example: !![] - // See: https://docs.google.com/document/d/11ZrfW0bDQ-kd7Gr_Ixqyk8p3TGvxckmhFH3Z8dFoPhY/edit# - if (node.argument.type === "UnaryExpression" && node.argument.argument.type === "ArrayExpression") { - sourceFile.counter.doubleUnaryArray++; - } -} - -export default { - name: "isUnaryExpression", - validateNode, - main, - breakOnMatch: false -}; diff --git a/src/probes/isVariableDeclaration.js b/src/probes/isVariableDeclaration.js deleted file mode 100644 index 3732d29..0000000 --- a/src/probes/isVariableDeclaration.js +++ /dev/null @@ -1,32 +0,0 @@ -// Import Third-party Dependencies -import { - getVariableDeclarationIdentifiers -} from "@nodesecure/estree-ast-utils"; - -// In case we are matching a Variable declaration, we have to save the identifier -// This allow the AST Analysis to retrieve required dependency when the stmt is mixed with variables. -function validateNode(node) { - return [ - node.type === "VariableDeclaration" - ]; -} - -function main(mainNode, options) { - const { sourceFile } = options; - - sourceFile.varkinds[mainNode.kind]++; - - for (const node of mainNode.declarations) { - sourceFile.idtypes.variableDeclarator++; - for (const { name } of getVariableDeclarationIdentifiers(node.id)) { - sourceFile.identifiersName.push({ name, type: "variableDeclarator" }); - } - } -} - -export default { - name: "isVariableDeclaration", - validateNode, - main, - breakOnMatch: false -}; diff --git a/src/utils/index.js b/src/utils/index.js index 9910987..66af913 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -5,3 +5,4 @@ export * from "./isUnsafeCallee.js"; export * from "./notNullOrUndefined.js"; export * from "./rootLocation.js"; export * from "./toArrayLocation.js"; +export * from "./isNode.js"; diff --git a/src/utils/isNode.js b/src/utils/isNode.js new file mode 100644 index 0000000..d867a6e --- /dev/null +++ b/src/utils/isNode.js @@ -0,0 +1,5 @@ +export function isNode(value) { + return ( + value !== null && typeof value === "object" && "type" in value && typeof value.type === "string" + ); +} diff --git a/test/Deobfuscator.spec.js b/test/Deobfuscator.spec.js new file mode 100644 index 0000000..d876b2f --- /dev/null +++ b/test/Deobfuscator.spec.js @@ -0,0 +1,349 @@ +// Import Node.js Dependencies +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +// Import Third-party Dependencies +import { walk } from "estree-walker"; + +// Import Internal Dependencies +import { Deobfuscator } from "../src/Deobfuscator.js"; +import { JsSourceParser } from "../src/JsSourceParser.js"; + +describe("Deobfuscator", () => { + describe("identifiers and counters", () => { + it("should detect two identifiers (class name and superClass name A.K.A extends)", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "class File extends Blob {}" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.Identifiers, 2); + assert.deepEqual(deobfuscator.identifiers, [ + { name: "File", type: "ClassDeclaration" }, + { name: "Blob", type: "ClassDeclaration" } + ]); + }); + + it("should detect one identifier because there is no superClass (extension)", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "class File {}" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + assert.deepEqual(deobfuscator.identifiers, [ + { name: "File", type: "ClassDeclaration" } + ]); + }); + + it("should detect one identifier because superClass is not an Identifier but a CallExpression", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "class File extends (foo()) {}" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + assert.deepEqual(deobfuscator.identifiers, [ + { name: "File", type: "ClassDeclaration" } + ]); + }); + + it("should detect one FunctionDeclaration node", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "function foo() {}" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.FunctionDeclaration, 1); + assert.deepEqual(deobfuscator.identifiers, [ + { name: "foo", type: "FunctionDeclaration" } + ]); + }); + + it("should detect zero FunctionDeclaration (because foo is a CallExpression Node)", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "foo();" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.FunctionDeclaration, 0); + assert.strictEqual(deobfuscator.identifiers.length, 0); + }); + + it("should detect zero FunctionDeclaration for an IIFE (because there is no Identifier)", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "(function() {})()" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.FunctionDeclaration, 0); + assert.strictEqual(deobfuscator.identifiers.length, 0); + }); + + it("should detect three identifiers (one function declaration and two FunctionParams identifier)", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "function foo(err, result) {}" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.FunctionDeclaration, 1); + assert.deepEqual(deobfuscator.identifiers, [ + { name: "err", type: "FunctionParams" }, + { name: "result", type: "FunctionParams" }, + { name: "foo", type: "FunctionDeclaration" } + ]); + }); + + it("should detect a MemberExpression with two no-computed property", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "process.mainModule.foo" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.deepEqual(counters.MemberExpression, { + false: 2 + }); + }); + + it("should detect a MemberExpression with two computed properties and one non-computed", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "process.mainModule['foo']['bar']" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.deepEqual(counters.MemberExpression, { + true: 2, + false: 1 + }); + }); + + it("should detect no MemberExpression at all", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "process" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.deepEqual(counters.MemberExpression, {}); + }); + + it("should detect three identifiers (one ClassDeclaration and two MethodDefinition)", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + `class File { + constructor() {} + foo() {} + }` + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + assert.deepEqual(deobfuscator.identifiers, [ + { name: "File", type: "ClassDeclaration" }, + { name: "constructor", type: "MethodDefinition" }, + { name: "foo", type: "MethodDefinition" } + ]); + }); + + it("should detect four identifiers (one ClassDeclaration and two MethodDefinition and one FunctionParams)", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + `class File { + get foo() {} + set bar(value) {} + }` + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + assert.deepEqual(deobfuscator.identifiers, [ + { name: "File", type: "ClassDeclaration" }, + { name: "foo", type: "MethodDefinition" }, + { name: "bar", type: "MethodDefinition" }, + { name: "value", type: "FunctionParams" } + ]); + }); + + it("should detect one AssignmentExpression (with two Identifiers)", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "obj = { foo: 1 }" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.AssignmentExpression, 1); + assert.deepEqual(deobfuscator.identifiers, [ + { name: "obj", type: "AssignmentExpression" }, + { name: "foo", type: "Property" } + ]); + }); + + it("should detect zero AssignmentExpression but one Identifier", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "Object.assign(obj, { foo: 1 })" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.AssignmentExpression, 0); + assert.deepEqual(deobfuscator.identifiers, [ + { name: "foo", type: "Property" } + ]); + }); + + it("should detect an ObjectExpression with two Property node", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + `const obj = { + log: ['a', 'b', 'c'], + get latest() { + return this.log[this.log.length - 1]; + } + };` + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.VariableDeclarator, 1); + assert.strictEqual(counters.Property, 2); + assert.deepEqual(counters.MemberExpression, { + true: 1, + false: 3 + }); + + assert.deepEqual(deobfuscator.identifiers, [ + { name: "obj", type: "VariableDeclarator" }, + { name: "log", type: "Property" }, + { name: "latest", type: "Property" } + ]); + }); + + it("should detect one UnaryArray", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "!![]" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.DoubleUnaryExpression, 1); + }); + + it("should detect zero UnaryArray", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "![]" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.DoubleUnaryExpression, 0); + }); + + it("should detect all VariableDeclaration kinds", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "var foo; const a = 5; let b = 'foo';" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.VariableDeclarator, 3); + assert.deepEqual(counters.VariableDeclaration, { + var: 1, + const: 1, + let: 1 + }); + assert.deepEqual(deobfuscator.identifiers, [ + { name: "foo", type: "VariableDeclarator" }, + { name: "a", type: "VariableDeclarator" }, + { name: "b", type: "VariableDeclarator" } + ]); + }); + + it("should count the number of VariableDeclarator", () => { + const deobfuscator = new Deobfuscator(); + + const body = new JsSourceParser().parse( + "let a,b,c;" + ); + walkAst(body, (node) => deobfuscator.walk(node)); + + const counters = deobfuscator.aggregateCounters(); + assert.strictEqual(counters.VariableDeclarator, 3); + assert.deepEqual(counters.VariableDeclaration, { + let: 1 + }); + assert.deepEqual(deobfuscator.identifiers, [ + { name: "a", type: "VariableDeclarator" }, + { name: "b", type: "VariableDeclarator" }, + { name: "c", type: "VariableDeclarator" } + ]); + }); + }); + + describe("analyzeString", () => { + it("should detect static dictionary string", () => { + const deobfuscator = new Deobfuscator(); + assert.equal(deobfuscator.hasDictionaryString, false); + + deobfuscator.analyzeString("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + + assert.ok(deobfuscator.hasDictionaryString); + }); + + it("should detect morse", () => { + const deobfuscator = new Deobfuscator(); + assert.equal(deobfuscator.morseLiterals.size, 0); + + const morseStr = "--.- --.--"; + deobfuscator.analyzeString(morseStr); + + assert.equal(deobfuscator.morseLiterals.size, 1); + assert.ok(deobfuscator.morseLiterals.has(morseStr)); + }); + }); +}); + +function walkAst(body, callback = () => void 0) { + walk(body, { + enter(node) { + if (!Array.isArray(node)) { + callback(node); + } + } + }); +} diff --git a/test/NodeCounter.spec.js b/test/NodeCounter.spec.js new file mode 100644 index 0000000..cf0f9dd --- /dev/null +++ b/test/NodeCounter.spec.js @@ -0,0 +1,135 @@ +// Import Node.js Dependencies +import { describe, it, mock } from "node:test"; +import assert from "node:assert/strict"; + +// Import Third-party Dependencies +import { walk } from "estree-walker"; + +// Import Internal Dependencies +import { NodeCounter } from "../src/NodeCounter.js"; +import { JsSourceParser } from "../src/JsSourceParser.js"; +import { isNode } from "../src/utils/index.js"; + +describe("NodeCounter", () => { + describe("constructor", () => { + it("should use name options instead of type", () => { + const nc = new NodeCounter("UnaryExpression", { + name: "DoubleUnaryExpression" + }); + + assert.equal(nc.name, "DoubleUnaryExpression"); + }); + }); + + it("should trigger filter and match functions when node.type is matching", () => { + const match = mock.fn(); + const filter = mock.fn(() => true); + + const nc = new NodeCounter( + "FunctionDeclaration", + { match, filter } + ); + + const body = new JsSourceParser().parse( + "function foo() {};" + ); + walkAst(body, (node) => nc.walk(node)); + + assert.equal(filter.mock.callCount(), 1); + assert.equal(match.mock.callCount(), 1); + assert.equal(nc.count, 1); + assert.deepEqual(nc.properties, {}); + }); + + it("should count one for a FunctionDeclaration with an identifier", () => { + const ids = []; + const nc = new NodeCounter( + "FunctionDeclaration", + { + match: (node) => ids.push(node.id.name) + } + ); + assert.equal(nc.type, "FunctionDeclaration"); + assert.equal(nc.name, "FunctionDeclaration"); + assert.equal(nc.lookup, null); + + const body = new JsSourceParser().parse( + "function foo() {};" + ); + walkAst(body, (node) => nc.walk(node)); + + assert.equal(nc.count, 1); + assert.deepEqual(nc.properties, {}); + assert.deepEqual(ids, ["foo"]); + }); + + it("should count zero for a FunctionExpression with no identifier", () => { + const nc = new NodeCounter( + "FunctionExpression", + { + filter: (node) => isNode(node.id) && node.id.type === "Identifier" + } + ); + assert.equal(nc.type, "FunctionExpression"); + assert.equal(nc.lookup, null); + + const body = new JsSourceParser().parse( + "const foo = function() {};" + ); + walkAst(body, (node) => nc.walk(node)); + + assert.equal(nc.count, 0); + assert.deepEqual(nc.properties, {}); + }); + + it("should count VariableDeclaration kinds property", () => { + const nc = new NodeCounter( + "VariableDeclaration[kind]" + ); + assert.equal(nc.type, "VariableDeclaration"); + assert.equal(nc.lookup, "kind"); + + const body = new JsSourceParser().parse( + `let foo, xd = 5; + const yo = 2; + const mdr = 5;` + ); + walkAst(body, (node) => nc.walk(node)); + + assert.equal(nc.count, 3); + assert.deepEqual(nc.properties, { + let: 1, + const: 2 + }); + }); + + it("should count MemberExpression computed property", () => { + const nc = new NodeCounter( + "MemberExpression[computed]" + ); + assert.equal(nc.name, "MemberExpression"); + assert.equal(nc.type, "MemberExpression"); + assert.equal(nc.lookup, "computed"); + + const body = new JsSourceParser().parse( + "yoo.xd[\"damn\"].oh;" + ); + walkAst(body, (node) => nc.walk(node)); + + assert.equal(nc.count, 3); + assert.deepEqual(nc.properties, { + true: 1, + false: 2 + }); + }); +}); + +function walkAst(body, callback = () => void 0) { + walk(body, { + enter(node) { + if (!Array.isArray(node)) { + callback(node); + } + } + }); +} diff --git a/test/probes/fixtures/objectExpression/class-objectExpression.js b/test/probes/fixtures/objectExpression/class-objectExpression.js deleted file mode 100644 index f747c8f..0000000 --- a/test/probes/fixtures/objectExpression/class-objectExpression.js +++ /dev/null @@ -1,11 +0,0 @@ -class NotObj { - log; - - constructor() { - this.log = ['a', 'b', 'c']; - } - - get latest() { - return this.log[this.log.length - 1]; - } -}; diff --git a/test/probes/fixtures/objectExpression/object-objectExpression.js b/test/probes/fixtures/objectExpression/object-objectExpression.js deleted file mode 100644 index 982adf5..0000000 --- a/test/probes/fixtures/objectExpression/object-objectExpression.js +++ /dev/null @@ -1,6 +0,0 @@ -const obj = { - log: ['a', 'b', 'c'], - get latest() { - return this.log[this.log.length - 1]; - } -}; diff --git a/test/probes/isAssignmentExpression.spec.js b/test/probes/isAssignmentExpression.spec.js deleted file mode 100644 index 3376ae3..0000000 --- a/test/probes/isAssignmentExpression.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -// Import Node.js dependencies -import { test } from "node:test"; -import assert from "node:assert"; - -// Import Internal Dependencies -import { getSastAnalysis, parseScript } from "../utils/index.js"; -import isAssignmentExpression from "../../src/probes/isAssignmentExpression.js"; - -test("should detect 1 assignment expression", () => { - const str = "obj = { foo: 1 }"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isAssignmentExpression) - .execute(ast.body); - - assert.equal(sourceFile.idtypes.assignExpr, 1); -}); - -test("should detect 0 assignment expression", () => { - const str = "Object.assign(obj, { foo: 1 })"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isAssignmentExpression) - .execute(ast.body); - - assert.equal(sourceFile.idtypes.assignExpr, 0); -}); diff --git a/test/probes/isBinaryExpression.spec.js b/test/probes/isBinaryExpression.spec.js index c1fe51a..55317f0 100644 --- a/test/probes/isBinaryExpression.spec.js +++ b/test/probes/isBinaryExpression.spec.js @@ -12,7 +12,7 @@ test("should detect 1 deep binary expression", () => { const { sourceFile } = getSastAnalysis(str, isBinaryExpression) .execute(ast.body); - assert.equal(sourceFile.counter.deepBinaryExpr, 1); + assert.equal(sourceFile.deobfuscator.deepBinaryExpression, 1); }); test("should not detect deep binary expression", () => { @@ -21,5 +21,5 @@ test("should not detect deep binary expression", () => { const { sourceFile } = getSastAnalysis(str, isBinaryExpression) .execute(ast.body); - assert.equal(sourceFile.counter.deepBinaryExpr, 0); + assert.equal(sourceFile.deobfuscator.deepBinaryExpression, 0); }); diff --git a/test/probes/isClassDeclaration.spec.js b/test/probes/isClassDeclaration.spec.js deleted file mode 100644 index 715d20f..0000000 --- a/test/probes/isClassDeclaration.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -// Import Node.js dependencies -import { test } from "node:test"; -import assert from "node:assert"; - -// Import Internal Dependencies -import { getSastAnalysis, parseScript } from "../utils/index.js"; -import isClassDeclaration from "../../src/probes/isClassDeclaration.js"; - -test("should detect two identifiers (class name and superClass name A.K.A extends)", () => { - const str = "class File extends Blob {}"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isClassDeclaration) - .execute(ast.body); - - assert.deepEqual(sourceFile.identifiersName, [ - { name: "File", type: "class" }, - { name: "Blob", type: "class" } - ]); -}); - -test("should detect one identifier because there is no superClass (extension)", () => { - const str = "class File {}"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isClassDeclaration) - .execute(ast.body); - - assert.deepEqual(sourceFile.identifiersName, [ - { name: "File", type: "class" } - ]); -}); - -test("should detect one identifier because superClass is not an Identifier but a CallExpression", () => { - const str = "class File extends (foo()) {}"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isClassDeclaration) - .execute(ast.body); - - assert.deepEqual(sourceFile.identifiersName, [ - { name: "File", type: "class" } - ]); -}); diff --git a/test/probes/isFunction.spec.js b/test/probes/isFunction.spec.js deleted file mode 100644 index 4611173..0000000 --- a/test/probes/isFunction.spec.js +++ /dev/null @@ -1,47 +0,0 @@ -// Import Node.js dependencies -import { test } from "node:test"; -import assert from "node:assert"; - -// Import Internal Dependencies -import { getSastAnalysis, parseScript } from "../utils/index.js"; -import isFunctionDeclaration from "../../src/probes/isFunction.js"; - -test("should detect one FunctionDeclaration node", () => { - const str = "function foo() {}"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isFunctionDeclaration) - .execute(ast.body); - - assert.equal(sourceFile.idtypes.functionDeclaration, 1); -}); - -test("should detect zero FunctionDeclaration (because foo is a CallExpression Node)", () => { - const str = "foo()"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isFunctionDeclaration) - .execute(ast.body); - - assert.equal(sourceFile.idtypes.functionDeclaration, 0); -}); - -test("should detect zero FunctionDeclaration for an IIFE (because there is no Identifier)", () => { - const str = "(function() {})()"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isFunctionDeclaration) - .execute(ast.body); - - assert.equal(sourceFile.idtypes.functionDeclaration, 0); -}); - -test("should detect three identifiers (one function declaration and two params identifier)", () => { - const str = "function foo(err, result) {}"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isFunctionDeclaration) - .execute(ast.body); - - assert.deepEqual(sourceFile.identifiersName, [ - { name: "err", type: "params" }, - { name: "result", type: "params" }, - { name: "foo", type: "functionDeclaration" } - ]); -}); diff --git a/test/probes/isLiteral.spec.js b/test/probes/isLiteral.spec.js index 9cf2c50..6fcdea8 100644 --- a/test/probes/isLiteral.spec.js +++ b/test/probes/isLiteral.spec.js @@ -11,7 +11,7 @@ test("should throw an unsafe-import because the hexadecimal string is equal to t const ast = parseScript(str); const sastAnalysis = getSastAnalysis(str, isLiteral); - t.mock.method(sastAnalysis.sourceFile, "analyzeString"); + t.mock.method(sastAnalysis.sourceFile.deobfuscator, "analyzeString"); sastAnalysis.execute(ast.body); assert.strictEqual(sastAnalysis.warnings().length, 1); @@ -19,7 +19,7 @@ test("should throw an unsafe-import because the hexadecimal string is equal to t assert.strictEqual(warning.kind, "unsafe-import"); assert.ok(sastAnalysis.dependencies().has("http")); - const calls = sastAnalysis.sourceFile.analyzeString.mock.calls; + const calls = sastAnalysis.sourceFile.deobfuscator.analyzeString.mock.calls; assert.strictEqual(calls.length, 1); assert.ok(calls[0].arguments.includes("http")); }); @@ -30,14 +30,14 @@ test("should throw an encoded-literal warning because the hexadecimal value is e const ast = parseScript(str); const sastAnalysis = getSastAnalysis(str, isLiteral); - t.mock.method(sastAnalysis.sourceFile, "analyzeString"); + t.mock.method(sastAnalysis.sourceFile.deobfuscator, "analyzeString"); sastAnalysis.execute(ast.body); assert.strictEqual(sastAnalysis.warnings().length, 1); const warning = sastAnalysis.getWarning("encoded-literal"); assert.strictEqual(warning.value, "72657175697265"); - const calls = sastAnalysis.sourceFile.analyzeString.mock.calls; + const calls = sastAnalysis.sourceFile.deobfuscator.analyzeString.mock.calls; assert.strictEqual(calls.length, 1); assert.ok(calls[0].arguments.includes("require")); }); diff --git a/test/probes/isMemberExpression.spec.js b/test/probes/isMemberExpression.spec.js deleted file mode 100644 index b5dbfad..0000000 --- a/test/probes/isMemberExpression.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -// Import Node.js dependencies -import { test } from "node:test"; -import assert from "node:assert"; - -// Import Internal Dependencies -import { getSastAnalysis, parseScript } from "../utils/index.js"; -import isMemberExpression from "../../src/probes/isMemberExpression.js"; - -test("should detect 1 member expression", () => { - const str = "process.mainModule"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isMemberExpression) - .execute(ast.body); - - assert.equal(sourceFile.counter.memberExpr, 1); -}); - -test("should detect 2 members expressions", () => { - const str = "process.mainModule.foo"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isMemberExpression) - .execute(ast.body); - - assert.equal(sourceFile.counter.memberExpr, 2); -}); - -test("should detect 1 member expression and 2 nodes", () => { - const str = "process.mainModule['foo']['bar']"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isMemberExpression) - .execute(ast.body); - - assert.equal(sourceFile.counter.memberExpr, 1); - assert.equal(sourceFile.counter.computedMemberExpr, 2); -}); - -test("should detect 0 member expression", () => { - const str = "process"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isMemberExpression) - .execute(ast.body); - - assert.equal(sourceFile.counter.memberExpr, 0); -}); diff --git a/test/probes/isMethodDefinition.spec.js b/test/probes/isMethodDefinition.spec.js deleted file mode 100644 index b6fa003..0000000 --- a/test/probes/isMethodDefinition.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -// Import Node.js dependencies -import { test } from "node:test"; -import assert from "node:assert"; - -// Import Internal Dependencies -import { getSastAnalysis, parseScript } from "../utils/index.js"; -import isMethodDefinition from "../../src/probes/isMethodDefinition.js"; - -test("should detect two identifiers (constructor and one method definition)", () => { - const str = `class File { - constructor() {} - foo() {} - }`; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isMethodDefinition) - .execute(ast.body); - - assert.deepEqual(sourceFile.identifiersName, [ - { name: "constructor", type: "method" }, - { name: "foo", type: "method" } - ]); -}); - -test("should detect two identifiers (getter and setter)", () => { - const str = `class File { - get foo() {} - set bar(value) {} - }`; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isMethodDefinition) - .execute(ast.body); - - assert.deepEqual(sourceFile.identifiersName, [ - { name: "foo", type: "method" }, - { name: "bar", type: "method" } - ]); -}); diff --git a/test/probes/isObjectExpression.spec.js b/test/probes/isObjectExpression.spec.js deleted file mode 100644 index b6f3991..0000000 --- a/test/probes/isObjectExpression.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -// Import Node.js Dependencies -import { readFileSync } from "node:fs"; -import { test } from "node:test"; -import assert from "node:assert"; - -// Import Internal Dependencies -import { getSastAnalysis, parseScript } from "../utils/index.js"; -import isObjectExpression from "../../src/probes/isObjectExpression.js"; - -// CONSTANTS -const FIXTURE_URL = new URL("fixtures/objectExpression/", import.meta.url); - -test("object with 2 properties should have 2 identifiers", () => { - const str = readFileSync(new URL("object-objectExpression.js", FIXTURE_URL), "utf-8"); - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isObjectExpression) - .execute(ast.body); - - assert.equal(sourceFile.idtypes.property, 2); - assert.equal(sourceFile.identifiersName[0].name, "log"); - assert.equal(sourceFile.identifiersName[1].name, "latest"); -}); - -test("class with 2 properties should have 0 identifier", () => { - const str = readFileSync(new URL("class-objectExpression.js", FIXTURE_URL), "utf-8"); - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isObjectExpression) - .execute(ast.body); - - assert.equal(sourceFile.idtypes.property, 0); - assert.equal(sourceFile.identifiersName.length, 0); -}); diff --git a/test/probes/isUnaryExpression.spec.js b/test/probes/isUnaryExpression.spec.js deleted file mode 100644 index f5aa027..0000000 --- a/test/probes/isUnaryExpression.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -// Import Node.js dependencies -import { test } from "node:test"; -import assert from "node:assert"; - -// Import Internal Dependencies -import { getSastAnalysis, parseScript } from "../utils/index.js"; -import isUnaryExpression from "../../src/probes/isUnaryExpression.js"; - -test("should detect one UnaryArray", () => { - const str = "!![]"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isUnaryExpression) - .execute(ast.body); - - assert.strictEqual(sourceFile.counter.doubleUnaryArray, 1); -}); - -test("should not detect any UnaryArray", () => { - const str = "![]"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isUnaryExpression) - .execute(ast.body); - - assert.strictEqual(sourceFile.counter.doubleUnaryArray, 0); -}); diff --git a/test/probes/isVariableDeclaration.spec.js b/test/probes/isVariableDeclaration.spec.js deleted file mode 100644 index f18ff73..0000000 --- a/test/probes/isVariableDeclaration.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -// Import Node.js dependencies -import { test } from "node:test"; -import assert from "node:assert"; - -// Import Internal Dependencies -import { getSastAnalysis, parseScript } from "../utils/index.js"; -import isVariableDeclaration from "../../src/probes/isVariableDeclaration.js"; - -test("should detect and save all VariableDeclaration kinds", () => { - const str = "var foo; const a = 5; let b = 'foo';"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isVariableDeclaration) - .execute(ast.body); - - assert.deepEqual(sourceFile.varkinds, { - const: 1, - let: 1, - var: 1 - }); -}); - -test("should count the number of VariableDeclarator node", () => { - const str = "let a,b,c;"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isVariableDeclaration) - .execute(ast.body); - - assert.strictEqual(sourceFile.idtypes.variableDeclarator, 3); -}); - -test("should detect and save VariableDeclarator Identifier", () => { - const str = "let foobar = 5;"; - const ast = parseScript(str); - const { sourceFile } = getSastAnalysis(str, isVariableDeclaration) - .execute(ast.body); - - assert.deepEqual(sourceFile.identifiersName, [ - { - name: "foobar", - type: "variableDeclarator" - } - ]); -});