This article demonstrates how to create a generic offline collector and then use it as a Velociraptor tool with conventional “online” clients.
Velociraptor offline collectors are a popular deployment mode in which one or more artifacts are collected from endpoints, the results are packaged inside a zip container, and then optionally uploaded to a remote storage destination. Typically the remote destination is a cloud storage provider such as AWS S3, Azure, or Google Cloud Storage, but it can also be a storage service set up on the local network.
Offline collectors provide the same capabilities as normal network-connected clients, but perform a one-off collection of predefined artifacts rather than being tasked to collect artifacts as needed by the server.
However, you might have Velociraptor clients deployed, and you are using these network-connected “online” clients to run smaller more focused queries, but you also occasionally want to do an offline-collector-style bulk file acquisitions.
With network-connected clients the results are uploaded to the Velociraptor server. File uploads are also done separately rather than being bundled into a single zip archive. This might not be desirable in certain circumstances, for example:
Offline collectors are created by the Server.Utils.CreateCollector
server artifact, which creates a special embeddable configuration with
the selected client artifacts, tools, and remote upload configuration
included. Since it’s all just VQL you could, in theory, create a
client artifact that replicates the actions of any offline collector.
However, in practice the
Server.Utils.CreateCollector
artifact is significantly complex. Creating a custom version of it
that can be run by clients would be a daunting task. It’s much easier
to create the offline collector in the normal way, and then run it via
a client as a tool. This is the approach taken in this article.
Offline collectors are sometimes used in situations when they aren’t really needed, or aren’t actually the best option.
Before deciding to use an offline collector, you should consider the following points.
You can collect exactly the same artifacts with online clients that you can collect with offline collectors.
Offline collectors are not the only available option when you can’t install clients. If you can’t (or don’t want to) install clients, you still have the option of using clients without installing them, as explained in the KB article How to create an “online collector” binary.
The overall goal here is to have the client download an offline collector as a tool from the server and run it. The offline collector will in turn create a collection container zip and upload it to a remote S3 server.
In this case I’ll use Garage which is an open source S3-compatible server written in Rust, but you could use AWS S3, or MinIO, or any other S3-compatible server, or even one the other remote destination options that are supported by Velociraptor offline collectors.
The high-level steps to achieve this goal are:
The Velociraptor server and clients are assumed to be set up and working correctly. As mentioned previously, the clients don’t necessarily need to be installed - they could be running without installation.
To keep things as simple as possible, I’ll use a single-node Garage server as the remote destination for the offline collector uploads. This is similar to using an SFTP or SMB server running on the local network.
Generate a basic configuration file as explained in the Garage Quickstart guide.
Configure the Garage server’s storage and access keys, also as per their quickstart guide.
Here are the commands I used (for brevity, the corresponding output is not shown):
# start the server
garage -c ./garage.toml server
# check the server status and get the node ID
garage -c ./garage.toml status
# create a cluster layout
garage -c ./garage.toml layout assign -z dc1 -c 1G d7d19e6f32edec53
# apply the layout
garage -c ./garage.toml layout apply --version 1
# create a bucket
garage -c ./garage.toml bucket create offline-collections
# check that the bucket has been created
garage -c ./garage.toml bucket list
# view the bucket configuration
garage -c ./garage.toml bucket info offline-collections
# create an identity for the collector to use
garage -c ./garage.toml key create offline-collections-key
# assign write-only access to the bucket for this user
garage -c ./garage.toml bucket allow --write offline-collections --key offline-collections-key
# create a bucket admin user
garage -c ./garage.toml key create offline-collections-owner
# assign full access to the bucket for this user
garage -c ./garage.toml bucket allow --write --read --owner offline-collections --key offline-collections-owner
I’ve created one bucket to receive the collection zips, named
offline-collections, and I’ve created 2 keys with access to it.
offline-collections-key: with only write access, that will be
used by the clients to upload their collection zipsoffline-collections-owner: with full access for browsing and
downloading the collection zips.The reason for creating 2 keys is that the write-only key will be included in the offline collector for the purpose of uploading the collection zips. Because it’s possible to extract the key from the offline collector I want it to have no greater permissions than are necessary. It only needs to be able to upload files to the bucket, and cannot list/download/modify/delete other files in the bucket.
The final bucket and access configuration can be inspected with the
command: garage -c ./garage.toml bucket info offline-collections.
==== BUCKET INFORMATION ====
Bucket: d5f9982666e004de45114bcfc1e9428755c15d12f55a2dac7bc77dc495343e97
Created: 2026-03-16 16:28:13.689 +02:00
Size: 0 B (0 B)
Objects: 0
Website access: false
Global alias: offline-collections
==== KEYS FOR THIS BUCKET ====
Permissions Access key Local aliases
W GKaa23dd92075b4dc6fc9fe54f offline-collections-key
RWO GK08c8137adf22d3b08c6ea088 offline-collections-owner
I’ll access the bucket using the offline-collections-owner
credentials on the command line using the mc (MinIO client)
utility available from https://dl.min.io/client/mc/release/.
# download the tool and make it executable
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x ./mc
# Save credentials and connection info for the mc tool
mc alias set \
garage \
http://192.168.56.1:3900 \
GK08c8137adf22d3b08c6ea088 \
389adafacb2e3b1c96f9216b9a77c89b74b4d64949d1a0e90f3ca88378f2ca4e \
--api S3v4
With the credentials configured, I’ll test access by uploading a file to the bucket, then listing the bucket’s contents, and finally clearing everything from the bucket (command output is omitted).
# copy a file to the bucket
mc cp /proc/cpuinfo garage/offline-collections/cpuinfo.txt
# list bucket contents
mc ls garage/offline-collections
# remove everything from the bucket
mc rm --force --recursive garage/offline-collections
Looks good. The S3 server is ready, so now we can create the offline collector.
In this step I am going to create a generic collector and add it to the server’s tools inventory.
There are several ways that this could be done, but I want to:
To achieve this I’ll create a server artifact that runs
Server.Utils.CreateCollector and stores the resulting offline
collector file as a tool that other client artifacts can use.
By having an artifact that performs this step, I’ll be able to easily create additional collectors with different sets of artifact selections as tools. These other collectors could have artifact selections for alternative Windows use cases, or they might be selections of artifacts chosen to target non-Windows platforms. All I need to do is select appropriate artifacts when creating additional collectors and give each collector a unique tool name.
For my first collector I intend to target Windows and have it run the following artifacts:
Windows.Triage.Targets (with HighLevelTargets = _Live)Generic.Forensic.SQLiteHunterGeneric.Client.InfoAs a test, and to ensure that I get the collector configuration right, I’ll first create it manually. Then I’ll test it on a Windows endpoint to ensure that the S3 upload aspect works correctly. To do this I run the “Build offline collector” wizard which is launched from the Server Artifacts screen.
Choose the artifacts I want to collect, as listed above.
Configure the artifact parameters.
In this case all I did was configure Windows.Triage.Targets with
the _Live High Level Target.
Configure the collector itself.
The reason I want to create and use a generic collector is because the client already has the correct Velociraptor binary - the one from which the client itself is running. There’s no reason to repack and distribute another full binary. The generic collector will be significantly smaller and provide exactly the same result.
Here I used the following settings, which correspond to my specific environment, including the configuration of my Garage S3 server. Settings not listed here were left as default.
Generic CollectorX509 Certificate/Frontend CertificateAWS Bucketoffline-collectionsgaragehttp://192.168.56.1:3900 (my garage server IP and S3_api port)YYcollector-windows-triageYLaunch the artifact and let it create the collector.
Now I can go download the collector from the artifact’s Uploaded Files tab.
I copy it over to a Windows machine that already has a Velociraptor client installed, and that therefore has the binary with which I can run the generic collector (as administrator).
C:\Program Files\Velociraptor>velociraptor -- --embedded_config collector-windows-triage
At the end of the collection it pauses, and I can see the that it reports successfully uploading the results to the Garage S3 server.
The “Delete Collection at Exit” setting removed the collection zip from the local disk after uploading it to the S3 bucket.
From my workstation I can connect to the Garage server and confirm that the collection zip and the collection log were uploaded:
$ mc ls garage/offline-collections
[2026-03-25 11:19:48 SAST] 107KiB STANDARD Collection-WIN-KMODJ1W0CYG-2026-03-25T09_19_03Z.log
[2026-03-25 11:19:44 SAST] 611MiB STANDARD Collection-WIN-KMODJ1W0CYG-2026-03-25T09_19_03Z.zip
So the manual collection works as expected, which means the collector settings are correct. However this was just a test step. The next step it to automate the creation of the collector and add it to Velociraptor’s tools inventory.
Now that I’m satisfied that my collector configuration works as expected, I’ll create a server artifact that reproduces the creation of that collector.
I want to make a few of the options configurable so I’ll add those as artifact parameters.
name: Custom.CreateCollectorTool
description: |
Creates a generic offline collector and adds it to the tool inventory.
type: SERVER
parameters:
- name: artifacts
type: json_array
default: '["Windows.Triage.Targets", "Generic.Client.Info", "Generic.Forensic.SQLiteHunter"]'
- name: artifact_parameters
type: json
default: |
{
"Generic.Forensic.SQLiteHunter": {
"DateAfter": "1970-01-01T00:00:00Z",
"DateBefore": "2100-01-01T00:00:00Z"
},
"Windows.Triage.Targets": {
"HighLevelTargets": "[\"_Live\"]"
}
}
- name: collector_target_args
type: json
default: |
{
"bucket": "offline-collections",
"credentialsKey": "GKaa23dd92075b4dc6fc9fe54f",
"credentialsSecret": "0bdfb02186ded9c01e0f755661c62e087ea2a280443d138ccaa05948e68faa78",
"region": "garage",
"endpoint": "http://192.168.56.1:3900",
"noverifycert": "Y"
}
- name: collector_tool_name
default: 'collector-windows-triage'
- name: collector_tool_version
default: '1'
- name: collector_target
default: 'S3'
sources:
- query: |
LET collector_spec <= dict(
OS="Generic",
artifacts=artifacts,
parameters=artifact_parameters,
target=collector_target,
target_args=collector_target_args,
encryption_scheme="X509",
opt_verbose="Y",
opt_banner="Y",
opt_prompt="Y",
opt_admin="Y",
opt_level=5,
opt_concurrency=2,
opt_format="jsonl",
opt_filename_template="Collection-%Hostname%-%TIMESTAMP%",
opt_collector_filename=collector_tool_name,
opt_cpu_limit=0,
opt_progress_timeout=1800,
opt_timeout=1800,
opt_delete_at_exit="Y")
SELECT inventory_add(tool=collector_tool_name,
accessor="fs",
file=Repacked.Components,
version=collector_tool_version,
serve_locally=true) AS CollectorTool
FROM Artifact.Server.Utils.CreateCollector(`**`=collector_spec)
Some things to notice about this artifact:
The Server.Utils.CreateCollector artifact accepts many parameters.
I’ve constructed a dict with key names matching the ones that can be
seen in the Requests tab after running
Server.Utils.CreateCollector in the previous test build.
The Server.Utils.CreateCollector has some parameters that I want
to be able to easily change, so I’ve added those as parameters to my
artifact. Dealing with some of their values is a bit tricky because
the JSON that you see in the Requests tab is heavily
backslash-escaped. In my artifact:
artifacts parameter needs to be a valid JSON arrayartifact_parametersand collector_target_args parameters
need to be a valid JSON objectsTo validate your JSON for these parameters, you can use an online tool such as https://jsonlint.com/, which also allows you to prettify it once it’s validated.
Tool versioning is quite important and useful, so I’ve added the
tool version as an artifact parameter. This allows me to store
multiple versions of the same tool in the tools inventory, and
potentially choose a particular version when I run my client
artifact.
In the collector_target_args parameter there are a few child
fields that are left empty by Server.Utils.CreateCollector, so
I’ve omitted those from the JSON to simplify it. I could have done
the same with the date filter parameters in
Generic.Forensic.SQLiteHunter, and it would just use the defaults
for those parameters, but I deliberately kept those fields to
highlight the nested JSON structure.
When passing the collector_spec dict to
Server.Utils.CreateCollector I’ve used
argument unpacking
(**= syntax) to make the SELECT clause more succinct.
I can now run this artifact on my server to generate the offline collector and store it in the tools repository.
The Uploaded Files tab of the collection makes the offline collector and associated spec file available for download, but I don’t need to do that because the tool is already stored in the tools inventory.
On the Results tab I can see that the tool has been added to the inventory.
I can also verify that the tool is in the inventory by running the following query in a notebook:
SELECT name, hash, version, serve_locally FROM inventory()
WHERE name =~ "collector-windows-triage"
With the tool added to the inventory, I can now create my client artifact that will use it.
It’s a very simple artifact that leverages the
Generic.Utils.FetchBinary utility artifact to fetch the generic
collector file from the tools repository.
name: Custom.RunOfflineCollectorTool
description: |
Runs a generic offline collector retrieved from the server's tools repository.
required_permissions:
- EXECVE
implied_permissions:
- FILESYSTEM_WRITE
tools:
# All we need here is the tool name and version because it already exists in
# the tools repository
- name: collector-windows-triage
version: "1"
parameters:
# You can have multiple collectors to choose from but they all need to be
# included under the `tools` top-level key.
- name: collector_name
type: choices
default: collector-windows-triage
choices:
- collector-windows-triage
sources:
- query: |
LET collector <= SELECT *
FROM Artifact.Generic.Utils.FetchBinary(
ToolName=collector_name,
IsExecutable=FALSE,
SleepDuration="0",
TemporaryOnly=TRUE)
LET host_info <= SELECT * FROM info()
LET cmd_args <= (host_info[0].Exe, "--", "--embedded_config",
collector[0].OSPath)
SELECT *
FROM execve(argv=cmd_args, sep="\n")
Some things to notice about this artifact:
It uses the info() plugin to get the full path to the Velociraptor
client’s own binary. It then uses that to run the generic collector.
For the Generic.Utils.FetchBinary artifact I’ve specified the
parameter TemporaryOnly=TRUE because it’s easy for an adversary to
print out the collector config and see what I’m collecting if the
file is left lying around on the filesystem. So by avoiding caching
the file in the local tools cache, there’s less chance of some else
finding it abusing it. It’s also very unlikely that I’d want to run
the same offline collector twice, so caching it isn’t really useful.
The artifact makes provision for adding more offline collectors, but I only have one at this stage.
in the tools section I have specified a version number. If I
create subsequent versions of the same tool, then I can switch to a
specific version if needed by changing this value. If I want it to
always use the highest version of the tool, I could not specify a
version and it will automatically choose the highest number (taking
into account Semantic Versioning if I had chosen to use that
scheme).
I now collect the Custom.RunOfflineCollectorTool on a client. I’m
doing this for a single client but it could just as easily be run via
a hunt to target many clients.
The collection proceeds on the client and I can view the results, which will come though in near-realtime as the offline collector is running. The collection results are just a line-parsed view the output from Stdout and Stderr.
Near the end of the results I can see that it uploaded the collection container zip to the S3 bucket.
On the collections Log tab, I can view the steps taken on the client to run the collector, including things like the command that was used and post-execution cleanup steps.
The offline collector doesn’t know it’s being run by a Velociraptor client and, as I mentioned in the overview, the collection data is therefore independent of the Velociraptor server. The client was just being used to run the collector. The collection results and collection log only provide information about running the collector.
With the collection zips stored in the S3 bucket, any external process can take over from there. You may have an automated process that monitors the bucket for new arrivals, downloads them, and feeds them into a processing pipeline.
In my case, I’m just going to download them manually using the MinIO
mc utility that I set up previously.
First I list what’s in the bucket:
mc ls garage/offline-collections
I then download the files to my desktop using mc:
mc cp --recursive garage/offline-collections /tmp/collections/
At this point I have many options regarding how I might want to work with the collection container zips.
For example I could:
extract the collection zips and work with their contents using other tools, perhaps also using Velociraptor’s command line analysis capabilities alongside the other tools.
or
run an Instant Velociraptor instance on my local workstation, or a set up separate Velociraptor server dedicated to analysis, and then either:
As always, Velociraptor give you many options and you’ll need to decide which options work best for your situation. Hopefully the steps demonstrated here are generic enough that they can be easily adapted for similar scenarios.