# Velociraptor Artifact Development Guide Guide to creating custom VQL artifacts for specific investigation and threat hunting scenarios. ## Table of Contents - [Artifact Structure](#artifact-structure) - [Parameter Types](#parameter-types) - [Source Types](#source-types) - [Best Practices](#best-practices) - [Common Patterns](#common-patterns) - [Testing Artifacts](#testing-artifacts) ## Artifact Structure Velociraptor artifacts are YAML files with a defined structure: ```yaml name: Category.Subcategory.ArtifactName description: | Detailed description of what this artifact collects and why. Include use cases and expected output. author: Your Name 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 }} ``` ### Required Fields - **name**: Unique artifact identifier in dot notation - **description**: What the artifact does and when to use it - **sources**: At least one VQL query source ### Optional Fields - **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 ## Parameter Types ### String Parameters ```yaml parameters: - name: SearchPath default: "C:/Windows/System32/" type: string description: Directory path to search ``` ### Integer Parameters ```yaml parameters: - name: DaysBack default: 7 type: int description: Number of days to look back ``` ### Boolean Parameters ```yaml parameters: - name: IncludeSystem default: Y type: bool description: Include system files ``` ### Regex Parameters ```yaml parameters: - name: ProcessPattern default: "(?i)(powershell|cmd)" type: regex description: Process name pattern to match ``` ### Choice Parameters ```yaml parameters: - name: LogLevel default: "INFO" type: choices choices: - DEBUG - INFO - WARNING - ERROR description: Logging verbosity ``` ### CSV Parameters ```yaml parameters: - name: IOCList default: | evil.com malicious.net type: csv description: List of IOC domains ``` ## Source Types ### Query Sources Standard VQL query that collects data: ```yaml sources: - name: ProcessCollection query: | SELECT Pid, Name, CommandLine, Username FROM pslist() WHERE Name =~ ProcessPattern ``` ### Event Sources Continuous monitoring queries for CLIENT_EVENT artifacts: ```yaml sources: - name: ProcessCreation query: | SELECT * FROM watch_evtx( filename="C:/Windows/System32/winevt/Logs/Security.evtx" ) WHERE System.EventID.Value = 4688 ``` ### Multiple Sources Artifacts can have multiple sources for different data collection: ```yaml sources: - name: Processes query: | SELECT * FROM pslist() - name: NetworkConnections query: | SELECT * FROM netstat() - name: LoadedDLLs query: | SELECT * FROM modules() ``` ## Best Practices ### 1. Use Preconditions Prevent artifact execution on incompatible systems: ```yaml # 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' ``` ### 2. Parameterize Paths and Patterns Make artifacts flexible and reusable: ```yaml parameters: - name: TargetPath default: "C:/Users/**/AppData/**" type: string - name: FilePattern default: "*.exe" type: string sources: - query: | SELECT * FROM glob(globs=TargetPath + "/" + FilePattern) ``` ### 3. Use LET for Query Composition Break complex queries into manageable parts: ```yaml 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.Pid ``` ### 4. Add Error Handling Handle missing data gracefully: ```yaml 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 failures ``` ### 5. Include Documentation Add inline comments and comprehensive descriptions: ```yaml 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) ``` ## Common Patterns ### Pattern: File Collection with Hashing ```yaml 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 IsDir ``` ### Pattern: Event Log Analysis ```yaml name: 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 DESC ``` ### Pattern: Process Tree Analysis ```yaml name: 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 CreateTime ``` ### Pattern: Network IOC Matching ```yaml name: 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.IP ``` ### Pattern: Registry Timeline ```yaml name: 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 ``` ## Testing Artifacts ### 1. Local Testing with GUI ```bash # 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 ``` ### 2. Command Line Testing ```bash # 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.json ``` ### 3. Notebook Testing Use VQL notebooks for interactive development: ```sql -- 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 */ ``` ### 4. Validation Checklist 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 ## Performance Considerations ### Limit Scope ```yaml # 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" ]) ``` ### Use Workers for Parallel Processing ```yaml 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 ) ``` ### Rate Limiting ```yaml 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) } ) ``` ## MITRE ATT&CK Mapping Map artifacts to MITRE ATT&CK techniques: ```yaml 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/ ``` ## Artifact Distribution ### Export Artifacts ```bash # Export single artifact velociraptor artifacts show Custom.Artifact.Name > artifact.yaml # Export all custom artifacts velociraptor artifacts list --filter Custom > all_artifacts.yaml ``` ### Import Artifacts ```bash # Via command line velociraptor --config server.config.yaml artifacts import artifact.yaml # Via GUI # Navigate to: View Artifacts → Upload Artifact Pack ``` ### Share via Artifact Exchange Contribute artifacts to the community: 1. Test thoroughly across different systems 2. Document clearly with examples 3. Add MITRE ATT&CK mappings 4. Submit to: https://docs.velociraptor.app/exchange/