Source: 2af6143 — Velociraptor is a Go-based endpoint monitoring, detection, and forensic collection tool built around VQL (Velocidex Query Language). At ~1,400 Go files and 418 YAML artifact definitions, it is a substantial, production-grade DFIR platform.
How It Works
Velociraptor deploys as a single binary that can operate in three modes: client (endpoint agent), frontend (server), or GUI (local triage tool). The entire system revolves around VQL — a SQL-like query language that describes what to collect rather than how to collect it. Users define “artifacts” (YAML files containing VQL queries, parameters, and metadata), and the server compiles these into VQLCollectorArgs protobufs that get pushed to clients for execution.
The client-server communication uses a custom crypto layer (in crypto/) over HTTP/WebSocket. Clients enroll via certificate exchange, poll for tasks, execute VQL queries locally, and stream results back. The server maintains an artifact repository, schedules collections, manages hunts, and stores results in a file-based datastore (with MySQL/PostgreSQL support for indexing).
The architecture deliberately keeps clients thin — they don’t need built-in artifact knowledge. The server sends both the VQL and any dependent artifact definitions with each collection request. This allows artifact editing on the server without redeploying clients.
Architecture
┌─────────────┐ gRPC/HTTP ┌──────────────────┐
│ GUI / API │ ◄──────────────► │ Frontend │
│ (Angular) │ │ (api/, server/) │
└─────────────┘ └────────┬─────────┘
│
┌───────────┼───────────┐
│ Services Layer │
│ (services/) │
│ launcher, journal, │
│ hunt_dispatcher, │
│ indexing, orgs, ... │
└───────────┬───────────┘
│
┌───────────┴───────────┐
│ Datastore │
│ (datastore/) │
│ file-based + remote │
└────────────────────────┘
│ │
┌────┴─────┐ ┌─────┴──────┐
│ Client │ ◄────────►│ Crypto + │
│ Executor │ HTTP/WS │ Comms │
│ (executor)│ │(http_comms) │
└───────────┘ └────────────┘
Key modules:
| Module | Role |
|---|---|
vql/ | VQL plugin/function registration and scope management |
vql_plugins/ | Blank-import aggregator — registers all VQL plugins at startup |
executor/ | Client-side VQL execution engine |
actions/ | Client action handlers (VQLClientAction, events, transactions) |
services/ | Server-side service orchestrator (~30 services: launcher, journal, hunts, orgs, indexing, etc.) |
http_comms/ | Client-server communication (polling, enrollment, WebSocket) |
crypto/ | End-to-end encryption, key management, certificate handling |
datastore/ | Persistence layer (file-based default, remote for distributed setups) |
artifacts/ | 418 built-in YAML artifact definitions across Windows/Linux/MacOS |
flows/ | Flow lifecycle management — artifact collection state machines |
api/ | gRPC/HTTP API surface + server builder |
The Spine
Entry point: bin/main.go — a kingpin CLI app. Subcommands (gui, client, frontend, query, etc.) select the startup path. All commands import vql_plugins via blank import to register VQL capabilities before anything runs.
Server startup path (startup/frontend.go):
StartFrontendServices()creates aServiceManagerwith a cancellable context and wait group- Starts throttler, org manager, API server builder
- The org manager spins up all configured services (launcher, journal, hunt dispatcher, indexing, etc.)
- Each service receives
ctx,wg, andconfig_obj— the standard service triple
Client startup path (startup/client.go):
StartClientServices()creates a lighter service set (journal, repository, inventory, launcher, event table)- Builds a
ClientExecutorwith inbound/outbound protobuf channels http_commsconnects the executor to the server via encrypted HTTP
Request lifecycle (artifact collection):
- User initiates collection via API →
Launcher.ScheduleArtifactCollection() - Launcher compiles
ArtifactCollectorArgs→VQLCollectorArgs(resolves parameters, dependencies, tools) - Serialized protobuf queued for client
- Client
executorreceivesVeloMessage, dispatches toVQLClientAction.StartQuery() - VQL queries execute against local scope with registered plugins
- Results streamed back through
responder→ outbound channel → comms → server - Server
flowspackage manages collection context, stores results viafile_store
Key Patterns
Service orchestrator pattern. Every subsystem is a “service” started through ServiceManager.Start(). Services receive a shared context.Context (cancelled on shutdown), a sync.WaitGroup (for graceful teardown), and a config object. This is consistent and predictable — the same pattern from the tiniest DNS cache to the hunt dispatcher.
VQL registration via init(). Plugins and functions self-register in package-level init() functions using RegisterPlugin() / RegisterFunction(). The vql_plugins/plugins.go file aggregates all imports. This is idiomatic Go but creates a global mutable registry — ordering matters, and duplicate names panic at startup.
Artifact-as-data, not code. Artifacts are YAML that embed VQL. The server compiles them into protobuf before sending to clients. Clients never see raw artifact YAML — they receive pre-compiled VQLCollectorArgs. This is a critical design decision that enables server-side artifact customization without client updates.
Config via protobuf. The entire configuration schema is defined in config/proto/config.proto (1,615 lines). This gives strong typing and backward compatibility but makes the config structure monolithic.
Scope-based caching. VQL scopes carry a $cache variable (ScopeCache in vql/scope.go) — a synchronized map that plugins use to share state within a query execution. This avoids redundant computation but is implicit — there is no type safety on cache values.
Non-Obvious Details
Global mutable state in VQL registration
exportedPluginsandexportedFunctionsare package-level maps protected by a mutex.OverridePlugin()andOverrideFunction()exist to replace registered plugins at runtime (used in tests and for platform-specific overrides). This means plugin behavior can change after registration, which is powerful but easy to misuse.
The
unimplemented.gopattern
vql/unimplemented.goprovides stubs for platform-specific VQL plugins. On non-Windows builds, Windows-only plugins likecrypto(),registry(), andwmi()resolve to stubs that return errors. This keeps cross-compilation clean without build tags on every file.
Artifact compilation is the performance bottleneck
The launcher compiles artifacts into VQLCollectorArgs before sending to clients. For hunts (same artifact, thousands of clients), the compilation result is cached to avoid redundant work. This caching is essential for scale — without it, launching a hunt across 10k endpoints would recompile the same artifact 10k times.
Client never trusts its own artifacts
The design explicitly states “the client never uses its built-in artifacts” — every collection ships the full artifact definition from the server. This is a security property (server controls what runs) but also means the client-side artifact repository at startup is mostly empty, populated per-query from server-sent definitions.
Critical files worth understanding first:
services/launcher.go— the compilation pipeline from artifact to VQLactions/vql.go— where VQL actually executes on the clientvql/vql.go— plugin/function registration machineryhttp_comms/comms.go(~1,240 lines) — the entire client-server communication protocolservices/services.go— the 80-line service manager that orchestrates everything
Assessment
Strengths:
- Single-binary, multi-mode design is excellent for DFIR practitioners — same binary for server, client, and local triage
- Artifact system is genuinely innovative — decoupling what to collect from how to deploy is a major operational advantage
- Service orchestrator is clean and consistent; the
ctx + wg + configpattern makes the codebase navigable despite its size - 418 built-in artifacts provide out-of-the-box coverage for common forensic collection scenarios
- VQL is expressive and extensible — the plugin system allows adding capabilities without modifying core
Concerns:
- Monolithic config proto (1,615 lines) makes configuration hard to reason about — changes ripple across many subsystems
- Global plugin registry with runtime overrides creates subtle coupling; testing requires careful setup/teardown
http_comms/comms.goat 1,240 lines is doing too much — enrollment, polling, WebSocket, retry logic, and encryption are interleaved- File-based datastore as default has scaling limitations (acknowledged; remote datastore exists but adds complexity)
- CGO dependency (SQLite, YARA, etc.) complicates cross-compilation and binary distribution
Recommendations:
- Consider splitting
http_commsinto distinct enrollment, transport, and retry components - The config proto would benefit from decomposition — separate client config, server config, and service-specific configs
- Plugin registration could use a more structured approach (e.g., provider interfaces) to reduce global mutable state
Related
- analyzing-nuclei — another security tool using a custom DSL (Nuclei templates vs VQL)
- analyzing-misp — threat intelligence platform; Velociraptor can integrate with MISP via VQL
- analyzing-opencti — CTI platform; complementary to Velociraptor’s endpoint visibility
- analyzing-bloodhound — AD attack path tool; Velociraptor can collect similar data via VQL artifacts
- analyzing-prowler — cloud security assessments; different domain but similar “configurable checks” pattern