@nodesecure/js-x-ray 4.5.0 → 5.0.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 CHANGED
@@ -77,24 +77,23 @@ The analysis will return: `http` (in try), `crypto`, `util` and `fs`.
77
77
 
78
78
  This section describes how use `warnings` export.
79
79
 
80
- The structure of the `warnings` is as follows:
81
- ```js
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
- * @property {string} warnings[name].severity - Warning severity.
88
- */
89
-
90
- export const warnings = Object.freeze({
91
- parsingError: {
92
- i18n: "sast_warnings.ast_error"
93
- code: "ast-error",
94
- severity: "Information"
95
- },
96
- ...otherWarnings
97
- });
80
+ ```ts
81
+ type WarningName = "parsing-error"
82
+ | "encoded-literal"
83
+ | "unsafe-regex"
84
+ | "unsafe-stmt"
85
+ | "unsafe-assign"
86
+ | "short-identifiers"
87
+ | "suspicious-literal"
88
+ | "obfuscated-code"
89
+ | "weak-crypto"
90
+ | "unsafe-import";
91
+
92
+ declare const warnings: Record<WarningName, {
93
+ i18n: string;
94
+ severity: "Information" | "Warning" | "Critical";
95
+ experimental?: boolean;
96
+ }>;
98
97
  ```
99
98
 
100
99
  We make a call to `i18n` through the package `NodeSecure/i18n` to get the translation.
@@ -103,7 +102,7 @@ We make a call to `i18n` through the package `NodeSecure/i18n` to get the transl
103
102
  import * as jsxray from "@nodesecure/js-x-ray";
104
103
  import * as i18n from "@nodesecure/i18n";
105
104
 
106
- console.log(i18n.getToken(jsxray.warnings.parsingError.i18n));
105
+ console.log(i18n.getToken(jsxray.warnings["parsing-error"].i18n));
107
106
  ```
108
107
 
109
108
  ## Warnings Legends
@@ -142,7 +141,7 @@ The method take a first argument which is the code you want to analyse. It will
142
141
  ```ts
143
142
  interface Report {
144
143
  dependencies: ASTDeps;
145
- warnings: Warning<BaseWarning>[];
144
+ warnings: Warning[];
146
145
  idsLengthAvg: number;
147
146
  stringScore: number;
148
147
  isOneLineRequire: boolean;
@@ -166,12 +165,12 @@ Run the SAST scanner on a given JavaScript file.
166
165
  ```ts
167
166
  export type ReportOnFile = {
168
167
  ok: true,
169
- warnings: Warning<BaseWarning>[];
168
+ warnings: Warning[];
170
169
  dependencies: ASTDeps;
171
170
  isMinified: boolean;
172
171
  } | {
173
172
  ok: false,
174
- warnings: Warning<BaseWarning>[];
173
+ warnings: Warning[];
175
174
  }
