# How the FLOWS_TO Taint Analysis Detects Security Vulnerabilities in Code Graph RAG

> Discover how FLOWS_TO taint analysis in Code Graph RAG detects security vulnerabilities by tracking data flow and checking exploitability in execution paths.

- Repository: [Vitali Avagyan/code-graph-rag](https://github.com/vitali87/code-graph-rag)
- Tags: deep-dive
- Published: 2026-08-13

---

**The `FLOWS_TO` taint analysis tracks untrusted data movement through code by emitting graph edges during intra-procedural walks, then uses BFS-based reachability queries to determine if vulnerabilities are actually exploitable in reachable execution paths.**

The `FLOWS_TO` relationship is the core mechanism in the `vitali87/code-graph-rag` repository for modeling how sensitive data propagates through a codebase. By capturing taint flows as native graph edges, the system can precisely answer whether untrusted input from a source (such as environment variables) can reach a dangerous sink (such as network calls). This article explains the complete pipeline from flow generation to security verdicts.

## Taint Flow Generation in the FlowProcessor

The **FlowProcessor** ([`codebase_rag/parsers/flow_access/processor.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/flow_access/processor.py)) performs the intra-procedural analysis that populates `FLOWS_TO` relationships. It walks each function's control flow, tracks how tainted values move between variables, and defers edge emission until the fixpoint is reached.

### Enabling and Configuring Flow Capture

Before processing begins, the processor verifies that the capture selection enables the `FLOWS_TO` relationship:

```python
self._enabled = selection.rel_enabled(cs.RelationshipType.FLOWS_TO)

```

It then loads language-specific I/O sink definitions (`IO_SINKS`) that classify calls as **read-sources** (where untrusted data enters), **write-sinks** (where tainted data exiting is dangerous), or **read-write sinks**.

### Intra-procedural Flow Walking

The processor selects a walking strategy based on the target language:

- **Lean flow** (`self._process_lean_flow`) – Used for non-Python languages, performing basic taint tracking without branch sensitivity.
- **Path-sensitive MAY walk** – Used for Python, evaluating each conditional branch separately and unioning taint states at merge points to balance precision and performance.

### Seeding Taint and Emitting Edges

Taint analysis begins by seeding **pseudo-origins** for every function parameter:

```python
tainted[pname] = Taint(..., {pname})

```

Free variables are similarly seeded so that taint reaching a sink becomes a capture summary. During the walk, the processor emits three categories of `FLOWS_TO` edges:

1. **Resource → Resource**: Connects a read source to a write sink when tainted data flows directly between resources.
2. **Caller → Callee**: Records argument passing via *arg edges* that link the caller's resource node to the callee's parameter.
3. **Callee → Caller**: Records return values via *return edges* connecting the callee's resource back to the caller's assignment site.

All deferred edges materialize in the `finalize()` method once the per-function return-taint fixpoint is known.

## Graph Schema and Storage

`FLOWS_TO` is defined as a relationship type in [`codebase_rag/constants/graph.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/constants/graph.py):

```python
class RelationshipType(StrEnum):
    READS_FROM = "READS_FROM"
    WRITES_TO = "WRITES_TO"
    FLOWS_TO = "FLOWS_TO"

```

It belongs to the **IO capture group**, ensuring edges are stored only when the `io` capture is enabled in the selection configuration.

## Querying Taint Reachability with FlowVerdict

The **flow-verdict** component ([`codebase_rag/flow_verdict.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/flow_verdict.py)) answers the question *"can data flow from source to sink?"* by querying the `FLOWS_TO` subgraph and performing reachability analysis.

### The Cypher Query Pattern

The system retrieves candidate edges using a Cypher query that scopes results to the target project:

```python
CYPHER_FLOW_EDGES = f"""
MATCH (a)-[:{cs.RelationshipType.FLOWS_TO.value}]->(b)
WHERE a.qualified_name STARTS WITH $project_prefix
   OR b.qualified_name STARTS WITH $project_prefix
   OR a.qualified_name = $project_name
   OR b.qualified_name = $project_name
RETURN a.qualified_name AS source, b.qualified_name AS target
"""

```

### Verdict Categories

The `flow_reachability_verdict` function implements a BFS over the retrieved edges and returns one of three verdicts:

- **FOUND** – A concrete path exists from the source qualified name to the sink qualified name through `FLOWS_TO` edges.
- **UNKNOWN** – No path is found, but **coverage gaps** exist (files not indexed for flow analysis). This prevents false negatives when the analysis is incomplete.
- **NO_FLOW** – No path exists and no coverage gaps are present, confirming the taint cannot reach the sink.

The function returns a `FlowVerdict` tuple containing the verdict, the concrete path (if any), and any uncovered files.

## Security Vulnerability Detection

Security findings are expressed as **`HAS_VULNERABILITY`** relationships pointing to `:SecurityIssue` nodes. The graph audit queries ([`codebase_rag/cypher_queries.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/cypher_queries.py)) merge `FLOWS_TO` paths with vulnerability tags to validate exploits.

For example, the system can determine whether a secret from `process.env` reaches a network-send sink by checking for a `FLOWS_TO` path from the `ENV` source to the `NETWORK` sink that is also tagged with a vulnerability. This correlation ensures only **reachable** taint flows surface as security issues, avoiding false positives from unreachable or dead code.

```python
from codebase_rag.flow_verdict import flow_reachability_verdict

def check_env_to_network(fetch_all, project_name):
    source = "ENV"          # qualified name of the source resource node

    sink   = "NETWORK"      # qualified name of the sink resource node

    verdict = flow_reachability_verdict(
        fetch_all=fetch_all,
        project_name=project_name,
        source_qn=source,
        sink_qn=sink,
    )
    print(f"Verdict: {verdict.verdict}")
    if verdict.verdict == "FOUND":
        print("Path:", " → ".join(verdict.path))
    elif verdict.verdict == "UNKNOWN":
        print("Uncovered files:", ", ".join(verdict.gaps))

```

## Summary

- The `FlowProcessor` in [`codebase_rag/parsers/flow_access/processor.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/flow_access/processor.py) generates `FLOWS_TO` edges during intra-procedural walks, using path-sensitive analysis for Python and lean analysis for other languages.
- Taint originates from **pseudo-origins** seeded from function parameters and free variables, then propagates through resource-to-resource, caller-to-callee, and callee-to-caller edges.
- The `FLOWS_TO` relationship type is defined in [`codebase_rag/constants/graph.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/constants/graph.py) and stored only when the IO capture group is enabled.
- The `flow_reachability_verdict` function in [`codebase_rag/flow_verdict.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/flow_verdict.py) queries these edges and returns **FOUND**, **UNKNOWN**, or **NO_FLOW** verdicts based on BFS reachability and coverage gap analysis.
- Security vulnerabilities are confirmed by correlating `FLOWS_TO` paths with `HAS_VULNERABILITY` relationships, ensuring only reachable taint flows generate alerts.

## Frequently Asked Questions

### What is the difference between lean flow and Python-specific flow analysis?

In [`codebase_rag/parsers/flow_access/processor.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/flow_access/processor.py), non-Python languages use a *lean* flow walk (`_process_lean_flow`) that performs basic taint tracking without branch sensitivity. For Python, the processor implements a *path-sensitive* MAY walk that evaluates conditional branches separately and unions taint states at merge points, providing more precise results for Python's complex control flow.

### How does the system avoid false positives in vulnerability detection?

The `flow_reachability_verdict` function distinguishes between **NO_FLOW** (definitely safe) and **UNKNOWN** (insufficient coverage) verdicts. By requiring a concrete BFS path through `FLOWS_TO` edges from source to sink, and by checking for coverage gaps, the system only reports **FOUND** when the vulnerability is provably reachable in the analyzed code, filtering out false positives from dead or unindexed code.

### What types of sources and sinks does FLOWS_TO track?

The processor loads language-specific `IO_SINKS` definitions that categorize calls as read-sources (where taint enters), write-sinks (where taint exiting is dangerous), or read-write sinks. Common examples include `ENV` for environment variables, `STDOUT` for output streams, and `NETWORK` for socket operations, though the exact definitions are configurable per language.

### When are FLOWS_TO edges actually written to the graph?

Edges are deferred during the intra-procedural walk and materialized only when `finalize()` is called on the `FlowProcessor` instance. This occurs after the per-function return-taint fixpoint is computed, ensuring that all return edges and cross-function flows accurately reflect the final taint state of the function.