#!/usr/bin/env python3 """Velociraptor incident response collection agent. Interfaces with the Velociraptor DFIR platform API to schedule artifact collections on endpoints, retrieve results, and generate IR reports. Supports common forensic artifacts including process listings, network connections, autoruns, event logs, and file system evidence. """ import argparse import json import os import sys import time from datetime import datetime, timezone try: import requests except ImportError: print("[!] 'requests' library required: pip install requests", file=sys.stderr) sys.exit(1) try: import grpc HAS_GRPC = True except ImportError: HAS_GRPC = False def get_velo_config(): """Return Velociraptor API configuration.""" api_url = os.environ.get("VELOCIRAPTOR_API_URL", "https://localhost:8001") api_key = os.environ.get("VELOCIRAPTOR_API_KEY", "") cert_path = os.environ.get("VELOCIRAPTOR_CERT", "") return api_url.rstrip("/"), api_key, cert_path def velo_api_call(api_url, api_key, endpoint, method="GET", data=None, cert_path=None): """Make an authenticated API call to Velociraptor.""" url = f"{api_url}{endpoint}" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } verify = cert_path if cert_path else False if method == "POST": resp = requests.post(url, headers=headers, json=data, verify=verify, timeout=60) else: resp = requests.get(url, headers=headers, verify=verify, timeout=60) resp.raise_for_status() return resp.json() def search_clients(api_url, api_key, query, cert_path=None): """Search for Velociraptor clients by hostname, label, or client ID.""" print(f"[*] Searching for clients: {query}") data = {"query": query, "count": 100} result = velo_api_call(api_url, api_key, "/api/v1/SearchClients", "POST", data, cert_path) clients = result.get("items", []) print(f"[+] Found {len(clients)} client(s)") for c in clients: os_info = c.get("os_info", {}) print(f" {c.get('client_id', 'N/A'):20s} | " f"{os_info.get('hostname', 'unknown'):20s} | " f"{os_info.get('system', 'unknown'):10s} | " f"Last seen: {c.get('last_seen_at', 'N/A')}") return clients def collect_artifact(api_url, api_key, client_id, artifacts, parameters=None, cert_path=None): """Schedule an artifact collection on a specific client.""" print(f"[*] Scheduling collection on {client_id}: {', '.join(artifacts)}") specs = [] for artifact in artifacts: spec = {"artifact": artifact} if parameters and artifact in parameters: spec["parameters"] = {"env": [ {"key": k, "value": v} for k, v in parameters[artifact].items() ]} specs.append(spec) data = { "client_id": client_id, "artifacts": artifacts, "specs": specs, } result = velo_api_call(api_url, api_key, "/api/v1/CollectArtifact", "POST", data, cert_path) flow_id = result.get("flow_id", "") print(f"[+] Collection started, flow ID: {flow_id}") return flow_id def get_flow_status(api_url, api_key, client_id, flow_id, cert_path=None): """Check the status of a collection flow.""" data = {"client_id": client_id, "flow_id": flow_id} result = velo_api_call(api_url, api_key, "/api/v1/GetFlowDetails", "POST", data, cert_path) context = result.get("context", {}) state = context.get("state", "UNSET") return state, context def wait_for_collection(api_url, api_key, client_id, flow_id, max_wait=300, cert_path=None): """Poll until a collection flow completes.""" print(f"[*] Waiting for collection to complete (max {max_wait}s)...") elapsed = 0 interval = 10 while elapsed < max_wait: state, context = get_flow_status(api_url, api_key, client_id, flow_id, cert_path) if state == "FINISHED": total_rows = context.get("total_collected_rows", 0) total_bytes = context.get("total_uploaded_bytes", 0) print(f"[+] Collection complete: {total_rows} rows, " f"{total_bytes / 1024:.1f} KB uploaded") return True, context if state == "ERROR": print(f"[!] Collection failed: {context.get('status', 'unknown')}", file=sys.stderr) return False, context print(f" State: {state} ({elapsed}s elapsed)") time.sleep(interval) elapsed += interval print("[!] Timed out waiting for collection", file=sys.stderr) return False, {} def get_flow_results(api_url, api_key, client_id, flow_id, artifact, cert_path=None): """Retrieve collected artifact results.""" print(f"[*] Retrieving results for {artifact}") data = { "client_id": client_id, "flow_id": flow_id, "artifact": artifact, "count": 10000, } result = velo_api_call(api_url, api_key, "/api/v1/GetTable", "POST", data, cert_path) rows = result.get("rows", []) columns = result.get("columns", []) print(f"[+] Retrieved {len(rows)} row(s), {len(columns)} column(s)") return rows, columns IR_ARTIFACT_SETS = { "triage": [ "Windows.System.Pslist", "Windows.Network.Netstat", "Windows.Sys.Users", "Windows.System.TaskScheduler", "Generic.System.Pstree", ], "persistence": [ "Windows.Sysinternals.Autoruns", "Windows.System.TaskScheduler", "Windows.Registry.Run", "Windows.System.Services", ], "network": [ "Windows.Network.Netstat", "Windows.Network.ArpCache", "Windows.Network.DNSCache", "Windows.Network.InterfaceAddresses", ], "logs": [ "Windows.EventLogs.Cleared", "Windows.EventLogs.RDPAuth", "Windows.EventLogs.PowershellScriptblock", ], "linux_triage": [ "Linux.Sys.Pslist", "Linux.Network.Netstat", "Linux.Sys.Users", "Linux.Sys.Crontab", "Linux.Sys.LastUserLogin", ], } def format_summary(client_id, artifacts, flow_context, results_summary): """Print collection summary.""" print(f"\n{'='*60}") print(f" Velociraptor IR Collection Report") print(f"{'='*60}") print(f" Client ID : {client_id}") print(f" Flow ID : {flow_context.get('session_id', 'N/A')}") print(f" State : {flow_context.get('state', 'N/A')}") print(f" Artifacts : {len(artifacts)}") print(f" Total Rows : {flow_context.get('total_collected_rows', 0)}") for artifact_name, row_count in results_summary.items(): print(f" {artifact_name:50s}: {row_count} rows") def main(): parser = argparse.ArgumentParser( description="Velociraptor IR collection agent" ) sub = parser.add_subparsers(dest="command") p_search = sub.add_parser("search", help="Search for clients") p_search.add_argument("--query", required=True, help="Search query (hostname, label, client ID)") p_collect = sub.add_parser("collect", help="Collect artifacts from client") p_collect.add_argument("--client-id", required=True, help="Velociraptor client ID") p_collect.add_argument("--artifacts", nargs="+", help="Specific artifact names to collect") p_collect.add_argument("--preset", choices=list(IR_ARTIFACT_SETS.keys()), help="Use a predefined IR artifact set") p_collect.add_argument("--wait", type=int, default=300, help="Max wait time in seconds") p_status = sub.add_parser("status", help="Check collection status") p_status.add_argument("--client-id", required=True) p_status.add_argument("--flow-id", required=True) parser.add_argument("--api-url", help="Velociraptor API URL (or VELOCIRAPTOR_API_URL env)") parser.add_argument("--api-key", help="API key (or VELOCIRAPTOR_API_KEY env)") parser.add_argument("--cert", help="CA cert path (or VELOCIRAPTOR_CERT env)") parser.add_argument("--output", "-o", help="Output JSON report path") parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() if not args.command: parser.print_help() sys.exit(1) if args.api_url: os.environ["VELOCIRAPTOR_API_URL"] = args.api_url if args.api_key: os.environ["VELOCIRAPTOR_API_KEY"] = args.api_key if args.cert: os.environ["VELOCIRAPTOR_CERT"] = args.cert api_url, api_key, cert_path = get_velo_config() if not api_key: print("[!] Set VELOCIRAPTOR_API_KEY env var or use --api-key", file=sys.stderr) sys.exit(1) result = {} if args.command == "search": clients = search_clients(api_url, api_key, args.query, cert_path) result = {"action": "search", "query": args.query, "clients": clients} elif args.command == "collect": artifacts = args.artifacts or IR_ARTIFACT_SETS.get(args.preset, []) if not artifacts: print("[!] Specify --artifacts or --preset", file=sys.stderr) sys.exit(1) flow_id = collect_artifact(api_url, api_key, args.client_id, artifacts, cert_path=cert_path) success, context = wait_for_collection(api_url, api_key, args.client_id, flow_id, args.wait, cert_path) results_summary = {} if success: for artifact in artifacts: try: rows, cols = get_flow_results(api_url, api_key, args.client_id, flow_id, artifact, cert_path) results_summary[artifact] = len(rows) except Exception as e: results_summary[artifact] = f"Error: {e}" format_summary(args.client_id, artifacts, context, results_summary) result = {"action": "collect", "client_id": args.client_id, "flow_id": flow_id, "state": context.get("state", "UNKNOWN"), "artifacts": results_summary} elif args.command == "status": state, context = get_flow_status(api_url, api_key, args.client_id, args.flow_id, cert_path) print(f"[*] Flow {args.flow_id}: {state}") result = {"action": "status", "flow_id": args.flow_id, "state": state, "context": context} report = { "timestamp": datetime.now(timezone.utc).isoformat(), "tool": "Velociraptor", "result": result, } if args.output: with open(args.output, "w") as f: json.dump(report, f, indent=2) print(f"\n[+] Report saved to {args.output}") elif args.verbose: print(json.dumps(report, indent=2)) if __name__ == "__main__": main()