Skip to main content

Trailmark

CI Mutation Testing

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 auto merges 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 --resolution counts 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. reads and writes edges 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
  1. 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.
  2. Index. GraphStore loads the graph into a rustworkx directed graph for fast traversal. QueryEngine.from_directory() also runs entrypoint detection.
  3. Query. QueryEngine resolves 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)

Source distribution for trailmark 0.7.0
File Size Uploaded Provenance
trailmark-0.7.0.tar.gz 1.9 MB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for trailmark 0.7.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page