将源代码解析为可查询的函数、类、调用和语义注解图,用于安全分析。
Trailmark 使用 tree-sitter 进行语言无关的 AST 解析,并使用 rustworkx 进行高性能图遍历。长期愿景是将此图与变异测试和覆盖率引导的模糊测试相结合,以识别从用户输入可达的假设与测试覆盖率之间的差距。
Trailmark 分三个阶段运行:解析、索引和查询。
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
特定语言的解析器遍历目录,将每个文件解析为 tree-sitter AST,并提取:
| 语言 | 扩展名 | 关键构造 |
|---|---|---|
| Python | .py | 函数、类、方法 |
| JavaScript | .js、.jsx、.mjs、.cjs | 函数、类、箭头函数 |
| TypeScript | .ts、.tsx | 函数、类、接口、枚举 |
| PHP | .php | 函数、类、接口、trait |
| Ruby | .rb | 方法、类、模块 |
| C | .c、.h | 函数、结构体、枚举 |
| C++ | .cpp、.hpp、.cc、.hh、.cxx、.hxx | 函数、类、结构体、命名空间 |
| C# | .cs | 方法、类、接口、结构体、枚举、命名空间 |
| Java | .java | 方法、类、接口、枚举 |
| Go | .go | 函数、方法、结构体、接口 |
| Rust | .rs | 函数、结构体、trait、枚举、impl 块、verus! 块 |
| Solidity | .sol | 合约、接口、库、函数、修饰器、结构体、枚举 |
| Cairo | .cairo | 函数、trait、结构体、枚举、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 或 trait 所有者。每个块独立解析;语法错误会产生一条包含文件和块位置的警告。诸如 vstd::prelude::verus! 这样的限定调用也受支持。局部辅助函数在其 ID 中保留其所在的外层函数(例如 file:outer.inner)。对这些辅助函数的调用以及诸如 use super::* 的相对导入会解析到文件中存在的声明。不同块中重复的局部名称会获得字节偏移后缀,以保持其节点彼此不同,并且调用会遵循块作用域。局部结构体、枚举、trait 和内联模块同样保留外层函数:例如 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(动态分派或歧义解析)。
GraphStore 将 CodeGraph 加载到 rustworkx 的 PyDiGraph 中,并构建双向的 ID/索引映射,以便快速遍历。
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() | 完整图导出 |
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 代码:
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 会生成如下所示的图: