Velociraptor transforms the hunter from a single-host forensic analyst into a fleet interrogator. The same question that would take days to answer manually — “Which of our 3,000 endpoints has a process connecting to an IP in AS12345?” — takes minutes when you can push a VQL query to the fleet.
VQL Fundamentals
Basic Structure
-- VQL syntax mirrors SQL with endpoint-native plugins
SELECT
Pid,
Ppid,
Name,
Exe,
CommandLine,
Username,
CreateTime
FROM pslist()
WHERE Name =~ "svchost" -- regex match
AND Exe !~ "System32" -- exe path doesn't contain System32
Key VQL plugins:
| Plugin | Returns | Hunt Use |
|---|---|---|
pslist() | Running processes | Process rarity, masquerading |
glob(globs=...) | File system paths | File presence, timestomping |
stat(filename=...) | File metadata | Hash, timestamps, size |
parse_mft() | MFT records | File system timeline |
netstat() | Network connections | Process-to-IP mapping |
reg_values(...) | Registry values | Persistence keys |
wmi(query=...) | WMI query results | Scheduled tasks, services |
parse_pe(file=...) | PE header info | Imports, sections, signature |
Enrichment Patterns
-- Hash every process executable and check against threat intel
SELECT
Pid,
Name,
Exe,
hash(path=Exe).SHA256 AS SHA256,
-- Velociraptor can call external lookup (VirusTotal, etc.)
lookupHashVT(hash=hash(path=Exe).SHA256) AS VTResult
FROM pslist()
WHERE NOT Exe =~ "System32|SysWOW64|Program Files"
Writing a Custom Artifact
Artifacts are YAML-defined, parameterized hunt units. Here’s a complete artifact for LOLBin execution via scheduled tasks:
name: Custom.Hunt.SuspiciousScheduledTasks
description: |
Find scheduled tasks whose action binary is a known LOLBin or runs from
an unusual path (AppData, Temp, Downloads). Targets T1053.005 persistence
combined with LOLBin execution (T1218).
author: Threat Hunt Team
parameters:
- name: LOLBinRegex
default: "certutil|mshta|regsvr32|rundll32|wmic|bitsadmin|msiexec|cmstp"
description: Regex of LOLBins to flag in task action
- name: SuspiciousPathRegex
default: "AppData|Temp|Downloads|Public|ProgramData"
description: Regex of paths considered unusual for scheduled tasks
sources:
- name: SuspiciousTasks
query: |
-- Query WMI for all scheduled tasks
LET tasks = SELECT *
FROM wmi(query="SELECT * FROM Win32_ScheduledJob",
namespace="ROOT\\CIMV2\\Security\\MicrosoftScheduledJob")
-- Also check Task Scheduler COM interface (more complete)
LET sched_tasks = SELECT
TaskName,
TaskPath,
XML AS TaskXML,
parse_xml(file=XML).Task.Actions.Exec.Command AS ActionBinary,
parse_xml(file=XML).Task.Actions.Exec.Arguments AS ActionArgs,
parse_xml(file=XML).Task.Triggers AS Triggers
FROM glob(globs="C:/Windows/System32/Tasks/**")
WHERE NOT IsDir
SELECT
TaskName,
TaskPath,
ActionBinary,
ActionArgs,
Triggers,
hash(path=ActionBinary).SHA256 AS BinaryHash
FROM sched_tasks
WHERE ActionBinary =~ LOLBinRegex
OR ActionBinary =~ SuspiciousPathRegex
Fleet Hunt: Rarity Analysis at Scale
The most impactful fleet hunt for unknown threats:
name: Custom.Hunt.ProcessRarity
description: |
Fleet-wide process rarity — collect all running process executables and their
paths. Centrally aggregate to find binaries running on very few hosts.
sources:
- name: ProcessInventory
query: |
SELECT
Name,
Exe,
hash(path=Exe).MD5 AS MD5,
parse_pe(file=Exe).VersionInformation.CompanyName AS Company,
authenticode(filename=Exe).Trusted AS Trusted
FROM pslist()
WHERE Exe AND NOT Exe =~ "\\[System Process\\]"
After collection from 3,000 endpoints, aggregate in the Velociraptor server:
-- In the hunt results, post-collection analysis
SELECT Exe, Name, Company, Trusted, count() AS host_count
FROM hunt_results
GROUP BY Exe, Name
HAVING host_count <= 5 -- seen on 5 or fewer hosts
ORDER BY host_count
Each result with host_count = 1: “Why does only this host run this binary?” — that’s the hunt question.
Live Response: The Investigation Phase
Once a hunt narrows suspects to 3 hosts, switch to investigation mode for deep collection:
name: Custom.Investigate.FullEndpointTriage
description: Targeted collection from a confirmed suspicious host.
sources:
- name: RunningProcesses
query: |
SELECT Pid, Ppid, Name, Exe, CommandLine, Username,
hash(path=Exe).SHA256 AS Hash
FROM pslist()
- name: NetworkConnections
query: |
SELECT Pid, Family, Type, Status,
Laddr.IP AS LocalIP, Laddr.Port AS LocalPort,
Raddr.IP AS RemoteIP, Raddr.Port AS RemotePort,
Name AS ProcessName
FROM netstat()
WHERE Status = "ESTABLISHED" AND Raddr.IP !~ "^(127\\.0\\.0\\.1|::1)"
- name: PersistenceLocations
query: |
SELECT Key, Name, Data
FROM read_reg_key(globs=[
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\**",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\**",
"HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\**"
])
- name: RecentDownloads
query: |
SELECT OSPath, Size, Mtime, Atime, Ctime,
hash(path=OSPath).SHA256 AS SHA256
FROM glob(globs=[
"C:/Users/*/Downloads/**",
"C:/Users/*/AppData/Local/Temp/**"
])
WHERE NOT IsDir
AND Mtime > now() - 7 * 86400 -- last 7 days
ORDER BY Mtime DESC