About the project

Ojuri — "the eye saw."

A forensically defensible AI agent for digital forensics and incident response.

Ojuri (Yoruba: "the eye saw") lets a Claude AI agent investigate a Windows disk image (and optionally a memory capture) by calling typed, read-only forensic primitives — not a shell. Every action is hash-chained into a tamper-evident audit log. A second AI agent verifies every claim against that log. When memory is provided, a third agent cross-references runtime state against on-disk persistence and flags discrepancies — and that agent's claims are themselves verified.

The result: AI-driven forensics with findings you can defend in writing.


Inspiration

Friday night. The only analyst still in the office. An incident on the screen, a clock that wouldn't stop, and a forensic question I needed to answer in twenty minutes — knowing whatever I wrote down had to hold up.

Ojuri is the partner I wished for in that chair.

Every DFIR analyst recognizes the same pattern. Investigations drag. The first day is urgent; by day five the alert has been triaged six times and is still waiting on one answer that nobody has bandwidth to chase. Attention is scarce, the right tooling isn't always within reach, and the question that matters most often arrives at the worst time. Cases lose momentum. People move on to the next fire. The work gets done — but I'd sit at my desk asking the same question over and over: how can this be done better?

That question is what drove me to build Ojuri.Every responder I've known cared about the work. The bottleneck is structural: forensics is detail-heavy, citation-heavy, and time-poor, and a junior analyst working alone needs a partner that scales their attention without compromising on rigor. Ojuri is that partner. Something an analyst can turn to for any forensic question and trust to give back a defensible answer, not a confident-sounding guess. Something that wouldn't just say "Run keys look clean" but would let me prove it — here's the exact hive, here's every entry, here's the cryptographic chain proving nobody touched the evidence, here's a second AI agreeing the citation is real.

Among the hackathons on Devpost, Find Evil! gave me a sense of purpose. The right brief at the right time: build something that does serious forensic work without pretending the AI is infallible. Ojuri is for the junior responder I was, and for every analyst who has ever been the only person in the room with an incident to triage and not enough hours to be sure.


What it does

Ojuri runs a three-agent pipeline against Windows evidence:

  1. The Investigator reads a case question in plain English, decides which forensic primitives to call, calls them, and produces structured findings. Each finding includes citations to specific audit-log entries.
  2. The Auditor has zero forensic tools. Its only job is to verify that every citation in every finding is real — i.e., that the cited evidence actually exists in the audit log and supports the claim. Mark VERIFIED or DISPUTED. The Auditor is launched from an empty directory with --strict-mcp-config, so the no-tool-access rule is enforced by process boundary, not by prompt.
  3. The Correlator runs only when a memory image is provided AND at least one finding verifies. It cross-references verified memory state against verified disk persistence, emitting typed CorrelationDiscrepancy records (process_not_persisted / persisted_not_running / timing_mismatch / name_mismatch_resolved). The Auditor then runs a second pass to verify the Correlator's citations the same way.

The whole pipeline is bounded by a self-correction loop with a hard iteration cap, deterministic exit codes, and pre-flight checks that reject bad inputs before any Claude subprocess spawns.

Empirical demonstration

On a real Windows 10 IP-theft case (Fred Rocba, SANS FOR500), Ojuri produced 6 findings (6/6 VERIFIED) and 15 cross-source discrepancies (15/15 VERIFIED). Total runtime: 12 minutes 54 seconds.

The headline discoveries:

  • The Apple iCloud suite — 6 processes (iCloudServices, iCloudDrive, iCloudPhotos, ApplePhotoStreams, APSDaemon, iCloudIE) running under fredr's explorer.exe, all started within 4 seconds of his logon, with no matching entry in either user's HKCU Run, HKLM Run, or any of the 225 scheduled tasks. In an IP-theft case where personal cloud sync is a plausible exfiltration channel, this is exactly the right thing to flag.
  • A 6-process Slack tree rooted at PID 1152, also logon-spawned by explorer.exe, also unanchored.
  • MRC.exe — an unidentified binary launched ~83 seconds before memory capture, parent explorer.exe, recommended for analyst follow-up.

