Trailmark
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.
Features
- 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.
Install
uv tool install trailmark # the CLI
uv add trailmark # the library, in a uv-managed project
Requires 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.
Quick start
# 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/project
Directory 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 >= 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.
How it works
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.
Example
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.
Supported languages
| 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.
Documentation
| 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 |
Agent skills
The Trailmark plugin packages Trailmark workflows as agent skills, including call-graph summaries, diagram generation, finding triage, and differential review.
Limitations
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.
Contributing
See CONTRIBUTING.md for setup, checks, mutation testing, and how to add a language parser.
License
Apache-2.0
Metadata
Release files for trailmark 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| trailmark-0.7.0.tar.gz | 1.9 MB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| trailmark-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.7 MB
Release files / trailmark-0.7.0.tar.gz
| Download URL | trailmark-0.7.0.tar.gz |
|---|---|
| Size | 1.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
94002f517b89b38d6f5bc5faaba21763d8e5380170aa950e0e77e6010b56b3fb
|
|
BLAKE2b-256 checksum How to use checksums |
1f1c650ab4888ccfa6f7382fef43c2813c108026d16a8c0ec210ddab961270f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / trailmark-0.7.0-py3-none-any.whl
| Download URL | trailmark-0.7.0-py3-none-any.whl |
|---|---|
| Size | 1.8 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c6a99239b6d96770a47355e2122594b484701773ef81a752946623f0643e1119
|
|
BLAKE2b-256 checksum How to use checksums |
98fca3b69e3be108ee8448c4ae7a43317f52683317ae8e67d708e409cdeac029
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for trailmark 0.7.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for trailmark-0.7.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.