176
175
  ```
177
176
 
package/index.d.ts CHANGED
@@ -1,140 +1,34 @@
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
- | "weak-crypto";
22
-
23
- type WarningLocation = [[number, number], [number, number]];
24
- interface BaseWarning {
25
- kind: "unsafe-import" | kindWithValue;
26
- file?: string;
27
- value: string;
28
- location: WarningLocation | WarningLocation[];
29
- }
30
-
31
- type Warning<T extends BaseWarning> = T extends { kind: kindWithValue } ? T : Omit<T, "value">;
32
-
33
- interface Report {
34
- dependencies: ASTDeps;
35
- warnings: Warning<BaseWarning>[];
36
- idsLengthAvg: number;
37
- stringScore: number;
38
- isOneLineRequire: boolean;
39
- }
40
-
41
- interface SourceLocation {
42
- start: {
43
- line: number;
44
- column: number;
45
- };
46
- end: {
47
- line: number;
48
- column: number;
49
- }
50
- }
51
-
52
- interface Dependency {
53
- unsafe: boolean;
54
- inTry: boolean;
55
- location?: SourceLocation;
56
- }
57
-
58
- interface WarningsNames {
59
- parsingError: {
60
- code: "ast-error",
61
- i18n: "sast_warnings.ast_error",
62
- severity: "Information"
63
- },
64
- unsafeImport: {
65
- code: "unsafe-import",
66
- i18n: "sast_warnings.unsafe_import",
67
- severity: "Warning"
68
- },
69
- unsafeRegex: {
70
- code: "unsafe-regex",
71
- i18n: "sast_warnings.unsafe_regex",
72
- severity: "Warning"
73
- },
74
- unsafeStmt: {
75
- code: "unsafe-stmt",
76
- i18n: "sast_warnings.unsafe_stmt",
77
- severity: "Warning"
78
- },
79
- unsafeAssign: {
80
- code: "unsafe-assign",
81
- i18n: "sast_warnings.unsafe_assign",
82
- severity: "Warning"
83
- },
84
- encodedLiteral: {
85
- code: "encoded-literal",
86
- i18n: "sast_warnings.encoded_literal",
87
- severity: "Information"
88
- },
89
- shortIdentifiers: {
90
- code: "short-identifiers",
91
- i18n: "sast_warnings.short_identifiers",
92
- severity: "Warning"
93
- },
94
- suspiciousLiteral: {
95
- code: "suspicious-literal",
96
- i18n: "sast_warnings.suspicious_literal",
97
- severity: "Warning"
98
- },
99
- obfuscatedCode: {
100
- code: "obfuscated-code",
101
- i18n: "sast_warnings.obfuscated_code",
102
- severity: "Critical"
103
- },
104
- weakCrypto: {
105
- code: "weak-crypto",
106
- i18n: "sast_warnings.weak_crypto",
107
- severity: "Information",
108
- experimental: true
109
- }
110
- }
111
-
112
- interface RuntimeOptions {
113
- module?: boolean;
114
- isMinified?: boolean;
115
- }
116
-
117
- export function runASTAnalysis(str: string, options?: RuntimeOptions): Report;
118
-
119
- export type ReportOnFile = {
120
- ok: true,
121
- warnings: Warning<BaseWarning>[];
122
- dependencies: ASTDeps;
123
- isMinified: boolean;
124
- } | {
125
- ok: false,
126
- warnings: Warning<BaseWarning>[];
127
- }
128
-
129
- export interface RuntimeFileOptions {
130
- packageName?: string;
131
- module?: boolean;
132
- }
133
-
134
- export function runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions): Promise<ReportOnFile>;
135
-
136
- export const warnings: WarningsNames;
137
- }
138
-
139
- export = JSXRay;
140
- export as namespace JSXRay;
1
+ import {
2
+ runASTAnalysis,
3
+ runASTAnalysisOnFile,
4
+ Report,
5
+ ReportOnFile,
6
+ RuntimeFileOptions,
7
+ RuntimeOptions
8
+ } from "./types/api";
9
+ import {
10
+ Warning,
11
+ WarningDefault,
12
+ WarningLocation,
13
+ WarningName,
14
+ WarningNameWithValue
15
+ } from "./types/warnings";
16
+ import { ASTDeps } from "./types/astdeps";
17
+
18
+ declare const warnings: Record<WarningName, Pick<WarningDefault, "experimental" | "i18n" | "severity">>;
19
+
20
+ export {
21
+ warnings,
22
+ runASTAnalysis,
23
+ runASTAnalysisOnFile,
24
+ Report,
25
+ ReportOnFile,
26
+ RuntimeFileOptions,
27
+ RuntimeOptions,
28
+ ASTDeps,
29
+ Warning,
30
+ WarningDefault,
31
+ WarningLocation,
32
+ WarningName,
33
+ WarningNameWithValue
34
+ }
package/index.js CHANGED
@@ -9,6 +9,7 @@ import isMinified from "is-minified-code";
9
9
 
10
10
  // Import Internal Dependencies
11
11
  import Analysis from "./src/Analysis.js";
12
+ import { warnings } from "./src/warnings.js";
12
13
 
13
14
  export function runASTAnalysis(str, options = Object.create(null)) {
14
15
  const { module = true, isMinified = false } = options;
@@ -83,59 +84,4 @@ export async function runASTAnalysisOnFile(pathToFile, options = {}) {
83
84
  }
84
85
  }
85
86
 
86
- export const warnings = Object.freeze({
87
- parsingError: {
88
- code: "ast-error",
89
- i18n: "sast_warnings.ast_error",
90
- severity: "Information"
91
- },
92
- unsafeImport: {
93
- code: "unsafe-import",
94
- i18n: "sast_warnings.unsafe_import",
95
- severity: "Warning"
96
- },
97
- unsafeRegex: {
98
- code: "unsafe-regex",
99
- i18n: "sast_warnings.unsafe_regex",
100
- severity: "Warning"
101
- },
102
- unsafeStmt: {
103
- code: "unsafe-stmt",
104
- i18n: "sast_warnings.unsafe_stmt",
105
- severity: "Warning"
106
- },
107
- unsafeAssign: {
108
- code: "unsafe-assign",
109
- i18n: "sast_warnings.unsafe_assign",
110
- severity: "Warning"
111
- },
112
- encodedLiteral: {
113
- code: "encoded-literal",
114
- i18n: "sast_warnings.encoded_literal",
115
- severity: "Information"
116
- },
117
- shortIdentifiers: {
118
- code: "short-identifiers",
119
- i18n: "sast_warnings.short_identifiers",
120
- severity: "Warning"
121
- },
122
- suspiciousLiteral: {
123
- code: "suspicious-literal",
124
- i18n: "sast_warnings.suspicious_literal",
125
- severity: "Warning"
126
- },
127
- obfuscatedCode: {
128
- code: "obfuscated-code",
129
- i18n: "sast_warnings.obfuscated_code",
130
- severity: "Critical",
131
- experimental: true
132
- },
133
- weakCrypto: {
134
- code: "weak-crypto",
135
- i18n: "sast_warnings.weak_crypto",
136
- severity: "Information",
137
- experimental: true
138
- }
139
- });
140
-
141
-
87
+ export { warnings };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodesecure/js-x-ray",
3
- "version": "4.5.0",
3
+ "version": "5.0.0",
4
4
  "description": "JavaScript AST XRay analysis",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
package/src/Analysis.js CHANGED
@@ -2,8 +2,9 @@
2
2
  import { Utils, Literal } from "@nodesecure/sec-literal";
3
3
 
4
4
  // Import Internal Dependencies
5
- import { rootLocation, toArrayLocation, generateWarning } from "./utils.js";
6
- import { warnings as _warnings, processMainModuleRequire } from "./constants.js";
5
+ import { rootLocation, toArrayLocation } from "./utils.js";
6
+ import { generateWarning } from "./warnings.js";
7
+ import { processMainModuleRequire } from "./constants.js";
7
8
  import ASTDeps from "./ASTDeps.js";
8
9
  import { isObfuscatedCode, hasTrojanSource } from "./obfuscators/index.js";
9
10
  import { runOnProbes } from "./probes/index.js";
@@ -15,19 +16,6 @@ const kDictionaryStrParts = [
15
16
  "0123456789"
16
17
  ];
17
18
 
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
19
  export default class Analysis {
32
20
  hasDictionaryString = false;
33
21
  hasPrefixedIdentifiers = false;
@@ -56,23 +44,24 @@ export default class Analysis {
56
44
  this.literalScores = [];
57
45
  }
58
46
 
59
- addWarning(symbol, value, location = rootLocation()) {
60
- if (symbol === _warnings.encodedLiteral && this.handledEncodedLiteralValues.has(value)) {
47
+ addWarning(name, value, location = rootLocation()) {
48
+ const isEncodedLiteral = name === "encoded-literal";
49
+ if (isEncodedLiteral && this.handledEncodedLiteralValues.has(value)) {
61
50
  const index = this.handledEncodedLiteralValues.get(value);
62
51
  this.warnings[index].location.push(toArrayLocation(location));
63
52
 
64
53
  return;
65
54
  }
66
- const warningName = kWarningsNameStr[symbol];
67
- this.warnings.push(generateWarning(warningName, { value, location }));
68
- if (symbol === _warnings.encodedLiteral) {
55
+
56
+ this.warnings.push(generateWarning(name, { value, location }));
57
+ if (isEncodedLiteral) {
69
58
  this.handledEncodedLiteralValues.set(value, this.warnings.length - 1);
70
59
  }
71
60
  }
72
61
 
73
62
  analyzeSourceString(sourceString) {
74
63
  if (hasTrojanSource(sourceString)) {
75
- this.addWarning(_warnings.obfuscatedCode, "trojan-source");
64
+ this.addWarning("obfuscated-code", "trojan-source");
76
65
  }
77
66
  }
78
67
 
@@ -107,7 +96,7 @@ export default class Analysis {
107
96
  this.counter.encodedArrayValue++;
108
97
  }
109
98
  else {
110
- this.addWarning(_warnings.encodedLiteral, node.value, node.loc);
99
+ this.addWarning("encoded-literal", node.value, node.loc);
111
100
  }
112
101
  }
113
102
  }
@@ -116,7 +105,7 @@ export default class Analysis {
116
105
  this.counter.identifiers = this.identifiersName.length;
117
106
  const [isObfuscated, kind] = isObfuscatedCode(this);
118
107
  if (isObfuscated) {
119
- this.addWarning(_warnings.obfuscatedCode, kind || "unknown");
108
+ this.addWarning("obfuscated-code", kind || "unknown");
120
109
  }
121
110
 
122
111
  const identifiersLengthArr = this.identifiersName
@@ -124,10 +113,10 @@ export default class Analysis {
124
113
 
125
114
  const [idsLengthAvg, stringScore] = [sum(identifiersLengthArr), sum(this.literalScores)];
126
115
  if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) {
127
- this.addWarning(_warnings.shortIdentifiers, idsLengthAvg);
116
+ this.addWarning("short-identifiers", idsLengthAvg);
128
117
  }
129
118
  if (stringScore >= 3) {
130
- this.addWarning(_warnings.suspiciousLiteral, stringScore);
119
+ this.addWarning("suspicious-literal", stringScore);
131
120
  }
132
121
 
133
122
  return { idsLengthAvg, stringScore, warnings: this.warnings };
@@ -149,5 +138,3 @@ export default class Analysis {
149
138
  function sum(arr = []) {
150
139
  return arr.length === 0 ? 0 : (arr.reduce((prev, curr) => prev + curr, 0) / arr.length);
151
140
  }
152
-
153
- Analysis.Warnings = _warnings;
package/src/constants.js CHANGED
@@ -33,16 +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
- weakCrypto: Symbol("WeakCrypto")
48
- });
@@ -1,68 +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
- 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
+ // 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,6 +1,3 @@
1
- // Require Internal Dependencies
2
- import { warnings } from "../constants.js";
3
-
4
1
  /**
5
2
  * @description Search for ESM ImportDeclaration
6
3
  * @see https://github.com/estree/estree/blob/master/es2015.md#importdeclaration
@@ -22,7 +19,7 @@ function main(node, options) {
22
19
  // Searching for dangerous import "data:text/javascript;..." statement.
23
20
  // see: https://2ality.com/2019/10/eval-via-import.html
24
21
  if (node.source.value.startsWith("data:text/javascript")) {
25
- analysis.addWarning(warnings.unsafeImport, node.source.value, node.loc);
22
+ analysis.addWarning("unsafe-import", node.source.value, node.loc);
26
23
  }
27
24
  analysis.dependencies.add(node.source.value, node.loc);
28
25
  }
@@ -5,7 +5,7 @@ import { builtinModules } from "repl";
5
5
  import { Hex } from "@nodesecure/sec-literal";
6
6
 
7
7
  // Import Internal Dependencies
8
- import { globalParts, warnings } from "../constants.js";
8
+ import { globalParts } from "../constants.js";
9
9
 
10
10
  // CONSTANTS
11
11
  const kNodeDeps = new Set(builtinModules);
@@ -34,10 +34,10 @@ function main(node, options) {
34
34
  // then we add it to the dependencies list and we throw an unsafe-import at the current location.
35
35
  if (kNodeDeps.has(value)) {
36
36
  analysis.dependencies.add(value, node.loc);
37
- analysis.addWarning(warnings.unsafeImport, null, node.loc);
37
+ analysis.addWarning("unsafe-import", null, node.loc);
38
38
  }
39
39
  else if (globalParts.has(value) || !Hex.isSafe(node.value)) {
40
- analysis.addWarning(warnings.encodedLiteral, node.value, node.loc);
40
+ analysis.addWarning("encoded-literal", node.value, node.loc);
41
41
  }
42
42
  }
43
43
  // Else we are checking all other string with our suspect method
@@ -1,6 +1,5 @@
1
1
  // Require Internal Dependencies
2
2
  import { isLiteralRegex } from "../utils.js";
3
- import { warnings } from "../constants.js";
4
3
 
5
4
  // Require Third-party Dependencies
6
5
  import safeRegex from "safe-regex";
@@ -22,7 +21,7 @@ function main(node, options) {
22
21
 
23
22
  // We use the safe-regex package to detect whether or not regex is safe!
24
23
  if (!safeRegex(node.regex.pattern)) {
25
- analysis.addWarning(warnings.unsafeRegex, node.regex.pattern, node.loc);
24
+ analysis.addWarning("unsafe-regex", node.regex.pattern, node.loc);
26
25
  }
27
26
  }
28
27
 
@@ -1,6 +1,5 @@
1
1
  // Import Internal Dependencies
2
2
  import { isLiteralRegex } from "../utils.js";
3
- import { warnings } from "../constants.js";
4
3
 
5
4
  // Import Third-party Dependencies
6
5
  import safeRegex from "safe-regex";
@@ -25,7 +24,7 @@ function main(node, options) {
25
24
 
26
25
  // We use the safe-regex package to detect whether or not regex is safe!
27
26
  if (!safeRegex(pattern)) {
28
- analysis.addWarning(warnings.unsafeRegex, pattern, node.loc);
27
+ analysis.addWarning("unsafe-regex", pattern, node.loc);
29
28
  }
30
29
  }
31
30
 
@@ -1,160 +1,159 @@
1
- /* eslint-disable consistent-return */
2
-
3
- // Import Internal Dependencies
4
- import { isRequireGlobalMemberExpr, getMemberExprName, arrExprToString, concatBinaryExpr } from "../utils.js";
5
- import { warnings } from "../constants.js";
6
-
7
- // Import Third-party Dependencies
8
- import { Hex } from "@nodesecure/sec-literal";
9
- import { walk } from "estree-walker";
10
-
11
- function validateNode(node, analysis) {
12
- return [
13
- isRequireIdentifiers(node, analysis) ||
14
- isRequireResolve(node) ||
15
- isRequireMemberExpr(node)
16
- ];
17
- }
18
-
19
- function isRequireResolve(node) {
20
- if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
21
- return false;
22
- }
23
-
24
- return node.callee.object.name === "require" && node.callee.property.name === "resolve";
25
- }
26
-
27
- function isRequireMemberExpr(node) {
28
- if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
29
- return false;
30
- }
31
-
32
- return isRequireGlobalMemberExpr(getMemberExprName(node.callee));
33
- }
34
-
35
- function isRequireIdentifiers(node, analysis) {
36
- if (node.type !== "CallExpression") {
37
- return false;
38
- }
39
- const fullName = node.callee.type === "MemberExpression" ? getMemberExprName(node.callee) : node.callee.name;
40
-
41
- return analysis.requireIdentifiers.has(fullName);
42
- }
43
-
44
- function main(node, options) {
45
- const { analysis } = options;
46
-
47
- const arg = node.arguments[0];
48
- switch (arg.type) {
49
- // const foo = "http"; require(foo);
50
- case "Identifier":
51
- if (analysis.identifiers.has(arg.name)) {
52
- analysis.dependencies.add(analysis.identifiers.get(arg.name), node.loc);
53
- }
54
- else {
55
- analysis.addWarning(warnings.unsafeImport, null, node.loc);
56
- }
57
- break;
58
-
59
- // require("http")
60
- case "Literal":
61
- analysis.dependencies.add(arg.value, node.loc);
62
- break;
63
-
64
- // require(["ht" + "tp"])
65
- case "ArrayExpression": {
66
- const value = arrExprToString(arg.elements, analysis.identifiers).trim();
67
- if (value === "") {
68
- analysis.addWarning(warnings.unsafeImport, null, node.loc);
69
- }
70
- else {
71
- analysis.dependencies.add(value, node.loc);
72
- }
73
- break;
74
- }
75
-
76
- // require("ht" + "tp");
77
- case "BinaryExpression": {
78
- if (arg.operator !== "+") {
79
- break;
80
- }
81
-
82
- const value = concatBinaryExpr(arg, analysis.identifiers);
83
- if (value === null) {
84
- analysis.addWarning(warnings.unsafeImport, null, node.loc);
85
- }
86
- else {
87
- analysis.dependencies.add(value, node.loc);
88
- }
89
- break;
90
- }
91
-
92
- // require(Buffer.from("...", "hex").toString());
93
- case "CallExpression": {
94
- const { dependencies } = parseRequireCallExpression(arg);
95
- dependencies.forEach((depName) => analysis.dependencies.add(depName, node.loc, true));
96
-
97
- analysis.addWarning(warnings.unsafeImport, null, node.loc);
98
-
99
- // We skip walking the tree to avoid anymore warnings...
100
- return Symbol.for("skipWalk");
101
- }
102
-
103
- default:
104
- analysis.addWarning(warnings.unsafeImport, null, node.loc);
105
- }
106
- }
107
-
108
- function parseRequireCallExpression(nodeToWalk) {
109
- const dependencies = new Set();
110
-
111
- walk(nodeToWalk, {
112
- enter(node) {
113
- if (node.type !== "CallExpression" || node.arguments.length === 0) {
114
- return;
115
- }
116
-
117
- if (node.arguments[0].type === "Literal" && Hex.isHex(node.arguments[0].value)) {
118
- dependencies.add(Buffer.from(node.arguments[0].value, "hex").toString());
119
-
120
- return this.skip();
121
- }
122
-
123
- const fullName = node.callee.type === "MemberExpression" ? getMemberExprName(node.callee) : node.callee.name;
124
- switch (fullName) {
125
- case "Buffer.from": {
126
- const [element, convert] = node.arguments;
127
-
128
- if (element.type === "ArrayExpression") {
129
- const depName = arrExprToString(element);
130
- if (depName.trim() !== "") {
131
- dependencies.add(depName);
132
- }
133
- }
134
- else if (element.type === "Literal" && convert.type === "Literal" && convert.value === "hex") {
135
- const value = Buffer.from(element.value, "hex").toString();
136
- dependencies.add(value);
137
- }
138
- break;
139
- }
140
- case "require.resolve": {
141
- const [element] = node.arguments;
142
-
143
- if (element.type === "Literal") {
144
- dependencies.add(element.value);
145
- }
146
- break;
147
- }
148
- }
149
- }
150
- });
151
-
152
- return {
153
- dependencies: [...dependencies]
154
- };
155
- }
156
-
157
- export default {
158
- name: "isRequire",
159
- validateNode, main, breakOnMatch: true, breakGroup: "import"
160
- };
1
+ /* eslint-disable consistent-return */
2
+
3
+ // Import Internal Dependencies
4
+ import { isRequireGlobalMemberExpr, getMemberExprName, arrExprToString, concatBinaryExpr } from "../utils.js";
5
+
6
+ // Import Third-party Dependencies
7
+ import { Hex } from "@nodesecure/sec-literal";
8
+ 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;
21
+ }
22
+
23
+ return node.callee.object.name === "require" && node.callee.property.name === "resolve";
24
+ }
25
+
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);
41
+ }
42
+
43
+ function main(node, options) {
44
+ const { analysis } = options;
45
+
46
+ const arg = node.arguments[0];
47
+ switch (arg.type) {
48
+ // const foo = "http"; require(foo);
49
+ case "Identifier":
50
+ if (analysis.identifiers.has(arg.name)) {
51
+ analysis.dependencies.add(analysis.identifiers.get(arg.name), node.loc);
52
+ }
53
+ else {
54
+ analysis.addWarning("unsafe-import", null, node.loc);
55
+ }
56
+ break;
57
+
58
+ // require("http")
59
+ case "Literal":
60
+ analysis.dependencies.add(arg.value, node.loc);
61
+ break;
62
+
63
+ // require(["ht" + "tp"])
64
+ case "ArrayExpression": {
65
+ const value = arrExprToString(arg.elements, analysis.identifiers).trim();
66
+ if (value === "") {
67
+ analysis.addWarning("unsafe-import", null, node.loc);
68
+ }
69
+ else {
70
+ analysis.dependencies.add(value, node.loc);
71
+ }
72
+ break;
73
+ }
74
+
75
+ // require("ht" + "tp");
76
+ case "BinaryExpression": {
77
+ if (arg.operator !== "+") {
78
+ break;
79
+ }
80
+
81
+ const value = concatBinaryExpr(arg, analysis.identifiers);
82
+ if (value === null) {
83
+ analysis.addWarning("unsafe-import", null, node.loc);
84
+ }
85
+ else {
86
+ analysis.dependencies.add(value, node.loc);
87
+ }
88
+ break;
89
+ }
90
+
91
+ // require(Buffer.from("...", "hex").toString());
92
+ case "CallExpression": {
93
+ const { dependencies } = parseRequireCallExpression(arg);
94
+ dependencies.forEach((depName) => analysis.dependencies.add(depName, node.loc, true));
95
+
96
+ analysis.addWarning("unsafe-import", null, node.loc);
97
+
98
+ // We skip walking the tree to avoid anymore warnings...
99
+ return Symbol.for("skipWalk");
100
+ }
101
+
102
+ default:
103
+ analysis.addWarning("unsafe-import", null, node.loc);
104
+ }
105
+ }
106
+
107
+ function parseRequireCallExpression(nodeToWalk) {
108
+ const dependencies = new Set();
109
+
110
+ walk(nodeToWalk, {
111
+ enter(node) {
112
+ if (node.type !== "CallExpression" || node.arguments.length === 0) {
113
+ return;
114
+ }
115
+
116
+ if (node.arguments[0].type === "Literal" && Hex.isHex(node.arguments[0].value)) {
117
+ dependencies.add(Buffer.from(node.arguments[0].value, "hex").toString());
118
+
119
+ return this.skip();
120
+ }
121
+
122
+ const fullName = node.callee.type === "MemberExpression" ? getMemberExprName(node.callee) : node.callee.name;
123
+ switch (fullName) {
124
+ case "Buffer.from": {
125
+ const [element, convert] = node.arguments;
126
+
127
+ 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);
136
+ }
137
+ break;
138
+ }
139
+ case "require.resolve": {
140
+ const [element] = node.arguments;
141
+
142
+ if (element.type === "Literal") {
143
+ dependencies.add(element.value);
144
+ }
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ });
150
+
151
+ return {
152
+ dependencies: [...dependencies]
153
+ };
154
+ }
155
+
156
+ export default {
157
+ name: "isRequire",
158
+ validateNode, main, breakOnMatch: true, breakGroup: "import"
159
+ };
@@ -1,6 +1,5 @@
1
1
  // Require Internal Dependencies
2
2
  import { isUnsafeCallee } from "../utils.js";
3
- import { warnings } from "../constants.js";
4
3
 
5
4
  /**
6
5
  * @description Detect unsafe statement
@@ -15,7 +14,7 @@ function validateNode(node) {
15
14
  function main(node, options) {
16
15
  const { analysis, data: calleeName } = options;
17
16
 
18
- analysis.addWarning(warnings.unsafeStmt, calleeName, node.loc);
17
+ analysis.addWarning("unsafe-stmt", calleeName, node.loc);
19
18
  }
20
19
 
21
20
  export default {
@@ -1,104 +1,104 @@
1
- // Require Internal Dependencies
2
- import { getIdName, getMemberExprName, isUnsafeCallee, isRequireGlobalMemberExpr } from "../utils.js";
3
- import { warnings, 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(warnings.unsafeAssign, 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(warnings.unsafeAssign, 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(warnings.unsafeAssign, 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(warnings.unsafeAssign, 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
+ // 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,8 +1,5 @@
1
- // Internal Dependencies
2
- import { warnings } from "../constants.js";
3
-
4
- // Constants
5
- const weakAlgorithms = new Set(["md5", "sha1", "ripemd160", "md4", "md2"]);
1
+ // CONSTANTS
2
+ const kWeakAlgorithms = new Set(["md5", "sha1", "ripemd160", "md4", "md2"]);
6
3
 
7
4
  function validateNode(node) {
8
5
  const isCallExpression = node.type === "CallExpression";
@@ -21,10 +18,10 @@ function main(node, { analysis }) {
21
18
  const isCryptoImported = analysis.dependencies.has("crypto");
22
19
 
23
20
  if (
24
- weakAlgorithms.has(arg.value) &&
21
+ kWeakAlgorithms.has(arg.value) &&
25
22
  isCryptoImported
26
23
  ) {
27
- analysis.addWarning(warnings.weakCrypto, arg.value, node.loc);
24
+ analysis.addWarning("weak-crypto", arg.value, node.loc);
28
25
  }
29
26
  }
30
27
 
package/src/utils.js CHANGED
@@ -164,25 +164,3 @@ export function toArrayLocation(location = rootLocation()) {
164
164
 
165
165
  return [[start.line || 0, start.column || 0], [end.line || 0, end.column || 0]];
166
166
  }
167
-
168
- export function generateWarning(kind, options) {
169
- const { location, file = null, value = null } = options;
170
-
171
- if (kind === "encoded-literal") {
172
- return { kind, value, location: [toArrayLocation(location)] };
173
- }
174
-
175
- const result = { kind, location: toArrayLocation(location) };
176
- if (notNullOrUndefined(file)) {
177
- result.file = file;
178
- }
179
- if (notNullOrUndefined(value)) {
180
- result.value = value;
181
- }
182
-
183
- if (kExperimentalWarnings.has(kind)) {
184
- result.experimental = true;
185
- }
186
-
187
- return result;
188
- }
@@ -0,0 +1,70 @@
1
+ // Import Internal Dependencies
2
+ import * as utils from "./utils.js";
3
+
4
+ export const warnings = Object.freeze({
5
+ "ast-error": {
6
+ i18n: "sast_warnings.ast_error",
7
+ severity: "Information"
8
+ },
9
+ "unsafe-import": {
10
+ i18n: "sast_warnings.unsafe_import",
11
+ severity: "Warning"
12
+ },
13
+ "unsafe-regex": {
14
+ i18n: "sast_warnings.unsafe_regex",
15
+ severity: "Warning"
16
+ },
17
+ "unsafe-stmt": {
18
+ code: "unsafe-stmt",
19
+ i18n: "sast_warnings.unsafe_stmt",
20
+ severity: "Warning"
21
+ },
22
+ "unsafe-assign": {
23
+ i18n: "sast_warnings.unsafe_assign",
24
+ severity: "Warning"
25
+ },
26
+ "encoded-literal": {
27
+ i18n: "sast_warnings.encoded_literal",
28
+ severity: "Information"
29
+ },
30
+ "short-identifiers": {
31
+ i18n: "sast_warnings.short_identifiers",
32
+ severity: "Warning"
33
+ },
34
+ "suspicious-literal": {
35
+ i18n: "sast_warnings.suspicious_literal",
36
+ severity: "Warning"
37
+ },
38
+ "obfuscated-code": {
39
+ i18n: "sast_warnings.obfuscated_code",
40
+ severity: "Critical",
41
+ experimental: true
42
+ },
43
+ "weak-crypto": {
44
+ i18n: "sast_warnings.weak_crypto",
45
+ severity: "Information",
46
+ experimental: true
47
+ }
48
+ });
49
+
50
+ export function generateWarning(kind, options) {
51
+ const { location, file = null, value = null } = options;
52
+
53
+ if (kind === "encoded-literal") {
54
+ return Object.assign(
55
+ { kind, value, location: [utils.toArrayLocation(location)] },
56
+ warnings[kind]
57
+ );
58
+ }
59
+
60
+ const result = { kind, location: utils.toArrayLocation(location) };
61
+ if (utils.notNullOrUndefined(file)) {
62
+ result.file = file;
63
+ }
64
+ if (utils.notNullOrUndefined(value)) {
65
+ result.value = value;
66
+ }
67
+
68
+ return Object.assign(result, warnings[kind]);
69
+ }
70
+