Quick proof (no dataset, no API key, 5 seconds):

git clone https://github.com/Hokutoman00/foveal-dfir
python run_prototype.py --no-grader

→ A/B verdict table in 5 s. Python 3.10+ stdlib only. No 41GB, No Ollama, No API key.

Evil found (one command): python -m cases.run_judge_demo → Evil found: CONFIRMED — Fred Rocba staged IP files to iCloud. Credential theft: FALSIFIED. Lateral movement: FALSIFIED.

AMBIGUOUS is the honest answer: When evidence conflicts, we report AMBIGUOUS — not a failure, but a bounded claim. actor_cadence on ROCBA returned AMBIGUOUS because system processes run 24h; that is the correct answer.


Inspiration

The hackathon names its own open problem: an autonomous DFIR agent that "just says find evil" hallucinates and needs a human's hand. We take that at face value. The failure mode is self-deception. We answer it structurally, not with more careful prompting.

A second observation from the brief: per GTG-1002, the adversary is itself an autonomous agent. It inherits the same structural limit we do.

What it does

foveal-dfir runs on top of the Protocol SIFT / Valhuntir base and adds an independent verification + boundary-honest layer that does not trust the investigating agent's self-assessment.

Three pillars run end-to-end on the official ROCBA case (Windows 10 build 19041, 18 GB memory + 23 GB disk):

  1. Code-enforced confidence staging. CONFIRMED requires at least two independent corroborating sources actually present in the finding's evidence list. Counted in Python (verifier/staging.py), not in the LLM's mood. On the memory pass, 16 of 4,818 candidate findings claim CONFIRMED; all 16 get downgraded to INDICATED because each carries only one source. On the disk pass, three entity-merged findings carry artifacts from two sources each (filesystem AND prefetch) and the rule permits CONFIRMED. The rule works both ways: cap on single, permit on multi.

  2. Boundary register. Areas the agent did not examine, or could not resolve, are emitted as a first-class output. Missed evidence is reported as missed, never silently dropped.

  3. Responsibility ledger. Each finding records investigator, structural_staging, quarantine, blind_grader, divergence_arbiter, consensus_verdict. Dissents are named, not silenced. The verdict_holder is consensus_verdict on AGREE_*, escalated to human_arbiter on DISAGREE.

A blind grader (qwen2.5:7b via local Ollama) re-judges each finding from raw evidence only — never sees the investigator's reasoning trace, is never told the claimed confidence. On the disk pass, only cloud_sync.icloud survives at CONFIRMED: all three layers had to agree.

How we built it

  • verifier/ package: grader.py, staging.py, quarantine.py, verify.py, divergence.py, boundary_register.py, responsibility_ledger.py, actor_cadence.py, falsifier.py, prior_fit.py, stereo_fusion.py.
  • case_ingest.py converts Volatility3 plugin JSON output into the verifier's finding format.
  • disk_ingest.py is the disk-side counterpart: it reads Sleuth Kit fls listings of Google Drive, iCloud, Downloads, and Prefetch and emits findings keyed by entity, so the same entity (cloud_sync.google_drive, cloud_sync.dropbox) can accumulate artifacts from two independent plugin sources before the structural rule decides.
  • cases/run_rocba.py and cases/run_rocba_disk.py are the end-to-end drivers; they emit audit_log.json, boundary_register.json, and summary.json per case.
  • ARCHITECTURE.md ships a Mermaid pipeline diagram and a written safety boundary.

Challenges we ran into

  • A 22 GB E01 disk image and 5.3 GB memory archive arrive through Egnyte; libewf 2014's libewf_glob is strict about filenames and 9P (WSL /mnt/c) filesystem behavior, so we copied the image to native ext4 and used ewfmount + Sleuth Kit on the FUSE-mounted raw view. The disk turned out to be a single-partition image (no MBR) so mmls was silent until we treated it as raw NTFS directly.
  • Volatility3's JSON renderer is fine, but the small-but-real bug of forcing CONFIRMED-claimed findings to stay CONFIRMED required us to actually count sources, not just check correlation presence (which is the SIFT-Guard README-vs-promotion.py gap we close).
  • Honesty around what the grader actually pushed back on: on the disk pass, two structurally-eligible CONFIRMED candidates were judged INDICATED by the grader, with the grader's reasoning kept in the audit log.

What we learned

  • The README rule everyone writes (≥ 2 independent sources for CONFIRMED) does not become a real constraint until it is enforced in code, OVER the consensus of the observers themselves.
  • "Salience" is the wrong lever in DFIR. Evil is by definition not salient. Coverage must be exhaustive OR explicitly declared. The boundary register makes "what we did not look at" a first-class output.
  • Distributed contribution is fine; diffuse accountability is not. Naming each observer's contribution per claim is the difference.

What's next

  • The disk pass currently fuses two plugins (filesystem + prefetch). Adding registry and event-log plugins will let multi-source CONFIRMED examples land more often, and let stereo_fusion reconstruct a richer kill-chain.
  • Pre-registered falsifiers per hypothesis are scaffolded but not yet tied to specific ROCBA hypotheses; that is the cleanest way to test the IR-Accuracy axis on real case data with explicit ground truth.

Built With

Share this project:

Updates