Skip to content

Commit 283d5b6

Browse files
authored
refactor: integrate estree-walker using meriyah ESTree types (#397)
1 parent f66af80 commit 283d5b6

File tree

14 files changed

+678
-80
lines changed

14 files changed

+678
-80
lines changed

.changeset/open-lines-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nodesecure/js-x-ray": minor
3+
---
4+
5+
Integrate estree-walker natively using meriyah ESTree types

workspaces/js-x-ray/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
"@nodesecure/sec-literal": "^1.2.0",
5050
"@nodesecure/tracer": "^2.0.0",
5151
"digraph-js": "^2.2.3",
52-
"estree-walker": "^3.0.1",
5352
"frequency-set": "^1.0.2",
5453
"is-minified-code": "^2.0.0",
5554
"meriyah": "^6.0.0",

workspaces/js-x-ray/src/AstAnalyser.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import fsSync from "node:fs";
44
import path from "node:path";
55

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

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

2626
export interface Dependency {
@@ -161,19 +161,16 @@ export class AstAnalyser {
161161
}
162162

163163
// we walk each AST Nodes, this is a purely synchronous I/O
164-
// @ts-expect-error
165-
walk(body, {
166-
enter(node: any) {
167-
// Skip the root of the AST.
168-
if (Array.isArray(node)) {
169-
return;
170-
}
171-
172-
source.walk(node);
173-
const action = runner.walk(node);
174-
if (action === "skip") {
175-
this.skip();
176-
}
164+
walkEnter(body, function walk(node) {
165+
// Skip the root of the AST.
166+
if (Array.isArray(node)) {
167+
return;
168+
}
169+
170+
source.walk(node);
171+
const action = runner.walk(node);
172+
if (action === "skip") {
173+
this.skip();
177174
}
178175
});
179176

workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.ts

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import path from "node:path";
33

44
// Import Third-party Dependencies
55
import { Hex } from "@nodesecure/sec-literal";
6-
import { walk as doWalk } from "estree-walker";
76
import {
87
arrayExpressionToString,
98
getMemberExpressionIdentifier,
@@ -17,6 +16,7 @@ import {
1716
isLiteral,
1817
isCallExpression
1918
} from "../../types/estree.js";
19+
import { walkEnter } from "../../walker/index.js";
2020

2121
export class RequireCallExpressionWalker {
2222
tracer: VariableTracer;
@@ -41,47 +41,44 @@ export class RequireCallExpressionWalker {
4141

4242
// we need the `this` context of doWalk.enter
4343
const self = this;
44-
// @ts-expect-error
45-
doWalk(callExprNode, {
46-
enter(node: any) {
47-
if (
48-
!isCallExpression(node) ||
49-
node.arguments.length === 0
50-
) {
51-
return;
52-
}
53-
54-
const castedNode = node as ESTree.CallExpression;
55-
const rootArgument = castedNode.arguments.at(0)!;
56-
if (
57-
rootArgument.type === "Literal" &&
58-
typeof rootArgument.value === "string" &&
59-
Hex.isHex(rootArgument.value)
60-
) {
61-
self.dependencies.add(Buffer.from(rootArgument.value, "hex").toString());
62-
this.skip();
63-
64-
return;
65-
}
66-
67-
const fullName = castedNode.callee.type === "MemberExpression" ?
68-
[...getMemberExpressionIdentifier(castedNode.callee)].join(".") :
69-
castedNode.callee.name;
70-
const tracedFullName = self.tracer.getDataFromIdentifier(fullName)?.identifierOrMemberExpr ?? fullName;
71-
switch (tracedFullName) {
72-
case "atob":
73-
self.#handleAtob(castedNode);
74-
break;
75-
case "Buffer.from":
76-
self.#handleBufferFrom(castedNode);
77-
break;
78-
case "require.resolve":
79-
self.#handleRequireResolve(rootArgument);
80-
break;
81-
case "path.join":
82-
self.#handlePathJoin(castedNode);
83-
break;
84-
}
44+
walkEnter(callExprNode, function enter(node) {
45+
if (
46+
!isCallExpression(node) ||
47+
node.arguments.length === 0
48+
) {
49+
return;
50+
}
51+
52+
const castedNode = node as ESTree.CallExpression;
53+
const rootArgument = castedNode.arguments.at(0)!;
54+
if (
55+
rootArgument.type === "Literal" &&
56+
typeof rootArgument.value === "string" &&
57+
Hex.isHex(rootArgument.value)
58+
) {
59+
self.dependencies.add(Buffer.from(rootArgument.value, "hex").toString());
60+
this.skip();
61+
62+
return;
63+
}
64+
65+
const fullName = castedNode.callee.type === "MemberExpression" ?
66+
[...getMemberExpressionIdentifier(castedNode.callee)].join(".") :
67+
castedNode.callee.name;
68+
const tracedFullName = self.tracer.getDataFromIdentifier(fullName)?.identifierOrMemberExpr ?? fullName;
69+
switch (tracedFullName) {
70+
case "atob":
71+
self.#handleAtob(castedNode);
72+
break;
73+
case "Buffer.from":
74+
self.#handleBufferFrom(castedNode);
75+
break;
76+
case "require.resolve":
77+
self.#handleRequireResolve(rootArgument);
78+
break;
79+
case "path.join":
80+
self.#handlePathJoin(castedNode);
81+
break;
8582
}
8683
});
8784

workspaces/js-x-ray/src/types/estree.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type RegExpLiteral<T> = ESTree.RegExpLiteral & {
1010
};
1111

1212
export function isNode(
13-
value: any
13+
value: unknown
1414
): value is ESTree.Node {
1515
return (
1616
value !== null &&
@@ -21,15 +21,15 @@ export function isNode(
2121
}
2222

2323
export function isLiteral(
24-
node: any
24+
node: unknown
2525
): node is Literal<string> {
2626
return isNode(node) &&
2727
node.type === "Literal" &&
2828
typeof node.value === "string";
2929
}
3030

3131
export function isTemplateLiteral(
32-
node: any
32+
node: unknown
3333
): node is ESTree.TemplateLiteral {
3434
if (!isNode(node) || node.type !== "TemplateLiteral") {
3535
return false;
@@ -47,7 +47,7 @@ export function isTemplateLiteral(
4747
}
4848

4949
export function isCallExpression(
50-
node: any
50+
node: unknown
5151
): node is ESTree.CallExpression {
5252
return isNode(node) && node.type === "CallExpression";
5353
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Import Third-party Dependencies
2+
import type { ESTree } from "meriyah";
3+
4+
// Import Internal Dependencies
5+
import { SyncWalker, type SyncHandler } from "./walker.sync.js";
6+
7+
export type WalkRootNode = ESTree.Program | ESTree.Program["body"] | ESTree.Node;
8+
9+
export function walk(
10+
ast: WalkRootNode,
11+
{ enter, leave }: { enter?: SyncHandler; leave?: SyncHandler; } = {}
12+
) {
13+
const instance = new SyncWalker(enter, leave);
14+
15+
return instance.visit(
16+
ast as unknown as ESTree.Node,
17+
{ parent: null }
18+
);
19+
}
20+
21+
export function walkEnter(
22+
ast: WalkRootNode,
23+
enter: SyncHandler
24+
): ESTree.Node | null {
25+
return walk(ast, { enter });
26+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Import Third-party Dependencies
2+
import type { ESTree } from "meriyah";
3+
4+
export interface WalkerContext {
5+
skip: () => void;
6+
remove: () => void;
7+
replace: (node: ESTree.Node) => void;
8+
}
9+
10+
export class WalkerBase {
11+
should_skip = false;
12+
should_remove = false;
13+
replacement: ESTree.Node | null = null;
14+
context: WalkerContext;
15+
16+
constructor() {
17+
this.context = {
18+
skip: () => (this.should_skip = true),
19+
remove: () => (this.should_remove = true),
20+
replace: (node) => (this.replacement = node)
21+
};
22+
}
23+
24+
// eslint-disable-next-line max-params
25+
replace(
26+
parent: ESTree.Node | null | undefined,
27+
prop: string | number | symbol | null | undefined,
28+
index: number | null | undefined,
29+
node: ESTree.Node
30+
) {
31+
if (parent && prop) {
32+
if (index === null) {
33+
parent[prop] = node;
34+
}
35+
else {
36+
parent[prop][index] = node;
37+
}
38+
}
39+
}
40+
41+
remove(
42+
parent: ESTree.Node | null | undefined,
43+
prop: string | number | symbol | null | undefined,
44+
index: number | null | undefined
45+
) {
46+
if (parent && prop) {
47+
if (index !== null && index !== undefined) {
48+
parent[prop].splice(index, 1);
49+
}
50+
else {
51+
delete parent[prop];
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)