What Is a Code Property Graph?

What Is a Code Property Graph?

A code property graph is one graph that merges three different views of a program so you can ask a security question and get a real answer. Instead of reading source as flat text, you query it like a map: where is this query, does this path run, and does an attacker controlled value actually reach it. This post explains what a code property graph is, why a single view of code misses bugs, and how a security query walks it across function calls.

What a code property graph merges

Source code can be modeled in several ways, and each way answers a different kind of question. A code property graph joins these views into one queryable structure so a single query can use all of them at once.

  • Abstract syntax tree (structure). The parse tree of the code. It knows this token is a function call, that token is a string literal, this block is the body of an if. It answers “what is this piece of code.”
  • Control flow graph (order of execution). Nodes are statements, edges are the order they can run in. It answers “can execution actually get here, and after this line, what runs next.”
  • Program dependence graph (data and control dependence). Edges connect a value where it is defined to every place it is used, and connect a statement to the condition that decides whether it runs. It answers “does this value flow into that spot, and what controls it.”

Keep each view apart and you keep three partial pictures. Merge them onto shared nodes and one walk of the graph can see structure, order, and flow together.

Why one view alone misses bugs

Take a plain security question: can request input reach this SQL query. No single view answers it.

  • The syntax tree finds the query and finds where input enters, but it does not know if the value moves from one to the other, and it does not know if that line ever runs.
  • The control flow graph knows the path runs, but it treats a safe query and a dangerous one the same. It sees statements, not the meaning of the data inside them.
  • The dependence graph knows the value flows, but on its own it cannot tell you the sink is a SQL execution rather than a log line, because that is a fact about syntax.

You need all three at once. Syntax to find the query and the input. Control flow to confirm the path is reachable. Dependence to prove the value actually lands in the query. A code property graph holds them together, so one query checks the whole claim instead of three tools guessing separately.

A tainted input reaching a sink

Here is a small example in an invented app called Acme Notes. A route reads a name from the request and builds a database query.

def get_note(request):
    name = request.args.get("name")     # source: attacker controlled
    if request.method == "GET":
        q = "SELECT * FROM notes WHERE owner = '" + name + "'"
        return db.execute(q)            # sink: SQL execution

Watch each layer of the code property graph do its part on this one snippet.

  • Syntax marks request.args.get as an input source and db.execute as a SQL sink. These are facts about what the nodes are.
  • Control flow shows that the db.execute line sits inside the if and does run on a GET request. The path is real, not dead code.
  • Dependence follows name into q through the string concatenation, then into the argument of db.execute. The tainted value reaches the sink with nothing cleaning it on the way.

All three agree, so the query returns a true finding: attacker input reaches a SQL sink on a reachable path. Change one fact and the answer flips. Wrap name in a parameter binding and the dependence edge now runs through an escaping step, so the same query reports the flow as safe. This is the mechanism behind source to sink dataflow analysis.

A single view of code can tell you a query exists. Only the merged graph can tell you an attacker controlled value reaches it on a path that runs.

Walking a code property graph across function calls

Real code does not keep the source and the sink in one function. The value crosses a call boundary, and the graph has to follow it. That is interprocedural analysis, and it is where a code property graph earns its keep.

def handler(request):
    raw = request.args.get("name")   # source
    show_note(raw)

def show_note(value):
    q = "SELECT * FROM notes WHERE owner = '" + value + "'"
    db.execute(q)                    # sink in a different function

To connect the source in handler to the sink in show_note, the query walks a call edge, binds the actual argument raw to the parameter value, and continues the dependence walk inside the callee. The graph treats that argument to parameter binding as one more dependence edge, so the flow stays connected across the seam. Follow enough of these edges and you get a witness path from the request all the way to the query, even when it passes through several helpers. That stitched path is the heart of interprocedural taint analysis.

The honest limits of a code property graph

A code property graph is only as good as the parse it is built from. If the builder cannot resolve where a call goes, the edge it needs is missing, and a missing edge is not proof that no flow exists. A few cases are genuinely hard.

  • Dynamic dispatch. When the method called depends on the run time type of an object, the graph may not know which body executes, so it either guesses conservatively or misses the target.
  • Reflection. Calling a function by a string name, as with getattr(obj, name)(), hides the target from a static parse. The edge into the real callee simply is not there.
  • Function pointers. In C, a call through a pointer can reach any function whose signature fits, so the graph either over connects or under connects.

Consider handler = ACTIONS[request.args.get("op")] followed by handler(data). A parser cannot see which function handler holds, so the call edge is unresolved. A serious tool marks that edge as conservative rather than pretending it does not exist, and a human reads the source to confirm. Honest tooling tells you where its map is solid and where it is guessing.

How UnboundCompute uses the graph

UnboundCompute builds a deterministic code property graph as its factual base, so a model reasons over real structure instead of guessing from raw text. The public graph engine is lachesis, and you can browse the rest of the stack from UnboundCompute on GitHub. The graph gives grounded facts about what the code is and how data moves, which is a different job than pattern matching over text. For more on that distinction, read scanners vs research.

A code property graph does not find bugs by itself, but it is the map an autonomous researcher reads before it forms and tests an idea about where an app breaks. See what we are building on our about page.

Frequently asked questions

What is a code property graph?

A code property graph is one graph that merges three views of a program: the abstract syntax tree for structure, the control flow graph for order of execution, and the program dependence graph for how data and control flow. Joining them on shared nodes lets a single query use structure, reachability, and data flow at once, which is why the engine behind it, such as lachesis, can answer real security questions.

Why not just use an abstract syntax tree?

An abstract syntax tree knows what each piece of code is, so it can find a SQL query and find where request input enters. It cannot tell you whether that line ever runs or whether the input actually flows into the query. You need the control flow graph for reachability and dependence edges for data flow, and a code property graph carries all three together.

How does a code property graph work across function calls?

When a value crosses a call boundary, the query walks the call edge and binds the actual argument to the parameter in the callee, then keeps following dependence edges inside that function. Chaining these edges produces a witness path from a source like a request parameter to a sink like db.execute, even through several helpers. This is the basis of interprocedural taint analysis.

What are the limits of a code property graph?

The graph is only as good as the parse it is built from. Dynamic dispatch, reflection such as calling a function by a string name, and C function pointers all hide the real call target from a static parse, so an edge may be missing or conservative. A missing edge is not proof that no flow exists, so honest tooling marks the guess and a human confirms it against the source.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.