/** * Cross-File Call Resolution * * Resolves method calls to their definitions across file boundaries, * enabling cross-file taint tracking. */ import type { CallInfo, MethodInfo, CircleIR, TaintSink, DFGDef, DFGUse } from '../types/index.js'; import { SymbolTable } from './symbol-table.js'; import { TypeHierarchyResolver } from './type-hierarchy.js'; /** * Resolved call with target information */ export interface ResolvedCall { call: CallInfo; sourceFile: string; targetFile: string; targetMethod: string; targetClass: string; resolution: 'exact' | 'polymorphic' | 'inferred'; candidates?: string[]; } /** * Taint propagation information for a method */ export interface MethodTaintInfo { methodFqn: string; file: string; taintedParams: number[]; returnsSource: boolean; sourceType?: string; sanitizes: boolean; sanitizedTypes?: string[]; } /** * Cross-file taint flow */ export interface CrossFileTaintFlow { sourceFile: string; sourceLine: number; sourceType: string; targetFile: string; targetLine: number; targetMethod: string; flowType: 'call_arg' | 'return_value' | 'field_access'; taintedArgPositions?: number[]; } /** * Inter-procedural taint chain that spans multiple files / call sites. * * Shape: SOURCE in file A → caller's wrapper-return site → caller's sink-call * site → SINK in file B. Used when no per-file source/sink is co-located in * a single caller frame, so `findCrossFileTaintFlows()` cannot fire. */ export interface InterproceduralTaintPath { source: { file: string; line: number; type: string; }; sink: { file: string; line: number; type: string; cwe: string; }; hops: Array<{ file: string; line: number; method: string; kind: 'source' | 'wrapper_return' | 'field_write' | 'field_read' | 'sink_call' | 'sink'; }>; confidence: number; } /** * Per-type field-binding taint summary. * * Records which fields on a class hold tainted data because a method in the * class wrote a tainted parameter into the field (`this.field = param`). * Used to surface cross-instance flows of the canonical Jenkins shape: * `@DataBoundConstructor C(p)` writes `this.f = p` → another class holds a * `C` instance and reads `instance.f` → that read flows to a sink. */ export interface FieldTaintInfo { typeFqn: string; fieldName: string; fieldType: string | null; file: string; /** Methods that write tainted data into this field. */ writers: Array<{ methodFqn: string; methodName: string; writeLine: number; /** Source type carried into the field (`http_param`, `autowired`, etc.). */ sourceType: string; /** Original source line in the writer's method body. */ sourceLine: number; }>; } /** * Per-file lookup index used inside the resolver hot loops. * * The pre-3.89.0 implementation re-ran linear `.filter()` scans on * `ir.calls`, `ir.taint.sinks`, `ir.dfg.defs`, and `ir.dfg.uses` inside * the O(F·T·M) nested walks of `findInterproceduralTaintPaths`, * `findFieldBindingTaintPaths`, and `findCrossFileTaintFlows`. On large * Java corpora (Sa-Token 895 files, langchain4j 1696 files) that pattern * burned 5B+ filter ops and produced the #141 cross-file hang. * * Building this index once per file at first use collapses each O(N) filter * to an O(1) Map lookup. Membership and ordering are byte-equivalent to the * original filters (range buckets preserve sort-by-line order; per-line * buckets preserve insertion order, which matches Array.filter semantics). */ export interface FileIndex { /** Calls bucketed by `location.line`. Preserves original array order within each line. */ callsByLine: Map; /** DFG defs bucketed by `line`. Preserves original array order. */ defsByLine: Map; /** DFG uses bucketed by `line`. Preserves original array order. */ usesByLine: Map; /** * Calls inside each method's `[start_line, end_line]` range, sorted by line ASC. * Matches the pre-refactor `callerIR.calls.filter(...).sort(...)` output order. */ callsByMethod: Map; /** * Sinks inside each method's `[start_line, end_line]` range, sorted by line ASC. * Set-equivalent to the pre-refactor `callerIR.taint.sinks.filter(...)` output. */ sinksByMethod: Map; /** * Defs inside each method's `[start_line, end_line]` range, sorted by line ASC. * Set-equivalent to the pre-refactor `callerIR.dfg.defs.filter(...)` output. */ defsByMethod: Map; } export declare function buildFileIndex(ir: CircleIR): FileIndex; /** * CrossFileResolver - Resolves calls and tracks taint across files */ export declare class CrossFileResolver { private readonly fileIndexes; private getFileIndex; private symbolTable; private typeHierarchy; private fileIRs; private methodTaintInfo; private fieldTaintInfo; private resolvedCalls; constructor(symbolTable: SymbolTable, typeHierarchy: TypeHierarchyResolver); /** * Add a file's IR for analysis */ addFile(filePath: string, ir: CircleIR): void; /** * Resolve a call to its target method(s) */ resolveCall(call: CallInfo, fromFile: string): ResolvedCall | undefined; /** * Resolve call with a receiver (instance method call) */ private resolveWithReceiver; /** * Resolve static or local method call */ private resolveStaticOrLocal; /** * Resolve by searching all known methods */ private resolveByMethodName; /** * Infer the type of a receiver variable */ private inferReceiverType; /** * Find polymorphic candidates (implementations/subclasses) */ private findPolymorphicCandidates; /** * Analyze methods for taint propagation characteristics */ private analyzeMethodTaint; /** * Per-file analysis of cross-instance field bindings. * * Records `FieldTaintInfo` entries for fields written by: * 1. `@DataBoundConstructor`-style constructors — surfaced as * `constructor_field` sources by `LanguageSourcesPass`. * 2. Setter methods `set()` — assume the canonical * `this. = ` body shape, so the setter PARAMETER acts * as the taint conduit at call sites. * 3. `@Autowired` field annotations — the field itself is a framework * injection point; the writer is synthetic (line = field decl). * * The entries are keyed `${typeFqn}.${fieldName}` and consumed by * `findFieldBindingTaintPaths()` to surface flows of the canonical Jenkins * shape: ctor writes field → another class reads instance.field → sink. */ private analyzeFieldTaint; /** * Check if method is a taint source. * * Excludes synthetic `interprocedural_param` sources — those are per-file * meta-analysis signals saying "this method's parameter MIGHT be tainted * when called with tainted data", not confirmed external inputs. Treating * them as sources for cross-file `returnsSource` would propagate ghost * taint into every callee with typed parameters. */ private isMethodTaintSource; /** * Get source type for a method (excluding synthetic interprocedural_param) */ private getSourceType; /** * Find which parameters propagate taint to a sink within this method. * * Two heuristics, combined: * 1. Annotation-based: params with @RequestParam/@RequestBody/@PathVariable. * 2. Sink-arg matching: if a known sink call in the method body references * a param by name in its arguments, that param propagates taint to a * sink — this is the summary that cross-file chaining needs to link * a caller's tainted argument to a downstream dangerous operation. */ private findTaintedParams; /** * Check if method name suggests sanitization */ private isSanitizerMethod; /** * Get types sanitized by a method */ private getSanitizedTypes; /** * Find all callers of a method across the project */ findCallers(methodFqn: string): ResolvedCall[]; /** * Find cross-file taint flows */ findCrossFileTaintFlows(): CrossFileTaintFlow[]; /** * Find inter-procedural taint chains spanning multiple files / call sites. * * Bridges the gap that `findCrossFileTaintFlows()` cannot cover: a real * source lives in callee A, its return value bubbles up to caller C as a * tainted local, and C then passes that local to callee B which contains * the actual dangerous sink. Neither A nor C alone has a co-located * source-and-sink, but the chain A → C → B is a real vulnerability. * * Algorithm (per caller method M): * 1. Seed `tainted` with real (non-`interprocedural_param`) sources in M. * 2. Walk calls in M in line order: * a. If callee has `returnsSource = true` and is not a sanitizer, * mark every `local` DFG def at this line as tainted, anchored to * the callee's source. * b. For each tainted arg in this call, if the callee's * `taintedParams` covers that position, emit one * `InterproceduralTaintPath` per sink inside the callee body. */ findInterproceduralTaintPaths(): InterproceduralTaintPath[]; /** * Find cross-instance field-binding taint paths. * * Closes the canonical Jenkins / framework-DI shape that * `findInterproceduralTaintPaths()` cannot cover because the "source" lives * on an aliased object's field, not in a callee return: * * File A: class C { @DataBoundConstructor C(p) { this.f = p; } } * File B: class E { final C step; E(C step){ this.step = step; } * m() { String x = step.f; sink(x); } } * * Algorithm (per caller method M in file B): * 1. Seed `tainted` with sources inside M (mirrors findInterproc step 1). * 2. Scan M's local-def DFG entries for expressions of shape * `.` where receiver's declared type owns `` * in the FieldTaintInfo cache. Mark the LHS local as tainted, anchor * its origin to the field-binding writer (e.g. the ctor in file A). * 3. After seeding, walk caller-body sinks the same way * `findInterproceduralTaintPaths()` step 2c does, and also forward * tainted locals into cross-file callees whose `taintedParams` mark * the arg position as sink-propagating. */ findFieldBindingTaintPaths(): InterproceduralTaintPath[]; /** * Check whether any loaded type with name `typeName` (simple or FQN suffix) * declares a field named `fieldName`. */ private typeHasField; /** * Resolve a receiver type-name + field-name to the cache key used by * `fieldTaintInfo`. Handles simple-name receivers (e.g. `ReadTrustedStep`) * by looking up matching FQN keys across loaded files. */ private resolveFieldTaintKey; /** * Find which method a tainted arg expression references. */ private matchTaintedArg; /** * Index methods by FQN for quick lookup during chain construction. */ private buildMethodIndex; /** Return the first local-def variable name at a given line, if any. */ private getLocalDefVarAt; /** * Collect the set of variable names DFG-reachable from `sourceVar` within * a single file — the source variable itself plus every local whose * definition transitively consumes it via def-use chains (cognium-dev * #146). Bounded forward BFS over `dfg.chains`. * * Used by the cross-file variable-connectivity gate so a tainted value * that is rebound before the cross-file call still connects. Example * (Rust actix): the source variable is the extractor param `q`, but the * call passes `cmd` from `let cmd = q.get("cmd").unwrap()`; the chain * `q → cmd` makes `cmd` reachable. * * Soundness guard: the walk does NOT follow a derivation whose def is * produced on a line carrying a *cross-file-resolvable call*. Such a * value crossed a file boundary and may have been transformed or * sanitized (e.g. `String safe = sanitizer.sanitizeUrl(raw)` — the * wrapper-sanitizer negative control). Verifying whether that call * preserves taint is the job of `findInterproceduralTaintPaths`, which * is sanitizer-aware; this coarse pass must stay conservative and stop * at the boundary. Same-file / stdlib accessor calls (`q.get(...)`, * `.unwrap()`) do not resolve cross-file, so genuine taint derivatives * are still followed. */ private collectTaintReachable; /** * Forward-reachable def set from a seed set of def ids, following * `dfg.chains` (cognium-dev #146/#266). Stops at any def produced on a * line carrying a cross-file-resolvable call (potential sanitizer * boundary — the #146 soundness guard). Returns the seed ∪ all * transitively derived def ids. Empty/absent DFG → just the seed. */ private forwardReachableDefs; /** * Def-precise clobber check for the interprocedural walker (cognium-dev * #266). Given a bare-variable call arg matched to a tainted var whose * caller-side taint reaches `taintDefIds`, returns true when the DFG * proves the arg's reaching def at the call line is NOT one of those * defs — i.e. the variable was reassigned to a non-tainted value between * the taint point and the call (`a = source(); a = "safe"; run(a)`). * * Conservative: returns false whenever the proof is unavailable (no bare * variable, no precise reaching def, empty def set / DFG), so it only * removes flows the DFG positively disproves — recall risk stays minimal * for the shared interprocedural pass, and empty-DFG languages (Python) * are unaffected. */ /** * Def-precise clobber check for the cross-file gates (cognium-dev #266). * Given the matched taint variable `varName` carried by a call at * `callLine` and the source's reachable def set `taintDefIds`, returns * true when the DFG proves `varName`'s reaching def at the call is NOT * one of those defs — i.e. the variable was reassigned to a non-tainted * value between the taint point and the call * (`a = source(); a = "safe"; run(a)` / `run(&a)`). Keyed on the variable * name (not `arg.variable`) so it also covers expression args * (Rust `&a`, `"p" + a`, …). * * Conservative: returns false whenever the proof is unavailable (no * precise reaching def, empty def set / DFG), so it only removes flows * the DFG positively disproves — recall risk stays minimal for the * shared cross-file passes, and empty-DFG languages (Python) are * unaffected. */ private taintClobbered; private findRealSourceLineInMethod; /** * Get taint info for a method */ getMethodTaintInfo(methodFqn: string): MethodTaintInfo | undefined; /** * Get all resolved calls from a file */ getResolvedCallsFromFile(filePath: string): ResolvedCall[]; /** * Get statistics */ getStats(): { totalFiles: number; totalCalls: number; resolvedCalls: number; crossFileCalls: number; methodsWithTaintInfo: number; }; /** * Clear all caches */ clear(): void; /** Expose field-taint summary (for tests + diagnostics). */ getFieldTaintInfo(typeFqn: string, fieldName: string): FieldTaintInfo | undefined; } /** * Build a cross-file resolver from multiple IR results */ export declare function buildCrossFileResolver(files: Array<{ ir: CircleIR; path: string; }>, symbolTable?: SymbolTable, typeHierarchy?: TypeHierarchyResolver): CrossFileResolver; //# sourceMappingURL=cross-file.d.ts.map