{"id":51015082,"url":"https://github.com/securityronin/ntfs-forensic","last_synced_at":"2026-06-21T09:02:44.787Z","repository":{"id":362781283,"uuid":"1260240570","full_name":"SecurityRonin/ntfs-forensic","owner":"SecurityRonin","description":"From-scratch NTFS reader (ntfs-core: MFT, attributes, indexes, data runs, LZNT1, $UsnJrnl:$J change journal over Read+Seek) plus a graded anomaly auditor (ntfs-forensic: timestomping, alternate data streams, deleted records, MFT/LogFile tamper checks) — panic-free, fuzzed, no unsafe","archived":false,"fork":false,"pushed_at":"2026-06-14T01:56:11.000Z","size":711,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T03:19:33.589Z","etag":null,"topics":["alternate-data-streams","anti-forensics","change-journal","dfir","digital-forensics","disk-forensics","filesystem","forensics","incident-response","mft","ntfs","rust","rust-forensics","security","timestomping","usn-journal"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SecurityRonin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T09:37:17.000Z","updated_at":"2026-06-14T01:56:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/SecurityRonin/ntfs-forensic","commit_stats":null,"previous_names":["securityronin/ntfs-forensic"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/SecurityRonin/ntfs-forensic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fntfs-forensic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fntfs-forensic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fntfs-forensic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fntfs-forensic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SecurityRonin","download_url":"https://codeload.github.com/SecurityRonin/ntfs-forensic/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SecurityRonin%2Fntfs-forensic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34603647,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alternate-data-streams","anti-forensics","change-journal","dfir","digital-forensics","disk-forensics","filesystem","forensics","incident-response","mft","ntfs","rust","rust-forensics","security","timestomping","usn-journal"],"created_at":"2026-06-21T09:02:39.688Z","updated_at":"2026-06-21T09:02:44.759Z","avatar_url":"https://github.com/SecurityRonin.png","language":"Rust","funding_links":["https://github.com/sponsors/h4x0r"],"categories":[],"sub_categories":[],"readme":"# ntfs-forensic\n\n[![ntfs-core](https://img.shields.io/crates/v/ntfs-core.svg?label=ntfs-core)](https://crates.io/crates/ntfs-core)\n[![ntfs-forensic](https://img.shields.io/crates/v/ntfs-forensic.svg?label=ntfs-forensic)](https://crates.io/crates/ntfs-forensic)\n[![Docs.rs](https://img.shields.io/docsrs/ntfs-forensic)](https://docs.rs/ntfs-forensic)\n[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)\n[![CI](https://github.com/SecurityRonin/ntfs-forensic/actions/workflows/ci.yml/badge.svg)](https://github.com/SecurityRonin/ntfs-forensic/actions)\n[![Sponsor](https://img.shields.io/badge/sponsor-h4x0r-ea4aaa?logo=github-sponsors)](https://github.com/sponsors/h4x0r)\n\n**A from-scratch NTFS reader and a graded anomaly auditor — reconstruct full file paths from the `$UsnJrnl:$J` change journal (even for deleted, MFT-reused files), and surface the timestomping, alternate data streams, deleted records, and MFT slack that a \"clean\" filesystem driver is built to hide.**\n\nTwo crates, one workspace:\n\n- **[`ntfs-core`](https://crates.io/crates/ntfs-core)** — the reader: `$MFT`, attributes, indexes, data runs, LZNT1, `$UsnJrnl:$J` change-journal record decode, and `NtfsFs` path navigation over any `Read + Seek` source. No `unsafe`, no C bindings.\n- **[`ntfs-forensic`](https://crates.io/crates/ntfs-forensic)** — the auditor: turns parsed MFT records into severity-graded [`forensicnomicon::report::Finding`](https://crates.io/crates/forensicnomicon)s, so an NTFS volume's anomalies aggregate uniformly with the partition and container layers.\n\n## Audit a raw MFT record in 30 seconds\n\n```toml\n[dependencies]\nntfs-forensic = \"0.5\"   # pulls in ntfs-core\n```\n\n```rust\nuse ntfs_forensic::audit_record;\nuse forensicnomicon::report::Source;\n\nlet src = Source { analyzer: \"ntfs-forensic\".into(), scope: \"NTFS\".into(), version: None };\n\n// Feed it a single raw 1024-byte MFT record; get back graded anomalies.\nfor anomaly in audit_record(\u0026mft_record_bytes) {\n    let finding = anomaly.to_finding(src.clone());\n    println!(\"[{:?}] {} — {}\", finding.severity, finding.code, finding.note);\n    // e.g. [Some(High)] NTFS-TIMESTOMP — $SI created before $FN …\n}\n```\n\n`audit_record` parses the header and attributes, extracts `$STANDARD_INFORMATION`/`$FILE_NAME`, and grades what it finds. A record whose header does not parse yields no anomalies (structural corruption is surfaced by the reader/carver, never a panic).\n\n## The anomaly codes\n\nEach anomaly is an **observation** (\"consistent with …\"); the examiner draws the conclusions. Codes are a stable, published contract.\n\n| Code | Severity | What it observes |\n|---|---|---|\n| `NTFS-TIMESTOMP` | High | `$STANDARD_INFORMATION` times show forgery tells vs. the harder-to-forge `$FILE_NAME` times (`$SI` predates `$FN`, or lands on a whole second) |\n| `NTFS-ADS` | Low | A named `$DATA` attribute — an alternate data stream (also used benignly, e.g. `Zone.Identifier`) |\n| `NTFS-SLACK-RESIDUE` | Low | Non-zero residue in an MFT record's slack, past its used size |\n| `NTFS-DELETED-RECORD` | Info | An MFT record not in use — a recoverable deleted file |\n| `NTFS-MFTMIRR-MISMATCH` | High | A system record in `$MFT` differs from its `$MFTMirr` copy |\n| `NTFS-LOGFILE-CLEARED` | Medium | `$LogFile` shows restart-area gaps consistent with the journal having been cleared |\n\nPer-record anomalies come from `audit_record` / `audit_components`; the volume-level pair (`NTFS-MFTMIRR-MISMATCH`, `NTFS-LOGFILE-CLEARED`) come from `audit_mft_mirror($MFT, $MFTMirr)` and `audit_logfile($LogFile)`.\n\n## The reader: navigate a volume\n\n`NtfsFs` (in `ntfs-core`, imported as `ntfs_core`) reads files and directories from any `Read + Seek` source:\n\n```rust\nuse ntfs_core::NtfsFs;\nuse std::fs::File;\n\nlet mut fs = NtfsFs::open(File::open(\"ntfs.img\")?)?;\n\n// Read a file by path…\nlet hosts = fs.read_file(r\"\\Windows\\System32\\drivers\\etc\\hosts\")?;\n\n// …or list the root directory (MFT record 5).\nlet root = fs.read_record(5)?;\nfor entry in fs.directory_entries(\u0026root)? {\n    if let Some(name) = entry.file_name {\n        println!(\"{}\", name.name);\n    }\n}\n# Ok::\u003c(), ntfs_core::NtfsError\u003e(())\n```\n\nThe bare crate name `ntfs` on crates.io is Colin Finck's general-purpose reader, so this crate publishes as `ntfs-core` and imports as `ntfs_core`.\n\n### Opening a partition inside a whole disk\n\n`OffsetReader` re-bases a partition to offset 0 and **structurally cannot read past the partition boundary** — feed it the offset and length from [`mbr-forensic`](https://github.com/SecurityRonin/mbr-forensic) / [`gpt-partition-forensic`](https://github.com/SecurityRonin/gpt-partition-forensic):\n\n```rust\nuse ntfs_core::{NtfsFs, OffsetReader};\nuse std::fs::File;\n\nlet part = OffsetReader::new(File::open(\"disk.img\")?, 1_048_576, 500_000_000)?;\nlet mut fs = NtfsFs::open(part)?;\n# Ok::\u003c(), ntfs_core::NtfsError\u003e(())\n```\n\n## What makes this different from a general-purpose NTFS crate\n\nMost NTFS crates answer one question: \"what files are on this volume?\" This workspace answers the questions a digital forensics examiner actually needs:\n\n| Capability | General-purpose NTFS crate | this workspace |\n|---|---|---|\n| MFT record + attribute parsing | ✅ | ✅ |\n| Directory index traversal (`$INDEX_ROOT` / INDX) | ✅ | ✅ |\n| Data runs, sparse files, LZNT1 decompression | ✅ | ✅ |\n| `$ATTRIBUTE_LIST` (heavily fragmented files) | partial | ✅ |\n| `$SI`-vs-`$FN` timestomping detection | ✗ | ✅ |\n| Alternate data stream enumeration | ✗ | ✅ |\n| Deleted-record carving (unallocated `FILE`/`BAAD`) | ✗ | ✅ |\n| MFT record slack extraction | ✗ | ✅ |\n| `$MFTMirr` / `$LogFile` tamper checks | ✗ | ✅ |\n| Update-sequence (fixup) torn-write detection | ✗ | ✅ |\n| `$UsnJrnl:$J` change-journal record decode (create / delete / rename / overwrite history) | ✗ | ✅ |\n| **`$UsnJrnl:$J` full-path reconstruction** (the *Rewind* algorithm — full paths even for deleted + MFT-reused files) | ✗ | ✅ |\n| USN streaming reader + free-space USN record carving | ✗ | ✅ |\n| ReFS USN V3 (128-bit file references) | ✗ | ✅ |\n| Partition-window isolation (cannot read past the volume) | ✗ | ✅ |\n| Severity-graded `report::Finding` output | ✗ | ✅ |\n| `#![forbid(unsafe_code)]` | — | ✅ |\n\n## `$UsnJrnl:$J`: reconstruct full paths — even for deleted files\n\nThe USN change journal records *what* changed and *which* MFT entry — but only the file's **own name**, never its path. `ntfs-core` reconstructs the **full path** of every journal event, including files that were deleted and whose `$MFT` record was later reused, by walking the journal with the *Rewind* algorithm:\n\n```rust\nuse ntfs_core::mft::MftData;\n\n// Seed from the live $MFT, then rewind the $UsnJrnl:$J event stream.\nlet mut engine = MftData::parse(\u0026mft_bytes)?.seed_rewind();\nfor resolved in engine.rewind(\u0026ntfs_core::usn::parse_usn_journal(\u0026usn_bytes)?) {\n    println!(\"{:\u003c10?} {:\u003c12?} {}\", resolved.source, resolved.record.reason, resolved.full_path);\n    // Allocated  FILE_DELETE  \\Users\\victim\\AppData\\Local\\Temp\\evil.exe\n}\n# Ok::\u003c(), ntfs_core::NtfsError\u003e(())\n```\n\n`RewindEngine` runs **two passes — reverse, then forward** — so a rename or an MFT-entry reuse part-way through the journal resolves to the *correct* path at each point in time. Events whose parent is no longer present in the live `$MFT` still resolve from the journal's own create/rename history, tagged `RecordSource::Carved` or `Ghost`. For journals too large to hold in memory, `UsnJournalReader` streams them; `carve_usn_records` recovers events from journal slack and unallocated space; and `RefsAnalyzer` handles ReFS's 128-bit USN V3 references.\n\n\u003e **Credit:** the journal-`$J` path-reconstruction technique was pioneered by [**CyberCX**](https://cybercx.com/) — see their writeup [*NTFS Usnjrnl Rewind*](https://cybercx.com/blog/ntfs-usnjrnl-rewind/) (April 2024) and the reference tool [`CyberCX-DFIR/usnjrnl_rewind`](https://github.com/CyberCX-DFIR/usnjrnl_rewind). This is an independent, clean-room Rust implementation built on `ntfs-core`'s own parsers; its SQLite export is column-compatible with `usnjrnl_rewind`.\n\n## Reader API (`ntfs-core`)\n\n| Item | Purpose |\n|---|---|\n| `NtfsFs::open` / `read_file` / `read_record` / `directory_entries` / `resolve_path` / `read_named_stream` | Navigate a volume by path or MFT record number |\n| `BootSector` | Volume boot record (BPB / extended BPB) |\n| `MftRecordHeader` / `apply_fixup` | FILE records and update-sequence-array fixup |\n| `parse_attributes` / `Attribute` | Resident and non-resident attribute walking |\n| `StandardInformation` / `FileName` | The two timestamp sets |\n| `decode_runlist` / `read_attribute_value` / `read_runs` | Data runs (VCN→LCN), sparse + non-resident reads |\n| `IndexRoot` / `parse_index_buffer` / `parse_entries` | Directory B-tree (`$INDEX_ROOT` / INDX) |\n| `parse_attribute_list` | Extension records for fragmented files |\n| `decompress` | LZNT1 decompression |\n| `carve_mft_entries` | Carve `FILE`/`BAAD` records from a raw `$MFT` region |\n| `compare_mft_mirror` / `parse_logfile` / `detect_journal_clearing` | `$MFTMirr` / `$LogFile` parsing primitives |\n| `parse_usn_record_v2` / `parse_usn_journal` / `UsnRecord` / `UsnReason` / `FileAttributes` | Decode `$UsnJrnl:$J` change-journal records (V2/V3) — each event's MFT + parent-MFT reference, reason flags, filename, attributes, and timestamp |\n| `UsnJournalReader` | Streaming, low-memory iterator over a `$J` stream too large to load whole |\n| `carve_usn_records` | Recover USN records from journal slack and unallocated space |\n| `MftData` / `MftEntry` | High-level `$MFT` aggregator (`$SI`/`$FN` timestamps, ADS, path resolution); seeds the rewind engine |\n| `RewindEngine` / `ResolvedRecord` | **Full-path reconstruction** from the USN journal (the *Rewind* algorithm — two-pass, rename- and MFT-reuse-aware) |\n| `RefsAnalyzer` / `RefsFileId` | ReFS USN V3 (128-bit file references), journal-rewind-only path reconstruction |\n| `OffsetReader` | Bounded partition window |\n\nThe auditor primitives — `detect_timestomp`, `alternate_data_streams`, `record_slack`, `is_deleted`, `carve_file_records` — live in `ntfs-forensic` alongside `audit_record`.\n\n## Trust, but verify\n\n`ntfs-forensic` is built for untrusted disk images from potentially compromised systems:\n\n- **`#![forbid(unsafe_code)]`** across both crates — no C bindings, no FFI.\n- **Panic-free on malicious input** — every length and offset is validated against both the structure's declared size and the actual buffer; the workspace denies `clippy::unwrap_used` and `clippy::expect_used` in production code.\n- **Fuzzed** — seven `cargo-fuzz` targets (`boot`, `record`, `attributes`, `attribute_list`, `runlist`, `index_buffer`, `compress`); a `fuzz.yml` CI workflow builds and smoke-runs each.\n- **Validated on real artifacts against independent oracles** — never against fixtures we hand-encoded and graded ourselves. The boot parser is cross-checked against **The Sleuth Kit** (`fsstat`) on the DEF CON 2018 `MaxPowers` image; LZNT1 decode against **TSK** (`icat`) on a real compressed stream from the DFIR Madness \"Stolen Szechuan Sauce\" CITADEL-DC01 image; `$MFT` parsing against the independent **`mft` crate**; and the `$LogFile` RCRD reader against **TSK** input extraction plus a differential page census (4470/4470 pages on DC01), with **LogFileParser** as the `$LogFile` decode oracle. Full evidence, tiers, corpora, and reproduction steps: **[docs/validation.md](docs/validation.md)**.\n- **100% line coverage** enforced in CI (`cargo llvm-cov --lib`, failing on any zero-hit line) — a regression backstop, not the correctness claim; the oracles above are.\n\n```bash\ncargo test\ncargo +nightly fuzz run record   # requires nightly + cargo-fuzz\n```\n\n## Where this fits\n\n`ntfs-core` is the NTFS FS-layer foundation for the SecurityRonin forensic family. The full `$UsnJrnl:$J` reader stack — decode, streaming, carving, and *Rewind* full-path reconstruction — lives **in `ntfs-core`**; [`usnjrnl-forensic`](https://github.com/SecurityRonin/usnjrnl-forensic) is now a thin CLI shell over it (output formats, live monitoring), and [`issen`](https://github.com/SecurityRonin/issen) consumes the workspace as its single, auditable NTFS engine. To get a `Read + Seek` over a disk image and locate the NTFS partition within it, these crates compose upstream:\n\n| Crate | Role |\n|---|---|\n| [`disk-forensic`](https://github.com/SecurityRonin/disk-forensic) | **Orchestrator** — auto-detects MBR / GPT / APM and yields each partition's offset / length |\n| [`mbr-forensic`](https://github.com/SecurityRonin/mbr-forensic) | MBR partition table → NTFS partition offset / length |\n| [`gpt-partition-forensic`](https://github.com/SecurityRonin/gpt-partition-forensic) | GPT partition table → NTFS partition offset / length |\n| [`ewf-forensic`](https://github.com/SecurityRonin/ewf-forensic) | E01 / Expert Witness Format container |\n| [`vhdx-forensic`](https://github.com/SecurityRonin/vhdx-forensic) | VHDX container |\n\n---\n\n[Privacy Policy](https://securityronin.github.io/ntfs-forensic/privacy/) · [Terms of Service](https://securityronin.github.io/ntfs-forensic/terms/) · © 2026 Security Ronin Ltd\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsecurityronin%2Fntfs-forensic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsecurityronin%2Fntfs-forensic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsecurityronin%2Fntfs-forensic/lists"}