@nodesecure/js-x-ray 4.2.1 → 4.3.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 (3) hide show
  1. package/index.js +98 -93
  2. package/package.json +8 -8
  3. package/src/Analysis.js +152 -152
package/index.js CHANGED
@@ -1,93 +1,98 @@
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
- // Import Internal Dependencies
11
- import Analysis from "./src/Analysis.js";
12
-
13
- export function runASTAnalysis(str, options = Object.create(null)) {
14
- const { module = true, isMinified = false } = options;
15
-
16
- // Note: if the file start with a shebang then we remove it because 'parseScript' may fail to parse it.
17
- // Example: #!/usr/bin/env node
18
- const strToAnalyze = str.charAt(0) === "#" ? str.slice(str.indexOf("\n")) : str;
19
- const { body } = meriyah.parseScript(strToAnalyze, {
20
- next: true, loc: true, raw: true, module: Boolean(module)
21
- });
22
-
23
- const sastAnalysis = new Analysis();
24
- sastAnalysis.analyzeSourceString(str);
25
-
26
- // we walk each AST Nodes, this is a purely synchronous I/O
27
- walk(body, {
28
- enter(node) {
29
- // Skip the root of the AST.
30
- if (Array.isArray(node)) {
31
- return;
32
- }
33
-
34
- const action = sastAnalysis.walk(node);
35
- if (action === "skip") {
36
- this.skip();
37
- }
38
- }
39
- });
40
-
41
- const dependencies = sastAnalysis.dependencies;
42
- const { idsLengthAvg, stringScore, warnings } = sastAnalysis.getResult(isMinified);
43
- const isOneLineRequire = body.length <= 1 && dependencies.size <= 1;
44
-
45
- return {
46
- dependencies, warnings, idsLengthAvg, stringScore, isOneLineRequire
47
- };
48
- }
49
-
50
- export async function runASTAnalysisOnFile(pathToFile, options = {}) {
51
- try {
52
- const { packageName = null, module = true } = options;
53
- const str = await fs.readFile(pathToFile, "utf-8");
54
-
55
- const isMin = pathToFile.includes(".min") || isMinified(str);
56
- const data = runASTAnalysis(str, {
57
- isMinified: isMin,
58
- module: path.extname(pathToFile) === ".mjs" ? true : module
59
- });
60
- if (packageName !== null) {
61
- data.dependencies.removeByName(packageName);
62
- }
63
-
64
- return {
65
- ok: true,
66
- dependencies: data.dependencies,
67
- warnings: data.warnings,
68
- isMinified: !data.isOneLineRequire && isMin
69
- };
70
- }
71
- catch (error) {
72
- return {
73
- ok: false,
74
- warnings: [
75
- { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
76
- ]
77
- };
78
- }
79
- }
80
-
81
- export const CONSTANTS = {
82
- Warnings: Object.freeze({
83
- parsingError: "ast-error",
84
- unsafeImport: "unsafe-import",
85
- unsafeRegex: "unsafe-regex",
86
- unsafeStmt: "unsafe-stmt",
87
- unsafeAssign: "unsafe-assign",
88
- encodedLiteral: "encoded-literal",
89
- shortIdentifiers: "short-identifiers",
90
- suspiciousLiteral: "suspicious-literal",
91
- obfuscatedCode: "obfuscated-code"
92
- })
93
- };
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
+ // Import Internal Dependencies
11
+ import Analysis from "./src/Analysis.js";
12
+
13
+ export function runASTAnalysis(str, options = Object.create(null)) {
14
+ const { module = true, isMinified = false } = options;
15
+
16
+ // Note: if the file start with a shebang then we remove it because 'parseScript' may fail to parse it.
17
+ // Example: #!/usr/bin/env node
18
+ const strToAnalyze = str.charAt(0) === "#" ? str.slice(str.indexOf("\n")) : str;
19
+ const isEcmaScriptModule = Boolean(module);
20
+ const { body } = meriyah.parseScript(strToAnalyze, {
21
+ next: true,
22
+ loc: true,
23
+ raw: true,
24
+ module: isEcmaScriptModule,
25
+ globalReturn: !isEcmaScriptModule
26
+ });
27
+
28
+ const sastAnalysis = new Analysis();
29
+ sastAnalysis.analyzeSourceString(str);
30
+
31
+ // we walk each AST Nodes, this is a purely synchronous I/O
32
+ walk(body, {
33
+ enter(node) {
34
+ // Skip the root of the AST.
35
+ if (Array.isArray(node)) {
36
+ return;
37
+ }
38
+
39
+ const action = sastAnalysis.walk(node);
40
+ if (action === "skip") {
41
+ this.skip();
42
+ }
43
+ }
44
+ });
45
+
46
+ const dependencies = sastAnalysis.dependencies;
47
+ const { idsLengthAvg, stringScore, warnings } = sastAnalysis.getResult(isMinified);
48
+ const isOneLineRequire = body.length <= 1 && dependencies.size <= 1;
49
+
50
+ return {
51
+ dependencies, warnings, idsLengthAvg, stringScore, isOneLineRequire
52
+ };
53
+ }
54
+
55
+ export async function runASTAnalysisOnFile(pathToFile, options = {}) {
56
+ try {
57
+ const { packageName = null, module = true } = options;
58
+ const str = await fs.readFile(pathToFile, "utf-8");
59
+
60
+ const isMin = pathToFile.includes(".min") || isMinified(str);
61
+ const data = runASTAnalysis(str, {
62
+ isMinified: isMin,
63
+ module: path.extname(pathToFile) === ".mjs" ? true : module
64
+ });
65
+ if (packageName !== null) {
66
+ data.dependencies.removeByName(packageName);
67
+ }
68
+
69
+ return {
70
+ ok: true,
71
+ dependencies: data.dependencies,
72
+ warnings: data.warnings,
73
+ isMinified: !data.isOneLineRequire && isMin
74
+ };
75
+ }
76
+ catch (error) {
77
+ return {
78
+ ok: false,
79
+ warnings: [
80
+ { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
81
+ ]
82
+ };
83
+ }
84
+ }
85
+
86
+ export const CONSTANTS = {
87
+ Warnings: Object.freeze({
88
+ parsingError: "ast-error",
89
+ unsafeImport: "unsafe-import",
90
+ unsafeRegex: "unsafe-regex",
91
+ unsafeStmt: "unsafe-stmt",
92
+ unsafeAssign: "unsafe-assign",
93
+ encodedLiteral: "encoded-literal",
94
+ shortIdentifiers: "short-identifiers",
95
+ suspiciousLiteral: "suspicious-literal",
96
+ obfuscatedCode: "obfuscated-code"
97
+ })
98
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodesecure/js-x-ray",
3
- "version": "4.2.1",
3
+ "version": "4.3.0",
4
4
  "description": "JavaScript AST XRay analysis",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
@@ -37,21 +37,21 @@
37
37
  },
38
38
  "homepage": "https://github.com/NodeSecure/js-x-ray#readme",
39
39
  "dependencies": {
40
- "@nodesecure/sec-literal": "^1.0.1",
40
+ "@nodesecure/sec-literal": "^1.1.0",
41
41
  "estree-walker": "^3.0.1",
42
42
  "is-minified-code": "^2.0.0",
43
- "meriyah": "^4.2.0",
43
+ "meriyah": "^4.2.1",
44
44
  "safe-regex": "^2.1.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@nodesecure/eslint-config": "^1.3.1",
48
48
  "@slimio/is": "^1.5.1",
49
- "@small-tech/esm-tape-runner": "^1.0.3",
49
+ "@small-tech/esm-tape-runner": "^2.0.0",
50
50
  "@small-tech/tap-monkey": "^1.3.0",
51
- "@types/node": "^17.0.15",
51
+ "@types/node": "^17.0.23",
52
52
  "cross-env": "^7.0.3",
53
- "eslint": "^8.8.0",
54
- "pkg-ok": "^2.3.1",
55
- "tape": "^5.5.0"
53
+ "eslint": "^8.12.0",
54
+ "pkg-ok": "^3.0.0",
55
+ "tape": "^5.5.2"
56
56
  }
57
57
  }
package/src/Analysis.js CHANGED
@@ -1,152 +1,152 @@
1
- // Import Third-party Dependencies
2
- import { Utils, Literal } from "@nodesecure/sec-literal";
3
-
4
- // Import Internal Dependencies
5
- import { rootLocation, toArrayLocation, generateWarning } from "./utils.js";
6
- import { warnings as _warnings, processMainModuleRequire } from "./constants.js";
7
- import ASTDeps from "./ASTDeps.js";
8
- import { isObfuscatedCode, hasTrojanSource } from "./obfuscators/index.js";
9
- import { runOnProbes } from "./probes/index.js";
10
-
11
- // CONSTANTS
12
- const kDictionaryStrParts = [
13
- "abcdefghijklmnopqrstuvwxyz",
14
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
15
- "0123456789"
16
- ];
17
-
18
- const kWarningsNameStr = Object.freeze({
19
- [_warnings.parsingError]: "parsing-error",
20
- [_warnings.unsafeImport]: "unsafe-import",
21
- [_warnings.unsafeRegex]: "unsafe-regex",
22
- [_warnings.unsafeStmt]: "unsafe-stmt",
23
- [_warnings.unsafeAssign]: "unsafe-assign",
24
- [_warnings.encodedLiteral]: "encoded-literal",
25
- [_warnings.shortIdentifiers]: "short-identifiers",
26
- [_warnings.suspiciousLiteral]: "suspicious-literal",
27
- [_warnings.obfuscatedCode]: "obfuscated-code"
28
- });
29
-
30
- export default class Analysis {
31
- hasDictionaryString = false;
32
- hasPrefixedIdentifiers = false;
33
- varkinds = { var: 0, let: 0, const: 0 };
34
- idtypes = { assignExpr: 0, property: 0, variableDeclarator: 0, functionDeclaration: 0 };
35
- counter = {
36
- identifiers: 0,
37
- doubleUnaryArray: 0,
38
- computedMemberExpr: 0,
39
- memberExpr: 0,
40
- deepBinaryExpr: 0,
41
- encodedArrayValue: 0,
42
- morseLiteral: 0
43
- };
44
- identifiersName = [];
45
-
46
- constructor() {
47
- this.dependencies = new ASTDeps();
48
-
49
- this.identifiers = new Map();
50
- this.globalParts = new Map();
51
- this.handledEncodedLiteralValues = new Map();
52
-
53
- this.requireIdentifiers = new Set(["require", processMainModuleRequire]);
54
- this.warnings = [];
55
- this.literalScores = [];
56
- }
57
-
58
- addWarning(symbol, value, location = rootLocation()) {
59
- if (symbol === _warnings.encodedLiteral && this.handledEncodedLiteralValues.has(value)) {
60
- const index = this.handledEncodedLiteralValues.get(value);
61
- this.warnings[index].location.push(toArrayLocation(location));
62
-
63
- return;
64
- }
65
- const warningName = kWarningsNameStr[symbol];
66
- this.warnings.push(generateWarning(warningName, { value, location }));
67
- if (symbol === _warnings.encodedLiteral) {
68
- this.handledEncodedLiteralValues.set(value, this.warnings.length - 1);
69
- }
70
- }
71
-
72
- analyzeSourceString(sourceString) {
73
- if (hasTrojanSource(sourceString)) {
74
- this.addWarning(_warnings.obfuscatedCode, "trojan-source");
75
- }
76
- }
77
-
78
- analyzeString(str) {
79
- const score = Utils.stringSuspicionScore(str);
80
- if (score !== 0) {
81
- this.literalScores.push(score);
82
- }
83
-
84
- if (!this.hasDictionaryString) {
85
- const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word));
86
- if (isDictionaryStr) {
87
- this.hasDictionaryString = true;
88
- }
89
- }
90
-
91
- // Searching for morse string like "--.- --.--."
92
- if (Utils.stringCharDiversity(str, ["\n"]) >= 3 && Utils.isMorse(str)) {
93
- this.counter.morseLiteral++;
94
- }
95
- }
96
-
97
- analyzeLiteral(node, inArrayExpr = false) {
98
- if (typeof node.value !== "string" || Utils.isSvg(node)) {
99
- return;
100
- }
101
- this.analyzeString(node.value);
102
-
103
- const { hasHexadecimalSequence, hasUnicodeSequence, isBase64 } = Literal.defaultAnalysis(node);
104
- if ((hasHexadecimalSequence || hasUnicodeSequence) && isBase64) {
105
- if (inArrayExpr) {
106
- this.counter.encodedArrayValue++;
107
- }
108
- else {
109
- this.addWarning(_warnings.encodedLiteral, node.value, node.loc);
110
- }
111
- }
112
- }
113
-
114
- getResult(isMinified) {
115
- this.counter.identifiers = this.identifiersName.length;
116
- const [isObfuscated, kind] = isObfuscatedCode(this);
117
- if (isObfuscated) {
118
- this.addWarning(_warnings.obfuscatedCode, kind || "unknown");
119
- }
120
-
121
- const identifiersLengthArr = this.identifiersName
122
- .filter((value) => value.type !== "property" && typeof value.name === "string").map((value) => value.name.length);
123
-
124
- const [idsLengthAvg, stringScore] = [sum(identifiersLengthArr), sum(this.literalScores)];
125
- if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) {
126
- this.addWarning(_warnings.shortIdentifiers, idsLengthAvg);
127
- }
128
- if (stringScore >= 3) {
129
- this.addWarning(_warnings.suspiciousLiteral, stringScore);
130
- }
131
-
132
- return { idsLengthAvg, stringScore, warnings: this.warnings };
133
- }
134
-
135
- walk(node) {
136
- // Detect TryStatement and CatchClause to known which dependency is required in a Try {} clause
137
- if (node.type === "TryStatement" && typeof node.handler !== "undefined") {
138
- this.dependencies.isInTryStmt = true;
139
- }
140
- else if (node.type === "CatchClause") {
141
- this.dependencies.isInTryStmt = false;
142
- }
143
-
144
- return runOnProbes(node, this);
145
- }
146
- }
147
-
148
- function sum(arr = []) {
149
- return arr.length === 0 ? 0 : (arr.reduce((prev, curr) => prev + curr, 0) / arr.length);
150
- }
151
-
152
- Analysis.Warnings = _warnings;
1
+ // Import Third-party Dependencies
2
+ import { Utils, Literal } from "@nodesecure/sec-literal";
3
+
4
+ // Import Internal Dependencies
5
+ import { rootLocation, toArrayLocation, generateWarning } from "./utils.js";
6
+ import { warnings as _warnings, processMainModuleRequire } from "./constants.js";
7
+ import ASTDeps from "./ASTDeps.js";
8
+ import { isObfuscatedCode, hasTrojanSource } from "./obfuscators/index.js";
9
+ import { runOnProbes } from "./probes/index.js";
10
+
11
+ // CONSTANTS
12
+ const kDictionaryStrParts = [
13
+ "abcdefghijklmnopqrstuvwxyz",
14
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
15
+ "0123456789"
16
+ ];
17
+
18
+ const kWarningsNameStr = Object.freeze({
19
+ [_warnings.parsingError]: "parsing-error",
20
+ [_warnings.unsafeImport]: "unsafe-import",
21
+ [_warnings.unsafeRegex]: "unsafe-regex",
22
+ [_warnings.unsafeStmt]: "unsafe-stmt",
23
+ [_warnings.unsafeAssign]: "unsafe-assign",
24
+ [_warnings.encodedLiteral]: "encoded-literal",
25
+ [_warnings.shortIdentifiers]: "short-identifiers",
26
+ [_warnings.suspiciousLiteral]: "suspicious-literal",
27
+ [_warnings.obfuscatedCode]: "obfuscated-code"
28
+ });
29
+
30
+ export default class Analysis {
31
+ hasDictionaryString = false;
32
+ hasPrefixedIdentifiers = false;
33
+ varkinds = { var: 0, let: 0, const: 0 };
34
+ idtypes = { assignExpr: 0, property: 0, variableDeclarator: 0, functionDeclaration: 0 };
35
+ counter = {
36
+ identifiers: 0,
37
+ doubleUnaryArray: 0,
38
+ computedMemberExpr: 0,
39
+ memberExpr: 0,
40
+ deepBinaryExpr: 0,
41
+ encodedArrayValue: 0,
42
+ morseLiteral: 0
43
+ };
44
+ identifiersName = [];
45
+
46
+ constructor() {
47
+ this.dependencies = new ASTDeps();
48
+
49
+ this.identifiers = new Map();
50
+ this.globalParts = new Map();
51
+ this.handledEncodedLiteralValues = new Map();
52
+
53
+ this.requireIdentifiers = new Set(["require", processMainModuleRequire]);
54
+ this.warnings = [];
55
+ this.literalScores = [];
56
+ }
57
+
58
+ addWarning(symbol, value, location = rootLocation()) {
59
+ if (symbol === _warnings.encodedLiteral && this.handledEncodedLiteralValues.has(value)) {
60
+ const index = this.handledEncodedLiteralValues.get(value);
61
+ this.warnings[index].location.push(toArrayLocation(location));
62
+
63
+ return;
64
+ }
65
+ const warningName = kWarningsNameStr[symbol];
66
+ this.warnings.push(generateWarning(warningName, { value, location }));
67
+ if (symbol === _warnings.encodedLiteral) {
68
+ this.handledEncodedLiteralValues.set(value, this.warnings.length - 1);
69
+ }
70
+ }
71
+
72
+ analyzeSourceString(sourceString) {
73
+ if (hasTrojanSource(sourceString)) {
74
+ this.addWarning(_warnings.obfuscatedCode, "trojan-source");
75
+ }
76
+ }
77
+
78
+ analyzeString(str) {
79
+ const score = Utils.stringSuspicionScore(str);
80
+ if (score !== 0) {
81
+ this.literalScores.push(score);
82
+ }
83
+
84
+ if (!this.hasDictionaryString) {
85
+ const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word));
86
+ if (isDictionaryStr) {
87
+ this.hasDictionaryString = true;
88
+ }
89
+ }
90
+
91
+ // Searching for morse string like "--.- --.--."
92
+ if (Utils.stringCharDiversity(str, ["\n"]) >= 3 && Utils.isMorse(str)) {
93
+ this.counter.morseLiteral++;
94
+ }
95
+ }
96
+
97
+ analyzeLiteral(node, inArrayExpr = false) {
98
+ if (typeof node.value !== "string" || Utils.isSvg(node)) {
99
+ return;
100
+ }
101
+ this.analyzeString(node.value);
102
+
103
+ const { hasHexadecimalSequence, hasUnicodeSequence, isBase64 } = Literal.defaultAnalysis(node);
104
+ if ((hasHexadecimalSequence || hasUnicodeSequence) && isBase64) {
105
+ if (inArrayExpr) {
106
+ this.counter.encodedArrayValue++;
107
+ }
108
+ else {
109
+ this.addWarning(_warnings.encodedLiteral, node.value, node.loc);
110
+ }
111
+ }
112
+ }
113
+
114
+ getResult(isMinified) {
115
+ this.counter.identifiers = this.identifiersName.length;
116
+ const [isObfuscated, kind] = isObfuscatedCode(this);
117
+ if (isObfuscated) {
118
+ this.addWarning(_warnings.obfuscatedCode, kind || "unknown");
119
+ }
120
+
121
+ const identifiersLengthArr = this.identifiersName
122
+ .filter((value) => value.type !== "property" && typeof value.name === "string").map((value) => value.name.length);
123
+
124
+ const [idsLengthAvg, stringScore] = [sum(identifiersLengthArr), sum(this.literalScores)];
125
+ if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) {
126
+ this.addWarning(_warnings.shortIdentifiers, idsLengthAvg);
127
+ }
128
+ if (stringScore >= 3) {
129
+ this.addWarning(_warnings.suspiciousLiteral, stringScore);
130
+ }
131
+
132
+ return { idsLengthAvg, stringScore, warnings: this.warnings };
133
+ }
134
+
135
+ walk(node) {
136
+ // Detect TryStatement and CatchClause to known which dependency is required in a Try {} clause
137
+ if (node.type === "TryStatement" && typeof node.handler !== "undefined") {
138
+ this.dependencies.isInTryStmt = true;
139
+ }
140
+ else if (node.type === "CatchClause") {
141
+ this.dependencies.isInTryStmt = false;
142
+ }
143
+
144
+ return runOnProbes(node, this);
145
+ }
146
+ }
147
+
148
+ function sum(arr = []) {
149
+ return arr.length === 0 ? 0 : (arr.reduce((prev, curr) => prev + curr, 0) / arr.length);
150
+ }
151
+
152
+ Analysis.Warnings = _warnings;