
trailmark v0.5.0
소스 코드의 그래프 데이터베이스 표현을 구축하고 쿼리합니다
Trailmark
소스 코드를 함수, 클래스, 호출, 시맨틱 주석으로 이루어진 쿼리 가능한 그래프로 파싱하여 보안 분석에 활용합니다.
Trailmark는 언어에 구애받지 않는 AST 파싱을 위해 tree-sitter를, 고성능 그래프 순회를 위해 rustworkx를 사용합니다. 장기적인 비전은 이 그래프를 뮤테이션 테스팅 및 커버리지 기반 퍼징과 결합하여, 사용자 입력으로부터 도달 가능한 가정과 테스트 커버리지 사이의 격차를 식별하는 것입니다.
작동 방식
Trailmark는 parse, index, query의 세 단계로 동작합니다.
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로 파싱하고 다음을 추출합니다:
- 노드 — 함수, 메서드, 클래스, 구조체, 인터페이스, 트레이트, 열거형, 모듈, 네임스페이스
- 엣지 — 호출, 상속, 구현, 포함, 임포트
- 메타데이터 — 타입 어노테이션, 순환 복잡도, 분기, 독스트링, 예외 타입
지원 언어
| 언어 | 확장자 | 주요 구성 요소 |
|---|---|---|
| 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 또는 trait 소유자를 유지합니다. 각 블록은 독립적으로 파싱되며, 구문 오류가 발생하면 파일과 블록 위치를 포함한 경고를 생성합니다. vstd::prelude::verus!와 같은 정규화된 호출도 지원됩니다. 로컬 헬퍼 함수는 ID에 자신을 둘러싼 함수를 유지합니다(예: file:outer.inner). 이러한 헬퍼에 대한 호출과 use super::*와 같은 상대 임포트는 파일에 존재하는 선언으로 해석됩니다. 서로 다른 블록에서 반복되는 로컬 이름에는 노드를 구분하기 위해 바이트 오프셋 접미사가 붙으며, 호출은 블록 범위를 존중합니다. 로컬 구조체, 열거형, 트레이트, 인라인 모듈도 둘러싼 함수를 유지합니다(예: file:outer.Guard.drop, file:outer.local.helper). 이들의 본문은 별도의 호출과 복잡도를 가집니다. 알려진 로컬 타입과 모듈에 대한 명시적 호출은 파일 내에서 해석됩니다. 단순 호출은 함수 및 모듈 범위를 검색하며, 연관 항목은 Type::method와 같은 경로를 사용합니다. 점 표기 호출은 수신자가 알려진 구조체나 열거형을 가리킬 때만 해석됩니다. self.method()를 포함한 다른 값 수신자는 해석되지 않은 상태로 남습니다. 암시적 소멸자 호출과 수신자 타입 추론은 모델링되지 않습니다. 이러한 규칙은 Verus 블록뿐만 아니라 일반 Rust에도 적용됩니다. 오프셋 접미사가 있는 선언 앞의 코드를 편집하면 해당 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 체계를 따릅니다. 디렉터리 파싱은 고유한 정의가 존재할 때 파일 간 직접 호출(bare cross-file call)을 해석하며, 모호한 파일 간 호출은 원래의 최선 추정 대상으로 남겨두고 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) | 결과(finding) 스타일 주석을 가진 노드 반환 |
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
}