├── requirements.txt ├── .gitignore ├── test_api.py ├── README.md ├── velociraptor_api.py └── mcp_velociraptor_bridge.py /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp 2 | pyvelociraptor 3 | asyncio 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pycache 2 | /__pycache__/* 3 | 4 | 5 | # api cleint config locally 6 | api_client.yaml 7 | -------------------------------------------------------------------------------- /test_api.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Expecting to print out server info() if API working as expected 3 | { 4 | "Hostname": "vr", 5 | "Uptime": 46570, 6 | "BootTime": 1743261689, 7 | "OS": "linux", 8 | "Platform": "ubuntu", 9 | "PlatformFamily": "debian", 10 | "PlatformVersion": "24.04", 11 | "KernelVersion": "6.8.0-56-generic", 12 | "VirtualizationSystem": "", 13 | "VirtualizationRole": "", 14 | "CompilerVersion": "go1.23.2", 15 | "HostID": "1c38feaf-a3d0-41d0-ad3b-00228390771f", 16 | "Exe": "/usr/local/bin/velociraptor.bin", 17 | "CWD": "/", 18 | "IsAdmin": false, 19 | "ClientStart": "2025-03-29T07:54:43.47676618Z", 20 | "LocalTZ": "UTC", 21 | "LocalTZOffset": 0, 22 | "Fqdn": "vr", 23 | "Architecture": "amd64" 24 | } 25 | ''' 26 | 27 | import yaml, grpc, json 28 | from pyvelociraptor import api_pb2, api_pb2_grpc 29 | 30 | config = yaml.safe_load(open("api_client.yaml")) 31 | 32 | # Prepare gRPC channel credentials 33 | creds = grpc.ssl_channel_credentials( 34 | root_certificates=config["ca_certificate"].encode("utf-8"), 35 | private_key=config["client_private_key"].encode("utf-8"), 36 | certificate_chain=config["client_cert"].encode("utf-8") 37 | ) 38 | channel_opts = (('grpc.ssl_target_name_override', "VelociraptorServer"),) 39 | channel = grpc.secure_channel(config["api_connection_string"], creds, options=channel_opts) 40 | stub = api_pb2_grpc.APIStub(channel) 41 | 42 | vql = 'SELECT * FROM info()' 43 | request = api_pb2.VQLCollectorArgs(Query=[api_pb2.VQLRequest(VQL=vql)]) 44 | 45 | print(f'\nExpecting to print out server info() if API working as expected') 46 | for response in stub.Query(request): 47 | if response.Response: 48 | rows = json.loads(response.Response) 49 | for line in rows: 50 | print(json.dumps(line, indent=4)) 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Velociraptor MCP 2 | Velociraptor MCP is a POC Model Context Protocol bridge for exposing LLMs to MCP clients. 3 | 4 | Initial version has several Windows orientated triage tools deployed. Best use is querying usecase to target machine name. 5 | 6 | e.g 7 | 8 | `can you give me all network connections on MACHINENAME and look for suspicious processes?` 9 | 10 | `can you tell me which artifacts target the USN journal` 11 | 12 | 13 | 14 | 15 | ## Installation 16 | ### 1. Setup an API account 17 | https://docs.velociraptor.app/docs/server_automation/server_api/ 18 | 19 | Generate an api config file: 20 | 21 | `velociraptor --config /etc/velociraptor/server.config.yaml config api_client --name api --role administrator,api api_client.yaml` 22 | 23 | ### 2. Clone mcp-velociraptor repo and test API 24 | 25 | - copy api_client.yaml to preferred config location and ensure configuration correct (pointing to appropriate IP address). 26 | - modify test_api.py to appropriate location. 27 | - Run test_api.py to confirm working 28 | - Modify mcp_velociraptor_bridge.py to correct API config 29 | 30 | ### 3. Connect to Claude desktop or MCP client of choice 31 | 32 | The easiest configuration is to run your venv python directly calling mcp_velociraptor_bridge. 33 | ```{ 34 | "mcpServers": { 35 | "velociraptor": { 36 | "command": "/path/to/venv/bin/python", 37 | "args": [ 38 | "/path/to/mcp_velociraptor_bridge.py" 39 | ] 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | ![image](https://github.com/user-attachments/assets/3e810f03-ca74-4757-b5dc-89d4e8f8aef6) 46 | 47 | 48 | ### 3. Caveats 49 | 50 | Due to the nature of DFIR, results depend on amount of data returned, model use and context window. 51 | 52 | I have included a function to find artifacts and dynamically create collections but had mixed results. 53 | I have been pleasantly surprised with some results and disappointed when running other collections that cause lots of rows. 54 | 55 | Please let me know how you go and feel free to add PR! 56 | 57 | 58 | `can you give me all network connections on MACHINENAME and look for suspicious processes?` 59 | image 60 | image 61 | image 62 | 63 | `can you tell me which artifacts target the USN journal` 64 | image 65 | 66 | -------------------------------------------------------------------------------- /velociraptor_api.py: -------------------------------------------------------------------------------- 1 | import yaml, grpc, json 2 | from pyvelociraptor import api_pb2, api_pb2_grpc 3 | from velociraptor_api import * 4 | 5 | stub = None 6 | 7 | def init_stub(config_path): 8 | global stub 9 | config = yaml.safe_load(open(config_path)) 10 | creds = grpc.ssl_channel_credentials( 11 | root_certificates=config["ca_certificate"].encode("utf-8"), 12 | private_key=config["client_private_key"].encode("utf-8"), 13 | certificate_chain=config["client_cert"].encode("utf-8") 14 | ) 15 | channel_opts = (('grpc.ssl_target_name_override', "VelociraptorServer"),) 16 | channel = grpc.secure_channel(config["api_connection_string"], creds, options=channel_opts) 17 | stub = api_pb2_grpc.APIStub(channel) 18 | 19 | 20 | def run_vql_query(vql: str): 21 | if stub is None: 22 | raise RuntimeError("Stub not initialized. Call init_stub() first.") 23 | request = api_pb2.VQLCollectorArgs(Query=[api_pb2.VQLRequest(VQL=vql)]) 24 | results = [] 25 | print(request) 26 | 27 | for resp in stub.Query(request): 28 | if hasattr(resp, "error") and resp.error: 29 | raise RuntimeError(f"Velociraptor API error: {resp.error}") 30 | if hasattr(resp, "Response") and resp.Response: 31 | results.extend(json.loads(resp.Response)) 32 | return results 33 | 34 | 35 | def find_client_info(hostname: str) -> dict: 36 | vql = ( 37 | f"SELECT client_id," 38 | "timestamp(epoch=first_seen_at) as FirstSeen," 39 | "timestamp(epoch=last_seen_at) as LastSeen," 40 | "os_info.hostname as Hostname," 41 | "os_info.fqdn as Fqdn," 42 | "os_info.system as OSType," 43 | "os_info.release as OS," 44 | "os_info.machine as Machine," 45 | "agent_information.version as AgentVersion " 46 | f"FROM clients() WHERE os_info.hostname =~ '^{hostname}$' OR os_info.fqdn =~ '^{hostname}$' ORDER BY LastSeen DESC LIMIT 1" 47 | ) 48 | 49 | result = run_vql_query(vql) 50 | if not result: 51 | return None 52 | return result[0] 53 | 54 | 55 | def realtime_collection(client_id: str, artifact: str, parameters: str = "", fields: str = "*", result_scope: str = "") -> str: 56 | vql = ( 57 | f"LET collection <= collect_client(urgent='TRUE',client_id='{client_id}', artifacts='{artifact}', env=dict({parameters})) " 58 | f"LET get_monitoring = SELECT * FROM watch_monitoring(artifact='System.Flow.Completion') WHERE FlowId = collection.flow_id LIMIT 1 " 59 | f"LET get_results = SELECT * FROM source(client_id=collection.request.client_id, flow_id=collection.flow_id,artifact='{artifact}{result_scope}') " 60 | f"SELECT {fields} FROM foreach(row= get_monitoring ,query= get_results) " 61 | ) 62 | 63 | try: 64 | results = run_vql_query(vql) 65 | except Exception as e: 66 | return f"Error starting collection: {e}" 67 | 68 | return str(results) 69 | 70 | def start_collection(client_id: str, artifact: str, parameters: str = "" ) -> str: 71 | vql = ( 72 | f"LET collection <= collect_client(urgent='TRUE',client_id='{client_id}', artifacts='{artifact}', env=dict({parameters})) " 73 | f" SELECT flow_id,request.artifacts as artifacts,request.specs[0] as specs FROM foreach(row= collection) " 74 | ) 75 | 76 | try: 77 | results = run_vql_query(vql) 78 | return results 79 | except Exception as e: 80 | return f"Error starting collection: {e}" 81 | 82 | 83 | def get_flow_status(client_id: str, flow_id: str, artifact: str) -> str: 84 | vql = ( 85 | f"SELECT * FROM flow_logs(client_id='{client_id}', flow_id='{flow_id}') " 86 | f"WHERE message =~ '^Collection {artifact} is done after'" 87 | f"LIMIT 100" 88 | ) 89 | 90 | try: 91 | results = run_vql_query(vql) 92 | except Exception as e: 93 | return f"Error checking flow status: {e}" 94 | 95 | if results and isinstance(results, list) and len(results) > 0: 96 | return "FINISHED" 97 | 98 | return "RUNNING" 99 | 100 | 101 | def get_flow_results(client_id: str, flow_id: str, artifact: str, fields: str = "*" ) -> str: 102 | vql = ( 103 | f"SELECT {fields} FROM source(client_id='{client_id}', flow_id='{flow_id}',artifact='{artifact}') " 104 | ) 105 | 106 | try: 107 | results = run_vql_query(vql) 108 | return results 109 | except Exception as e: 110 | return f"Error checking flow status: {e}" 111 | -------------------------------------------------------------------------------- /mcp_velociraptor_bridge.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | 3 | import yaml, grpc, json 4 | from pyvelociraptor import api_pb2, api_pb2_grpc 5 | from velociraptor_api import * 6 | import asyncio 7 | 8 | 9 | mcp = FastMCP("velociraptor-mcp") 10 | 11 | # add api config path - wrong config and MCP connection will fail 12 | api_client_config = "/path/to/api_client.yaml" 13 | init_stub(api_client_config) 14 | 15 | 16 | @mcp.tool() 17 | def client_info(hostname: str) -> dict: 18 | """ 19 | Retrieve client information from the Velociraptor server. 20 | 21 | Args: 22 | hostname: Hostname or FQDN of the target endpoint. 23 | 24 | Returns: 25 | A dictionary containing client metadata, including the client_id, 26 | which can be used to target other artifact collections. 27 | """ 28 | return find_client_info(hostname) 29 | 30 | @mcp.tool() 31 | async def linux_pslist( 32 | client_id: str, 33 | ProcessRegex: str = ".", 34 | Fields: str = "*" 35 | ) -> str: 36 | """ 37 | List running processes on a Linux host. 38 | 39 | Args: 40 | client_id: The Velociraptor client ID. 41 | ProcessRegex: Case-insensitive regex to filter process names. 42 | Fields: Comma-separated string of fields to return. 43 | 44 | Returns: 45 | Process list as a string or error message. 46 | 47 | """ 48 | artifact = "Linux.Sys.Pslist" 49 | result_scope = "" 50 | parameters = ( 51 | f"ProcessRegex='{ProcessRegex}'" 52 | ) 53 | 54 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 55 | 56 | @mcp.tool() 57 | async def linux_groups( 58 | client_id: str, 59 | GroupFile: str = "/etc/group", 60 | Fields: str = "*" 61 | ) -> str: 62 | """ 63 | List groups on a Linux host. 64 | 65 | Args: 66 | client_id: The Velociraptor client ID. 67 | GroupFile: The location of the group file 68 | Fields: Comma-separated string of fields to return. 69 | 70 | Returns: 71 | The group names as a string or error message. 72 | 73 | """ 74 | artifact = "Linux.Sys.Groups" 75 | result_scope = "" 76 | parameters = ( 77 | f"GroupFile='{GroupFile}'" 78 | ) 79 | 80 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 81 | 82 | @mcp.tool() 83 | async def linux_mounts( 84 | client_id: str, 85 | Fields: str = "*" 86 | ) -> str: 87 | """ 88 | List mounts on a Linux host. 89 | 90 | Args: 91 | client_id: The Velociraptor client ID. 92 | Fields: Comma-separated string of fields to return. 93 | 94 | Returns: 95 | The mounted filesystems as a string or error message. 96 | 97 | """ 98 | artifact = "Linux.Mounts" 99 | result_scope = "" 100 | parameters = "" # No parameters for this artifact 101 | 102 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 103 | 104 | @mcp.tool() 105 | async def linux_netstat_enriched( 106 | client_id: str, 107 | IPRegex: str = ".", 108 | PortRegex: str = ".", 109 | ProcessNameRegex: str = ".", 110 | UsernameRegex: str = ".", 111 | ConnectionStatusRegex: str= "LISTEN|ESTAB", 112 | ProcessPathRegex: str = ".", 113 | CommandLineRegex: str = ".", 114 | CallChainRegex: str = ".", 115 | Fields: str = "*" 116 | ) -> str: 117 | """ 118 | List network connections (netstat) with process metadata on a Linux host. 119 | 120 | Args: 121 | client_id: The Velociraptor client ID. 122 | IPRegex: Regex to filter remote/local IP addresses. 123 | PortRegex: Regex to filter local/remote ports (e.g., '^443$'). 124 | ProcessNameRegex: Regex to filter process names. 125 | UsernameRegex: Regex to filter user accounts associated with the process. 126 | ConnectionStatusRegex: Regex to filter connection status. 127 | ProcessPathRegex: Regex to filter full process paths. 128 | CommandLineRegex: Regex to filter command-line arguments. 129 | CallChainRegex: Regex to filter process callchain. 130 | Fields: Comma-separated string of fields to return. 131 | 132 | Returns: 133 | Netstat results as a string or error message. 134 | 135 | """ 136 | artifact = "Linux.Network.NetstatEnriched" 137 | result_scope = "" 138 | parameters = ( 139 | f"IPRegex='{IPRegex}'," 140 | f"PortRegex='{PortRegex}'," 141 | f"ProcessNameRegex='{ProcessNameRegex}'," 142 | f"UsernameRegex='{UsernameRegex}'," 143 | f"ConnectionStatusRegex='{ConnectionStatusRegex}'," 144 | f"ProcessPathRegex='{ProcessPathRegex}'," 145 | f"CommandLineRegex='{CommandLineRegex}'," 146 | f"CallChainRegex='{CallChainRegex}'" 147 | ) 148 | 149 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 150 | 151 | @mcp.tool() 152 | async def linux_users( 153 | client_id: str, 154 | Fields: str = "*" 155 | ) -> str: 156 | """ 157 | List users on a Linux host. 158 | 159 | Args: 160 | client_id: The Velociraptor client ID. 161 | Fields: Comma-separated string of fields to return. 162 | 163 | Returns: 164 | The user results as a string or error message. 165 | 166 | """ 167 | artifact = "Linux.Sys.Users" 168 | result_scope = "" 169 | parameters = "" # No parameters for this artifact 170 | 171 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 172 | 173 | @mcp.tool() 174 | async def windows_pslist( 175 | client_id: str, 176 | ProcessRegex: str = ".", 177 | PidRegex: str = ".", 178 | ExePathRegex: str = ".", 179 | CommandLineRegex: str = ".", 180 | UsernameRegex: str = ".", 181 | Fields: str = "Pid, Ppid, TokenIsElevated, Name, Exe, CommandLine, Username, Authenticode.Trusted" 182 | ) -> str: 183 | """ 184 | List running processes on a Windows host. 185 | 186 | Args: 187 | client_id: Velociraptor client ID. 188 | ProcessRegex: Case-insensitive regex to filter process names. 189 | PidRegex: Regex to filter process IDs. 190 | ExePathRegex: Regex to filter executable path on disk. 191 | CommandLineRegex: Regex to filter process command line. 192 | UsernameRegex: Regex to filter user context of the process. 193 | Fields: Comma-separated list of fields to return. 194 | 195 | Returns: 196 | Process list results as a string or error message. 197 | """ 198 | artifact = "Windows.System.Pslist" 199 | result_scope = "" 200 | parameters = ( 201 | f"ProcessRegex='{ProcessRegex}'," 202 | f"PidRegex='{PidRegex}'," 203 | f"ExePathRegex='{ExePathRegex}'," 204 | f"CommandLineRegex='{CommandLineRegex}'," 205 | f"UsernameRegex='{UsernameRegex}'" 206 | ) 207 | 208 | return realtime_collection(client_id,artifact,parameters,Fields,result_scope) 209 | 210 | @mcp.tool() 211 | async def windows_netstat_enriched( 212 | client_id: str, 213 | IPRegex: str = ".", 214 | PortRegex: str = ".", 215 | ProcessNameRegex: str = ".", 216 | ProcessPathRegex: str = ".", 217 | CommandLineRegex: str = ".", 218 | UsernameRegex: str = ".", 219 | Fields: str = "Pid,Ppid,Name,Path,CommandLine,Username,Authenticode.Trusted,Type,Status,Laddr,Lport,Raddr,Rport" 220 | ) -> str: 221 | """ 222 | List network connections (netstat) with process metadata on a Windows host. 223 | 224 | Args: 225 | client_id: Velociraptor client ID. 226 | IPRegex: Regex to filter remote/local IP addresses. 227 | PortRegex: Regex to filter local/remote ports (e.g., '^443$'). 228 | ProcessNameRegex: Regex to filter process names. 229 | ProcessPathRegex: Regex to filter full process paths. 230 | CommandLineRegex: Regex to filter command-line arguments. 231 | UsernameRegex: Regex to filter user accounts associated with the process. 232 | Fields: Comma-separated list of fields to return. 233 | 234 | Returns: 235 | Netstat results as a string or error message. 236 | """ 237 | artifact = "Windows.Network.NetstatEnriched/Netstat" 238 | result_scope = "" 239 | parameters = ( 240 | f"IPRegex='{IPRegex}'," 241 | f"PortRegex='{PortRegex}'," 242 | f"ProcessNameRegex='{ProcessNameRegex}'," 243 | f"ProcessPathRegex='{ProcessPathRegex}'," 244 | f"CommandLineRegex='{CommandLineRegex}'," 245 | f"UsernameRegex='{UsernameRegex}'" 246 | ) 247 | 248 | return realtime_collection(client_id,artifact,parameters,Fields,result_scope) 249 | 250 | ## 251 | ## Persistence 252 | @mcp.tool() 253 | async def windows_scheduled_tasks( 254 | client_id: str, 255 | Fields: str = "OSPath,Mtime,Command,ExpandedCommand,Arguments,ComHandler,UserId,StartBoundary,Authenticode" 256 | ) -> str: 257 | """ 258 | List scheduled tasks (persistance) with metadata on a Windows host 259 | 260 | Args: 261 | client_id: Velociraptor client ID. 262 | Fields: Comma-separated list of fields to return. 263 | 264 | Returns: 265 | Scheduled task results as a string or error message. 266 | """ 267 | artifact = "Windows.System.TaskScheduler" 268 | result_scope = "/Analysis" 269 | parameters = "" # No parameters for this artifact 270 | 271 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 272 | 273 | 274 | @mcp.tool() 275 | async def windows_services( 276 | client_id: str, 277 | Fields: str = "UserAccount,Created,ServiceDll,FailureCommand,FailureActions,AbsoluteExePath,HashServiceExe,CertinfoServiceExe,HashServiceDll,CertinfoServiceDll" 278 | ) -> str: 279 | """ 280 | List services with metadata on a Windows host. 281 | 282 | Args: 283 | client_id: Velociraptor client ID. 284 | Fields: Comma-separated list of fields to return. 285 | 286 | Returns: 287 | Service artifact results as a string or error message. 288 | """ 289 | artifact = "Windows.System.Services" 290 | result_scope = "" 291 | parameters = "" # No parameters for this artifact 292 | 293 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 294 | 295 | 296 | ## 297 | ## User Activity 298 | 299 | @mcp.tool() 300 | async def windows_recentdocs( 301 | client_id: str, 302 | Fields: str = "Username,LastWriteTime,Value,Key,MruEntries,HiveName" 303 | ) -> str: 304 | """ 305 | Collect RecentDocs from Registry on a Windows host. 306 | 307 | Args: 308 | client_id: Velociraptor client ID. 309 | Fields: Comma-separated list of fields to return. 310 | 311 | Returns: 312 | RecentDocs artifact results as a string or error message. 313 | """ 314 | artifact = "Windows.Registry.RecentDocs" 315 | result_scope = "" 316 | parameters = "" # No parameters for this artifact 317 | 318 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 319 | 320 | 321 | @mcp.tool() 322 | async def windows_shellbags( 323 | client_id: str, 324 | Fields: str = "ModTime,Name,_OSPath,Hive,KeyPath,Description,Path,_RawData,_Parsed" 325 | ) -> str: 326 | """ 327 | Collect Shellbags from Registry on a Windows host. 328 | 329 | Args: 330 | client_id: Velociraptor client ID. 331 | Fields: Comma-separated list of fields to return. 332 | 333 | Returns: 334 | Shellbags artifact results as a string or error message. 335 | """ 336 | artifact = "Windows.Forensics.Shellbags" 337 | result_scope = "" 338 | parameters = "" # No parameters for this artifact 339 | 340 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 341 | 342 | 343 | @mcp.tool() 344 | async def windows_mounted_mass_storage_usb( 345 | client_id: str, 346 | Fields: str = "KeyLastWriteTimestamp, KeyName, FriendlyName, HardwareID" 347 | ) -> str: 348 | """ 349 | Collect evidence of mounted mass storage from Registry on a Windows host. 350 | 351 | Args: 352 | client_id: Velociraptor client ID. 353 | Fields: Comma-separated list of fields to return. 354 | 355 | Returns: 356 | Mounted mass storage artifact results as a string or error message. 357 | """ 358 | artifact = "Windows.Mounted.Mass.Storage" 359 | result_scope = "" 360 | parameters = "" # No parameters for this artifact 361 | 362 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 363 | 364 | @mcp.tool() 365 | async def windows_evidence_of_download( 366 | client_id: str, 367 | Fields: str = "DownloadedFilePath,_ZoneIdentifierContent,FileHash,HostUrl,ReferrerUrl" 368 | ) -> str: 369 | """ 370 | Collect evidence of download from a Windows host. 371 | 372 | Args: 373 | client_id: Velociraptor client ID. 374 | Fields: Comma-separated list of fields to return. 375 | 376 | Returns: 377 | Evidence of Download artifact results as a string or error message. 378 | """ 379 | artifact = "Windows.Analysis.EvidenceOfDownload" 380 | result_scope = "" 381 | parameters = "" # No parameters for this artifact 382 | 383 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 384 | 385 | @mcp.tool() 386 | async def windows_mountpoints2( 387 | client_id: str, 388 | Fields: str = "ModifiedTime, MountPoint, Hive, Key" 389 | ) -> str: 390 | """ 391 | Collect evidence of download from a Windows host. 392 | 393 | Args: 394 | client_id: Velociraptor client ID. 395 | Fields: Comma-separated list of fields to return. 396 | 397 | Returns: 398 | Evidence of Download artifact results as a string or error message. 399 | """ 400 | artifact = "Windows.Registry.MountPoints2" 401 | result_scope = "" 402 | parameters = "" # No parameters for this artifact 403 | 404 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 405 | 406 | 407 | ## 408 | ## Evidence of execution 409 | @mcp.tool() 410 | async def windows_execution_amcache( 411 | client_id: str, 412 | Fields: str = "FullPath,SHA1,ProgramID,FileDescription,FileVersion,Publisher,CompileTime,LastModified,LastRunTime" 413 | ) -> str: 414 | """ 415 | Collect evidence of execution from Amcache on a Windows host. 416 | 417 | Args: 418 | client_id: Velociraptor client ID. 419 | Fields: Comma-separated list of fields to return. 420 | 421 | Returns: 422 | Amcache artifact results as a string or error message. 423 | """ 424 | artifact = "Windows.Detection.Amcache" 425 | result_scope = "" 426 | parameters = "" # No parameters for this artifact 427 | 428 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 429 | 430 | 431 | @mcp.tool() 432 | async def windows_execution_bam( 433 | client_id: str, 434 | Fields: str = "*" 435 | ) -> str: 436 | """ 437 | Extract evidence of execution from the BAM (Background Activity Moderator) registry key on a Windows host. 438 | 439 | Args: 440 | client_id: Velociraptor client ID. 441 | Fields: Comma-separated list of fields to return. 442 | 443 | Returns: 444 | BAM artifact results as a string or error message. 445 | """ 446 | artifact = "Windows.Forensics.Bam" 447 | result_scope = "" 448 | parameters = "" # No parameters for this artifact 449 | 450 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 451 | 452 | @mcp.tool() 453 | async def windows_execution_activitiesCache( 454 | client_id: str, 455 | Fields: str = "*" 456 | ) -> str: 457 | """ 458 | Evidence of execution from activitiesCache.db (windows timeline) of system activity on a Windows host. 459 | 460 | Args: 461 | client_id: Velociraptor client ID. 462 | Fields: Comma-separated list of fields to return. 463 | 464 | Returns: 465 | Timeline artifact results as a string or error message. 466 | """ 467 | artifact = "Windows.Forensics.Timeline" 468 | result_scope = "" 469 | parameters = "" # No parameters for this artifact 470 | 471 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 472 | 473 | @mcp.tool() 474 | async def windows_execution_userassist( 475 | client_id: str, 476 | Fields: str = "Name,User,LastExecution,NumberOfExecutions" 477 | ) -> str: 478 | """ 479 | Extract evidence of execution from UserAssist registry keys. 480 | 481 | Args: 482 | client_id: Velociraptor client ID. 483 | Fields: Comma-separated list of fields to return. 484 | 485 | Returns: 486 | UserAssist artifact results as a string or error message. 487 | """ 488 | artifact = "Windows.Registry.UserAssist" 489 | result_scope = "" 490 | parameters = "" # No parameters for this artifact 491 | 492 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 493 | 494 | @mcp.tool() 495 | async def windows_execution_shimcache( 496 | client_id: str, 497 | Fields: str = "Position,ModificationTime,Path,ExecutionFlag,ControlSet" 498 | ) -> str: 499 | """ 500 | Parse ShimCache (AppCompatCache) entries from the registry on a Windows host. 501 | 502 | Note: 503 | Presence of a ShimCache entry may not indicate actual execution—only that the file was accessed or observed by the system. 504 | 505 | Args: 506 | client_id: Velociraptor client ID. 507 | Fields: Comma-separated list of fields to return. 508 | 509 | Returns: 510 | ShimCache (AppCompatCache) artifact results as a string or error message. 511 | """ 512 | artifact = "Windows.Registry.AppCompatCache" 513 | result_scope = "" 514 | parameters = "" # No parameters for this artifact 515 | 516 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 517 | 518 | 519 | @mcp.tool() 520 | async def windows_execution_prefetch( 521 | client_id: str, 522 | Fields: str = "Binary,CreationTime,LastRunTimes,RunCount,Hash" 523 | #"Executable,LastRunTimes,RunCount,PrefetchFileName,Version,Hash,CreationTime,ModificationTime,Binary" 524 | ) -> str: 525 | """ 526 | Parse Prefetch files on a Windows host to identify previously executed programs. 527 | 528 | Args: 529 | client_id: Velociraptor client ID. 530 | Fields: Comma-separated list of fields to return. 531 | 532 | Returns: 533 | Prefetch artifact results as a string or error message. 534 | """ 535 | artifact = "Windows.Forensics.Prefetch" 536 | result_scope = "" 537 | parameters = "" # No parameters for this artifact 538 | 539 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 540 | 541 | 542 | @mcp.tool() 543 | async def windows_ntfs_mft( 544 | client_id: str, 545 | MFTDrive: str = "C:", 546 | PathRegex: str = ".", 547 | FileRegex: str = "^velociraptor\\.exe$", 548 | DateAfter: str = "", 549 | DateBefore: str = "", 550 | Fields: str = "*" 551 | ) -> str: 552 | """ 553 | Search MFT for filename or path on a Windows machine. This is a forensic collection and may return many rows. If failure retry with collect_artifact(). 554 | Args: 555 | client_id: The Velociraptor client ID. 556 | MFTDrive: Target drive letter (default is C:). 557 | FileRegex: Regex to match filenames or folders. 558 | PathRegex: Regex to match file paths (more costly). 559 | DateAfter: Filter for files modified/created after this timestamp. 560 | DateBefore: Filter for files modified/created before this timestamp. 561 | Fields: Comma-separated string of fields to return. 562 | 563 | Returns: 564 | A result string or error message. 565 | 566 | """ 567 | artifact = "Windows.NTFS.MFT" 568 | result_scope = "" 569 | parameters = ( 570 | f"MFTDrive='{MFTDrive}'," 571 | f"PathRegex='{PathRegex}'," 572 | f"FileRegex='{FileRegex}'," 573 | f"DateAfter='{DateAfter}'," 574 | f"DateBefore='{DateBefore}'" 575 | ) 576 | 577 | return realtime_collection(client_id, artifact, parameters, Fields, result_scope) 578 | 579 | @mcp.tool() 580 | async def get_collection_results( 581 | client_id: str, 582 | flow_id: str, 583 | artifact: str, 584 | fields: str = "*", 585 | max_retries: int = 10, 586 | retry_delay: int = 30 587 | ) -> str: 588 | """ 589 | Retrieve Velociraptor collection results for a given client, flow ID, and artifact. 590 | Waits and retries if the flow hasn't finished or if no results are immediately available. 591 | 592 | Args: 593 | client_id: The Velociraptor client ID. 594 | flow_id: The flow ID returned from the initial collection. 595 | artifact: The name of the artifact collected (e.g., Windows.NTFS.MFT). 596 | fields: Comma-separated string of fields to return (default is "*"). 597 | max_retries: Number of times to retry if the flow hasn't finished or no results yet. 598 | retry_delay: Time (in seconds) to wait between retries. 599 | 600 | Returns: 601 | Collection results as a string or an error message. 602 | """ 603 | for attempt in range(max_retries): 604 | status = get_flow_status(client_id, flow_id, artifact) 605 | if status != "FINISHED": 606 | await asyncio.sleep(retry_delay) 607 | continue 608 | 609 | result = get_flow_results(client_id, flow_id, artifact, fields) 610 | return result 611 | 612 | return "No results found after multiple retries or the flow did not finish." 613 | 614 | 615 | @mcp.tool() 616 | async def collect_artifact( 617 | client_id: str, 618 | artifact: str, 619 | parameters: str = "" 620 | ) -> str: 621 | """ 622 | Start a Velociraptor artifact collection and wait for results (up to 10 minutes). 623 | 624 | Args: 625 | client_id: Velociraptor client ID to target. 626 | artifact: Name of the Velociraptor artifact to collect. 627 | parameters: A comma-separated string of key='value' pairs to pass to the artifact. 628 | fields: Comma-separated fields to return. 629 | check_interval: Number of seconds to wait before checking for results again. 630 | timeout: Maximum number of seconds to wait for the collection to complete. 631 | 632 | Returns: 633 | The collection results or the initial collection output if it times out. 634 | """ 635 | 636 | # Start the collection 637 | response = start_collection(client_id, artifact, parameters) 638 | 639 | # Ensure the response contains the flow ID 640 | if not isinstance(response, list) or not response or "flow_id" not in response[0]: 641 | return f"Failed to start collection: {response}" 642 | 643 | return response[0] 644 | 645 | 646 | @mcp.tool() 647 | async def collect_forensic_triage( 648 | client_id: str, 649 | Fields: str = "*" 650 | ) -> str: 651 | """ 652 | Collect forensic triage files using the Windows.KapeFiles.Targets artifact. 653 | 654 | Args: 655 | client_id: Velociraptor client ID. 656 | Fields: Comma-separated list of fields to return (default is '*'). 657 | 658 | Returns: 659 | Collection results or flow metadata. 660 | """ 661 | artifact = "Windows.KapeFiles.Targets" 662 | parameters = "_BasicCollection='Y'" 663 | 664 | return start_collection(client_id, artifact, parameters, Fields) 665 | 666 | @mcp.tool() 667 | async def list_windows_artifacts() -> list[dict]: 668 | """ 669 | Finds Availible Windows artifacts. 670 | 671 | Generally paramaters that target filename regexs are more performant in NTFS queries: MFT, USN and can also be used to target top level folders. 672 | A Path glob is performant, and path regex is useful to specifically filter locations. 673 | """ 674 | vql = """ 675 | LET params(data) = SELECT name FROM data 676 | SELECT name, description, params(data=parameters) AS parameters 677 | FROM artifact_definitions() 678 | WHERE type =~ 'client' AND name =~ '^windows\\.' 679 | """ 680 | 681 | def shorten(desc: str) -> str: 682 | return desc.strip().split(".")[0][:120].rstrip() + "..." if desc else "" 683 | 684 | try: 685 | results = run_vql_query(vql) 686 | summaries = [] 687 | for r in results: 688 | summaries.append({ 689 | "name": r["name"], 690 | "short_description": shorten(r.get("description", "")), 691 | "parameters": [p["name"] for p in r.get("parameters", [])] 692 | }) 693 | return summaries 694 | except Exception as e: 695 | return [{"error": f"Failed to summarize artifact definitions: {str(e)}"}] 696 | 697 | async def list_linux_artifacts() -> list[dict]: 698 | """ 699 | Finds Availible Linux artifacts. 700 | 701 | """ 702 | vql = """ 703 | LET params(data) = SELECT name FROM data 704 | SELECT name, description, params(data=parameters) AS parameters 705 | FROM artifact_definitions() 706 | WHERE type =~ 'client' AND name =~ 'linux\\.' 707 | """ 708 | 709 | def shorten(desc: str) -> str: 710 | return desc.strip().split(".")[0][:120].rstrip() + "..." if desc else "" 711 | 712 | try: 713 | results = run_vql_query(vql) 714 | summaries = [] 715 | for r in results: 716 | summaries.append({ 717 | "name": r["name"], 718 | "short_description": shorten(r.get("description", "")), 719 | "parameters": [p["name"] for p in r.get("parameters", [])] 720 | }) 721 | return summaries 722 | except Exception as e: 723 | return [{"error": f"Failed to summarize artifact definitions: {str(e)}"}] 724 | 725 | 726 | if __name__ == "__main__": 727 | mcp.run() 728 | 729 | --------------------------------------------------------------------------------