Guide to creating custom VQL artifacts for specific investigation and threat hunting scenarios.
Velociraptor artifacts are YAML files with a defined structure:
name: Category.Subcategory.ArtifactName
description: |
Detailed description of what this artifact collects and why.
Include use cases and expected output.
author: Your Name <email@domain.com>
type: CLIENT # CLIENT, SERVER, or CLIENT_EVENT
parameters:
- name: ParameterName
default: "default_value"
type: string
description: Parameter description
precondition: |
SELECT OS FROM info() WHERE OS = 'windows'
sources:
- name: SourceName
query: |
SELECT * FROM plugin()
WHERE condition
reports:
- type: CLIENT
template: |
# Report Title
{{ .Description }}
{{ range .Rows }}
- {{ .Column }}
{{ end }}- name: Unique artifact identifier in dot notation
- description: What the artifact does and when to use it
- sources: At least one VQL query source
- author: Creator information
- type: Artifact type (CLIENT, SERVER, CLIENT_EVENT)
- parameters: User-configurable inputs
- precondition: Check before running (OS, software presence)
- reports: Output formatting templates
- references: External documentation links
parameters:
- name: SearchPath
default: "C:/Windows/System32/"
type: string
description: Directory path to searchparameters:
- name: DaysBack
default: 7
type: int
description: Number of days to look backparameters:
- name: IncludeSystem
default: Y
type: bool
description: Include system filesparameters:
- name: ProcessPattern
default: "(?i)(powershell|cmd)"
type: regex
description: Process name pattern to matchparameters:
- name: LogLevel
default: "INFO"
type: choices
choices:
- DEBUG
- INFO
- WARNING
- ERROR
description: Logging verbosityparameters:
- name: IOCList
default: |
evil.com
malicious.net
type: csv
description: List of IOC domainsStandard VQL query that collects data:
sources:
- name: ProcessCollection
query: |
SELECT Pid, Name, CommandLine, Username
FROM pslist()
WHERE Name =~ ProcessPatternContinuous monitoring queries for CLIENT_EVENT artifacts:
sources:
- name: ProcessCreation
query: |
SELECT * FROM watch_evtx(
filename="C:/Windows/System32/winevt/Logs/Security.evtx"
)
WHERE System.EventID.Value = 4688Artifacts can have multiple sources for different data collection:
sources:
- name: Processes
query: |
SELECT * FROM pslist()
- name: NetworkConnections
query: |
SELECT * FROM netstat()
- name: LoadedDLLs
query: |
SELECT * FROM modules()Prevent artifact execution on incompatible systems:
# Windows-only artifact
precondition: |
SELECT OS FROM info() WHERE OS = 'windows'
# Requires specific tool
precondition: |
SELECT * FROM stat(filename="C:/Tools/sysinternals/psexec.exe")
# Version check
precondition: |
SELECT * FROM info() WHERE OS = 'windows' AND OSVersion =~ '10'Make artifacts flexible and reusable:
parameters:
- name: TargetPath
default: "C:/Users/**/AppData/**"
type: string
- name: FilePattern
default: "*.exe"
type: string
sources:
- query: |
SELECT * FROM glob(globs=TargetPath + "/" + FilePattern)Break complex queries into manageable parts:
sources:
- query: |
-- Define reusable subqueries
LET SuspiciousProcesses = SELECT Pid, Name, CommandLine
FROM pslist()
WHERE CommandLine =~ "(?i)(bypass|hidden)"
LET NetworkConnections = SELECT Pid, Raddr.IP AS RemoteIP
FROM netstat()
WHERE Status = "ESTABLISHED"
-- Join and correlate
SELECT sp.Name,
sp.CommandLine,
nc.RemoteIP
FROM SuspiciousProcesses sp
JOIN NetworkConnections nc ON sp.Pid = nc.PidHandle missing data gracefully:
sources:
- query: |
SELECT * FROM foreach(
row={
SELECT FullPath FROM glob(globs=SearchPath)
},
query={
SELECT FullPath,
hash(path=FullPath, accessor="file").SHA256 AS SHA256
FROM scope()
WHERE log(message="Processing: " + FullPath)
},
workers=5
)
WHERE SHA256 -- Filter out hash failuresAdd inline comments and comprehensive descriptions:
description: |
## Overview
This artifact hunts for suspicious scheduled tasks.
## Use Cases
- Persistence mechanism detection
- Lateral movement artifact collection
- Threat hunting campaigns
## Output
Returns task name, actions, triggers, and creation time.
## References
- MITRE ATT&CK T1053.005 (Scheduled Task/Job)name: Custom.Windows.FileCollection
description: Collect files matching patterns with hashes
parameters:
- name: GlobPatterns
default: |
C:/Users/**/AppData/**/*.exe
C:/Windows/Temp/**/*.dll
type: csv
sources:
- query: |
SELECT FullPath,
Size,
timestamp(epoch=Mtime) AS Modified,
timestamp(epoch=Btime) AS Created,
hash(path=FullPath, accessor="file") AS Hashes
FROM foreach(
row={
SELECT * FROM parse_csv(filename=GlobPatterns, accessor="data")
},
query={
SELECT * FROM glob(globs=_value)
}
)
WHERE NOT IsDirname: Custom.Windows.EventLogHunt
description: Hunt for specific event IDs with context
parameters:
- name: LogFile
default: "C:/Windows/System32/winevt/Logs/Security.evtx"
type: string
- name: EventIDs
default: "4624,4625,4672"
type: csv
sources:
- query: |
LET EventIDList = SELECT parse_string_with_regex(
string=EventIDs,
regex="(\\d+)"
).g1 AS EventID FROM scope()
SELECT timestamp(epoch=System.TimeCreated.SystemTime) AS EventTime,
System.EventID.Value AS EventID,
System.Computer AS Computer,
EventData
FROM parse_evtx(filename=LogFile)
WHERE str(str=System.EventID.Value) IN EventIDList.EventID
ORDER BY EventTime DESCname: Custom.Windows.ProcessTree
description: Build process tree from a starting PID
parameters:
- name: RootPID
default: 0
type: int
description: Starting process PID (0 for all)
sources:
- query: |
LET ProcessList = SELECT Pid, Ppid, Name, CommandLine, Username, CreateTime
FROM pslist()
LET RECURSIVE GetChildren(ParentPID) = SELECT *
FROM ProcessList
WHERE Ppid = ParentPID
LET RECURSIVE BuildTree(Level, ParentPID) = SELECT
Level,
Pid,
Ppid,
Name,
CommandLine,
Username,
CreateTime
FROM GetChildren(ParentPID=ParentPID)
UNION ALL
SELECT * FROM BuildTree(Level=Level+1, ParentPID=Pid)
SELECT * FROM if(
condition=RootPID > 0,
then={
SELECT * FROM BuildTree(Level=0, ParentPID=RootPID)
},
else={
SELECT 0 AS Level, * FROM ProcessList
}
)
ORDER BY CreateTimename: Custom.Windows.NetworkIOCMatch
description: Match network connections against IOC list
parameters:
- name: IOCList
default: |
IP,Description
192.0.2.1,C2 Server
198.51.100.50,Malicious Host
type: csv
sources:
- query: |
LET IOCs = SELECT IP, Description
FROM parse_csv(filename=IOCList, accessor="data")
LET Connections = SELECT
Raddr.IP AS RemoteIP,
Raddr.Port AS RemotePort,
Pid,
process_tracker_get(id=Pid).Name AS ProcessName,
process_tracker_get(id=Pid).CommandLine AS CommandLine
FROM netstat()
WHERE Status = "ESTABLISHED"
SELECT c.RemoteIP,
c.RemotePort,
c.ProcessName,
c.CommandLine,
i.Description AS IOCMatch
FROM Connections c
JOIN IOCs i ON c.RemoteIP = i.IPname: Custom.Windows.RegistryTimeline
description: Timeline registry modifications in specific keys
parameters:
- name: RegistryPaths
default: |
HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/Run/**
HKEY_CURRENT_USER/SOFTWARE/Microsoft/Windows/CurrentVersion/Run/**
type: csv
- name: DaysBack
default: 7
type: int
sources:
- query: |
LET StartTime = timestamp(epoch=now() - DaysBack * 86400)
SELECT timestamp(epoch=Key.Mtime) AS Modified,
Key.FullPath AS RegistryPath,
ValueName,
ValueData.value AS Value
FROM foreach(
row={
SELECT * FROM parse_csv(filename=RegistryPaths, accessor="data")
},
query={
SELECT * FROM read_reg_key(globs=_value)
}
)
WHERE Key.Mtime > StartTime
ORDER BY Modified DESC# Start Velociraptor in GUI mode
velociraptor gui
# Navigate to: View Artifacts → Add Artifact
# Paste your artifact YAML and click Save
# Run artifact via Collected Artifacts → New Collection# Test artifact syntax
velociraptor artifacts show Custom.Artifact.Name
# Run artifact locally
velociraptor artifacts collect Custom.Artifact.Name \
--args ParameterName=value \
--format json
# Run with output file
velociraptor artifacts collect Custom.Artifact.Name \
--output results.jsonUse VQL notebooks for interactive development:
-- Test query components in isolation
SELECT * FROM pslist() WHERE Name =~ "powershell" LIMIT 10
-- Test parameter substitution
LET ProcessPattern = "(?i)(powershell|cmd)"
SELECT * FROM pslist() WHERE Name =~ ProcessPattern
-- Test full artifact query
/* Paste your artifact query here */Before deploying artifacts:
- Artifact name follows convention: Category.Subcategory.Name
- Description includes use cases and expected output
- Parameters have sensible defaults
- Precondition prevents incompatible execution
- Query tested in notebook mode
- Error handling for missing data
- Performance acceptable on test system
- Output format is useful and parseable
- Documentation includes MITRE ATT&CK mapping if applicable
# BAD: Scans entire filesystem
SELECT * FROM glob(globs="C:/**/*.exe")
# GOOD: Targeted scope
SELECT * FROM glob(globs=[
"C:/Users/**/AppData/**/*.exe",
"C:/Windows/Temp/**/*.exe"
])sources:
- query: |
SELECT * FROM foreach(
row={SELECT * FROM glob(globs=SearchPath)},
query={
SELECT FullPath,
hash(path=FullPath, accessor="file").SHA256 AS SHA256
FROM scope()
},
workers=10 -- Process 10 files concurrently
)sources:
- query: |
SELECT * FROM foreach(
row={SELECT * FROM glob(globs="C:/**")},
query={
SELECT * FROM scope()
WHERE rate(query_name="my_query", ops_per_sec=100)
}
)Map artifacts to MITRE ATT&CK techniques:
name: Custom.Windows.PersistenceHunt
description: |
Hunt for persistence mechanisms.
MITRE ATT&CK Techniques:
- T1547.001: Registry Run Keys / Startup Folder
- T1053.005: Scheduled Task/Job
- T1543.003: Windows Service
- T1546.003: Windows Management Instrumentation Event Subscription
references:
- https://attack.mitre.org/techniques/T1547/001/
- https://attack.mitre.org/techniques/T1053/005/# Export single artifact
velociraptor artifacts show Custom.Artifact.Name > artifact.yaml
# Export all custom artifacts
velociraptor artifacts list --filter Custom > all_artifacts.yaml# Via command line
velociraptor --config server.config.yaml artifacts import artifact.yaml
# Via GUI
# Navigate to: View Artifacts → Upload Artifact PackContribute artifacts to the community:
- Test thoroughly across different systems
- Document clearly with examples
- Add MITRE ATT&CK mappings
- Submit to: https://docs.velociraptor.app/exchange/