UNPKG

@nodesecure/js-x-ray

Version:
187 lines 7.54 kB
// Import Node.js Dependencies import assert from "node:assert"; // Import Internal Dependencies 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 isWeakCrypto from "./probes/isWeakCrypto.js"; import isMonkeyPatch from "./probes/isMonkeyPatch.js"; import isRandom from "./probes/isRandom.js"; import isPrototypePollution from "./probes/isPrototypePollution.js"; import { getCallExpressionIdentifier } 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(); 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, isWeakCrypto, isBinaryExpression, isArrayExpression, isUnsafeCommand, isSerializeEnv, dataExfiltration, sqlInjection, isMonkeyPatch, isPrototypePollution ]; static Optionals = { "synchronous-io": isSyncIO, "log-usage": logUsage, "insecure-random": isRandom }; constructor(sourceFile, probes = ProbeRunner.Defaults) { this.sourceFile = sourceFile; 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`); 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}'`); } const context = probe.initialize(this.#getProbeContext(probe)); if (context) { probe.context = structuredClone(context); } } } this.probes = probes; } #getProbeContext(probe) { const setEntryPoint = (handlerName) => { if (typeof probe.main === "object") { this.#selectedEntryPoints.set(probe, handlerName); } }; return { sourceFile: this.sourceFile, context: probe.context, setEntryPoint }; } #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 = Array.isArray(probe.validateNode) ? probe.validateNode : [probe.validateNode]; 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); return mainHandler(node, { ...ctx, signals: ProbeRunner.Signals, data }); } return null; } walk(node) { const breakGroups = new Set(); let tracedIdentifierReport; let tracedIdentifier; if (node.type === "CallExpression") { const id = getCallExpressionIdentifier(node, { externalIdentifierLookup: (name) => this.sourceFile.tracer.literalIdentifiers.get(name)?.value ?? null }); if (id !== null) { tracedIdentifierReport = this.sourceFile.tracer.getDataFromIdentifier(id); tracedIdentifier = id; } } for (const probe of this.probes) { if (probe.breakGroup && 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 { 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]; } } } //# sourceMappingURL=ProbeRunner.js.map