
trailmark v0.5.0
Construir e consultar uma representação de banco de dados em grafo do código-fonte
Trailmark
Analise código fonte em grafos consultáveis de funções, classes, chamadas e anotações semânticas para análise de segurança.
O Trailmark usa tree-sitter para análise de AST independente de linguagem e rustworkx para travessia de grafos de alto desempenho. A visão de longo prazo é combinar este grafo com testes de mutação e fuzzing guiado por cobertura para identificar lacunas entre suposições e cobertura de teste que são alcançáveis a partir da entrada do usuário.
Como Funciona
O Trailmark opera em três fases: analisar, indexar e consultar.```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. Análise
Um parser específico da linguagem percorre o diretório, analisa cada arquivo em uma AST tree-sitter e extrai:
- **Nós** — funções, métodos, classes, structs, interfaces, traits, enums, módulos, namespaces
- **Arestas** — chamadas, herança, implementação, contenção, imports
- **Metadados** — anotações de tipo, complexidade ciclomática, branches, docstrings, tipos de exceção
### Linguagens Suportadas
| Linguagem | Extensões | Principais construtos |
| --- | --- | --- |
| Python | `.py` | funções, classes, métodos |
| JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` | funções, classes, funções seta |
| TypeScript | `.ts`, `.tsx` | funções, classes, interfaces, enums |
| PHP | `.php` | funções, classes, interfaces, traits |
| Ruby | `.rb` | métodos, classes, módulos |
| C | `.c`, `.h` | funções, structs, enums |
| C++ | `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx`, `.hxx` | funções, classes, structs, namespaces |
| C# | `.cs` | métodos, classes, interfaces, structs, enums, namespaces |
| Java | `.java` | métodos, classes, interfaces, enums |
| Go | `.go` | funções, métodos, structs, interfaces |
| Rust | `.rs` | funções, structs, traits, enums, blocos impl |
| Solidity | `.sol` | contratos, interfaces, bibliotecas, funções, modificadores, structs, enums |
| Cairo | `.cairo` | funções, traits, structs, enums, blocos impl, contratos StarkNet |
| Circom | `.circom` | templates, funções, sinais, componentes |
| Haskell | `.hs` | funções, tipos de dados, classes de tipo, instâncias |
| Erlang | `.erl` | funções, registros, comportamentos, módulos |
| Miden Assembly | `.masm` | procedimentos, pontos de entrada, constantes, invocações |
| Swift | `.swift` | funções, classes, structs, enums, protocolos, extensões |
| Objective-C | `.m`, `.mm`, `.h` | funções C, classes, métodos (nomenclatura baseada em seletores) |
| Kotlin | `.kt`, `.kts` | funções, classes, interfaces, classes de dados, objetos, métodos |
| Dart | `.dart` | funções, classes, classes abstratas, métodos, construtores |
| Move | `.move` | módulos, funções, imports, chamadas diretas |
| Tact | `.tact` | contratos, structs, receptores, funções |
| Func | `.fc`, `.func` | funções, includes, chamadas diretas |
| Sway | `.sw` | interfaces ABI, structs, métodos impl, funções |
| Rego | `.rego` | pacotes, imports, regras de política, chamadas de regras |
| Proto | `.proto` | serviços, RPCs, mensagens, campos, enums |
| Thrift | `.thrift` | serviços, funções, structs, campos, enums |
| GraphQL | `.graphql`, `.gql` | tipos de objeto, operações raiz, campos, enums |
| SQL | `.sql` | esquemas, tabelas, visões, funções, procedimentos |```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
Os IDs de nó seguem o esquema module:function, module:Class ou module:Class.method para consulta inequívoca. A análise de diretório resolve chamadas entre arquivos isoladas quando existe uma definição única; chamadas ambíguas entre arquivos são deixadas no seu destino de melhor esforço original e marcadas como uncertain. A confiança da aresta é marcada como certain (chamadas diretas, self.method()), inferred (acesso a atributos em objetos não-self) ou uncertain (despacho dinâmico ou resolução ambígua).
2. Índice
O GraphStore carrega o CodeGraph em um PyDiGraph do rustworkx e constrói mapeamentos bidirecionais de ID/índice para travessia rápida.
3. Consulta
O QueryEngine fornece uma API de alto nível sobre o grafo indexado:
| Método | Descrição |
|---|---|
callers_of(name) | Chamadores diretos do alvo nomeado |
callees_of(name) | Calados diretos da fonte nomeada |
ancestors_of(name) | Todas as funções que podem alcançar transitivamente o alvo (fatia ascendente) |
reachable_from(name) | Todas as funções transitivamente alcançáveis a partir da fonte |
paths_between(src, dst) | Todos os caminhos de chamada simples entre dois nós |
connect_subgraphs(source, target) | Caminhos conectando dois subgrafos nomeados |
entrypoint_paths_to(name) | Caminhos de qualquer ponto de entrada detectado até o alvo |
attack_surface() | Pontos de entrada marcados com nível de confiança, valor do ativo e atributos do analisador quando presentes |
complexity_hotspots(n) | Funções com complexidade ciclomática ≥ n |
functions_that_raise(exc) | Funções cuja lista de exceções detectadas pelo analisador inclui exc |
generic_parameters(name) | Parâmetros de tipo genérico declarados por um nó |
type_references(name) | Referências de tipo de parâmetro, retorno, exceção e limites genéricos |
annotate(name, kind, description, source) | Adicionar uma anotação semântica a um nó |
annotations_of(name, kind=None) | Obter anotações para um nó, opcionalmente filtradas por tipo |
nodes_with_annotation(kind) | Todos os nós marcados com o tipo de anotação dado |
clear_annotations(name, kind=None) | Remover anotações de um nó |
diff_against(other) | Diff estrutural deste grafo do mecanismo vs. outro |
preanalysis() | Executar as passagens de pré-análise embutidas e armazenar anotações/subgrafos |
augment_sarif(path) | Mesclar descobertas SARIF no grafo |
augment_weaudit(path) | Mesclar descobertas weAudit no grafo |
augment_binary(path) | Mesclar um arquivo JSON de grafo de análise binária externa |
findings(kind=None) | Retornar nós que carregam anotações do tipo descoberta |
subgraph(name) | Retornar os nós em um subgrafo nomeado |
subgraph_edges(name) | Retornar arestas induzidas dentro de um subgrafo nomeado |
subgraph_names() | Listar todos os subgrafos nomeados atualmente no grafo |
summary() | Contagens de nós, contagens de arestas, dependências |
to_json() | Exportação completa do grafo |
Modelo de Dados```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
**Tipos de nó:** `function`, `method`, `class`, `module`, `struct`, `interface`, `trait`, `enum`, `namespace`, `contract`, `library`, `template`, `proxy`
**Origens do nó:** `source`, `proxy`, `binary`, `synthetic`
**Tipos de aresta:** `calls`, `inherits`, `implements`, `contains`, `imports`, `resolves_to`, `type_uses`, `specializes`, `corresponds_to`
**Confiança da aresta:** `certain`, `inferred`, `uncertain`
Chamadas não resolvidas são materializadas como nós proxy, como
`proxy.unresolved:<raw-symbol>`, para que os resultados da travessia possam mostrar onde a
análise de código-fonte perdeu a resolução, em vez de descartar silenciosamente essa aresta. O
suporte a análise binária importa grafos de chamadas JSON externos; o Trailmark não
desmonta executáveis por si só.
### Exemplo de Grafo
Dado este código 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 produz um grafo como:```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
## Instalação
Os exemplos abaixo acompanham o branch de desenvolvimento atual. Para o pacote mais recente publicado, instale a partir do PyPI. Para o conjunto exato de funcionalidades descrito aqui, instale a partir de um checkout e execute os comandos via `uv run`.```bash
# Latest published release
uv pip install trailmark
# Current checkout / development branch
uv sync --all-groups
Requer Python ≥ 3.12.
Trailmark utiliza tree-sitter-language-pack para a maioria das gramáticas. As versões atuais utilizam o repositório de certificados da plataforma para downloads de gramáticas. Em ambientes com inspeção TLS ou offline, pré-popule o cache de pacotes com python -c "import tree_sitter_language_pack as p; p.download_all()" em uma plataforma compatível e, em seguida, copie o diretório de cache tree-sitter-language-pack resultante para a máquina de destino. HTTPS_PROXY também é respeitado. A gramática SQL é fornecida como dependência do wheel tree-sitter-sql e não utiliza esse cache.
Usage```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
### Detecção de pontos de entrada
O Trailmark preenche automaticamente `graph.entrypoints` para que `attack_surface()`, a propagação de taint e a travessia de fronteiras de privilégio tenham dados com os quais trabalhar. A detecção é executada em quatro camadas, cada uma substituindo a anterior:
1. **Heurística genérica `main`.** Qualquer função chamada `main` em qualquer linguagem. Marcada como `user_input` / `trusted_internal` / `low`.
2. **Varredura com reconhecimento de framework.** Padrões de decorador, atributo e visibilidade por linguagem — veja a tabela abaixo.
3. **`pyproject.toml [project.scripts]`.** Destinos explícitos de CLI recebem uma classificação de confiança/ativo atualizada.
4. **Arquivo de substituição local do repositório.** Pontos de entrada selecionados manualmente em `.trailmark/entrypoints.toml` sempre vencem.
Cobertura de frameworks:
| Linguagem | Frameworks detectados |
| --- | --- |
| 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 atributos `#[Route]` + anotações legadas |
| Rust | actix-web, rocket, exportações FFI (`#[no_mangle]`, `pub extern "C"`), atributos async-main |
| Solidity | visibilidade `external` / `public` |
| Cairo / StarkNet | `#[external]`, `#[view]`, `#[l1_handler]`, `#[constructor]` |
| Circom | declarações `component main` |
| Miden Assembly | diretivas `export.<name>` |
| Haskell | `main ::` / `main =` de nível superior |
| Erlang | funções listadas em `-export([...])` |
| Swift | atributo de aplicativo `@main` |
| Objective-C | seletores de ciclo de vida `UIApplicationDelegate` (ex.: `application:openURL:options:`) |
| Kotlin | anotações Spring MVC / WebFlux (compartilhadas com Java), métodos de ciclo de vida de componentes Android (`onCreate`, `onReceive`, `onBind`, ...) |
| Dart | marcadores `@pragma('vm:entry-point')` chamáveis nativamente |
| Go | registros de stdlib `http.HandleFunc` / `http.Handle`, registros de handlers no estilo gin/chi/echo `<router>.GET/POST/...` |
| Ruby | ações de controller Rails (classes que herdam `ApplicationController` / `ActionController::*`), métodos `perform` de worker Sidekiq |
| C / C++ | linkage `extern "C"`, `__attribute__((visibility("default")))`, `__declspec(dllexport)` |
Para qualquer coisa que as heurísticas percam, declare pontos de entrada explicitamente em `.trailmark/entrypoints.toml` na raiz do projeto. O arquivo suporta entradas baseadas em nó único e em regras:```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"
Entradas posteriores sobrescrevem as anteriores quando duas regras marcam o mesmo nó, então coloque regras amplas primeiro e correções específicas depois.
Veja docs/entrypoint-patterns.md para a referência completa, incluindo frameworks ainda não implementados (Express / Koa / Fastify, Laravel, Cobra, axum, warp, clap e outros) com padrões prontos para grep que contribuidores podem usar para adicionar novos detectores.
A detecção de Solidity usa metadados do parser em vez de regex de assinatura. Declarações de interface são excluídas e uma sobrescrita derivada suprime a implementação base correspondente. Funções concretas public e external permanecem como pontos de entrada, incluindo funções view e pure; seus atributos solidity_visibility e solidity_mutability são retornados por attack_surface() para que os chamadores possam distinguir a exposição somente leitura. attack_surface() inclui atributos de ponto de entrada específicos do parser quando eles estão anexados ao nó do grafo subjacente.
Links entre linguagens e externos
O parsing poliglota mescla grafos de linguagens, mas muitas relações RPC, FFI, subprocesso e host/contrato não são visíveis na sintaxe do código-fonte. Declare estas deterministicamente em .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
Referências podem ser IDs de nós exatos ou nomes/sufixos únicos. Referências ambíguas, endpoints internos desconhecidos, valores de enumeração inválidos e TOML malformado geram `ValueError`. Definir `external = true` permite explicitamente endpoints não resolvidos e cria nós proxy. Este arquivo é uma interface de configuração pública estável.
### Limitações da análise
- `entrypoint_paths_to()` relata a acessibilidade do grafo de chamadas, não o fluxo de dados controlado por atacante. Use resultados de taint da pré-análise como um sinal separado grosseiro; o Trailmark ainda não realiza análise de taint interprocedural.
- O TypeScript resolve chamadas diretas e receptores simples atribuídos com `new ConcreteClass()`. O despacho de interface por meio de manifestos, nomes de propriedades computadas, contêineres de injeção de dependência e outros mecanismos dinâmicos permanece no melhor esforço.
- O suporte a SQL é orientado ao PostgreSQL e extrai esquemas, tabelas, visões, funções, procedimentos e dependências de rotinas/visões. Não é um validador completo de dialeto SQL ou analisador semântico de consultas.
### API programática```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 e PROCEDURE são aditivos na v0.5.0;
consumidores que fazem correspondência exaustiva de valores de enum devem adicionar casos para eles.
Desenvolvimento```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
## Licença
Apache-2.0