Trailmark parses source code into a graph of functions, types, calls, and other relationships, then lets you ask structural questions about it:
- Which entrypoints can reach this function, and along which call paths?
- What changed in the attack surface between two commits?
- Which functions write this Solidity storage variable?
- Where did call resolution stop, so that reachability results are incomplete?
It parses the supported 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.
- Code graphs. Functions, methods, classes, structs, traits, contracts, and
modules, connected by calls, inheritance, containment, and type use.
--language automerges every supported language in a tree into one graph. - Attack surface. Framework-aware entrypoint detection (Flask, FastAPI,
Spring, ASP.NET Core, actix-web, NestJS, Solidity visibility, and more), each
tagged with a trust level and asset value. Override or extend detection with
.trailmark/entrypoints.toml. - Reachability. Direct callers and callees, transitive slices, and bounded call paths from entrypoints to a target.
- Structural diffs. Compare two directories or git refs and report added or removed declarations, signature and type changes, call-edge confidence changes, and entrypoints.
- Diagrams. Mermaid call graphs, class hierarchies, module dependencies, containment views, complexity maps, and entrypoint-to-target flows.
- Findings on the graph. Import SARIF and weAudit results, attach your own annotations, and run preanalysis passes (blast radius, privilege boundaries, coarse taint) that store their results as annotations and named subgraphs.
- Visible gaps. Calls that cannot be resolved become proxy nodes instead of
disappearing, and
trailmark analyze --resolutioncounts them. - Links Trailmark cannot see. Declare RPC, FFI, and subprocess edges in
.trailmark/links.toml, or import a call graph produced by a binary-analysis tool. - Solidity storage access.
readsandwritesedges between functions and the contract state variables they touch.
uv tool install trailmark # the CLI
uv add trailmark # the library, in a uv-managed projectRequires Python ≥ 3.12. CI tests Python 3.12 through 3.15.
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, Move, 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.
Skills integrators: see v0.7 compatibility for schema, identity, state, and reachability checks.
# Node and edge counts for a Python project (Python is the default language)
trailmark analyze --summary path/to/project
# The same summary as JSON, including source-file counts per language (v0.7+)
trailmark analyze --summary --json --language auto 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/projectDirectory analysis rejects missing roots and zero matching source files. The
summary includes selected languages and inspected file counts; use
--allow-empty for an intentional empty input.
Graph JSON includes the exporting Trailmark version and an export schema version
(v0.7+); compare snapshots built with the same Trailmark version and languages.
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 >= 10Names 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
- Parse. A parser for each language walks the directory and extracts nodes
(functions, types, modules), edges (calls, inheritance, containment, and so on),
and metadata (parameters, return types, branches, cyclomatic complexity,
docstrings, raised exceptions). A linking pass then resolves calls across
files, links types to their declarations, and applies
.trailmark/links.toml. - Index.
GraphStoreloads the graph into a rustworkx directed graph for fast traversal.QueryEngine.from_directory()also runs entrypoint detection. - Query.
QueryEngineresolves 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.
| Language | Extensions | --language |
Extracts |
|---|---|---|---|
| Python | .py |
python |
functions, classes, methods |
| R | .r |
r |
named/anonymous functions, closures, defaults, branches, calls, package/source dependencies |
| JavaScript | .js, .jsx, .mjs, .cjs |
javascript |
functions, classes, arrow functions |
| TypeScript | .ts, .tsx |
typescript |
functions, classes, interfaces, enums |
| PHP | .php |
php |
functions, classes, interfaces, traits |
| Perl | .pl, .pm |
perl |
packages, subroutines, signatures/prototypes, anonymous subs, script calls, imports |
| Ruby | .rb |
ruby |
methods, classes, modules |
| Lua | .lua |
lua |
local/global functions, table methods, anonymous functions, chunks, imports, direct calls |
| C | .c, .h |
c |
functions, structs, enums |
| Bash | .sh, .bash |
bash |
functions, script execution, branches, commands, literal source dependencies |
| C++ | .cpp, .cc, .cxx, .hpp, .hh, .hxx |
cpp |
functions, classes, structs, namespaces |
| C# | .cs |
c_sharp |
methods, classes, interfaces, structs, enums, namespaces |
| PowerShell | .ps1, .psm1 |
powershell |
functions, filters, classes, methods, script blocks, parameters, command calls, imports |
| Java | .java |
java |
methods, classes, interfaces, enums |
| VB.NET | .vb |
vb |
namespaces, modules, types, methods, constructors/accessors, lambdas, overloads, direct calls |
| Go | .go |
go |
functions, methods, structs, interfaces |
| Rust | .rs |
rust |
functions, structs, traits, enums, impl blocks, verus! blocks |
| Solidity | .sol |
solidity |
contracts, interfaces, libraries, functions, modifiers, structs, enums, state variables |
| Aiken | .ak |
aiken |
validators, handlers, helper functions, custom types, imports, direct and pipeline calls |
| Cairo | .cairo |
cairo |
functions, traits, structs, enums, impl blocks, StarkNet contracts |
| Circom | .circom |
circom |
templates, functions, signals, components |
| Haskell | .hs |
haskell |
functions, data types, type classes, instances |
| Julia | .jl |
julia |
modules, types, long/short functions, closures, signatures, branches, imports, conservative local calls |
| Erlang | .erl |
erlang |
functions, records, behaviours, modules |
| Miden Assembly | .masm |
masm |
procedures, entrypoints, constants, invocations |
| Swift | .swift |
swift |
functions, classes, structs, enums, protocols, extensions |
| Objective-C | .m, .mm, .h |
objc |
C functions, classes, methods (selector-based naming) |
| Kotlin | .kt, .kts |
kotlin |
functions, classes, interfaces, data classes, objects, methods |
| Dart | .dart |
dart |
functions, classes, abstract classes, methods, constructors |
| Move | .move |
move |
modules, functions, imports, direct calls |
| Tact | .tact |
tact |
contracts, structs, receivers, functions |
| Func | .fc, .func |
func |
functions, includes, direct calls |
| Sway | .sw |
sway |
ABI interfaces, structs, impl methods, functions |
| Zig | .zig |
zig |
Functions, named structs/enums/unions, container methods, imports, direct calls |
| Rego | .rego |
rego |
packages, imports, policy rules, rule calls |
| Proto | .proto |
proto |
services, RPCs, messages, fields, enums |
| Thrift | .thrift |
thrift |
services, functions, structs, fields, enums |
| GraphQL | .graphql, .gql |
graphql |
object types, root operations, fields, enums |
| SQL | .sql |
sql |
schemas, tables, views, functions, procedures |
Pass --language auto to detect languages, or a comma-separated list such as
--language python,rust. Language notes
covers Rust and Verus scoping, Solidity storage access, header handling, and
other per-language behavior.
| Guide | What it covers |
|---|---|
| CLI reference | Every subcommand, flag, and output format |
| Python API | QueryEngine methods, name lookup, path queries, the parse-only API |
| Graph model | Node and edge kinds, confidence, node IDs, proxies, directory walking |
| Entrypoints | Detection layers, framework coverage, .trailmark/entrypoints.toml |
| Cross-language links | .trailmark/links.toml for RPC, FFI, and external edges |
| Findings and preanalysis | SARIF, weAudit, binary call graphs, annotations, preanalysis passes |
| Call resolution | Measuring unresolved calls and what they mean for reachability |
| Language notes | Rust and Verus, Solidity state, TypeScript, SQL, C-family headers |
| Parser coverage | Callable ownership, declaration identity and grammar limits |
| Installation | Grammar caches, offline installs, bundled grammars, Nix, checkouts |
| Changelog | What changed in each release |
The Trailmark plugin packages Trailmark workflows as agent skills, including call-graph summaries, diagram generation, finding triage, and differential review.
Trailmark builds its graph from syntax and naming. It does not compile code, so:
- Paths are call-graph reachability, not attacker-controlled data flow. Preanalysis taint is a coarse, separate signal.
- Dynamic dispatch, overloads that need argument types, and calls through receivers whose type would have to be inferred stay unresolved.
- Macro handling is partial: bounded C declaration wrappers can be expanded, while arbitrary preprocessing and implicit calls such as destructors are not modeled.
- A missing edge is not proof that a call or storage access cannot happen.
Each guide lists the limits specific to its feature.
See CONTRIBUTING.md for setup, checks, mutation testing, and how to add a language parser.
Apache-2.0