Claude 24f816b6a3
Consolidate 22 sibling repos into layered organism structure
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):

- brain/        LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
                dispatch, A51 channels, and the OSINT cluster
- knowledge/    LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
                MITRE ATT&CK data
- reference/    defensive threat-reference (C3, shhbruh doc) + AdaYaml parser

License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.

Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.

https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
2026-06-10 06:53:01 +00:00

278 lines
10 KiB
Python

#!/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()