├── 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 | 
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 |
60 |
61 |
62 |
63 | `can you tell me which artifacts target the USN journal`
64 |
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 |
--------------------------------------------------------------------------------