diff --git a/package.json b/package.json index 3ef010fd..e79c18e6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@openally/config.typescript": "^1.0.3", "@types/node": "^24.0.2", "c8": "^10.1.2", - "glob": "^11.0.0", "iterator-matcher": "^2.1.0", "tsx": "^4.20.3", "typescript": "^5.8.3" diff --git a/tsconfig.json b/tsconfig.json index 1bab6ec7..e8853d05 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,9 @@ { "files": [], "references": [ + { + "path": "./workspaces/js-x-ray" + }, { "path": "./workspaces/sec-literal" }, diff --git a/workspaces/js-x-ray/docs/AstAnalyser.md b/workspaces/js-x-ray/docs/AstAnalyser.md index b3a0d12c..f3c010a6 100644 --- a/workspaces/js-x-ray/docs/AstAnalyser.md +++ b/workspaces/js-x-ray/docs/AstAnalyser.md @@ -31,7 +31,7 @@ interface AstAnalyserOptions { /** * @default false */ - optionalWarnings?: boolean | Iterable; + optionalWarnings?: boolean | Iterable; } ``` @@ -47,13 +47,14 @@ class AstAnalyser { options?: RuntimeOptions ) => Report; analyseFile( - pathToFile: string, + pathToFile: string | URL, options?: RuntimeFileOptions ): Promise; analyseFileSync( - pathToFile: string, + pathToFile: string | URL, options?: RuntimeFileOptions ): ReportOnFile; + prepareSource(source: string, options?: PrepareSourceOptions): string } ``` @@ -148,7 +149,14 @@ export const customProbes = [ main: (node, options) => { const { sourceFile, data: calleeName } = options; if (node.declarations[0].init.value === "danger") { - sourceFile.addWarning("unsafe-danger", calleeName, node.loc); + sourceFile.warnings.push({ + kind: "unsafe-danger", + value: calleeName, + location: node.loc, + source: "JS-X-Ray Custom Probe", + i18n: "sast_warnings.unsafe-danger", + severity: "Warning" + }); return ProbeSignals.Skip; } diff --git a/workspaces/js-x-ray/package.json b/workspaces/js-x-ray/package.json index b03f9672..68fd746b 100644 --- a/workspaces/js-x-ray/package.json +++ b/workspaces/js-x-ray/package.json @@ -5,12 +5,12 @@ "type": "module", "exports": { ".": { - "import": "./src/index.js", - "types": "./src/index.d.ts" + "import": "./dist/index.js", + "types": "./dist/index.d.ts" }, "./warnings": { - "import": "./src/warnings.js", - "types": "./types/warnings.d.ts" + "import": "./dist/warnings.js", + "types": "./dist/warnings.d.ts" }, "./package.json": "./package.json" }, @@ -18,7 +18,9 @@ "node": ">=20.0.0" }, "scripts": { - "test-only": "glob -c \"node --test-reporter=spec --test\" \"./test/**/*.spec.js\"", + "prepublishOnly": "npm run build", + "build": "tsc", + "test-only": "tsx --test-reporter=spec --test \"./test/**/*.spec.ts\"", "test": "c8 --all --src ./src -r html npm run test-only" }, "repository": { diff --git a/workspaces/js-x-ray/src/AstAnalyser.js b/workspaces/js-x-ray/src/AstAnalyser.ts similarity index 70% rename from workspaces/js-x-ray/src/AstAnalyser.js rename to workspaces/js-x-ray/src/AstAnalyser.ts index 9c13c524..f3f326b6 100644 --- a/workspaces/js-x-ray/src/AstAnalyser.js +++ b/workspaces/js-x-ray/src/AstAnalyser.ts @@ -5,23 +5,80 @@ import path from "node:path"; // Import Third-party Dependencies import { walk } from "estree-walker"; +import type { ESTree } from "meriyah"; import isMinified from "is-minified-code"; // Import Internal Dependencies -import { SourceFile } from "./SourceFile.js"; +import { generateWarning, type Warning } from "./warnings.js"; +import { + SourceFile, + type ProbesOptions, + type SourceFlags +} from "./SourceFile.js"; import { isOneLineExpressionExport } from "./utils/index.js"; -import { JsSourceParser } from "./JsSourceParser.js"; +import { JsSourceParser, type SourceParser } from "./JsSourceParser.js"; -export class AstAnalyser { +export interface Dependency { + unsafe: boolean; + inTry: boolean; + location?: null | ESTree.SourceLocation; +} + +export interface RuntimeOptions { + /** + * @default true + */ + module?: boolean; + /** + * @default false + */ + removeHTMLComments?: boolean; + /** + * @default false + */ + isMinified?: boolean; + initialize?: (sourceFile: SourceFile) => void; + finalize?: (sourceFile: SourceFile) => void; +} + +export interface RuntimeFileOptions extends Omit { + packageName?: string; +} + +export interface Report { + dependencies: Map; + warnings: Warning[]; + flags: Set; + idsLengthAvg: number; + stringScore: number; +} + +export type ReportOnFile = { + ok: true; + warnings: Warning[]; + dependencies: Map; + flags: Set; +} | { + ok: false; + warnings: Warning[]; +}; + +export interface AstAnalyserOptions extends ProbesOptions { /** - * @constructor - * @param {object} [options={}] - * @param {SourceParser} [options.customParser] - * @param {Array} [options.customProbes] - * @param {boolean} [options.skipDefaultProbes=false] - * @param {boolean | Iterable} [options.optionalWarnings=false] + * @default JsSourceParser */ - constructor(options = {}) { + customParser?: SourceParser; +} + +export interface PrepareSourceOptions { + removeHTMLComments?: boolean; +} + +export class AstAnalyser { + parser: SourceParser; + probesOptions: ProbesOptions; + + constructor(options: AstAnalyserOptions = {}) { this.parser = options.customParser ?? new JsSourceParser(); this.probesOptions = { customProbes: options.customProbes ?? [], @@ -30,7 +87,10 @@ export class AstAnalyser { }; } - analyse(str, options = Object.create(null)) { + analyse( + str: string, + options: RuntimeOptions = {} + ): Report { const { isMinified = false, module = true, @@ -54,6 +114,7 @@ export class AstAnalyser { } // we walk each AST Nodes, this is a purely synchronous I/O + // @ts-expect-error walk(body, { enter(node) { // Skip the root of the AST. @@ -61,7 +122,7 @@ export class AstAnalyser { return; } - const action = source.walk(node); + const action = source.walk(node as ESTree.Node); if (action === "skip") { this.skip(); } @@ -90,9 +151,9 @@ export class AstAnalyser { } async analyseFile( - pathToFile, - options = {} - ) { + pathToFile: string | URL, + options: RuntimeFileOptions = {} + ): Promise { try { const { packageName = null, @@ -130,20 +191,22 @@ export class AstAnalyser { flags: data.flags }; } - catch (error) { + catch (error: any) { return { ok: false, warnings: [ - { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] } + generateWarning("parsing-error", { + value: error.message + }) ] }; } } analyseFileSync( - pathToFile, - options = {} - ) { + pathToFile: string | URL, + options: RuntimeFileOptions = {} + ): ReportOnFile { try { const { packageName = null, @@ -181,22 +244,22 @@ export class AstAnalyser { flags: data.flags }; } - catch (error) { + catch (error: any) { return { ok: false, warnings: [ - { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] } + generateWarning("parsing-error", { + value: error.message + }) ] }; } } - /** - * @param {!string} source - * @param {object} options - * @param {boolean} [options.removeHTMLComments=false] - */ - prepareSource(source, options = {}) { + prepareSource( + source: string, + options: PrepareSourceOptions = {} + ): string { if (typeof source !== "string") { throw new TypeError("source must be a string"); } @@ -214,11 +277,7 @@ export class AstAnalyser { this.#removeHTMLComment(rawNoShebang) : rawNoShebang; } - /** - * @param {!string} str - * @returns {string} - */ - #removeHTMLComment(str) { + #removeHTMLComment(str: string): string { return str.replaceAll(/)/g, ""); } } diff --git a/workspaces/js-x-ray/src/Deobfuscator.js b/workspaces/js-x-ray/src/Deobfuscator.ts similarity index 62% rename from workspaces/js-x-ray/src/Deobfuscator.js rename to workspaces/js-x-ray/src/Deobfuscator.ts index f1ad6cff..a166ba5f 100644 --- a/workspaces/js-x-ray/src/Deobfuscator.js +++ b/workspaces/js-x-ray/src/Deobfuscator.ts @@ -2,6 +2,7 @@ import { getVariableDeclarationIdentifiers } from "@nodesecure/estree-ast-utils"; import { Utils, Patterns } from "@nodesecure/sec-literal"; import { match } from "ts-pattern"; +import type { ESTree } from "meriyah"; // Import Internal Dependencies import { NodeCounter } from "./NodeCounter.js"; @@ -13,7 +14,7 @@ import * as freejsobfuscator from "./obfuscators/freejsobfuscator.js"; import * as obfuscatorio from "./obfuscators/obfuscator-io.js"; // CONSTANTS -const kIdentifierNodeExtractor = extractNode("Identifier"); +const kIdentifierNodeExtractor = extractNode("Identifier"); const kDictionaryStrParts = [ "abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", @@ -21,48 +22,72 @@ const kDictionaryStrParts = [ ]; const kMinimumIdsCount = 5; +export type ObfuscatedEngine = + | "jsfuck" + | "jjencode" + | "morse" + | "freejsobfuscator" + | "obfuscator.io" + | "unknown"; + +export interface ObfuscatedIdentifier { + name: string; + type: string; +} + +export interface ObfuscatedCounters { + Identifiers: number; + VariableDeclaration?: { + const?: number; + let?: number; + var?: number; + }; + VariableDeclarator?: number; + AssignmentExpression?: number; + FunctionDeclaration?: number; + MemberExpression?: Record; + Property?: number; + UnaryExpression?: number; + DoubleUnaryExpression?: number; +} + export class Deobfuscator { deepBinaryExpression = 0; encodedArrayValue = 0; hasDictionaryString = false; hasPrefixedIdentifiers = false; - /** @type {Set} */ - morseLiterals = new Set(); + morseLiterals = new Set(); + literalScores: number[] = []; - /** @type {number[]} */ - literalScores = []; - - /** @type {({ name: string; type: string; })[]} */ - identifiers = []; + identifiers: ObfuscatedIdentifier[] = []; #counters = [ - new NodeCounter("VariableDeclaration[kind]"), - new NodeCounter("AssignmentExpression", { + new NodeCounter("VariableDeclaration[kind]"), + new NodeCounter("AssignmentExpression", { match: (node, nc) => this.#extractCounterIdentifiers(nc, node.left) }), - new NodeCounter("FunctionDeclaration", { + new NodeCounter("FunctionDeclaration", { match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id) }), - new NodeCounter("MemberExpression[computed]"), - new NodeCounter("Property", { + new NodeCounter("MemberExpression[computed]"), + new NodeCounter("Property", { filter: (node) => node.key.type === "Identifier", match: (node, nc) => this.#extractCounterIdentifiers(nc, node.key) }), - new NodeCounter("UnaryExpression", { + new NodeCounter("UnaryExpression", { name: "DoubleUnaryExpression", filter: ({ argument }) => argument.type === "UnaryExpression" && argument.argument.type === "ArrayExpression" }), - new NodeCounter("VariableDeclarator", { + new NodeCounter("VariableDeclarator", { match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id) }) ]; - /** - * @param {!NodeCounter} nc - * @param {*} node - */ - #extractCounterIdentifiers(nc, node) { + #extractCounterIdentifiers( + nc: NodeCounter, + node: ESTree.Node | null + ) { if (node === null) { return; } @@ -78,12 +103,16 @@ export class Deobfuscator { } case "Property": case "FunctionDeclaration": - this.identifiers.push({ name: node.name, type }); + if (node.type === "Identifier") { + this.identifiers.push({ name: node.name, type }); + } break; } } - analyzeString(str) { + analyzeString( + str: string + ): void { const score = Utils.stringSuspicionScore(str); if (score !== 0) { this.literalScores.push(score); @@ -102,43 +131,47 @@ export class Deobfuscator { } } - walk(node) { - const { type } = node; - - const isFunctionParams = node.type === "FunctionDeclaration" || node.type === "FunctionExpression"; - const nodesToExtract = match(type) - .with("ClassDeclaration", () => [node.id, node.superClass]) - .with("FunctionDeclaration", () => node.params) - .with("FunctionExpression", () => node.params) - .with("MethodDefinition", () => [node.key]) + walk( + node: ESTree.Node + ): void { + const nodesToExtract = match(node) + .with({ type: "ClassDeclaration" }, (node) => [node.id, node.superClass]) + .with({ type: "FunctionDeclaration" }, (node) => node.params) + .with({ type: "FunctionExpression" }, (node) => node.params) + .with({ type: "MethodDefinition" }, (node) => [node.key]) .otherwise(() => []); + const isFunctionParams = + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression"; + kIdentifierNodeExtractor( - ({ name }) => this.identifiers.push({ name, type: isFunctionParams ? "FunctionParams" : type }), + ({ name }) => this.identifiers.push({ + name, + type: isFunctionParams ? "FunctionParams" : node.type + }), nodesToExtract ); this.#counters.forEach((counter) => counter.walk(node)); } - aggregateCounters() { - const defaultValue = { - Identifiers: this.identifiers.length - }; - + aggregateCounters(): ObfuscatedCounters { return this.#counters.reduce((result, counter) => { result[counter.name] = counter.lookup ? counter.properties : counter.count; return result; - }, defaultValue); + }, { + Identifiers: this.identifiers.length + }); } #calcAvgPrefixedIdentifiers( - counters, - prefix - ) { + counters: ObfuscatedCounters, + prefix: Record + ): number { const valuesArr = Object .values(prefix) .slice() @@ -148,14 +181,14 @@ export class Deobfuscator { } const nbOfPrefixedIds = valuesArr.length === 1 ? - valuesArr.pop() : - (valuesArr.pop() + valuesArr.pop()); - const maxIds = counters.Identifiers - counters.Property; + valuesArr.pop()! : + (valuesArr.pop()! + valuesArr.pop()!); + const maxIds = counters.Identifiers - (counters.Property ?? 0); return ((nbOfPrefixedIds / maxIds) * 100); } - assertObfuscation() { + assertObfuscation(): ObfuscatedEngine | null { const counters = this.aggregateCounters(); if (jsfuck.verify(counters)) { diff --git a/workspaces/js-x-ray/src/EntryFilesAnalyser.js b/workspaces/js-x-ray/src/EntryFilesAnalyser.ts similarity index 74% rename from workspaces/js-x-ray/src/EntryFilesAnalyser.js rename to workspaces/js-x-ray/src/EntryFilesAnalyser.ts index d85c922e..55b7073b 100644 --- a/workspaces/js-x-ray/src/EntryFilesAnalyser.js +++ b/workspaces/js-x-ray/src/EntryFilesAnalyser.ts @@ -4,18 +4,39 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; // Import Third-party Dependencies -import { DiGraph } from "digraph-js"; +import { + DiGraph, + type VertexBody, + type VertexDefinition +} from "digraph-js"; // Import Internal Dependencies -import { AstAnalyser } from "./AstAnalyser.js"; +import { + AstAnalyser, + type ReportOnFile, + type RuntimeFileOptions +} from "./AstAnalyser.js"; // CONSTANTS const kDefaultExtensions = ["js", "cjs", "mjs", "node"]; -export class EntryFilesAnalyser { - #rootPath = null; +export interface EntryFilesAnalyserOptions { + astAnalyzer?: AstAnalyser; + loadExtensions?: (defaults: string[]) => string[]; + rootPath?: string | URL; + ignoreENOENT?: boolean; +} - constructor(options = {}) { +export class EntryFilesAnalyser { + #rootPath: string | null = null; + astAnalyzer: AstAnalyser; + allowedExtensions: Set; + dependencies: DiGraph>; + ignoreENOENT: boolean; + + constructor( + options: EntryFilesAnalyserOptions = {} + ) { const { astAnalyzer = new AstAnalyser(), loadExtensions, @@ -29,15 +50,15 @@ export class EntryFilesAnalyser { : kDefaultExtensions; this.allowedExtensions = new Set(rawAllowedExtensions); - this.#rootPath = options.rootPath === null ? + this.#rootPath = rootPath === null ? null : fileURLToPathExtended(rootPath); this.ignoreENOENT = ignoreENOENT; } async* analyse( - entryFiles, - options = {} - ) { + entryFiles: Iterable, + options: RuntimeFileOptions = {} + ): AsyncGenerator { this.dependencies = new DiGraph(); for (const entryFile of new Set(entryFiles)) { @@ -58,7 +79,9 @@ export class EntryFilesAnalyser { } } - #normalizeAndCleanEntryFile(file) { + #normalizeAndCleanEntryFile( + file: string | URL + ): string { let normalizedEntryFile = path.normalize( fileURLToPathExtended(file) ); @@ -69,16 +92,18 @@ export class EntryFilesAnalyser { return normalizedEntryFile; } - #getRelativeFilePath(file) { + #getRelativeFilePath( + file: string + ): string { return this.#rootPath ? path.relative(this.#rootPath, file) : file; } async* #analyseFile( - file, - relativeFile, - options + file: string, + relativeFile: string, + options: RuntimeFileOptions ) { this.dependencies.addVertex({ id: relativeFile, @@ -92,7 +117,7 @@ export class EntryFilesAnalyser { ); yield { file: relativeFile, ...report }; - if (!report.ok) { + if (!report.ok || typeof report.dependencies === "undefined") { return; } @@ -126,8 +151,8 @@ export class EntryFilesAnalyser { } async #getInternalDepPath( - filePath - ) { + filePath: string + ): Promise { const fileExtension = path.extname(filePath); if (fileExtension === "") { @@ -155,14 +180,14 @@ export class EntryFilesAnalyser { } async #fileExists( - filePath - ) { + filePath: string | URL + ): Promise { try { await fs.access(filePath, fs.constants.R_OK); return true; } - catch (error) { + catch (error: any) { if (error.code !== "ENOENT") { throw error; } @@ -173,8 +198,8 @@ export class EntryFilesAnalyser { } function fileURLToPathExtended( - file -) { + file: string | URL +): string { return file instanceof URL ? fileURLToPath(file) : file; diff --git a/workspaces/js-x-ray/src/JsSourceParser.js b/workspaces/js-x-ray/src/JsSourceParser.js deleted file mode 100644 index 2b0ac1a7..00000000 --- a/workspaces/js-x-ray/src/JsSourceParser.js +++ /dev/null @@ -1,57 +0,0 @@ -// Import Third-party Dependencies -import * as meriyah from "meriyah"; - -// CONSTANTS -const kParsingOptions = { - next: true, - loc: true, - raw: true, - jsx: true -}; - -export class JsSourceParser { - /** - * @param {object} options - * @param {boolean} options.isEcmaScriptModule - */ - parse(source, options = {}) { - const { - isEcmaScriptModule - } = options; - - try { - const { body } = meriyah.parseScript( - source, - { - ...kParsingOptions, - module: isEcmaScriptModule, - globalReturn: !isEcmaScriptModule - } - ); - - return body; - } - catch (error) { - const isIllegalReturn = error.description.includes("Illegal return statement"); - - if (error.name === "SyntaxError" && ( - error.description.includes("The import keyword") || - error.description.includes("The export keyword") || - isIllegalReturn - )) { - const { body } = meriyah.parseScript( - source, - { - ...kParsingOptions, - module: true, - globalReturn: isIllegalReturn - } - ); - - return body; - } - - throw error; - } - } -} diff --git a/workspaces/js-x-ray/src/JsSourceParser.ts b/workspaces/js-x-ray/src/JsSourceParser.ts new file mode 100644 index 00000000..717c454e --- /dev/null +++ b/workspaces/js-x-ray/src/JsSourceParser.ts @@ -0,0 +1,77 @@ +// Import Third-party Dependencies +import { + parseScript, + type ESTree, + type Options +} from "meriyah"; + +// CONSTANTS +const kParsingOptions: Partial = { + next: true, + loc: true, + raw: true, + jsx: true +}; + +export type SourceParserSyntaxError = SyntaxError & { + start: number; + end: number; + range: [number, number]; + description: string; + loc: ESTree.SourceLocation; +}; + +export interface SourceParser { + parse(source: string, options: unknown): ESTree.Statement[]; +} + +export interface JsSourceParserOptions { + isEcmaScriptModule?: boolean; +} + +export class JsSourceParser implements SourceParser { + parse( + source: string, + options: JsSourceParserOptions = {} + ): ESTree.Program["body"] { + const { + isEcmaScriptModule + } = options; + + try { + const { body } = parseScript( + source, + { + ...kParsingOptions, + module: isEcmaScriptModule, + globalReturn: !isEcmaScriptModule + } + ); + + return body; + } + catch (error: unknown) { + const syntaxError = error as SourceParserSyntaxError; + const isIllegalReturn = syntaxError.description.includes("Illegal return statement"); + + if (syntaxError.name === "SyntaxError" && ( + syntaxError.description.includes("The import keyword") || + syntaxError.description.includes("The export keyword") || + isIllegalReturn + )) { + const { body } = parseScript( + source, + { + ...kParsingOptions, + module: true, + globalReturn: isIllegalReturn + } + ); + + return body; + } + + throw error; + } + } +} diff --git a/workspaces/js-x-ray/src/NodeCounter.js b/workspaces/js-x-ray/src/NodeCounter.ts similarity index 51% rename from workspaces/js-x-ray/src/NodeCounter.js rename to workspaces/js-x-ray/src/NodeCounter.ts index 8d784630..7ac15889 100644 --- a/workspaces/js-x-ray/src/NodeCounter.js +++ b/workspaces/js-x-ray/src/NodeCounter.ts @@ -1,33 +1,42 @@ // Import Third-party Dependencies import FrequencySet from "frequency-set"; +import type { ESTree } from "meriyah"; // Import Internal Dependencies -import { isNode } from "./utils/index.js"; +import { isNode } from "./types/estree.js"; function noop() { return true; } -export class NodeCounter { - lookup = null; +export type NodeCounterFilterCallback = (node: T) => boolean; +export type NodeCounterMatchCallback = (node: T, nc: NodeCounter) => void; + +export interface NodeCounterOptions { + name?: string; + filter?: NodeCounterFilterCallback; + match?: NodeCounterMatchCallback; +} + +export class NodeCounter { + type: string; + name: string; + lookup: string | null = null; #count = 0; - #properties = null; - #filterFn = noop; - #matchFn = noop; + #properties: FrequencySet | null = null; + #filterFn: NodeCounterFilterCallback = noop; + #matchFn: NodeCounterMatchCallback = noop; /** - * @param {!string} type - * @param {Object} [options] - * @param {string} [options.name] - * @param {(node: any) => boolean} [options.filter] - * @param {(node: any, nc: NodeCounter) => void} [options.match] - * * @example * new NodeCounter("FunctionDeclaration"); * new NodeCounter("VariableDeclaration[kind]"); */ - constructor(type, options = {}) { + constructor( + type: string, + options: NodeCounterOptions = {} + ) { if (typeof type !== "string") { throw new TypeError("type must be a string"); } @@ -47,31 +56,35 @@ export class NodeCounter { this.#matchFn = options.match ?? noop; } - get count() { + get count(): number { return this.#count; } - get properties() { + get properties(): Record { return Object.fromEntries( this.#properties?.entries() ?? [] ); } - walk(node) { + walk( + node: ESTree.Node + ): void { if (!isNode(node) || node.type !== this.type) { return; } - if (!this.#filterFn(node)) { + const castedNode = node as T; + + if (!this.#filterFn(castedNode)) { return; } this.#count++; if (this.lookup === null) { - this.#matchFn(node, this); + this.#matchFn(castedNode, this); } else if (this.lookup in node) { - this.#properties.add(node[this.lookup]); - this.#matchFn(node, this); + this.#properties?.add(node[this.lookup]); + this.#matchFn(castedNode, this); } } } diff --git a/workspaces/js-x-ray/src/ProbeRunner.js b/workspaces/js-x-ray/src/ProbeRunner.ts similarity index 71% rename from workspaces/js-x-ray/src/ProbeRunner.js rename to workspaces/js-x-ray/src/ProbeRunner.ts index 9d9e0671..25ca2ee2 100644 --- a/workspaces/js-x-ray/src/ProbeRunner.js +++ b/workspaces/js-x-ray/src/ProbeRunner.ts @@ -1,6 +1,9 @@ // Import Node.js Dependencies import assert from "node:assert"; +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + // Import Internal Dependencies import isUnsafeCallee from "./probes/isUnsafeCallee.js"; import isLiteral from "./probes/isLiteral.js"; @@ -17,20 +20,27 @@ import isUnsafeCommand from "./probes/isUnsafeCommand.js"; import isSyncIO from "./probes/isSyncIO.js"; import isSerializeEnv from "./probes/isSerializeEnv.js"; -/** - * @typedef {import('./SourceFile.js').SourceFile} SourceFile - */ - -/** - * @typedef {Object} Probe - * @property {string} name - * @property {any} validateNode - * @property {(node: any, options: any) => any} main - * @property {(options: any) => void} teardown - * @property {boolean} [breakOnMatch=false] - * @property {string} [breakGroup] - * @property {(sourceFile: SourceFile) => void} [initialize] - */ +import type { SourceFile } from "./SourceFile.js"; +import type { OptionalWarningName } from "./warnings.js"; + +export type ProbeReturn = void | null | symbol; +export type ProbeInitializeCallback = (sourceFile: SourceFile) => void; +export type ProbeMainCallback = ( + node: any, + options: { sourceFile: SourceFile; data?: any; } +) => ProbeReturn; +export type ProbeTeardownCallback = (options: { sourceFile: SourceFile; }) => void; +export type ProbeValidationCallback = (node: ESTree.Node, sourceFile: SourceFile) => [boolean, any?]; + +export interface Probe { + name: string; + initialize?: ProbeInitializeCallback; + validateNode: ProbeValidationCallback | ProbeValidationCallback[]; + main: ProbeMainCallback; + teardown?: ProbeTeardownCallback; + breakOnMatch?: boolean; + breakGroup?: string; +} export const ProbeSignals = Object.freeze({ Break: Symbol.for("breakWalk"), @@ -38,13 +48,14 @@ export const ProbeSignals = Object.freeze({ }); export class ProbeRunner { + probes: Probe[]; + sourceFile: SourceFile; + /** * Note: * The order of the table has an importance/impact on the correct execution of the probes - * - * @type {Probe[]} */ - static Defaults = [ + static Defaults: Probe[] = [ isFetch, isRequire, isESMExport, @@ -60,22 +71,13 @@ export class ProbeRunner { isSerializeEnv ]; - /** - * - * @type {Record} - */ - static Optionals = { + static Optionals: Record = { "synchronous-io": isSyncIO }; - /** - * - * @param {!SourceFile} sourceFile - * @param {Probe[]} [probes=ProbeRunner.Defaults] - */ constructor( - sourceFile, - probes = ProbeRunner.Defaults + sourceFile: SourceFile, + probes: Probe[] = ProbeRunner.Defaults ) { this.sourceFile = sourceFile; @@ -100,12 +102,10 @@ export class ProbeRunner { this.probes = probes; } - /** - * @param {!Probe} probe - * @param {!any} node - * @returns {null|void|Symbol} - */ - #runProbe(probe, node) { + #runProbe( + probe: Probe, + node: ESTree.Node + ): ProbeReturn { const validationFns = Array.isArray(probe.validateNode) ? probe.validateNode : [probe.validateNode]; for (const validateNode of validationFns) { @@ -125,12 +125,13 @@ export class ProbeRunner { return null; } - walk(node) { - /** @type {Set} */ - const breakGroups = new Set(); + walk( + node: ESTree.Node + ): null | "skip" { + const breakGroups = new Set(); for (const probe of this.probes) { - if (breakGroups.has(probe.breakGroup)) { + if (probe.breakGroup && breakGroups.has(probe.breakGroup)) { continue; } diff --git a/workspaces/js-x-ray/src/SourceFile.js b/workspaces/js-x-ray/src/SourceFile.js deleted file mode 100644 index 3324ec36..00000000 --- a/workspaces/js-x-ray/src/SourceFile.js +++ /dev/null @@ -1,157 +0,0 @@ -// Import Third-party Dependencies -import { Utils, Literal } from "@nodesecure/sec-literal"; -import { VariableTracer } from "@nodesecure/tracer"; - -// Import Internal Dependencies -import { rootLocation, toArrayLocation } from "./utils/index.js"; -import { generateWarning } from "./warnings.js"; -import { ProbeRunner } from "./ProbeRunner.js"; -import { Deobfuscator } from "./Deobfuscator.js"; -import * as trojan from "./obfuscators/trojan-source.js"; - -// CONSTANTS -const kMaximumEncodedLiterals = 10; - -export class SourceFile { - inTryStatement = false; - dependencyAutoWarning = false; - deobfuscator = new Deobfuscator(); - dependencies = new Map(); - encodedLiterals = new Map(); - warnings = []; - /** @type {Set} */ - flags = new Set(); - - constructor(sourceCodeString, probesOptions = {}) { - this.tracer = new VariableTracer() - .enableDefaultTracing(); - - let probes = ProbeRunner.Defaults; - if (Array.isArray(probesOptions.customProbes) && probesOptions.customProbes.length > 0) { - probes = probesOptions.skipDefaultProbes === true ? probesOptions.customProbes : [...probes, ...probesOptions.customProbes]; - } - - if (typeof probesOptions.optionalWarnings === "boolean") { - if (probesOptions.optionalWarnings) { - probes = [...probes, ...Object.values(ProbeRunner.Optionals)]; - } - } - else { - const optionalProbes = Array.from(probesOptions.optionalWarnings ?? []) - .flatMap((warning) => ProbeRunner.Optionals[warning] ?? []); - - probes = [...probes, ...optionalProbes]; - } - - this.probesRunner = new ProbeRunner(this, probes); - - if (trojan.verify(sourceCodeString)) { - this.addWarning("obfuscated-code", "trojan-source"); - } - } - - addDependency(name, location = null, unsafe = this.dependencyAutoWarning) { - if (typeof name !== "string" || name.trim() === "") { - return; - } - - const dependencyName = name.charAt(name.length - 1) === "/" ? - name.slice(0, -1) : name; - this.dependencies.set(dependencyName, { - unsafe, - inTry: this.inTryStatement, - ...(location === null ? {} : { location }) - }); - - if (this.dependencyAutoWarning) { - this.addWarning("unsafe-import", dependencyName, location); - } - } - - addWarning(name, value, location = rootLocation()) { - const isEncodedLiteral = name === "encoded-literal"; - if (isEncodedLiteral) { - if (this.encodedLiterals.size > kMaximumEncodedLiterals) { - return; - } - - if (this.encodedLiterals.has(value)) { - const index = this.encodedLiterals.get(value); - this.warnings[index].location.push(toArrayLocation(location)); - - return; - } - } - - this.warnings.push(generateWarning(name, { value, location })); - if (isEncodedLiteral) { - this.encodedLiterals.set(value, this.warnings.length - 1); - } - } - - analyzeLiteral(node, inArrayExpr = false) { - if (typeof node.value !== "string" || Utils.isSvg(node)) { - return; - } - this.deobfuscator.analyzeString(node.value); - - const { hasHexadecimalSequence, hasUnicodeSequence, isBase64 } = Literal.defaultAnalysis(node); - if ((hasHexadecimalSequence || hasUnicodeSequence) && isBase64) { - if (inArrayExpr) { - this.deobfuscator.encodedArrayValue++; - } - else { - this.addWarning("encoded-literal", node.value, node.loc); - } - } - } - - getResult(isMinified) { - const obfuscatorName = this.deobfuscator.assertObfuscation(this); - if (obfuscatorName !== null) { - this.addWarning("obfuscated-code", obfuscatorName); - } - - const identifiersLengthArr = this.deobfuscator.identifiers - .filter((value) => value.type !== "Property" && typeof value.name === "string") - .map((value) => value.name.length); - - const [idsLengthAvg, stringScore] = [ - sum(identifiersLengthArr), - sum(this.deobfuscator.literalScores) - ]; - if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) { - this.addWarning("short-identifiers", idsLengthAvg); - } - if (stringScore >= 3) { - this.addWarning("suspicious-literal", stringScore); - } - - if (this.encodedLiterals.size > kMaximumEncodedLiterals) { - this.addWarning("suspicious-file", null); - this.warnings = this.warnings - .filter((warning) => warning.kind !== "encoded-literal"); - } - - return { idsLengthAvg, stringScore, warnings: this.warnings }; - } - - walk(node) { - this.tracer.walk(node); - this.deobfuscator.walk(node); - - // Detect TryStatement and CatchClause to known which dependency is required in a Try {} clause - if (node.type === "TryStatement" && node.handler) { - this.inTryStatement = true; - } - else if (node.type === "CatchClause") { - this.inTryStatement = false; - } - - return this.probesRunner.walk(node); - } -} - -function sum(arr = []) { - return arr.length === 0 ? 0 : (arr.reduce((prev, curr) => prev + curr, 0) / arr.length); -} diff --git a/workspaces/js-x-ray/src/SourceFile.ts b/workspaces/js-x-ray/src/SourceFile.ts new file mode 100644 index 00000000..6f21c279 --- /dev/null +++ b/workspaces/js-x-ray/src/SourceFile.ts @@ -0,0 +1,226 @@ +// Import Third-party Dependencies +import { Utils, Literal } from "@nodesecure/sec-literal"; +import { VariableTracer } from "@nodesecure/tracer"; +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { rootLocation, toArrayLocation } from "./utils/index.js"; +import { + generateWarning, + type OptionalWarningName, + type Warning +} from "./warnings.js"; +import type { Dependency } from "./AstAnalyser.js"; +import { ProbeRunner, type Probe } from "./ProbeRunner.js"; +import { Deobfuscator } from "./Deobfuscator.js"; +import * as trojan from "./obfuscators/trojan-source.js"; + +// CONSTANTS +const kMaximumEncodedLiterals = 10; + +export type SourceFlags = + | "fetch" + | "oneline-require" + | "is-minified"; + +export interface ProbesOptions { + /** + * @default [] + */ + customProbes?: Probe[]; + /** + * @default false + */ + skipDefaultProbes?: boolean; + /** + * @default false + */ + optionalWarnings?: boolean | Iterable; +} + +export class SourceFile { + tracer: VariableTracer; + probesRunner: ProbeRunner; + inTryStatement = false; + dependencyAutoWarning = false; + deobfuscator = new Deobfuscator(); + dependencies = new Map(); + encodedLiterals = new Map(); + warnings: Warning[] = []; + flags = new Set(); + + constructor( + sourceCodeString: string, + probesOptions: ProbesOptions = {} + ) { + this.tracer = new VariableTracer() + .enableDefaultTracing(); + + let probes = ProbeRunner.Defaults; + if ( + Array.isArray(probesOptions.customProbes) && + probesOptions.customProbes.length > 0 + ) { + probes = probesOptions.skipDefaultProbes === true ? + probesOptions.customProbes : + [...probes, ...probesOptions.customProbes]; + } + + if (typeof probesOptions.optionalWarnings === "boolean") { + if (probesOptions.optionalWarnings) { + probes = [...probes, ...Object.values(ProbeRunner.Optionals)]; + } + } + else { + const optionalProbes = Array.from(probesOptions.optionalWarnings ?? []) + .flatMap((warning) => ProbeRunner.Optionals[warning] ?? []); + + probes = [...probes, ...optionalProbes]; + } + + this.probesRunner = new ProbeRunner(this, probes); + + if (trojan.verify(sourceCodeString)) { + this.warnings.push( + generateWarning("obfuscated-code", { value: "trojan-source" }) + ); + } + } + + addDependency( + name: string, + location?: ESTree.SourceLocation | null, + unsafe: boolean = this.dependencyAutoWarning + ) { + if (typeof name !== "string" || name.trim() === "") { + return; + } + + const dependencyName = name.charAt(name.length - 1) === "/" ? + name.slice(0, -1) : name; + this.dependencies.set(dependencyName, { + unsafe, + inTry: this.inTryStatement, + ...(location ? { location } : {}) + }); + + if (this.dependencyAutoWarning) { + this.warnings.push( + generateWarning("unsafe-import", { + value: dependencyName, + location: location || void 0 + }) + ); + } + } + + addEncodedLiteral( + value: string, + location = rootLocation() + ) { + if (this.encodedLiterals.size > kMaximumEncodedLiterals) { + return; + } + + if (this.encodedLiterals.has(value)) { + const index = this.encodedLiterals.get(value)!; + this.warnings[index].location.push(toArrayLocation(location)); + + return; + } + + this.warnings.push(generateWarning("encoded-literal", { value, location })); + this.encodedLiterals.set(value, String(this.warnings.length - 1)); + } + + analyzeLiteral( + node: any, + inArrayExpr = false + ) { + if (typeof node.value !== "string" || Utils.isSvg(node)) { + return; + } + this.deobfuscator.analyzeString(node.value); + + const { + hasHexadecimalSequence, + hasUnicodeSequence, + isBase64 + } = Literal.defaultAnalysis(node)!; + if ((hasHexadecimalSequence || hasUnicodeSequence) && isBase64) { + if (inArrayExpr) { + this.deobfuscator.encodedArrayValue++; + } + else { + this.addEncodedLiteral(node.value, node.loc); + } + } + } + + getResult( + isMinified: boolean + ): { idsLengthAvg: number; stringScore: number; warnings: Warning[]; } { + const obfuscatorName = this.deobfuscator.assertObfuscation(); + if (obfuscatorName !== null) { + this.warnings.push( + generateWarning("obfuscated-code", { value: obfuscatorName }) + ); + } + + const identifiersLengthArr = this.deobfuscator.identifiers + .filter((value) => value.type !== "Property" && typeof value.name === "string") + .map((value) => value.name.length); + + const [idsLengthAvg, stringScore] = [ + sum(identifiersLengthArr), + sum(this.deobfuscator.literalScores) + ]; + if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) { + this.warnings.push( + generateWarning("short-identifiers", { value: String(idsLengthAvg) }) + ); + } + if (stringScore >= 3) { + this.warnings.push( + generateWarning("suspicious-literal", { value: String(stringScore) }) + ); + } + + if (this.encodedLiterals.size > kMaximumEncodedLiterals) { + this.warnings.push( + generateWarning("suspicious-file", { value: null }) + ); + this.warnings = this.warnings + .filter((warning) => warning.kind !== "encoded-literal"); + } + + return { + idsLengthAvg, + stringScore, + warnings: this.warnings + }; + } + + walk( + node: ESTree.Node + ): null | "skip" { + this.tracer.walk(node); + this.deobfuscator.walk(node); + + // Detect TryStatement and CatchClause to known which dependency is required in a Try {} clause + if (node.type === "TryStatement" && node.handler) { + this.inTryStatement = true; + } + else if (node.type === "CatchClause") { + this.inTryStatement = false; + } + + return this.probesRunner.walk(node); + } +} + +function sum( + arr: number[] = [] +): number { + return arr.length === 0 ? 0 : (arr.reduce((prev, curr) => prev + curr, 0) / arr.length); +} diff --git a/workspaces/js-x-ray/src/index.d.ts b/workspaces/js-x-ray/src/index.d.ts deleted file mode 100644 index 735f1bf5..00000000 --- a/workspaces/js-x-ray/src/index.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - AstAnalyser, - AstAnalyserOptions, - - EntryFilesAnalyser, - EntryFilesAnalyserOptions, - - SourceParser, - JsSourceParser, - Report, - ReportOnFile, - RuntimeFileOptions, - RuntimeOptions, - SourceLocation, - Dependency -} from "./types/api.js"; -import { - Warning, - WarningDefault, - WarningLocation, - WarningName, - WarningNameWithValue -} from "./types/warnings.js"; - -declare const warnings: Record>; - -export { - warnings, - AstAnalyser, - AstAnalyserOptions, - EntryFilesAnalyser, - EntryFilesAnalyserOptions, - JsSourceParser, - SourceParser, - Report, - ReportOnFile, - RuntimeFileOptions, - RuntimeOptions, - SourceLocation, - Dependency, - Warning, - WarningDefault, - WarningLocation, - WarningName, - WarningNameWithValue -} diff --git a/workspaces/js-x-ray/src/index.js b/workspaces/js-x-ray/src/index.js deleted file mode 100644 index 6dab697a..00000000 --- a/workspaces/js-x-ray/src/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { warnings } from "./warnings.js"; -export { JsSourceParser } from "./JsSourceParser.js"; -export { AstAnalyser } from "./AstAnalyser.js"; -export { EntryFilesAnalyser } from "./EntryFilesAnalyser.js"; diff --git a/workspaces/js-x-ray/src/index.ts b/workspaces/js-x-ray/src/index.ts new file mode 100644 index 00000000..3d499a39 --- /dev/null +++ b/workspaces/js-x-ray/src/index.ts @@ -0,0 +1,5 @@ +export * from "./warnings.js"; +export * from "./JsSourceParser.js"; +export * from "./AstAnalyser.js"; +export * from "./EntryFilesAnalyser.js"; +export * from "./SourceFile.js"; diff --git a/workspaces/js-x-ray/src/obfuscators/freejsobfuscator.js b/workspaces/js-x-ray/src/obfuscators/freejsobfuscator.js deleted file mode 100644 index 7b3f843c..00000000 --- a/workspaces/js-x-ray/src/obfuscators/freejsobfuscator.js +++ /dev/null @@ -1,9 +0,0 @@ -// Import Third-party Dependencies -import { Utils } from "@nodesecure/sec-literal"; - -export function verify(identifiers, prefix) { - const pValue = Object.keys(prefix).pop(); - const regexStr = `^${Utils.escapeRegExp(pValue)}[a-zA-Z]{1,2}[0-9]{0,2}$`; - - return identifiers.every(({ name }) => new RegExp(regexStr).test(name)); -} diff --git a/workspaces/js-x-ray/src/obfuscators/freejsobfuscator.ts b/workspaces/js-x-ray/src/obfuscators/freejsobfuscator.ts new file mode 100644 index 00000000..850a6c07 --- /dev/null +++ b/workspaces/js-x-ray/src/obfuscators/freejsobfuscator.ts @@ -0,0 +1,17 @@ +// Import Third-party Dependencies +import { Utils } from "@nodesecure/sec-literal"; + +// Import Internal Dependencies +import { + type ObfuscatedIdentifier +} from "../Deobfuscator.js"; + +export function verify( + identifiers: ObfuscatedIdentifier[], + prefix: Record +) { + const pValue = Object.keys(prefix).pop()!; + const regexStr = `^${Utils.escapeRegExp(pValue)}[a-zA-Z]{1,2}[0-9]{0,2}$`; + + return identifiers.every(({ name }) => new RegExp(regexStr).test(name)); +} diff --git a/workspaces/js-x-ray/src/obfuscators/jjencode.js b/workspaces/js-x-ray/src/obfuscators/jjencode.ts similarity index 55% rename from workspaces/js-x-ray/src/obfuscators/jjencode.js rename to workspaces/js-x-ray/src/obfuscators/jjencode.ts index e1914487..510ca7a3 100644 --- a/workspaces/js-x-ray/src/obfuscators/jjencode.js +++ b/workspaces/js-x-ray/src/obfuscators/jjencode.ts @@ -1,14 +1,26 @@ // Import Internal Dependencies import { notNullOrUndefined } from "../utils/index.js"; +import { + type ObfuscatedIdentifier, + type ObfuscatedCounters +} from "../Deobfuscator.js"; // CONSTANTS const kJJRegularSymbols = new Set(["$", "_"]); -export function verify(identifiers, counters) { - if (counters.VariableDeclarator > 0 || counters.FunctionDeclaration > 0) { +export function verify( + identifiers: ObfuscatedIdentifier[], + counters: ObfuscatedCounters +) { + if ( + (counters.VariableDeclarator && counters.VariableDeclarator > 0) || + (counters.FunctionDeclaration && counters.FunctionDeclaration > 0) + ) { return false; } - if (counters.AssignmentExpression > counters.Property) { + if ( + (counters.AssignmentExpression ?? 0) > (counters.Property ?? 0) + ) { return false; } diff --git a/workspaces/js-x-ray/src/obfuscators/jsfuck.js b/workspaces/js-x-ray/src/obfuscators/jsfuck.js deleted file mode 100644 index d1982894..00000000 --- a/workspaces/js-x-ray/src/obfuscators/jsfuck.js +++ /dev/null @@ -1,11 +0,0 @@ -// CONSTANTS -const kJSFuckMinimumDoubleUnaryExpr = 5; - -export function verify(counters) { - const hasZeroAssign = counters.AssignmentExpression === 0 - && counters.FunctionDeclaration === 0 - && counters.Property === 0 - && counters.VariableDeclarator === 0; - - return hasZeroAssign && counters.DoubleUnaryExpression >= kJSFuckMinimumDoubleUnaryExpr; -} diff --git a/workspaces/js-x-ray/src/obfuscators/jsfuck.ts b/workspaces/js-x-ray/src/obfuscators/jsfuck.ts new file mode 100644 index 00000000..cbfa9324 --- /dev/null +++ b/workspaces/js-x-ray/src/obfuscators/jsfuck.ts @@ -0,0 +1,19 @@ +// Import Internal Dependencies +import { + type ObfuscatedCounters +} from "../Deobfuscator.js"; + +// CONSTANTS +const kJSFuckMinimumDoubleUnaryExpr = 5; + +export function verify( + counters: ObfuscatedCounters +) { + const hasZeroAssign = counters.AssignmentExpression === 0 + && counters.FunctionDeclaration === 0 + && counters.Property === 0 + && counters.VariableDeclarator === 0; + + return hasZeroAssign && + (counters.DoubleUnaryExpression ?? 0) >= kJSFuckMinimumDoubleUnaryExpr; +} diff --git a/workspaces/js-x-ray/src/obfuscators/obfuscator-io.js b/workspaces/js-x-ray/src/obfuscators/obfuscator-io.ts similarity index 54% rename from workspaces/js-x-ray/src/obfuscators/obfuscator-io.js rename to workspaces/js-x-ray/src/obfuscators/obfuscator-io.ts index 3e66c22c..6d0ae97b 100644 --- a/workspaces/js-x-ray/src/obfuscators/obfuscator-io.js +++ b/workspaces/js-x-ray/src/obfuscators/obfuscator-io.ts @@ -1,5 +1,17 @@ -export function verify(deobfuscator, counters) { - if ((counters.MemberExpression?.false ?? 0) > 0) { +// Import Internal Dependencies +import { + Deobfuscator, + type ObfuscatedCounters +} from "../Deobfuscator.js"; + +export function verify( + deobfuscator: Deobfuscator, + counters: ObfuscatedCounters +) { + if ( + (counters.MemberExpression?.false ?? 0) > 0 || + !counters.DoubleUnaryExpression + ) { return false; } diff --git a/workspaces/js-x-ray/src/obfuscators/trojan-source.js b/workspaces/js-x-ray/src/obfuscators/trojan-source.ts similarity index 88% rename from workspaces/js-x-ray/src/obfuscators/trojan-source.js rename to workspaces/js-x-ray/src/obfuscators/trojan-source.ts index bdc1b180..84ede59f 100644 --- a/workspaces/js-x-ray/src/obfuscators/trojan-source.js +++ b/workspaces/js-x-ray/src/obfuscators/trojan-source.ts @@ -17,7 +17,9 @@ const kUnsafeUnicodeControlCharacters = [ "\u061C" ]; -export function verify(sourceString) { +export function verify( + sourceString: string +): boolean { for (const unsafeCharacter of kUnsafeUnicodeControlCharacters) { if (sourceString.includes(unsafeCharacter)) { return true; diff --git a/workspaces/js-x-ray/src/probes/isArrayExpression.js b/workspaces/js-x-ray/src/probes/isArrayExpression.ts similarity index 61% rename from workspaces/js-x-ray/src/probes/isArrayExpression.js rename to workspaces/js-x-ray/src/probes/isArrayExpression.ts index ff624851..7353ff42 100644 --- a/workspaces/js-x-ray/src/probes/isArrayExpression.js +++ b/workspaces/js-x-ray/src/probes/isArrayExpression.ts @@ -1,8 +1,12 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + // Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; import { extractNode } from "../utils/index.js"; // CONSTANTS -const kLiteralExtractor = extractNode("Literal"); +const kLiteralExtractor = extractNode("Literal"); /** * @description Search for ArrayExpression AST Node (Commonly known as JS Arrays) @@ -11,13 +15,18 @@ const kLiteralExtractor = extractNode("Literal"); * @example * ["foo", "bar", 1] */ -function validateNode(node) { +function validateNode( + node: ESTree.Node +): [boolean, any?] { return [ node.type === "ArrayExpression" ]; } -function main(node, { sourceFile }) { +function main( + node: ESTree.ArrayExpression, + { sourceFile }: { sourceFile: SourceFile; } +) { kLiteralExtractor( (literalNode) => sourceFile.analyzeLiteral(literalNode, true), node.elements diff --git a/workspaces/js-x-ray/src/probes/isBinaryExpression.js b/workspaces/js-x-ray/src/probes/isBinaryExpression.ts similarity index 72% rename from workspaces/js-x-ray/src/probes/isBinaryExpression.js rename to workspaces/js-x-ray/src/probes/isBinaryExpression.ts index 27794110..ff9e02fb 100644 --- a/workspaces/js-x-ray/src/probes/isBinaryExpression.js +++ b/workspaces/js-x-ray/src/probes/isBinaryExpression.ts @@ -1,3 +1,9 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; + /** * @description Search for BinaryExpression AST Node. * @@ -5,16 +11,24 @@ * @example * 5 + 5 + 10 */ -function validateNode(node) { +function validateNode( + node: ESTree.Node +): [boolean, any?] { return [ node.type === "BinaryExpression" ]; } -function main(node, options) { +function main( + node: ESTree.BinaryExpression, + options: { sourceFile: SourceFile; } +) { const { sourceFile } = options; - const [binaryExprDeepness, hasUnaryExpression] = walkBinaryExpression(node); + const [ + binaryExprDeepness, + hasUnaryExpression + ] = walkBinaryExpression(node); if (binaryExprDeepness >= 3 && hasUnaryExpression) { sourceFile.deobfuscator.deepBinaryExpression++; } @@ -27,10 +41,15 @@ function main(node, options) { * @example * 0x1*-0x12df+-0x1fb9*-0x1+0x2*-0x66d */ -function walkBinaryExpression(expr, level = 1) { +function walkBinaryExpression( + expr: ESTree.BinaryExpression, + level = 1 +): [number, boolean] { const [lt, rt] = [expr.left.type, expr.right.type]; let hasUnaryExpression = lt === "UnaryExpression" || rt === "UnaryExpression"; - let currentLevel = lt === "BinaryExpression" || rt === "BinaryExpression" ? level + 1 : level; + let currentLevel = lt === "BinaryExpression" || rt === "BinaryExpression" ? + level + 1 : + level; for (const currExpr of [expr.left, expr.right]) { if (currExpr.type === "BinaryExpression") { diff --git a/workspaces/js-x-ray/src/probes/isESMExport.js b/workspaces/js-x-ray/src/probes/isESMExport.js deleted file mode 100644 index 03605eb0..00000000 --- a/workspaces/js-x-ray/src/probes/isESMExport.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @description Search for ESM Export - * - * @example - * export { bar } from "./foo.js"; - * export * from "./bar.js"; - */ -function validateNode(node) { - return [ - /** - * We must be sure that the source property is a Literal to not fall in a trap - * export const foo = "bar"; - */ - (node.type === "ExportNamedDeclaration" && node.source?.type === "Literal") || - node.type === "ExportAllDeclaration" - ]; -} - -function main(node, { sourceFile }) { - sourceFile.addDependency( - node.source.value, - node.loc - ); -} - -export default { - name: "isESMExport", - validateNode, - main, - breakOnMatch: true -}; diff --git a/workspaces/js-x-ray/src/probes/isESMExport.ts b/workspaces/js-x-ray/src/probes/isESMExport.ts new file mode 100644 index 00000000..119dc2cd --- /dev/null +++ b/workspaces/js-x-ray/src/probes/isESMExport.ts @@ -0,0 +1,50 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import type { Literal } from "../types/estree.js"; + +/** + * @description Search for ESM Export + * + * @example + * export { bar } from "./foo.js"; + * export * from "./bar.js"; + */ +function validateNode( + node: ESTree.Node +): [boolean, any?] { + if ( + node.type !== "ExportNamedDeclaration" && + node.type !== "ExportAllDeclaration" + ) { + return [false]; + } + + return [ + node.source !== null && + node.source.type === "Literal" && + typeof node.source.value === "string" + ]; +} + +function main( + node: ( + | ESTree.ExportNamedDeclaration + | ESTree.ExportAllDeclaration + ) & { source: Literal; }, + { sourceFile }: { sourceFile: SourceFile; } +) { + sourceFile.addDependency( + node.source.value, + node.loc + ); +} + +export default { + name: "isESMExport", + validateNode, + main, + breakOnMatch: true +}; diff --git a/workspaces/js-x-ray/src/probes/isFetch.js b/workspaces/js-x-ray/src/probes/isFetch.ts similarity index 53% rename from workspaces/js-x-ray/src/probes/isFetch.js rename to workspaces/js-x-ray/src/probes/isFetch.ts index 2a6b4029..2025013e 100644 --- a/workspaces/js-x-ray/src/probes/isFetch.js +++ b/workspaces/js-x-ray/src/probes/isFetch.ts @@ -1,13 +1,22 @@ // Import Third-party Dependencies import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; +import type { ESTree } from "meriyah"; -function validateNode(node) { +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; + +function validateNode( + node: ESTree.Node +): [boolean, any?] { const id = getCallExpressionIdentifier(node); return [id === "fetch"]; } -function main(_node, { sourceFile }) { +function main( + _node: ESTree.Node, + { sourceFile }: { sourceFile: SourceFile; } +) { sourceFile.flags.add("fetch"); } diff --git a/workspaces/js-x-ray/src/probes/isImportDeclaration.js b/workspaces/js-x-ray/src/probes/isImportDeclaration.js deleted file mode 100644 index 3412fabf..00000000 --- a/workspaces/js-x-ray/src/probes/isImportDeclaration.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @description Search for ESM ImportDeclaration - * @see https://github.com/estree/estree/blob/master/es2015.md#importdeclaration - * @example - * import * as foo from "bar"; - * import fs from "fs"; - * import "make-promises-safe"; - */ -function validateNode(node) { - return [ - // Note: the source property is the right-side Literal part of the Import - ["ImportDeclaration", "ImportExpression"].includes(node.type) && node.source.type === "Literal" - ]; -} - -function main(node, options) { - const { sourceFile } = options; - - // Searching for dangerous import "data:text/javascript;..." statement. - // see: https://2ality.com/2019/10/eval-via-import.html - if (node.source.value.startsWith("data:text/javascript")) { - sourceFile.addWarning("unsafe-import", node.source.value, node.loc); - } - sourceFile.addDependency(node.source.value, node.loc); -} - -export default { - name: "isImportDeclaration", - validateNode, - main, - breakOnMatch: true, - breakGroup: "import" -}; diff --git a/workspaces/js-x-ray/src/probes/isImportDeclaration.ts b/workspaces/js-x-ray/src/probes/isImportDeclaration.ts new file mode 100644 index 00000000..ca8031f8 --- /dev/null +++ b/workspaces/js-x-ray/src/probes/isImportDeclaration.ts @@ -0,0 +1,58 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; +import type { Literal } from "../types/estree.js"; + +/** + * @description Search for ESM ImportDeclaration + * @see https://github.com/estree/estree/blob/master/es2015.md#importdeclaration + * @example + * import * as foo from "bar"; + * import fs from "fs"; + * import "make-promises-safe"; + */ +function validateNode( + node: ESTree.Node +): [boolean, any?] { + if (node.type !== "ImportDeclaration" && node.type !== "ImportExpression") { + return [false]; + } + + // Note: the source property is the right-side Literal part of the Import + return [ + node.source.type === "Literal" && + typeof node.source.value === "string" + ]; +} + +function main( + node: ( + | ESTree.ImportDeclaration + | ESTree.ImportExpression + ) & { source: Literal; }, + options: { sourceFile: SourceFile; } +) { + const { sourceFile } = options; + + // Searching for dangerous import "data:text/javascript;..." statement. + // see: https://2ality.com/2019/10/eval-via-import.html + if (node.source.value.startsWith("data:text/javascript")) { + sourceFile.warnings.push( + generateWarning( + "unsafe-import", { value: node.source.value, location: node.loc } + ) + ); + } + sourceFile.addDependency(node.source.value, node.loc); +} + +export default { + name: "isImportDeclaration", + validateNode, + main, + breakOnMatch: true, + breakGroup: "import" +}; diff --git a/workspaces/js-x-ray/src/probes/isLiteral.js b/workspaces/js-x-ray/src/probes/isLiteral.ts similarity index 74% rename from workspaces/js-x-ray/src/probes/isLiteral.js rename to workspaces/js-x-ray/src/probes/isLiteral.ts index 068240b5..155721fe 100644 --- a/workspaces/js-x-ray/src/probes/isLiteral.js +++ b/workspaces/js-x-ray/src/probes/isLiteral.ts @@ -3,6 +3,12 @@ import { builtinModules } from "node:module"; // Import Third-party Dependencies import { Hex } from "@nodesecure/sec-literal"; +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; +import type { Literal } from "../types/estree.js"; const kMapRegexIps = Object.freeze({ // eslint-disable-next-line @stylistic/max-len @@ -18,20 +24,27 @@ const kShadyLinkRegExps = [ /(http[s]?:\/\/(bit\.ly|ipinfo\.io|httpbin\.org).*)$/, /(http[s]?:\/\/.*\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|cd|ws|icu|cam|uno|email|stream))$/ ]; + /** * @description Search for Literal AST Node * @see https://github.com/estree/estree/blob/master/es5.md#literal * @example * "foobar" */ -function validateNode(node) { +function validateNode( + node: ESTree.Node +): [boolean, any?] { return [ node.type === "Literal" && typeof node.value === "string" ]; } -function main(node, options) { +function main( + node: Literal, + options: { sourceFile: SourceFile; } +) { const { sourceFile } = options; + const location = node.loc ?? void 0; // We are searching for value obfuscated as hex of a minimum length of 4. if (/^[0-9A-Fa-f]{4,}$/g.test(node.value)) { @@ -42,17 +55,25 @@ function main(node, options) { // then we add it to the dependencies list and we throw an unsafe-import at the current location. if (kNodeDeps.has(value)) { sourceFile.addDependency(value, node.loc); - sourceFile.addWarning("unsafe-import", null, node.loc); + sourceFile.warnings.push( + generateWarning( + "unsafe-import", { value: null, location } + ) + ); } else if (value === "require" || !Hex.isSafe(node.value)) { - sourceFile.addWarning("encoded-literal", node.value, node.loc); + sourceFile.addEncodedLiteral(node.value, location); } } // Else we are checking all other string with our suspect method else { for (const regex of kShadyLinkRegExps) { if (regex.test(node.value)) { - sourceFile.addWarning("shady-link", node.value, node.loc); + sourceFile.warnings.push( + generateWarning( + "shady-link", { value: node.value, location } + ) + ); return; } diff --git a/workspaces/js-x-ray/src/probes/isLiteralRegex.js b/workspaces/js-x-ray/src/probes/isLiteralRegex.ts similarity index 50% rename from workspaces/js-x-ray/src/probes/isLiteralRegex.js rename to workspaces/js-x-ray/src/probes/isLiteralRegex.ts index b4d0a728..9fd0721c 100644 --- a/workspaces/js-x-ray/src/probes/isLiteralRegex.js +++ b/workspaces/js-x-ray/src/probes/isLiteralRegex.ts @@ -1,6 +1,10 @@ // Import Third-party Dependencies -import { isLiteralRegex } from "@nodesecure/estree-ast-utils"; import safeRegex from "safe-regex"; +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; /** * @description Search for RegExpLiteral AST Node @@ -8,18 +12,25 @@ import safeRegex from "safe-regex"; * @example * /hello/ */ -function validateNode(node) { +function validateNode( + node: ESTree.Node +): [boolean, any?] { return [ - isLiteralRegex(node) + node.type === "Literal" && "regex" in node ]; } -function main(node, options) { +function main( + node: ESTree.RegExpLiteral, + options: { sourceFile: SourceFile; } +) { const { sourceFile } = options; // We use the safe-regex package to detect whether or not regex is safe! if (!safeRegex(node.regex.pattern)) { - sourceFile.addWarning("unsafe-regex", node.regex.pattern, node.loc); + sourceFile.warnings.push( + generateWarning("unsafe-regex", { value: node.regex.pattern, location: node.loc }) + ); } } diff --git a/workspaces/js-x-ray/src/probes/isRegexObject.js b/workspaces/js-x-ray/src/probes/isRegexObject.ts similarity index 51% rename from workspaces/js-x-ray/src/probes/isRegexObject.js rename to workspaces/js-x-ray/src/probes/isRegexObject.ts index 50cbaa68..aef7502c 100644 --- a/workspaces/js-x-ray/src/probes/isRegexObject.js +++ b/workspaces/js-x-ray/src/probes/isRegexObject.ts @@ -1,6 +1,11 @@ // Import Third-party Dependencies -import { isLiteralRegex } from "@nodesecure/estree-ast-utils"; import safeRegex from "safe-regex"; +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; +import type { Literal, RegExpLiteral } from "../types/estree.js"; /** * @description Search for Regex Object constructor. @@ -8,16 +13,27 @@ import safeRegex from "safe-regex"; * @example * new RegExp("..."); */ -function validateNode(node) { +function validateNode( + node: ESTree.Node +): [boolean, any?] { return [ isRegexConstructor(node) && node.arguments.length > 0 ]; } -function main(node, options) { +function main( + node: ESTree.NewExpression & { + callee: ESTree.Identifier; + }, + options: { sourceFile: SourceFile; } +) { const { sourceFile } = options; - const arg = node.arguments[0]; + const arg = node.arguments.at(0) as Literal | RegExpLiteral; + if (!arg) { + return; + } + /** * Note: RegExp Object can contain a RegExpLiteral * @see https://github.com/estree/estree/blob/master/es5.md#regexpliteral @@ -25,15 +41,21 @@ function main(node, options) { * @example * new RegExp(/^foo/) */ - const pattern = isLiteralRegex(arg) ? arg.regex.pattern : arg.value; + const pattern = arg.type === "Literal" && "regex" in arg ? + arg.regex.pattern : + arg.value; // We use the safe-regex package to detect whether or not regex is safe! if (!safeRegex(pattern)) { - sourceFile.addWarning("unsafe-regex", pattern, node.loc); + sourceFile.warnings.push( + generateWarning("unsafe-regex", { value: pattern, location: node.loc }) + ); } } -function isRegexConstructor(node) { +function isRegexConstructor( + node: ESTree.Node +): node is ESTree.NewExpression { if (node.type !== "NewExpression" || node.callee.type !== "Identifier") { return false; } diff --git a/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.js b/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.js deleted file mode 100644 index 4a58d3e8..00000000 --- a/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.js +++ /dev/null @@ -1,93 +0,0 @@ -// Import Node.js Dependencies -import path from "node:path"; - -// Import Third-party Dependencies -import { Hex } from "@nodesecure/sec-literal"; -import { walk as doWalk } from "estree-walker"; -import { - arrayExpressionToString, - getMemberExpressionIdentifier, - getCallExpressionArguments -} from "@nodesecure/estree-ast-utils"; - -export class RequireCallExpressionWalker { - constructor(tracer) { - this.tracer = tracer; - this.dependencies = new Set(); - this.triggerWarning = true; - } - - walk(nodeToWalk) { - this.dependencies = new Set(); - this.triggerWarning = true; - - // we need the `this` context of doWalk.enter - const self = this; - doWalk(nodeToWalk, { - enter(node) { - if (node.type !== "CallExpression" || node.arguments.length === 0) { - return; - } - - const rootArgument = node.arguments.at(0); - if (rootArgument.type === "Literal" && Hex.isHex(rootArgument.value)) { - self.dependencies.add(Buffer.from(rootArgument.value, "hex").toString()); - this.skip(); - - return; - } - - const fullName = node.callee.type === "MemberExpression" ? - [...getMemberExpressionIdentifier(node.callee)].join(".") : - node.callee.name; - const tracedFullName = self.tracer.getDataFromIdentifier(fullName)?.identifierOrMemberExpr ?? fullName; - switch (tracedFullName) { - case "atob": - self.#handleAtob(node); - break; - case "Buffer.from": - self.#handleBufferFrom(node); - break; - case "require.resolve": - self.#handleRequireResolve(rootArgument); - break; - case "path.join": - self.#handlePathJoin(node); - break; - } - } - }); - - return { dependencies: this.dependencies, triggerWarning: this.triggerWarning }; - } - - #handleAtob(node) { - const nodeArguments = getCallExpressionArguments(node, { tracer: this.tracer }); - if (nodeArguments !== null) { - this.dependencies.add(Buffer.from(nodeArguments.at(0), "base64").toString()); - } - } - - #handleBufferFrom(node) { - const [element] = node.arguments; - if (element.type === "ArrayExpression") { - const depName = [...arrayExpressionToString(element)].join("").trim(); - this.dependencies.add(depName); - } - } - - #handleRequireResolve(rootArgument) { - if (rootArgument.type === "Literal") { - this.dependencies.add(rootArgument.value); - } - } - - #handlePathJoin(node) { - if (!node.arguments.every((arg) => arg.type === "Literal" && typeof arg.value === "string")) { - return; - } - const constructedPath = path.posix.join(...node.arguments.map((arg) => arg.value)); - this.dependencies.add(constructedPath); - this.triggerWarning = false; - } -} diff --git a/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.ts b/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.ts new file mode 100644 index 00000000..7b0bcd43 --- /dev/null +++ b/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.ts @@ -0,0 +1,139 @@ +// Import Node.js Dependencies +import path from "node:path"; + +// Import Third-party Dependencies +import { Hex } from "@nodesecure/sec-literal"; +import { walk as doWalk } from "estree-walker"; +import { + arrayExpressionToString, + getMemberExpressionIdentifier, + getCallExpressionArguments +} from "@nodesecure/estree-ast-utils"; +import type { ESTree } from "meriyah"; +import { VariableTracer } from "@nodesecure/tracer"; + +// Import Internal Dependencies +import { + isLiteral, + isCallExpression +} from "../../types/estree.js"; + +export class RequireCallExpressionWalker { + tracer: VariableTracer; + dependencies = new Set(); + triggerWarning = true; + + constructor( + tracer: VariableTracer + ) { + this.tracer = tracer; + } + + reset() { + this.dependencies.clear(); + this.triggerWarning = true; + } + + walk( + callExprNode: ESTree.CallExpression + ) { + this.reset(); + + // we need the `this` context of doWalk.enter + const self = this; + // @ts-expect-error + doWalk(callExprNode, { + enter(node: any) { + if ( + !isCallExpression(node) || + node.arguments.length === 0 + ) { + return; + } + + const castedNode = node as ESTree.CallExpression; + const rootArgument = castedNode.arguments.at(0)!; + if ( + rootArgument.type === "Literal" && + typeof rootArgument.value === "string" && + Hex.isHex(rootArgument.value) + ) { + self.dependencies.add(Buffer.from(rootArgument.value, "hex").toString()); + this.skip(); + + return; + } + + const fullName = castedNode.callee.type === "MemberExpression" ? + [...getMemberExpressionIdentifier(castedNode.callee)].join(".") : + castedNode.callee.name; + const tracedFullName = self.tracer.getDataFromIdentifier(fullName)?.identifierOrMemberExpr ?? fullName; + switch (tracedFullName) { + case "atob": + self.#handleAtob(castedNode); + break; + case "Buffer.from": + self.#handleBufferFrom(castedNode); + break; + case "require.resolve": + self.#handleRequireResolve(rootArgument); + break; + case "path.join": + self.#handlePathJoin(castedNode); + break; + } + } + }); + + return { + dependencies: this.dependencies, + triggerWarning: this.triggerWarning + }; + } + + #handleAtob( + node: ESTree.CallExpression + ): void { + const nodeArguments = getCallExpressionArguments( + node, + { tracer: this.tracer } + ); + if (nodeArguments !== null && nodeArguments.length > 0) { + this.dependencies.add( + Buffer.from(nodeArguments.at(0)!, "base64").toString() + ); + } + } + + #handleBufferFrom( + node: ESTree.CallExpression + ) { + const [element] = node.arguments; + if (element.type === "ArrayExpression") { + const depName = [...arrayExpressionToString(element)].join("").trim(); + this.dependencies.add(depName); + } + } + + #handleRequireResolve( + node: ESTree.Node + ) { + if (isLiteral(node)) { + this.dependencies.add(node.value); + } + } + + #handlePathJoin( + node: ESTree.CallExpression + ) { + if (!node.arguments.every((arg) => isLiteral(arg))) { + return; + } + + const constructedPath = path.posix.join( + ...node.arguments.map((arg) => arg.value) + ); + this.dependencies.add(constructedPath); + this.triggerWarning = false; + } +} diff --git a/workspaces/js-x-ray/src/probes/isRequire/isRequire.js b/workspaces/js-x-ray/src/probes/isRequire/isRequire.ts similarity index 62% rename from workspaces/js-x-ray/src/probes/isRequire/isRequire.js rename to workspaces/js-x-ray/src/probes/isRequire/isRequire.ts index af62e8bb..573b0feb 100644 --- a/workspaces/js-x-ray/src/probes/isRequire/isRequire.js +++ b/workspaces/js-x-ray/src/probes/isRequire/isRequire.ts @@ -7,12 +7,19 @@ import { getCallExpressionIdentifier, getCallExpressionArguments } from "@nodesecure/estree-ast-utils"; +import type { ESTree } from "meriyah"; // Import Internal Dependencies import { ProbeSignals } from "../../ProbeRunner.js"; +import { SourceFile } from "../../SourceFile.js"; +import { isLiteral } from "../../types/estree.js"; import { RequireCallExpressionWalker } from "./RequireCallExpressionWalker.js"; +import { generateWarning } from "../../warnings.js"; -function validateNodeRequire(node, { tracer }) { +function validateNodeRequire( + node: ESTree.Node, + { tracer }: SourceFile +): [boolean, any?] { const id = getCallExpressionIdentifier(node, { resolveCallExpression: false }); @@ -30,17 +37,24 @@ function validateNodeRequire(node, { tracer }) { ]; } -function validateNodeEvalRequire(node) { +function validateNodeEvalRequire( + node: ESTree.Node +): [boolean, any?] { const id = getCallExpressionIdentifier(node); if (id !== "eval") { return [false]; } - if (node.callee.type !== "CallExpression") { + + const castedNode = node as ESTree.CallExpression; + if (castedNode.callee.type !== "CallExpression") { return [false]; } - const args = getCallExpressionArguments(node.callee); + const args = getCallExpressionArguments(castedNode.callee); + if (args === null) { + return [false]; + } return [ args.length > 0 && args.at(0) === "require", @@ -48,11 +62,16 @@ function validateNodeEvalRequire(node) { ]; } -function teardown({ sourceFile }) { +function teardown( + { sourceFile }: { sourceFile: SourceFile; } +) { sourceFile.dependencyAutoWarning = false; } -function main(node, options) { +function main( + node: ESTree.CallExpression, + options: { sourceFile: SourceFile; data?: string; } +) { const { sourceFile, data: calleeName } = options; const { tracer } = sourceFile; @@ -60,28 +79,36 @@ function main(node, options) { return; } const arg = node.arguments.at(0); + if (arg === undefined) { + return; + } if (calleeName === "eval") { sourceFile.dependencyAutoWarning = true; } + const location = node.loc; switch (arg.type) { // const foo = "http"; require(foo); case "Identifier": if (sourceFile.tracer.literalIdentifiers.has(arg.name)) { sourceFile.addDependency( - sourceFile.tracer.literalIdentifiers.get(arg.name), + sourceFile.tracer.literalIdentifiers.get(arg.name)!, node.loc ); } else { - sourceFile.addWarning("unsafe-import", null, node.loc); + sourceFile.warnings.push( + generateWarning("unsafe-import", { value: null, location }) + ); } break; // require("http") case "Literal": - sourceFile.addDependency(arg.value, node.loc); + if (isLiteral(arg)) { + sourceFile.addDependency(arg.value, node.loc); + } break; // require(["ht", "tp"]) @@ -91,7 +118,9 @@ function main(node, options) { .trim(); if (value === "") { - sourceFile.addWarning("unsafe-import", null, node.loc); + sourceFile.warnings.push( + generateWarning("unsafe-import", { value: null, location }) + ); } else { sourceFile.addDependency(value, node.loc); @@ -102,7 +131,9 @@ function main(node, options) { // require("ht" + "tp"); case "BinaryExpression": { if (arg.operator !== "+") { - sourceFile.addWarning("unsafe-import", null, node.loc); + sourceFile.warnings.push( + generateWarning("unsafe-import", { value: null, location }) + ); break; } @@ -114,7 +145,9 @@ function main(node, options) { sourceFile.addDependency([...iter].join(""), node.loc); } catch { - sourceFile.addWarning("unsafe-import", null, node.loc); + sourceFile.warnings.push( + generateWarning("unsafe-import", { value: null, location }) + ); } break; } @@ -126,7 +159,9 @@ function main(node, options) { dependencies.forEach((depName) => sourceFile.addDependency(depName, node.loc, true)); if (triggerWarning) { - sourceFile.addWarning("unsafe-import", null, node.loc); + sourceFile.warnings.push( + generateWarning("unsafe-import", { value: null, location }) + ); } // We skip walking the tree to avoid anymore warnings... @@ -134,8 +169,12 @@ function main(node, options) { } default: - sourceFile.addWarning("unsafe-import", null, node.loc); + sourceFile.warnings.push( + generateWarning("unsafe-import", { value: null, location }) + ); } + + return; } export default { diff --git a/workspaces/js-x-ray/src/probes/isSerializeEnv.js b/workspaces/js-x-ray/src/probes/isSerializeEnv.ts similarity index 52% rename from workspaces/js-x-ray/src/probes/isSerializeEnv.js rename to workspaces/js-x-ray/src/probes/isSerializeEnv.ts index 563d342a..6c042b08 100644 --- a/workspaces/js-x-ray/src/probes/isSerializeEnv.js +++ b/workspaces/js-x-ray/src/probes/isSerializeEnv.ts @@ -1,7 +1,13 @@ // Import Third-party Dependencies -import { getCallExpressionIdentifier, getMemberExpressionIdentifier } from "@nodesecure/estree-ast-utils"; +import { + getCallExpressionIdentifier, + getMemberExpressionIdentifier +} from "@nodesecure/estree-ast-utils"; +import type { ESTree } from "meriyah"; // Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; import { ProbeSignals } from "../ProbeRunner.js"; /** @@ -12,32 +18,41 @@ import { ProbeSignals } from "../ProbeRunner.js"; * JSON.stringify(process["env"]) * JSON.stringify(process[`env`]) */ -function validateNode(node) { +function validateNode( + node: ESTree.Node +): [boolean, any?] { const id = getCallExpressionIdentifier(node); if (id !== "JSON.stringify") { return [false]; } - if (node.arguments.length === 0) { + const castedNode = node as ESTree.CallExpression; + if (castedNode.arguments.length === 0) { return [false]; } - const firstArg = node.arguments[0]; - + const firstArg = castedNode.arguments[0]; if (firstArg.type === "MemberExpression") { const memberExprId = [...getMemberExpressionIdentifier(firstArg)].join("."); if (memberExprId === "process.env") { - return [true, "serialize-environment"]; + return [true]; } } return [false]; } -function main(node, options) { - const { sourceFile, data: warningType } = options; +function main( + node: ESTree.Node, + options: { sourceFile: SourceFile; } +) { + const { sourceFile } = options; - sourceFile.addWarning(warningType, "JSON.stringify(process.env)", node.loc); + const warning = generateWarning("serialize-environment", { + value: "JSON.stringify(process.env)", + location: node.loc + }); + sourceFile.warnings.push(warning); return ProbeSignals.Skip; } diff --git a/workspaces/js-x-ray/src/probes/isSyncIO.js b/workspaces/js-x-ray/src/probes/isSyncIO.ts similarity index 54% rename from workspaces/js-x-ray/src/probes/isSyncIO.js rename to workspaces/js-x-ray/src/probes/isSyncIO.ts index c0b1b31f..0633a8c6 100644 --- a/workspaces/js-x-ray/src/probes/isSyncIO.js +++ b/workspaces/js-x-ray/src/probes/isSyncIO.ts @@ -1,7 +1,12 @@ // Import Third-party Dependencies import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; +import type { ESTree } from "meriyah"; -// Constants +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; + +// CONSTANTS const kTracedNodeCoreModules = ["fs", "crypto", "child_process", "zlib"]; const kSyncIOIdentifierOrMemberExps = [ "crypto.pbkdf2Sync", @@ -33,26 +38,46 @@ const kSyncIOIdentifierOrMemberExps = [ "zlib.brotliDecompressSync" ]; -function validateNode(node, { tracer }) { +function validateNode( + node: ESTree.Node, + { tracer }: SourceFile +): [boolean, any?] { const id = getCallExpressionIdentifier(node, { tracer }); - if (id === null || !kTracedNodeCoreModules.some((moduleName) => tracer.importedModules.has(moduleName))) { + if ( + id === null || + !kTracedNodeCoreModules.some((moduleName) => tracer.importedModules.has(moduleName)) + ) { return [false]; } const data = tracer.getDataFromIdentifier(id); - return [data !== null && data.identifierOrMemberExpr.endsWith("Sync")]; + return [ + data !== null && + data.identifierOrMemberExpr.endsWith("Sync") + ]; } -function initialize(sourceFile) { - kSyncIOIdentifierOrMemberExps.forEach((identifierOrMemberExp) => sourceFile.tracer.trace(identifierOrMemberExp, { - followConsecutiveAssignment: true, - moduleName: identifierOrMemberExp.split(".")[0] - })); +function initialize( + sourceFile: SourceFile +) { + kSyncIOIdentifierOrMemberExps.forEach((identifierOrMemberExp) => { + return sourceFile.tracer.trace(identifierOrMemberExp, { + followConsecutiveAssignment: true, + moduleName: identifierOrMemberExp.split(".")[0] + }); + }); } -function main(node, { sourceFile }) { - sourceFile.addWarning("synchronous-io", node.callee.name, node.loc); +function main( + node: ESTree.CallExpression, + { sourceFile }: { sourceFile: SourceFile; } +) { + const warning = generateWarning("synchronous-io", { + value: node.callee.name, + location: node.loc + }); + sourceFile.warnings.push(warning); } export default { diff --git a/workspaces/js-x-ray/src/probes/isUnsafeCallee.js b/workspaces/js-x-ray/src/probes/isUnsafeCallee.js deleted file mode 100644 index 72947d1a..00000000 --- a/workspaces/js-x-ray/src/probes/isUnsafeCallee.js +++ /dev/null @@ -1,35 +0,0 @@ -// Import Internal Dependencies -import { isUnsafeCallee } from "../utils/index.js"; -import { ProbeSignals } from "../ProbeRunner.js"; - -/** - * @description Detect unsafe statement - * @example - * eval("this"); - * Function("return this")(); - */ -function validateNode(node) { - return isUnsafeCallee(node); -} - -function main(node, options) { - const { sourceFile, data: calleeName } = options; - - if ( - calleeName === "Function" && - node.callee.arguments.length > 0 && - node.callee.arguments[0].value === "return this" - ) { - return ProbeSignals.Skip; - } - sourceFile.addWarning("unsafe-stmt", calleeName, node.loc); - - return ProbeSignals.Skip; -} - -export default { - name: "isUnsafeCallee", - validateNode, - main, - breakOnMatch: false -}; diff --git a/workspaces/js-x-ray/src/probes/isUnsafeCallee.ts b/workspaces/js-x-ray/src/probes/isUnsafeCallee.ts new file mode 100644 index 00000000..af92ddc9 --- /dev/null +++ b/workspaces/js-x-ray/src/probes/isUnsafeCallee.ts @@ -0,0 +1,89 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; +import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; +import { ProbeSignals } from "../ProbeRunner.js"; + +/** + * @description Detect unsafe statement + * @example + * eval("this"); + * Function("return this")(); + */ +function validateNode( + node: ESTree.Node +): [boolean, any?] { + return isUnsafeCallee(node); +} + +function main( + node: ESTree.CallExpression, + options: { sourceFile: SourceFile; data?: string; } +) { + const { sourceFile, data: calleeName } = options; + + if (!calleeName) { + return ProbeSignals.Skip; + } + if ( + calleeName === "Function" && + node.callee.arguments.length > 0 && + node.callee.arguments[0].value === "return this" + ) { + return ProbeSignals.Skip; + } + + const warning = generateWarning("unsafe-stmt", { + value: calleeName, + location: node.loc + }); + sourceFile.warnings.push(warning); + + return ProbeSignals.Skip; +} + +function isEvalCallee( + node: ESTree.CallExpression +): boolean { + const identifier = getCallExpressionIdentifier(node, { + resolveCallExpression: false + }); + + return identifier === "eval"; +} + +function isFunctionCallee( + node: ESTree.CallExpression +): boolean { + const identifier = getCallExpressionIdentifier(node); + + return identifier === "Function" && node.callee.type === "CallExpression"; +} + +export function isUnsafeCallee( + node: ESTree.CallExpression | ESTree.Node +): [boolean, "eval" | "Function" | null] { + if (node.type !== "CallExpression") { + return [false, null]; + } + + if (isEvalCallee(node)) { + return [true, "eval"]; + } + + if (isFunctionCallee(node)) { + return [true, "Function"]; + } + + return [false, null]; +} + +export default { + name: "isUnsafeCallee", + validateNode, + main, + breakOnMatch: false +}; diff --git a/workspaces/js-x-ray/src/probes/isUnsafeCommand.js b/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts similarity index 69% rename from workspaces/js-x-ray/src/probes/isUnsafeCommand.js rename to workspaces/js-x-ray/src/probes/isUnsafeCommand.ts index ca127747..3167c728 100644 --- a/workspaces/js-x-ray/src/probes/isUnsafeCommand.js +++ b/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts @@ -1,14 +1,24 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + // Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; import { ProbeSignals } from "../ProbeRunner.js"; +import { isLiteral } from "../types/estree.js"; // CONSTANTS const kUnsafeCommands = ["csrutil"]; -function isUnsafeCommand(command) { +function isUnsafeCommand( + command: string +): boolean { return kUnsafeCommands.some((unsafeCommand) => command.includes(unsafeCommand)); } -function isSpawnOrExec(name) { +function isSpawnOrExec( + name: string +): boolean { return name === "spawn" || name === "exec" || name === "spawnSync" || @@ -25,7 +35,9 @@ function isSpawnOrExec(name) { * const { exec } = require("child_process"); * exec("csrutil status"); */ -function validateNode(node) { +function validateNode( + node: ESTree.Node +): [boolean, any?] { if (node.type !== "CallExpression" || node.arguments.length === 0) { return [false]; } @@ -74,29 +86,39 @@ function validateNode(node) { return [false]; } -function main(node, options) { - const { sourceFile } = options; +function main( + node: ESTree.CallExpression, + options: { sourceFile: SourceFile; data?: string; } +) { + const { sourceFile, data: methodName } = options; const commandArg = node.arguments[0]; - if (!commandArg || commandArg.type !== "Literal") { + if (!isLiteral(commandArg)) { return null; } let command = commandArg.value; - if (typeof command === "string" && isUnsafeCommand(command)) { + if (isUnsafeCommand(command)) { // Spawned command arguments are filled into an Array // as second arguments. This is why we should add them // manually to the command string. - if (options.data === "spawn" || options.data === "spawnSync") { - const args = node.arguments.at(1); - if (args && Array.isArray(args.elements)) { - args.elements.forEach((element) => { - command += ` ${element.value}`; - }); + if (methodName === "spawn" || methodName === "spawnSync") { + const arrExpr = node.arguments.at(1); + + if (arrExpr && arrExpr.type === "ArrayExpression") { + arrExpr.elements + .filter((element) => isLiteral(element)) + .forEach((element) => { + command += ` ${element.value}`; + }); } } - sourceFile.addWarning("unsafe-command", command, node.loc); + const warning = generateWarning("unsafe-command", { + value: command, + location: node.loc + }); + sourceFile.warnings.push(warning); return ProbeSignals.Skip; } diff --git a/workspaces/js-x-ray/src/probes/isWeakCrypto.js b/workspaces/js-x-ray/src/probes/isWeakCrypto.js deleted file mode 100644 index 44eccf76..00000000 --- a/workspaces/js-x-ray/src/probes/isWeakCrypto.js +++ /dev/null @@ -1,45 +0,0 @@ -// Import Third-party Dependencies -import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; - -// CONSTANTS -const kWeakAlgorithms = new Set([ - "md5", - "sha1", - "ripemd160", - "md4", - "md2" -]); - -function validateNode(node, { tracer }) { - const id = getCallExpressionIdentifier(node); - if (id === null || !tracer.importedModules.has("crypto")) { - return [false]; - } - - const data = tracer.getDataFromIdentifier(id); - - return [data !== null && data.identifierOrMemberExpr === "crypto.createHash"]; -} - -function initialize(sourceFile) { - sourceFile.tracer.trace("crypto.createHash", { - followConsecutiveAssignment: true, - moduleName: "crypto" - }); -} - -function main(node, { sourceFile }) { - const arg = node.arguments.at(0); - - if (kWeakAlgorithms.has(arg.value)) { - sourceFile.addWarning("weak-crypto", arg.value, node.loc); - } -} - -export default { - name: "isWeakCrypto", - validateNode, - main, - initialize, - breakOnMatch: false -}; diff --git a/workspaces/js-x-ray/src/probes/isWeakCrypto.ts b/workspaces/js-x-ray/src/probes/isWeakCrypto.ts new file mode 100644 index 00000000..e0699182 --- /dev/null +++ b/workspaces/js-x-ray/src/probes/isWeakCrypto.ts @@ -0,0 +1,69 @@ +// Import Third-party Dependencies +import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; +import { + isLiteral +} from "../types/estree.js"; + +// CONSTANTS +const kWeakAlgorithms = new Set([ + "md5", + "sha1", + "ripemd160", + "md4", + "md2" +]); + +function validateNode( + node: ESTree.Node, + sourceFile: SourceFile +): [boolean, any?] { + const { tracer } = sourceFile; + + const id = getCallExpressionIdentifier(node); + if (id === null || !tracer.importedModules.has("crypto")) { + return [false]; + } + + const data = tracer.getDataFromIdentifier(id); + + return [ + data !== null && data.identifierOrMemberExpr === "crypto.createHash" + ]; +} + +function initialize( + sourceFile: SourceFile +) { + sourceFile.tracer.trace("crypto.createHash", { + followConsecutiveAssignment: true, + moduleName: "crypto" + }); +} + +function main( + node: ESTree.CallExpression, + { sourceFile }: { sourceFile: SourceFile; } +) { + const arg = node.arguments.at(0); + + if (isLiteral(arg) && kWeakAlgorithms.has(arg.value)) { + const warning = generateWarning( + "weak-crypto", + { value: arg.value, location: node.loc } + ); + sourceFile.warnings.push(warning); + } +} + +export default { + name: "isWeakCrypto", + validateNode, + main, + initialize, + breakOnMatch: false +}; diff --git a/workspaces/js-x-ray/src/types/api.d.ts b/workspaces/js-x-ray/src/types/api.d.ts deleted file mode 100644 index 8b298fd3..00000000 --- a/workspaces/js-x-ray/src/types/api.d.ts +++ /dev/null @@ -1,182 +0,0 @@ -// Third-party -import type { DiGraph, VertexDefinition, VertexBody } from "digraph-js"; -import type { ESTree } from "meriyah"; - -// Internal -import { - Warning, - WarningName -} from "./warnings.js"; - -export { - AstAnalyser, - AstAnalyserOptions, - - EntryFilesAnalyser, - EntryFilesAnalyserOptions, - - JsSourceParser, - SourceParser, - - RuntimeOptions, - RuntimeFileOptions, - - Report, - ReportOnFile, - - SourceFlags, - SourceLocation, - Dependency -} - -type SourceFlags = - | "fetch" - | "oneline-require" - | "is-minified"; - -interface SourceLocation { - start: { - line: number; - column: number; - }; - end: { - line: number; - column: number; - } -} - -interface Dependency { - unsafe: boolean; - inTry: boolean; - location?: null | SourceLocation; -} - -interface RuntimeOptions { - /** - * @default true - */ - module?: boolean; - /** - * @default false - */ - removeHTMLComments?: boolean; - /** - * @default false - */ - isMinified?: boolean; - initialize?: (sourceFile: SourceFile) => void; - finalize?: (sourceFile: SourceFile) => void; -} - -interface RuntimeFileOptions extends Omit { - packageName?: string; -} - -interface AstAnalyserOptions { - /** - * @default JsSourceParser - */ - customParser?: SourceParser; - /** - * @default [] - */ - customProbes?: Probe[]; - /** - * @default false - */ - skipDefaultProbes?: boolean; - /** - * @default false - */ - optionalWarnings?: boolean | Iterable; -} - -interface Probe { - validateNode: Function | Function[]; - main: Function; - initialize?: (sourceFile: SourceFile) => void; -} - -interface Report { - dependencies: Map; - warnings: Warning[]; - flags: Set; - idsLengthAvg: number; - stringScore: number; -} - -type ReportOnFile = { - ok: true, - warnings: Warning[]; - dependencies: Map; - flags: Set; -} | { - ok: false, - warnings: Warning[]; -} - -interface SourceParser { - parse(source: string, options: unknown): ESTree.Statement[]; -} - -declare class AstAnalyser { - constructor(options?: AstAnalyserOptions); - analyse: ( - str: string, - options?: RuntimeOptions - ) => Report; - analyseFile( - pathToFile: string, - options?: RuntimeFileOptions - ): Promise; - analyseFileSync( - pathToFile: string, - options?: RuntimeFileOptions - ): ReportOnFile; -} - -declare class SourceFile { - flags: Set; - - constructor(source: string, options: any); - addDependency( - name: string, - location?: string | null, - unsafe?: boolean - ): void; - addWarning( - name: WarningName, - value: string, - location?: any - ): void; - analyzeLiteral(node: any, inArrayExpr?: boolean): void; - getResult(isMinified: boolean): any; - walk(node: any): "skip" | null; -} - -interface EntryFilesAnalyserOptions { - astAnalyzer?: AstAnalyser; - loadExtensions?: (defaults: string[]) => string[]; - rootPath?: string | URL; - ignoreENOENT?: boolean; -} - -declare class EntryFilesAnalyser { - public astAnalyzer: AstAnalyser; - public allowedExtensions: Set; - public dependencies: DiGraph>; - - constructor(options?: EntryFilesAnalyserOptions); - - /** - * Asynchronously analyze a set of entry files yielding analysis reports. - */ - analyse( - entryFiles: Iterable, - options?: RuntimeFileOptions - ): AsyncGenerator; -} - -declare class JsSourceParser implements SourceParser { - parse(source: string, options: unknown): ESTree.Statement[]; -} diff --git a/workspaces/js-x-ray/src/types/estree.ts b/workspaces/js-x-ray/src/types/estree.ts new file mode 100644 index 00000000..e2c9da38 --- /dev/null +++ b/workspaces/js-x-ray/src/types/estree.ts @@ -0,0 +1,35 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + +export type Literal = ESTree.Literal & { + value: T; +}; + +export type RegExpLiteral = ESTree.RegExpLiteral & { + value: T; +}; + +export function isNode( + value: any +): value is ESTree.Node { + return ( + value !== null && + typeof value === "object" && + "type" in value && + typeof value.type === "string" + ); +} + +export function isLiteral( + node: any +): node is Literal { + return isNode(node) && + node.type === "Literal" && + typeof node.value === "string"; +} + +export function isCallExpression( + node: any +): node is ESTree.CallExpression { + return isNode(node) && node.type === "CallExpression"; +} diff --git a/workspaces/js-x-ray/src/types/warnings.d.ts b/workspaces/js-x-ray/src/types/warnings.d.ts deleted file mode 100644 index aa471ca7..00000000 --- a/workspaces/js-x-ray/src/types/warnings.d.ts +++ /dev/null @@ -1,37 +0,0 @@ - -export { - Warning, - WarningDefault, - WarningLocation, - WarningName, - WarningNameWithValue -} - -type WarningNameWithValue = "parsing-error" -| "encoded-literal" -| "unsafe-regex" -| "unsafe-stmt" -| "short-identifiers" -| "suspicious-literal" -| "suspicious-file" -| "obfuscated-code" -| "weak-crypto" -| "shady-link" -| "unsafe-command"; -type WarningName = WarningNameWithValue | "unsafe-import"; - -type WarningLocation = [[number, number], [number, number]]; - -interface WarningDefault { - kind: T; - file?: string; - value: string; - source: string; - location: null | WarningLocation | WarningLocation[]; - i18n: string; - severity: "Information" | "Warning" | "Critical"; - experimental?: boolean; -} - -type Warning = - T extends { kind: WarningNameWithValue } ? T : Omit; diff --git a/workspaces/js-x-ray/src/utils/extractNode.js b/workspaces/js-x-ray/src/utils/extractNode.js deleted file mode 100644 index dba737d3..00000000 --- a/workspaces/js-x-ray/src/utils/extractNode.js +++ /dev/null @@ -1,14 +0,0 @@ -// Import Internal Dependencies -import { notNullOrUndefined } from "./notNullOrUndefined.js"; - -export function extractNode(expectedType) { - return (callback, nodes) => { - const finalNodes = Array.isArray(nodes) ? nodes : [nodes]; - - for (const node of finalNodes) { - if (notNullOrUndefined(node) && node.type === expectedType) { - callback(node); - } - } - }; -} diff --git a/workspaces/js-x-ray/src/utils/extractNode.ts b/workspaces/js-x-ray/src/utils/extractNode.ts new file mode 100644 index 00000000..46559639 --- /dev/null +++ b/workspaces/js-x-ray/src/utils/extractNode.ts @@ -0,0 +1,22 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + +// Import Internal Dependencies +import { isNode } from "../types/estree.js"; + +export type NodeExtractorCallback = (node: T) => void; +export type NodeOrNull = ESTree.Node | null; + +export function extractNode( + expectedType: T["type"] +) { + return (callback: NodeExtractorCallback, nodes: NodeOrNull | NodeOrNull[]) => { + const finalNodes = Array.isArray(nodes) ? nodes : [nodes]; + + for (const node of finalNodes) { + if (isNode(node) && node.type === expectedType) { + callback(node as T); + } + } + }; +} diff --git a/workspaces/js-x-ray/src/utils/index.js b/workspaces/js-x-ray/src/utils/index.ts similarity index 50% rename from workspaces/js-x-ray/src/utils/index.js rename to workspaces/js-x-ray/src/utils/index.ts index 66af9134..2995244b 100644 --- a/workspaces/js-x-ray/src/utils/index.js +++ b/workspaces/js-x-ray/src/utils/index.ts @@ -1,8 +1,4 @@ -export * from "./exportAssignmentHasRequireLeave.js"; export * from "./extractNode.js"; export * from "./isOneLineExpressionExport.js"; -export * from "./isUnsafeCallee.js"; export * from "./notNullOrUndefined.js"; -export * from "./rootLocation.js"; export * from "./toArrayLocation.js"; -export * from "./isNode.js"; diff --git a/workspaces/js-x-ray/src/utils/isNode.js b/workspaces/js-x-ray/src/utils/isNode.js deleted file mode 100644 index d867a6e4..00000000 --- a/workspaces/js-x-ray/src/utils/isNode.js +++ /dev/null @@ -1,5 +0,0 @@ -export function isNode(value) { - return ( - value !== null && typeof value === "object" && "type" in value && typeof value.type === "string" - ); -} diff --git a/workspaces/js-x-ray/src/utils/isOneLineExpressionExport.js b/workspaces/js-x-ray/src/utils/isOneLineExpressionExport.js deleted file mode 100644 index 0ea1ca43..00000000 --- a/workspaces/js-x-ray/src/utils/isOneLineExpressionExport.js +++ /dev/null @@ -1,24 +0,0 @@ -// Import Internal Dependencies -import { exportAssignmentHasRequireLeave } from "./exportAssignmentHasRequireLeave.js"; - -export function isOneLineExpressionExport(body) { - if (body.length === 0 || body.length > 1) { - return false; - } - - const [firstNode] = body; - if (firstNode.type !== "ExpressionStatement") { - return false; - } - - switch (firstNode.expression.type) { - // module.exports = require('...'); - case "AssignmentExpression": - return exportAssignmentHasRequireLeave(firstNode.expression.right); - // require('...'); - case "CallExpression": - return exportAssignmentHasRequireLeave(firstNode.expression); - default: - return false; - } -} diff --git a/workspaces/js-x-ray/src/utils/exportAssignmentHasRequireLeave.js b/workspaces/js-x-ray/src/utils/isOneLineExpressionExport.ts similarity index 53% rename from workspaces/js-x-ray/src/utils/exportAssignmentHasRequireLeave.js rename to workspaces/js-x-ray/src/utils/isOneLineExpressionExport.ts index 9ef76ea9..a67dc439 100644 --- a/workspaces/js-x-ray/src/utils/exportAssignmentHasRequireLeave.js +++ b/workspaces/js-x-ray/src/utils/isOneLineExpressionExport.ts @@ -1,9 +1,36 @@ // Import Third-party Dependencies +import type { ESTree } from "meriyah"; import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; -export function exportAssignmentHasRequireLeave(expr) { +export function isOneLineExpressionExport( + body: ESTree.Program["body"] +): boolean { + if (body.length === 0 || body.length > 1) { + return false; + } + + const [firstNode] = body; + if (firstNode.type !== "ExpressionStatement") { + return false; + } + + switch (firstNode.expression.type) { + // module.exports = require('...'); + case "AssignmentExpression": + return exportAssignmentHasRequireLeave(firstNode.expression.right); + // require('...'); + case "CallExpression": + return exportAssignmentHasRequireLeave(firstNode.expression); + default: + return false; + } +} + +function exportAssignmentHasRequireLeave( + expr: ESTree.Expression +): boolean { if (expr.type === "LogicalExpression") { return atLeastOneBranchHasRequireLeave(expr.left, expr.right); } @@ -32,7 +59,10 @@ export function exportAssignmentHasRequireLeave(expr) { return false; } -function atLeastOneBranchHasRequireLeave(left, right) { +function atLeastOneBranchHasRequireLeave( + left: ESTree.Expression, + right: ESTree.Expression +): boolean { return [ exportAssignmentHasRequireLeave(left), exportAssignmentHasRequireLeave(right) diff --git a/workspaces/js-x-ray/src/utils/isUnsafeCallee.js b/workspaces/js-x-ray/src/utils/isUnsafeCallee.js deleted file mode 100644 index 6f611f4f..00000000 --- a/workspaces/js-x-ray/src/utils/isUnsafeCallee.js +++ /dev/null @@ -1,28 +0,0 @@ -// Import Third-party Dependencies -import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; - -function isEvalCallee(node) { - const identifier = getCallExpressionIdentifier(node, { - resolveCallExpression: false - }); - - return identifier === "eval"; -} - -function isFunctionCallee(node) { - const identifier = getCallExpressionIdentifier(node); - - return identifier === "Function" && node.callee.type === "CallExpression"; -} - -export function isUnsafeCallee(node) { - if (isEvalCallee(node)) { - return [true, "eval"]; - } - - if (isFunctionCallee(node)) { - return [true, "Function"]; - } - - return [false, null]; -} diff --git a/workspaces/js-x-ray/src/utils/notNullOrUndefined.js b/workspaces/js-x-ray/src/utils/notNullOrUndefined.js deleted file mode 100644 index 9eee5851..00000000 --- a/workspaces/js-x-ray/src/utils/notNullOrUndefined.js +++ /dev/null @@ -1,3 +0,0 @@ -export function notNullOrUndefined(value) { - return value !== null && value !== void 0; -} diff --git a/workspaces/js-x-ray/src/utils/notNullOrUndefined.ts b/workspaces/js-x-ray/src/utils/notNullOrUndefined.ts new file mode 100644 index 00000000..42efc539 --- /dev/null +++ b/workspaces/js-x-ray/src/utils/notNullOrUndefined.ts @@ -0,0 +1,5 @@ +export function notNullOrUndefined( + value: T +): value is NonNullable { + return value !== null && value !== void 0; +} diff --git a/workspaces/js-x-ray/src/utils/rootLocation.js b/workspaces/js-x-ray/src/utils/rootLocation.js deleted file mode 100644 index 8d6275a9..00000000 --- a/workspaces/js-x-ray/src/utils/rootLocation.js +++ /dev/null @@ -1,3 +0,0 @@ -export function rootLocation() { - return { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } }; -} diff --git a/workspaces/js-x-ray/src/utils/toArrayLocation.js b/workspaces/js-x-ray/src/utils/toArrayLocation.js deleted file mode 100644 index 6eedfe67..00000000 --- a/workspaces/js-x-ray/src/utils/toArrayLocation.js +++ /dev/null @@ -1,11 +0,0 @@ -// Import Internal Dependencies -import { rootLocation } from "./rootLocation.js"; - -export function toArrayLocation(location = rootLocation()) { - const { start, end = start } = location; - - return [ - [start.line || 0, start.column || 0], - [end.line || 0, end.column || 0] - ]; -} diff --git a/workspaces/js-x-ray/src/utils/toArrayLocation.ts b/workspaces/js-x-ray/src/utils/toArrayLocation.ts new file mode 100644 index 00000000..5659a676 --- /dev/null +++ b/workspaces/js-x-ray/src/utils/toArrayLocation.ts @@ -0,0 +1,22 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + +export type SourceArrayLocation = [[number, number], [number, number]]; + +export function rootLocation(): ESTree.SourceLocation { + return { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 } + }; +} + +export function toArrayLocation( + location: ESTree.SourceLocation = rootLocation() +): SourceArrayLocation { + const { start, end = start } = location; + + return [ + [start.line || 0, start.column || 0], + [end.line || 0, end.column || 0] + ]; +} diff --git a/workspaces/js-x-ray/src/warnings.js b/workspaces/js-x-ray/src/warnings.ts similarity index 50% rename from workspaces/js-x-ray/src/warnings.js rename to workspaces/js-x-ray/src/warnings.ts index 8ce73ce9..6425bbea 100644 --- a/workspaces/js-x-ray/src/warnings.js +++ b/workspaces/js-x-ray/src/warnings.ts @@ -1,7 +1,44 @@ +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + // Import Internal Dependencies -import { toArrayLocation } from "./utils/toArrayLocation.js"; +import { + toArrayLocation, + rootLocation, + type SourceArrayLocation +} from "./utils/toArrayLocation.js"; import { notNullOrUndefined } from "./utils/notNullOrUndefined.js"; +export type OptionalWarningName = + | "synchronous-io"; + +export type WarningName = + | "parsing-error" + | "encoded-literal" + | "unsafe-regex" + | "unsafe-stmt" + | "short-identifiers" + | "suspicious-literal" + | "suspicious-file" + | "obfuscated-code" + | "weak-crypto" + | "shady-link" + | "unsafe-command" + | "unsafe-import" + | "serialize-environment" + | OptionalWarningName; + +export interface Warning { + kind: T | (string & {}); + file?: string; + value: string | null; + source: string; + location: null | SourceArrayLocation | SourceArrayLocation[]; + i18n: string; + severity: "Information" | "Warning" | "Critical"; + experimental?: boolean; +} + export const warnings = Object.freeze({ "parsing-error": { i18n: "sast_warnings.parsing_error", @@ -67,26 +104,43 @@ export const warnings = Object.freeze({ severity: "Warning", experimental: false } -}); +}) satisfies Record>; -export function generateWarning(kind, options) { - const { location, file = null, value = null, source = "JS-X-Ray" } = options; +export interface GenerateWarningOptions { + location?: ESTree.SourceLocation | null; + file?: string | null; + value: string | null; + source?: string; +} - if (kind === "encoded-literal") { - return Object.assign( - { kind, value, location: [toArrayLocation(location)], source }, - warnings[kind] - ); - } +export function generateWarning( + kind: T, + options: GenerateWarningOptions +): Warning { + const { + file = null, + value, + source = "JS-X-Ray" + } = options; + const location = options.location ?? rootLocation(); - const result = { kind, location: toArrayLocation(location), source }; - if (notNullOrUndefined(file)) { - result.file = file; - } - if (notNullOrUndefined(value)) { - result.value = value; + if (kind === "encoded-literal") { + return { + kind, + value, + location: [toArrayLocation(location)], + source, + ...warnings[kind] + }; } - return Object.assign(result, warnings[kind]); + return { + kind, + location: toArrayLocation(location), + source, + ...warnings[kind], + ...(notNullOrUndefined(file) ? { file } : {}), + ...(notNullOrUndefined(value) ? { value } : { value: null }) + }; } diff --git a/workspaces/js-x-ray/test/AstAnalyser.spec.js b/workspaces/js-x-ray/test/AstAnalyser.spec.ts similarity index 89% rename from workspaces/js-x-ray/test/AstAnalyser.spec.js rename to workspaces/js-x-ray/test/AstAnalyser.spec.ts index 2fab8f76..c0ad1f29 100644 --- a/workspaces/js-x-ray/test/AstAnalyser.spec.js +++ b/workspaces/js-x-ray/test/AstAnalyser.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable max-nested-callbacks */ // Import Node.js Dependencies -import { describe, it } from "node:test"; +import { describe, it, TestContext } from "node:test"; import assert from "node:assert"; import { readFileSync, writeFileSync, unlinkSync } from "node:fs"; import os from "node:os"; @@ -85,7 +85,7 @@ describe("AstAnalyser", () => { ); const [encodedLiteral] = warnings; - assert.strictEqual(encodedLiteral.location.length, 3); + assert.strictEqual(encodedLiteral.location?.length, 3); }); it("should be capable to follow a malicious code with hexa computation and reassignments", () => { @@ -131,7 +131,7 @@ describe("AstAnalyser", () => { `); assert.ok(dependencies.has("http")); - assert.ok(dependencies.get("http").inTry); + assert.ok(dependencies.get("http")!.inTry); }); it("should return isOneLineRequire true given a single line CJS export", () => { @@ -158,7 +158,10 @@ describe("AstAnalyser", () => { }); it("should append to list of probes (default)", () => { - const analyser = new AstAnalyser({ customParser: new JsSourceParser(), customProbes }); + const analyser = new AstAnalyser({ + customParser: new JsSourceParser(), + customProbes + }); const result = analyser.analyse(kIncriminedCodeSampleCustomProbe); assert.equal(result.warnings[0].kind, kWarningUnsafeDanger); @@ -169,7 +172,7 @@ describe("AstAnalyser", () => { it("should replace list of probes", () => { const analyser = new AstAnalyser({ - parser: new JsSourceParser(), + customParser: new JsSourceParser(), customProbes, skipDefaultProbes: true }); @@ -180,7 +183,7 @@ describe("AstAnalyser", () => { }); it("should call with the expected arguments", (t) => { - t.mock.method(AstAnalyser.prototype, "analyse"); + const astAnalyserMock = t.mock.method(AstAnalyser.prototype, "analyse"); const source = "const http = require(\"http\");"; new AstAnalyser().analyse(source, { module: true, removeHTMLComments: true }); @@ -188,7 +191,7 @@ describe("AstAnalyser", () => { const source2 = "const fs = require(\"fs\");"; new AstAnalyser().analyse(source2, { module: false, removeHTMLComments: false }); - const calls = AstAnalyser.prototype.analyse.mock.calls; + const calls = astAnalyserMock.mock.calls; assert.strictEqual(calls.length, 2); assert.deepEqual(calls[0].arguments, [source, { module: true, removeHTMLComments: true }]); @@ -202,6 +205,7 @@ describe("AstAnalyser", () => { it("should throw if initialize is not a function", () => { assert.throws(() => { analyser.analyse("const foo = 'bar';", { + // @ts-expect-error initialize: "foo" }); }, /options.initialize must be a function/); @@ -233,6 +237,7 @@ describe("AstAnalyser", () => { it("should throw if finalize is not a function", () => { assert.throws(() => { analyser.analyse("const foo = 'bar';", { + // @ts-expect-error finalize: "foo" }); }, /options.finalize must be a function/); @@ -260,7 +265,7 @@ describe("AstAnalyser", () => { }); it("intialize should be called before finalize", () => { - const calls = []; + const calls: string[] = []; const analyser = new AstAnalyser(); analyser.analyse("const foo = 'bar';", { @@ -301,7 +306,7 @@ describe("AstAnalyser", () => { }); it("should call the method with the expected arguments", async(t) => { - t.mock.method(AstAnalyser.prototype, "analyseFile"); + const astAnalyserMock = t.mock.method(AstAnalyser.prototype, "analyseFile"); const url = new URL("depName.js", kFixtureURL); await new AstAnalyser().analyseFile( @@ -315,7 +320,7 @@ describe("AstAnalyser", () => { { module: true, packageName: "foobar2" } ); - const calls = AstAnalyser.prototype.analyseFile.mock.calls; + const calls = astAnalyserMock.mock.calls; assert.strictEqual(calls.length, 2); assert.deepEqual(calls[0].arguments, [url, { module: false, packageName: "foobar" }]); @@ -325,7 +330,7 @@ describe("AstAnalyser", () => { it("should implement new customProbes while keeping default probes", async() => { const result = await new AstAnalyser( { - parser: new JsSourceParser(), + customParser: new JsSourceParser(), customProbes, skipDefaultProbes: false } @@ -340,7 +345,7 @@ describe("AstAnalyser", () => { it("should implement new customProbes while skipping/removing default probes", async() => { const result = await new AstAnalyser( { - parser: new JsSourceParser(), + customParser: new JsSourceParser(), customProbes, skipDefaultProbes: true } @@ -356,10 +361,10 @@ describe("AstAnalyser", () => { describe("initialize", () => { it("should throw if initialize is not a function", async() => { - const res = await analyser.analyseFile( - url, { - initialize: "foo" - }); + const res = await analyser.analyseFile(url, { + // @ts-expect-error + initialize: "foo" + }); assert.strictEqual(res.ok, false); assert.strictEqual(res.warnings[0].value, "options.initialize must be a function"); @@ -389,10 +394,10 @@ describe("AstAnalyser", () => { describe("finalize", () => { it("should throw if finalize is not a function", async() => { - const res = await analyser.analyseFile( - url, { - finalize: "foo" - }); + const res = await analyser.analyseFile(url, { + // @ts-expect-error + finalize: "foo" + }); assert.strictEqual(res.ok, false); assert.strictEqual(res.warnings[0].value, "options.finalize must be a function"); @@ -421,7 +426,7 @@ describe("AstAnalyser", () => { }); it("intialize should be called before finalize", async() => { - const calls = []; + const calls: string[] = []; await analyser.analyseFile(url, { initialize: () => calls.push("initialize"), @@ -469,7 +474,7 @@ describe("AstAnalyser", () => { assert.ok(result.flags instanceof Set); }); - it("should add is-minified flag for minified files", (t) => { + it("should add is-minified flag for minified files", (t: TestContext) => { t.plan(3); const minifiedContent = "var a=require(\"fs\"),b=require(\"http\");" + "a.readFile(\"test.txt\",function(c,d){b.createServer().listen(3000)});"; @@ -481,15 +486,17 @@ describe("AstAnalyser", () => { const result = getAnalyser().analyseFileSync(tempMinFile); t.assert.ok(result.ok); - t.assert.ok(result.flags.has("is-minified")); - t.assert.strictEqual(result.flags.has("oneline-require"), false); + if (result.ok) { + t.assert.ok(result.flags.has("is-minified")); + t.assert.strictEqual(result.flags.has("oneline-require"), false); + } } finally { unlinkSync(tempMinFile); } }); - it("should add oneline-require flag for one-line exports", (t) => { + it("should add oneline-require flag for one-line exports", (t: TestContext) => { t.plan(4); const oneLineContent = "module.exports = require('foo');"; const tempOneLineFile = path.join(os.tmpdir(), "temp-oneline.js"); @@ -514,10 +521,10 @@ describe("AstAnalyser", () => { describe("initialize", () => { it("should throw if initialize is not a function", () => { - const res = getAnalyser().analyseFileSync( - url, { - initialize: "foo" - }); + const res = getAnalyser().analyseFileSync(url, { + // @ts-expect-error + initialize: "foo" + }); assert.strictEqual(res.ok, false); assert.strictEqual(res.warnings[0].value, "options.initialize must be a function"); @@ -547,8 +554,9 @@ describe("AstAnalyser", () => { describe("finalize", () => { it("should throw if finalize is not a function", () => { - const res = getAnalyser().analyseFileSync( - url, { + const res = getAnalyser() + .analyseFileSync(url, { + // @ts-expect-error finalize: "foo" }); @@ -579,7 +587,7 @@ describe("AstAnalyser", () => { }); it("intialize should be called before finalize", () => { - const calls = []; + const calls: string[] = []; getAnalyser().analyseFileSync(url, { initialize: () => calls.push("initialize"), @@ -663,8 +671,8 @@ describe("AstAnalyser", () => { }); it("should properly instanciate default or custom parser (using analyseFile)", async(t) => { - t.mock.method(JsSourceParser.prototype, "parse"); - t.mock.method(FakeSourceParser.prototype, "parse"); + const jsSourceParserMock = t.mock.method(JsSourceParser.prototype, "parse"); + const fakeSourceParserMock = t.mock.method(FakeSourceParser.prototype, "parse"); await new AstAnalyser().analyseFile( new URL("depName.js", kFixtureURL), @@ -678,13 +686,19 @@ describe("AstAnalyser", () => { { module: true, packageName: "foobar2" } ); - assert.strictEqual(JsSourceParser.prototype.parse.mock.calls.length, 1); - assert.strictEqual(FakeSourceParser.prototype.parse.mock.calls.length, 1); + assert.strictEqual( + jsSourceParserMock.mock.calls.length, + 1 + ); + assert.strictEqual( + fakeSourceParserMock.mock.calls.length, + 1 + ); }); it("should properly instanciate default or custom parser (using analyse)", (t) => { - t.mock.method(JsSourceParser.prototype, "parse"); - t.mock.method(FakeSourceParser.prototype, "parse"); + const jsSourceParserMock = t.mock.method(JsSourceParser.prototype, "parse"); + const fakeSourceParserMock = t.mock.method(FakeSourceParser.prototype, "parse"); new AstAnalyser().analyse("const http = require(\"http\");", { module: true, removeHTMLComments: true }); @@ -694,24 +708,33 @@ describe("AstAnalyser", () => { { module: false, removeHTMLComments: false } ); - assert.strictEqual(JsSourceParser.prototype.parse.mock.calls.length, 1); - assert.strictEqual(FakeSourceParser.prototype.parse.mock.calls.length, 1); + assert.strictEqual( + jsSourceParserMock.mock.calls.length, + 1 + ); + assert.strictEqual( + fakeSourceParserMock.mock.calls.length, + 1 + ); }); }); describe("optional warnings", () => { it("should not crash when there is an unknown optional warning", () => { new AstAnalyser({ + // @ts-expect-error optionalWarnings: ["unknown"] }).analyse(""); }); }); }); -let analyser = null; -function getAnalyser() { +let analyser: AstAnalyser | null = null; +function getAnalyser(): NonNullable { if (!analyser) { - analyser = new AstAnalyser(new JsSourceParser()); + analyser = new AstAnalyser({ + customParser: new JsSourceParser() + }); } return analyser; diff --git a/workspaces/js-x-ray/test/Deobfuscator.spec.js b/workspaces/js-x-ray/test/Deobfuscator.spec.ts similarity index 98% rename from workspaces/js-x-ray/test/Deobfuscator.spec.js rename to workspaces/js-x-ray/test/Deobfuscator.spec.ts index 610929ec..f385fe22 100644 --- a/workspaces/js-x-ray/test/Deobfuscator.spec.js +++ b/workspaces/js-x-ray/test/Deobfuscator.spec.ts @@ -3,6 +3,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; // Import Third-party Dependencies +import type { ESTree } from "meriyah"; import { walk } from "estree-walker"; // Import Internal Dependencies @@ -338,7 +339,11 @@ describe("Deobfuscator", () => { }); }); -function walkAst(body, callback = () => void 0) { +function walkAst( + body: ESTree.Program["body"], + callback: (node: any) => void = (_node) => undefined +) { + // @ts-expect-error walk(body, { enter(node) { if (!Array.isArray(node)) { diff --git a/workspaces/js-x-ray/test/EntryFilesAnalyser.spec.js b/workspaces/js-x-ray/test/EntryFilesAnalyser.spec.ts similarity index 95% rename from workspaces/js-x-ray/test/EntryFilesAnalyser.spec.js rename to workspaces/js-x-ray/test/EntryFilesAnalyser.spec.ts index e2668e56..9b618468 100644 --- a/workspaces/js-x-ray/test/EntryFilesAnalyser.spec.js +++ b/workspaces/js-x-ray/test/EntryFilesAnalyser.spec.ts @@ -16,7 +16,7 @@ describe("EntryFilesAnalyser", () => { const entryUrl = new URL("entry.js", kFixtureURL); const deepEntryUrl = new URL("deps/deepEntry.js", kFixtureURL); - t.mock.method(AstAnalyser.prototype, "analyseFile"); + const analyseFileMock = t.mock.method(AstAnalyser.prototype, "analyseFile"); const generator = entryFilesAnalyser.analyse([ entryUrl, @@ -37,7 +37,7 @@ describe("EntryFilesAnalyser", () => { ); // Check that shared dependencies are not analyzed several times - const calls = AstAnalyser.prototype.analyseFile.mock.calls; + const calls = analyseFileMock.mock.calls; assert.strictEqual(calls.length, 6); }); @@ -45,7 +45,7 @@ describe("EntryFilesAnalyser", () => { const entryFilesAnalyser = new EntryFilesAnalyser(); const entryUrl = new URL("export.js", kFixtureURL); - t.mock.method(AstAnalyser.prototype, "analyseFile"); + const analyseFileMock = t.mock.method(AstAnalyser.prototype, "analyseFile"); const generator = entryFilesAnalyser.analyse([ entryUrl @@ -61,7 +61,7 @@ describe("EntryFilesAnalyser", () => { ); // Check that shared dependencies are not analyzed several times - const calls = AstAnalyser.prototype.analyseFile.mock.calls; + const calls = analyseFileMock.mock.calls; assert.strictEqual(calls.length, 2); }); @@ -221,8 +221,10 @@ describe("EntryFilesAnalyser", () => { }); // TODO: replace with Array.fromAsync when droping Node.js 20 -async function fromAsync(asyncIter) { - const items = []; +async function fromAsync( + asyncIter: AsyncIterable +): Promise { + const items: T[] = []; for await (const item of asyncIter) { items.push(item); diff --git a/workspaces/js-x-ray/test/JsSourceParser.spec.js b/workspaces/js-x-ray/test/JsSourceParser.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/JsSourceParser.spec.js rename to workspaces/js-x-ray/test/JsSourceParser.spec.ts diff --git a/workspaces/js-x-ray/test/NodeCounter.spec.js b/workspaces/js-x-ray/test/NodeCounter.spec.ts similarity index 87% rename from workspaces/js-x-ray/test/NodeCounter.spec.js rename to workspaces/js-x-ray/test/NodeCounter.spec.ts index 40327e47..d4ff5496 100644 --- a/workspaces/js-x-ray/test/NodeCounter.spec.js +++ b/workspaces/js-x-ray/test/NodeCounter.spec.ts @@ -3,12 +3,13 @@ import { describe, it, mock } from "node:test"; import assert from "node:assert/strict"; // Import Third-party Dependencies +import type { ESTree } from "meriyah"; import { walk } from "estree-walker"; // Import Internal Dependencies import { NodeCounter } from "../src/NodeCounter.js"; -import { isNode } from "../src/utils/index.js"; import { JsSourceParser } from "../src/index.js"; +import { isNode } from "../src/types/estree.js"; describe("NodeCounter", () => { describe("constructor", () => { @@ -25,7 +26,7 @@ describe("NodeCounter", () => { const match = mock.fn(); const filter = mock.fn(() => true); - const nc = new NodeCounter( + const nc = new NodeCounter( "FunctionDeclaration", { match, filter } ); @@ -42,11 +43,15 @@ describe("NodeCounter", () => { }); it("should count one for a FunctionDeclaration with an identifier", () => { - const ids = []; - const nc = new NodeCounter( + const ids: string[] = []; + const nc = new NodeCounter( "FunctionDeclaration", { - match: (node) => ids.push(node.id.name) + match: (node) => { + if (node.id) { + ids.push(node.id.name); + } + } } ); assert.equal(nc.type, "FunctionDeclaration"); @@ -64,7 +69,7 @@ describe("NodeCounter", () => { }); it("should count zero for a FunctionExpression with no identifier", () => { - const nc = new NodeCounter( + const nc = new NodeCounter( "FunctionExpression", { filter: (node) => isNode(node.id) && node.id.type === "Identifier" @@ -124,7 +129,10 @@ describe("NodeCounter", () => { }); }); -function walkAst(body, callback = () => void 0) { +function walkAst( + body: any, + callback: (node: any) => void = () => void 0 +) { walk(body, { enter(node) { if (!Array.isArray(node)) { diff --git a/workspaces/js-x-ray/test/ProbeRunner.spec.js b/workspaces/js-x-ray/test/ProbeRunner.spec.ts similarity index 71% rename from workspaces/js-x-ray/test/ProbeRunner.spec.js rename to workspaces/js-x-ray/test/ProbeRunner.spec.ts index ed80a4dd..e1c09582 100644 --- a/workspaces/js-x-ray/test/ProbeRunner.spec.js +++ b/workspaces/js-x-ray/test/ProbeRunner.spec.ts @@ -2,8 +2,14 @@ import { describe, it, mock } from "node:test"; import assert from "node:assert"; +// Import Third-party Dependencies +import type { ESTree } from "meriyah"; + // Import Internal Dependencies -import { ProbeRunner, ProbeSignals } from "../src/ProbeRunner.js"; +import { + ProbeRunner, + ProbeSignals +} from "../src/ProbeRunner.js"; import { SourceFile } from "../src/SourceFile.js"; describe("ProbeRunner", () => { @@ -17,12 +23,13 @@ describe("ProbeRunner", () => { it("should use provided probes with validate node as func", () => { const fakeProbe = [ { - validateNode: (node) => [node.type === "CallExpression"], + validateNode: (node: ESTree.Node) => [node.type === "CallExpression"], main: mock.fn(), teardown: mock.fn() } ]; + // @ts-expect-error const pr = new ProbeRunner(new SourceFile(""), fakeProbe); assert.strictEqual(pr.probes, fakeProbe); }); @@ -33,19 +40,22 @@ describe("ProbeRunner", () => { validateNode: [], main: mock.fn(), teardown: mock.fn() - }]; + } + ]; + // @ts-expect-error const pr = new ProbeRunner(new SourceFile(""), fakeProbe); assert.strictEqual(pr.probes, fakeProbe); }); it("should fail if main not present", () => { const fakeProbe = { - validateNode: (node) => [node.type === "CallExpression"], + validateNode: (node: ESTree.Node) => [node.type === "CallExpression"], teardown: mock.fn() }; function instantiateProbeRunner() { + // @ts-expect-error return new ProbeRunner(new SourceFile(""), [fakeProbe]); } @@ -59,6 +69,7 @@ describe("ProbeRunner", () => { }; function instantiateProbeRunner() { + // @ts-expect-error return new ProbeRunner(new SourceFile(""), [fakeProbe]); } @@ -73,6 +84,7 @@ describe("ProbeRunner", () => { }; function instantiateProbeRunner() { + // @ts-expect-error return new ProbeRunner(new SourceFile(""), [fakeProbe]); } @@ -82,48 +94,50 @@ describe("ProbeRunner", () => { describe("walk", () => { it("should pass validateNode, enter main and then teardown", () => { - const sourceFile = {}; + const sourceFile = new SourceFile(""); const fakeProbe = { - validateNode: (node) => [node.type === "CallExpression"], + validateNode: (node: ESTree.Node) => [node.type === "Literal"], main: mock.fn(), teardown: mock.fn() }; - const pr = new ProbeRunner(sourceFile, [ - fakeProbe - ]); + // @ts-expect-error + const pr = new ProbeRunner(sourceFile, [fakeProbe]); - const astNode = { - type: "CallExpression" + const astNode: ESTree.Literal = { + type: "Literal", + value: "test" }; const result = pr.walk(astNode); assert.strictEqual(result, null); assert.strictEqual(fakeProbe.main.mock.calls.length, 1); - assert.deepEqual(fakeProbe.main.mock.calls.at(0).arguments, [ + assert.deepEqual(fakeProbe.main.mock.calls.at(0)?.arguments, [ astNode, { sourceFile, data: null } ]); assert.strictEqual(fakeProbe.teardown.mock.calls.length, 1); - assert.deepEqual(fakeProbe.teardown.mock.calls.at(0).arguments, [ + assert.deepEqual(fakeProbe.teardown.mock.calls.at(0)?.arguments, [ { sourceFile } ]); }); it("should trigger and return a skip signal", () => { - const sourceFile = {}; const fakeProbe = { - validateNode: (node) => [node.type === "CallExpression"], + validateNode: (node: ESTree.Node) => [node.type === "Literal"], main: () => ProbeSignals.Skip, teardown: mock.fn() }; - const pr = new ProbeRunner(sourceFile, [ - fakeProbe - ]); + const pr = new ProbeRunner( + new SourceFile(""), + // @ts-expect-error + [fakeProbe] + ); - const astNode = { - type: "CallExpression" + const astNode: ESTree.Node = { + type: "Literal", + value: "test" }; const result = pr.walk(astNode); diff --git a/workspaces/js-x-ray/test/fixtures/FakeSourceParser.js b/workspaces/js-x-ray/test/fixtures/FakeSourceParser.js deleted file mode 100644 index af51c98d..00000000 --- a/workspaces/js-x-ray/test/fixtures/FakeSourceParser.js +++ /dev/null @@ -1,5 +0,0 @@ -export class FakeSourceParser { - parse(str, options) { - return [{ type: "LiteralExpression" }]; - } -} diff --git a/workspaces/js-x-ray/test/fixtures/FakeSourceParser.ts b/workspaces/js-x-ray/test/fixtures/FakeSourceParser.ts new file mode 100644 index 00000000..2a7a96fe --- /dev/null +++ b/workspaces/js-x-ray/test/fixtures/FakeSourceParser.ts @@ -0,0 +1,8 @@ +// Import Internal Dependencies +import type { SourceParser } from "../../src/index.js"; + +export class FakeSourceParser implements SourceParser { + parse(str: string, options: unknown): any { + return [{ type: "LiteralExpression" }]; + } +} diff --git a/workspaces/js-x-ray/test/issues/109-html-comment-parsing.spec.js b/workspaces/js-x-ray/test/issues/109-html-comment-parsing.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/issues/109-html-comment-parsing.spec.js rename to workspaces/js-x-ray/test/issues/109-html-comment-parsing.spec.ts diff --git a/workspaces/js-x-ray/test/issues/163-illegalReturnStatement.spec.js b/workspaces/js-x-ray/test/issues/163-illegalReturnStatement.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/issues/163-illegalReturnStatement.spec.js rename to workspaces/js-x-ray/test/issues/163-illegalReturnStatement.spec.ts diff --git a/workspaces/js-x-ray/test/issues/170-isOneLineRequire-logicalExpression-CJS-export.spec.js b/workspaces/js-x-ray/test/issues/170-isOneLineRequire-logicalExpression-CJS-export.spec.ts similarity index 97% rename from workspaces/js-x-ray/test/issues/170-isOneLineRequire-logicalExpression-CJS-export.spec.js rename to workspaces/js-x-ray/test/issues/170-isOneLineRequire-logicalExpression-CJS-export.spec.ts index 68253ced..85bebde6 100644 --- a/workspaces/js-x-ray/test/issues/170-isOneLineRequire-logicalExpression-CJS-export.spec.js +++ b/workspaces/js-x-ray/test/issues/170-isOneLineRequire-logicalExpression-CJS-export.spec.ts @@ -5,7 +5,7 @@ import assert from "node:assert"; // Import Internal Dependencies import { AstAnalyser } from "../../src/index.js"; -const validTestCases = [ +const validTestCases: [string, string[]][] = [ ["module.exports = require('fs') || require('constants');", ["fs", "constants"]], ["module.exports = require('constants') ? require('fs') : require('foo');", ["constants", "fs", "foo"]], @@ -44,7 +44,7 @@ test("it should return isOneLineRequire true given a single line CJS export with }); }); -const invalidTestCases = [ +const invalidTestCases: [string, string[]][] = [ // should have at least one `require` callee ["module.exports = notRequire('foo') || {};", []], ["module.exports = {} || notRequire('foo');", []], diff --git a/workspaces/js-x-ray/test/issues/177-wrongUnsafeRequire.spec.js b/workspaces/js-x-ray/test/issues/177-wrongUnsafeRequire.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/issues/177-wrongUnsafeRequire.spec.js rename to workspaces/js-x-ray/test/issues/177-wrongUnsafeRequire.spec.ts diff --git a/workspaces/js-x-ray/test/issues/178-path-join-literal-args-is-not-unsafe.spec.js b/workspaces/js-x-ray/test/issues/178-path-join-literal-args-is-not-unsafe.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/issues/178-path-join-literal-args-is-not-unsafe.spec.js rename to workspaces/js-x-ray/test/issues/178-path-join-literal-args-is-not-unsafe.spec.ts diff --git a/workspaces/js-x-ray/test/issues/179-UnsafeEvalRequire.spec.js b/workspaces/js-x-ray/test/issues/179-UnsafeEvalRequire.spec.ts similarity index 68% rename from workspaces/js-x-ray/test/issues/179-UnsafeEvalRequire.spec.js rename to workspaces/js-x-ray/test/issues/179-UnsafeEvalRequire.spec.ts index 23a26853..ef0196a0 100644 --- a/workspaces/js-x-ray/test/issues/179-UnsafeEvalRequire.spec.js +++ b/workspaces/js-x-ray/test/issues/179-UnsafeEvalRequire.spec.ts @@ -16,12 +16,14 @@ const kWarningUnsafeStatement = "unsafe-stmt"; test("should detect unsafe-import and unsafe-statement", () => { const sastAnalysis = new AstAnalyser().analyse(kIncriminedCodeSample); - assert.equal(sastAnalysis.warnings.at(0).value, "stream"); - assert.equal(sastAnalysis.warnings.at(0).kind, kWarningUnsafeImport); - assert.equal(sastAnalysis.warnings.at(1).value, "eval"); - assert.equal(sastAnalysis.warnings.at(1).kind, kWarningUnsafeStatement); + const [firstWarning, secondWarning] = sastAnalysis.warnings; + + assert.equal(firstWarning.value, "stream"); + assert.equal(firstWarning.kind, kWarningUnsafeImport); + assert.equal(secondWarning.value, "eval"); + assert.equal(secondWarning.kind, kWarningUnsafeStatement); assert.equal(sastAnalysis.warnings.length, 2); assert.equal(sastAnalysis.dependencies.has("stream"), true); - assert.equal(sastAnalysis.dependencies.get("stream").unsafe, true); + assert.equal(sastAnalysis.dependencies.get("stream")!.unsafe, true); assert.equal(sastAnalysis.dependencies.size, 1); }); diff --git a/workspaces/js-x-ray/test/issues/180-logicalexpr-return-this.spec.js b/workspaces/js-x-ray/test/issues/180-logicalexpr-return-this.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/issues/180-logicalexpr-return-this.spec.js rename to workspaces/js-x-ray/test/issues/180-logicalexpr-return-this.spec.ts diff --git a/workspaces/js-x-ray/test/issues/283-oneline-require-minified.spec.js b/workspaces/js-x-ray/test/issues/283-oneline-require-minified.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/issues/283-oneline-require-minified.spec.js rename to workspaces/js-x-ray/test/issues/283-oneline-require-minified.spec.ts diff --git a/workspaces/js-x-ray/test/issues/295-deobfuscator-function-declaration-id-null.spec.js b/workspaces/js-x-ray/test/issues/295-deobfuscator-function-declaration-id-null.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/issues/295-deobfuscator-function-declaration-id-null.spec.js rename to workspaces/js-x-ray/test/issues/295-deobfuscator-function-declaration-id-null.spec.ts diff --git a/workspaces/js-x-ray/test/issues/312-try-finally.spec.js b/workspaces/js-x-ray/test/issues/312-try-finally.spec.ts similarity index 93% rename from workspaces/js-x-ray/test/issues/312-try-finally.spec.js rename to workspaces/js-x-ray/test/issues/312-try-finally.spec.ts index 2762abc5..594fa927 100644 --- a/workspaces/js-x-ray/test/issues/312-try-finally.spec.js +++ b/workspaces/js-x-ray/test/issues/312-try-finally.spec.ts @@ -22,7 +22,7 @@ test("SourceFile inTryStatement must ignore try/finally statements", () => { assert.strictEqual(dependencies.size, 1); assert.ok(dependencies.has("foobar")); - const dependency = dependencies.get("foobar"); + const dependency = dependencies.get("foobar")!; assert.strictEqual(dependency.unsafe, false); assert.strictEqual(dependency.inTry, false); }); diff --git a/workspaces/js-x-ray/test/issues/59-undefined-depName.spec.js b/workspaces/js-x-ray/test/issues/59-undefined-depName.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/issues/59-undefined-depName.spec.js rename to workspaces/js-x-ray/test/issues/59-undefined-depName.spec.ts diff --git a/workspaces/js-x-ray/test/obfuscated.spec.js b/workspaces/js-x-ray/test/obfuscated.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/obfuscated.spec.js rename to workspaces/js-x-ray/test/obfuscated.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isArrayExpression.spec.js b/workspaces/js-x-ray/test/probes/isArrayExpression.spec.ts similarity index 78% rename from workspaces/js-x-ray/test/probes/isArrayExpression.spec.js rename to workspaces/js-x-ray/test/probes/isArrayExpression.spec.ts index 8dbb9768..3376d241 100644 --- a/workspaces/js-x-ray/test/probes/isArrayExpression.spec.js +++ b/workspaces/js-x-ray/test/probes/isArrayExpression.spec.ts @@ -12,12 +12,12 @@ test("it should trigger analyzeLiteral method one time", (t) => { const ast = parseScript(str); const sastAnalysis = getSastAnalysis(str, isArrayExpression); - t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); + const analyzeLiteralMock = t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); sastAnalysis.execute(ast.body); assert.strictEqual(sastAnalysis.warnings().length, 0); - const calls = sastAnalysis.sourceFile.analyzeLiteral.mock.calls; + const calls = analyzeLiteralMock.mock.calls; assert.strictEqual(calls.length, 1); const literalNode = calls[0].arguments[0]; @@ -30,10 +30,10 @@ test("it should trigger analyzeLiteral method two times (ignoring the holey betw const ast = parseScript(str); const sastAnalysis = getSastAnalysis(str, isArrayExpression); - t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); + const analyzeLiteralMock = t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); sastAnalysis.execute(ast.body); - const calls = sastAnalysis.sourceFile.analyzeLiteral.mock.calls; + const calls = analyzeLiteralMock.mock.calls; assert.strictEqual(calls.length, 2); assert.strictEqual(calls[0].arguments[0].value, 5); assert.strictEqual(calls[1].arguments[0].value, 10); @@ -45,10 +45,10 @@ test("it should trigger analyzeLiteral one time (ignoring non-literal Node)", (t const ast = parseScript(str); const sastAnalysis = getSastAnalysis(str, isArrayExpression); - t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); + const analyzeLiteralMock = t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); sastAnalysis.execute(ast.body); - const calls = sastAnalysis.sourceFile.analyzeLiteral.mock.calls; + const calls = analyzeLiteralMock.mock.calls; assert.strictEqual(calls.length, 1); const literalNode = calls[0].arguments[0]; diff --git a/workspaces/js-x-ray/test/probes/isBinaryExpression.spec.js b/workspaces/js-x-ray/test/probes/isBinaryExpression.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isBinaryExpression.spec.js rename to workspaces/js-x-ray/test/probes/isBinaryExpression.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isESMExport.spec.js b/workspaces/js-x-ray/test/probes/isESMExport.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isESMExport.spec.js rename to workspaces/js-x-ray/test/probes/isESMExport.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isFetch.spec.js b/workspaces/js-x-ray/test/probes/isFetch.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isFetch.spec.js rename to workspaces/js-x-ray/test/probes/isFetch.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isImportDeclaration.spec.js b/workspaces/js-x-ray/test/probes/isImportDeclaration.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isImportDeclaration.spec.js rename to workspaces/js-x-ray/test/probes/isImportDeclaration.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isLiteral.spec.js b/workspaces/js-x-ray/test/probes/isLiteral.spec.ts similarity index 93% rename from workspaces/js-x-ray/test/probes/isLiteral.spec.js rename to workspaces/js-x-ray/test/probes/isLiteral.spec.ts index 7ffbb662..60f5b052 100644 --- a/workspaces/js-x-ray/test/probes/isLiteral.spec.js +++ b/workspaces/js-x-ray/test/probes/isLiteral.spec.ts @@ -11,15 +11,15 @@ test("should throw an unsafe-import because the hexadecimal string is equal to t const ast = parseScript(str); const sastAnalysis = getSastAnalysis(str, isLiteral); - t.mock.method(sastAnalysis.sourceFile.deobfuscator, "analyzeString"); + const analyzeStringMock = t.mock.method(sastAnalysis.sourceFile.deobfuscator, "analyzeString"); sastAnalysis.execute(ast.body); assert.strictEqual(sastAnalysis.warnings().length, 1); const warning = sastAnalysis.getWarning("unsafe-import"); - assert.strictEqual(warning.kind, "unsafe-import"); + assert.strictEqual(warning?.kind, "unsafe-import"); assert.ok(sastAnalysis.dependencies().has("http")); - const calls = sastAnalysis.sourceFile.deobfuscator.analyzeString.mock.calls; + const calls = analyzeStringMock.mock.calls; assert.strictEqual(calls.length, 1); assert.ok(calls[0].arguments.includes("http")); }); @@ -29,14 +29,14 @@ test("should throw an encoded-literal warning because the hexadecimal value is e const ast = parseScript(str); const sastAnalysis = getSastAnalysis(str, isLiteral); - t.mock.method(sastAnalysis.sourceFile.deobfuscator, "analyzeString"); + const analyzeStringMock = t.mock.method(sastAnalysis.sourceFile.deobfuscator, "analyzeString"); sastAnalysis.execute(ast.body); assert.strictEqual(sastAnalysis.warnings().length, 1); const warning = sastAnalysis.getWarning("encoded-literal"); - assert.strictEqual(warning.value, "72657175697265"); + assert.strictEqual(warning?.value, "72657175697265"); - const calls = sastAnalysis.sourceFile.deobfuscator.analyzeString.mock.calls; + const calls = analyzeStringMock.mock.calls; assert.strictEqual(calls.length, 1); assert.ok(calls[0].arguments.includes("require")); }); @@ -67,11 +67,11 @@ test("should not throw any warnings without hexadecimal value (and should call a const ast = parseScript(str); const sastAnalysis = getSastAnalysis(str, isLiteral); - t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); + const analyzeLiteralMock = t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); sastAnalysis.execute(ast.body); assert.strictEqual(sastAnalysis.warnings().length, 0); - const calls = sastAnalysis.sourceFile.analyzeLiteral.mock.calls; + const calls = analyzeLiteralMock.mock.calls; assert.strictEqual(calls.length, 1); const astNode = calls[0].arguments[0]; diff --git a/workspaces/js-x-ray/test/probes/isLiteralRegex.spec.js b/workspaces/js-x-ray/test/probes/isLiteralRegex.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isLiteralRegex.spec.js rename to workspaces/js-x-ray/test/probes/isLiteralRegex.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isRegexObject.spec.js b/workspaces/js-x-ray/test/probes/isRegexObject.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isRegexObject.spec.js rename to workspaces/js-x-ray/test/probes/isRegexObject.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isRequire.spec.js b/workspaces/js-x-ray/test/probes/isRequire.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isRequire.spec.js rename to workspaces/js-x-ray/test/probes/isRequire.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isSerializeEnv.spec.js b/workspaces/js-x-ray/test/probes/isSerializeEnv.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isSerializeEnv.spec.js rename to workspaces/js-x-ray/test/probes/isSerializeEnv.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isSyncIO.spec.js b/workspaces/js-x-ray/test/probes/isSyncIO.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isSyncIO.spec.js rename to workspaces/js-x-ray/test/probes/isSyncIO.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isUnsafeCallee.spec.js b/workspaces/js-x-ray/test/probes/isUnsafeCallee.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isUnsafeCallee.spec.js rename to workspaces/js-x-ray/test/probes/isUnsafeCallee.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.js b/workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.js rename to workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.ts diff --git a/workspaces/js-x-ray/test/probes/isWeakCrypto.spec.js b/workspaces/js-x-ray/test/probes/isWeakCrypto.spec.ts similarity index 100% rename from workspaces/js-x-ray/test/probes/isWeakCrypto.spec.js rename to workspaces/js-x-ray/test/probes/isWeakCrypto.spec.ts diff --git a/workspaces/js-x-ray/test/utils/index.js b/workspaces/js-x-ray/test/utils/index.ts similarity index 57% rename from workspaces/js-x-ray/test/utils/index.js rename to workspaces/js-x-ray/test/utils/index.ts index 3b95f237..9e924c14 100644 --- a/workspaces/js-x-ray/test/utils/index.js +++ b/workspaces/js-x-ray/test/utils/index.ts @@ -3,14 +3,26 @@ import * as meriyah from "meriyah"; import { walk } from "estree-walker"; // Import Internal Dependencies -import { SourceFile } from "../../src/SourceFile.js"; -import { ProbeRunner, ProbeSignals } from "../../src/ProbeRunner.js"; +import { + SourceFile, + type Dependency, + type Warning +} from "../../src/index.js"; +import { + ProbeRunner, + ProbeSignals, + type Probe +} from "../../src/ProbeRunner.js"; -export function getWarningKind(warnings) { +export function getWarningKind( + warnings: Warning[] +): string[] { return warnings.slice().map((warn) => warn.kind).sort(); } -export function parseScript(str) { +export function parseScript( + str: string +) { return meriyah.parseScript(str, { next: true, loc: true, @@ -21,28 +33,28 @@ export function parseScript(str) { } export function getSastAnalysis( - sourceCodeString, - probe + sourceCodeString: string, + probe: Probe ) { return { sourceFile: new SourceFile(sourceCodeString), - getWarning(warning) { - return this.sourceFile.warnings.find( - (item) => item.kind === warning + getWarning(warning: string): Warning | undefined { + return this.warnings().find( + (item: Warning) => item.kind === warning ); }, - warnings() { + warnings(): Warning[] { return this.sourceFile.warnings; }, - dependencies() { + dependencies(): Map { return this.sourceFile.dependencies; }, - execute(body) { + execute(body: any) { const probeRunner = new ProbeRunner(this.sourceFile, [probe]); const self = this; walk(body, { - enter(node) { + enter(node: any) { // Skip the root of the AST. if (Array.isArray(node)) { return; @@ -62,15 +74,26 @@ export function getSastAnalysis( }; } -export const customProbes = [ +export const customProbes: Probe[] = [ { name: "customProbeUnsafeDanger", - validateNode: (node, _sourceFile) => [node.type === "VariableDeclaration" && node.declarations[0].init.value === "danger"] - , - main: (node, options) => { + validateNode(node: any): [boolean, any?] { + return [ + node.type === "VariableDeclaration" && + node.declarations[0].init.value === "danger" + ]; + }, + main(node, options) { const { sourceFile, data: calleeName } = options; if (node.declarations[0].init.value === "danger") { - sourceFile.addWarning("unsafe-danger", calleeName, node.loc); + sourceFile.warnings.push({ + kind: "unsafe-danger", + value: calleeName, + location: node.loc, + source: "JS-X-Ray Custom Probe", + i18n: "sast_warnings.unsafe-danger", + severity: "Warning" + }); return ProbeSignals.Skip; } diff --git a/workspaces/js-x-ray/test/warnings.spec.js b/workspaces/js-x-ray/test/warnings.spec.ts similarity index 98% rename from workspaces/js-x-ray/test/warnings.spec.js rename to workspaces/js-x-ray/test/warnings.spec.ts index 827ec389..4e4b9194 100644 --- a/workspaces/js-x-ray/test/warnings.spec.js +++ b/workspaces/js-x-ray/test/warnings.spec.ts @@ -8,6 +8,7 @@ import { generateWarning } from "../src/warnings.js"; test("Given an encoded-literal kind it should generate a warning with deep location array", () => { const result = generateWarning("encoded-literal", { + value: null, location: rootLocation() }); diff --git a/workspaces/js-x-ray/tsconfig.json b/workspaces/js-x-ray/tsconfig.json new file mode 100644 index 00000000..a2ff792c --- /dev/null +++ b/workspaces/js-x-ray/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"], + "references": [ + { + "path": "../estree-ast-utils" + }, + { + "path": "../tracer" + }, + { + "path": "../sec-literal" + } + ] +} diff --git a/workspaces/sec-literal/test/hex.spec.ts b/workspaces/sec-literal/test/hex.spec.ts index 730a4fbc..8ac9b63f 100644 --- a/workspaces/sec-literal/test/hex.spec.ts +++ b/workspaces/sec-literal/test/hex.spec.ts @@ -30,7 +30,8 @@ describe("isHex()", () => { const hexValue = 100; assert.strictEqual( - isHex(hexValue as any), + // @ts-expect-error + isHex(hexValue), false, "100 is typeof number so it must always return false" ); diff --git a/workspaces/sec-literal/test/literal.spec.ts b/workspaces/sec-literal/test/literal.spec.ts index fa9ca921..7d52f676 100644 --- a/workspaces/sec-literal/test/literal.spec.ts +++ b/workspaces/sec-literal/test/literal.spec.ts @@ -30,7 +30,8 @@ test("toRaw must return a string when we give a valid EStree Literal", () => { }); test("defaultAnalysis() of something else than a Literal must always return null", () => { - assert.strictEqual(defaultAnalysis(10 as any), null); + // @ts-expect-error + assert.strictEqual(defaultAnalysis(10), null); }); test("defaultAnalysis() of an Hexadecimal value", () => { diff --git a/workspaces/sec-literal/test/patterns.spec.ts b/workspaces/sec-literal/test/patterns.spec.ts index 2fea475e..af836e5b 100644 --- a/workspaces/sec-literal/test/patterns.spec.ts +++ b/workspaces/sec-literal/test/patterns.spec.ts @@ -47,7 +47,8 @@ describe("commonStringSuffix()", () => { describe("commonHexadecimalPrefix()", () => { test("should throw a TypeError if identifiersArray is not an Array", () => { - assert.throws(() => commonHexadecimalPrefix(10 as any), { + // @ts-expect-error + assert.throws(() => commonHexadecimalPrefix(10), { name: "TypeError", message: "identifiersArray must be an Array" }); diff --git a/workspaces/sec-literal/test/utils.spec.ts b/workspaces/sec-literal/test/utils.spec.ts index f578fed3..7d01a5ba 100644 --- a/workspaces/sec-literal/test/utils.spec.ts +++ b/workspaces/sec-literal/test/utils.spec.ts @@ -42,7 +42,8 @@ test("isSvgPath must return true when we give a valid svg path and false when th assert.strictEqual(isSvgPath("M150"), false, "the length of an svg path must be always higher than four characters"); assert.strictEqual(isSvgPath("hello world!"), false); assert.strictEqual( - isSvgPath(10 as any), + // @ts-expect-error + isSvgPath(10), false, "isSvgPath argument must always return false for anything that is not a string primitive" ); diff --git a/workspaces/ts-source-parser/package.json b/workspaces/ts-source-parser/package.json index b0df7d3f..6c021743 100644 --- a/workspaces/ts-source-parser/package.json +++ b/workspaces/ts-source-parser/package.json @@ -9,8 +9,7 @@ "prepublishOnly": "npm run build", "build": "tsc", "test-only": "tsx --test-reporter=spec --test \"./test/**/*.spec.ts\"", - "test": "c8 --all --src ./src -r html npm run test-only", - "check": "npm run lint && npm run test" + "test": "c8 --all --src ./src -r html npm run test-only" }, "repository": { "type": "git",