diff --git a/.changeset/pretty-moose-go.md b/.changeset/pretty-moose-go.md new file mode 100644 index 0000000..1d4e4a4 --- /dev/null +++ b/.changeset/pretty-moose-go.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/js-x-ray": minor +--- + +Move Signals into probe.main context diff --git a/workspaces/js-x-ray/docs/AstAnalyser.md b/workspaces/js-x-ray/docs/AstAnalyser.md index 11fa8fe..b954e3e 100644 --- a/workspaces/js-x-ray/docs/AstAnalyser.md +++ b/workspaces/js-x-ray/docs/AstAnalyser.md @@ -147,7 +147,7 @@ export const customProbes = [ node.type === "VariableDeclaration" && node.declarations[0].init.value === "danger" ], main: (node, ctx) => { - const { sourceFile, data: calleeName } = ctx; + const { sourceFile, data: calleeName, signals } = ctx; if (node.declarations[0].init.value === "danger") { sourceFile.warnings.push({ kind: "unsafe-danger", @@ -158,7 +158,7 @@ export const customProbes = [ severity: "Warning" }); - return ProbeSignals.Skip; + return signals.Skip; } return null; diff --git a/workspaces/js-x-ray/src/ProbeRunner.ts b/workspaces/js-x-ray/src/ProbeRunner.ts index 884bfc2..41470bf 100644 --- a/workspaces/js-x-ray/src/ProbeRunner.ts +++ b/workspaces/js-x-ray/src/ProbeRunner.ts @@ -29,6 +29,10 @@ export type ProbeContext = { sourceFile: SourceFile; context?: T; }; +export type ProbeMainContext = ProbeContext & { + data?: any; + signals: typeof ProbeRunner.Signals; +}; export type ProbeValidationCallback = ( node: ESTree.Node, ctx: ProbeContext @@ -41,7 +45,7 @@ export interface Probe { validateNode: ProbeValidationCallback | ProbeValidationCallback[]; main: ( node: any, - ctx: ProbeContext & { data?: any; } + ctx: ProbeMainContext ) => ProbeReturn; teardown?: (ctx: ProbeContext) => void; breakOnMatch?: boolean; @@ -49,15 +53,16 @@ export interface Probe { context?: ProbeContext; } -export const ProbeSignals = Object.freeze({ - Break: Symbol.for("breakWalk"), - Skip: Symbol.for("skipWalk") -}); - export class ProbeRunner { probes: Probe[]; sourceFile: SourceFile; + 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 @@ -138,6 +143,7 @@ export class ProbeRunner { if (isMatching) { return probe.main(node, { ...ctx, + signals: ProbeRunner.Signals, data }); } @@ -157,15 +163,15 @@ export class ProbeRunner { } try { - const result = this.#runProbe(probe, node); - if (result === null) { + const signal = this.#runProbe(probe, node); + if (signal === ProbeRunner.Signals.Continue) { continue; } - if (result === ProbeSignals.Skip) { + if (signal === ProbeRunner.Signals.Skip) { return "skip"; } - if (result === ProbeSignals.Break || probe.breakOnMatch) { + if (signal === ProbeRunner.Signals.Break || probe.breakOnMatch) { const breakGroup = probe.breakGroup || null; if (breakGroup === null) { diff --git a/workspaces/js-x-ray/src/probes/isRequire/isRequire.ts b/workspaces/js-x-ray/src/probes/isRequire/isRequire.ts index 60ed01d..c9e77bc 100644 --- a/workspaces/js-x-ray/src/probes/isRequire/isRequire.ts +++ b/workspaces/js-x-ray/src/probes/isRequire/isRequire.ts @@ -10,7 +10,7 @@ import { import type { ESTree } from "meriyah"; // Import Internal Dependencies -import { ProbeSignals, type ProbeContext } from "../../ProbeRunner.js"; +import type { ProbeContext, ProbeMainContext } from "../../ProbeRunner.js"; import { isLiteral } from "../../types/estree.js"; import { RequireCallExpressionWalker } from "./RequireCallExpressionWalker.js"; import { generateWarning } from "../../warnings.js"; @@ -70,9 +70,9 @@ function teardown( function main( node: ESTree.CallExpression, - options: ProbeContext & { data?: string; } + ctx: ProbeMainContext ) { - const { sourceFile, data: calleeName } = options; + const { sourceFile, data: calleeName, signals } = ctx; const { tracer } = sourceFile; if (node.arguments.length === 0) { @@ -170,7 +170,7 @@ function main( } // We skip walking the tree to avoid anymore warnings... - return ProbeSignals.Skip; + return signals.Skip; } default: diff --git a/workspaces/js-x-ray/src/probes/isSerializeEnv.ts b/workspaces/js-x-ray/src/probes/isSerializeEnv.ts index c27fc50..a0ad605 100644 --- a/workspaces/js-x-ray/src/probes/isSerializeEnv.ts +++ b/workspaces/js-x-ray/src/probes/isSerializeEnv.ts @@ -7,7 +7,10 @@ import type { ESTree } from "meriyah"; // Import Internal Dependencies import { generateWarning } from "../warnings.js"; -import { ProbeSignals, type ProbeContext } from "../ProbeRunner.js"; +import type { + ProbeContext, + ProbeMainContext +} from "../ProbeRunner.js"; /** * @description Detect serialization of process.env which could indicate environment variable exfiltration @@ -59,9 +62,9 @@ function validateNode( function main( node: ESTree.Node, - ctx: ProbeContext + ctx: ProbeMainContext ) { - const { sourceFile } = ctx; + const { sourceFile, signals } = ctx; const warning = generateWarning("serialize-environment", { value: "JSON.stringify(process.env)", @@ -69,7 +72,7 @@ function main( }); sourceFile.warnings.push(warning); - return ProbeSignals.Skip; + return signals.Skip; } function initialize( diff --git a/workspaces/js-x-ray/src/probes/isUnsafeCallee.ts b/workspaces/js-x-ray/src/probes/isUnsafeCallee.ts index af92ddc..20eafa6 100644 --- a/workspaces/js-x-ray/src/probes/isUnsafeCallee.ts +++ b/workspaces/js-x-ray/src/probes/isUnsafeCallee.ts @@ -3,9 +3,8 @@ 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"; +import type { ProbeMainContext } from "../ProbeRunner.js"; /** * @description Detect unsafe statement @@ -21,19 +20,19 @@ function validateNode( function main( node: ESTree.CallExpression, - options: { sourceFile: SourceFile; data?: string; } + ctx: ProbeMainContext ) { - const { sourceFile, data: calleeName } = options; + const { sourceFile, data: calleeName, signals } = ctx; if (!calleeName) { - return ProbeSignals.Skip; + return signals.Skip; } if ( calleeName === "Function" && node.callee.arguments.length > 0 && node.callee.arguments[0].value === "return this" ) { - return ProbeSignals.Skip; + return signals.Skip; } const warning = generateWarning("unsafe-stmt", { @@ -42,7 +41,7 @@ function main( }); sourceFile.warnings.push(warning); - return ProbeSignals.Skip; + return signals.Skip; } function isEvalCallee( diff --git a/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts b/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts index 4ef7c0c..132248c 100644 --- a/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts +++ b/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts @@ -2,10 +2,12 @@ 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, isTemplateLiteral } from "../types/estree.js"; +import { + isLiteral, + isTemplateLiteral +} from "../types/estree.js"; +import type { ProbeMainContext } from "../ProbeRunner.js"; // CONSTANTS const kUnsafeCommands = ["csrutil", "uname", "ping", "curl"]; @@ -102,9 +104,9 @@ function validateNode( function main( node: ESTree.CallExpression, - options: { sourceFile: SourceFile; data?: string; } + ctx: ProbeMainContext ) { - const { sourceFile, data: methodName } = options; + const { sourceFile, data: methodName, signals } = ctx; const commandArg = node.arguments[0]; if (!isLiteral(commandArg) && !isTemplateLiteral(commandArg)) { @@ -134,7 +136,7 @@ function main( }); sourceFile.warnings.push(warning); - return ProbeSignals.Skip; + return signals.Skip; } return null; diff --git a/workspaces/js-x-ray/test/ProbeRunner.spec.ts b/workspaces/js-x-ray/test/ProbeRunner.spec.ts index 8b5f778..45d2dee 100644 --- a/workspaces/js-x-ray/test/ProbeRunner.spec.ts +++ b/workspaces/js-x-ray/test/ProbeRunner.spec.ts @@ -7,8 +7,7 @@ import type { ESTree } from "meriyah"; // Import Internal Dependencies import { - ProbeRunner, - ProbeSignals + ProbeRunner } from "../src/ProbeRunner.js"; import { SourceFile } from "../src/SourceFile.js"; @@ -113,7 +112,7 @@ describe("ProbeRunner", () => { assert.strictEqual(fakeProbe.main.mock.calls.length, 1); assert.deepEqual(fakeProbe.main.mock.calls.at(0)?.arguments, [ - astNode, { sourceFile, data: null, context: undefined } + astNode, { sourceFile, data: null, context: undefined, signals: ProbeRunner.Signals } ]); assert.strictEqual(fakeProbe.teardown.mock.calls.length, 1); @@ -126,7 +125,7 @@ describe("ProbeRunner", () => { const data = { test: "data" }; const fakeProbe = { validateNode: mock.fn((_: ESTree.Node) => [true, data]), - main: mock.fn(() => ProbeSignals.Skip) + main: mock.fn(() => ProbeRunner.Signals.Skip) }; const sourceFile = new SourceFile(); @@ -149,14 +148,14 @@ describe("ProbeRunner", () => { astNode, expectedContext ]); assert.deepEqual(fakeProbe.main.mock.calls.at(0)?.arguments, [ - astNode, { ...expectedContext, data } + astNode, { ...expectedContext, data, signals: ProbeRunner.Signals } ]); }); it("should trigger and return a skip signal", () => { const fakeProbe = { validateNode: (node: ESTree.Node) => [node.type === "Literal"], - main: () => ProbeSignals.Skip, + main: () => ProbeRunner.Signals.Skip, teardown: mock.fn() }; @@ -181,20 +180,20 @@ describe("ProbeRunner", () => { it("should call the finalize methods", () => { const fakeProbe = { validateNode: (_: ESTree.Node) => [true], - main: () => ProbeSignals.Skip, + main: () => ProbeRunner.Signals.Skip, finalize: mock.fn() }; const fakeProbeSkip = { validateNode: (_: ESTree.Node) => [true], - main: () => ProbeSignals.Skip, + main: () => ProbeRunner.Signals.Skip, teardown: mock.fn(), finalize: mock.fn() }; const fakeProbeBreak = { validateNode: (_: ESTree.Node) => [true], - main: () => ProbeSignals.Break, + main: () => ProbeRunner.Signals.Break, teardown: mock.fn(), finalize: mock.fn() }; @@ -227,7 +226,7 @@ describe("ProbeRunner", () => { const fakeProbe = { initialize: mock.fn(() => fakeCtx), validateNode: mock.fn((_: ESTree.Node) => [true]), - main: mock.fn(() => ProbeSignals.Skip), + main: mock.fn(() => ProbeRunner.Signals.Skip), finalize: mock.fn() }; @@ -251,7 +250,7 @@ describe("ProbeRunner", () => { astNode, expectedContext ]); assert.deepEqual(fakeProbe.main.mock.calls.at(0)?.arguments, [ - astNode, { ...expectedContext, data: null } + astNode, { ...expectedContext, data: null, signals: ProbeRunner.Signals } ]); assert.deepEqual(fakeProbe.initialize.mock.calls.at(0)?.arguments, [ { sourceFile, context: undefined } @@ -266,7 +265,7 @@ describe("ProbeRunner", () => { const fakeProbe = { initialize: mock.fn(), validateNode: mock.fn((_: ESTree.Node) => [true]), - main: mock.fn(() => ProbeSignals.Skip), + main: mock.fn(() => ProbeRunner.Signals.Skip), finalize: mock.fn(), context: fakeCtx }; @@ -291,7 +290,7 @@ describe("ProbeRunner", () => { astNode, expectedContext ]); assert.deepEqual(fakeProbe.main.mock.calls.at(0)?.arguments, [ - astNode, { ...expectedContext, data: null } + astNode, { ...expectedContext, data: null, signals: ProbeRunner.Signals } ]); assert.deepEqual(fakeProbe.finalize.mock.calls.at(0)?.arguments, [ expectedContext diff --git a/workspaces/js-x-ray/test/utils/index.ts b/workspaces/js-x-ray/test/utils/index.ts index 1f15ed1..a5fc49d 100644 --- a/workspaces/js-x-ray/test/utils/index.ts +++ b/workspaces/js-x-ray/test/utils/index.ts @@ -10,7 +10,6 @@ import { } from "../../src/index.js"; import { ProbeRunner, - ProbeSignals, type Probe } from "../../src/ProbeRunner.js"; @@ -82,8 +81,8 @@ export const customProbes: Probe[] = [ node.declarations[0].init.value === "danger" ]; }, - main(node, options) { - const { sourceFile, data: calleeName } = options; + main(node, ctx) { + const { sourceFile, data: calleeName, signals } = ctx; if (node.declarations[0].init.value === "danger") { sourceFile.warnings.push({ kind: "unsafe-danger", @@ -94,7 +93,7 @@ export const customProbes: Probe[] = [ severity: "Warning" }); - return ProbeSignals.Skip; + return signals.Skip; } return null;