
Construire et interroger une représentation en base de données graphe du code source.
Analyse du code source en graphes interrogeables de fonctions, classes, appels et annotations sémantiques pour l'analyse de sécurité.
Trailmark utilise tree-sitter pour l'analyse AST indépendante du langage et rustworkx pour le parcours de graphe haute performance. La vision à long terme est de combiner ce graphe avec des tests de mutation et du fuzzing guidé par la couverture afin d'identifier les écarts entre les hypothèses et la couverture de test qui sont accessibles depuis une entrée utilisateur.
Trailmark opère en trois phases : analyse, indexation et interrogation.```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. Analyse
Un analyseur spécifique au langage parcourt le répertoire, analyse chaque fichier en un AST tree-sitter et extrait :
- **Nœuds** — fonctions, méthodes, classes, structures, interfaces, traits, énumérations, modules, espaces de noms
- **Arêtes** — appels, héritage, implémentation, contenance, importations
- **Métadonnées** — annotations de type, complexité cyclomatique, branches, docstrings, types d'exceptions
### Langages pris en charge
| Langage | Extensions | Constructions clés |
| --- | --- | --- |
| Python | `.py` | fonctions, classes, méthodes |
| JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` | fonctions, classes, fonctions fléchées |
| TypeScript | `.ts`, `.tsx` | fonctions, classes, interfaces, énumérations |
| PHP | `.php` | fonctions, classes, interfaces, traits |
| Ruby | `.rb` | méthodes, classes, modules |
| C | `.c`, `.h` | fonctions, structures, énumérations |
| C++ | `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx`, `.hxx` | fonctions, classes, structures, espaces de noms |
| C# | `.cs` | méthodes, classes, interfaces, structures, énumérations, espaces de noms |
| Java | `.java` | méthodes, classes, interfaces, énumérations |
| Go | `.go` | fonctions, méthodes, structures, interfaces |
| Rust | `.rs` | fonctions, structures, traits, énumérations, blocs impl |
| Solidity | `.sol` | contrats, interfaces, bibliothèques, fonctions, modificateurs, structures, énumérations |
| Cairo | `.cairo` | fonctions, traits, structures, énumérations, blocs impl, contrats StarkNet |
| Circom | `.circom` | templates, fonctions, signaux, composants |
| Haskell | `.hs` | fonctions, types de données, classes de types, instances |
| Erlang | `.erl` | fonctions, enregistrements, comportements, modules |
| Miden Assembly | `.masm` | procédures, points d'entrée, constantes, invocations |
| Swift | `.swift` | fonctions, classes, structures, énumérations, protocoles, extensions |
| Objective-C | `.m`, `.mm`, `.h` | fonctions C, classes, méthodes (nommage basé sur les sélecteurs) |
| Kotlin | `.kt`, `.kts` | fonctions, classes, interfaces, classes de données, objets, méthodes |
| Dart | `.dart` | fonctions, classes, classes abstraites, méthodes, constructeurs |
| Move | `.move` | modules, fonctions, importations, appels directs |
| Tact | `.tact` | contrats, structures, récepteurs, fonctions |
| Func | `.fc`, `.func` | fonctions, inclusions, appels directs |
| Sway | `.sw` | interfaces ABI, structures, méthodes impl, fonctions |
| Rego | `.rego` | packages, importations, règles de politique, appels de règles |
| Proto | `.proto` | services, RPC, messages, champs, énumérations |
| Thrift | `.thrift` | services, fonctions, structures, champs, énumérations |
| GraphQL | `.graphql`, `.gql` | types d'objets, opérations racines, champs, énumérations |
| SQL | `.sql` | schémas, tables, vues, fonctions, procédures |```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
Les identifiants de nœuds suivent le schéma module:function, module:Class ou module:Class.method pour une recherche sans ambiguïté. L'analyse des répertoires résout les appels inter-fichiers nus lorsqu'une définition unique existe ; les appels inter-fichiers ambigus sont laissés à leur cible initiale du mieux possible et marqués uncertain. La confiance des arêtes est étiquetée comme certain (appels directs, self.method()), inferred (accès aux attributs sur des objets non-self), ou uncertain (dispatch dynamique ou résolution ambiguë).
Le GraphStore charge le CodeGraph dans un PyDiGraph de rustworkx et construit des correspondances d'identifiants/d'index bidirectionnelles pour un parcours rapide.
Le QueryEngine fournit une API de haut niveau sur le graphe indexé :
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
**Types de nœuds :** `function`, `method`, `class`, `module`, `struct`, `interface`, `trait`, `enum`, `namespace`, `contract`, `library`, `template`, `proxy`
**Origines des nœuds :** `source`, `proxy`, `binary`, `synthetic`
**Types d'arêtes :** `calls`, `inherits`, `implements`, `contains`, `imports`, `resolves_to`, `type_uses`, `specializes`, `corresponds_to`
**Confiance des arêtes :** `certain`, `inferred`, `uncertain`
Les appels non résolus sont matérialisés sous forme de nœuds proxy tels que
`proxy.unresolved:<raw-symbol>` afin que les résultats de parcours puissent montrer où
l'analyse de la source a perdu la résolution au lieu de supprimer silencieusement cette arête. La prise en charge de l'analyse binaire importe des graphes d'appels JSON externes ; Trailmark ne désassemble pas lui-même les exécutables.
### Exemple de graphe
Avec ce code 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 produit un graphique comme :```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
## Installation
Les exemples ci-dessous suivent la branche de développement actuelle. Pour la dernière
version publiée, installez depuis PyPI. Pour l'ensemble exact des fonctionnalités décrites
ici, installez à partir d'un clone et exécutez les commandes via `uv run`.```bash
# Latest published release
uv pip install trailmark
# Current checkout / development branch
uv sync --all-groups
Nécessite Python ≥ 3.12.
Trailmark utilise tree-sitter-language-pack pour la plupart des grammaires. Les versions actuelles utilisent le magasin de certificats de la plateforme pour les téléchargements de grammaires. Dans les environnements inspectés par TLS ou hors ligne, pré-remplissez le cache de paquets avec python -c "import tree_sitter_language_pack as p; p.download_all()" sur une plateforme compatible, puis copiez le répertoire de cache tree-sitter-language-pack résultant sur la machine cible. HTTPS_PROXY est également pris en charge. La grammaire SQL est livrée en tant que dépendance wheel tree-sitter-sql et n'utilise pas ce cache.
trailmark --version # or: trailmark -V trailmark version # subcommand form
trailmark analyze path/to/project
trailmark analyze --language rust path/to/project trailmark analyze --language javascript path/to/project
trailmark analyze --language auto path/to/project trailmark analyze --language python,rust,solidity path/to/project
trailmark analyze --summary path/to/project
trailmark analyze --complexity 10 path/to/project
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
trailmark entrypoints path/to/project trailmark entrypoints --json path/to/project
trailmark diff before/ after/ trailmark diff --repo . main HEAD # compare git refs trailmark diff --json before/ after/ # machine-readable output
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
### Détection des points d'entrée
Trailmark peuple automatiquement `graph.entrypoints` afin que `attack_surface()`, la propagation de la contamination et le franchissement des frontières de privilèges disposent des données nécessaires. La détection s'effectue en quatre couches, chacune remplaçant la précédente :
1. **Heuristique générique `main`.** Toute fonction nommée `main`, quel que soit le langage. Étiquetée `user_input` / `trusted_internal` / `low`.
2. **Analyse adaptée au framework.** Patterns de décorateurs, d'attributs et de visibilité par langage — voir le tableau ci-dessous.
3. **`pyproject.toml [project.scripts]`.** Les cibles CLI explicites bénéficient d'une classification de confiance / actif rehaussée.
4. **Fichier de remplacement local au dépôt.** Les points d'entrée soigneusement sélectionnés dans `.trailmark/entrypoints.toml` l'emportent toujours.
Couverture des frameworks :
| Langage | Frameworks détectés |
| --- | --- |
| Python | Flask, FastAPI, aiohttp, Click, Typer, Celery |
| JavaScript / TypeScript | NestJS, Next.js (App Router + Pages API), AWS Lambda |
| Java | Spring MVC / WebFlux, JAX-RS, Kafka listeners, servlets |
| C# | ASP.NET Core, Azure Functions |
| PHP | Symfony `#[Route]` attributes + legacy annotations |
| Rust | actix-web, rocket, FFI exports (`#[no_mangle]`, `pub extern "C"`), async-main attributes |
| Solidity | `external` / `public` visibility |
| Cairo / StarkNet | `#[external]`, `#[view]`, `#[l1_handler]`, `#[constructor]` |
| Circom | `component main` declarations |
| Miden Assembly | `export.<name>` directives |
| Haskell | top-level `main ::` / `main =` |
| Erlang | functions listed in `-export([...])` |
| Swift | `@main` app attribute |
| Objective-C | `UIApplicationDelegate` lifecycle selectors (e.g. `application:openURL:options:`) |
| Kotlin | Spring MVC / WebFlux annotations (shared with Java), Android component lifecycle methods (`onCreate`, `onReceive`, `onBind`, ...) |
| Dart | `@pragma('vm:entry-point')` native-callable markers |
| Go | `http.HandleFunc` / `http.Handle` stdlib registrations, gin/chi/echo-style `<router>.GET/POST/...` handler registrations |
| Ruby | Rails controller actions (classes inheriting `ApplicationController` / `ActionController::*`), Sidekiq worker `perform` methods |
| C / C++ | `extern "C"` linkage, `__attribute__((visibility("default")))`, `__declspec(dllexport)` |
Pour tout ce que les heuristiques ne détectent pas, déclarez explicitement les points d'entrée dans `.trailmark/entrypoints.toml` à la racine du projet. Ce fichier prend en charge aussi bien les entrées mono-nœud que les entrées basées sur des règles :
```toml
# Single-node: simple, high-trust (e.g. a public-safety audit endpoint)
"src/api/audit.py" = { trust = "high", asset = "critical", taint = "trusted_internal" }
# Rule-based: all files matching a pattern in a module
"src/ingestion/**/*.py" = { trust = "medium", asset = "usual", taint = "user_input" }
# Rule-based: any Python file, anywhere
"**/*.py" = { trust = "low", asset = "unknown", taint = "user_input" }
``````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"
Les entrées ultérieures remplacent les précédentes lorsque deux règles ciblent le même nœud, placez donc les règles générales en premier et les corrections spécifiques après.
Voir docs/entrypoint-patterns.md pour la référence complète, y compris les frameworks non encore implémentés (Express / Koa / Fastify, Laravel, Cobra, axum, warp, clap, et autres) avec des motifs prêts pour grep que les contributeurs peuvent utiliser pour ajouter de nouveaux détecteurs.
La détection Solidity utilise les métadonnées du parseur plutôt que des expressions régulières de signatures. Les déclarations d'interface sont exclues et une substitution dérivée supprime l'implémentation de base correspondante. Les fonctions concrètes public et external restent des points d'entrée, y compris les fonctions view et pure ; leurs attributs solidity_visibility et solidity_mutability sont renvoyés par attack_surface() afin que les appelants puissent distinguer l'exposition en lecture seule. attack_surface() inclut les attributs de point d'entrée spécifiques au parseur lorsqu'ils sont attachés au nœud du graphe sous-jacent.
L'analyse polyglotte fusionne les graphes de langues, mais de nombreuses relations RPC, FFI, sous-processus et hôte/contrat ne sont pas visibles dans la syntaxe source. Déclarez-les de manière déterministe dans .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
Les références peuvent être des ID de nœuds exacts ou des noms/suffixes uniques. Les références ambiguës, les endpoints internes inconnus, les valeurs d'énumération invalides et le TOML malformé lèvent une `ValueError`. Définir `external = true` permet explicitement les endpoints non résolus et crée des nœuds proxy. Ce fichier constitue une interface de configuration publique stable.
### Limitations de l'analyse
- `entrypoint_paths_to()` signale l'accessibilité du graphe d'appels, et non le flux de données contrôlé par un attaquant. Utilisez les résultats de taint de pré-analyse comme un signal séparé grossier ; Trailmark n'effectue pas encore d'analyse de taint interprocédurale.
- TypeScript résout les appels directs et les récepteurs simples assignés avec `new ConcreteClass()`. Le dispatch d'interface via des manifestes, des noms de propriétés calculées, des conteneurs d'injection de dépendances et d'autres mécanismes dynamiques reste au mieux.
- Le support SQL est orienté PostgreSQL et extrait les schémas, tables, vues, fonctions, procédures et dépendances de routines/vues. Il ne s'agit pas d'un validateur complet de dialecte SQL ou d'un analyseur sémantique de requêtes.
### API programmatique```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, et PROCEDURE sont ajoutés dans v0.5.0;
les consommateurs qui effectuent une correspondance exhaustive des valeurs d'énumération devraient ajouter des cas pour elles.
uv sync --all-groups
uv run ruff check --fix uv run ruff format
uv tool install ty && ty check
uv run pytest -q tests/
OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES uv run mutmut run
## Licence
Apache-2.0
| Méthode | Description |
|---|
callers_of(name) | Appelants directs de la cible nommée |
callees_of(name) | Appelés directs de la source nommée |
ancestors_of(name) | Toutes les fonctions pouvant atteindre transitivement la cible (tranche ascendante) |
reachable_from(name) | Toutes les fonctions atteignables transitivement depuis la source |
paths_between(src, dst) | Tous les chemins d'appel simples entre deux nœuds |
connect_subgraphs(source, target) | Chemins reliant deux sous-graphes nommés |
entrypoint_paths_to(name) | Chemins de tout point d'entrée détecté vers la cible |
attack_surface() | Points d'entrée étiquetés avec niveau de confiance, valeur d'actif et attributs d'analyseur syntaxique lorsqu'ils sont présents |
complexity_hotspots(n) | Fonctions avec une complexité cyclomatique ≥ n |
functions_that_raise(exc) | Fonctions dont la liste des exceptions détectées par l'analyseur inclut exc |
generic_parameters(name) | Paramètres de type générique déclarés par un nœud |
type_references(name) | Références de type pour les paramètres, retours, exceptions et bornes génériques |
annotate(name, kind, description, source) | Ajouter une annotation sémantique à un nœud |
annotations_of(name, kind=None) | Obtenir les annotations pour un nœud, éventuellement filtrées par type |
nodes_with_annotation(kind) | Tous les nœuds étiquetés avec le type d'annotation donné |
clear_annotations(name, kind=None) | Supprimer les annotations d'un nœud |
diff_against(other) | Diff structurel du graphe de ce moteur par rapport à un autre |
preanalysis() | Exécuter les passes d'analyse préliminaire intégrées et stocker les annotations/sous-graphes |
augment_sarif(path) | Fusionner les résultats SARIF dans le graphe |
augment_weaudit(path) | Fusionner les résultats weAudit dans le graphe |
augment_binary(path) | Fusionner un fichier JSON de graphe d'analyse binaire externe |
findings(kind=None) | Retourner les nœuds portant des annotations de type découverte |
subgraph(name) | Retourner les nœuds d'un sous-graphe nommé |
subgraph_edges(name) | Retourner les arêtes induites à l'intérieur d'un sous-graphe nommé |
subgraph_names() | Lister tous les sous-graphes nommés actuellement sur le graphe |
summary() | Comptages de nœuds, comptages d'arêtes, dépendances |
to_json() | Export complet du graphe |