@nodesecure/js-x-ray 7.1.1 → 7.2.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/README.md +4 -0
- package/package.json +1 -1
- package/src/AstAnalyser.js +160 -137
- package/src/EntryFilesAnalyser.js +112 -99
- package/types/api.d.ts +2 -0
package/README.md
CHANGED
|
@@ -223,6 +223,8 @@ interface RuntimeOptions {
|
|
|
223
223
|
module?: boolean;
|
|
224
224
|
removeHTMLComments?: boolean;
|
|
225
225
|
isMinified?: boolean;
|
|
226
|
+
initialize?: (sourceFile: SourceFile) => void;
|
|
227
|
+
finalize?: (sourceFile: SourceFile) => void;
|
|
226
228
|
}
|
|
227
229
|
```
|
|
228
230
|
|
|
@@ -256,6 +258,8 @@ interface RuntimeFileOptions {
|
|
|
256
258
|
module?: boolean;
|
|
257
259
|
removeHTMLComments?: boolean;
|
|
258
260
|
packageName?: string;
|
|
261
|
+
initialize?: (sourceFile: SourceFile) => void;
|
|
262
|
+
finalize?: (sourceFile: SourceFile) => void;
|
|
259
263
|
}
|
|
260
264
|
```
|
|
261
265
|
|
package/package.json
CHANGED
package/src/AstAnalyser.js
CHANGED
|
@@ -1,137 +1,160 @@
|
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* @param {!string}
|
|
132
|
-
* @
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
initialize,
|
|
36
|
+
finalize
|
|
37
|
+
} = options;
|
|
38
|
+
|
|
39
|
+
const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
|
|
40
|
+
isEcmaScriptModule: Boolean(module)
|
|
41
|
+
});
|
|
42
|
+
const source = new SourceFile(str, this.probesOptions);
|
|
43
|
+
|
|
44
|
+
// TODO: this check should be factorized in a way that we reuse it
|
|
45
|
+
// on analyze and anlyseFile
|
|
46
|
+
if (initialize) {
|
|
47
|
+
if (typeof initialize !== "function") {
|
|
48
|
+
throw new TypeError("options.initialize must be a function");
|
|
49
|
+
}
|
|
50
|
+
initialize(source);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// we walk each AST Nodes, this is a purely synchronous I/O
|
|
54
|
+
walk(body, {
|
|
55
|
+
enter(node) {
|
|
56
|
+
// Skip the root of the AST.
|
|
57
|
+
if (Array.isArray(node)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const action = source.walk(node);
|
|
62
|
+
if (action === "skip") {
|
|
63
|
+
this.skip();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// TODO: this check should be factorized in a way that we reuse it
|
|
69
|
+
// on analyze and anlyseFile
|
|
70
|
+
if (finalize) {
|
|
71
|
+
if (typeof finalize !== "function") {
|
|
72
|
+
throw new TypeError("options.finalize must be a function");
|
|
73
|
+
}
|
|
74
|
+
finalize(source);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...source.getResult(isMinified),
|
|
79
|
+
dependencies: source.dependencies,
|
|
80
|
+
isOneLineRequire: isOneLineExpressionExport(body)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async analyseFile(
|
|
85
|
+
pathToFile,
|
|
86
|
+
options = {}
|
|
87
|
+
) {
|
|
88
|
+
try {
|
|
89
|
+
const {
|
|
90
|
+
packageName = null,
|
|
91
|
+
module = true,
|
|
92
|
+
removeHTMLComments = false,
|
|
93
|
+
initialize,
|
|
94
|
+
finalize
|
|
95
|
+
} = options;
|
|
96
|
+
|
|
97
|
+
const str = await fs.readFile(pathToFile, "utf-8");
|
|
98
|
+
const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
|
|
99
|
+
|
|
100
|
+
const isMin = filePathString.includes(".min") || isMinified(str);
|
|
101
|
+
const data = this.analyse(str, {
|
|
102
|
+
isMinified: isMin,
|
|
103
|
+
module: path.extname(filePathString) === ".mjs" ? true : module,
|
|
104
|
+
removeHTMLComments,
|
|
105
|
+
initialize,
|
|
106
|
+
finalize
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (packageName !== null) {
|
|
110
|
+
data.dependencies.delete(packageName);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
dependencies: data.dependencies,
|
|
116
|
+
warnings: data.warnings,
|
|
117
|
+
isMinified: !data.isOneLineRequire && isMin
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
warnings: [
|
|
124
|
+
{ kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {!string} source
|
|
132
|
+
* @param {object} options
|
|
133
|
+
* @param {boolean} [options.removeHTMLComments=false]
|
|
134
|
+
*/
|
|
135
|
+
prepareSource(source, options = {}) {
|
|
136
|
+
if (typeof source !== "string") {
|
|
137
|
+
throw new TypeError("source must be a string");
|
|
138
|
+
}
|
|
139
|
+
const { removeHTMLComments = false } = options;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* if the file start with a shebang then we remove it because meriyah.parseScript fail to parse it.
|
|
143
|
+
* @example
|
|
144
|
+
* #!/usr/bin/env node
|
|
145
|
+
*/
|
|
146
|
+
const rawNoShebang = source.startsWith("#") ?
|
|
147
|
+
source.slice(source.indexOf("\n") + 1) : source;
|
|
148
|
+
|
|
149
|
+
return removeHTMLComments ?
|
|
150
|
+
this.#removeHTMLComment(rawNoShebang) : rawNoShebang;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {!string} str
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
#removeHTMLComment(str) {
|
|
158
|
+
return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -1,99 +1,112 @@
|
|
|
1
|
-
// Import Node.js Dependencies
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* @
|
|
15
|
-
* @param {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
// Import Internal Dependencies
|
|
7
|
+
import { AstAnalyser } from "./AstAnalyser.js";
|
|
8
|
+
|
|
9
|
+
// CONSTANTS
|
|
10
|
+
const kDefaultExtensions = ["js", "cjs", "mjs", "node"];
|
|
11
|
+
|
|
12
|
+
export class EntryFilesAnalyser {
|
|
13
|
+
/**
|
|
14
|
+
* @constructor
|
|
15
|
+
* @param {object} [options={}]
|
|
16
|
+
* @param {AstAnalyser} [options.astAnalyzer=new AstAnalyser()]
|
|
17
|
+
* @param {function} [options.loadExtensions]
|
|
18
|
+
*/
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.astAnalyzer = options.astAnalyzer ?? new AstAnalyser();
|
|
21
|
+
const rawAllowedExtensions = options.loadExtensions
|
|
22
|
+
? options.loadExtensions(kDefaultExtensions)
|
|
23
|
+
: kDefaultExtensions;
|
|
24
|
+
|
|
25
|
+
this.allowedExtensions = new Set(rawAllowedExtensions);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Asynchronously analyze a set of entry files yielding analysis reports.
|
|
30
|
+
*
|
|
31
|
+
* @param {(string | URL)[]} entryFiles
|
|
32
|
+
* @yields {Object} - Yields an object containing the analysis report for each file.
|
|
33
|
+
*/
|
|
34
|
+
async* analyse(entryFiles) {
|
|
35
|
+
this.analyzedDeps = new Set();
|
|
36
|
+
|
|
37
|
+
for (const file of entryFiles) {
|
|
38
|
+
yield* this.#analyzeFile(file);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async* #analyzeFile(file) {
|
|
43
|
+
const filePath = file instanceof URL ? fileURLToPath(file) : file;
|
|
44
|
+
const report = await this.astAnalyzer.analyseFile(file);
|
|
45
|
+
|
|
46
|
+
yield { url: filePath, ...report };
|
|
47
|
+
|
|
48
|
+
if (!report.ok) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
yield* this.#analyzeDeps(
|
|
53
|
+
report.dependencies,
|
|
54
|
+
path.dirname(filePath)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async* #analyzeDeps(deps, basePath) {
|
|
59
|
+
for (const [name] of deps) {
|
|
60
|
+
const depPath = await this.#getInternalDepPath(name, basePath);
|
|
61
|
+
|
|
62
|
+
if (depPath && !this.analyzedDeps.has(depPath)) {
|
|
63
|
+
this.analyzedDeps.add(depPath);
|
|
64
|
+
|
|
65
|
+
yield* this.#analyzeFile(depPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async #getInternalDepPath(name, basePath) {
|
|
71
|
+
const depPath = path.join(basePath, name);
|
|
72
|
+
const existingExt = path.extname(name);
|
|
73
|
+
|
|
74
|
+
if (existingExt === "") {
|
|
75
|
+
for (const ext of this.allowedExtensions) {
|
|
76
|
+
const depPathWithExt = `${depPath}.${ext}`;
|
|
77
|
+
|
|
78
|
+
const fileExist = await this.#fileExists(depPathWithExt);
|
|
79
|
+
if (fileExist) {
|
|
80
|
+
return depPathWithExt;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
if (!this.allowedExtensions.has(existingExt.slice(1))) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fileExist = await this.#fileExists(depPath);
|
|
90
|
+
if (fileExist) {
|
|
91
|
+
return depPath;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async #fileExists(path) {
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(path, fs.constants.R_OK);
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code !== "ENOENT") {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/types/api.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ interface RuntimeOptions {
|
|
|
53
53
|
* @default false
|
|
54
54
|
*/
|
|
55
55
|
isMinified?: boolean;
|
|
56
|
+
initialize?: (sourceFile: SourceFile) => void;
|
|
57
|
+
finalize?: (sourceFile: SourceFile) => void;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
interface RuntimeFileOptions extends Omit<RuntimeOptions, "isMinified"> {
|