For each finding, the Auditor verified the citations. For each discrepancy, the Auditor's second pass verified its citations. The audit chain hash is sha256:71edb1d7de26f4b93cb555159c05d89335a5cb3aa3135e7ff3897ab963414e14, recorded in both verdict files. Any reviewer can re-run verify_chain.py against the audit log to independently confirm no tampering occurred.


How we built it

The architectural thesis

Most existing DFIR AI agents give the language model a shell. The only thing between "the model decides to do X" and "the system executes X" is a prompt telling the model not to do certain things. This is not security; it's hope. When the model hallucinates — and they do — the prompt-guardrail evaporates.

Ojuri inverts the design: the AI is untrusted by construction. Instead of giving it a shell and asking it to behave, we give it nothing but a typed catalogue of read-only forensic operations. What the AI cannot do, it cannot do — regardless of what it is convinced to attempt.

Five layers

┌────────────────────────────────────────────────────────────┐
│  REASONING LAYER (top)   ←  Three Claude agents (untrusted)│
├────────────────────────────────────────────────────────────┤
│  CAPABILITY LAYER        ←  MCP server: 9 typed primitives │
│                          ←  TRUST BOUNDARY                 │
├────────────────────────────────────────────────────────────┤
│  BACKEND LAYER           ←  Real DFIR tools (swappable)    │
├────────────────────────────────────────────────────────────┤
│  EVIDENCE LAYER          ←  Read-only mount, kernel-enforced│
├────────────────────────────────────────────────────────────┤
│  VERIFIER (independent)  ←  verify_chain.py, stdlib only   │
└────────────────────────────────────────────────────────────┘

The Capability Layer is the trust boundary. Above it, everything is untrusted (the AI may hallucinate). Below it, everything is deterministic code that can be tested and verified. The MCP server exposes 9 typed forensic primitives with Pydantic-validated input and output schemas. The AI cannot pass arbitrary shell strings; it cannot get back free-form text. Every call is hash-chained into the audit log before the AI sees the result.

Nine primitives across six backend strategies

# Primitive What it answers Backend strategy
1 list_evidence_artefacts What artifacts are on this volume? (mandatory first call) D: Python filesystem walk
2 get_registry_autostarts HKLM Run / RunOnce / RunOnceEx A: subprocess + parse (RegRipper)
3 get_user_autostarts HKCU per-user persistence A: subprocess + parse (RegRipper)
4 get_userassist What did the user actually run A: subprocess + parse (RegRipper)
5 get_scheduled_tasks What tasks are configured E: Python XML parse
6 get_browser_artifacts URLs, downloads from Chrome / Edge F: Python sqlite3
7 get_prefetch_entries What programs ran B: direct library (pyscca)
8 get_mft_timeline Full NTFS file timeline C: subprocess + CSV (MFTECmd)
9 get_memory_pslist Processes at memory capture A: subprocess + parse (Volatility 3)

Six distinct backend strategies. The remaining 21 primitives in our catalogue each slot into one of these six proven patterns — the architecture is shown, not just claimed.

The audit chain

Every primitive call appends one JSONL record to audit.log. Each record contains a SHA-256 of the previous record (blockchain-style chain) plus a hash of the actual tool output. The first record's previous_record_hash is the sentinel all-zero hash, marking the start of the chain.

The tool outputs themselves are stored separately under outputs/seq-NNN.json — keeping the audit log small and easy to verify, while letting the Auditor read the exact recorded payload for citation verification.

scripts/verify_chain.py is a standalone Python script (stdlib only, no Ojuri imports) that re-derives every hash from scratch and detects:

  • Chain corruption (exit code 1) — any record in audit.log was modified
  • Output tampering (exit code 4) — any outputs/seq-NNN.json was modified
  • File/argument errors (exit code 2)
  • Success (exit code 0)

The verifier reimplements canonicalization from scratch so drift between writer and reader is detectable.

The three-agent isolation pattern

The Auditor and Correlator must not be able to call forensic primitives — that would compromise the "produce vs verify" separation. We enforce this architecturally, not by prompt:

  • Each is launched in a fresh empty working directory (/tmp/ojuri_auditor_<pid>_<iter>, etc.) that contains no .mcp.json
  • Each is given --strict-mcp-config with no --mcp-config flag — zero MCP servers loaded regardless of any discoverable configuration
  • Each has read-only filesystem reach to specific directories via --add-dir; that grants no MCP capability

