@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
145 lines (127 loc) • 3.72 kB
JavaScript
// Import Native Dependencies
import assert from "node:assert";
// Import all the probes
import isUnsafeCallee from "./probes/isUnsafeCallee.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 isImportDeclaration from "./probes/isImportDeclaration.js";
import isWeakCrypto from "./probes/isWeakCrypto.js";
import isBinaryExpression from "./probes/isBinaryExpression.js";
import isArrayExpression from "./probes/isArrayExpression.js";
import isESMExport from "./probes/isESMExport.js";
import isFetch from "./probes/isFetch.js";
// Import Internal Dependencies
import { SourceFile } from "./SourceFile.js";
/**
* @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]
*/
export const ProbeSignals = Object.freeze({
Break: Symbol.for("breakWalk"),
Skip: Symbol.for("skipWalk")
});
export class ProbeRunner {
/**
* Note:
* The order of the table has an importance/impact on the correct execution of the probes
*
* @type {Probe[]}
*/
static Defaults = [
isFetch,
isRequire,
isESMExport,
isUnsafeCallee,
isLiteral,
isLiteralRegex,
isRegexObject,
isImportDeclaration,
isWeakCrypto,
isBinaryExpression,
isArrayExpression
];
/**
*
* @param {!SourceFile} sourceFile
* @param {Probe[]} [probes=ProbeRunner.Defaults]
*/
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",
`Invalid probe ${probe.name}: main must be a function`
);
}
this.probes = probes;
}
/**
* @param {!Probe} probe
* @param {!any} node
* @returns {null|void|Symbol}
*/
#runProbe(probe, node) {
const validationFns = Array.isArray(probe.validateNode) ?
probe.validateNode : [probe.validateNode];
for (const validateNode of validationFns) {
const [isMatching, data = null] = validateNode(
node,
this.sourceFile
);
if (isMatching) {
return probe.main(node, {
sourceFile: this.sourceFile,
data
});
}
}
return null;
}
walk(node) {
/** @type {Set<string>} */
const breakGroups = new Set();
for (const probe of this.probes) {
if (breakGroups.has(probe.breakGroup)) {
continue;
}
try {
const result = this.#runProbe(probe, node);
if (result === null) {
continue;
}
if (result === ProbeSignals.Skip) {
return "skip";
}
if (result === ProbeSignals.Break || probe.breakOnMatch) {
const breakGroup = probe.breakGroup || null;
if (breakGroup === null) {
break;
}
else {
breakGroups.add(breakGroup);
}
}
}
finally {
if (probe.teardown) {
probe.teardown({ sourceFile: this.sourceFile });
}
}
}
return null;
}
}