
Создавайте и запрашивайте графовое представление исходного кода.
# Trailmark
[](https://github.com/trailofbits/trailmark/actions/workflows/ci.yml)
[](https://github.com/trailofbits/trailmark/actions/workflows/mutation.yml)
Разбирает исходный код в графы функций, классов, вызовов и семантических аннотаций, пригодные для запросов, для целей анализа безопасности.
Trailmark использует [tree-sitter](https://tree-sitter.github.io/) для языконезависимого разбора AST и [rustworkx](https://www.rustworkx.org/) для высокопроизводительного обхода графа. Долгосрочная цель — объединить этот граф с мутационным тестированием и фаззингом с покрытием для выявления разрывов между предположениями и тестовым покрытием, достижимых из пользовательского ввода.
## Как это работает
Trailmark работает в три фазы: **разбор**, **индексация** и **запрос**.
```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. Разбор
Парсер для конкретного языка обходит каталог, разбирает каждый файл в AST tree-sitter и извлекает:
- **Узлы** — функции, методы, классы, структуры, интерфейсы, трейты, перечисления, модули, пространства имён
- **Рёбра** — вызовы, наследование, реализация, вложенность, импорты
- **Метаданные** — аннотации типов, цикломатическая сложность, ветвления, docstring'и, типы исключений
### Поддерживаемые языки
| Язык | Расширения | Ключевые конструкции |
| --- | --- | --- |
| Python | `.py` | функции, классы, методы |
| JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` | функции, классы, стрелочные функции |
| TypeScript | `.ts`, `.tsx` | функции, классы, интерфейсы, перечисления |
| PHP | `.php` | функции, классы, интерфейсы, трейты |
| Ruby | `.rb` | методы, классы, модули |
| C | `.c`, `.h` | функции, структуры, перечисления |
| C++ | `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx`, `.hxx` | функции, классы, структуры, пространства имён |
| C# | `.cs` | методы, классы, интерфейсы, структуры, перечисления, пространства имён |
| Java | `.java` | методы, классы, интерфейсы, перечисления |
| Go | `.go` | функции, методы, структуры, интерфейсы |
| Rust | `.rs` | функции, структуры, трейты, перечисления, блоки impl, блоки `verus!` |
| Solidity | `.sol` | контракты, интерфейсы, библиотеки, функции, модификаторы, структуры, перечисления |
| Cairo | `.cairo` | функции, трейты, структуры, перечисления, блоки impl, контракты StarkNet |
| Circom | `.circom` | шаблоны, функции, сигналы, компоненты |
| Haskell | `.hs` | функции, типы данных, классы типов, экземпляры |
| Erlang | `.erl` | функции, записи, поведения, модули |
| Miden Assembly | `.masm` | процедуры, точки входа, константы, вызовы |
| Swift | `.swift` | функции, классы, структуры, перечисления, протоколы, расширения |
| Objective-C | `.m`, `.mm`, `.h` | функции C, классы, методы (именование на основе селекторов) |
| Kotlin | `.kt`, `.kts` | функции, классы, интерфейсы, классы данных, объекты, методы |
| Dart | `.dart` | функции, классы, абстрактные классы, методы, конструкторы |
| Move | `.move` | модули, функции, импорты, прямые вызовы |
| Tact | `.tact` | контракты, структуры, получатели, функции |
| Func | `.fc`, `.func` | функции, включения, прямые вызовы |
| Sway | `.sw` | интерфейсы ABI, структуры, методы impl, функции |
| Rego | `.rego` | пакеты, импорты, правила политик, вызовы правил |
| Proto | `.proto` | сервисы, RPC, сообщения, поля, перечисления |
| Thrift | `.thrift` | сервисы, функции, структуры, поля, перечисления |
| GraphQL | `.graphql`, `.gql` | объектные типы, корневые операции, поля, перечисления |
| SQL | `.sql` | схемы, таблицы, представления, функции, процедуры |
Rust-парсер извлекает обычные функции, функции `spec` и `proof` с телами
из блоков `verus! { ... }`. Грамматика Verus в настоящее время поддерживает только
блоки, ограниченные фигурными скобками; `verus!(...)` и `verus![...]` пропускаются с предупреждением.
Встроенные модули сохраняют квалифицированные идентификаторы (например, `file.inner:function`), а
методы сохраняют своего владельца impl или trait. Каждый блок разбирается независимо;
синтаксические ошибки порождают предупреждение с указанием файла и расположения блока.
Квалифицированные вызовы, такие как `vstd::prelude::verus!`, также поддерживаются.
Локальные вспомогательные функции сохраняют в своих идентификаторах объемлющую функцию (например,
`file:outer.inner`). Вызовы этих вспомогательных функций и относительные импорты, такие как
`use super::*`, разрешаются в объявления, присутствующие в файле.
Повторяющиеся локальные имена в разных блоках получают суффикс байтового смещения, чтобы сохранить
их узлы различимыми, а вызовы учитывают область видимости блока.
Локальные структуры, перечисления, трейты и встроенные модули также сохраняют объемлющую функцию:
например, `file:outer.Guard.drop` и `file:outer.local.helper`.
Их тела имеют отдельные вызовы и сложность. Явные вызовы известных локальных
типов и модулей разрешаются в пределах файла. Вызовы без квалификации ищут в областях видимости функций и модулей;
ассоциированные элементы используют пути вида `Type::method`. Вызовы через точку разрешаются
только когда получатель именует известную структуру или перечисление. Другие получатели-значения,
включая `self.method()`, остаются неразрешенными. Неявные вызовы деструкторов и вывод
типа получателя не моделируются. Эти правила применяются как к обычному Rust, так и к
блокам Verus. Редактирование кода перед объявлением с суффиксом смещения изменяет
его ID, поэтому `trailmark diff` может сообщить об этом объявлении как об удаленном и добавленном.
Trailmark не раскрывает произвольные макросы. Литеральные вызовы Verus внутри
непрозрачных макросов или тел функций пропускаются с предупреждением. Невызванные
определения `macro_rules!` не раскрываются. Сигнатуры функций без тел
и вызовы в клаузах `requires`/`ensures` не извлекаются.
```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
```
Идентификаторы узлов следуют схеме `module:function`, `module:Class` или `module:Class.method` для однозначного поиска. При разборе каталога разрешаются вызовы между файлами без указания модуля, если существует единственное определение; неоднозначные межфайловые вызовы остаются на исходной приблизительной цели и помечаются как `uncertain`. Достоверность рёбер помечается как `certain` (прямые вызовы, `self.method()`), `inferred` (доступ к атрибутам объектов, не являющихся self) или `uncertain` (динамическая диспетчеризация или неоднозначное разрешение).
### 2. Индекс
`GraphStore` загружает `CodeGraph` в rustworkx `PyDiGraph` и строит двунаправленные сопоставления ID/индексов для быстрого обхода.
### 3. Запросы
`QueryEngine` предоставляет высокоуровневый API поверх индексированного графа:
| Метод | Описание |
| --- | --- |
| `callers_of(name)` | Прямые вызывающие для указанной цели |
| `callees_of(name)` | Прямые вызываемые для указанного источника |
| `ancestors_of(name)` | Все функции, которые транзитивно могут достичь цели (восходящий срез) |
| `reachable_from(name)` | Все функции, транзитивно достижимые из источника |
| `paths_between(src, dst)` | Все простые пути вызовов между двумя узлами |
| `connect_subgraphs(source, target)` | Пути, соединяющие два именованных подграфа |
| `entrypoint_paths_to(name)` | Пути от любой обнаруженной точки входа к цели |
| `attack_surface()` | Точки входа с тегами уровня доверия, ценности актива и атрибутов парсера, если они присутствуют |
| `complexity_hotspots(n)` | Функции с цикломатической сложностью ≥ n |
| `functions_that_raise(exc)` | Функции, в список исключений которых, обнаруженных парсером, входит `exc` |
| `generic_parameters(name)` | Обобщённые параметры типов, объявленные узлом |
| `type_references(name)` | Ссылки на типы параметров, возвращаемых значений, исключений и обобщённых границ |
| `annotate(name, kind, description, source)` | Добавить семантическую аннотацию к узлу |
| `annotations_of(name, kind=None)` | Получить аннотации для узла, при необходимости отфильтрованные по виду |
| `nodes_with_annotation(kind)` | Все узлы, помеченные указанным видом аннотации |
| `clear_annotations(name, kind=None)` | Удалить аннотации из узла |
| `diff_against(other)` | Структурное сравнение графа этого движка с другим |
| `preanalysis()` | Запустить встроенные проходы предварительного анализа и сохранить аннотации/подграфы |
| `augment_sarif(path)` | Объединить результаты SARIF с графом |
| `augment_weaudit(path)` | Объединить результаты weAudit с графом |
| `augment_binary(path)` | Объединить внешний JSON-файл графа бинарного анализа |
| `findings(kind=None)` | Вернуть узлы, несущие аннотации в стиле находок |
| `subgraph(name)` | Вернуть узлы именованного подграфа |
| `subgraph_edges(name)` | Вернуть индуцированные рёбра внутри именованного подграфа |
| `subgraph_names()` | Перечислить все именованные подграфы, присутствующие в графе |
| `summary()` | Количество узлов, количество рёбер, зависимости |
| `to_json()` | Полный экспорт графа |
### Модель данных
```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
```
**Виды узлов:** `function`, `method`, `class`, `module`, `struct`, `interface`, `trait`, `enum`, `namespace`, `contract`, `library`, `template`, `proxy`
**Происхождение узлов:** `source`, `proxy`, `binary`, `synthetic`
**Виды рёбер:** `calls`, `inherits`, `implements`, `contains`, `imports`, `resolves_to`, `type_uses`, `specializes`, `corresponds_to`
**Достоверность рёбер:** `certain`, `inferred`, `uncertain`
Неразрешённые вызовы материализуются как прокси-узлы вида
`proxy.unresolved:<raw-symbol>`, чтобы результаты обхода могли показать, где
анализ исходного кода потерял разрешение, вместо того чтобы молча отбрасывать это
ребро. Разбор каталогов, включая разбор одного языка, также применяет ссылки
репозитория и материализует рёбра `TYPE_USES`. Поддержка бинарного анализа
импортирует внешние JSON-графы вызовов; Trailmark сам не дизассемблирует
исполняемые файлы.
### Пример графа
Дан следующий код на 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 строит граф вида:
```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
```
## Установка
Приведённые ниже примеры соответствуют текущей ветке разработки. Для последней
опубликованной версии пакета установите его из PyPI. Чтобы получить именно тот
набор функций, который описан здесь, установите из рабочей копии и запускайте команды через `uv run`.
```bash
# Latest published release
uv pip install trailmark
# Current checkout / development branch
uv sync --all-groups
```
Требуется Python ≥ 3.12.
Trailmark требует `tree-sitter-language-pack` версии 1.22.1 или новее для большинства грамматик.
Кэши грамматик, принадлежащие root, поддерживаются, если они не доступны для записи группе или остальным пользователям.
Текущие релизы используют системное хранилище сертификатов для загрузки грамматик. В
средах с TLS-инспекцией или без доступа к сети предварительно заполните кэш пакета с помощью
`python -c "import tree_sitter_language_pack as p; p.download_all()"` на
соответствующей платформе, затем скопируйте полученный каталог кэша `tree-sitter-language-pack`
на целевую машину. `HTTPS_PROXY` также учитывается. Грамматика SQL
поставляется как зависимость в виде wheel-пакета `tree-sitter-sql` и не использует этот кэш.
Встроенные грамматики Circom, Miden Assembly и Verus компилируются при первом использовании.
Для этого требуются компилятор C (`cc`), заголовочные файлы для разработки Python и доступ на запись
к каталогам установленных грамматик. Файлы Rust без блоков Verus не
вызывают компиляцию. Если грамматику Verus не удаётся собрать или загрузить, Trailmark
записывает предупреждение, пропускает блоки Verus и продолжает разбор обычного Rust.
Кэш привязки Verus включает содержимое скомпилированного исходного кода, ABI Python,
платформу и параметры компилятора. Изменённые входные данные выбирают новую привязку; завершённые
сборки публикуются атомарно. Ошибки компилятора включают код завершения и
stderr в предупреждении.
Git-клоны сохраняют определение грамматики Verus и генерируют её C-парсер во время
сборок с помощью Tree-sitter CLI 0.27.1. Перед `uv sync` или `uv build` в клоне
запустите `npm ci` с включёнными скриптами установки npm; для генерации нужен Node.js.
Сгенерированный парсер и заголовочные файлы игнорируются git. Wheel-пакеты и дистрибутивы исходного кода
включают эти сгенерированные исходники, поэтому для установки опубликованных пакетов не нужны ни
Node.js, ни Tree-sitter CLI.
Редактируемые установки через `uv` повторно запускают хук сборки для восстановления отсутствующих сгенерированных файлов;
при неизменных входных данных используется существующий вывод.
## Использование
```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
```
### Обнаружение точек входа
Trailmark автоматически заполняет `graph.entrypoints`, чтобы `attack_surface()`, распространение taint-меток и пересечение границ привилегий имели данные для работы. Обнаружение выполняется в четыре слоя, каждый из которых переопределяет предыдущий:
1. **Общая эвристика `main`.** Любая функция с именем `main` на любом языке. Помечается как `user_input` / `trusted_internal` / `low`.
2. **Сканирование с учётом фреймворков.** Шаблоны декораторов, атрибутов и видимости для каждого языка — см. таблицу ниже.
3. **`pyproject.toml [project.scripts]`.** Явные CLI-цели получают повышенную классификацию доверия/актива.
4. **Файл переопределения в репозитории.** Вручную подобранные точки входа в `.trailmark/entrypoints.toml` всегда имеют приоритет.
Покрытие фреймворков:
| Язык | Обнаруживаемые фреймворки |
| --- | --- |
| 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, сервлеты |
| C# | ASP.NET Core, Azure Functions |
| PHP | Атрибуты Symfony `#[Route]` + устаревшие аннотации |
| Rust | actix-web, rocket, FFI-экспорты (`#[no_mangle]`, `pub extern "C"`), атрибуты async-main |
| Solidity | Видимость `external` / `public` |
| Cairo / StarkNet | `#[external]`, `#[view]`, `#[l1_handler]`, `#[constructor]` |
| Circom | Объявления `component main` |
| Miden Assembly | Директивы `export.<name>` |
| Haskell | Верхнеуровневые `main ::` / `main =` |
| Erlang | Функции, перечисленные в `-export([...])` |
| Swift | Атрибут приложения `@main` |
| Objective-C | Селекторы жизненного цикла `UIApplicationDelegate` (например, `application:openURL:options:`) |
| Kotlin | Аннотации Spring MVC / WebFlux (общие с Java), методы жизненного цикла компонентов Android (`onCreate`, `onReceive`, `onBind`, ...) |
| Dart | Маркеры нативных вызовов `@pragma('vm:entry-point')` |
| Go | Регистрации из стандартной библиотеки `http.HandleFunc` / `http.Handle`, регистрации обработчиков в стиле gin/chi/echo `<router>.GET/POST/...` |
| Ruby | Действия контроллеров Rails (классы, наследующие `ApplicationController` / `ActionController::*`), методы `perform` воркеров Sidekiq |
| C / C++ | Связывание `extern "C"`, `__attribute__((visibility("default")))`, `__declspec(dllexport)` |
Для всего, что пропускают эвристики, объявите точки входа явно в `.trailmark/entrypoints.toml` в корне проекта. Файл поддерживает как записи для отдельных узлов, так и записи на основе правил:
```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"
```
Когда два правила помечают один и тот же узел, более поздние записи переопределяют более ранние, поэтому размещайте общие правила первыми, а конкретные исправления — после них.
Полный справочник см. в [docs/entrypoint-patterns.md](https://github.com/trailofbits/trailmark/blob/main/docs/entrypoint-patterns.md), включая ещё не реализованные фреймворки (Express / Koa / Fastify, Laravel, Cobra, axum, warp, clap и другие) с готовыми к grep шаблонами, которые участники могут использовать для добавления новых детекторов.
Обнаружение Solidity использует метаданные парсера, а не регулярные выражения сигнатур. Объявления интерфейсов исключаются, а производные переопределения аннотируют соответствующие базовые реализации с помощью `solidity_overridden_by` вместо удаления базовой точки входа. Конкретные функции `public` и `external` остаются точками входа, включая функции `view` и `pure`; их атрибуты `solidity_visibility`, `solidity_mutability` и переопределения возвращаются `attack_surface()`, чтобы вызывающие стороны могли различать доступ только для чтения и фильтровать переопределённые базовые реализации. `attack_surface()` включает специфичные для парсера атрибуты точек входа, когда они прикреплены к базовому узлу графа.
### Межъязыковые и внешние связи
Многоязычный парсинг объединяет графы языков, но многие связи RPC, FFI, подпроцессов и хост/контракт не видны в синтаксисе исходного кода. Объявляйте их детерминированно в `.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"
target_external = true # required because target is unresolved
```
---
[Read more](https://github.com/trailofbits/trailmark)