@nodesecure/js-x-ray 4.4.0 → 4.5.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/src/ASTDeps.js CHANGED
@@ -1,55 +1,63 @@
1
-
2
- export default class ASTDeps {
3
- #inTry = false;
4
- dependencies = Object.create(null);
5
-
6
- get isInTryStmt() {
7
- return this.#inTry;
8
- }
9
-
10
- set isInTryStmt(value) {
11
- if (typeof value !== "boolean") {
12
- throw new TypeError("value must be a boolean!");
13
- }
14
-
15
- this.#inTry = value;
16
- }
17
-
18
- removeByName(name) {
19
- if (Reflect.has(this.dependencies, name)) {
20
- delete this.dependencies[name];
21
- }
22
- }
23
-
24
- add(depName, location = null, unsafe = false) {
25
- if (depName.trim() === "") {
26
- return;
27
- }
28
-
29
- const cleanDepName = depName.charAt(depName.length - 1) === "/" ? depName.slice(0, -1) : depName;
30
- const dep = {
31
- unsafe,
32
- inTry: this.isInTryStmt
33
- };
34
- if (location !== null) {
35
- dep.location = location;
36
- }
37
- this.dependencies[cleanDepName] = dep;
38
- }
39
-
40
- get size() {
41
- return Object.keys(this.dependencies).length;
42
- }
43
-
44
- * getDependenciesInTryStatement() {
45
- for (const [depName, props] of Object.entries(this.dependencies)) {
46
- if (props.inTry === true && props.unsafe === false) {
47
- yield depName;
48
- }
49
- }
50
- }
51
-
52
- * [Symbol.iterator]() {
53
- yield* Object.keys(this.dependencies);
54
- }
55
- }
1
+
2
+ export default class ASTDeps {
3
+ #inTry = false;
4
+ dependencies = Object.create(null);
5
+
6
+ get isInTryStmt() {
7
+ return this.#inTry;
8
+ }
9
+
10
+ set isInTryStmt(value) {
11
+ if (typeof value !== "boolean") {
12
+ throw new TypeError("value must be a boolean!");
13
+ }
14
+
15
+ this.#inTry = value;
16
+ }
17
+
18
+ removeByName(name) {
19
+ if (Reflect.has(this.dependencies, name)) {
20
+ delete this.dependencies[name];
21
+ }
22
+ }
23
+
24
+ add(depName, location = null, unsafe = false) {
25
+ if (depName.trim() === "") {
26
+ return;
27
+ }
28
+
29
+ const cleanDepName = depName.charAt(depName.length - 1) === "/" ? depName.slice(0, -1) : depName;
30
+ const dep = {
31
+ unsafe,
32
+ inTry: this.isInTryStmt
33
+ };
34
+ if (location !== null) {
35
+ dep.location = location;
36
+ }
37
+ this.dependencies[cleanDepName] = dep;
38
+ }
39
+
40
+ has(depName) {
41
+ if (depName.trim() === "") {
42
+ return false;
43
+ }
44
+
45
+ return Reflect.has(this.dependencies, depName);
46
+ }
47
+
48
+ get size() {
49
+ return Object.keys(this.dependencies).length;
50
+ }
51
+
52
+ * getDependenciesInTryStatement() {
53
+ for (const [depName, props] of Object.entries(this.dependencies)) {
54
+ if (props.inTry === true && props.unsafe === false) {
55
+ yield depName;
56
+ }
57
+ }
58
+ }
59
+
60
+ * [Symbol.iterator]() {
61
+ yield* Object.keys(this.dependencies);
62
+ }
63
+ }
package/src/Analysis.js CHANGED
@@ -1,152 +1,153 @@
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
+ [_warnings.weakCrypto]: "weak-crypto"
29
+ });
30
+
31
+ export default class Analysis {
32
+ hasDictionaryString = false;
33
+ hasPrefixedIdentifiers = false;
34
+ varkinds = { var: 0, let: 0, const: 0 };
35
+ idtypes = { assignExpr: 0, property: 0, variableDeclarator: 0, functionDeclaration: 0 };
36
+ counter = {
37
+ identifiers: 0,
38
+ doubleUnaryArray: 0,
39
+ computedMemberExpr: 0,
40
+ memberExpr: 0,
41
+ deepBinaryExpr: 0,
42
+ encodedArrayValue: 0,
43
+ morseLiteral: 0
44
+ };
45
+ identifiersName = [];
46
+
47
+ constructor() {
48
+ this.dependencies = new ASTDeps();
49
+
50
+ this.identifiers = new Map();
51
+ this.globalParts = new Map();
52
+ this.handledEncodedLiteralValues = new Map();
53
+
54
+ this.requireIdentifiers = new Set(["require", processMainModuleRequire]);
55
+ this.warnings = [];
56
+ this.literalScores = [];
57
+ }
58
+
59
+ addWarning(symbol, value, location = rootLocation()) {
60
+ if (symbol === _warnings.encodedLiteral && this.handledEncodedLiteralValues.has(value)) {
61
+ const index = this.handledEncodedLiteralValues.get(value);
62
+ this.warnings[index].location.push(toArrayLocation(location));
63
+
64
+ return;
65
+ }
66
+ const warningName = kWarningsNameStr[symbol];
67
+ this.warnings.push(generateWarning(warningName, { value, location }));
68
+ if (symbol === _warnings.encodedLiteral) {
69
+ this.handledEncodedLiteralValues.set(value, this.warnings.length - 1);
70
+ }
71
+ }
72
+
73
+ analyzeSourceString(sourceString) {
74
+ if (hasTrojanSource(sourceString)) {
75
+ this.addWarning(_warnings.obfuscatedCode, "trojan-source");
76
+ }
77
+ }
78
+
79
+ analyzeString(str) {
80
+ const score = Utils.stringSuspicionScore(str);
81
+ if (score !== 0) {
82
+ this.literalScores.push(score);
83
+ }
84
+
85
+ if (!this.hasDictionaryString) {
86
+ const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word));
87
+ if (isDictionaryStr) {
88
+ this.hasDictionaryString = true;
89
+ }
90
+ }
91
+
92
+ // Searching for morse string like "--.- --.--."
93
+ if (Utils.stringCharDiversity(str, ["\n"]) >= 3 && Utils.isMorse(str)) {
94
+ this.counter.morseLiteral++;
95
+ }
96
+ }
97
+
98
+ analyzeLiteral(node, inArrayExpr = false) {
99
+ if (typeof node.value !== "string" || Utils.isSvg(node)) {
100
+ return;
101
+ }
102
+ this.analyzeString(node.value);
103
+
104
+ const { hasHexadecimalSequence, hasUnicodeSequence, isBase64 } = Literal.defaultAnalysis(node);
105
+ if ((hasHexadecimalSequence || hasUnicodeSequence) && isBase64) {
106
+ if (inArrayExpr) {
107
+ this.counter.encodedArrayValue++;
108
+ }
109
+ else {
110
+ this.addWarning(_warnings.encodedLiteral, node.value, node.loc);
111
+ }
112
+ }
113
+ }
114
+
115
+ getResult(isMinified) {
116
+ this.counter.identifiers = this.identifiersName.length;
117
+ const [isObfuscated, kind] = isObfuscatedCode(this);
118
+ if (isObfuscated) {
119
+ this.addWarning(_warnings.obfuscatedCode, kind || "unknown");
120
+ }
121
+
122
+ const identifiersLengthArr = this.identifiersName
123
+ .filter((value) => value.type !== "property" && typeof value.name === "string").map((value) => value.name.length);
124
+
125
+ const [idsLengthAvg, stringScore] = [sum(identifiersLengthArr), sum(this.literalScores)];
126
+ if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) {
127
+ this.addWarning(_warnings.shortIdentifiers, idsLengthAvg);
128
+ }
129
+ if (stringScore >= 3) {
130
+ this.addWarning(_warnings.suspiciousLiteral, stringScore);
131
+ }
132
+
133
+ return { idsLengthAvg, stringScore, warnings: this.warnings };
134
+ }
135
+
136
+ walk(node) {
137
+ // Detect TryStatement and CatchClause to known which dependency is required in a Try {} clause
138
+ if (node.type === "TryStatement" && typeof node.handler !== "undefined") {
139
+ this.dependencies.isInTryStmt = true;
140
+ }
141
+ else if (node.type === "CatchClause") {
142
+ this.dependencies.isInTryStmt = false;
143
+ }
144
+
145
+ return runOnProbes(node, this);
146
+ }
147
+ }
148
+
149
+ function sum(arr = []) {
150
+ return arr.length === 0 ? 0 : (arr.reduce((prev, curr) => prev + curr, 0) / arr.length);
151
+ }
152
+
153
+ Analysis.Warnings = _warnings;
package/src/constants.js CHANGED
@@ -43,5 +43,6 @@ export const warnings = Object.freeze({
43
43
  encodedLiteral: Symbol("EncodedLiteral"),
44
44
  shortIdentifiers: Symbol("ShortIdentifiers"),
45
45
  suspiciousLiteral: Symbol("SuspiciousLiteral"),
46
- obfuscatedCode: Symbol("ObfuscatedCode")
46
+ obfuscatedCode: Symbol("ObfuscatedCode"),
47
+ weakCrypto: Symbol("WeakCrypto")
47
48
  });
