UNPKG

@nodesecure/js-x-ray

Version:
229 lines 9.88 kB
// Import Node.js Dependencies import assert from "node:assert"; // Import Internal Dependencies import unsafeVmContext from "./probes/unsafe-vm-context.js"; import logUsage from "./probes/log-usage.js"; import sqlInjection from "./probes/sql-injection.js"; import dataExfiltration from "./probes/data-exfiltration.js"; import isArrayExpression from "./probes/isArrayExpression.js"; import isBinaryExpression from "./probes/isBinaryExpression.js"; import isESMExport from "./probes/isESMExport.js"; import isFetch from "./probes/isFetch.js"; import isImportDeclaration from "./probes/isImportDeclaration.js"; import isLiteral from "./probes/isLiteral.js"; import isLiteralRegex from "./probes/isLiteralRegex.js"; import isRegexObject from "./probes/isRegexObject.js"; import isRequire from "./probes/isRequire/isRequire.js"; import isSerializeEnv from "./probes/isSerializeEnv.js"; import isSyncIO from "./probes/isSyncIO.js"; import isUnsafeCallee from "./probes/isUnsafeCallee.js"; import isUnsafeCommand from "./probes/isUnsafeCommand.js"; import isMonkeyPatch from "./probes/isMonkeyPatch.js"; import isRandom from "./probes/isRandom.js"; import isPrototypePollution from "./probes/isPrototypePollution.js"; import { isWeakAlgorithm, isWeakScrypt, isUnsafePrehash, isWeakBcrypt, isPasswordShucking } from "./probes/crypto/index.js"; import { getCallExpressionIdentifier, isCallExpression } from "./estree/index.js"; import { CALL_EXPRESSION_DATA, CALL_EXPRESSION_IDENTIFIER } from "./contants.js"; const kProbeOriginalContext = Symbol.for("ProbeOriginalContext"); export class ProbeRunner { probes; sourceFile; #selectedEntryPoints = new Map(); #breakGroups = new Set(); #probeValidateFns = new Map(); #probeCtx = new Map(); #probeMainCtx = new Map(); #nodeTypeIndex = new Map(); #catchAllProbes = []; #callExprIdentifierOptions; static Signals = Object.freeze({ Break: Symbol.for("breakWalk"), Skip: Symbol.for("skipWalk"), Continue: null }); /** * Note: * The order of the table has an importance/impact on the correct execution of the probes */ static Defaults = [ isFetch, isRequire, isESMExport, isUnsafeCallee, isLiteral, isLiteralRegex, isRegexObject, isImportDeclaration, isWeakAlgorithm, unsafeVmContext, isBinaryExpression, isArrayExpression, isUnsafeCommand, isSerializeEnv, dataExfiltration, sqlInjection, isMonkeyPatch, isPrototypePollution ]; static Optionals = { "synchronous-io": isSyncIO, "log-usage": logUsage, "insecure-random": isRandom, "crypto.weak-scrypt": isWeakScrypt, "crypto.unsafe-prehash": isUnsafePrehash, "crypto.weak-bcrypt": isWeakBcrypt, "crypto.password-shucking": isPasswordShucking }; constructor(sourceFile, probes = ProbeRunner.Defaults) { this.sourceFile = sourceFile; this.#callExprIdentifierOptions = { externalIdentifierLookup: (name) => sourceFile.tracer.literalIdentifiers.get(name)?.value ?? null }; for (const probe of probes) { assert(typeof probe.validateNode === "function" || Array.isArray(probe.validateNode), `Invalid probe ${probe.name}: validateNode must be a function or an array of functions`); assert(typeof probe.main === "function" || typeof probe.main === "object", `Invalid probe ${probe.name}: main must be a function or an object with named handlers`); if (typeof probe.main === "object") { assert("default" in probe.main && typeof probe.main.default === "function", `Invalid probe ${probe.name}: named main handlers must provide a 'default' handler`); } assert(typeof probe.initialize === "function" || probe.initialize === undefined, `Invalid probe ${probe.name}: initialize must be a function or undefined`); // Pre-build per-probe caches before calling initialize so #getProbeContext can use them. const setEntryPoint = (handlerName) => { if (typeof probe.main === "object") { this.#selectedEntryPoints.set(probe, handlerName); } }; const ctx = { sourceFile: this.sourceFile, context: probe.context, setEntryPoint }; const mainCtx = { sourceFile: this.sourceFile, context: probe.context, setEntryPoint, signals: ProbeRunner.Signals, data: null }; this.#probeCtx.set(probe, ctx); this.#probeMainCtx.set(probe, mainCtx); this.#probeValidateFns.set(probe, Array.isArray(probe.validateNode) ? probe.validateNode : [probe.validateNode]); if (probe.initialize) { const isDefined = Reflect.defineProperty(probe, kProbeOriginalContext, { enumerable: false, value: structuredClone(probe.context), configurable: true }); if (!isDefined) { throw new Error(`Failed to define original context for probe '${probe.name}'`); } // Pass a fresh object for initialize so any captured reference reflects // the state at call-time (probe.context is undefined before initialize returns). const context = probe.initialize({ sourceFile: this.sourceFile, context: probe.context, setEntryPoint }); if (context) { probe.context = structuredClone(context); ctx.context = probe.context; mainCtx.context = probe.context; } } } this.probes = probes; this.#catchAllProbes = probes.filter((probe) => !probe.nodeTypes || probe.nodeTypes.length === 0); const allNodeTypes = new Set(probes.flatMap((probe) => probe.nodeTypes ?? [])); for (const nodeType of allNodeTypes) { const list = probes.filter((probe) => !probe.nodeTypes || probe.nodeTypes.length === 0 || probe.nodeTypes.includes(nodeType)); this.#nodeTypeIndex.set(nodeType, list); } } #getProbeContext(probe) { const ctx = this.#probeCtx.get(probe); ctx.context = probe.context; return ctx; } #getProbeHandler(probe) { if (typeof probe.main === "function") { return probe.main; } const selectedName = this.#selectedEntryPoints.get(probe); const handlerName = (selectedName && selectedName in probe.main) ? selectedName : "default"; return probe.main[handlerName]; } #runProbe(probe, node) { const validationFns = this.#probeValidateFns.get(probe); const ctx = this.#getProbeContext(probe); for (const validateNode of validationFns) { const [isMatching, data = null] = validateNode(node, ctx); if (!isMatching) { continue; } const mainHandler = this.#getProbeHandler(probe); this.#selectedEntryPoints.delete(probe); const mainCtx = this.#probeMainCtx.get(probe); mainCtx.context = probe.context; mainCtx.data = data; return mainHandler(node, mainCtx); } return null; } walk(node) { this.#breakGroups.clear(); let tracedIdentifierReport; let tracedIdentifier; if (isCallExpression(node)) { const id = getCallExpressionIdentifier(node, this.#callExprIdentifierOptions); if (id !== null) { tracedIdentifierReport = this.sourceFile.tracer.getDataFromIdentifier(id); tracedIdentifier = id; } } const probesForNode = this.#nodeTypeIndex.get(node.type) ?? this.#catchAllProbes; for (const probe of probesForNode) { if (probe.breakGroup && this.#breakGroups.has(probe.breakGroup)) { continue; } try { if (probe.context && tracedIdentifierReport) { probe.context[CALL_EXPRESSION_IDENTIFIER] = tracedIdentifier; probe.context[CALL_EXPRESSION_DATA] = tracedIdentifierReport; } const signal = this.#runProbe(probe, node); if (signal === ProbeRunner.Signals.Continue) { continue; } if (signal === ProbeRunner.Signals.Skip) { return "skip"; } if (signal === ProbeRunner.Signals.Break || probe.breakOnMatch) { const breakGroup = probe.breakGroup || null; if (breakGroup === null) { break; } else { this.#breakGroups.add(breakGroup); } } } finally { probe.teardown?.(this.#getProbeContext(probe)); if (probe.context) { delete probe.context[CALL_EXPRESSION_DATA]; delete probe.context[CALL_EXPRESSION_IDENTIFIER]; } } } return null; } finalize() { for (const probe of this.probes) { probe.finalize?.(this.#getProbeContext(probe)); probe.context = probe[kProbeOriginalContext]; } this.sourceFile.tracer.removeAllListeners(); } } //# sourceMappingURL=ProbeRunner.js.map