@nodesecure/js-x-ray 7.0.0 → 7.1.1

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 CHANGED
@@ -67,11 +67,13 @@ require(Buffer.from("6673", "hex").toString());
67
67
 
68
68
  Then use `js-x-ray` to run an analysis of the JavaScript code:
69
69
  ```js
70
- import { runASTAnalysis } from "@nodesecure/js-x-ray";
70
+ import { AstAnalyser } from "@nodesecure/js-x-ray";
71
71
  import { readFileSync } from "node:fs";
72
72
 
73
- const { warnings, dependencies } = runASTAnalysis(
74
- readFileSync("./file.js", "utf-8")
73
+ const scanner = new AstAnalyser();
74
+
75
+ const { warnings, dependencies } = await scanner.analyseFile(
76
+ "./file.js"
75
77
  );
76
78
 
77
79
  console.log(dependencies);
@@ -174,11 +176,16 @@ You can pass an array of probes to the `runASTAnalysis/runASTAnalysisOnFile` fun
174
176
  Here using the example probe upper:
175
177
 
176
178
  ```ts
177
- import { runASTAnalysis } from "@nodesecure/js-x-ray";
179
+ import { AstAnalyser } from "@nodesecure/js-x-ray";
178
180
 
179
181
  // add your customProbes here (see example above)
180
182
 
181
- const result = runASTAnalysis("const danger = 'danger';", { customProbes, skipDefaultProbes: true });
183
+ const scanner = new AstAnalyser({
184
+ customProbes,
185
+ skipDefaultProbes: true
186
+ });
187
+
188
+ const result = scanner.analyse("const danger = 'danger';");
182
189
 
183
190
  console.log(result);