Subprocess isolation removes the capability entirely. A prompt-instructed or same-process "critic" can be convinced to "just check this one thing with a tool." Subprocess isolation gives no tool to misuse.

Pre-flight checks

Two orchestrator-level checks fire before any Claude subprocess is spawned:

  1. Evidence-mount check — the --evidence-root must contain Users/, Windows/, or $MFT. A stale FUSE-released mountpoint passes the directory-exists check but is empty; without this guard a full agent run is wasted discovering nothing.
  2. Memory-image two-stage check — size threshold (default ≥100 MB) and an authoritative Volatility 3 windows.info probe (with 300-second timeout). If Volatility can't parse the image, the memory primitive can't either; surfacing the error here costs 10-30 seconds instead of 12+ wasted Claude-API minutes.

Both fail fast with exit code 2 and an analyst-actionable error message.


Challenges we ran into

1. EPROCESS.ImageFileName is 15 bytes.

Windows truncates process names at 15 bytes when storing them in the kernel EPROCESS structure. Volatility 3's windows.pslist faithfully returns these truncated names. So iCloudDrive.exe becomes iCloudDrive.ex, iCloudPhotos.exe becomes iCloudPhotos.e, ApplePhotoStreams.exe becomes ApplePhotoStre. We discovered this empirically during the first Rocba memory primitive integration when our test assertions failed in puzzling ways.

The Correlator's matching logic now handles it: a memory name matches a disk-side basename if the disk name starts with the memory name. When that's the only reason a match succeeded, the Correlator emits an INFORMATIONAL name_mismatch_resolved discrepancy so the analyst trusts the positive correlation despite the quirk.

2. The Volatility symbol cache permission problem.

On first contact with a new Windows kernel build, Volatility downloads symbol tables (~500 MB - 2 GB) to ~/.volatility3/symbols/. If that directory doesn't exist or isn't writable, every Volatility call fails with a confusing error deep in the stack. We added defensive checks and explicit error messages, plus the OJURI_VOLATILITY_SYMBOLS env var so users can override the cache location.

3. Prompt drift between server and Investigator.

Initially the Investigator's system prompt named its available tools statically. Then we added a new primitive. Now the server and prompt were out of sync — the AI either didn't know about the new tool or claimed to call tools that didn't exist. We refactored: the Investigator's prompt now contains a {TOOL_LIST} template variable, and the orchestrator's get_tool_list() introspects the running server module to fill it in. Adding a primitive automatically updates the prompt. Two sources of truth collapsed into one.

4. Auditor capture by the Investigator's framing.

Our first dual-agent design used an in-process "Task tool" sub-agent for the Auditor. We discovered the Auditor could be subtly induced by the Investigator's prompt to "just check this one thing with the get_user_autostarts tool" — defeating the entire purpose. We rebuilt it: the Auditor now runs as a subprocess of claude -p, launched from an empty directory with --strict-mcp-config. Zero tools, enforced by the OS process boundary. The same pattern was used when we added the Correlator.

5. The Correlator placement debate.

When adding memory analysis, we considered three options for where to put cross-source reasoning: (a) make it a phase of the Investigator's loop, (b) embed it in the Auditor's verification step, (c) dedicate a third agent. Option (a) re-mixes "produce" and "audit" responsibilities. Option (b) overloads the Auditor with open-ended discrepancy reasoning when its job is mechanical citation checking. We chose (c) — the only option that preserves the defensibility chain. The Correlator can only reason over already-verified findings; its own claims are then verified by the Auditor's second pass.

6. The exit code on output tampering.

