@nodesecure/js-x-ray 6.3.0 → 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.
- package/LICENSE +1 -1
- package/README.md +105 -18
- package/index.d.ts +8 -3
- package/index.js +37 -134
- package/package.json +8 -8
- package/src/AstAnalyser.js +137 -0
- package/src/Deobfuscator.js +192 -0
- package/src/JsSourceParser.js +57 -0
- package/src/NodeCounter.js +76 -0
- package/src/ProbeRunner.js +140 -0
- package/src/{Analysis.js → SourceFile.js} +55 -68
- package/src/obfuscators/freejsobfuscator.js +9 -9
- package/src/obfuscators/jjencode.js +7 -7
- package/src/obfuscators/jsfuck.js +6 -6
- package/src/obfuscators/obfuscator-io.js +7 -7
- package/src/obfuscators/trojan-source.js +28 -28
- package/src/probes/isArrayExpression.js +32 -30
- package/src/probes/isBinaryExpression.js +5 -3
- package/src/probes/isImportDeclaration.js +7 -4
- package/src/probes/isLiteral.js +10 -8
- package/src/probes/isLiteralRegex.js +5 -3
- package/src/probes/isRegexObject.js +5 -3
- package/src/probes/isRequire/RequireCallExpressionWalker.js +93 -0
- package/src/probes/isRequire/isRequire.js +142 -0
- package/src/probes/isUnsafeCallee.js +15 -5
- package/src/probes/isWeakCrypto.js +5 -3
- package/src/utils/exportAssignmentHasRequireLeave.js +40 -0
- package/src/utils/extractNode.js +14 -0
- package/src/utils/index.js +8 -0
- package/src/utils/isNode.js +5 -0
- package/src/utils/isOneLineExpressionExport.js +18 -0
- package/src/utils/isUnsafeCallee.js +28 -0
- package/src/utils/notNullOrUndefined.js +3 -0
- package/src/utils/rootLocation.js +3 -0
- package/src/utils/toArrayLocation.js +11 -0
- package/src/warnings.js +1 -1
- package/types/api.d.ts +62 -18
- package/src/ASTDeps.js +0 -63
- package/src/obfuscators/index.js +0 -69
- package/src/probes/index.js +0 -70
- package/src/probes/isAssignmentExpression.js +0 -29
- package/src/probes/isClassDeclaration.js +0 -25
- package/src/probes/isFunction.js +0 -38
- package/src/probes/isMemberExpression.js +0 -16
- package/src/probes/isMethodDefinition.js +0 -25
- package/src/probes/isObjectExpression.js +0 -29
- package/src/probes/isRequire.js +0 -164
- package/src/probes/isUnaryExpression.js +0 -26
- package/src/probes/isVariableDeclaration.js +0 -30
- package/src/utils.js +0 -48
- package/types/astdeps.d.ts +0 -34
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -68,22 +68,19 @@ require(Buffer.from("6673", "hex").toString());
|
|
|
68
68
|
Then use `js-x-ray` to run an analysis of the JavaScript code:
|
|
69
69
|
```js
|
|
70
70
|
import { runASTAnalysis } from "@nodesecure/js-x-ray";
|
|
71
|
-
import { readFileSync } from "fs";
|
|
71
|
+
import { readFileSync } from "node:fs";
|
|
72
72
|
|
|
73
|
-
const
|
|
74
|
-
|
|
73
|
+
const { warnings, dependencies } = runASTAnalysis(
|
|
74
|
+
readFileSync("./file.js", "utf-8")
|
|
75
|
+
);
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
console.log(dependenciesName);
|
|
80
|
-
console.log(inTryDeps);
|
|
81
|
-
console.log(warnings);
|
|
77
|
+
console.log(dependencies);
|
|
78
|
+
console.dir(warnings, { depth: null });
|
|
82
79
|
```
|
|
83
80
|
|
|
84
81
|
The analysis will return: `http` (in try), `crypto`, `util` and `fs`.
|
|
85
82
|
|
|
86
|
-
> [!
|
|
83
|
+
> [!TIP]
|
|
87
84
|
> There is also a lot of suspicious code example in the `./examples` cases directory. Feel free to try the tool on these files.
|
|
88
85
|
|
|
89
86
|
## Warnings
|
|
@@ -137,16 +134,90 @@ This section describe all the possible warnings returned by JSXRay. Click on the
|
|
|
137
134
|
| [weak-crypto](./docs/weak-crypto.md) | ✔️ | The code probably contains a weak crypto algorithm (md5, sha1...) |
|
|
138
135
|
| [shady-link](./docs/shady-link.md) | ✔️ | The code contains shady/unsafe link |
|
|
139
136
|
|
|
140
|
-
##
|
|
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
|
+
|
|
141
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
|
+
```
|
|
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
|
|
142
205
|
<details>
|
|
143
|
-
<summary>runASTAnalysis(str: string, options?: RuntimeOptions): Report</summary>
|
|
206
|
+
<summary>runASTAnalysis(str: string, options?: RuntimeOptions & AstAnalyserOptions): Report</summary>
|
|
144
207
|
|
|
145
208
|
```ts
|
|
146
209
|
interface RuntimeOptions {
|
|
147
210
|
module?: boolean;
|
|
148
|
-
isMinified?: boolean;
|
|
149
211
|
removeHTMLComments?: boolean;
|
|
212
|
+
isMinified?: boolean;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
interface AstAnalyserOptions {
|
|
218
|
+
customParser?: SourceParser;
|
|
219
|
+
customProbes?: Probe[];
|
|
220
|
+
skipDefaultProbes?: boolean;
|
|
150
221
|
}
|
|
151
222
|
```
|
|
152
223
|
|
|
@@ -165,13 +236,21 @@ interface Report {
|
|
|
165
236
|
</details>
|
|
166
237
|
|
|
167
238
|
<details>
|
|
168
|
-
<summary>runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions): Promise< ReportOnFile ></summary>
|
|
239
|
+
<summary>runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions & AstAnalyserOptions): Promise< ReportOnFile ></summary>
|
|
169
240
|
|
|
170
241
|
```ts
|
|
171
|
-
interface
|
|
242
|
+
interface RuntimeFileOptions {
|
|
172
243
|
module?: boolean;
|
|
173
|
-
isMinified?: boolean;
|
|
174
244
|
removeHTMLComments?: boolean;
|
|
245
|
+
packageName?: string;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
interface AstAnalyserOptions {
|
|
251
|
+
customParser?: SourceParser;
|
|
252
|
+
customProbes?: Probe[];
|
|
253
|
+
skipDefaultProbes?: boolean;
|
|
175
254
|
}
|
|
176
255
|
```
|
|
177
256
|
|
|
@@ -197,8 +276,9 @@ Click on one of the links to access the documentation of the workspace:
|
|
|
197
276
|
|
|
198
277
|
| name | package and link |
|
|
199
278
|
| --- | --- |
|
|
200
|
-
| estree-ast-
|
|
279
|
+
| estree-ast-utils | [@nodesecure/estree-ast-utils](./workspaces/estree-ast-utils) |
|
|
201
280
|
| sec-literal | [@nodesecure/sec-literal ](./workspaces/sec-literal) |
|
|
281
|
+
| ts-source-parser | [@nodesecure/ts-source-parser ](./workspaces/ts-source-parser) |
|
|
202
282
|
|
|
203
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).
|
|
204
284
|
```bash
|
|
@@ -210,7 +290,7 @@ $ yarn add @nodesecure/estree-ast-util
|
|
|
210
290
|
## Contributors ✨
|
|
211
291
|
|
|
212
292
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
213
|
-
[](#contributors-)
|
|
214
294
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
215
295
|
|
|
216
296
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
@@ -234,6 +314,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
234
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>
|
|
235
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>
|
|
236
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>
|
|
237
324
|
</tr>
|
|
238
325
|
</tbody>
|
|
239
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
|
-
|
|
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
|
|
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
|
-
|
|
6
|
+
function runASTAnalysis(
|
|
7
|
+
str,
|
|
8
|
+
options = Object.create(null)
|
|
9
|
+
) {
|
|
24
10
|
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
customParser = new JsSourceParser(),
|
|
12
|
+
customProbes = [],
|
|
13
|
+
skipDefaultProbes = false,
|
|
14
|
+
...opts
|
|
28
15
|
} = options;
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
isEcmaScriptModule: Boolean(module),
|
|
35
|
-
removeHTMLComments
|
|
17
|
+
const analyser = new AstAnalyser({
|
|
18
|
+
customParser,
|
|
19
|
+
customProbes,
|
|
20
|
+
skipDefaultProbes
|
|
36
21
|
});
|
|
37
22
|
|
|
38
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
142
|
-
|
|
37
|
+
const analyser = new AstAnalyser({
|
|
38
|
+
customParser,
|
|
39
|
+
customProbes,
|
|
40
|
+
skipDefaultProbes
|
|
41
|
+
});
|
|
143
42
|
|
|
144
|
-
|
|
145
|
-
}
|
|
43
|
+
return analyser.analyseFile(pathToFile, opts);
|
|
146
44
|
}
|
|
147
45
|
|
|
148
|
-
export {
|
|
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": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "JavaScript AST XRay analysis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./index.js",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"workspaces": [
|
|
22
22
|
"workspaces/estree-ast-utils",
|
|
23
|
-
"workspaces/sec-literal"
|
|
23
|
+
"workspaces/sec-literal",
|
|
24
|
+
"workspaces/ts-source-parser"
|
|
24
25
|
],
|
|
25
26
|
"keywords": [
|
|
26
27
|
"ast",
|
|
@@ -46,21 +47,20 @@
|
|
|
46
47
|
"@nodesecure/estree-ast-utils": "^1.3.1",
|
|
47
48
|
"@nodesecure/sec-literal": "^1.2.0",
|
|
48
49
|
"estree-walker": "^3.0.1",
|
|
50
|
+
"frequency-set": "^1.0.2",
|
|
49
51
|
"is-minified-code": "^2.0.0",
|
|
50
52
|
"meriyah": "^4.3.3",
|
|
51
|
-
"safe-regex": "^2.1.1"
|
|
53
|
+
"safe-regex": "^2.1.1",
|
|
54
|
+
"ts-pattern": "^5.0.6"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"@nodesecure/eslint-config": "^1.6.0",
|
|
55
|
-
"@small-tech/esm-tape-runner": "^2.0.0",
|
|
56
|
-
"@small-tech/tap-monkey": "^1.4.0",
|
|
57
58
|
"@types/node": "^20.6.2",
|
|
58
|
-
"c8": "^
|
|
59
|
+
"c8": "^9.0.0",
|
|
59
60
|
"cross-env": "^7.0.3",
|
|
60
61
|
"eslint": "^8.31.0",
|
|
61
62
|
"glob": "^10.3.4",
|
|
62
63
|
"iterator-matcher": "^2.1.0",
|
|
63
|
-
"pkg-ok": "^3.0.0"
|
|
64
|
-
"tape": "^5.7.2"
|
|
64
|
+
"pkg-ok": "^3.0.0"
|
|
65
65
|
}
|
|
66
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
|
+
}
|