@nodesecure/js-x-ray 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +9 -209
  3. package/package.json +62 -77
  4. package/src/{AstAnalyser.js → AstAnalyser.ts} +283 -222
  5. package/src/{Deobfuscator.js → Deobfuscator.ts} +228 -195
  6. package/src/{EntryFilesAnalyser.js → EntryFilesAnalyser.ts} +206 -167
  7. package/src/JsSourceParser.ts +77 -0
  8. package/src/NodeCounter.ts +90 -0
  9. package/src/{ProbeRunner.js → ProbeRunner.ts} +167 -144
  10. package/src/SourceFile.ts +226 -0
  11. package/src/index.ts +5 -0
  12. package/src/obfuscators/freejsobfuscator.ts +17 -0
  13. package/src/obfuscators/{jjencode.js → jjencode.ts} +39 -27
  14. package/src/obfuscators/jsfuck.ts +19 -0
  15. package/src/obfuscators/{obfuscator-io.js → obfuscator-io.ts} +25 -13
  16. package/src/obfuscators/{trojan-source.js → trojan-source.ts} +3 -1
  17. package/src/probes/{isArrayExpression.js → isArrayExpression.ts} +12 -3
  18. package/src/probes/{isBinaryExpression.js → isBinaryExpression.ts} +74 -55
  19. package/src/probes/isESMExport.ts +50 -0
  20. package/src/probes/{isFetch.js → isFetch.ts} +28 -19
  21. package/src/probes/isImportDeclaration.ts +58 -0
  22. package/src/probes/{isLiteral.js → isLiteral.ts} +91 -70
  23. package/src/probes/isLiteralRegex.ts +42 -0
  24. package/src/probes/{isRegexObject.js → isRegexObject.ts} +71 -49
  25. package/src/probes/isRequire/RequireCallExpressionWalker.ts +142 -0
  26. package/src/probes/isRequire/{isRequire.js → isRequire.ts} +195 -148
  27. package/src/probes/isSerializeEnv.ts +65 -0
  28. package/src/probes/isSyncIO.ts +96 -0
  29. package/src/probes/isUnsafeCallee.ts +89 -0
  30. package/src/probes/isUnsafeCommand.ts +133 -0
  31. package/src/probes/isWeakCrypto.ts +69 -0
  32. package/src/types/estree.ts +35 -0
  33. package/src/utils/extractNode.ts +22 -0
  34. package/src/utils/index.ts +4 -0
  35. package/src/utils/{exportAssignmentHasRequireLeave.js → isOneLineExpressionExport.ts} +70 -40
  36. package/src/utils/notNullOrUndefined.ts +5 -0
  37. package/src/utils/toArrayLocation.ts +22 -0
  38. package/src/warnings.ts +146 -0
  39. package/index.d.ts +0 -46
  40. package/index.js +0 -4
  41. package/src/JsSourceParser.js +0 -57
  42. package/src/NodeCounter.js +0 -76
  43. package/src/SourceFile.js +0 -147
  44. package/src/obfuscators/freejsobfuscator.js +0 -9
  45. package/src/obfuscators/jsfuck.js +0 -11
  46. package/src/probes/isESMExport.js +0 -31
  47. package/src/probes/isImportDeclaration.js +0 -33
  48. package/src/probes/isLiteralRegex.js +0 -31
  49. package/src/probes/isRequire/RequireCallExpressionWalker.js +0 -93
  50. package/src/probes/isUnsafeCallee.js +0 -35
  51. package/src/probes/isWeakCrypto.js +0 -37
  52. package/src/utils/extractNode.js +0 -14
  53. package/src/utils/index.js +0 -8
  54. package/src/utils/isNode.js +0 -5
  55. package/src/utils/isOneLineExpressionExport.js +0 -24
  56. package/src/utils/isUnsafeCallee.js +0 -28
  57. package/src/utils/notNullOrUndefined.js +0 -3
  58. package/src/utils/rootLocation.js +0 -3
  59. package/src/utils/toArrayLocation.js +0 -11
  60. package/src/warnings.js +0 -77
  61. package/types/api.d.ts +0 -177
  62. package/types/warnings.d.ts +0 -36
