@nodesecure/js-x-ray 5.1.0 → 6.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.
@@ -1,54 +1,46 @@
1
1
  /* eslint-disable consistent-return */
2
2
 
3
- // Import Internal Dependencies
4
- import { isRequireGlobalMemberExpr, getMemberExprName, arrExprToString, concatBinaryExpr } from "../utils.js";
5
-
6
3
  // Import Third-party Dependencies
7
4
  import { Hex } from "@nodesecure/sec-literal";
8
5
  import { walk } from "estree-walker";
9
-
10
- function validateNode(node, analysis) {
11
- return [
12
- isRequireIdentifiers(node, analysis) ||
13
- isRequireResolve(node) ||
14
- isRequireMemberExpr(node)
15
- ];
16
- }
17
-
18
- function isRequireResolve(node) {
19
- if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
20
- return false;
6
+ import {
7
+ concatBinaryExpression,
8
+ arrayExpressionToString,
9
+ getMemberExpressionIdentifier,
10
+ getCallExpressionIdentifier
11
+ } from "@nodesecure/estree-ast-utils";
12
+
13
+ function validateNode(node, { tracer }) {
14
+ const id = getCallExpressionIdentifier(node);
15
+ if (id === null) {
16
+ return [false];
21
17
  }
22
18
 
23
- return node.callee.object.name === "require" && node.callee.property.name === "resolve";
24
- }
19
+ const data = tracer.getDataFromIdentifier(id);
25
20
 
26
- function isRequireMemberExpr(node) {
27
- if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
28
- return false;
29
- }
30
-
31
- return isRequireGlobalMemberExpr(getMemberExprName(node.callee));
32
- }
33
-
34
- function isRequireIdentifiers(node, analysis) {
35
- if (node.type !== "CallExpression") {
36
- return false;
37
- }
38
- const fullName = node.callee.type === "MemberExpression" ? getMemberExprName(node.callee) : node.callee.name;
39
-
40
- return analysis.requireIdentifiers.has(fullName);
21
+ return [
22
+ data !== null && data.name === "require",
23
+ data?.identifierOrMemberExpr ?? void 0
24
+ ];
41
25
  }
42
26
 
