import type pg from "pg"; import type { DefinerFunction, FunctionCoverage, OwnershipModel, Persona, ProbeResult, SchemaSnapshot, SeedResult, SeededRow } from "../types.js"; /** * Probe callable `SECURITY DEFINER` functions the way the table probes probe * tables. * * The gap this closes is not hypothetical. On a real production schema — 41 * tables, row-level security on every one — the entire cross-tenant support * capability lived in definer functions, and *no policy anywhere referenced it*. * A definer function runs with its owner's privileges, so it evaluates outside * the caller's row-level security completely. Every table around it reported * clean, correctly, while the most powerful door in the database stood open and * unexamined. * * The oracle is the same one the rest of the engine uses, and it stays exact: a * finding means the function handed back a string we planted for the *other* * persona — a uuid we generated, or a canary. Nothing here decides whether a * result "looks like" a leak. * * Two things make that oracle trustworthy rather than merely plausible: * * - **Arguments we passed are not oracle tokens.** A function that echoes its * own argument back is not leaking anything, and without this rule * `get_org(:org_id)` returning `(org_id, …)` would read as a breach on every * schema that has one. * - **A control call proves the oracle can fire.** Before asking whether a * stranger can see Bob's rows, we ask whether *Bob* can, with the same * arguments. If even Bob gets back nothing we planted, then a stranger's * empty result proves nothing, and we say the function is unchecked instead * of counting a passing check. This is the function-plane twin of proving we * can re-select a planted row before probing a table. */ export interface FunctionProbeOptions { mode: "read_only" | "full"; } export interface FunctionProbeOutcome { probes: ProbeResult[]; coverage: FunctionCoverage; } export declare function probeFunctions(client: pg.PoolClient, snapshot: SchemaSnapshot, model: OwnershipModel, seedResult: SeedResult, opts: FunctionProbeOptions): Promise; type Decision = { invoke: true; } | { invoke: false; reason: string; }; export declare function invocationDecision(fn: DefinerFunction): Decision; interface CallPlan { ok: true; /** The argument list as it appears between the parentheses. */ argSql: string; values: string[]; /** Human-readable, for the reproduction. */ described: string; } type CallOutcome = CallPlan | { ok: false; reason: string; }; /** * Build the call that *is* the cross-user attempt. * * Passing the other persona's identifiers is not a guess about what the * function wants — Crossline planted those rows, so "Bob's org id" is a fact, * and handing it to a function while authenticated as Alice is exactly the * question worth asking. Where no argument can be synthesised from what we * planted, we say so and call nothing: a call with invented arguments would * return nothing whether or not the function protects anyone. */ export declare function buildCall(fn: DefinerFunction, ctx: { bob: Persona; planted: Map; }): CallOutcome; export interface OracleToken { tableId: string; /** * How firmly this identifier ties the result to *that table's row*. * * A canary is planted in a payload column, so it can only be in the result if * the row itself is. A primary key is different: other tables carry it as a * foreign key, so seeing it proves a row containing it came back, not which * table that row was in. */ strength: "canary" | "key"; } /** * The identifiers that can serve as an oracle, mapped to the table they belong * to. * * Only high-entropy strings qualify: a uuid we generated, or a canary. A row * keyed by `id = 1` is deliberately excluded — `1` turns up in counts, flags and * offsets, and a tool that reported a breach because the number one appeared in * a result set would be uninstalled the same week. The cost is silence on * integer-keyed tables with no text column, and silence is the correct thing to * trade a guess for. * * The persona's own user and org ids are excluded from the key tokens for the * same reason, in stronger form: those are the values every other table carries * as a foreign key by design. Counting them would attribute a leaked `documents` * row to `profiles`, and a finding that sends the reader to the wrong table's * policies is a false positive wearing the right table's severity. */ export declare function oracleTokens(seedResult: SeedResult): Map; export {};