@nodesecure/js-x-ray 6.2.1 → 7.0.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 (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +121 -22
  3. package/index.d.ts +8 -3
  4. package/index.js +37 -134
  5. package/package.json +12 -3
  6. package/src/AstAnalyser.js +137 -0
  7. package/src/Deobfuscator.js +192 -0
  8. package/src/JsSourceParser.js +57 -0
  9. package/src/NodeCounter.js +76 -0
  10. package/src/ProbeRunner.js +140 -0
  11. package/src/{Analysis.js → SourceFile.js} +55 -68
  12. package/src/obfuscators/freejsobfuscator.js +9 -9
  13. package/src/obfuscators/jjencode.js +7 -7
  14. package/src/obfuscators/jsfuck.js +6 -6
  15. package/src/obfuscators/obfuscator-io.js +7 -7
  16. package/src/obfuscators/trojan-source.js +28 -28
  17. package/src/probes/isArrayExpression.js +32 -30
  18. package/src/probes/isBinaryExpression.js +5 -3
  19. package/src/probes/isImportDeclaration.js +7 -4
  20. package/src/probes/isLiteral.js +10 -8
  21. package/src/probes/isLiteralRegex.js +5 -3
  22. package/src/probes/isRegexObject.js +5 -3
  23. package/src/probes/isRequire/RequireCallExpressionWalker.js +93 -0
  24. package/src/probes/isRequire/isRequire.js +142 -0
  25. package/src/probes/isUnsafeCallee.js +15 -5
  26. package/src/probes/isWeakCrypto.js +5 -3
  27. package/src/utils/exportAssignmentHasRequireLeave.js +40 -0
  28. package/src/utils/extractNode.js +14 -0
  29. package/src/utils/index.js +8 -0
  30. package/src/utils/isNode.js +5 -0
  31. package/src/utils/isOneLineExpressionExport.js +18 -0
  32. package/src/utils/isUnsafeCallee.js +28 -0
  33. package/src/utils/notNullOrUndefined.js +3 -0
  34. package/src/utils/rootLocation.js +3 -0
  35. package/src/utils/toArrayLocation.js +11 -0
  36. package/src/warnings.js +1 -1
  37. package/types/api.d.ts +62 -18
  38. package/types/warnings.d.ts +6 -5
  39. package/src/ASTDeps.js +0 -63
  40. package/src/obfuscators/index.js +0 -69
  41. package/src/probes/index.js +0 -70
  42. package/src/probes/isAssignmentExpression.js +0 -29
  43. package/src/probes/isClassDeclaration.js +0 -25
  44. package/src/probes/isFunction.js +0 -38
  45. package/src/probes/isMemberExpression.js +0 -16
  46. package/src/probes/isMethodDefinition.js +0 -25
  47. package/src/probes/isObjectExpression.js +0 -29
  48. package/src/probes/isRequire.js +0 -164
  49. package/src/probes/isUnaryExpression.js +0 -26
  50. package/src/probes/isVariableDeclaration.js +0 -30
  51. package/src/utils.js +0 -48
  52. package/types/astdeps.d.ts +0 -33
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 NodeSecure
3
+ Copyright (c) 2021-2024 NodeSecure
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -20,12 +20,10 @@
20
20
  </a>
21
21
  </p>
22
22
 
23
- JavaScript AST analysis. This package has been created to export the [Node-Secure](https://github.com/ES-Community/nsecure) AST Analysis to enable better code evolution and allow better access to developers and researchers.
23
+ JavaScript AST analysis. This package has been created to export the [NodeSecure](https://github.com/NodeSecure/cli) AST Analysis to enable better code evolution and allow better access to developers and researchers.
24
24
 
25
25
  The goal is to quickly identify dangerous code and patterns for developers and Security researchers. Interpreting the results of this tool will still require you to have a set of security notions.
26
26
 
27
- > **Note** I have no particular background in security. I'm simply becoming more and more interested and passionate about static code analysis. But I would be more than happy to learn that my work can help prevent potential future attacks (or leaks).
28
-
29
27
  ## Goals
30
28
  The objective of the project is to successfully detect all potentially suspicious JavaScript codes.. The target is obviously codes that are added or injected for malicious purposes..
31
29
 
@@ -70,22 +68,20 @@ require(Buffer.from("6673", "hex").toString());
70
68
  Then use `js-x-ray` to run an analysis of the JavaScript code:
71
69
  ```js
72
70
  import { runASTAnalysis } from "@nodesecure/js-x-ray";
73
- import { readFileSync } from "fs";
74
-
75
- const str = readFileSync("./file.js", "utf-8");
76
- const { warnings, dependencies } = runASTAnalysis(str);
71
+ import { readFileSync } from "node:fs";
77
72
 
78
- const dependenciesName = [...dependencies];
79
- const inTryDeps = [...dependencies.getDependenciesInTryStatement()];
73
+ const { warnings, dependencies } = runASTAnalysis(
74
+ readFileSync("./file.js", "utf-8")
75
+ );
80
76
 
81
- console.log(dependenciesName);
82
- console.log(inTryDeps);
83
- console.log(warnings);
77
+ console.log(dependencies);
78
+ console.dir(warnings, { depth: null });
84
79
  ```
85
80
 
86
81
  The analysis will return: `http` (in try), `crypto`, `util` and `fs`.
87
82
 
88
- > **Warning** There is also a lot of suspicious code example in the `./examples` cases directory. Feel free to try the tool on these files.
83
+ > [!TIP]
84
+ > There is also a lot of suspicious code example in the `./examples` cases directory. Feel free to try the tool on these files.
89
85
 
90
86
  ## Warnings
91
87
 
@@ -122,8 +118,6 @@ console.log(i18n.getTokenSync(jsxray.warnings["parsing-error"].i18n));
122
118
 
123
119
  ## Warnings Legends
124
120
 
125
- > **Warning** versions of NodeSecure greather than v0.7.0 are no longer compatible with the warnings table below.
126
-
127
121
  This section describe all the possible warnings returned by JSXRay. Click on the warning **name** for additional information and examples.
128
122
 
129
123
  | name | experimental | description |
@@ -140,16 +134,90 @@ This section describe all the possible warnings returned by JSXRay. Click on the
140
134
  | [weak-crypto](./docs/weak-crypto.md) | ✔️ | The code probably contains a weak crypto algorithm (md5, sha1...) |
141
135
  | [shady-link](./docs/shady-link.md) | ✔️ | The code contains shady/unsafe link |
142
136
 
143
- ## API
137
+ ## Custom Probes
138
+
139
+ You can also create custom probes to detect specific pattern in the code you are analyzing.
140
+
141
+ A probe is a pair of two functions (`validateNode` and `main`) that will be called on each node of the AST. It will return a warning if the pattern is detected.
142
+ Below a basic probe that detect a string assignation to `danger`:
143
+
144
+ ```ts
145
+ export const customProbes = [
146
+ {
147
+ name: "customProbeUnsafeDanger",
148
+ validateNode: (node, sourceFile) => [
149
+ node.type === "VariableDeclaration" && node.declarations[0].init.value === "danger"
150
+ ],
151
+ main: (node, options) => {
152
+ const { sourceFile, data: calleeName } = options;
153
+ if (node.declarations[0].init.value === "danger") {
154
+ sourceFile.addWarning("unsafe-danger", calleeName, node.loc);
155
+
156
+ return ProbeSignals.Skip;
157
+ }
158
+
159
+ return null;
160
+ }
161
+ }
162
+ ];
163
+ ```
164
+
165
+ You can pass an array of probes to the `runASTAnalysis/runASTAnalysisOnFile` functions as `options`, or directly to the `AstAnalyser` constructor.
166
+
167
+ | Name | Type | Description | Default Value |
168
+ |------------------|----------------------------------|-----------------------------------------------------------------------|-----------------|
169
+ | `customParser` | `SourceParser \| undefined` | An optional custom parser to be used for parsing the source code. | `JsSourceParser` |
170
+ | `customProbes` | `Probe[] \| undefined` | An array of custom probes to be used during AST analysis. | `[]` |
171
+ | `skipDefaultProbes` | `boolean \| undefined` | If `true`, default probes will be skipped and only custom probes will be used. | `false` |
172
+
173
+
174
+ Here using the example probe upper:
175
+
176
+ ```ts
177
+ import { runASTAnalysis } from "@nodesecure/js-x-ray";
178
+
179
+ // add your customProbes here (see example above)
180
+
181
+ const result = runASTAnalysis("const danger = 'danger';", { customProbes, skipDefaultProbes: true });
182
+
183
+ console.log(result);
184
+ ```
185
+
186
+ Result:
187
+
188
+ ```sh
189
+ ✗ node example.js
190
+ {
191
+ idsLengthAvg: 0,
192
+ stringScore: 0,
193
+ warnings: [ { kind: 'unsafe-danger', location: [Array], source: 'JS-X-Ray' } ],
194
+ dependencies: Map(0) {},
195
+ isOneLineRequire: false
196
+ }
197
+ ```
144
198
 
199
+ Congrats, you have created your first custom probe! 🎉
200
+
201
+ > [!TIP]
202
+ > Check the types in [index.d.ts](index.d.ts) and [types/api.d.ts](types/api.d.ts) for more details about the `options`
203
+
204
+ ## API
145
205
  <details>
146
- <summary>runASTAnalysis(str: string, options?: RuntimeOptions): Report</summary>
206
+ <summary>runASTAnalysis(str: string, options?: RuntimeOptions & AstAnalyserOptions): Report</summary>
147
207
 
148
208
  ```ts
149
209
  interface RuntimeOptions {
150
210
  module?: boolean;
151
- isMinified?: boolean;
152
211
  removeHTMLComments?: boolean;
212
+ isMinified?: boolean;
213
+ }
214
+ ```
215
+
216
+ ```ts
217
+ interface AstAnalyserOptions {
218
+ customParser?: SourceParser;
219
+ customProbes?: Probe[];
220
+ skipDefaultProbes?: boolean;
153
221
  }
154
222
  ```
155
223
 
@@ -168,13 +236,21 @@ interface Report {
168
236
  </details>
169
237
 
170
238
  <details>
171
- <summary>runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions): Promise< ReportOnFile ></summary>
239
+ <summary>runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions & AstAnalyserOptions): Promise< ReportOnFile ></summary>
172
240
 
173
241
  ```ts
174
- interface RuntimeOptions {
242
+ interface RuntimeFileOptions {
175
243
  module?: boolean;
176
- isMinified?: boolean;
177
244
  removeHTMLComments?: boolean;
245
+ packageName?: string;
246
+ }
247
+ ```
248
+
249
+ ```ts
250
+ interface AstAnalyserOptions {
251
+ customParser?: SourceParser;
252
+ customProbes?: Probe[];
253
+ skipDefaultProbes?: boolean;
178
254
  }
179
255
  ```
180
256
 
@@ -194,11 +270,27 @@ export type ReportOnFile = {
194
270
 
195
271
  </details>
196
272
 
273
+ ## Workspaces
274
+
275
+ Click on one of the links to access the documentation of the workspace:
276
+
277
+ | name | package and link |
278
+ | --- | --- |
279
+ | estree-ast-utils | [@nodesecure/estree-ast-utils](./workspaces/estree-ast-utils) |
280
+ | sec-literal | [@nodesecure/sec-literal ](./workspaces/sec-literal) |
281
+ | ts-source-parser | [@nodesecure/ts-source-parser ](./workspaces/ts-source-parser) |
282
+
283
+ These packages are available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com).
284
+ ```bash
285
+ $ npm i @nodesecure/estree-ast-util
286
+ # or
287
+ $ yarn add @nodesecure/estree-ast-util
288
+ ```
197
289
 
198
290
  ## Contributors ✨
199
291
 
200
292
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
201
- [![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)
293
+ [![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors-)
202
294
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
203
295
 
204
296
  Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -222,6 +314,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
222
314
  <td align="center" valign="top" width="14.28%"><a href="https://maji.kiwi"><img src="https://avatars.githubusercontent.com/u/33150916?v=4?s=100" width="100px;" alt="Maji"/><br /><sub><b>Maji</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=M4gie" title="Code">💻</a></td>
223
315
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/targos"><img src="https://avatars.githubusercontent.com/u/2352663?v=4?s=100" width="100px;" alt="Michaël Zasso"/><br /><sub><b>Michaël Zasso</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=targos" title="Code">💻</a> <a href="https://github.com/NodeSecure/js-x-ray/issues?q=author%3Atargos" title="Bug reports">🐛</a></td>
224
316
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/fabnguess"><img src="https://avatars.githubusercontent.com/u/72697416?v=4?s=100" width="100px;" alt="Kouadio Fabrice Nguessan"/><br /><sub><b>Kouadio Fabrice Nguessan</b></sub></a><br /><a href="#maintenance-fabnguess" title="Maintenance">🚧</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=fabnguess" title="Code">💻</a></td>
317
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/jean-michelet"><img src="https://avatars.githubusercontent.com/u/110341611?v=4?s=100" width="100px;" alt="Jean"/><br /><sub><b>Jean</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=jean-michelet" title="Tests">⚠️</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=jean-michelet" title="Code">💻</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=jean-michelet" title="Documentation">📖</a></td>
318
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/tchapacan"><img src="https://avatars.githubusercontent.com/u/28821702?v=4?s=100" width="100px;" alt="tchapacan"/><br /><sub><b>tchapacan</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=tchapacan" title="Code">💻</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=tchapacan" title="Tests">⚠️</a></td>
319
+ <td align="center" valign="top" width="14.28%"><a href="http://miikkak.dev"><img src="https://avatars.githubusercontent.com/u/65869801?v=4?s=100" width="100px;" alt="mkarkkainen"/><br /><sub><b>mkarkkainen</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=mkarkkainen" title="Code">💻</a></td>
320
+ </tr>
321
+ <tr>
322
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/FredGuiou"><img src="https://avatars.githubusercontent.com/u/99122562?v=4?s=100" width="100px;" alt="FredGuiou"/><br /><sub><b>FredGuiou</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=FredGuiou" title="Documentation">📖</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=FredGuiou" title="Code">💻</a></td>
323
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/madina0801"><img src="https://avatars.githubusercontent.com/u/101329759?v=4?s=100" width="100px;" alt="Madina"/><br /><sub><b>Madina</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=madina0801" title="Code">💻</a></td>
225
324
  </tr>
226
325
  </tbody>
227
326
  </table>
package/index.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import {
2
+ AstAnalyser,
3
+ SourceParser,
2
4
  runASTAnalysis,
3
5
  runASTAnalysisOnFile,
4
6
  Report,
5
7
  ReportOnFile,
6
8
  RuntimeFileOptions,
7
- RuntimeOptions
9
+ RuntimeOptions,
10
+ SourceLocation,
11
+ Dependency
8
12
  } from "./types/api.js";
9
13
  import {
10
14
  Warning,
@@ -13,19 +17,20 @@ import {
13
17
  WarningName,
14
18
  WarningNameWithValue
15
19
  } from "./types/warnings.js";
16
- import { ASTDeps, Dependency } from "./types/astdeps.js";
17
20
 
18
21
  declare const warnings: Record<WarningName, Pick<WarningDefault, "experimental" | "i18n" | "severity">>;
19
22
 
20
23
  export {
21
24
  warnings,
25
+ AstAnalyser,
26
+ SourceParser,
22
27
  runASTAnalysis,
23
28
  runASTAnalysisOnFile,
24
29
  Report,
25
30
  ReportOnFile,
26
31
  RuntimeFileOptions,
27
32
  RuntimeOptions,
28
- ASTDeps,
33
+ SourceLocation,
29
34
  Dependency,
30
35
  Warning,
31
36
  WarningDefault,
package/index.js CHANGED
@@ -1,148 +1,51 @@
1
- // Import Node.js Dependencies
2
- import fs from "fs/promises";
3
- import path from "path";
4
-
5
- // Import Third-party Dependencies
6
- import { walk } from "estree-walker";
7
- import * as meriyah from "meriyah";
8
- import isMinified from "is-minified-code";
9
-
10
1
  // Import Internal Dependencies
11
- import Analysis from "./src/Analysis.js";
12
2
  import { warnings } from "./src/warnings.js";
13
- import * as utils from "./src/utils.js";
14
-
15
- // CONSTANTS
16
- const kMeriyahDefaultOptions = {
17
- next: true,
18
- loc: true,
19
- raw: true,
20
- jsx: true
21
- };
3
+ import { JsSourceParser } from "./src/JsSourceParser.js";
4
+ import { AstAnalyser } from "./src/AstAnalyser.js";
22
5
 
23
- export function runASTAnalysis(str, options = Object.create(null)) {
6
+ function runASTAnalysis(
7
+ str,
8
+ options = Object.create(null)
9
+ ) {
24
10
  const {
25
- module = true,
26
- isMinified = false,
27
- removeHTMLComments = false
11
+ customParser = new JsSourceParser(),
12
+ customProbes = [],
13
+ skipDefaultProbes = false,
14
+ ...opts
28
15
  } = options;
29
16
 
30
- // Note: if the file start with a shebang then we remove it because 'parseScript' may fail to parse it.
31
- // Example: #!/usr/bin/env node
32
- const strToAnalyze = str.charAt(0) === "#" ? str.slice(str.indexOf("\n")) : str;
33
- const body = parseScriptExtended(strToAnalyze, {
34
- isEcmaScriptModule: Boolean(module),
35
- removeHTMLComments
17
+ const analyser = new AstAnalyser({
18
+ customParser,
19
+ customProbes,
20
+ skipDefaultProbes
36
21
  });
37
22
 
38
- const sastAnalysis = new Analysis();
39
- sastAnalysis.analyzeSourceString(str);
40
-
41
- // we walk each AST Nodes, this is a purely synchronous I/O
42
- walk(body, {
43
- enter(node) {
44
- // Skip the root of the AST.
45
- if (Array.isArray(node)) {
46
- return;
47
- }
48
-
49
- const action = sastAnalysis.walk(node);
50
- if (action === "skip") {
51
- this.skip();
52
- }
53
- }
54
- });
55
-
56
- const dependencies = sastAnalysis.dependencies;
57
- const { idsLengthAvg, stringScore, warnings } = sastAnalysis.getResult(isMinified);
58
- const isOneLineRequire = body.length <= 1 && dependencies.size <= 1;
59
-
60
- return {
61
- dependencies, warnings, idsLengthAvg, stringScore, isOneLineRequire
62
- };
63
- }
64
-
65
- export async function runASTAnalysisOnFile(pathToFile, options = {}) {
66
- try {
67
- const {
68
- packageName = null,
69
- module = true,
70
- removeHTMLComments = false
71
- } = options;
72
-
73
- const str = await fs.readFile(pathToFile, "utf-8");
74
- const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
75
-
76
- const isMin = filePathString.includes(".min") || isMinified(str);
77
- const data = runASTAnalysis(str, {
78
- isMinified: isMin,
79
- module: path.extname(filePathString) === ".mjs" ? true : module,
80
- removeHTMLComments
81
- });
82
- if (packageName !== null) {
83
- data.dependencies.removeByName(packageName);
84
- }
85
-
86
- return {
87
- ok: true,
88
- dependencies: data.dependencies,
89
- warnings: data.warnings,
90
- isMinified: !data.isOneLineRequire && isMin
91
- };
92
- }
93
- catch (error) {
94
- return {
95
- ok: false,
96
- warnings: [
97
- { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
98
- ]
99
- };
100
- }
23
+ return analyser.analyse(str, opts);
101
24
  }
102
25
 
103
- function parseScriptExtended(strToAnalyze, options = {}) {
104
- const { isEcmaScriptModule, removeHTMLComments } = options;
105
-
106
- /**
107
- * @see https://github.com/NodeSecure/js-x-ray/issues/109
108
- */
109
- const cleanedStrToAnalyze = removeHTMLComments ?
110
- utils.removeHTMLComment(strToAnalyze) : strToAnalyze;
111
-
112
- try {
113
- const { body } = meriyah.parseScript(
114
- cleanedStrToAnalyze,
115
- {
116
- ...kMeriyahDefaultOptions,
117
- module: isEcmaScriptModule,
118
- globalReturn: !isEcmaScriptModule
119
- }
120
- );
121
-
122
- return body;
123
- }
124
- catch (error) {
125
- const isIllegalReturn = error.description.includes("Illegal return statement");
126
-
127
- if (error.name === "SyntaxError" && (
128
- error.description.includes("The import keyword") ||
129
- error.description.includes("The export keyword") ||
130
- isIllegalReturn
131
- )) {
132
- const { body } = meriyah.parseScript(
133
- cleanedStrToAnalyze,
134
- {
135
- ...kMeriyahDefaultOptions,
136
- module: true,
137
- globalReturn: isIllegalReturn
138
- }
139
- );
26
+ async function runASTAnalysisOnFile(
27
+ pathToFile,
28
+ options = {}
29
+ ) {
30
+ const {
31
+ customParser = new JsSourceParser(),
32
+ customProbes = [],
33
+ skipDefaultProbes = false,
34
+ ...opts
35
+ } = options;
140
36
 
141
- return body;
142
- }
37
+ const analyser = new AstAnalyser({
38
+ customParser,
39
+ customProbes,
40
+ skipDefaultProbes
41
+ });
143
42
 
144
- throw error;
145
- }
43
+ return analyser.analyseFile(pathToFile, opts);
146
44
  }
147
45
 
148
- export { warnings };
46
+ export {
47
+ warnings,
48
+ AstAnalyser,
49
+ runASTAnalysis,
50
+ runASTAnalysisOnFile
51
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodesecure/js-x-ray",
3
- "version": "6.2.1",
3
+ "version": "7.0.0",
4
4
  "description": "JavaScript AST XRay analysis",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
@@ -18,6 +18,11 @@
18
18
  "type": "git",
19
19
  "url": "git+https://github.com/NodeSecure/js-x-ray.git"
20
20
  },
21
+ "workspaces": [
22
+ "workspaces/estree-ast-utils",
23
+ "workspaces/sec-literal",
24
+ "workspaces/ts-source-parser"
25
+ ],
21
26
  "keywords": [
22
27
  "ast",
23
28
  "nsecure",
@@ -42,16 +47,20 @@
42
47
  "@nodesecure/estree-ast-utils": "^1.3.1",
43
48
  "@nodesecure/sec-literal": "^1.2.0",
44
49
  "estree-walker": "^3.0.1",
50
+ "frequency-set": "^1.0.2",
45
51
  "is-minified-code": "^2.0.0",
46
52
  "meriyah": "^4.3.3",
47
- "safe-regex": "^2.1.1"
53
+ "safe-regex": "^2.1.1",
54
+ "ts-pattern": "^5.0.6"
48
55
  },
49
56
  "devDependencies": {
50
57
  "@nodesecure/eslint-config": "^1.6.0",
51
58
  "@types/node": "^20.6.2",
52
- "c8": "^8.0.1",
59
+ "c8": "^9.0.0",
60
+ "cross-env": "^7.0.3",
53
61
  "eslint": "^8.31.0",
54
62
  "glob": "^10.3.4",
63
+ "iterator-matcher": "^2.1.0",
55
64
  "pkg-ok": "^3.0.0"
56
65
  }
57
66
  }
@@ -0,0 +1,137 @@
1
+ // Import Node.js Dependencies
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ // Import Third-party Dependencies
6
+ import { walk } from "estree-walker";
7
+ import isMinified from "is-minified-code";
8
+
9
+ // Import Internal Dependencies
10
+ import { SourceFile } from "./SourceFile.js";
11
+ import { isOneLineExpressionExport } from "./utils/index.js";
12
+ import { JsSourceParser } from "./JsSourceParser.js";
13
+
14
+ export class AstAnalyser {
15
+ /**
16
+ * @constructor
17
+ * @param {object} [options={}]
18
+ * @param {SourceParser} [options.customParser]
19
+ * @param {Array<object>} [options.customProbes]
20
+ * @param {boolean} [options.skipDefaultProbes=false]
21
+ */
22
+ constructor(options = {}) {
23
+ this.parser = options.customParser ?? new JsSourceParser();
24
+ this.probesOptions = {
25
+ customProbes: options.customProbes ?? [],
26
+ skipDefaultProbes: options.skipDefaultProbes ?? false
27
+ };
28
+ }
29
+
30
+ analyse(str, options = Object.create(null)) {
31
+ const {
32
+ isMinified = false,
33
+ module = true,
34
+ removeHTMLComments = false
35
+ } = options;
36
+
37
+ const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
38
+ isEcmaScriptModule: Boolean(module)
39
+ });
40
+
41
+ const source = new SourceFile(str, this.probesOptions);
42
+
43
+ // we walk each AST Nodes, this is a purely synchronous I/O
44
+ walk(body, {
45
+ enter(node) {
46
+ // Skip the root of the AST.
47
+ if (Array.isArray(node)) {
48
+ return;
49
+ }
50
+
51
+ const action = source.walk(node);
52
+ if (action === "skip") {
53
+ this.skip();
54
+ }
55
+ }
56
+ });
57
+
58
+ return {
59
+ ...source.getResult(isMinified),
60
+ dependencies: source.dependencies,
61
+ isOneLineRequire: isOneLineExpressionExport(body)
62
+ };
63
+ }
64
+
65
+ async analyseFile(
66
+ pathToFile,
67
+ options = {}
68
+ ) {
69
+ try {
70
+ const {
71
+ packageName = null,
72
+ module = true,
73
+ removeHTMLComments = false
74
+ } = options;
75
+
76
+ const str = await fs.readFile(pathToFile, "utf-8");
77
+ const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
78
+
79
+ const isMin = filePathString.includes(".min") || isMinified(str);
80
+ const data = this.analyse(str, {
81
+ isMinified: isMin,
82
+ module: path.extname(filePathString) === ".mjs" ? true : module,
83
+ removeHTMLComments
84
+ });
85
+
86
+ if (packageName !== null) {
87
+ data.dependencies.delete(packageName);
88
+ }
89
+
90
+ return {
91
+ ok: true,
92
+ dependencies: data.dependencies,
93
+ warnings: data.warnings,
94
+ isMinified: !data.isOneLineRequire && isMin
95
+ };
96
+ }
97
+ catch (error) {
98
+ return {
99
+ ok: false,
100
+ warnings: [
101
+ { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
102
+ ]
103
+ };
104
+ }
105
+ }
106
+
107
+ /**
108
+ * @param {!string} source
109
+ * @param {object} options
110
+ * @param {boolean} [options.removeHTMLComments=false]
111
+ */
112
+ prepareSource(source, options = {}) {
113
+ if (typeof source !== "string") {
114
+ throw new TypeError("source must be a string");
115
+ }
116
+ const { removeHTMLComments = false } = options;
117
+
118
+ /**
119
+ * if the file start with a shebang then we remove it because meriyah.parseScript fail to parse it.
120
+ * @example
121
+ * #!/usr/bin/env node
122
+ */
123
+ const rawNoShebang = source.startsWith("#") ?
124
+ source.slice(source.indexOf("\n") + 1) : source;
125
+
126
+ return removeHTMLComments ?
127
+ this.#removeHTMLComment(rawNoShebang) : rawNoShebang;
128
+ }
129
+
130
+ /**
131
+ * @param {!string} str
132
+ * @returns {string}
133
+ */
134
+ #removeHTMLComment(str) {
135
+ return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
136
+ }
137
+ }