@@ -1,66 +1,68 @@
1
- // Import all the probes
2
- import isUnsafeCallee from "./isUnsafeCallee.js";
3
- import isLiteral from "./isLiteral.js";
4
- import isLiteralRegex from "./isLiteralRegex.js";
5
- import isRegexObject from "./isRegexObject.js";
6
- import isVariableDeclaration from "./isVariableDeclaration.js";
7
- import isAssignmentExprOrMemberExpr from "./isAssignmentExprOrMemberExpr.js";
8
- import isRequire from "./isRequire.js";
9
- import isImportDeclaration from "./isImportDeclaration.js";
10
- import isMemberExpression from "./isMemberExpression.js";
11
- import isArrayExpression from "./isArrayExpression.js";
12
- import isFunctionDeclaration from "./isFunctionDeclaration.js";
13
- import isAssignmentExpression from "./isAssignmentExpression.js";
14
- import isObjectExpression from "./isObjectExpression.js";
15
- import isUnaryExpression from "./isUnaryExpression.js";
16
-
17
- // CONSTANTS
18
- const kListOfProbes = [
19
- isUnsafeCallee,
20
- isLiteral,
21
- isLiteralRegex,
22
- isRegexObject,
23
- isVariableDeclaration,
24
- isAssignmentExprOrMemberExpr,
25
- isRequire,
26
- isImportDeclaration,
27
- isMemberExpression,
28
- isAssignmentExpression,
29
- isObjectExpression,
30
- isArrayExpression,
31
- isFunctionDeclaration,
32
- isUnaryExpression
33
- ];
34
-
35
- const kSymBreak = Symbol.for("breakWalk");
36
- const kSymSkip = Symbol.for("skipWalk");
37
-
38
- export function runOnProbes(node, analysis) {
39
- const breakedGroups = new Set();
40
-
41
- for (const probe of kListOfProbes) {
42
- if (breakedGroups.has(probe.breakGroup)) {
43
- continue;
44
- }
45
-
46
- const [isMatching, data = null] = probe.validateNode(node, analysis);
47
- if (isMatching) {
48
- const result = probe.main(node, { analysis, data });
49
-
50
- if (result === kSymSkip) {
51
- return "skip";
52
- }
53
- if (result === kSymBreak || probe.breakOnMatch) {
54
- const breakGroup = probe.breakGroup || null;
55
- if (breakGroup === null) {
56
- break;
57
- }
58
- else {
59
- breakedGroups.add(breakGroup);
60
- }
61
- }
62
- }
63
- }
64
-
65
- return null;
66
- }
1
+ // Import all the probes
2
+ import isUnsafeCallee from "./isUnsafeCallee.js";
3
+ import isLiteral from "./isLiteral.js";
4
+ import isLiteralRegex from "./isLiteralRegex.js";
5
+ import isRegexObject from "./isRegexObject.js";
6
+ import isVariableDeclaration from "./isVariableDeclaration.js";
7
+ import isAssignmentExprOrMemberExpr from "./isAssignmentExprOrMemberExpr.js";
8
+ import isRequire from "./isRequire.js";
9
+ import isImportDeclaration from "./isImportDeclaration.js";
10
+ import isMemberExpression from "./isMemberExpression.js";
11
+ import isArrayExpression from "./isArrayExpression.js";
12
+ import isFunctionDeclaration from "./isFunctionDeclaration.js";
13
+ import isAssignmentExpression from "./isAssignmentExpression.js";
14
+ import isObjectExpression from "./isObjectExpression.js";
15
+ import isUnaryExpression from "./isUnaryExpression.js";
16
+ import isWeakCrypto from "./isWeakCrypto.js";
17
+
18
+ // CONSTANTS
19
+ const kListOfProbes = [
20
+ isUnsafeCallee,
21
+ isLiteral,
22
+ isLiteralRegex,
23
+ isRegexObject,
24
+ isVariableDeclaration,
25
+ isAssignmentExprOrMemberExpr,
26
+ isRequire,
27
+ isImportDeclaration,
28
+ isMemberExpression,
29
+ isAssignmentExpression,
30
+ isObjectExpression,
31
+ isArrayExpression,
32
+ isFunctionDeclaration,
33
+ isUnaryExpression,
34
+ isWeakCrypto
35
+ ];
36
+
37
+ const kSymBreak = Symbol.for("breakWalk");
38
+ const kSymSkip = Symbol.for("skipWalk");
39
+
40
+ export function runOnProbes(node, analysis) {
41
+ const breakedGroups = new Set();
42
+
43
+ for (const probe of kListOfProbes) {
44
+ if (breakedGroups.has(probe.breakGroup)) {
45
+ continue;
46
+ }
47
+
48
+ const [isMatching, data = null] = probe.validateNode(node, analysis);
49
+ if (isMatching) {
50
+ const result = probe.main(node, { analysis, data });
51
+
52
+ if (result === kSymSkip) {
53
+ return "skip";
54
+ }
55
+ if (result === kSymBreak || probe.breakOnMatch) {
56
+ const breakGroup = probe.breakGroup || null;
57
+ if (breakGroup === null) {
58
+ break;
59
+ }
60
+ else {
61
+ breakedGroups.add(breakGroup);
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
@@ -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
+ };