Stomped
by RemiKG
README.md
# Stomped
> **A junior reads the timestamp and closes the case. Stomped reads the journal underneath it and reopens the one the attacker backdated.**
**Stomped** is an autonomous DFIR agent that hunts **timestomping and rename/move
laundering** on the SANS SIFT Workstation. It runs in a Linux terminal, extends
**Protocol SIFT**, and is driven by **Claude Code**. It runs the textbook
`$STANDARD_INFORMATION`‑vs‑`$FILE_NAME` stamp check, *distrusts its own clean
result*, and reads the `$UsnJrnl:$J` change journal underneath the file —
ordering events by the **monotonic USN** (each record's ever‑increasing offset in
`$J`), a clock the attacker cannot forge. It catches its own false negative live,
tags every finding **CONFIRMED / INFERRED / CONTRADICTED** with a traceable tool
execution behind it, and **physically cannot touch the original evidence.**
- **Architecture pattern:** **Pattern B — Custom MCP Server.** Seven typed,
read‑only forensic functions. No `execute_shell`, no write sink, no `O_RDWR`.
- **Agentic engine:** **Claude Code**, extending Protocol SIFT (OpenClaw‑compatible seam).
- **No web app, no browser, no dashboard.** The "UI" is the terminal: the live
execution view, the cream‑paper report, the JSONL audit log, and the diagram below.