@@ -1,222 +1,283 @@
1
- // Import Node.js Dependencies
2
- import fs from "node:fs/promises";
3
- import fsSync from "node:fs";
4
- import path from "node:path";
5
-
6
- // Import Third-party Dependencies
7
- import { walk } from "estree-walker";
8
- import isMinified from "is-minified-code";
9
-
10
- // Import Internal Dependencies
11
- import { SourceFile } from "./SourceFile.js";
12
- import { isOneLineExpressionExport } from "./utils/index.js";
13
- import { JsSourceParser } from "./JsSourceParser.js";
14
-
15
- export class AstAnalyser {
16
- /**
17
- * @constructor
18
- * @param {object} [options={}]
19
- * @param {SourceParser} [options.customParser]
20
- * @param {Array<object>} [options.customProbes]
21
- * @param {boolean} [options.skipDefaultProbes=false]
22
- */
23
- constructor(options = {}) {
24
- this.parser = options.customParser ?? new JsSourceParser();
25
- this.probesOptions = {
26
- customProbes: options.customProbes ?? [],
27
- skipDefaultProbes: options.skipDefaultProbes ?? false
28
- };
29
- }
30
-
31
- analyse(str, options = Object.create(null)) {
32
- const {
33
- isMinified = false,
34
- module = true,
35
- removeHTMLComments = false,
36
- initialize,
37
- finalize
38
- } = options;
39
-
40
- const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
41
- isEcmaScriptModule: Boolean(module)
42
- });
43
- const source = new SourceFile(str, this.probesOptions);
44
-
45
- // TODO: this check should be factorized in a way that we reuse it
46
- // on analyze and anlyseFile
47
- if (initialize) {
48
- if (typeof initialize !== "function") {
49
- throw new TypeError("options.initialize must be a function");
50
- }
51
- initialize(source);
52
- }
53
-
54
- // we walk each AST Nodes, this is a purely synchronous I/O
55
- walk(body, {
56
- enter(node) {
57
- // Skip the root of the AST.
58
- if (Array.isArray(node)) {
59
- return;
60
- }
61
-
62
- const action = source.walk(node);
63
- if (action === "skip") {
64
- this.skip();
65
- }
66
- }
67
- });
68
-
69
- // TODO: this check should be factorized in a way that we reuse it
70
- // on analyze and anlyseFile
71
- if (finalize) {
72
- if (typeof finalize !== "function") {
73
- throw new TypeError("options.finalize must be a function");
74
- }
75
- finalize(source);
76
- }
77
-
78
- // Add oneline-require flag if this is a one-line require expression
79
- if (isOneLineExpressionExport(body)) {
80
- source.flags.add("oneline-require");
81
- }
82
-
83
- return {
84
- ...source.getResult(isMinified),
85
- dependencies: source.dependencies,
86
- flags: source.flags
87
- };
88
- }
89
-
90
- async analyseFile(
91
- pathToFile,
92
- options = {}
93
- ) {
94
- try {
95
- const {
96
- packageName = null,
97
- module = true,
98
- removeHTMLComments = false,
99
- initialize,
100
- finalize
101
- } = options;
102
-
103
- const str = await fs.readFile(pathToFile, "utf-8");
104
- const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
105
-
106
- const isMin = filePathString.includes(".min") || isMinified(str);
107
- const data = this.analyse(str, {
108
- isMinified: isMin,
109
- module: path.extname(filePathString) === ".mjs" ? true : module,
110
- removeHTMLComments,
111
- initialize,
112
- finalize
113
- });
114
-
115
- if (packageName !== null) {
116
- data.dependencies.delete(packageName);
117
- }
118
-
119
- // Add is-minified flag if the file is minified and not a one-line require
120
- if (!data.flags.has("oneline-require") && isMin) {
121
- data.flags.add("is-minified");
122
- }
123
-
124
- return {
125
- ok: true,
126
- dependencies: data.dependencies,
127
- warnings: data.warnings,
128
- flags: data.flags
129
- };
130
- }
131
- catch (error) {
132
- return {
133
- ok: false,
134
- warnings: [
135
- { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
136
- ]
137
- };
138
- }
139
- }
140
-
141
- analyseFileSync(
142
- pathToFile,
143
- options = {}
144
- ) {
145
- try {
146
- const {
147
- packageName = null,
148
- module = true,
149
- removeHTMLComments = false,
150
- initialize,
151
- finalize
152
- } = options;
153
-
154
- const str = fsSync.readFileSync(pathToFile, "utf-8");
155
- const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
156
-
157
- const isMin = filePathString.includes(".min") || isMinified(str);
158
- const data = this.analyse(str, {
159
- isMinified: isMin,
160
- module: path.extname(filePathString) === ".mjs" ? true : module,
161
- removeHTMLComments,
162
- initialize,
163
- finalize
164
- });
165
-
166
- if (packageName !== null) {
167
- data.dependencies.delete(packageName);
168
- }
169
-
170
- // Add is-minified flag if the file is minified and not a one-line require
171
- if (!data.flags.has("oneline-require") && isMin) {
172
- data.flags.add("is-minified");
173
- }
174
-
175
- return {
176
- ok: true,
177
- dependencies: data.dependencies,
178
- warnings: data.warnings,
179
- flags: data.flags
180
- };
181
- }
182
- catch (error) {
183
- return {
184
- ok: false,
185
- warnings: [
186
- { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
187
- ]
188
- };
189
- }
190
- }
191
-
192
- /**
193
- * @param {!string} source
194
- * @param {object} options
195
- * @param {boolean} [options.removeHTMLComments=false]
196
- */
197
- prepareSource(source, options = {}) {
198
- if (typeof source !== "string") {
199
- throw new TypeError("source must be a string");
200
- }
201
- const { removeHTMLComments = false } = options;
202
-
203
- /**
204
- * if the file start with a shebang then we remove it because meriyah.parseScript fail to parse it.
205
- * @example
206
- * #!/usr/bin/env node
207
- */
208
- const rawNoShebang = source.startsWith("#") ?
209
- source.slice(source.indexOf("\n") + 1) : source;
210
-
211
- return removeHTMLComments ?
212
- this.#removeHTMLComment(rawNoShebang) : rawNoShebang;
213
- }
214
-
215
- /**
216
- * @param {!string} str
217
- * @returns {string}
218
- */
219
- #removeHTMLComment(str) {
220
- return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
221
- }
222
- }
1
+ // Import Node.js Dependencies
2
+ import fs from "node:fs/promises";
3
+ import fsSync from "node:fs";
4
+ import path from "node:path";
5
+
6
+ // Import Third-party Dependencies
7
+ import { walk } from "estree-walker";
8
+ import type { ESTree } from "meriyah";
9
+ import isMinified from "is-minified-code";
10
+
11
+ // Import Internal Dependencies
12
+ import { generateWarning, type Warning } from "./warnings.js";
13
+ import {
14
+ SourceFile,
15
+ type ProbesOptions,
16
+ type SourceFlags
17
+ } from "./SourceFile.js";
18
+ import { isOneLineExpressionExport } from "./utils/index.js";
19
+ import { JsSourceParser, type SourceParser } from "./JsSourceParser.js";
20
+
21
+ export interface Dependency {
22
+ unsafe: boolean;
23
+ inTry: boolean;
24
+ location?: null | ESTree.SourceLocation;
25
+ }
26
+
27
+ export interface RuntimeOptions {
28
+ /**
29
+ * @default true
30
+ */
31
+ module?: boolean;
32
+ /**
33
+ * @default false
34
+ */
35
+ removeHTMLComments?: boolean;
36
+ /**
37
+ * @default false
38
+ */
39
+ isMinified?: boolean;
40
+ initialize?: (sourceFile: SourceFile) => void;
41
+ finalize?: (sourceFile: SourceFile) => void;
42
+ }
43
+
44
+ export interface RuntimeFileOptions extends Omit<RuntimeOptions, "isMinified"> {
45
+ packageName?: string;
46
+ }
47
+
48
+ export interface Report {
49
+ dependencies: Map<string, Dependency>;
50
+ warnings: Warning[];
51
+ flags: Set<SourceFlags>;
52
+ idsLengthAvg: number;
53
+ stringScore: number;
54
+ }
55
+
56
+ export type ReportOnFile = {
57
+ ok: true;
58
+ warnings: Warning[];
59
+ dependencies: Map<string, Dependency>;
60
+ flags: Set<SourceFlags>;
61
+ } | {
62
+ ok: false;
63
+ warnings: Warning[];
64
+ };
65
+
66
+ export interface AstAnalyserOptions extends ProbesOptions {
67
+ /**
68
+ * @default JsSourceParser
69
+ */
70
+ customParser?: SourceParser;
71
+ }
72
+
73
+ export interface PrepareSourceOptions {
74
+ removeHTMLComments?: boolean;
75
+ }
76
+
77
+ export class AstAnalyser {
78
+ parser: SourceParser;
79
+ probesOptions: ProbesOptions;
80
+
81
+ constructor(options: AstAnalyserOptions = {}) {
82
+ this.parser = options.customParser ?? new JsSourceParser();
83
+ this.probesOptions = {
84
+ customProbes: options.customProbes ?? [],
85
+ skipDefaultProbes: options.skipDefaultProbes ?? false,
86
+ optionalWarnings: options.optionalWarnings ?? false
87
+ };
88
+ }
89
+
90
+ analyse(
91
+ str: string,
92
+ options: RuntimeOptions = {}
93
+ ): Report {
94
+ const {
95
+ isMinified = false,
96
+ module = true,
97
+ removeHTMLComments = false,
98
+ initialize,
99
+ finalize
100
+ } = options;
101
+
102
+ const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
103
+ isEcmaScriptModule: Boolean(module)
104
+ });
105
+ const source = new SourceFile(str, this.probesOptions);
106
+
107
+ // TODO: this check should be factorized in a way that we reuse it
108
+ // on analyze and anlyseFile
109
+ if (initialize) {
110
+ if (typeof initialize !== "function") {
111
+ throw new TypeError("options.initialize must be a function");
112
+ }
113
+ initialize(source);
114
+ }
115
+
116
+ // we walk each AST Nodes, this is a purely synchronous I/O
117
+ // @ts-expect-error
118
+ walk(body, {
119
+ enter(node) {
120
+ // Skip the root of the AST.
121
+ if (Array.isArray(node)) {
122
+ return;
123
+ }
124
+
125
+ const action = source.walk(node as ESTree.Node);
126
+ if (action === "skip") {
127
+ this.skip();
128
+ }
129
+ }
130
+ });
131
+
132
+ // TODO: this check should be factorized in a way that we reuse it
133
+ // on analyze and anlyseFile
134
+ if (finalize) {
135
+ if (typeof finalize !== "function") {
136
+ throw new TypeError("options.finalize must be a function");
137
+ }
138
+ finalize(source);
139
+ }
140
+
141
+ // Add oneline-require flag if this is a one-line require expression
142
+ if (isOneLineExpressionExport(body)) {
143
+ source.flags.add("oneline-require");
144
+ }
145
+
146
+ return {
147
+ ...source.getResult(isMinified),
148
+ dependencies: source.dependencies,
149
+ flags: source.flags
150
+ };
151
+ }
152
+
153
+ async analyseFile(
154
+ pathToFile: string | URL,
155
+ options: RuntimeFileOptions = {}
156
+ ): Promise<ReportOnFile> {
157
+ try {
158
+ const {
159
+ packageName = null,
160
+ module = true,
161
+ removeHTMLComments = false,
162
+ initialize,
163
+ finalize
164
+ } = options;
165
+
166
+ const str = await fs.readFile(pathToFile, "utf-8");
167
+ const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
168
+
169
+ const isMin = filePathString.includes(".min") || isMinified(str);
170
+ const data = this.analyse(str, {
171
+ isMinified: isMin,
172
+ module: path.extname(filePathString) === ".mjs" ? true : module,
173
+ removeHTMLComments,
174
+ initialize,
175
+ finalize
176
+ });
177
+
178
+ if (packageName !== null) {
179
+ data.dependencies.delete(packageName);
180
+ }
181
+
182
+ // Add is-minified flag if the file is minified and not a one-line require
183
+ if (!data.flags.has("oneline-require") && isMin) {
184
+ data.flags.add("is-minified");
185
+ }
186
+
187
+ return {
188
+ ok: true,
189
+ dependencies: data.dependencies,
190
+ warnings: data.warnings,
191
+ flags: data.flags
192
+ };
193
+ }
194
+ catch (error: any) {
195
+ return {
196
+ ok: false,
197
+ warnings: [
198
+ generateWarning("parsing-error", {
199
+ value: error.message
200
+ })
201
+ ]
202
+ };
203
+ }
204
+ }
205
+
206
+ analyseFileSync(
207
+ pathToFile: string | URL,
208
+ options: RuntimeFileOptions = {}
209
+ ): ReportOnFile {
210
+ try {
211
+ const {
212
+ packageName = null,
213
+ module = true,
214
+ removeHTMLComments = false,
215
+ initialize,
216
+ finalize
217
+ } = options;
218
+
219
+ const str = fsSync.readFileSync(pathToFile, "utf-8");
220
+ const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
221
+
222
+ const isMin = filePathString.includes(".min") || isMinified(str);
223
+ const data = this.analyse(str, {
224
+ isMinified: isMin,
225
+ module: path.extname(filePathString) === ".mjs" ? true : module,
226
+ removeHTMLComments,
227
+ initialize,
228
+ finalize
229
+ });
230
+
231
+ if (packageName !== null) {
232
+ data.dependencies.delete(packageName);
233
+ }
234
+
235
+ // Add is-minified flag if the file is minified and not a one-line require
236
+ if (!data.flags.has("oneline-require") && isMin) {
237
+ data.flags.add("is-minified");
238
+ }
239
+
240
+ return {
241
+ ok: true,
242
+ dependencies: data.dependencies,
243
+ warnings: data.warnings,
244
+ flags: data.flags
245
+ };
246
+ }
247
+ catch (error: any) {
248
+ return {
249
+ ok: false,
250
+ warnings: [
251
+ generateWarning("parsing-error", {
252
+ value: error.message
253
+ })
254
+ ]
255
+ };
256
+ }
257
+ }
258
+
259
+ prepareSource(
260
+ source: string,
261
+ options: PrepareSourceOptions = {}
262
+ ): string {
263
+ if (typeof source !== "string") {
264
+ throw new TypeError("source must be a string");
265
+ }
266
+ const { removeHTMLComments = false } = options;
267
+
268
+ /**
269
+ * if the file start with a shebang then we remove it because meriyah.parseScript fail to parse it.
270
+ * @example
271
+ * #!/usr/bin/env node
272
+ */
273
+ const rawNoShebang = source.startsWith("#") ?
274
+ source.slice(source.indexOf("\n") + 1) : source;
275
+
276
+ return removeHTMLComments ?
277
+ this.#removeHTMLComment(rawNoShebang) : rawNoShebang;
278
+ }
279
+
280
+ #removeHTMLComment(str: string): string {
281
+ return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
282
+ }
283
+ }