184
191
  ```
@@ -202,6 +209,12 @@ Congrats, you have created your first custom probe! 🎉
202
209
  > 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
210
 
204
211
  ## API
212
+
213
+ - [AstAnalyser](./docs/api/AstAnalyser.md)
214
+ - [EntryFilesAnalyser](./docs/api/EntryFilesAnalyser.md)
215
+
216
+ Legacy APIs waiting to be deprecated;
217
+
205
218
  <details>
206
219
  <summary>runASTAnalysis(str: string, options?: RuntimeOptions & AstAnalyserOptions): Report</summary>
207
220
 
@@ -290,7 +303,7 @@ $ yarn add @nodesecure/estree-ast-util
290
303
  ## Contributors ✨
291
304
 
292
305
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
293
- [![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors-)
306
+ [![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#contributors-)
294
307
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
295
308
 
296
309
  Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -321,6 +334,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
321
334
  <tr>
322
335
  <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
336
  <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>
337
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/sairuss7"><img src="https://avatars.githubusercontent.com/u/87803528?v=4?s=100" width="100px;" alt="SairussDev"/><br /><sub><b>SairussDev</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=sairuss7" title="Code">💻</a></td>
324
338
  </tr>
325
339
  </tbody>
326
340
  </table>
package/index.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import {
2
2
  AstAnalyser,
3
+ AstAnalyserOptions,
4
+
5
+ EntryFilesAnalyser,
6
+ EntryFilesAnalyserOptions,
7
+
3
8
  SourceParser,
9
+ JsSourceParser,
4
10
  runASTAnalysis,
5
11
  runASTAnalysisOnFile,
6
12
  Report,
@@ -23,6 +29,10 @@ declare const warnings: Record<WarningName, Pick<WarningDefault, "experimental"
23
29
  export {
24
30
  warnings,
25
31
  AstAnalyser,
32
+ AstAnalyserOptions,
33
+ EntryFilesAnalyser,
34
+ EntryFilesAnalyserOptions,
35
+ JsSourceParser,
26
36
  SourceParser,
27
37
  runASTAnalysis,
28
38
  runASTAnalysisOnFile,
package/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { warnings } from "./src/warnings.js";
3
3
  import { JsSourceParser } from "./src/JsSourceParser.js";
4
4
  import { AstAnalyser } from "./src/AstAnalyser.js";
5
+ import { EntryFilesAnalyser } from "./src/EntryFilesAnalyser.js";
5
6
 
6
7
  function runASTAnalysis(
7
8
  str,
@@ -28,8 +29,8 @@ async function runASTAnalysisOnFile(
28
29
  options = {}
29
30
  ) {
30
31
  const {
31
- customParser = new JsSourceParser(),
32
32
  customProbes = [],
33
+ customParser = new JsSourceParser(),
33
34
  skipDefaultProbes = false,
34
35
  ...opts
35
36
  } = options;
@@ -46,6 +47,8 @@ async function runASTAnalysisOnFile(
46
47
  export {
47
48
  warnings,
48
49
  AstAnalyser,
50
+ EntryFilesAnalyser,
51
+ JsSourceParser,
49
52
  runASTAnalysis,
50
53
  runASTAnalysisOnFile
51
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodesecure/js-x-ray",
3
- "version": "7.0.0",
3
+ "version": "7.1.1",
4
4
  "description": "JavaScript AST XRay analysis",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
@@ -58,7 +58,7 @@
58
58
  "@types/node": "^20.6.2",
59
59
  "c8": "^9.0.0",
60
60
  "cross-env": "^7.0.3",
61
- "eslint": "^8.31.0",
61
+ "eslint": "^9.0.0",
62
62
  "glob": "^10.3.4",
63
63
  "iterator-matcher": "^2.1.0",
64
64
  "pkg-ok": "^3.0.0"
@@ -1,137 +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
- }
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
+ }
@@ -0,0 +1,99 @@
1
+ // Import Node.js Dependencies
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ // Import Internal Dependencies
6
+ import { AstAnalyser } from "./AstAnalyser.js";
7
+
8
+ const kDefaultExtensions = ["js", "cjs", "mjs", "node"];
9
+
10
+ export class EntryFilesAnalyser {
11
+ /**
12
+ * @constructor
13
+ * @param {object} [options={}]
14
+ * @param {AstAnalyser} [options.astAnalyzer=new AstAnalyser()]
15
+ * @param {function} [options.loadExtensions]
16
+ */
17
+ constructor(options = {}) {
18
+ this.astAnalyzer = options.astAnalyzer ?? new AstAnalyser();
19
+ this.allowedExtensions = options.loadExtensions
20
+ ? options.loadExtensions(kDefaultExtensions)
21
+ : kDefaultExtensions;
22
+ }
23
+
24
+ /**
25
+ * Asynchronously analyze a set of entry files yielding analysis reports.
26
+ *
27
+ * @param {(string | URL)[]} entryFiles
28
+ * @yields {Object} - Yields an object containing the analysis report for each file.
29
+ */
30
+ async* analyse(entryFiles) {
31
+ this.analyzedDeps = new Set();
32
+
33
+ for (const file of entryFiles) {
34
+ yield* this.#analyzeFile(file);
35
+ }
36
+ }
37
+
38
+ async* #analyzeFile(file) {
39
+ const filePath = file instanceof URL ? file.pathname : file;
40
+ const report = await this.astAnalyzer.analyseFile(file);
41
+
42
+ yield { url: filePath, ...report };
43
+
44
+ if (!report.ok) {
45
+ return;
46
+ }
47
+
48
+ yield* this.#analyzeDeps(report.dependencies, path.dirname(filePath));
49
+ }
50
+
51
+ async* #analyzeDeps(deps, basePath) {
52
+ for (const [name] of deps) {
53
+ const depPath = await this.#getInternalDepPath(name, basePath);
54
+ if (depPath && !this.analyzedDeps.has(depPath)) {
55
+ this.analyzedDeps.add(depPath);
56
+
57
+ yield* this.#analyzeFile(depPath);
58
+ }
59
+ }
60
+ }
61
+
62
+ async #getInternalDepPath(name, basePath) {
63
+ const depPath = path.join(basePath, name);
64
+ const existingExt = path.extname(name);
65
+ if (existingExt !== "") {
66
+ if (!this.allowedExtensions.includes(existingExt.slice(1))) {
67
+ return null;
68
+ }
69
+
70
+ if (await this.#fileExists(depPath)) {
71
+ return depPath;
72
+ }
73
+ }
74
+
75
+ for (const ext of this.allowedExtensions) {
76
+ const depPathWithExt = `${depPath}.${ext}`;
77
+ if (await this.#fileExists(depPathWithExt)) {
78
+ return depPathWithExt;
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ async #fileExists(path) {
86
+ try {
87
+ await fs.access(path, fs.constants.F_OK);
88
+
89
+ return true;
90
+ }
91
+ catch (error) {
92
+ if (error.code !== "ENOENT") {
93
+ throw error;
94
+ }
95
+
96
+ return false;
97
+ }
98
+ }
99
+ }
@@ -9,7 +9,7 @@
9
9
  function validateNode(node) {
10
10
  return [
11
11
  // Note: the source property is the right-side Literal part of the Import
12
- node.type === "ImportDeclaration" && node.source.type === "Literal"
12
+ ["ImportDeclaration", "ImportExpression"].includes(node.type) && node.source.type === "Literal"
13
13
  ];
14
14
  }
15
15
 
@@ -4,13 +4,19 @@ import { builtinModules } from "repl";
4
4
  // Import Third-party Dependencies
5
5
  import { Hex } from "@nodesecure/sec-literal";
6
6
 
7
+ const kMapRegexIps = Object.freeze({
8
+ regexIPv4: /^(https?:\/\/)(?!127\.)(?!.*:(?:0{1,3}|25[6-9])\.)(?!.*:(?:25[6-9])\.(?:0{1,3}|25[6-9])\.)(?!.*:(?:25[6-9])\.(?:25[6-9])\.(?:0{1,3}|25[6-9])\.)(?!.*:(?:25[6-9])\.(?:25[6-9])\.(?:25[6-9])\.(?:0{1,3}|25[6-9]))((?:\d{1,2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d{2}|2[0-4]\d|25[0-5])(?::\d{1,5})?(\/[^\s]*)?$/,
9
+ regexIPv6: /^(https?:\/\/)(\[[0-9A-Fa-f:]+\])(?::\d{1,5})?(\/[^\s]*)?$/
10
+ });
11
+
7
12
  // CONSTANTS
8
13
  const kNodeDeps = new Set(builtinModules);
9
14
  const kShadyLinkRegExps = [
15
+ kMapRegexIps.regexIPv4,
16
+ kMapRegexIps.regexIPv6,
10
17
  /(http[s]?:\/\/bit\.ly.*)$/,
11
18
  /(http[s]?:\/\/.*\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|cd|ws|icu|cam|uno|email|stream))$/
12
19
  ];
13
-
14
20
  /**
15
21
  * @description Search for Literal AST Node
16
22
  * @see https://github.com/estree/estree/blob/master/es5.md#literal
package/types/api.d.ts CHANGED
@@ -3,6 +3,12 @@ import { Statement } from "meriyah/dist/src/estree.js";
3
3
 
4
4
  export {
5
5
  AstAnalyser,
6
+ AstAnalyserOptions,
7
+
8
+ EntryFilesAnalyser,
9
+ EntryFilesAnalyserOptions,
10
+
11
+ JsSourceParser,
6
12
  SourceParser,
7
13
  runASTAnalysis,
8
14
  runASTAnalysisOnFile,
@@ -98,8 +104,24 @@ interface SourceParser {
98
104
  declare class AstAnalyser {
99
105
  constructor(options?: AstAnalyserOptions);
100
106
  analyse: (str: string, options?: RuntimeOptions) => Report;
101
- analyzeFile(pathToFile: string, options?: RuntimeFileOptions): Promise<ReportOnFile>;
107
+ analyseFile(pathToFile: string, options?: RuntimeFileOptions): Promise<ReportOnFile>;
108
+ }
109
+
110
+ interface EntryFilesAnalyserOptions {
111
+ astAnalyzer?: AstAnalyser;
112
+ loadExtensions?: (defaults: string[]) => string[];
113
+ }
114
+
115
+ declare class EntryFilesAnalyser {
116
+ constructor(options?: EntryFilesAnalyserOptions);
117
+
118
+ /**
119
+ * Asynchronously analyze a set of entry files yielding analysis reports.
120
+ */
121
+ analyse(entryFiles: (string | URL)[]): AsyncGenerator<ReportOnFile & { url: string }>;
102
122
  }
103
123
 
124
+ declare class JsSourceParser implements SourceParser {}
125
+
104
126
  declare function runASTAnalysis(str: string, options?: RuntimeOptions & AstAnalyserOptions): Report;
105
127
  declare function runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions & AstAnalyserOptions): Promise<ReportOnFile>;