@nodesecure/js-x-ray 4.4.0 → 5.0.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/src/Analysis.js CHANGED
@@ -1,152 +1,140 @@
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 } from "./utils.js";
6
+ import { generateWarning } from "./warnings.js";
7
+ import { processMainModuleRequire } from "./constants.js";
8
+ import ASTDeps from "./ASTDeps.js";
9
+ import { isObfuscatedCode, hasTrojanSource } from "./obfuscators/index.js";
10
+ import { runOnProbes } from "./probes/index.js";
11
+
12
+ // CONSTANTS
13
+ const kDictionaryStrParts = [
14
+ "abcdefghijklmnopqrstuvwxyz",
15
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
16
+ "0123456789"
17
+ ];
18
+
19
+ export default class Analysis {
20
+ hasDictionaryString = false;
21
+ hasPrefixedIdentifiers = false;
22
+ varkinds = { var: 0, let: 0, const: 0 };
23
+ idtypes = { assignExpr: 0, property: 0, variableDeclarator: 0, functionDeclaration: 0 };
24
+ counter = {
25
+ identifiers: 0,
26
+ doubleUnaryArray: 0,
27
+ computedMemberExpr: 0,
28
+ memberExpr: 0,
29
+ deepBinaryExpr: 0,
30
+ encodedArrayValue: 0,
31
+ morseLiteral: 0
32
+ };
33
+ identifiersName = [];
34
+
35
+ constructor() {
36
+ this.dependencies = new ASTDeps();
37
+
38
+ this.identifiers = new Map();
39
+ this.globalParts = new Map();
40
+ this.handledEncodedLiteralValues = new Map();
41
+
42
+ this.requireIdentifiers = new Set(["require", processMainModuleRequire]);
43
+ this.warnings = [];
44
+ this.literalScores = [];
45
+ }
46
+
47
+ addWarning(name, value, location = rootLocation()) {
48
+ const isEncodedLiteral = name === "encoded-literal";
49
+ if (isEncodedLiteral && this.handledEncodedLiteralValues.has(value)) {
50
+ const index = this.handledEncodedLiteralValues.get(value);
51
+ this.warnings[index].location.push(toArrayLocation(location));
52
+
53
+ return;
54
+ }
55
+
56
+ this.warnings.push(generateWarning(name, { value, location }));
57
+ if (isEncodedLiteral) {
58
+ this.handledEncodedLiteralValues.set(value, this.warnings.length - 1);
59
+ }
60
+ }
61
+
62
+ analyzeSourceString(sourceString) {
63
+ if (hasTrojanSource(sourceString)) {
64
+ this.addWarning("obfuscated-code", "trojan-source");
65
+ }
66
+ }
67
+
68
+ analyzeString(str) {
69
+ const score = Utils.stringSuspicionScore(str);
70
+ if (score !== 0) {
71
+ this.literalScores.push(score);
72
+ }
73
+
74
+ if (!this.hasDictionaryString) {
75
+ const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word));
76
+ if (isDictionaryStr) {
77
+ this.hasDictionaryString = true;
78
+ }
79
+ }
80
+
81
+ // Searching for morse string like "--.- --.--."
82
+ if (Utils.stringCharDiversity(str, ["\n"]) >= 3 && Utils.isMorse(str)) {
83
+ this.counter.morseLiteral++;
84
+ }
85
+ }
86
+
87
+ analyzeLiteral(node, inArrayExpr = false) {
88
+ if (typeof node.value !== "string" || Utils.isSvg(node)) {
89
+ return;
90
+ }
91
+ this.analyzeString(node.value);
92
+
93
+ const { hasHexadecimalSequence, hasUnicodeSequence, isBase64 } = Literal.defaultAnalysis(node);
94
+ if ((hasHexadecimalSequence || hasUnicodeSequence) && isBase64) {
95
+ if (inArrayExpr) {
96
+ this.counter.encodedArrayValue++;
97
+ }
98
+ else {
99
+ this.addWarning("encoded-literal", node.value, node.loc);
100
+ }
101
+ }
102
+ }
103
+
104
+ getResult(isMinified) {
105
+ this.counter.identifiers = this.identifiersName.length;
106
+ const [isObfuscated, kind] = isObfuscatedCode(this);
107
+ if (isObfuscated) {
108
+ this.addWarning("obfuscated-code", kind || "unknown");
109
+ }
110
+
111
+ const identifiersLengthArr = this.identifiersName
112
+ .filter((value) => value.type !== "property" && typeof value.name === "string").map((value) => value.name.length);
113
+
114
+ const [idsLengthAvg, stringScore] = [sum(identifiersLengthArr), sum(this.literalScores)];
115
+ if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) {
116
+ this.addWarning("short-identifiers", idsLengthAvg);
117
+ }
118
+ if (stringScore >= 3) {
119
+ this.addWarning("suspicious-literal", stringScore);
120
+ }
121
+
122
+ return { idsLengthAvg, stringScore, warnings: this.warnings };
123
+ }
124
+
125
+ walk(node) {
126
+ // Detect TryStatement and CatchClause to known which dependency is required in a Try {} clause
127
+ if (node.type === "TryStatement" && typeof node.handler !== "undefined") {
128
+ this.dependencies.isInTryStmt = true;
129
+ }
130
+ else if (node.type === "CatchClause") {
131
+ this.dependencies.isInTryStmt = false;
132
+ }
133
+
134
+ return runOnProbes(node, this);
135
+ }
136
+ }
137
+
138
+ function sum(arr = []) {
139
+ return arr.length === 0 ? 0 : (arr.reduce((prev, curr) => prev + curr, 0) / arr.length);
140
+ }
package/src/constants.js CHANGED
@@ -33,15 +33,3 @@ export const unsafeUnicodeControlCharacters = [
33
33
  "\u200F",
34
34
  "\u061C"
35
35
  ];
