@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
187 lines • 7.54 kB
JavaScript
// 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