
trailmark v0.5.0
Crea e interroga una rappresentazione a database grafico del codice sorgente.
Trailmark
Analizza il codice sorgente in grafici interrogabili di funzioni, classi, chiamate e annotazioni semantiche per l'analisi della sicurezza.
Trailmark utilizza tree-sitter per l'analisi AST indipendente dal linguaggio e rustworkx per l'esplorazione di grafici ad alte prestazioni. La visione a lungo termine è combinare questo grafico con il mutation testing e il fuzzing guidato dalla copertura per identificare lacune tra le supposizioni e la copertura dei test che sono raggiungibili dall'input utente.
Come Funziona
Trailmark opera in tre fasi: analisi, indicizzazione e interrogazione.```mermaid flowchart TD A["Source Files"] --> B["tree-sitter Parser"] B --> C["CodeGraph (nodes + edges)"] C --> D["rustworkx GraphStore"] D --> E["QueryEngine"] E --> F["JSON / Summary / Hotspots"]
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. Analisi
Un parser specifico per linguaggio esamina la directory, analizza ogni file in un AST tree-sitter ed estrae:
- **Nodi** — funzioni, metodi, classi, struct, interfacce, trait, enumerazioni, moduli, namespace
- **Archi** — chiamate, ereditarietà, implementazione, contenimento, importazioni
- **Metadati** — annotazioni di tipo, complessità ciclomatica, rami, docstring, tipi di eccezioni
### Linguaggi supportati
| Linguaggio | Estensioni | Costrutti chiave |
| --- | --- | --- |
| Python | `.py` | funzioni, classi, metodi |
| JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` | funzioni, classi, funzioni freccia |
| TypeScript | `.ts`, `.tsx` | funzioni, classi, interfacce, enumerazioni |
| PHP | `.php` | funzioni, classi, interfacce, trait |
| Ruby | `.rb` | metodi, classi, moduli |
| C | `.c`, `.h` | funzioni, struct, enumerazioni |
| C++ | `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx`, `.hxx` | funzioni, classi, struct, namespace |
| C# | `.cs` | metodi, classi, interfacce, struct, enumerazioni, namespace |
| Java | `.java` | metodi, classi, interfacce, enumerazioni |
| Go | `.go` | funzioni, metodi, struct, interfacce |
| Rust | `.rs` | funzioni, struct, trait, enumerazioni, blocchi impl |
| Solidity | `.sol` | contratti, interfacce, librerie, funzioni, modificatori, struct, enumerazioni |
| Cairo | `.cairo` | funzioni, trait, struct, enumerazioni, blocchi impl, contratti StarkNet |
| Circom | `.circom` | template, funzioni, segnali, componenti |
| Haskell | `.hs` | funzioni, tipi di dato, type class, istanze |
| Erlang | `.erl` | funzioni, record, comportamenti, moduli |
| Miden Assembly | `.masm` | procedure, punti di ingresso, costanti, invocazioni |
| Swift | `.swift` | funzioni, classi, struct, enumerazioni, protocolli, estensioni |
| Objective-C | `.m`, `.mm`, `.h` | funzioni C, classi, metodi (nomi basati su selettori) |
| Kotlin | `.kt`, `.kts` | funzioni, classi, interfacce, data class, oggetti, metodi |
| Dart | `.dart` | funzioni, classi, classi astratte, metodi, costruttori |
| Move | `.move` | moduli, funzioni, import, chiamate dirette |
| Tact | `.tact` | contratti, struct, ricevitori, funzioni |
| Func | `.fc`, `.func` | funzioni, include, chiamate dirette |
| Sway | `.sw` | interfacce ABI, struct, metodi impl, funzioni |
| Rego | `.rego` | pacchetti, import, regole di policy, chiamate a regole |
| Proto | `.proto` | servizi, RPC, messaggi, campi, enumerazioni |
| Thrift | `.thrift` | servizi, funzioni, struct, campi, enumerazioni |
| GraphQL | `.graphql`, `.gql` | tipi oggetto, operazioni radice, campi, enumerazioni |
| SQL | `.sql` | schemi, tabelle, viste, funzioni, procedure |```mermaid
flowchart TD
subgraph "Per-File Parsing"
F["Source file"] --> TS["tree-sitter AST"]
TS --> EX["Extract nodes"]
TS --> EC["Extract call edges"]
TS --> EB["Count branches"]
TS --> ET["Resolve types"]
end
EX --> CG["CodeGraph"]
EC --> CG
EB --> CG
ET --> CG
classDef src fill:#007bff26,stroke:#007bff,color:#007bff
classDef parse fill:#28a74526,stroke:#28a745,color:#28a745
classDef extract fill:#ffc10726,stroke:#e6a817,color:#e6a817
classDef data fill:#6f42c126,stroke:#6f42c1,color:#6f42c1
class F src
class TS parse
class EX,EC,EB,ET extract
class CG data
Gli ID dei nodi seguono lo schema module:function, module:Class o module:Class.method per una ricerca univoca. L'analisi delle directory risolve le chiamate semplici tra file quando esiste una definizione univoca; le chiamate ambigue tra file vengono lasciate al loro obiettivo originale di miglior sforzo e contrassegnate come uncertain. La confidenza degli archi è etichettata come certain (chiamate dirette, self.method()), inferred (accesso ad attributi su oggetti non self) o uncertain (dispatch dinamico o risoluzione ambigua).
2. Indice
Il GraphStore carica il CodeGraph in un PyDiGraph di rustworkx e costruisce mappature bidirezionali ID/indice per una rapida traversata.
3. Interrogazione
Il QueryEngine fornisce un'API di alto livello sul grafo indicizzato:
| Metodo | Descrizione |
|---|---|
callers_of(name) | Chiamanti diretti del target nominato |
callees_of(name) | Chiamati diretti della sorgente nominata |
ancestors_of(name) | Ogni funzione che può raggiungere transitivamente il target (slice verso l'alto) |
reachable_from(name) | Ogni funzione raggiungibile transitivamente dalla sorgente |
paths_between(src, dst) | Tutti i percorsi di chiamata semplici tra due nodi |
connect_subgraphs(source, target) | Percorsi che collegano due sottografi nominati |
entrypoint_paths_to(name) | Percorsi da qualsiasi entrypoint rilevato al target |
attack_surface() | Entrypoint etichettati con livello di fiducia, valore dell'asset e attributi del parser quando presenti |
complexity_hotspots(n) | Funzioni con complessità ciclomatica ≥ n |
functions_that_raise(exc) | Funzioni la cui lista di eccezioni rilevate dal parser include exc |
generic_parameters(name) | Parametri di tipo generico dichiarati da un nodo |
type_references(name) | Riferimenti di tipo per parametro, ritorno, eccezione e vincolo generico |
annotate(name, kind, description, source) | Aggiungi un'annotazione semantica a un nodo |
annotations_of(name, kind=None) | Ottieni annotazioni per un nodo, opzionalmente filtrate per tipo |
nodes_with_annotation(kind) | Ogni nodo etichettato con il tipo di annotazione dato |
clear_annotations(name, kind=None) | Rimuovi annotazioni da un nodo |
diff_against(other) | Diff strutturale del grafo di questo engine rispetto a un altro |
preanalysis() | Esegui i passaggi di pre-analisi integrati e memorizza annotazioni/sottografi |
augment_sarif(path) | Unisci i risultati SARIF nel grafo |
augment_weaudit(path) | Unisci i risultati weAudit nel grafo |
augment_binary(path) | Unisci un file JSON di grafo di analisi binaria esterna |
findings(kind=None) | Restituisci nodi che portano annotazioni di tipo 'finding' |
subgraph(name) | Restituisci i nodi in un sottografo nominato |
subgraph_edges(name) | Restituisci gli archi indotti all'interno di un sottografo nominato |
subgraph_names() | Elenca ogni sottografo nominato presente nel grafo |
summary() | Conteggi di nodi, archi, dipendenze |
to_json() | Esportazione completa del grafo |
Modello dei dati```mermaid
classDiagram class CodeGraph { language: str root_path: str nodes: dict[str, CodeUnit] edges: list[CodeEdge] annotations: dict[str, list[Annotation]] entrypoints: dict[str, EntrypointTag] dependencies: list[str] add_annotation(node_id, annotation) clear_annotations(node_id, kind=None) merge(other) }
class CodeUnit {
id: str
name: str
kind: NodeKind
location: SourceLocation
parameters: tuple[Parameter]
return_type: TypeRef
exception_types: tuple[TypeRef]
cyclomatic_complexity: int
branches: tuple[BranchInfo]
docstring: str
}
class CodeEdge {
source_id: str
target_id: str
kind: EdgeKind
confidence: EdgeConfidence
}
class Annotation {
kind: AnnotationKind
description: str
source: str
}
class EntrypointTag {
kind: EntrypointKind
trust_level: TrustLevel
description: str
asset_value: AssetValue
}
CodeGraph "1" *-- "*" CodeUnit
CodeGraph "1" *-- "*" CodeEdge
CodeGraph "1" *-- "*" Annotation
CodeGraph "1" *-- "*" EntrypointTag
**Tipi di nodo:** `function`, `method`, `class`, `module`, `struct`, `interface`, `trait`, `enum`, `namespace`, `contract`, `library`, `template`, `proxy`
**Origini dei nodi:** `source`, `proxy`, `binary`, `synthetic`
**Tipi di archi:** `calls`, `inherits`, `implements`, `contains`, `imports`, `resolves_to`, `type_uses`, `specializes`, `corresponds_to`
**Confidenza degli archi:** `certain`, `inferred`, `uncertain`
Le chiamate non risolte vengono materializzate come nodi proxy del tipo
`proxy.unresolved:<raw-symbol>` in modo che i risultati della traversata possano mostrare dove l'analisi del sorgente ha perso la risoluzione invece di eliminare silenziosamente quell'arco. Il supporto per l'analisi binaria importa grafi di chiamate JSON esterni; Trailmark non disassembla gli eseguibili da solo.
### Grafico di esempio
Dato questo codice Python:```python
class Auth:
def verify(self, token: str) -> bool:
return self._check_sig(token)
def _check_sig(self, token: str) -> bool:
...
def handle_request(req: Request) -> Response:
auth = Auth()
if auth.verify(req.token):
return process(req)
return deny()
Trailmark produce un grafico come:```mermaid graph TD HR["handle_request"] -->|calls| AV["Auth.verify"] HR -->|calls| P["process"] HR -->|calls| D["deny"] AV -->|calls| CS["Auth._check_sig"] A["Auth"] -->|contains| AV A -->|contains| CS
classDef fn fill:#007bff26,stroke:#007bff,color:#007bff
classDef cls fill:#6f42c126,stroke:#6f42c1,color:#6f42c1
class HR,P,D fn
class A,AV,CS cls
## Installazione
Gli esempi seguenti seguono il branch di sviluppo corrente. Per l'ultimo
pacchetto pubblicato, installa da PyPI. Per l'esatto insieme di funzionalità descritto
qui, installa da un checkout ed esegui i comandi con `uv run`.```bash
# Latest published release
uv pip install trailmark
# Current checkout / development branch
uv sync --all-groups
Richiede Python ≥ 3.12.
Trailmark utilizza tree-sitter-language-pack per la maggior parte delle grammatiche. Le versioni correnti utilizzano il negozio di certificati della piattaforma per i download delle grammatiche. In ambienti con ispezione TLS o offline, pre-popola la cache dei pacchetti con python -c "import tree_sitter_language_pack as p; p.download_all()" su una piattaforma corrispondente, quindi copia la directory della cache di tree-sitter-language-pack risultante sulla macchina di destinazione. Viene anche rispettata HTTPS_PROXY. La grammatica SQL viene fornita come dipendenza wheel tree-sitter-sql e non utilizza quella cache.
Utilizzo```bash
Report the installed version
trailmark --version # or: trailmark -V trailmark version # subcommand form
Full JSON graph (Python, the default)
trailmark analyze path/to/project
Analyze a different language
trailmark analyze --language rust path/to/project trailmark analyze --language javascript path/to/project
Polyglot: auto-detect and merge every supported language found in the
tree, or pass an explicit comma-separated list.
trailmark analyze --language auto path/to/project trailmark analyze --language python,rust,solidity path/to/project
Summary statistics
trailmark analyze --summary path/to/project
Complexity hotspots (threshold >= 10)
trailmark analyze --complexity 10 path/to/project
Augment the graph with external findings (SARIF from static analyzers,
weAudit findings from the VS Code extension). Each --sarif / --weaudit
flag is repeatable. Add --json to print the augmented graph.
trailmark augment --sarif results.sarif path/to/project trailmark augment --weaudit findings.json path/to/project trailmark augment --sarif a.sarif --sarif b.sarif --json path/to/project
List detected entrypoints (attack surface). Uses heuristic detection
(main() functions, pyproject.toml [project.scripts]) plus an optional
override file at .trailmark/entrypoints.toml (see below).
trailmark entrypoints path/to/project trailmark entrypoints --json path/to/project
Structural diff between two code graphs. Accepts directory paths or
git refs (branches, tags, commits). Surfaces added/removed nodes,
call-edge changes, and — most usefully — attack-surface changes.
trailmark diff before/ after/ trailmark diff --repo . main HEAD # compare git refs trailmark diff --json before/ after/ # machine-readable output
Generate a Mermaid diagram from the code graph. --type is required; the
choices are call-graph, class-hierarchy, module-deps, containment,
complexity, and data-flow. Use --focus to scope large graphs.
trailmark diagram --target path/to/project --type call-graph trailmark diagram -t path/to/project -T call-graph -f parse_file --depth 3 trailmark diagram -t path/to/project -T complexity --threshold 5 --direction LR
### Rilevamento dei punti di ingresso
Trailmark popola automaticamente `graph.entrypoints` in modo che `attack_surface()`, la propagazione delle taint e l'attraversamento dei confini di privilegio abbiano dati su cui lavorare. Il rilevamento avviene su quattro livelli, ciascuno dei quali sovrascrive il precedente:
1. **Euristico generico `main`.** Qualsiasi funzione denominata `main` in qualsiasi linguaggio. Etichettata `user_input` / `trusted_internal` / `low`.
2. **Scansione basata sul framework.** Pattern di decorator, attributi e visibilità per linguaggio — vedere la tabella sottostante.
3. **`pyproject.toml [project.scripts]`.** I target CLI espliciti ricevono una classificazione di trust/asset migliorata.
4. **File di override locale del repository.** I punti di ingresso curati manualmente in `.trailmark/entrypoints.toml` hanno sempre la precedenza.
Copertura dei framework:
| Linguaggio | Framework rilevati |
| --- | --- |
| Python | Flask, FastAPI, aiohttp, Click, Typer, Celery |
| JavaScript / TypeScript | NestJS, Next.js (App Router + Pages API), AWS Lambda |
| Java | Spring MVC / WebFlux, JAX-RS, listener Kafka, servlet |
| C# | ASP.NET Core, Azure Functions |
| PHP | Attributi Symfony `#[Route]` + annotazioni legacy |
| Rust | actix-web, rocket, esportazioni FFI (`#[no_mangle]`, `pub extern "C"`), attributi async-main |
| Solidity | Visibilità `external` / `public` |
| Cairo / StarkNet | `#[external]`, `#[view]`, `#[l1_handler]`, `#[constructor]` |
| Circom | Dichiarazioni `component main` |
| Miden Assembly | Direttive `export.<name>` |
| Haskell | `main ::` / `main =` al livello top |
| Erlang | Funzioni elencate in `-export([...])` |
| Swift | Attributo app `@main` |
| Objective-C | Selettori del ciclo di vita `UIApplicationDelegate` (es. `application:openURL:options:`) |
| Kotlin | Annotazioni Spring MVC / WebFlux (condivise con Java), metodi del ciclo di vita dei componenti Android (`onCreate`, `onReceive`, `onBind`, ...) |
| Dart | Marcatori nativi richiamabili `@pragma('vm:entry-point')` |
| Go | Registrazioni `http.HandleFunc` / `http.Handle` della stdlib, registrazioni di handler `<router>.GET/POST/...` in stile gin/chi/echo |
| Ruby | Azioni dei controller Rails (classi che ereditano `ApplicationController` / `ActionController::*`), metodi `perform` dei worker Sidekiq |
| C / C++ | Collegamento `extern "C"`, `__attribute__((visibility("default")))`, `__declspec(dllexport)` |
Per tutto ciò che gli euristici non riescono a rilevare, dichiarare i punti di ingresso esplicitamente in `.trailmark/entrypoints.toml` nella root del progetto. Il file supporta sia voci a singolo nodo che voci basate su regole:```toml
# Single-node entry
[[entrypoint]]
node = "my_module:handle_request" # node id, or "module.path:function"
kind = "api" # user_input | api | database | file_system | third_party
trust = "untrusted_external" # untrusted_external | semi_trusted_external | trusted_internal
asset_value = "high" # high | medium | low
description = "HTTP POST /auth"
# Rule: every PHP script under public_html/ is a web-exposed entrypoint.
[[entrypoint]]
file_glob = "public_html/**/*.php"
kind = "user_input"
trust = "untrusted_external"
asset_value = "high"
description = "Web-exposed PHP script"
# Rule: any function that takes a PSR-7 request object.
[[entrypoint]]
param_type = "ServerRequestInterface"
kind = "api"
trust = "untrusted_external"
asset_value = "high"
description = "PSR-7 HTTP handler"
# Rule: functions named `handle_*`.
[[entrypoint]]
name_regex = "^handle_"
kind = "api"
trust = "untrusted_external"
# Rule: conditions compose with AND — web.py files AND name starts with handle_.
[[entrypoint]]
file_glob = "public/*.py"
name_regex = "^handle_"
kind = "api"
trust = "untrusted_external"
Le voci successive sovrascrivono quelle precedenti quando due regole etichettano lo stesso nodo, quindi posiziona prima le regole generali e dopo le correzioni specifiche.
Vedi docs/entrypoint-patterns.md per il riferimento completo, inclusi framework non ancora implementati (Express / Koa / Fastify, Laravel, Cobra, axum, warp, clap e altri) con pattern pronti per grep che i contributori possono utilizzare per aggiungere nuovi rilevatori.
Il rilevamento Solidity utilizza i metadati del parser anziché le regex di firma. Le dichiarazioni di interfaccia sono escluse e un override derivato sopprime l'implementazione base corrispondente. Le funzioni concrete public ed external rimangono entrypoint, incluse le funzioni view e pure; i loro attributi solidity_visibility e solidity_mutability vengono restituiti da attack_surface() in modo che i chiamanti possano distinguere l'esposizione in sola lettura. attack_surface() include attributi entrypoint specifici del parser quando sono collegati al nodo del grafo sottostante.
Collegamenti cross-language ed esterni
L'analisi poliglotta unisce i grafi dei linguaggi, ma molte relazioni RPC, FFI, subprocess e host/contratto non sono visibili nella sintassi del codice sorgente. Dichiarale deterministicamente in .trailmark/links.toml:```toml
[[link]]
source = "backend:submit"
target = "contract:Verifier.verify"
kind = "calls" # defaults to calls
confidence = "certain" # defaults to inferred
description = "JSON-RPC eth_call"
[[link]] source = "backend:notify" target = "payments-webhook" external = true # required when either endpoint is unresolved
References may be exact node IDs or unique names/suffixes. Ambiguous references,
unknown internal endpoints, invalid enum values, and malformed TOML raise
`ValueError`. Setting `external = true` explicitly permits unresolved endpoints
and creates proxy nodes. This file is a stable public configuration interface.
### Limitazioni dell'analisi
- `entrypoint_paths_to()` segnala la raggiungibilità del grafo delle chiamate, non il flusso di dati controllato dall'attaccante. Utilizzare i risultati di taint pre-analisi come un segnale separato grossolano; Trailmark non esegue ancora l'analisi interprocedurale di taint.
- TypeScript risolve chiamate dirette e ricevitori semplici assegnati con `new ConcreteClass()`. Il dispatch di interfaccia tramite manifesti, nomi di proprietà calcolati, contenitori di dependency injection e altri meccanismi dinamici rimane a livello di best-effort.
- Il supporto SQL è orientato a PostgreSQL ed estrae schemi, tabelle, viste, funzioni, procedure e dipendenze tra routine/viste. Non è un validatore di dialetto SQL completo né un analizzatore semantico di query.
### API programmatica```python
from trailmark.parse import parse_directory, parse_file
from trailmark.query.api import QueryEngine
# Parse-only API: get the raw CodeGraph without building GraphStore/QueryEngine.
graph = parse_file("path/to/file.py")
graph = parse_directory("path/to/project", language="auto")
# Single-language (default) or auto-detect + merge across all languages
engine = QueryEngine.from_directory("path/to/project")
engine = QueryEngine.from_directory("path/to/project", language="auto")
engine = QueryEngine.from_directory("path/to/project", language="python,rust")
# Direct neighbors
engine.callers_of("handle_request")
engine.callees_of("handle_request")
# Transitive slicing — who could reach this sink, or what could it reach?
engine.ancestors_of("Auth._check_sig")
engine.reachable_from("handle_request")
# Attack-surface paths from any detected entrypoint
engine.entrypoint_paths_to("Auth._check_sig")
# All call paths between two nodes
engine.paths_between("handle_request", "Auth._check_sig")
# Functions with cyclomatic complexity >= 10
engine.complexity_hotspots(10)
# What functions can raise a given exception? (uses parser-detected
# exception_types; no runtime tracing required)
engine.functions_that_raise("PermissionError")
# Add and query semantic annotations
from trailmark.models.annotations import AnnotationKind
engine.annotate(
"handle_request",
AnnotationKind.ASSUMPTION,
"Caller has already authenticated the session token",
source="llm",
)
engine.annotations_of("handle_request")
engine.nodes_with_annotation(AnnotationKind.FINDING)
# Diff against an earlier snapshot of the same codebase
before = QueryEngine.from_directory("before/")
diff = engine.diff_against(before)
# diff contains: summary_delta, nodes {added/removed/modified},
# edges {added/removed}, entrypoints {added/removed/modified}
# Run the built-in audit-oriented preanalysis passes
engine.preanalysis()
engine.findings()
engine.subgraph_names()
# Programmatic augmentation hooks for external tooling
engine.augment_sarif("results.sarif")
engine.augment_weaudit("findings.json")
NodeKind.SCHEMA, TABLE, VIEW, and PROCEDURE sono aggiuntivi nella v0.5.0;
i consumatori che effettuano un match esaustivo dei valori enum dovrebbero aggiungere casi per essi.
Development```bash
Install package and dev dependencies
uv sync --all-groups
Lint and format
uv run ruff check --fix uv run ruff format
Type check
uv tool install ty && ty check
Tests
uv run pytest -q tests/
Mutation testing (on macOS, set this env var to avoid rustworkx fork segfaults)
OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES uv run mutmut run
## Licenza
Apache-2.0