A SAST sink is only actionable when a real entry point can reach it. Call graph reachability traces the path from HTTP handlers through every call frame to the vulnerable function - filtering noise before a single probe fires.
SAST identifies sinks where dangerous functions accept untrusted input. What it cannot answer without a call graph is whether any real entry point actually reaches that sink. A vulnerability in a function no request handler calls is not exploitable - it is noise that trains teams to ignore alerts. Call graph reachability is the filter that separates findings worth fixing this week from findings worth scheduling next quarter.
Every SAST finding starts at a sink: a SQL query built from a parameter, a file read from a path, a subprocess call wrapping user input. The CWE is accurate. The finding is only actionable when a request from outside the trust boundary can actually reach that function. Call graph analysis builds a directed graph of every function call in the codebase, traces forward from HTTP route handlers, message queue consumers, and scheduled jobs, and flags only the sinks that sit on a path reachable from a real entry point. A sink only called from unit tests, or buried behind an authentication wall that the entry point cannot cross, stays off the list.
The taint path is the chain from where user-controlled data enters the program - the source - to where it reaches a dangerous function - the sink - traced through every intermediate call. Inter-procedural analysis follows that chain across function and file boundaries. A handler that passes a request parameter to a service function, which extracts a field and passes it to a repository, which interpolates it into a query string, has a taint path that spans three files and three layers. Intra-procedural analysis sees each layer in isolation and may raise no finding. Inter-procedural analysis follows the value across each call edge and surfaces the finding at the true entry point - the source - not buried at the sink function name.
# routes/user.py - source: user-controlled value enters here def update_profile(request): user_id = request.args.get("id") # [taint source] return profile_service.save(user_id) # taint propagates into service layer # services/profile.py - taint crosses the module boundary def save(user_id: str): query = f"SELECT * FROM users WHERE id = {user_id}" # [sink] return db.execute(query) # inter-procedural path confirmed: source -> sink
The confirming artifact is the full call path - not just the sink function in isolation. The finding carries the source location, every intermediate call frame, and the sink, so the remediation targets the right layer: parameterise the query at the repository, or reject non-integer input at the route handler. Both are correct fixes; only the call path tells you which one removes the taint earlier.
A CVE in a dependency says the package contains a vulnerability. Call graph reachability answers the narrower question: does the application actually call the specific function the CVE describes? A deserialization bug in the XML parsing module of a library the application uses only for JSON serialization is not a path. The CVE is accurate; the finding in this context is noise that masks the ones that need a same-week fix.
apPosture traces call edges from entry points through first-party code and into the dependency tree. If the graph reaches the specific symbol the CVE affects - the vulnerable class method or function, not just the package name - the finding enters the prioritised queue as reachable. If no path connects, the finding is recorded as present-but-unreachable, excluded from the SLA clock, and visible in the posture for completeness without polluting the actionable backlog.
Static call graphs have two known weak spots: dynamic dispatch and reflection. A factory method that instantiates the right class at runtime, a plugin loader, a dependency injection container - these create call edges the graph cannot follow from source alone. The analysis may miss paths behind these patterns, or mark more sinks as reachable than actually are under real traffic. Neither problem invalidates the approach; both are reasons to treat static reachability as the filter before a runtime probe, not as the final verdict on exploitability.
| Analysis mode | What it confirms | Known blind spot |
|---|---|---|
| SAST only | Sink location and CWE; taint path when statically resolvable | Dynamic dispatch, reflection, runtime plugin loading |
| DAST only | Runtime exploitability for the probed endpoint and parameter | Sink location in source; code paths not exercised by the probe |
| Call graph + DAST | Source location, full call path, and runtime confirmation | None for statically traceable paths - highest-confidence state |
DAST closes the gap in both directions. A runtime probe that successfully triggers a sink confirms the path is exercisable under real conditions, regardless of what the call graph predicted. A probe that fails to trigger a sink the graph said was reachable reveals a dynamic dispatch the static analysis could not resolve. When SAST traces a source-to-sink path and DAST confirms it in the runtime response, the finding reaches the highest-confidence band available: source-located, path-traced, and runtime-proven. That band drives the risk score to its peak and the SLA clock to its shortest interval.
The call graph is the filter applied before DAST probes run, so runtime confirmation effort concentrates on paths that are statically plausible. A finding that arrives with a full source-to-sink trace, a DAST-confirmed runtime response, and a remediation that targets the source rather than a symptom three layers downstream is the kind of report a team acts on the same day it arrives - not the kind it queues behind a hundred unverified pattern matches.