36
-
37
- export const warnings = Object.freeze({
38
- parsingError: Symbol("ParsingError"),
39
- unsafeImport: Symbol("UnsafeImport"),
40
- unsafeRegex: Symbol("UnsafeRegex"),
41
- unsafeStmt: Symbol("UnsafeStmt"),
42
- unsafeAssign: Symbol("UnsafeAssign"),
43
- encodedLiteral: Symbol("EncodedLiteral"),
44
- shortIdentifiers: Symbol("ShortIdentifiers"),
45
- suspiciousLiteral: Symbol("SuspiciousLiteral"),
46
- obfuscatedCode: Symbol("ObfuscatedCode")
47
- });
@@ -13,6 +13,7 @@ import isFunctionDeclaration from "./isFunctionDeclaration.js";
13
13
  import isAssignmentExpression from "./isAssignmentExpression.js";
14
14
  import isObjectExpression from "./isObjectExpression.js";
15
15
  import isUnaryExpression from "./isUnaryExpression.js";
16
+ import isWeakCrypto from "./isWeakCrypto.js";
16
17
 
17
18
  // CONSTANTS
18
19
  const kListOfProbes = [
@@ -29,7 +30,8 @@ const kListOfProbes = [
29
30
  isObjectExpression,
30
31
  isArrayExpression,
31
32
  isFunctionDeclaration,
32
- isUnaryExpression
33
+ isUnaryExpression,
34
+ isWeakCrypto
33
35
  ];
34
36
 
35
37
  const kSymBreak = Symbol.for("breakWalk");
@@ -1,21 +1,27 @@
1
- // Search for Array
2
- function validateNode(node) {
3
- return [
4
- node.type === "ArrayExpression"
5
- ];
6
- }
7
-
8
- function main(node, options) {
9
- const { analysis } = options;
10
-
11
- for (const elem of node.elements) {
12
- if (elem !== null && elem.type === "Literal") {
13
- analysis.analyzeLiteral(elem, true);
14
- }
15
- }
16
- }
17
-
18
- export default {
19
- name: "isArrayExpression",
20
- validateNode, main, breakOnMatch: false
21
- };
1
+ /**
2
+ * @description Search for ArrayExpression AST Node (Commonly known as JS Arrays)
3
+ *
4
+ * @see https://github.com/estree/estree/blob/master/es5.md#arrayexpression
5
+ * @example
6
+ * ["foo", "bar", 1]
7
+ */
8
+ function validateNode(node) {
9
+ return [
10
+ node.type === "ArrayExpression"
11
+ ];
12
+ }
13
+
14
+ function main(node, options) {
15
+ const { analysis } = options;
16
+
17
+ for (const elem of node.elements) {
18
+ if (elem !== null && elem.type === "Literal") {
19
+ analysis.analyzeLiteral(elem, true);
20
+ }
21
+ }
22
+ }
23
+
24
+ export default {
25
+ name: "isArrayExpression",
26
+ validateNode, main, breakOnMatch: false
27
+ };
@@ -1,22 +1,29 @@
1
- // Import Internal Dependencies
2
- import { getIdName } from "../utils.js";
3
-
4
- function validateNode(node) {
5
- return [
6
- node.type === "AssignmentExpression"
7
- ];
8
- }
9
-
10
- function main(node, options) {
11
- const { analysis } = options;
12
-
13
- analysis.idtypes.assignExpr++;
14
- for (const name of getIdName(node.left)) {
15
- analysis.identifiersName.push({ name, type: "assignExpr" });
16
- }
17
- }
18
-
19
- export default {
20
- name: "isAssignmentExpression",
21
- validateNode, main, breakOnMatch: false
22
- };
1
+ // Import Internal Dependencies
2
+ import { getIdName } from "../utils.js";
3
+
4
+ /**
5
+ * @description Search for AssignmentExpression (Not to be confused with AssignmentPattern).
6
+ *
7
+ * @see https://github.com/estree/estree/blob/master/es5.md#assignmentexpression
8
+ * @example
9
+ * (foo = 5)
10
+ */
11
+ function validateNode(node) {
12
+ return [
13
+ node.type === "AssignmentExpression"
14
+ ];
15
+ }
16
+
17
+ function main(node, options) {
18
+ const { analysis } = options;
19
+
20
+ analysis.idtypes.assignExpr++;
21
+ for (const name of getIdName(node.left)) {
22
+ analysis.identifiersName.push({ name, type: "assignExpr" });
23
+ }
24
+ }
25
+
26
+ export default {
27
+ name: "isAssignmentExpression",
28
+ validateNode, main, breakOnMatch: false
29
+ };
@@ -1,39 +1,53 @@
1
- function validateNode(node) {
2
- return [
3
- node.type === "BinaryExpression"
4
- ];
5
- }
6
-
7
- function main(node, options) {
8
- const { analysis } = options;
9
-
10
- const [binaryExprDeepness, hasUnaryExpression] = walkBinaryExpression(node);
11
- if (binaryExprDeepness >= 3 && hasUnaryExpression) {
12
- analysis.counter.deepBinaryExpr++;
13
- }
14
- }
15
-
16
- function walkBinaryExpression(expr, level = 1) {
17
- const [lt, rt] = [expr.left.type, expr.right.type];
18
- let hasUnaryExpression = lt === "UnaryExpression" || rt === "UnaryExpression";
19
- let currentLevel = lt === "BinaryExpression" || rt === "BinaryExpression" ? level + 1 : level;
20
-
21
- for (const currExpr of [expr.left, expr.right]) {
22
- if (currExpr.type === "BinaryExpression") {
23
- const [deepLevel, deepHasUnaryExpression] = walkBinaryExpression(currExpr, currentLevel);
24
- if (deepLevel > currentLevel) {
25
- currentLevel = deepLevel;
26
- }
27
- if (!hasUnaryExpression && deepHasUnaryExpression) {
28
- hasUnaryExpression = true;
29
- }
30
- }
31
- }
32
-
33
- return [currentLevel, hasUnaryExpression];
34
- }
35
-
36
- export default {
37
- name: "isBinaryExpression",
38
- validateNode, main, breakOnMatch: false
39
- };
1
+ /**
2
+ * @description Search for BinaryExpression AST Node.
3
+ *
4
+ * @see https://github.com/estree/estree/blob/master/es5.md#binaryexpression
5
+ * @example
6
+ * 5 + 5 + 10
7
+ */
8
+ function validateNode(node) {
9
+ return [
10
+ node.type === "BinaryExpression"
11
+ ];
12
+ }
13
+
14
+ function main(node, options) {
15
+ const { analysis } = options;
16
+
17
+ const [binaryExprDeepness, hasUnaryExpression] = walkBinaryExpression(node);
18
+ if (binaryExprDeepness >= 3 && hasUnaryExpression) {
19
+ analysis.counter.deepBinaryExpr++;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * @description Look for suspicious BinaryExpression (read the Obfuscator.io section of the linked G.Doc)
25
+ * @see https://docs.google.com/document/d/11ZrfW0bDQ-kd7Gr_Ixqyk8p3TGvxckmhFH3Z8dFoPhY/edit?usp=sharing
26
+ * @see https://github.com/estree/estree/blob/master/es5.md#unaryexpression
27
+ * @example
28
+ * 0x1*-0x12df+-0x1fb9*-0x1+0x2*-0x66d
29
+ */
30
+ function walkBinaryExpression(expr, level = 1) {
31
+ const [lt, rt] = [expr.left.type, expr.right.type];
32
+ let hasUnaryExpression = lt === "UnaryExpression" || rt === "UnaryExpression";
33
+ let currentLevel = lt === "BinaryExpression" || rt === "BinaryExpression" ? level + 1 : level;
34
+
35
+ for (const currExpr of [expr.left, expr.right]) {
36
+ if (currExpr.type === "BinaryExpression") {
37
+ const [deepLevel, deepHasUnaryExpression] = walkBinaryExpression(currExpr, currentLevel);
38
+ if (deepLevel > currentLevel) {
39
+ currentLevel = deepLevel;
40
+ }
41
+ if (!hasUnaryExpression && deepHasUnaryExpression) {
42
+ hasUnaryExpression = true;
43
+ }
44
+ }
45
+ }
46
+
47
+ return [currentLevel, hasUnaryExpression];
48
+ }
49
+
50
+ export default {
51
+ name: "isBinaryExpression",
52
+ validateNode, main, breakOnMatch: false
53
+ };
@@ -1,20 +1,27 @@
1
- function validateNode(node) {
2
- return [
3
- node.type === "FunctionDeclaration"
4
- ];
5
- }
6
-
7
- function main(node, options) {
8
- const { analysis } = options;
9
-
10
- if (node.id === null || node.id.type !== "Identifier") {
11
- return;
12
- }
13
- analysis.idtypes.functionDeclaration++;
14
- analysis.identifiersName.push({ name: node.id.name, type: "functionDeclaration" });
15
- }
16
-
17
- export default {
18
- name: "isFunctionDeclaration",
19
- validateNode, main, breakOnMatch: false
20
- };
1
+ /**
2
+ * @description Search for FunctionDeclaration AST Node.
3
+ *
4
+ * @see https://github.com/estree/estree/blob/master/es5.md#functiondeclaration
5
+ * @example
6
+ * function foo() {}
7
+ */
8
+ function validateNode(node) {
9
+ return [
10
+ node.type === "FunctionDeclaration"
11
+ ];
12
+ }
13
+
14
+ function main(node, options) {
15
+ const { analysis } = options;
16
+
17
+ if (node.id === null || node.id.type !== "Identifier") {
18
+ return;
19
+ }
20
+ analysis.idtypes.functionDeclaration++;
21
+ analysis.identifiersName.push({ name: node.id.name, type: "functionDeclaration" });
22
+ }
23
+
24
+ export default {
25
+ name: "isFunctionDeclaration",
26
+ validateNode, main, breakOnMatch: false
27
+ };
@@ -1,26 +1,30 @@
1
- // Require Internal Dependencies
2
- import { warnings } from "../constants.js";
3
-
4
- // Looking for ESM ImportDeclaration
5
- // see: https://github.com/estree/estree/blob/master/es2015.md#importdeclaration
6
- function validateNode(node) {
7
- return [
8
- node.type === "ImportDeclaration" && node.source.type === "Literal"
9
- ];
10
- }
11
-
12
- function main(node, options) {
13
- const { analysis } = options;
14
-
15
- // Searching for dangerous import "data:text/javascript;..." statement.
16
- // see: https://2ality.com/2019/10/eval-via-import.html
17
- if (node.source.value.startsWith("data:text/javascript")) {
18
- analysis.addWarning(warnings.unsafeImport, node.source.value, node.loc);
19
- }
20
- analysis.dependencies.add(node.source.value, node.loc);
21
- }
22
-
23
- export default {
24
- name: "isImportDeclaration",
25
- validateNode, main, breakOnMatch: true, breakGroup: "import"
26
- };
1
+ /**
2
+ * @description Search for ESM ImportDeclaration
3
+ * @see https://github.com/estree/estree/blob/master/es2015.md#importdeclaration
4
+ * @example
5
+ * import * as foo from "bar";
6
+ * import fs from "fs";
7
+ * import "make-promises-safe";
8
+ */
9
+ function validateNode(node) {
10
+ return [
11
+ // Note: the source property is the right-side Literal part of the Import
12
+ node.type === "ImportDeclaration" && node.source.type === "Literal"
13
+ ];
14
+ }
15
+
16
+ function main(node, options) {
17
+ const { analysis } = options;
18
+
19
+ // Searching for dangerous import "data:text/javascript;..." statement.
20
+ // see: https://2ality.com/2019/10/eval-via-import.html
21
+ if (node.source.value.startsWith("data:text/javascript")) {
22
+ analysis.addWarning("unsafe-import", node.source.value, node.loc);
23
+ }
24
+ analysis.dependencies.add(node.source.value, node.loc);
25
+ }
26
+
27
+ export default {
28
+ name: "isImportDeclaration",
29
+ validateNode, main, breakOnMatch: true, breakGroup: "import"
30
+ };