
trailmark v0.5.0
ソースコードのグラフデータベース表現を構築し、クエリを実行する
Trailmark
ソースコードを、関数、クラス、呼び出し、セマンティックアノテーションのクエリ可能なグラフに解析し、セキュリティ分析に活用します。
Trailmarkは、言語に依存しないAST解析にtree-sitterを、高性能なグラフ探索にrustworkxを使用しています。長期的なビジョンは、このグラフをミューテーションテストやカバレッジガイド付きファジングと組み合わせることで、ユーザー入力から到達可能な、前提とテストカバレッジの間のギャップを特定することです。
仕組み
Trailmarkは、parse、index、queryの3つのフェーズで動作します。
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. パース
言語固有のパーサーがディレクトリを走査し、各ファイルを tree-sitter AST にパースして以下を抽出します:
- ノード — 関数、メソッド、クラス、構造体、インターフェース、トレイト、列挙型、モジュール、名前空間
- エッジ — 呼び出し、継承、実装、包含、インポート
- メタデータ — 型注釈、循環的複雑度、分岐、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パーサーは、verus! { ... } ブロックから本体を持つ通常の関数、spec 関数、proof 関数を抽出します。Verus の文法は現在、波括弧で区切られたブロックのみをサポートしており、verus!(...) と verus![...] は警告付きでスキップされます。インラインモジュールは修飾 ID(例えば file.inner:function)を保持し、メソッドはその impl またはトレイトの所有者を保持します。各ブロックは独立して解析され、構文エラーはファイルとブロックの位置を含む警告を生成します。vstd::prelude::verus! のような修飾された呼び出しもサポートされています。ローカルヘルパー関数は、その ID に外側の関数を保持します(例えば 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 節内の呼び出しは抽出されません。
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
ノードIDは、曖昧さのない検索のために 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) | 2つのノード間のすべての単純な呼び出しパス |
connect_subgraphs(source, target) | 2つの名前付きサブグラフを接続するパス |
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() | グラフ全体のエクスポート |
データモデル
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
}