@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.
- package/LICENSE +1 -1
- package/README.md +121 -22
- package/index.d.ts +8 -3
- package/index.js +37 -134
- package/package.json +12 -3
- 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/types/warnings.d.ts +6 -5
- 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 -33
package/LICENSE
CHANGED
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 [
|
|
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
|
|
79
|
-
|
|
73
|
+
const { warnings, dependencies } = runASTAnalysis(
|
|
74
|
+
readFileSync("./file.js", "utf-8")
|
|
75
|
+
);
|
|
80
76
|
|
|
81
|
-
console.log(
|
|
82
|
-
console.
|
|
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
|
-
>
|
|
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
|
-
##
|
|
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
|
|
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
|
-
[](#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
|
-
|
|
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",
|
|
@@ -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": "^
|
|
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
|
+
}
|