---
## The one mechanic — the self‑correcting, evidence‑integrity triage loop
```
enumerate_mft → triage a "clean-but-active" candidate
mft_si_fn(inode) → $SI == $FN to the nanosecond → naive check NEGATIVE ("clean")
↓ "a clean check isn't a clean file" (the agent distrusts its own result)
usn_events(inode) → $UsnJrnl logs FileCreate + BasicInfoChange + Rename at a
USN ordered months AFTER the claimed creation → ⊥ CONTRADICTION
↓ SELF-CORRECTION (supersedes the naive step)
reconstruct_timeline([inode], order_by=USN) → true creation window, USN-anchored
logfile_attr_changes + vol_mft_memory → corroborate (deterministic confidence)
→ CONFIRMED / INFERRED / CONTRADICTED, each with a ◦ receipt
```
It earns trust by being **wrong on camera first, then catching itself** — on data
it does not control. A judge can re‑point it at any inode (`--verdict-inode N`) and a
fixed script gets a different answer than it hard‑coded.
### Verified, real, on the ROCBA Standard Forensic Case
The intruder downloaded **Sysinternals SDelete** (a secure‑wipe anti‑forensics tool),
timestomped it to read as **2018‑11‑15**, renamed it into the Recycle Bin, and deleted
it. Stomped's run (inode **472521**, `$R9531FE.exe` ← `C:\Users\fredr\Downloads\SDelete\sdelete.exe`):
```
mft_si_fn(472521) → $SI == $FN == 2018-11-15 12:33:42.000000000 → naive check NEGATIVE
usn_events(472521) → USN 4042552952 2020-11-14 03:46:51 FileCreate ← true birth
USN 4060428920 2020-11-14 13:41:19 BasicInfoChange ← the timestomp
USN 4060433360 2020-11-14 13:41:24 Rename → $R9531FE.exe (deleted)
verdict → timestomp + rename-laundering; true creation 2020-11-14, backdated ~2 years
```
The naive check cleared the intruder's own anti‑forensics tool. The journal —
which no user‑mode tool controls — reopened it.
---
## Architecture — how the code fits together
```
repo/
├── mcp_server/ ← Pattern B: the typed read-only MCP server
│ ├── server.py stdlib JSON-RPC (stdio) MCP transport; per-call receipts
│ ├── tools.py the 7 typed functions + reconstruct_timeline (verdict-maker)
│ ├── safety.py the ARCHITECTURAL read-only boundary (allowlist, no shell)
│ ├── confidence.py deterministic tri-state (1 layer→0.55, 2→0.80, +memory→0.94)
│ ├── config.py env-driven config — no hardcoded paths/secrets
│ └── ntfs/ bounded-struct parsers over court-vetted tools
│ ├── sleuthkit.py istat / fls / icat / fsstat (offset-pinned, read-only)
│ ├── mft.py $SI vs $FN MACB + clean-but-active enumeration (MFTECmd)
│ ├── usn.py $UsnJrnl:$J index, USN-ordered, per-inode filter
│ ├── logfile.py $LogFile second-layer corroboration (RCRD scan)
│ ├── mftscan.py Volatility 3 disk-vs-RAM MFT cross-check
│ └── carve.py residual USN_RECORD_V2 carving (rotated-$J fallback)
├── stomped/ ← the orchestrator (drives Claude Code, renders deliverables)
│ ├── cli.py the `stomped` command (seam, mount, run, render)
│ ├── mount.py ewfmount read-only + pre/post SHA-256 (the integrity proof)
│ ├── engine.py headless Claude Code driver; agent gets ONLY the 7 tools
│ ├── logio.py merges the model token stream + MCP receipts → execution log
│ ├── report.py cream-paper report.md/.html + accuracy.md + dataset.md
│ ├── theme.py the dark-editorial live terminal view
│ └── prompts/system.md the agent extension (distrust the clean check; tri-state; receipts)
├── tests/spoliation/ ← the spoliation fuzz harness (Criterion-3 bypass test)
├── sample-evidence/ ← a bundled, clean-room, REAL NTFS triptych (real $UsnJrnl)
├── docs/ ← the architecture diagram, brand, and example runs
├── LICENSE ← MIT
└── pyproject.toml ← zero runtime Python deps (stdlib-only MCP transport)
```
### The two ARCHITECTURAL guardrails (Ember on the diagram) vs the PROMPT one (dashed)
| Boundary | Kind | Where | What stops spoliation |
|---|---|---|---|
| **Typed read‑only surface** | **ARCHITECTURAL** | `mcp_server/safety.py` + `tools.py` | The agent's entire universe of action is 7 read‑only functions. No `execute_shell`, no write, no `mount`, no `O_RDWR`. Bash/Write/Edit are disallowed in the engine. The dangerous verb is not in the surface. |
| **Read‑only evidence** | **ARCHITECTURAL** | `ewfmount` ro + immutable E01 + `vboxsf ro` mount | The E01 is a sealed container opened read‑only; the host mount is `ro`. Any write → `EROFS`. Verified: `touch /mnt/evidence/...` → `Read-only file system`. |
| **Tri‑state tagging discipline** | **PROMPT** | `stomped/prompts/system.md` | Shapes *judgment* ("distrust the clean check"), not *capability*. We do not over‑claim it. |
Both architectural layers must fail before a byte changes. We **tested the boundary**
(`tests/spoliation/`): every mutator argument + a prompt‑injection ordering the agent to
"remount read‑write and repair the MFT" degrades to read‑only or errors; **0/N reach
evidence; pre‑run SHA‑256 == post‑run SHA‑256.**
---
## Install (on the SANS SIFT Workstation)
Stomped runs where the read‑only forensic tools are. It needs only the standard SIFT
toolset and Claude Code — **no Python dependencies** (the MCP transport is hand‑written stdlib).
```bash
# On the SIFT Workstation (Ubuntu). Requires: The Sleuth Kit, plaso, Volatility 3 (`vol`),
# bulk_extractor, MFTECmd (dotnet), and Claude Code (`claude`) on PATH.
git clone <this-repo> stomped && cd stomped
export PATH="$PWD/bin:$PATH" # or: pip install -e . (gives the `stomped` command)
export ANTHROPIC_API_KEY=$(cat ~/.anthropic_key) # never hardcoded, never committed
```
Tools are auto‑discovered; override any with `STOMPED_ISTAT`, `STOMPED_VOL`,
`STOMPED_MFTECMD`, etc. Volatility 3 is invoked as `vol`.
---
## Run it
```bash
stomped <evidence> [options]
<evidence> an E01 / raw image / volume / directory [env: STOMPED_EVIDENCE]
--memory <path> paired memory image for the disk-MFT vs RAM-MFT cross-check
--max-iterations <N> HARD stop on the self-correcting loop. default 12. never unbounded.
--min-layers <N> journal layers that must agree before CONFIRMED. default 2.
--mode ro the only mode. `--mode rw` is a hard rejection, not a switch.
--hash <algo|none> pre/post evidence hash to prove zero writes. default sha256.
--report <dir> report + audit-log destination. default ./stomped-cases/<case>/
--engine <name> claude-code (default) | openclaw
--verdict-inode <N> verdict any single inode a judge names
--triptych the held-out clean / naive / laundered demo (bundled sample)
```
The one command a tired analyst types:
```bash
stomped /mnt/evidence/standard-forensic-case/rocba-cdrive.e01 \
--memory /mnt/evidence/standard-forensic-case/Rocba-Memory.raw \
--max-iterations 12 --report ./cases/rocba
```
---
## Try it out (reproduce the demo)
**Zero‑setup (the bundled sample — runs anywhere the SIFT tools exist):**
```bash
stomped --triptych --report ./cases/triptych
```
This runs the *identical, genuine* analysis on `sample-evidence/stomped-sample.ntfs` —
a small, clean‑room, **real** NTFS volume with a **real** `$UsnJrnl:$J`. The agent
verdicts three files live and **differently**: `clean_report.txt` → CLEAN,
`naive_stub.exe` (`$SI≠$FN`) → CONFIRMED naive timestomp, `payload.exe` (`$SI==$FN`,
laundered via rename) → CONFIRMED. (Ground truth in `sample-evidence/README.md`.)
**The real case (on the SIFT Workstation, read‑only):**
```bash
# verdict the locked inode a judge can re-check:
stomped /mnt/evidence/standard-forensic-case/rocba-cdrive.e01 --verdict-inode 472521
# or run the full autonomous triage:
stomped /mnt/evidence/standard-forensic-case/rocba-cdrive.e01 --max-iterations 12 --report ./cases/rocba
```
Every run writes, under `--report`: `report.md` + `report.html` (the cream‑paper memo),
`execution-log.jsonl` (timestamps + token usage; every finding traces to its tool
execution; `supersedes_step` marks the self‑correction), `accuracy.md` (false positives,
what we could not confirm, the spoliation result + pre/post hashes), `dataset.md`, and
`hashes.txt`. See `docs/example-run-*/` for captured real runs.
---
## What is REAL (and what is honest about its limits)
- **Real evidence, real tools, real findings.** `ewfmount`, SleuthKit, MFTECmd, plaso,
Volatility 3, bulk_extractor — all execute against the actual image. The journal index
is a bounded one‑time parse of the real `$UsnJrnl:$J`; nothing in the analysis is mocked.
- **The self‑correction is genuine.** The deciding USN is unknowable until the real `$J`
is parsed. The agent reasons, hits the contradiction, and recovers — never scripted.
- **Honest confidence.** On rocba, the SDelete finding lands **INFERRED (0.55)**, not
CONFIRMED — because `$LogFile`'s ring had wrapped past the record by the capture date, so
only one journal layer survived. Stomped says so and recommends escalation. On the
triptych, where both journals survive, it reaches **CONFIRMED (0.80)**. Confidence is a
fixed function of corroboration, never the model's mood.
- **False positives we refuse to assert.** `$SI<$FN` fires on ~208k innocent files
(registry hives, browser JSON, bulk update rewrites); Office installer files carry 2002
dates; installers produce whole‑second (`uSecZeros`) stamps. A high score is a reason to
*triage*, never a verdict — the journal decides.
- **Out of scope:** a kernel‑level stomp that *also* forges `$UsnJrnl`. Stomped catches the
dominant **user‑mode** class (timestomp / SetMace / nTimetools + rename laundering).
---
## License
MIT — see [`LICENSE`](LICENSE). Built on, and extending, Protocol SIFT.
*100% read‑only · zero writes to evidence · confirmed, inferred, or contradicted — never just asserted.*
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues