Skip to content

Repository files navigation

TRUDI

Threat Response Unit for Digital Investigation

Autonomous DFIR agent built on the SANS SIFT Workstation. TRUDI runs a complete incident response investigation — disk triage, memory forensics, Windows artifact parsing, IOC enrichment, YARA hunting — and produces a structured analyst report with a full audit trail, without prompting for confirmation at each step.

A separate model directs the investigation phase-by-phase, and an adversarial reviewer challenges every conclusion before it reaches the report. TRUDI only reports what survives review.

Built for the Find Evil! hackathon — SANS Institute / Devpost, April–June 2026.


Contents

This README: How it works · Prerequisites · Setup · API keys · Running a local model · Starting a case · Live monitoring (experimental) · What gets produced · Trace dashboard · Documentation · Tool namespaces · YARA rules · Evidence constraints · Test suite · Repository layout · License

Documentation:

Doc What's in it
Try It Out Step-by-step: browse a finished run (no key) or drive a fresh investigation end-to-end
Architecture Components, MCP boundary, guardrail tiers, security boundaries (Mermaid source · diagram PNG)
Project Description Design rationale — reasoning loop, gates, curiosity budget
Dataset Documentation Every case's provenance, evidence source, findings, and answer key
Accuracy Report False positives, missed artifacts, hallucinations caught, confidence calibration, spoliation
Live-monitoring demo (experimental) Velociraptor + victim Docker stack and the auto-protect loop walkthrough
Live-endpoint testing (experimental) Read-only live.* SSH triage against a running host
Media Dashboard screenshots + demo video notes

How it works

TRUDI is a three-model system — one analyst and two independently-configured reasoning models that direct and challenge it:

Claude (primary analyst) — orchestrates the investigation, selects tools, runs them via the TRUDI MCP server, interprets output, and writes the report.

DAIR phase director (dair.*) — runs the investigation as a recursive state machine (Triage → Collect → Analyze → Scan → Report). After every tool batch, dair_assess re-reads the evidence picture, decides the next phase, and emits a bound priority_tools work order. DAIR prescribes; Claude executes. Its backend is configured independently via DAIR_BACKEND.

Adversarial reviewer (reason.*) — challenges the investigation from two directions:

  1. Upstream — at each Triage entry it generates a prioritized plan and competing hypotheses (including at least one non-leading actor/mechanism), binding which discriminating tools run first.
  2. Downstream — before any conclusion reaches the report it evaluates the finding, scores confidence, checks citations, and runs a pre-report gate — flagging unsupported claims, logical gaps, and alternative explanations.

Both reasoning surfaces are swappable and first-class in both directions: REASON_BACKEND and DAIR_BACKEND each accept the Claude API or any OpenAI-compatible / local endpoint — a thinking model such as Qwen3 or DeepSeek-R1, or a security-tuned local model such as Titus — over vLLM / SGLang / llama.cpp, and the two surfaces may run on different models. The models exchange structured DIRECTIVES blocks that bind tool selection for the next phase. Disagreements are resolved by a capped self-correction loop (max 3 iterations); unresolved items are reported as UNCERTAIN rather than dropped.

Backends: run the reviewer and DAIR director on Claude (Opus) or on a local model — the harness is designed to hold analytical quality across backends, and both are documented as equals. See Reasoning backends.

Execution flow

case opened
    │
    ├─ misc.start_execution_log           ← trace log initialized
    ├─ [parallel] pre-plan triage         ← ez.recmd_hive ×3 + vol.symbol_check + hash.verify_evidence_hash
    ├─ reason.hypothesize (case question)  ← competing hypotheses, incl. ≥1 non-leading
    ├─ reason.plan                         ← prioritized Triage plan
    │
    └─ DAIR loop ── repeats until next_phase = Report ───────────────┐
         ├─ dair.dair_assess              ← phase decision + priority_tools work order
         ├─ [tool batch — disk, memory, artifacts, network, …]       │  (+ optional curiosity probes)
         │      └─ reason.hypothesize     ← per suspicious artifact   │
         └─ dair.dair_assess (results) ──────────────────────────────┘
    │
    (before any CONFIRMED/LIKELY finding)
    ├─ reason.evaluate_finding / confidence_score / cite_check
    ├─ misc.record_finding                 ← gated, linked to source call_id
    │
    (Report phase)
    ├─ reason.synthesize                   ← cross-finding consistency check
    ├─ reason.pre_report_check             ← blocks until findings + attribution resolved
    ├─ misc.export_execution_log           ← trace written to reports/
    └─ misc.write_final_report             ← gated final report write

Every tool call, DAIR call, reason call, and confirmed finding is written to a live JSON trace log throughout the investigation. The markdown export is human-readable.


Prerequisites

  1. SANS SIFT Workstation — Ubuntu 22.04 x86-64 with forensic tools (Volatility 3, EZ Tools, Sleuth Kit, Plaso, YARA, bulk_extractor, etc.)

  2. Claude Code CLI — the agent runtime

    • Install: curl -fsSL https://claude.ai/install.sh | bash (or npm install -g @anthropic-ai/claude-code)
    • OpenCode is also supported, side-by-side — see OpenCode support
  3. Python 3.10+ and dotnet — both included in SIFT Workstation

  4. Reasoning backendsrequired TRUDI uses two independently-configured reasoning models — the adversarial reviewer (REASON_BACKEND) and the DAIR phase director (DAIR_BACKEND). These are core to how TRUDI works, not add-ons: without them the agent runs unsupervised, findings are never challenged or confidence-scored, and phase direction falls back to a static path. Configure both before running. Each takes the same options and may point at the same or different models:

    Backend Config
    claude (default) ANTHROPIC_API_KEY=sk-ant-... — no server required
    openai-compat REASON_URL / DAIR_URL + REASON_API_KEY / DAIR_API_KEY
    Local model …_BACKEND=openai-compat + …_URL=http://localhost:8000 + …_MODEL=<id>
    Hosted (HF endpoint) …_BACKEND=openai-compat + …_URL=<hf-endpoint> + …_API_KEY=hf_...

    The simplest setup is Claude for both (add an ANTHROPIC_API_KEY and you're done); running the reviewer + DAIR on a local model is equally supported (see Reasoning backends). Either way a backend is required — TRUDI will start without one, but that is not a supported way to evaluate it: reason and DAIR calls are skipped and you're seeing a hollowed-out agent, not TRUDI.

System forensic packages

install.sh installs these automatically (and enables the universe apt component if needed). On a full SIFT Workstation most are already present; on a leaner base, or if the installer logs a ! warning about one, install them by hand:

sudo add-apt-repository -y universe        # pst-utils / pff-tools / tcpxtract live here
sudo apt-get update
sudo apt-get install -y pff-tools pst-utils binwalk tcpxtract sleuthkit ewf-tools
apt package Binaries TRUDI tools that need it
pff-tools pffexport, pffinfo misc.pff_export (PST/OST email extraction)
pst-utils readpst, lspst misc.readpst_extract (PST→mbox) — not libpst-utils; that package does not exist
sleuthkit fls, icat, istat, mmls, blkls, mactime, tsk_recover tsk.*
ewf-tools ewfmount, ewfinfo, ewfverify ewf.*, img.* E01 mounting
tcpxtract tcpxtract net.tcpxtract_streams
binwalk binwalk firmware / embedded carving
chainsaw (GitHub release v2.10.2/usr/local/bin, not apt) chainsaw misc.chainsaw_hunt (Sigma over EVTX) — optional; TRUDI runs without it

Verify after install: for b in pffexport readpst fls ewfmount tcpxtract; do command -v "$b" || echo "MISSING: $b"; done


Setup

git clone https://github.com/nebulae/trudi ~/trudi
cd ~/trudi
./install.sh

install.sh does the following:

  • Checks for python3, dotnet, and claude CLI
  • Creates a Python venv at ~/.venv and installs all dependencies
  • Copies .env.example.env (edit this to add API keys)
  • Backs up any existing ~/.claude/CLAUDE.md with a UTC timestamp, then installs the TRUDI orchestrator
  • Registers Claude Code hooks and slash commands (run directly from the repo — no drift-prone deployed copies)
  • Configures OpenCode side-by-side when detected — MCP registration, forensic-binary deny rules, /trudi-* commands, hook-adapter plugin, AGENTS.md
  • Registers the TRUDI MCP server globally with claude mcp add --scope global
  • Runs the full test suite (1,100+ tests) as a smoke check

If ~/.claude/CLAUDE.md already exists, the backup is written to ~/.claude/CLAUDE.md.<YYYYMMDDTHHMMSS>.bak — the original is never overwritten without a backup.

API keys

Edit ~/trudi/.env. TRUDI runs without any keys, but missing keys degrade the run rather than break it — and the degradation is not cosmetic:

Key Powers If absent
ANTHROPIC_API_KEY The reason.* reviewer and dair.* director (when …_BACKEND=claude) Reason and DAIR calls are skipped. Findings are never challenged, confidence-scored, or citation-checked; phase direction falls back to a static path. The audit trail and analytical rigor that distinguish a TRUDI run are lost.
VIRUSTOTAL_API_KEY enrich.vt_lookup_* Hash/IP/domain reputation lookups return "unconfigured"; everything else proceeds.
ABUSEIPDB_API_KEY enrich.abuseipdb_check IP abuse scoring skipped; everything else proceeds.

A reasoning backend is required — either ANTHROPIC_API_KEY (Claude) or a local / OpenAI-compatible endpoint (below). The adversarial review and phase direction depend on it; a run with no backend is a hollowed-out agent, not TRUDI. The two enrichment keys are recommended (they add IOC corroboration) but optional and never block the investigation.

# IOC enrichment
VIRUSTOTAL_API_KEY=your_key_here
ABUSEIPDB_API_KEY=your_key_here

# Reasoning backends — reviewer + DAIR director. Pick ONE of the two blocks below
# for each (they may differ). Both backends are first-class.

# (A) Claude backend — Opus for both:
REASON_BACKEND=claude
DAIR_BACKEND=claude
ANTHROPIC_API_KEY=sk-ant-...          # used by both when backend=claude
REASON_MODEL=claude-opus-4-8
DAIR_MODEL=claude-opus-4-8

# (B) Local / OpenAI-compatible backend (see "Running a local model" below):
# REASON_BACKEND=openai-compat
# REASON_URL=http://localhost:8000     # or https://api.openai.com/v1
# REASON_API_KEY=sk-...                # if the endpoint needs one
# DAIR_BACKEND=openai-compat
# DAIR_URL=http://localhost:8000       # may differ from REASON_URL

Running a local model

The reviewer and DAIR director run on any local model behind an OpenAI-compatible server (vLLM / SGLang / llama.cpp). This is a supported, first-class configuration — TRUDI has been driven end-to-end on local backends including Titus-CybersecurityLLM (a security-tuned GGUF) and general thinking models such as Qwen3, producing complete, gated reports. Set REASON_BACKEND=openai-compat / DAIR_BACKEND=openai-compat and the …_URL / …_MODEL for each (they may differ). Two families:

A security-tuned model over llama.cpp — e.g. Titus, a quantized GGUF:

llama-server -m Titus-CybersecurityLLM-v1.0-Q4_K_M-No-MTP.gguf \
  -c 32768 -np 1 --host 0.0.0.0 --port 8000
# then set: REASON_BACKEND=openai-compat, REASON_URL=http://localhost:8000

Thinking models (Qwen3, DeepSeek-R1, gpt-oss … over vLLM / SGLang / llama.cpp):

Thinking models spend a chain-of-thought before the answer, and every server bills those tokens against max_tokens. TRUDI keeps thinking enabled and budgets for it instead:

REASON_BACKEND=openai-compat
REASON_URL=http://localhost:8000
REASON_MODEL=unsloth/Qwen3.8-27B-GGUF:Q4_K_M  # the id your server lists at GET /v1/models
DAIR_BACKEND=openai-compat          # (auto-discovered from /v1/models if left unset)
DAIR_URL=http://localhost:8000
DAIR_MODEL=unsloth/Qwen3.8-27B-GGUF:Q4_K_M

TRUDI_COMPAT_THINKING_BUDGET=8192   # extra output tokens reserved for the think phase
TRUDI_COMPAT_MAX_TOKENS_CEILING=32768  # ≤ per-slot context (n_ctx / n_parallel) − ~5.5k prompt
TRUDI_DAIR_MAX_TOKENS=8192          # DAIR's answer budget (default 4096). A very "chatty"
                                    # thinking model can burn the whole budget on chain-of-
                                    # thought and truncate the DAIR JSON — raise this (the
                                    # request budget is TRUDI_DAIR_MAX_TOKENS + THINKING_BUDGET)
TRUDI_REASON_TIMEOUT=1200           # a 27B at ~40 tok/s needs ~4 min for 10k tokens
TRUDI_DAIR_TIMEOUT=1200
# TRUDI_COMPAT_THINKING_GUIDANCE=   # brevity line appended to compat system prompts; "off" disables
# TRUDI_COMPAT_NO_THINK_TOOLS=dair_assess,reason_cite_check,reason_confidence_score,reason_audit_findings  # default
# TRUDI_COMPAT_EXTRA_BODY='{"chat_template_kwargs":{"enable_thinking":true}}'  # optional pass-through

Thinking is a per-surface switch: by default DAIR and the mechanical checks (cite_check, confidence_score, audit_findings) run with enable_thinking=false + /no_think, while hypothesize / evaluate_finding / synthesize / plan keep it. Measured on a 27B at 40 tok/s with thinking everywhere: hypothesize 8.6k tokens / 197 s, DAIR 14.6k tokens / 341 s. That matters because Claude Code backgrounds any tool call running past CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS (default 120 s) and lets the agent continue without the result — which silently breaks the DAIR-driven loop. Either keep every reason/DAIR call under that cutoff, or raise it in the case's .claude/settings.json ("env": {"CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS": "0"} disables backgrounding; pair with a per-server "timeout" in .mcp.json).

Size the ceiling to the server: llama-server splits -c across -np slots, and concurrent reason + DAIR calls share the KV cache — a request that cannot fit returns HTTP 500 Context size has been exceeded for every in-flight request. -c 32768 -np 1 with CEILING=26624 / THINKING_BUDGET=12288 is a working baseline for a 27–35B thinking model.

How it behaves: the request budget is max_tokens + THINKING_BUDGET. If the server returns finish_reason=length with an empty answer (the whole budget went to thinking), TRUDI retries once at double the budget (capped at the ceiling). Chain-of-thought in reasoning_content or <think>…</think> is captured as diagnostics (backend_meta.reasoning_* on the trace entry), never promoted to the answer. Failed calls record their cause on the reason_call / dair_call entry plus a call_abandoned entry. Set TRUDI_COMPAT_THINKING_BUDGET=0 for non-thinking chat models (e.g. GPT-class) to use the legacy single-attempt path.


Starting a case

1. Create the case directory

cp -r ~/trudi/case-template ~/cases/<CASE_ID>

2. Edit the case CLAUDE.md

Fill in evidence paths, hostnames, and scope in ~/cases/<CASE_ID>/CLAUDE.md. This is the only manual step — everything else is autonomous.

~/cases/<CASE_ID>/
├── CLAUDE.md                  ← edit this
├── .claude/
│   └── settings.json          ← MCP tool allowlist (pre-populated)
├── evidence/                  ← place disk images and memory captures here
├── analysis/                  ← intermediate artifacts (auto-created)
├── exports/                   ← tool output: CSV, JSON, bodyfiles (auto-created)
└── reports/                   ← final report + trace log (auto-created)

3. Place evidence

cp /path/to/image.E01 ~/cases/<CASE_ID>/evidence/
cp /path/to/memory.img ~/cases/<CASE_ID>/evidence/

4. Open Claude Code in the case directory

cd ~/cases/<CASE_ID>
claude

5. Start the investigation

Investigate this case. Start with the pre-enumeration triage, then follow the plan.

TRUDI will run autonomously from there. It will not ask for confirmation between steps. Final output is a structured report in reports/ and a full execution trace in both JSON and markdown.


Live monitoring & autonomous response (experimental)

Status: experimental. This layer runs today but is scaffolding; the core of TRUDI is the read-only static investigator described above. It's documented here for completeness.

Beyond static-image investigation, TRUDI can watch a live endpoint via Velociraptor and respond autonomously. This is the only mode where TRUDI writes to a system, and only outside the evidence boundary, through a separately gated path.

  • monitor.* — baseline capture, watcher lifecycle, alert draining, per-investigation traces
  • velo.* — read-only Velociraptor API (clients, artifact collection, VQL)
  • live.* — read-only live triage over SSH (processes, network, persistence, services, logons)
  • respond.*gated containment (process suspend/kill, network block) over a structured, type-validated SSH path

Auto-protect (default on) auto-executes the reversible + low-risk tier of containment and surfaces each action's rollback command; anything destructive (irreversible, or risk ≥ medium) pauses the loop until the operator types approve ACT-N. The auto-vs-approval boundary is server-classified from each recipe's risk/reversible metadata — the agent cannot reclassify it.

Try it end-to-end: demo/live-monitoring/README.md — a self-contained Velociraptor + victim Docker stack with one-command bring-up, staged Atomic Red Team attacks, and a full walkthrough of the TRUDI loop (baseline → watcher → alert drain → auto-protect, including the approve ACT-N flow). It's driven by the bundled /trudi-* slash commands (/trudi-start-watcher, /trudi-watch-alerts, /trudi-check-alerts, /trudi-stop-watcher, /trudi-clear-case), which install.sh installs to ~/.claude/commands/. For the read-only SSH triage path on its own, see docs/live-endpoint-testing.md.


What gets produced

File Contents
reports/<CASE_ID>_investigation_report.md Structured analyst report — executive summary, attack timeline, findings with confidence levels, environment caveats
reports/<CASE_ID>_trace.md Human-readable audit trail — every tool call, reason call, and confirmed finding with UTC timestamps
reports/<CASE_ID>_trace.json Machine-readable trace — same data, structured for ingestion
analysis/<CASE_ID>_trace.json Live trace (written incrementally during the investigation)
exports/ Raw tool output — MFT CSV, EVTX exports, prefetch, registry, amcache, shimcache, USN journal, netscan, etc.

Trace dashboard

Every investigation writes a live JSON trace; the bundled dashboard renders it in the browser as it runs. Launch it once and it serves every case under ~/cases:

./dashboard.sh                 # serves ~/cases on http://127.0.0.1:8765
./dashboard.sh --port 9090
./dashboard.sh --cases-root /data/cases

install.sh also installs a trudi-dashboard launcher to /usr/local/bin. At case open TRUDI prints the live dashboard URL for the active trace, so you can watch the investigation unfold in real time.

View Shows
Trace Viewer Chronological stream of every tool call, DAIR call, reason call, and finding — UTC timestamps, arguments, and gate results
Investigation Chain Finding-lineage chain — each finding linked back through input_call_ids to the exact tool executions that produced it
Investigation Graph The trace as a causal DAG — hypotheses, tool runs, and findings as nodes; lineage as edges

Trace Viewer — every tool / DAIR / reason call and finding, with arguments and gate results:

TRUDI Trace Viewer

Investigation Chain — each finding linked back through input_call_ids to the exact tool executions that produced it:

TRUDI Investigation Chain

📹 Demo video: TRUDI walkthrough · 📦 Demo run bundle (trace, report, console): cases/vanko/


Documentation

Doc What
Architecture Components, MCP boundary, guardrail tiers, security boundaries (Mermaid source)
Project description Design rationale — the reasoning loop, gates, curiosity budget
Datasets Each case's source, evidence pointer, and expected findings; runs bundled under cases/ and installed to ~/cases/ for the dashboard
Accuracy report False positives, missed artifacts, hallucinations caught in testing, confidence calibration, and an evidence-integrity / spoliation section
Try it out Browse a finished run in ~2 min (no key), or drive a fresh investigation end-to-end
Execution logs A committed run's trace + report + console; every bundled case under cases/ ships its full trace (reports/<CASE_ID>_trace.{json,md} via misc.export_execution_log)
Demo video End-to-end run walkthrough

Experimental extras: live-monitoring walkthrough (Velociraptor + auto-protect) and live-endpoint triage (read-only live.* over SSH).

Every finding in an execution log links to the exact tool call that produced it — viewable live in the Trace dashboard. Start with How it works, then Try it out.


Tool namespaces

All forensic execution goes through the TRUDI MCP server — 25 namespaces in total. Claude never calls binaries directly when an MCP tool exists.

Namespace Domain Key tools
img.* Disk image mounting ewf_mount, vshadow_mount, bde_mount, xmount, photorec_carve, losetup_create
vol.* Memory forensics (Volatility 3) pstree, pslist, psscan, cmdline, netscan, dlllist, malfind, hollowprocesses, pebmasquerade, suspicious_threads, scheduled_tasks, registry_hivelist, dumpfiles
tsk.* Filesystem (Sleuth Kit) fls, icat, istat, ils, mactime, tsk_recover, sigfind, mmls, fsstat, jls, jcat
ewf.* E01 images ewf_mount, ewf_info, ewf_verify, mount_full_image, mount_ntfs
ez.* Windows artifacts (EZ Tools) mftecmd, evtxecmd, recmd_hive, amcacheparser, appcompatcacheparser, pecmd, lecmd, jlecmd, sbecmd, wxtcmd, sqlecmd, rbcmd
plaso.* Super-timeline plaso_create_timeline, plaso_export_csv, plaso_filter_incident_window, plaso_info
yara.* Threat hunting yara_scan_file, yara_scan_directory, yara_scan_memory_image, yara_scan_strings
hash.* Integrity / similarity hash_file, hash_directory, verify_evidence_hash, ssdeep_hash, hashdeep_compute
strings.* Static analysis strings_extract, strings_grep, hexdump, file_identify, exiftool_metadata, stat_file
carve.* File carving carve_bulk_extractor_scan, carve_foremost_carve, carve_scalpel_carve
net.* Network analysis tcpdump_read, tcpdump_extract_http, tcpdump_extract_dns, tcpdump_extract_ips, ngrep_search
enrich.* Threat intel vt_lookup_hash, vt_lookup_ip, vt_lookup_domain, abuseipdb_check
misc.* Windows artifacts evtx_dump, evtx_filter, regripper_hive, parse_scheduled_tasks, usbdeviceforensics, usnparser_parse, analyzeMFT_parse, hindsight_chrome, clamscan_file, pe_scanner, pdf_parser_analyze
read.* Produced-output reads (traced, citable) read_output (CSV/JSON/TXT under analysis/exports/reports — query, columns, where), read_mail (extracted mbox/.eml — message bodies, sender/recipient roster)
reason.* Adversarial review reason_plan, reason_hypothesize, reason_evaluate_finding, reason_confidence_score, reason_cite_check, reason_synthesize, reason_pre_report_check
dair.* Phase director (state machine) dair_assess
correlate.* Cross-tool correlation process_to_file, network_to_process, mitre_map, mitre_validate
accuracy.* Ground-truth scoring accuracy_compare, accuracy_export_report
coverage.* Evidence coverage reporting coverage_report
af.* Anti-forensics detection timestomp_drift, event_log_clear, sysmon_evasion, usn_gaps, prefetch_deletion
attribution.* Threat-actor attribution attribute_actors
live.* Live endpoint triage (read-only, SSH) live_processes, live_network_connections, live_persistence_audit, live_services, live_recent_logins, live_yara_scan
velo.* Velociraptor API (read-only re: evidence) list_clients, collect_artifact, wait_for_flow, get_collection_results, query
monitor.* Live-monitoring lifecycle baseline_capture, start_watcher, check_alerts, start_investigation, end_investigation
respond.* Gated containment & response suggest_containment, approve_action, execute_action, revert_action

live.*, velo.*, monitor.*, and respond.* belong to the experimental live-monitoring layer (see above) and are not part of the core static investigator.


Bundled YARA rules

Located in rules/ — used automatically by yara.* tool calls:

Ruleset Covers
cobalt_strike/ Default named pipes, reflective loader, stager patterns, beacon config
persistence/ Scheduled task XML anomalies, Run key patterns, service install signatures
lateral_movement/ Pass-the-hash, net use, SMB lateral movement indicators
powershell/ Obfuscated PowerShell, AMSI bypass, download cradles
anti_forensics/ Log clearing, timestomping, MFT manipulation indicators

Evidence constraints

TRUDI enforces read-only evidence handling at the executor level — not just by instruction. The core/paths.py module blocks any output write that resolves to /cases/, /mnt/, /media/, or any path containing an evidence/ segment. This check runs before every subprocess call that takes an output path. It cannot be bypassed via prompt.

All tool output is capped at 50 KB / 150 lines before being returned to the agent to prevent context flooding. Truncation is flagged explicitly in the response.


Running the test suite

cd ~/trudi
source ~/.venv/bin/activate
pytest -n auto --no-cov -q        # ~30 s on a many-core box (parallel, no coverage)
pytest --cov --tb=short           # serial with coverage (CI shape; a few minutes)

1,700+ tests. All tool calls are mocked — tests run without SIFT tools installed. The suite is I/O-bound (every test writes a trace): tests run with the trace fsync off and a per-test hook.lock (tests/conftest.py), so -n auto scales linearly — and never contends with a live TRUDI session.


OpenCode support (side-by-side)

TRUDI also runs under OpenCode — configured automatically by install.sh when OpenCode is detected (or on re-run after installing it). Claude Code and OpenCode are supported side-by-side: the same repo, MCP server, gates, hooks, commands, and orchestrator serve both.

What opencode/register_opencode.py sets up under ~/.config/opencode/:

Piece How
MCP server opencode.jsonmcp["trudi-sift"] pointing at the venv python + server.py
Forensic-binary deny rules permission.bash deny map derived from the same ban list Claude Code uses (case-template/.claude/settings.json) — one source of truth, parsed not copied
/trudi-* commands symlinked from claude/commands/ (repo path — no drift)
Execution-time guard + trace hooks plugin/trudi.js, a logic-free adapter that translates OpenCode plugin events (tool.execute.before/after, chat.message, session.idle) into the stdin-JSON contract of the Python hooks in claude/hooks/ and spawns them — the tested Python hooks stay the single source of truth; a guard deny becomes a thrown error, which blocks the tool call
Orchestrator claude/CLAUDE.md installed as AGENTS.md (existing file backed up first)

Why: a fully local stack. OpenCode can drive the investigation on a local model while REASON_BACKEND / DAIR_BACKEND already run on local models — no cloud dependency anywhere in the loop. The server-side control plane (typed claims, tiering, gates, DAIR) is client-agnostic and identical under both agents.

Local provider config — declare the context limit. OpenCode cannot see a custom OpenAI-compatible server's context size; without it, it never compacts proactively and every long turn slams into the server's wall ("request exceeds the available context size") before recovering reactively. In the provider's model entry set "limit": {"context": <~85% of your llama-server -c>, "output": 8192} — set it BELOW the server's actual -c, not equal to it. OpenCode compacts at its own threshold; if limit.context equals the server ceiling, a single turn's request can overshoot the compaction point and still hit the server's hard wall ("request (N tokens) exceeds the available context size"). A ~10–15% gap (e.g. 78000 for -c 90112) gives the compaction room to land under the server limit.

Known limits: assistant-narration copying into the trace reads the Claude Code transcript and is skipped under OpenCode (tool calls, user messages, and the Stop audit still land in the trace); the OpenCode plugin API moves quickly — the adapter is contract-tested against the Python hooks (tests/security/test_opencode_contract.py), but re-verify event payload shapes when upgrading OpenCode.

Local driving models must emit real tool calls. A fine-tune's GGUF can ship a chat template without the tools branch — the server then silently drops the tool schemas and the model narrates tool calls as prose instead of executing them. Diagnostic: send a tools-bearing request and check usage.prompt_tokens; a count that ignores your tool definitions means the template never rendered them. Fix: serve with the base model's chat template (llama-server --jinja --chat-template-file <base>.jinja) — e.g. Titus (a Qwen3.6-35B-A3B LoRA) drives correctly under the base Qwen3.6 template.


Repository layout

trudi/
├── server.py              ← FastMCP server — mounts all 24 tool namespaces
├── install.sh             ← one-command setup on a SIFT Workstation
├── claude/
│   └── CLAUDE.md          ← global orchestrator (installed to ~/.claude/CLAUDE.md)
├── opencode/              ← OpenCode side-by-side support (registrar + hook-adapter plugin)
├── case-template/         ← starter case directory for new investigations
│   ├── CLAUDE.md
│   ├── .claude/settings.json
│   └── evidence/ analysis/ exports/ reports/
├── cases/                 ← bundled case studies (traces + reports, no evidence)
│   └── <CASE>/            ← installed to ~/cases/ for the dashboard (see docs/datasets.md)
├── docs/                  ← docs: architecture, accuracy-report, datasets,
│                            project-description + media/ (screenshots, demo video notes)
├── core/
│   ├── executor.py        ← safe subprocess runner (retry, timeout, line cap)
│   ├── execution_log.py   ← trace log singleton
│   └── paths.py           ← evidence path enforcement + tool binary locations
├── tools/                 ← one module per MCP namespace (24 total)
├── rules/                 ← bundled YARA rulesets (5 categories)
└── tests/                 ← full test suite (1,100+ tests, mocked)

License

Released under the MIT License. © 2026 Trinity Harrison.

About

Autonomous DFIR agent — SANS SIFT Workstation MCP server for incident response

Resources

Stars

57 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages