Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/open-lines-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/js-x-ray": minor
---

Integrate estree-walker natively using meriyah ESTree types
1 change: 0 additions & 1 deletion workspaces/js-x-ray/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"@nodesecure/sec-literal": "^1.2.0",
"@nodesecure/tracer": "^2.0.0",
"digraph-js": "^2.2.3",
"estree-walker": "^3.0.1",
"frequency-set": "^1.0.2",
"is-minified-code": "^2.0.0",
"meriyah": "^6.0.0",
Expand Down
25 changes: 11 additions & 14 deletions workspaces/js-x-ray/src/AstAnalyser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import fsSync from "node:fs";
import path from "node:path";

// Import Third-party Dependencies
import { walk } from "estree-walker";
import type { ESTree } from "meriyah";
import isMinified from "is-minified-code";

Expand All @@ -21,6 +20,7 @@ import {
import { isOneLineExpressionExport } from "./utils/index.js";
import { JsSourceParser, type SourceParser } from "./JsSourceParser.js";
import { ProbeRunner, type Probe } from "./ProbeRunner.js";
import { walkEnter } from "./walker/index.js";
import * as trojan from "./obfuscators/trojan-source.js";

export interface Dependency {
Expand Down Expand Up @@ -161,19 +161,16 @@ export class AstAnalyser {
}

// we walk each AST Nodes, this is a purely synchronous I/O
// @ts-expect-error
walk(body, {
enter(node: any) {
// Skip the root of the AST.
if (Array.isArray(node)) {
return;
}

source.walk(node);
const action = runner.walk(node);
if (action === "skip") {
this.skip();
}
walkEnter(body, function walk(node) {
// Skip the root of the AST.
if (Array.isArray(node)) {
return;
}

source.walk(node);
const action = runner.walk(node);
if (action === "skip") {
this.skip();
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import path from "node:path";

// Import Third-party Dependencies
import { Hex } from "@nodesecure/sec-literal";
import { walk as doWalk } from "estree-walker";
import {
arrayExpressionToString,
getMemberExpressionIdentifier,
Expand All @@ -17,6 +16,7 @@ import {
isLiteral,
isCallExpression
} from "../../types/estree.js";
import { walkEnter } from "../../walker/index.js";

export class RequireCallExpressionWalker {
tracer: VariableTracer;
Expand All @@ -41,47 +41,44 @@ export class RequireCallExpressionWalker {

// we need the `this` context of doWalk.enter
const self = this;
// @ts-expect-error
doWalk(callExprNode, {
enter(node: any) {
if (
!isCallExpression(node) ||
node.arguments.length === 0
) {
return;
}

const castedNode = node as ESTree.CallExpression;
const rootArgument = castedNode.arguments.at(0)!;
if (
rootArgument.type === "Literal" &&
typeof rootArgument.value === "string" &&
Hex.isHex(rootArgument.value)
) {
self.dependencies.add(Buffer.from(rootArgument.value, "hex").toString());
this.skip();

return;
}

const fullName = castedNode.callee.type === "MemberExpression" ?
[...getMemberExpressionIdentifier(castedNode.callee)].join(".") :
castedNode.callee.name;
const tracedFullName = self.tracer.getDataFromIdentifier(fullName)?.identifierOrMemberExpr ?? fullName;
switch (tracedFullName) {
case "atob":
self.#handleAtob(castedNode);
break;
case "Buffer.from":
self.#handleBufferFrom(castedNode);
break;
case "require.resolve":
self.#handleRequireResolve(rootArgument);
break;
case "path.join":
self.#handlePathJoin(castedNode);
break;
}
walkEnter(callExprNode, function enter(node) {
if (
!isCallExpression(node) ||
node.arguments.length === 0
) {
return;
}

const castedNode = node as ESTree.CallExpression;
const rootArgument = castedNode.arguments.at(0)!;
if (
rootArgument.type === "Literal" &&
typeof rootArgument.value === "string" &&
Hex.isHex(rootArgument.value)
) {
self.dependencies.add(Buffer.from(rootArgument.value, "hex").toString());
this.skip();

return;
}

const fullName = castedNode.callee.type === "MemberExpression" ?
[...getMemberExpressionIdentifier(castedNode.callee)].join(".") :
castedNode.callee.name;
const tracedFullName = self.tracer.getDataFromIdentifier(fullName)?.identifierOrMemberExpr ?? fullName;
switch (tracedFullName) {
case "atob":
self.#handleAtob(castedNode);
break;
case "Buffer.from":
self.#handleBufferFrom(castedNode);
break;
case "require.resolve":
self.#handleRequireResolve(rootArgument);
break;
case "path.join":
self.#handlePathJoin(castedNode);
break;
}
});

Expand Down
8 changes: 4 additions & 4 deletions workspaces/js-x-ray/src/types/estree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type RegExpLiteral<T> = ESTree.RegExpLiteral & {
};

export function isNode(
value: any
value: unknown
): value is ESTree.Node {
return (
value !== null &&
Expand All @@ -21,15 +21,15 @@ export function isNode(
}

export function isLiteral(
node: any
node: unknown
): node is Literal<string> {
return isNode(node) &&
node.type === "Literal" &&
typeof node.value === "string";
}

export function isTemplateLiteral(
node: any
node: unknown
): node is ESTree.TemplateLiteral {
if (!isNode(node) || node.type !== "TemplateLiteral") {
return false;
Expand All @@ -47,7 +47,7 @@ export function isTemplateLiteral(
}

export function isCallExpression(
node: any
node: unknown
): node is ESTree.CallExpression {
return isNode(node) && node.type === "CallExpression";
}
26 changes: 26 additions & 0 deletions workspaces/js-x-ray/src/walker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Import Third-party Dependencies
import type { ESTree } from "meriyah";

// Import Internal Dependencies
import { SyncWalker, type SyncHandler } from "./walker.sync.js";

export type WalkRootNode = ESTree.Program | ESTree.Program["body"] | ESTree.Node;

export function walk(
ast: WalkRootNode,
{ enter, leave }: { enter?: SyncHandler; leave?: SyncHandler; } = {}
) {
const instance = new SyncWalker(enter, leave);

return instance.visit(
ast as unknown as ESTree.Node,
{ parent: null }
);
}

export function walkEnter(
ast: WalkRootNode,
enter: SyncHandler
): ESTree.Node | null {
return walk(ast, { enter });
}
55 changes: 55 additions & 0 deletions workspaces/js-x-ray/src/walker/walker.base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Import Third-party Dependencies
import type { ESTree } from "meriyah";

export interface WalkerContext {
skip: () => void;
remove: () => void;
replace: (node: ESTree.Node) => void;
}

export class WalkerBase {
should_skip = false;
should_remove = false;
replacement: ESTree.Node | null = null;
context: WalkerContext;

constructor() {
this.context = {
skip: () => (this.should_skip = true),
remove: () => (this.should_remove = true),
replace: (node) => (this.replacement = node)
};
}

// eslint-disable-next-line max-params
replace(
parent: ESTree.Node | null | undefined,
prop: string | number | symbol | null | undefined,
index: number | null | undefined,
node: ESTree.Node
) {
if (parent && prop) {
if (index === null) {
parent[prop] = node;
}
else {
parent[prop][index] = node;
}
}
}

remove(
parent: ESTree.Node | null | undefined,
prop: string | number | symbol | null | undefined,
index: number | null | undefined
) {
if (parent && prop) {
if (index !== null && index !== undefined) {
parent[prop].splice(index, 1);
}
else {
delete parent[prop];
}
}
}
}
Loading