@nodesecure/js-x-ray 4.2.0 → 4.4.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 +32 -0
- package/index.d.ts +124 -92
- package/index.js +46 -14
- package/package.json +10 -10
- package/src/Analysis.js +152 -152
package/README.md
CHANGED
|
@@ -73,6 +73,38 @@ The analysis will return: `http` (in try), `crypto`, `util` and `fs`.
|
|
|
73
73
|
|
|
74
74
|
> ⚠️ There is also a lot of suspicious code example in the root cases directory. Feel free to try the tool on these files.
|
|
75
75
|
|
|
76
|
+
## Warnings
|
|
77
|
+
|
|
78
|
+
This section describes how use `warnings` export.
|
|
79
|
+
|
|
80
|
+
The structure of the `warnings` is as follows:
|
|
81
|
+
```
|
|
82
|
+
/**
|
|
83
|
+
* @property {object} warnings - The default values for Constants.
|
|
84
|
+
* @property {string} warnings[name] - The default warning name (parsingError, unsafeImport etc...).
|
|
85
|
+
* @property {string} warnings[name].i18n - i18n token.
|
|
86
|
+
* @property {string} warnings[name].code - Used to perform unit tests.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
export const warnings = Object.freeze({
|
|
90
|
+
parsingError: {
|
|
91
|
+
i18n: "sast_warnings.ast_error"
|
|
92
|
+
code: "ast-error",
|
|
93
|
+
},
|
|
94
|
+
...otherWarnings
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
We make a call to `i18n` through the package `NodeSecure/i18n` to get the translation.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
import * as jsxray from "@nodesecure/js-x-ray";
|
|
102
|
+
import * as i18n from "@nodesecure/i18n";
|
|
103
|
+
|
|
104
|
+
console.log(i18n.getToken(jsxray.warnings.parsingError.i18n));
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
|
|
76
108
|
## Warnings Legends (v2.0+)
|
|
77
109
|
|
|
78
110
|
> Node-secure versions equal or lower than 0.7.0 are no longer compatible with the warnings table below.
|
package/index.d.ts
CHANGED
|
@@ -1,92 +1,124 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
1
|
+
declare class ASTDeps {
|
|
2
|
+
constructor();
|
|
3
|
+
removeByName(name: string): void;
|
|
4
|
+
add(depName: string): void;
|
|
5
|
+
getDependenciesInTryStatement(): IterableIterator<string>;
|
|
6
|
+
|
|
7
|
+
public isInTryStmt: boolean;
|
|
8
|
+
public dependencies: Record<string, JSXRay.Dependency>;
|
|
9
|
+
public readonly size: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare namespace JSXRay {
|
|
13
|
+
type kindWithValue = "parsing-error"
|
|
14
|
+
| "encoded-literal"
|
|
15
|
+
| "unsafe-regex"
|
|
16
|
+
| "unsafe-stmt"
|
|
17
|
+
| "unsafe-assign"
|
|
18
|
+
| "short-identifiers"
|
|
19
|
+
| "suspicious-literal"
|
|
20
|
+
| "obfuscated-code";
|
|
21
|
+
|
|
22
|
+
type WarningLocation = [[number, number], [number, number]];
|
|
23
|
+
interface BaseWarning {
|
|
24
|
+
kind: "unsafe-import" | kindWithValue;
|
|
25
|
+
file?: string;
|
|
26
|
+
value: string;
|
|
27
|
+
location: WarningLocation | WarningLocation[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Warning<T extends BaseWarning> = T extends { kind: kindWithValue } ? T : Omit<T, "value">;
|
|
31
|
+
|
|
32
|
+
interface Report {
|
|
33
|
+
dependencies: ASTDeps;
|
|
34
|
+
warnings: Warning<BaseWarning>[];
|
|
35
|
+
idsLengthAvg: number;
|
|
36
|
+
stringScore: number;
|
|
37
|
+
isOneLineRequire: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SourceLocation {
|
|
41
|
+
start: {
|
|
42
|
+
line: number;
|
|
43
|
+
column: number;
|
|
44
|
+
};
|
|
45
|
+
end: {
|
|
46
|
+
line: number;
|
|
47
|
+
column: number;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Dependency {
|
|
52
|
+
unsafe: boolean;
|
|
53
|
+
inTry: boolean;
|
|
54
|
+
location?: SourceLocation;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface WarningsNames {
|
|
58
|
+
parsingError: {
|
|
59
|
+
code: "ast-error",
|
|
60
|
+
i18n: "sast_warnings.ast_error"
|
|
61
|
+
},
|
|
62
|
+
unsafeImport: {
|
|
63
|
+
code: "unsafe-import",
|
|
64
|
+
i18n: "sast_warnings.unsafe_import"
|
|
65
|
+
},
|
|
66
|
+
unsafeRegex: {
|
|
67
|
+
code: "unsafe-regex",
|
|
68
|
+
i18n: "sast_warnings.unsafe_regex"
|
|
69
|
+
},
|
|
70
|
+
unsafeStmt: {
|
|
71
|
+
code: "unsafe-stmt",
|
|
72
|
+
i18n: "sast_warnings.unsafe_stmt"
|
|
73
|
+
},
|
|
74
|
+
unsafeAssign: {
|
|
75
|
+
code: "unsafe-assign",
|
|
76
|
+
i18n: "sast_warnings.unsafe_assign"
|
|
77
|
+
},
|
|
78
|
+
encodedLiteral: {
|
|
79
|
+
code: "encoded-literal",
|
|
80
|
+
i18n: "sast_warnings.encoded_literal"
|
|
81
|
+
},
|
|
82
|
+
shortIdentifiers: {
|
|
83
|
+
code: "short-identifiers",
|
|
84
|
+
i18n: "sast_warnings.short_identifiers"
|
|
85
|
+
},
|
|
86
|
+
suspiciousLiteral: {
|
|
87
|
+
code: "suspicious-literal",
|
|
88
|
+
i18n: "sast_warnings.suspicious_literal"
|
|
89
|
+
},
|
|
90
|
+
obfuscatedCode: {
|
|
91
|
+
code: "obfuscated-code",
|
|
92
|
+
i18n: "sast_warnings.obfuscated_code"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface RuntimeOptions {
|
|
97
|
+
module?: boolean;
|
|
98
|
+
isMinified?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function runASTAnalysis(str: string, options?: RuntimeOptions): Report;
|
|
102
|
+
|
|
103
|
+
export type ReportOnFile = {
|
|
104
|
+
ok: true,
|
|
105
|
+
warnings: Warning<BaseWarning>[];
|
|
106
|
+
dependencies: ASTDeps;
|
|
107
|
+
isMinified: boolean;
|
|
108
|
+
} | {
|
|
109
|
+
ok: false,
|
|
110
|
+
warnings: Warning<BaseWarning>[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RuntimeFileOptions {
|
|
114
|
+
packageName?: string;
|
|
115
|
+
module?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions): Promise<ReportOnFile>;
|
|
119
|
+
|
|
120
|
+
export const warnings: WarningsNames;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export = JSXRay;
|
|
124
|
+
export as namespace JSXRay;
|
package/index.js
CHANGED
|
@@ -16,8 +16,13 @@ export function runASTAnalysis(str, options = Object.create(null)) {
|
|
|
16
16
|
// Note: if the file start with a shebang then we remove it because 'parseScript' may fail to parse it.
|
|
17
17
|
// Example: #!/usr/bin/env node
|
|
18
18
|
const strToAnalyze = str.charAt(0) === "#" ? str.slice(str.indexOf("\n")) : str;
|
|
19
|
+
const isEcmaScriptModule = Boolean(module);
|
|
19
20
|
const { body } = meriyah.parseScript(strToAnalyze, {
|
|
20
|
-
next: true,
|
|
21
|
+
next: true,
|
|
22
|
+
loc: true,
|
|
23
|
+
raw: true,
|
|
24
|
+
module: isEcmaScriptModule,
|
|
25
|
+
globalReturn: !isEcmaScriptModule
|
|
21
26
|
});
|
|
22
27
|
|
|
23
28
|
const sastAnalysis = new Analysis();
|
|
@@ -78,16 +83,43 @@ export async function runASTAnalysisOnFile(pathToFile, options = {}) {
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
export const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
86
|
+
export const warnings = Object.freeze({
|
|
87
|
+
parsingError: {
|
|
88
|
+
code: "ast-error",
|
|
89
|
+
i18n: "sast_warnings.ast_error"
|
|
90
|
+
},
|
|
91
|
+
unsafeImport: {
|
|
92
|
+
code: "unsafe-import",
|
|
93
|
+
i18n: "sast_warnings.unsafe_import"
|
|
94
|
+
},
|
|
95
|
+
unsafeRegex: {
|
|
96
|
+
code: "unsafe-regex",
|
|
97
|
+
i18n: "sast_warnings.unsafe_regex"
|
|
98
|
+
},
|
|
99
|
+
unsafeStmt: {
|
|
100
|
+
code: "unsafe-stmt",
|
|
101
|
+
i18n: "sast_warnings.unsafe_stmt"
|
|
102
|
+
},
|
|
103
|
+
unsafeAssign: {
|
|
104
|
+
code: "unsafe-assign",
|
|
105
|
+
i18n: "sast_warnings.unsafe_assign"
|
|
106
|
+
},
|
|
107
|
+
encodedLiteral: {
|
|
108
|
+
code: "encoded-literal",
|
|
109
|
+
i18n: "sast_warnings.encoded_literal"
|
|
110
|
+
},
|
|
111
|
+
shortIdentifiers: {
|
|
112
|
+
code: "short-identifiers",
|
|
113
|
+
i18n: "sast_warnings.short_identifiers"
|
|
114
|
+
},
|
|
115
|
+
suspiciousLiteral: {
|
|
116
|
+
code: "suspicious-literal",
|
|
117
|
+
i18n: "sast_warnings.suspicious_literal"
|
|
118
|
+
},
|
|
119
|
+
obfuscatedCode: {
|
|
120
|
+
code: "obfuscated-code",
|
|
121
|
+
i18n: "sast_warnings.obfuscated_code"
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodesecure/js-x-ray",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.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
|
|
41
|
-
"estree-walker": "^3.0.
|
|
40
|
+
"@nodesecure/sec-literal": "^1.1.0",
|
|
41
|
+
"estree-walker": "^3.0.1",
|
|
42
42
|
"is-minified-code": "^2.0.0",
|
|
43
|
-
"meriyah": "^4.2.
|
|
43
|
+
"meriyah": "^4.2.1",
|
|
44
44
|
"safe-regex": "^2.1.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@nodesecure/eslint-config": "^1.3.
|
|
47
|
+
"@nodesecure/eslint-config": "^1.3.1",
|
|
48
48
|
"@slimio/is": "^1.5.1",
|
|
49
|
-
"@small-tech/esm-tape-runner": "^
|
|
49
|
+
"@small-tech/esm-tape-runner": "^2.0.0",
|
|
50
50
|
"@small-tech/tap-monkey": "^1.3.0",
|
|
51
|
-
"@types/node": "^
|
|
51
|
+
"@types/node": "^17.0.31",
|
|
52
52
|
"cross-env": "^7.0.3",
|
|
53
|
-
"eslint": "^8.
|
|
54
|
-
"pkg-ok": "^
|
|
55
|
-
"tape": "^5.3
|
|
53
|
+
"eslint": "^8.15.0",
|
|
54
|
+
"pkg-ok": "^3.0.0",
|
|
55
|
+
"tape": "^5.5.3"
|
|
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;
|