
Build and query a graph database representation of source code
Trailmark parses source code into a graph of functions, types, calls, and other relationships, then lets you ask structural questions about it:
It supports 30 languages through tree-sitter, stores the graph in rustworkx, and exposes it through a command-line tool and a Python API. Trailmark was built for security review: every edge records whether it came straight from syntax or from a heuristic, and calls it could not resolve stay in the graph.
The long-term vision is to combine this graph with mutation testing and coverage-guided fuzzing to identify gaps between assumptions and test coverage that are reachable from user input.
--language auto merges every supported language in a tree into one graph..trailmark/entrypoints.toml.trailmark analyze --resolution counts them..trailmark/links.toml, or import a call graph produced by a binary-analysis
tool.reads and writes edges between functions and
the contract state variables they touch.uv tool install trailmark # the CLI
uv add trailmark # the library, in a uv-managed project
Requires Python ≥ 3.12. With Nix, nix run github:trailofbits/trailmark
runs the CLI directly.
This README and the guides track the main branch. The
changelog
lists what each published release includes, and the CLI and Python API guides
note the version that added each newer feature.
Most grammars are downloaded and cached on first use. The bundled Circom, Miden Assembly, and Verus grammars are compiled on first use, which needs a C compiler. See Installation for offline machines, TLS-inspecting proxies, and building from a checkout.
# Node and edge counts for a Python project (Python is the default language)
trailmark analyze --summary path/to/project
# Full JSON graph of every supported language in the tree
trailmark analyze --language auto path/to/project > graph.json
# Detected entrypoints with trust level and asset value
trailmark entrypoints --language auto path/to/project
# How the attack surface changed between two git refs
trailmark diff --repo . main HEAD
# Mermaid call graph around one function
trailmark diagram -t path/to/project -T call-graph -f handle_request
# How many call sites could not be resolved, grouped by callee name
trailmark analyze --language rust --resolution path/to/project
The same questions from Python:
from trailmark.query.api import QueryEngine
engine = QueryEngine.from_directory("path/to/project", language="auto")
engine.attack_surface() # entrypoints and their trust levels
engine.callers_of("verify_signature") # direct callers
engine.ancestors_of("verify_signature") # everything that can reach it
engine.entrypoint_paths_to("verify_signature")
engine.complexity_hotspots(10) # cyclomatic complexity >= 10
Names can be bare (verify_signature), qualified (Auth.verify), or full node
IDs (auth:Auth.verify). Query results are plain dicts and lists that serialize
directly to JSON.
flowchart LR
A["Source files"] --> B["tree-sitter parsers"]
B --> C["CodeGraph"]
C --> D["GraphStore (rustworkx)"]
D --> E["QueryEngine"]
E --> F["CLI / Python API"]
classDef src fill:#007bff26,stroke:#007bff,color:#007bff
classDef parse fill:#28a74526,stroke:#28a745,color:#28a745
classDef data fill:#6f42c126,stroke:#6f42c1,color:#6f42c1
classDef query fill:#ffc10726,stroke:#e6a817,color:#e6a817
class A src
class B parse
class C,D data
class E,F query
.trailmark/links.toml.GraphStore loads the graph into a rustworkx directed graph for
fast traversal. QueryEngine.from_directory() also runs entrypoint detection.QueryEngine resolves names to node IDs, runs traversals, and
returns JSON-serializable results.Given app.py:
class Auth:
def verify(self, token):
return self._check_sig(token)
def _check_sig(self, token): ...
def handle_request(req):
auth = Auth()
if auth.verify(req.token):
return process(req)
return deny()
def process(req): ...
def deny(): ...
Trailmark produces this graph (module containment edges omitted):
graph TD
HR["app:handle_request"] -->|calls| A["app:Auth"]
HR -->|calls| PV["proxy.unresolved:auth.verify"]
HR -->|calls| P["app:process"]
HR -->|calls| D["app:deny"]
AV["app:Auth.verify"] -->|calls| CS["app:Auth._check_sig"]
A -->|contains| AV
A -->|contains| CS
classDef fn fill:#007bff26,stroke:#007bff,color:#007bff
classDef cls fill:#6f42c126,stroke:#6f42c1,color:#6f42c1
classDef proxy fill:#dc354526,stroke:#dc3545,color:#dc3545
class HR,P,D fn
class A,AV,CS cls
class PV proxy
self._check_sig(...) resolves because self names the enclosing class.
auth.verify(...) does not, because Trailmark does not infer that the local
variable auth holds an Auth. The call is kept as a proxy node rather than
dropped. As a result, engine.ancestors_of("Auth._check_sig") returns only
app:Auth.verify, and engine.unresolved_calls("handle_request") shows the
exact call site where the path was lost. See
Graph model
for every node kind, edge kind, and ID rule, and
Call resolution
for working with proxies.