43
27
  function main(node, options) {
44
28
  const { analysis } = options;
29
+ const { tracer } = analysis;
30
+
31
+ if (node.arguments.length === 0) {
32
+ return;
33
+ }
34
+ const arg = node.arguments.at(0);
45
35
 
46
- const arg = node.arguments[0];
47
36
  switch (arg.type) {
48
37
  // const foo = "http"; require(foo);
49
38
  case "Identifier":
50
- if (analysis.identifiers.has(arg.name)) {
51
- analysis.dependencies.add(analysis.identifiers.get(arg.name), node.loc);
39
+ if (analysis.tracer.literalIdentifiers.has(arg.name)) {
40
+ analysis.dependencies.add(
41
+ analysis.tracer.literalIdentifiers.get(arg.name),
42
+ node.loc
43
+ );
52
44
  }
53
45
  else {
54
46
  analysis.addWarning("unsafe-import", null, node.loc);
@@ -60,9 +52,12 @@ function main(node, options) {
60
52
  analysis.dependencies.add(arg.value, node.loc);
61
53
  break;
62
54
 
63
- // require(["ht" + "tp"])
55
+ // require(["ht", "tp"])
64
56
  case "ArrayExpression": {
65
- const value = arrExprToString(arg.elements, analysis.identifiers).trim();
57
+ const value = [...arrayExpressionToString(arg, { tracer })]
58
+ .join("")
59
+ .trim();
60
+
66
61
  if (value === "") {
67
62
  analysis.addWarning("unsafe-import", null, node.loc);
68
63
  }
@@ -75,23 +70,27 @@ function main(node, options) {
75
70
  // require("ht" + "tp");
76
71
  case "BinaryExpression": {
77
72
  if (arg.operator !== "+") {
73
+ analysis.addWarning("unsafe-import", null, node.loc);
78
74
  break;
79
75
  }
80
76
 
81
- const value = concatBinaryExpr(arg, analysis.identifiers);
82
- if (value === null) {
83
- analysis.addWarning("unsafe-import", null, node.loc);
77
+ try {
78
+ const iter = concatBinaryExpression(arg, {
79
+ tracer, stopOnUnsupportedNode: true
80
+ });
81
+
82
+ analysis.dependencies.add([...iter].join(""), node.loc);
84
83
  }
85
- else {
86
- analysis.dependencies.add(value, node.loc);
84
+ catch {
85
+ analysis.addWarning("unsafe-import", null, node.loc);
87
86
  }
88
87
  break;
89
88
  }
90
89
 
91
90
  // require(Buffer.from("...", "hex").toString());
92
91
  case "CallExpression": {
93
- const { dependencies } = parseRequireCallExpression(arg);
94
- dependencies.forEach((depName) => analysis.dependencies.add(depName, node.loc, true));
92
+ walkRequireCallExpression(arg)
93
+ .forEach((depName) => analysis.dependencies.add(depName, node.loc, true));
95
94
 
96
95
  analysis.addWarning("unsafe-import", null, node.loc);
97
96
 
@@ -104,7 +103,7 @@ function main(node, options) {
104
103
  }
105
104
  }
106
105
 
107
- function parseRequireCallExpression(nodeToWalk) {
106
+ function walkRequireCallExpression(nodeToWalk) {
108
107
  const dependencies = new Set();
109
108
 
110
109
  walk(nodeToWalk, {
@@ -113,34 +112,30 @@ function parseRequireCallExpression(nodeToWalk) {
113
112
  return;
114
113
  }
115
114
 
116
- if (node.arguments[0].type === "Literal" && Hex.isHex(node.arguments[0].value)) {
117
- dependencies.add(Buffer.from(node.arguments[0].value, "hex").toString());
115
+ const rootArgument = node.arguments.at(0);
116
+ if (rootArgument.type === "Literal" && Hex.isHex(rootArgument.value)) {
117
+ dependencies.add(Buffer.from(rootArgument.value, "hex").toString());
118
118
 
119
119
  return this.skip();
120
120
  }
121
121
 
122
- const fullName = node.callee.type === "MemberExpression" ? getMemberExprName(node.callee) : node.callee.name;
122
+ const fullName = node.callee.type === "MemberExpression" ?
123
+ [...getMemberExpressionIdentifier(node.callee)].join(".") :
124
+ node.callee.name;
125
+
123
126
  switch (fullName) {
124
127
  case "Buffer.from": {
125
- const [element, convert] = node.arguments;
128
+ const [element] = node.arguments;
126
129
 
127
130
  if (element.type === "ArrayExpression") {
128
- const depName = arrExprToString(element);
129
- if (depName.trim() !== "") {
130
- dependencies.add(depName);
131
- }
132
- }
133
- else if (element.type === "Literal" && convert.type === "Literal" && convert.value === "hex") {
134
- const value = Buffer.from(element.value, "hex").toString();
135
- dependencies.add(value);
131
+ const depName = [...arrayExpressionToString(element)].join("").trim();
132
+ dependencies.add(depName);
136
133
  }
137
134
  break;
138
135
  }
139
136
  case "require.resolve": {
140
- const [element] = node.arguments;
141
-
142
- if (element.type === "Literal") {
143
- dependencies.add(element.value);
137
+ if (rootArgument.type === "Literal") {
138
+ dependencies.add(rootArgument.value);
144
139
  }
145
140
  break;
146
141
  }
@@ -148,9 +143,7 @@ function parseRequireCallExpression(nodeToWalk) {
148
143
  }
149
144
  });
150
145
 
151
- return {
152
- dependencies: [...dependencies]
153
- };
146
+ return [...dependencies];
154
147
  }
155
148
 
156
149
  export default {
@@ -1,4 +1,4 @@
1
- // Require Internal Dependencies
1
+ // Import Internal Dependencies
2
2
  import { isUnsafeCallee } from "../utils.js";
3
3
 
4
4
  /**
@@ -15,6 +15,8 @@ function main(node, options) {
15
15
  const { analysis, data: calleeName } = options;
16
16
 
17
17
  analysis.addWarning("unsafe-stmt", calleeName, node.loc);
18
+
19
+ return Symbol.for("skipWalk");
18
20
  }
19
21
 
20
22
  export default {
@@ -1,104 +1,30 @@
1
- // Require Internal Dependencies
2
- import { getIdName, getMemberExprName, isUnsafeCallee, isRequireGlobalMemberExpr } from "../utils.js";
3
- import { globalParts, processMainModuleRequire } from "../constants.js";
4
-
5
- // CONSTANTS
6
- const kUnsafeCallee = new Set(["eval", "Function"]);
7
-
8
- // In case we are matching a Variable declaration, we have to save the identifier
9
- // This allow the AST Analysis to retrieve required dependency when the stmt is mixed with variables.
10
- function validateNode(node) {
11
- return [
12
- node.type === "VariableDeclaration"
13
- ];
14
- }
15
-
16
- function main(mainNode, options) {
17
- const { analysis } = options;
18
-
19
- analysis.varkinds[mainNode.kind]++;
20
-
21
- for (const node of mainNode.declarations) {
22
- analysis.idtypes.variableDeclarator++;
23
- for (const name of getIdName(node.id)) {
24
- analysis.identifiersName.push({ name, type: "variableDeclarator" });
25
- }
26
-
27
- if (node.init === null || node.id.type !== "Identifier") {
28
- continue;
29
- }
30
-
31
- if (node.init.type === "Literal") {
32
- analysis.identifiers.set(node.id.name, String(node.init.value));
33
- }
34
-
35
- // Searching for someone who assign require to a variable, ex:
36
- // const r = require
37
- else if (node.init.type === "Identifier") {
38
- if (kUnsafeCallee.has(node.init.name)) {
39
- analysis.addWarning("unsafe-assign", node.init.name, node.loc);
40
- }
41
- else if (analysis.requireIdentifiers.has(node.init.name)) {
42
- analysis.requireIdentifiers.add(node.id.name);
43
- analysis.addWarning("unsafe-assign", node.init.name, node.loc);
44
- }
45
- else if (globalParts.has(node.init.name)) {
46
- analysis.globalParts.set(node.id.name, node.init.name);
47
- getRequirablePatterns(analysis.globalParts)
48
- .forEach((name) => analysis.requireIdentifiers.add(name));
49
- }
50
- }
51
-
52
- // Same as before but for pattern like process.mainModule and require.resolve
53
- else if (node.init.type === "MemberExpression") {
54
- const value = getMemberExprName(node.init);
55
- const members = value.split(".");
56
-
57
- if (analysis.globalParts.has(members[0]) || members.every((part) => globalParts.has(part))) {
58
- analysis.globalParts.set(node.id.name, members.slice(1).join("."));
59
- analysis.addWarning("unsafe-assign", value, node.loc);
60
- }
61
- getRequirablePatterns(analysis.globalParts)
62
- .forEach((name) => analysis.requireIdentifiers.add(name));
63
-
64
- if (isRequireStatement(value)) {
65
- analysis.requireIdentifiers.add(node.id.name);
66
- analysis.addWarning("unsafe-assign", value, node.loc);
67
- }
68
- }
69
- else if (isUnsafeCallee(node.init)[0]) {
70
- analysis.globalParts.set(node.id.name, "global");
71
- globalParts.add(node.id.name);
72
- analysis.requireIdentifiers.add(`${node.id.name}.${processMainModuleRequire}`);
73
- }
74
- }
75
- }
76
-
77
- function isRequireStatement(value) {
78
- return value.startsWith("require") ||
79
- value.startsWith(processMainModuleRequire) ||
80
- isRequireGlobalMemberExpr(value);
81
- }
82
-
83
- function getRequirablePatterns(parts) {
84
- const result = new Set();
85
-
86
- for (const [id, path] of parts.entries()) {
87
- if (path === "process") {
88
- result.add(`${id}.mainModule.require`);
89
- }
90
- else if (path === "mainModule") {
91
- result.add(`${id}.require`);
92
- }
93
- else if (path.includes("require")) {
94
- result.add(id);
95
- }
96
- }
97
-
98
- return [...result];
99
- }
100
-
101
- export default {
102
- name: "isVariableDeclaration",
103
- validateNode, main, breakOnMatch: false
104
- };
1
+ // Import Third-party Dependencies
2
+ import {
3
+ getVariableDeclarationIdentifiers
4
+ } from "@nodesecure/estree-ast-utils";
5
+
6
+ // In case we are matching a Variable declaration, we have to save the identifier
7
+ // This allow the AST Analysis to retrieve required dependency when the stmt is mixed with variables.
8
+ function validateNode(node) {
9
+ return [
10
+ node.type === "VariableDeclaration"
11
+ ];
12
+ }
13
+
14
+ function main(mainNode, options) {
15
+ const { analysis } = options;
16
+
17
+ analysis.varkinds[mainNode.kind]++;
18
+
19
+ for (const node of mainNode.declarations) {
20
+ analysis.idtypes.variableDeclarator++;
21
+ for (const { name } of getVariableDeclarationIdentifiers(node.id)) {
22
+ analysis.identifiersName.push({ name, type: "variableDeclarator" });
23
+ }
24
+ }
25
+ }
26
+
27
+ export default {
28
+ name: "isVariableDeclaration",
29
+ validateNode, main, breakOnMatch: false
30
+ };
@@ -1,26 +1,30 @@
1
+ // Import Third-party Dependencies
2
+ import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils";
3
+
1
4
  // CONSTANTS
2
- const kWeakAlgorithms = new Set(["md5", "sha1", "ripemd160", "md4", "md2"]);
5
+ const kWeakAlgorithms = new Set([
6
+ "md5",
7
+ "sha1",
8
+ "ripemd160",
9
+ "md4",
10
+ "md2"
11
+ ]);
12
+
13
+ function validateNode(node, { tracer }) {
14
+ const id = getCallExpressionIdentifier(node);
15
+ if (id === null || !tracer.importedModules.has("crypto")) {
16
+ return [false];
17
+ }
3
18
 
4
- function validateNode(node) {
5
- const isCallExpression = node.type === "CallExpression";
6
- const isSimpleIdentifier = isCallExpression &&
7
- node.callee.type === "Identifier" &&
8
- node.callee.name === "createHash";
9
- const isMemberExpression = isCallExpression &&
10
- node.callee.type === "MemberExpression" &&
11
- node.callee.property.name === "createHash";
19
+ const data = tracer.getDataFromIdentifier(id);
12
20
 
13
- return [isSimpleIdentifier || isMemberExpression];
21
+ return [data !== null && data.identifierOrMemberExpr === "crypto.createHash"];
14
22
  }
15
23
 
16
24
  function main(node, { analysis }) {
17
25
  const arg = node.arguments.at(0);
18
- const isCryptoImported = analysis.dependencies.has("crypto");
19
26
 
20
- if (
21
- kWeakAlgorithms.has(arg.value) &&
22
- isCryptoImported
23
- ) {
27
+ if (kWeakAlgorithms.has(arg.value)) {
24
28
  analysis.addWarning("weak-crypto", arg.value, node.loc);
25
29
  }
26
30
  }
package/src/utils.js CHANGED
@@ -1,166 +1,44 @@
1
1
  // Import Third-party Dependencies
2
- import { Hex } from "@nodesecure/sec-literal";
3
-
4
- // Import Internal Dependencies
5
- import { globalIdentifiers, processMainModuleRequire } from "./constants.js";
6
-
7
- // CONSTANTS
8
- const kBinaryExprTypes = new Set(["Literal", "BinaryExpression", "Identifier"]);
9
- const kExperimentalWarnings = new Set(["weak-crypto"]);
2
+ import {
3
+ getCallExpressionIdentifier
4
+ } from "@nodesecure/estree-ast-utils";
10
5
 
11
6
  export function notNullOrUndefined(value) {
12
7
  return value !== null && value !== void 0;
13
8
  }
14
9
 
15
- export function* getIdName(node) {
16
- switch (node.type) {
17
- case "Identifier":
18
- yield node.name;
19
- break;
20
- case "RestElement":
21
- yield node.argument.name;
22
- break;
23
- case "AssignmentPattern":
24
- yield node.left.name;
25
- break;
26
- case "ArrayPattern":
27
- yield* node.elements.filter(notNullOrUndefined).map((id) => [...getIdName(id)]).flat();
28
- break;
29
- case "ObjectPattern":
30
- yield* node.properties.filter(notNullOrUndefined).map((property) => [...getIdName(property)]).flat();
31
- break;
32
- }
33
- }
34
-
35
- export function isRequireGlobalMemberExpr(value) {
36
- return [...globalIdentifiers]
37
- .some((name) => value.startsWith(`${name}.${processMainModuleRequire}`));
38
- }
39
-
40
10
  export function isUnsafeCallee(node) {
41
- if (node.type !== "CallExpression") {
42
- return [false, null];
43
- }
44
-
45
- if (node.callee.type === "Identifier") {
46
- return [node.callee.name === "eval", "eval"];
47
- }
48
-
49
- if (node.callee.type !== "CallExpression") {
50
- return [false, null];
51
- }
52
- const callee = node.callee.callee;
53
-
54
- return [callee.type === "Identifier" && callee.name === "Function", "Function"];
55
- }
11
+ const identifier = getCallExpressionIdentifier(node);
56
12
 
57
- export function isLiteralRegex(node) {
58
- return node.type === "Literal" && Reflect.has(node, "regex");
13
+ // For Function we are looking for this: `Function("...")();`
14
+ // A double CallExpression
15
+ return [
16
+ identifier === "eval" || (identifier === "Function" && node.callee.type === "CallExpression"),
17
+ identifier
18
+ ];
59
19
  }
60
20
 
61
- export function arrExprToString(elements, identifiers = null) {
62
- let ret = "";
63
- const isArrayExpr = typeof elements === "object" && Reflect.has(elements, "elements");
64
- const localElements = isArrayExpr ? elements.elements : elements;
65
-
66
- for (const row of localElements) {
67
- if (row.type === "Literal") {
68
- if (row.value === "") {
69
- continue;
70
- }
71
-
72
- const value = Number(row.value);
73
- ret += Number.isNaN(value) ? row.value : String.fromCharCode(value);
74
- }
75
- else if (row.type === "Identifier" && identifiers !== null && identifiers.has(row.name)) {
76
- ret += identifiers.get(row.name);
77
- }
78
- }
79
-
80
- return ret;
21
+ export function rootLocation() {
22
+ return { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } };
81
23
  }
82
24
 
83
- export function concatBinaryExpr(node, identifiers = new Set()) {
84
- const { left, right } = node;
85
- if (!kBinaryExprTypes.has(left.type) || !kBinaryExprTypes.has(right.type)) {
86
- return null;
87
- }
88
- let str = "";
89
-
90
- for (const childNode of [left, right]) {
91
- switch (childNode.type) {
92
- case "BinaryExpression": {
93
- const value = concatBinaryExpr(childNode, identifiers);
94
- if (value !== null) {
95
- str += value;
96
- }
97
- break;
98
- }
99
- case "ArrayExpression": {
100
- str += arrExprToString(childNode.elements, identifiers);
101
- break;
102
- }
103
- case "Literal":
104
- str += childNode.value;
105
- break;
106
- case "Identifier":
107
- if (identifiers.has(childNode.name)) {
108
- str += identifiers.get(childNode.name);
109
- }
110
- break;
111
- }
112
- }
25
+ export function toArrayLocation(location = rootLocation()) {
26
+ const { start, end = start } = location;
113
27
 
114
- return str;
28
+ return [
29
+ [start.line || 0, start.column || 0],
30
+ [end.line || 0, end.column || 0]
31
+ ];
115
32
  }
116
33
 
117
- export function getMemberExprName(node) {
118
- let name = "";
34
+ export function extractNode(expectedType) {
35
+ return (callback, nodes) => {
36
+ const finalNodes = Array.isArray(nodes) ? nodes : [nodes];
119
37
 
120
- switch (node.object.type) {
121
- case "MemberExpression":
122
- name += getMemberExprName(node.object);
123
- break;
124
- case "Identifier":
125
- name += node.object.name;
126
- break;
127
- case "Literal":
128
- name += node.object.value;
129
- break;
130
- }
131
-
132
- switch (node.property.type) {
133
- case "Identifier":
134
- name += `.${node.property.name}`;
135
- break;
136
- case "Literal":
137
- name += `.${node.property.value}`;
138
- break;
139
- case "CallExpression": {
140
- const args = node.property.arguments;
141
- if (args.length > 0 && args[0].type === "Literal" && Hex.isHex(args[0].value)) {
142
- name += `.${Buffer.from(args[0].value, "hex").toString()}`;
143
- }
144
- break;
145
- }
146
- case "BinaryExpression": {
147
- const value = concatBinaryExpr(node.property);
148
- if (value !== null && value.trim() !== "") {
149
- name += `.${value}`;
38
+ for (const node of finalNodes) {
39
+ if (notNullOrUndefined(node) && node.type === expectedType) {
40
+ callback(node);
150
41
  }
151
- break;
152
42
  }
153
- }
154
-
155
- return name;
156
- }
157
-
158
- export function rootLocation() {
159
- return { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } };
160
- }
161
-
162
- export function toArrayLocation(location = rootLocation()) {
163
- const { start, end = start } = location;
164
-
165
- return [[start.line || 0, start.column || 0], [end.line || 0, end.column || 0]];
43
+ };
166
44
  }
package/src/warnings.js CHANGED
@@ -19,10 +19,6 @@ export const warnings = Object.freeze({
19
19
  i18n: "sast_warnings.unsafe_stmt",
20
20
  severity: "Warning"
21
21
  },
22
- "unsafe-assign": {
23
- i18n: "sast_warnings.unsafe_assign",
24
- severity: "Warning"
25
- },
26
22
  "encoded-literal": {
27
23
  i18n: "sast_warnings.encoded_literal",
28
24
  severity: "Information"
@@ -35,6 +31,11 @@ export const warnings = Object.freeze({
35
31
  i18n: "sast_warnings.suspicious_literal",
36
32
  severity: "Warning"
37
33
  },
34
+ "suspicious-file": {
35
+ i18n: "sast_warnings.suspicious_file",
36
+ severity: "Critical",
37
+ experimental: true
38
+ },
38
39
  "obfuscated-code": {
39
40
  i18n: "sast_warnings.obfuscated_code",
40
41
  severity: "Critical",
package/types/api.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ASTDeps } from "./astdeps";
2
- import { Warning } from "./warnings";
1
+ import { ASTDeps } from "./astdeps.js";
2
+ import { Warning } from "./warnings.js";
3
3
 
4
4
  export {
5
5
  runASTAnalysis,
@@ -11,9 +11,9 @@ type WarningNameWithValue = "parsing-error"
11
11
  | "encoded-literal"
12
12
  | "unsafe-regex"
13
13
  | "unsafe-stmt"
14
- | "unsafe-assign"
15
14
  | "short-identifiers"
16
15
  | "suspicious-literal"
16
+ | "suspicious-file"
17
17
  | "obfuscated-code"
18
18
  | "weak-crypto";
19
19
  type WarningName = WarningNameWithValue | "unsafe-import";
package/src/constants.js DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * This is one of the way to get a valid require.
3
- *
4
- * @see https://nodejs.org/api/process.html#process_process_mainmodule
5
- */
6
- export const processMainModuleRequire = "process.mainModule.require";
7
-
8
- /**
9
- * JavaScript dangerous global identifiers that can be used by hackers
10
- */
11
- export const globalIdentifiers = new Set(["global", "globalThis", "root", "GLOBAL", "window"]);
12
-
13
- /**
14
- * Dangerous Global identifiers parts
15
- */
16
- export const globalParts = new Set([...globalIdentifiers, "process", "mainModule", "require"]);
17
-
18
- /**
19
- * Dangerous Unicode control characters that can be used by hackers
20
- * to perform trojan source.
21
- */
22
- export const unsafeUnicodeControlCharacters = [
23
- "\u202A",
24
- "\u202B",
25
- "\u202D",
26
- "\u202E",
27
- "\u202C",
28
- "\u2066",
29
- "\u2067",
30
- "\u2068",
31
- "\u2069",
32
- "\u200E",
33
- "\u200F",
34
- "\u061C"
35
- ];