/** * Whole-program binding resolver for the audit scanner. * * The per-call pattern matchers in `patterns.ts` only see one * `CallExpression` in isolation. That misses the most common real-world * shape of the Ox-Security stdio-RCE class: a `child_process` member is * bound to a *renamed* local and then called through that local, so the * dotted callee name no longer contains `child_process` / `exec`. * * The cold-cross review of v0.1.1 found four false-negative shapes that * the name-only matcher silently passes: * * 1. `const execAsync = promisify(exec); execAsync(`ls ${x}`)` * — promisify(exec) is THE canonical way MCP servers `await` exec. * 2. `import cp from "node:child_process"; cp.exec(`ls ${x}`)` * via a default/namespace import bound to an arbitrary local name. * 3. `const { exec: sh } = require("child_process"); sh(`ls ${x}`)` * — destructure-rename off `require`. * 4. `import { exec as run } from "node:child_process"; run(`ls ${x}`)` * — named-import alias. * * This module runs a single pre-pass over the AST, resolves every local * identifier (and object member) that provably originates from * `child_process` to its canonical method name, and lets the scanner * rewrite a call's callee to that canonical name before the rules run. * * Conservatism is the whole point of a security audit: the resolver only * ever maps bindings whose origin is *proven* to be `child_process` * (an import from `node:child_process` / `child_process`, or a * `require("child_process")`). It never guesses. Resolution is purely * *additive* — the name-based matching in `patterns.ts` is left intact, * so a binding the resolver cannot prove is simply matched the old way. * That guarantees the layer cannot introduce a false negative relative * to the previous behaviour, and the only new findings are real aliases. */ import type { TSESTree } from "@typescript-eslint/types"; export interface BindingResolution { /** * Local identifier name → canonical child_process method * (e.g. `sh` → `exec`). A call `sh(...)` is then treated as * `child_process.exec(...)`. */ identifierToMethod: Map; /** * Local identifier name → it is a child_process namespace/default * binding (e.g. `cp` from `import cp from "node:child_process"`), so * `cp.exec(...)` resolves to the `exec` method. */ namespaceLocals: Set; /** Local names bound to `promisify` (bare or via `util.promisify`). */ promisifyLocals: Set; /** Local names bound to a `util` namespace (for `util.promisify`). */ utilLocals: Set; } /** * True if `node` is a `require("child_process")` call expression. */ declare function isRequireOf(node: TSESTree.Node | null | undefined, sources: Set): boolean; /** * Resolve the child_process method that `arg` refers to, if any. Handles * a bare identifier (`exec`) and a member (`cp.exec`). */ declare function resolveMethodOfExpression(arg: TSESTree.Node | null | undefined, out: BindingResolution): string | null; /** * Pre-pass: walk the whole program collecting child_process bindings. * * Two ordered passes so that `const ea = promisify(exec)` resolves even * when the `exec` import / require appears anywhere in the file: pass 1 * records imports + require bindings + promisify locals; pass 2 records * promisify-derived locals once the method bindings are known. */ export declare function resolveBindings(program: TSESTree.Node): BindingResolution; /** * Resolve the canonical child_process method for a call, using the * binding map. Returns null when the call is not a *resolvable alias* * (in which case the name-based matcher in patterns.ts still applies). */ export declare function resolveCallMethod(node: TSESTree.CallExpression, bindings: BindingResolution): string | null; export declare const __test__: { isRequireOf: typeof isRequireOf; resolveMethodOfExpression: typeof resolveMethodOfExpression; CHILD_PROCESS_METHODS: Set; }; export {}; //# sourceMappingURL=bindings.d.ts.map