During documentation we tested verify_chain.py against a deliberately tampered outputs file. The script printed "OUTPUT TAMPER DETECTED" but our test showed exit code 0. Panic. Then we realized: we'd piped the verifier through head to truncate the output, and $? returns the last pipeline command's exit code (head's), not python's. The verifier was returning 4 all along. We documented this gotcha in the user guide (use ${PIPESTATUS[0]} to capture the verifier's exit code through a pipe).


Accomplishments we're proud of

  • A working, tested, three-agent DFIR pipeline with 191 unit tests + 1 integration test passing, demonstrably producing verified findings and discrepancies on real Windows 10 evidence.
  • Six distinct backend strategies proven across nine primitives — the architectural pattern works for subprocess+parse (4 different tool families), direct library calls, CSV-mediation, pure Python filesystem walk, pure Python XML parsing, and Python sqlite3. The remaining 21 primitives in our catalogue all map to one of these six patterns.
  • Architectural separation that survives prompt manipulation. The Auditor and Correlator have zero MCP tools enforced at the OS process boundary, not at the prompt level. We don't have to trust the AI not to misuse capabilities; we ensure it cannot have them.
  • A standalone independent verifier (scripts/verify_chain.py) that any reviewer can run on any audit log to detect tampering — uses only Python stdlib, imports nothing from Ojuri, so drift between writer and reader is itself detectable.
  • Comprehensive documentation: 6-file plain-English user guide (~3,000 lines), 1,086-line technical architecture document, 23-entry decision log explaining every architectural choice and why, full demo walkthrough with cryptographically-anchored citations.
  • Honest scope. Our findings explicitly list which Windows persistence mechanisms we don't check (shell:startup, Winlogon Shell/Userinit, COM/CLSID hijacks, full Services ImagePath, WMI subscriptions). "Not persisted via any enumerated mechanism" is not the same as "not persisted." We surface this distinction in every discrepancy detail so analysts know the limitations.

What we learned

Defensibility is an architectural property, not a prompt property. You cannot make an AI agent "more careful" with words. You can make it physically incapable of doing things it shouldn't.

Separation of duties matters in AI as much as in accounting. Letting the same agent produce findings and self-verify them is structurally equivalent to letting an accountant audit their own books. The Auditor and Correlator have zero forensic tools by construction. They cannot re-investigate; they can only check that the Investigator's claims are anchored in real recorded evidence.

Cryptography is cheap; trust is expensive. A hash-chained audit log adds nanoseconds per call. Re-verifying it later adds milliseconds. The cost of not having one — being unable to prove your findings weren't tampered with after the fact — is potentially the entire investigation.

LLMs lie when given the option, and tell the truth when not. When given a free-text "report what you found," Claude (and all current frontier LLMs) will sometimes embellish. When given a structured schema that requires audit_sequence: N, excerpt: "..." for every claim, the surface for embellishment shrinks dramatically. The Auditor closes the rest.

The 15-byte EPROCESS truncation lives in production. We discovered it the hard way during the first memory primitive integration. It's now documented in DECISIONS, in the technical architecture, in the user guide, and handled correctly in the Correlator's matching logic. Any other DFIR-AI system that doesn't handle this will produce subtly wrong correlations on long process names.

Prompt engineering is a maintenance burden you can architect away. The dynamic tool-list pattern (server introspection + template substitution) means we add a primitive in one place. Two-sources-of-truth was a real bug; we collapsed it.


What's next

The Architecture (§12) specifies a roadmap with these priorities:

  • More memory primitives. get_memory_pslist is built; near-term additions reuse the same Volatility-backed pattern: get_memory_netscan (network artefacts), get_memory_malfind (injected/unbacked executable regions), get_memory_pstree, get_memory_cmdline, get_memory_dlllist. The Correlator can already consume these via the existing CorrelationDiscrepancy schema.

  • The remaining 21 disk primitives. Each slots into one of the six proven backend strategies. We expect this to be straightforward, not architectural.

  • Live endpoint backend. A LiveEndpointBackend that connects to a triage agent on a running machine rather than a mounted image — same primitive contract, different backend.

  • More evidence formats. AFF4 (v0.4), VMDK/VHDX (v0.5), native Windows E01 opening via Arsenal Image Mounter (v0.3 — fixes the WOF reparse-point gap on Linux).

  • Native Windows port. WOF (Windows Overlay Filter) compressed files are decoded only by the Windows kernel's WOF driver. No Linux NTFS implementation handles them. A native Windows port would baseline these files correctly.

  • Persistent learning loop. Feeding audited, verified findings back as reusable case-knowledge — preserving the defensibility chain (every reused fact still cites real evidence in an audit log).


Built With

  • anthropic-api
  • claude-code
  • custom-mcp-server
  • fastmcp
  • libewf
  • mcp
  • mftecmd
  • multi-agent
  • ntfs
  • pydantic
  • pyscca
  • python
  • regripper
  • sift-workstation
  • volatility-3
Share this project:

Updates